> ## Content Index
> Fetch the complete content index at: https://www.popit.kr/llms.txt
> Use this file to discover other available public pages before exploring further.

# JPA Native Query 사용 시 DTO로 매핑하기
- URL: https://www.popit.kr/jpa-native-query-dto-mapping/
- Published: 2018-01-20T02:32:12.000Z
- Updated: 2018-03-14T23:27:44.000Z
- Author: 유 영모
- Tags: #소프트웨어, Java, JPA, Hibernate, qlrm, DTO

이 글은 필자가 [미디엄Medium](https://medium.com/@SlackBeck?ref=popit.kr)에 쓴 2편의 글을 합하여 개정한 것이다.

---

[JPA Native Query](http://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html?ref=popit.kr#createNativeQuery-java.lang.String-)를 사용하다 보면 반환 값으로 엔터티Entity 가 아닌 [DTOData Transfer Object](https://en.wikipedia.org/wiki/Data%5Ftransfer%5Fobject?ref=popit.kr)로 받고 싶을 때가 있다.

대표적인 경우가 통계이다. 통계 지표를 만들어낼 때 사용하는 SQL은 대부분이 복잡하고 길다. 문제는 SQL 실행 결과Result Set를 기존의 엔터티에 담기에는 형태가 맞지 않는데 있다.\[1\]

이 글은 [JPA Native Query](http://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html?ref=popit.kr#createNativeQuery-java.lang.String-) 반환 값으로 DTO로 만드는 여러 방법을 다룬다.

# 간단한 예제로 시작하기

이해하기 쉽게 간단한 예제로 시작해 보자. 예제는 상품(Product)과 회원(Member), 그리고 주문(Order) 3개의 엔터티로 구성되어 있다.\[2\]

![ERD](https://www.popit.kr/content/images/wp/2018/01/ERD-600x116.jpg)

```java
@Entity
@Table(name = "product")
public class Product {
    @Id
    @GeneratedValue
    @Column(name = "product_id")
    private Long id;
 
    private String name;
 
    private int price;
 
    @Column(name = "stock_amount")
    private int stockAmount;
    ...
}
 
@Entity
@Table(name = "member")
public class Member {
 
    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;
 
    private String name;}
 
    private int age;
 
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<Order>();
    ...
}
 
@Entity
@Table(name = "order")
public class Order {
 
    @Id
    @GeneratedValue
    @Column(name = "order_id")
    private Long id;
 
 
    @Column(name = "order_amount")
    private int orderAmount;
 
    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;
 
    @ManyToOne
    @JoinColumn(name = "product_id")
    private Product product;
 
    ..
}
```

# 각 상품을 구매한 회원을 조회해 보자

JPA NativeQuery로 수행할 SQL과 실행 결과 값을 매핑할 DTO는 아래와 같다.\[3\]

```mysql
SELECT "product"."product_id" AS productId, "product"."name" AS productName, "product"."price",
    "product"."stock_amount" AS stockAmount, "order"."order_id" AS orderId,
    "member"."member_id" AS memberId, "member"."name" AS memberName
FROM "product"
    LEFT JOIN "order"
        ON "product"."product_id" = "order"."PRODUCT_ID"
    LEFT JOIN "member"
        ON "order"."MEMBER_ID" = "member"."member_id"
```

```java
public class ProductOrderedMemberDTO {
    private Long productId;
    private String productName;
    private int price;
    private int stockAmount;
    private Long orderId;
    private Long memberId;
    private String memberName;
 
    // .. getter, setter
}
```

# ResultClass로 첫 시도

[EntityManager](http://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html?ref=popit.kr)는 NativeQuery 사용 시 결과 값을 다양하게 지원하는데 그중 하나가 ResultClass이다.

![image2017-10-15_13-29-44](https://www.popit.kr/content/images/wp/2018/01/image2017-10-15_13-29-44-600x150.png)

```java
@Test
public void test_상품별_구매한_회원_목록_조회_by_resultClass() {
    // given
    setUpTestFixture();
 
    // when
    EntityManager em = emf.createEntityManager();
 
    String sql = "SELECT \"product\".\"product_id\" AS productId, \"product\".\"name\" AS productName, \"product\".\"price\", \n" +
            "    \"product\".\"stock_amount\" AS stockAmount, \"order\".\"order_id\" AS orderId,\n" +
            "    \"member\".\"member_id\" AS memberId, \"member\".\"name\" AS memberName\n" +
            "FROM \"product\" \n" +
            "    LEFT JOIN \"order\" \n" +
            "        ON \"product\".\"product_id\" = \"order\".\"PRODUCT_ID\"\n" +
            "    LEFT JOIN \"member\" \n" +
            "        ON \"order\".\"MEMBER_ID\" = \"member\".\"member_id\"";
 
    Query nativeQuery = em.createNativeQuery(sql, ProductOrderedMemberDTO.class);
    List<ProductOrderedMemberDTO> products = nativeQuery.getResultList();
 
    // then
    // ...
}
```

테스트 코드를 수행하면 어떻게 될까?

![image2017-10-15_13-32-49](https://www.popit.kr/content/images/wp/2018/01/image2017-10-15_13-32-49-600x59.png)

ProductOrderedMemberDTO는 엔터티가 아니기 때문에 MappingException 이 발생한다.

# Object Array로 해결하기

일단 ProductOrderedMemberDTO는 엔터티가 아니기 때문에 ResultClass는 사용할 수 없다. ResultClass를 지정하지 않고 반환된 Object Array를 사용해서 DTO로 매핑하는 방법이 있다.

![image2017-10-15_13-38-44](https://www.popit.kr/content/images/wp/2018/01/image2017-10-15_13-38-44-600x162.png)

```java
@Test
public void test_상품별_구매한_회원_목록_조회_by_ObjectArray() {
    // given
    setUpTestFixture();
 
    // when
    EntityManager em = emf.createEntityManager();
 
    String sql = "SELECT \"product\".\"product_id\" AS productId, \"product\".\"name\" AS productName, \"product\".\"price\", \n" +
            "    \"product\".\"stock_amount\" AS stockAmount, \"order\".\"order_id\" AS orderId,\n" +
            "    \"member\".\"member_id\" AS memberId, \"member\".\"name\" AS memberName\n" +
            "FROM \"product\" \n" +
            "    LEFT JOIN \"order\" \n" +
            "        ON \"product\".\"product_id\" = \"order\".\"PRODUCT_ID\"\n" +
            "    LEFT JOIN \"member\" \n" +
            "        ON \"order\".\"MEMBER_ID\" = \"member\".\"member_id\"";
 
    Query nativeQuery = em.createNativeQuery(sql);
 
    List<Object[]> resultList = nativeQuery.getResultList();
 
    List<ProductOrderedMemberDTO> products = resultList.stream().map(product -> new ProductOrderedMemberDTO(
            ((BigInteger) product[0]).longValue(),
            (String) product[1],
            (Integer) product[2],
            (Integer) product[3],
            product[4] == null ? null : ((BigInteger)product[4]).longValue(),
            product[5] == null ? null : ((BigInteger)product[5]).longValue(),
            (String) product[6])).collect(Collectors.toList());
 
    // then
    Assert.assertEquals(5, products.size());
    // ...
    em.close()
}
```

하지만 이 방법은 Object를 DTO 데이터 타입에 맞게 캐스팅해 주어야 하는 번거로움이 있다.

# ResultSetMapping으로 해결하기

[EntityManager](http://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html?ref=popit.kr)는 ResultClass 이외에도 ResultSetMapping을 지원한다. 이를 사용하여 앞선 Object Array 방식을 개선해 보자.

![image2017-10-15_13-47-18](https://www.popit.kr/content/images/wp/2018/01/image2017-10-15_13-47-18-600x138.png)

먼저 [SqlResultSetMapping](http://docs.oracle.com/javaee/7/api/javax/persistence/SqlResultSetMapping.html?ref=popit.kr) 을 사용하여 DTO와 매핑될 메타정보를 선언해 준다.

필자는 Product Entity에 선언하였으며, [ConstructorResult](https://docs.oracle.com/javaee/7/api/javax/persistence/ConstructorResult.html?ref=popit.kr)을 사용하였기 때문에 ProductOrderedMemberDTO에 생성자를 추가해 준다.  

```java
@SqlResultSetMapping(
        name="ProductOrderedMemberMapping",
        classes = @ConstructorResult(
                targetClass = ProductOrderedMemberDTO.class,
                columns = {
                        @ColumnResult(name="productId", type = Long.class),
                        @ColumnResult(name="productName", type = String.class),
                        @ColumnResult(name="price", type = Integer.class),
                        @ColumnResult(name="stockAmount", type = Integer.class),
                        @ColumnResult(name="orderId", type = Long.class),
                        @ColumnResult(name="memberId", type = Long.class),
                        @ColumnResult(name="memberName", type = String.class),
                })
)
@Entity
@Table(name = "product")
public class Product {
    // ...
}
 
 
public class ProductOrderedMemberDTO {
    // ...
    public ProductOrderedMemberDTO(Long productId, String productName, int price, int stockAmount,
                                   Long orderId, Long memberId, String memberName) {
        this.productId = productId;
        this.productName = productName;
        this.price = price;
        this.stockAmount = stockAmount;
        this.orderId = orderId;
        this.memberId = memberId;
        this.memberName = memberName;
    }
    // ...
}
```

ResultSetMapping을 사용하기 위한 작업이 끝났다. 이제 테스트해 보자.  

```java
@Test
public void test_상품별_구매한_회원_목록_조회_by_ResultSetMappingg() {
    // given
    setUpTestFixture();
 
    // when
    EntityManager em = emf.createEntityManager();
 
    String sql = "SELECT \"product\".\"product_id\" AS productId, \"product\".\"name\" AS productName, \"product\".\"price\", \n" +
            "    \"product\".\"stock_amount\" AS stockAmount, \"order\".\"order_id\" AS orderId,\n" +
            "    \"member\".\"member_id\" AS memberId, \"member\".\"name\" AS memberName\n" +
            "FROM \"product\" \n" +
            "    LEFT JOIN \"order\" \n" +
            "        ON \"product\".\"product_id\" = \"order\".\"PRODUCT_ID\"\n" +
            "    LEFT JOIN \"member\" \n" +
            "        ON \"order\".\"MEMBER_ID\" = \"member\".\"member_id\"";
 
    Query nativeQuery = em.createNativeQuery(sql, "ProductOrderedMemberMapping");
    List<ProductOrderedMemberDTO> products = nativeQuery.getResultList();
 
    // then
    Assert.assertEquals(5, products.size());
    // ...
    em.close();
}
```

Object를 DTO 데이터 타입에 맞게 변환해 주어야 하는 번거로움이 사라졌다.

# ResultSetMapping 제약 사항

ResultSetMapping의 경우 엔터티에 [SqlResultSetMapping](http://docs.oracle.com/javaee/7/api/javax/persistence/SqlResultSetMapping.html?ref=popit.kr)으로 DTO와 매핑될 메타정보를 선언해 주어야 한다. 하지만 엔터티가 수정이 불가한 상황(다른 사람에게 제공받아 개발해야 하는 경우)이거나 엔터티와 DTO의 의존 관계를 제거하고 싶은 경우에는 어떻게 해야 할까?

# [QLRMQuery Language Result Mapper](http://simasch.github.io/qlrm/?ref=popit.kr)

QLRM은 JPA를 지원하는 ResultSet Mapper이다. QLRM을 사용하면 ResultSetMapping 제약 사항을 벗어날 수 있다. 즉, QLRM은 JPA Native Query 결과값을 DTO로 매핑해 준다.  

```java
JpaResultMapper jpaResultMapper = new JpaResultMapper();
 
Query q = em.createNativeQuery("SELECT ID, NAME FROM EMPLOYEE");
List<EmployeeTO> list = jpaResultMapper.list(q, EmployeeTO.class);
```

# QLRM 사용해 보기

먼저 QLRM을 사용하기 위해 Maven 의존성 모듈을 추가한다.  

```
<dependency>
    <groupId>ch.simas.qlrm</groupId>
    <artifactId>qlrm</artifactId>
    <version>1.7.1</version>
</dependency>
```

그리고 ProductOrderedMemberDTO에 생성자를 만든다.(SELECT 컬럼과 동일한 순서와 타입으로)  

```java
public class ProductOrderedMemberDTO {
    private Long productId;
    private String productName;
    private int price;
    private int stockAmount;
    private Long orderId;
    private Long memberId;
    private String memberName;
 
    public ProductOrderedMemberDTO(BigInteger productId, String productName,
                                   Integer price, Integer stockAmount,
                                   BigInteger orderId, BigInteger memberId,
                                   String memberName) {
        // ...
    }
    // ...

```

마지막으로 QLRM JpaResultMapper에 Native Query와 DTO를 넘겨주면 끝이다.  

```java
@Test
public void test_상품별_구매한_회원_목록_조회_by_QLRM() {
    // given
    setUpTestFixture();
 
    // when
    EntityManager em = emf.createEntityManager();
 
    String sql = "SELECT \"product\".\"product_id\" AS productId, \"product\".\"name\" AS productName, \"product\".\"price\", \n" +
            "    \"product\".\"stock_amount\" AS stockAmount, \"order\".\"order_id\" AS orderId,\n" +
            "    \"member\".\"member_id\" AS memberId, \"member\".\"name\" AS memberName\n" +
            "FROM \"product\" \n" +
            "    LEFT JOIN \"order\" \n" +
            "        ON \"product\".\"product_id\" = \"order\".\"PRODUCT_ID\"\n" +
            "    LEFT JOIN \"member\" \n" +
            "        ON \"order\".\"MEMBER_ID\" = \"member\".\"member_id\"";
 
    Query nativeQuery = em.createNativeQuery(sql);
 
    JpaResultMapper jpaResultMapper = new JpaResultMapper();
    List<ProductOrderedMemberDTO> products = jpaResultMapper.list(nativeQuery, ProductOrderedMemberDTO.class);
 
    // then
    Assert.assertEquals(5, products.size());
    // ...
    em.close();
}
```

# GitHub

전체 코드는 [필자의 Github](https://github.com/YooYoungmo/article-jpa-native-query?ref=popit.kr)에서 확인할 수 있다.

# 주석

\[1\] 일반적으로 많은 테이블을 조인하여 만들어지기 때문이다.

\[2\] 예제는 [JPA 프로그래밍 책](http://acornpub.co.kr/book/jpa-programmig?ref=popit.kr)에서 일부를 발췌하였다.

\[3\] 이 글에서는 DTO 매핑이 주제이기 때문에 SQL 자체에 대해서는 논외로 한다