Java / Spring
[QueryDsl] 프로젝션과 결과 반환 - DTO 조회, @QueryProjection 본문
queryDsl 기능 중, entity 에 직접 접근하지 않고 DTO 클래스로 entity 에 대한 데이터를 조회하는 방법
현재 Entity 클래스와 DTO 클래스가 존재한다. 요구사항에 따라 Member 클래스의 username, age 라는 데이터를 별도로 조회하는 기능을 구현해야 한다고 했을 때 Entity 클래스에 직접 접근하여 jpql 을 custom 할 수 있지만 좋은 설계가 아니다.
가능한 entity 클래스의 데이터를 건드리지 않고 DTO 클래스만을 컨트롤하여 데이터를 조회하는 것이 좋은 설계 방법이다.
※ 이유 :
1) 성능 최적화
필요한 데이터만 조회 : Entity 를 사용하면 Entity 클래스에 정의된 모든 필드를 가져오지만, DTO 를 사용하면 필요한 필드만 조회할 수 있다. 이는 데이터 전송 양을 줄여 성능을 최적화하는 데 도움이 된다.
(N + 1 문제 회피 가능성 : Entity 를 사용할 때 연관된 Entity 가 많으면 N + 1 문제가 발생 할 수 있다. DTO 로 조회하면 이러만 문제 발생 가능성을 예방 할 수 있다)
2) 데이터 보호
불필요한 데이터 노출 방지 : Entity 에는 비즈니스 로직에서 민감한 정보가 포함될 수 있는데, DTO 를 사용하면 외부에 노출할 필요가 없는 데이터를 가릴 수 있다.
3) 계층 분리와 역할 분담
Entity 는 비즈니스 로직에 집중할 수 있고 DTO는 오로지 데이터 전송을 위한 역할을 담당하기 때문에 역할이 명확하게 분리되어 각 클래스의 책임이 분명해지고 유지보수성이 높아진다.
4) 영속성 관리 이슈 방지
Entity 는 영속성 컨텍스트에 묶여 있다, 이 말은 즉 Entity 는 영속성 컨텍스트에서 관리되기 때문에 Entity 를 직접 반환하면 불필요하게 영속성 컨텍스트에 의해 관리되는 상태가 이어질 가능성이 높다. 이를 피하기 위해 DTO 로 변환하여 영속성 컨텍스트 의존을 끊을 수 있다.
5) 트랜잭션 범위 제한
Entity 를 외부에 노출하면 트랜잭션 문제가 발생 할 수 있다. 물론 Service Layer 등에서 Entity 를 DTO 로 변환하여 Web Layer 로 넘어가게 할 수 있지만 Data 를 DTO 로 직접 조회하거나 주고 받게 되면 이러한 로직이 없이도 트랜잭션 문제를 예방 할 수 있다.
먼저 queryDsl 을 사용하기 전, 순수한 jpql 쿼리를 통해 DTO 에 접근 하는 방법을 알아보자
1. 순수 JPA 방식으로 DTO 접근
# Member (Entity) 클래스
@Entity
@Table(name = "member")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
# MemberDTO ( Data Transfer Object, 데이터 전송 객체 ) 클래스
package study.querydsl.dto;
import lombok.Data;
@Data
public class MemberDTO {
private String username;
private int age;
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
Test 코드에서 아래와 같은 방법으로 jpql 을 사용하여 DTO 클래스에 직접 접근하도록 하였다.
* 순수 JPA 방식 특징
- 순수 JPA 에서 DTO 를 조회할 떄는 new 명령어를 사용해야한다.
- DTO 의 pakage 이름을 다 적어줘야해서 코드가 지저분해진다.
- 생성자 방식만 지원 한다.
- 단점 : 패키지경로와 클래스명을 모두 적어줘야 한다는 단점이 존재함
DTO 클래스가 ENTITY 클래스와 접근하는 방식- jpql 에서 DTO 로 조회할 때, jpql 문법의 일부로 select new 키워드를 사용하는데 이 때, new 키워드가 특정 DTO 클래스의 생성자를 호출하는 역할을 한다.
구체적으로 select new studyquerydsl.dto.MemberDTO(m.username, m.age) 에서 jpql 은 Member Entity 에서 m.username 과 m.age 값을 가져온 뒤, 그 값을 MemberDTO 클래스의 생성자에 전달하여 객체를 생성하는 방식이다.
그래서 from 절에서 Member 클래스의 Entity 값을 가르기고 select 절에서 Entity 에 접근시키기 위한 new DTO 객체를 생성 하는 것이다.※ 정리 : MemberDTO 에 username 과 age 를 인자로 받는 생성자가 정의 되어 있어야 한다.
@Test
public void findDtoByJPQL() throws Exception {
List<MemberDTO> resultList = em.createQuery("select new study.querydsl.dto.MemberDTO(m.username, m.age) " +
"from Member m",
MemberDTO.class)
.getResultList();
System.out.println("resultList = " + resultList);
}
2. Querydsl 방식으로 DTO 접근
먼저 DTO 를 QueryDsl 로 사용하기 위해 클래스 내부 생성자에 @QueryProjection 어노테이션을 추가한다
@Data
@NoArgsConstructor
public class MemberDTO {
private String username;
private int age;
@QueryProjection // Dto 를 Q 파일로 등록하기 위한 QueryProjection 어노테이션
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
}
이렇게 되면 DTO 의 생성자를 QueryDsl 로 사용할 수 있고 컴파일을 하면 QMemberDTO 클래스가 생성된다.
잠깐 생성된 Q클래스를 살펴보면
public 으로 정의된 QMemberDTO 생성자에서 경로와 내부에 들어갈 파라미터 값이 고정으로 설정 되어 있다.
@Generated("com.querydsl.codegen.DefaultProjectionSerializer")
public class QMemberDTO extends ConstructorExpression<MemberDTO> {
private static final long serialVersionUID = 1356708610L;
public QMemberDTO(com.querydsl.core.types.Expression<String> username, com.querydsl.core.types.Expression<Integer> age) {
super(MemberDTO.class, new Class<?>[]{String.class, int.class}, username, age);
}
}
QMemberDTO 생성자 쿼리 조회
@Test
public void findDtoByQueryProjection() throws Exception {
List<MemberDTO> result = queryFactory
.select(new QMemberDTO(member.username, member.age))
.from(member)
.fetch();
for (MemberDTO memberDTO : result) {
System.out.println("memberDTO = " + memberDTO);
}
}
위와 같이 테스트 코드에서 생성자를 select() 메서드 내부 파라미터로 새롭게 호출하여 내부에 들어갈 파라미터 값을 넣는다
QmemberDTO 클래스 사용 시 장점은 내부 파라미터값이 항상 고정으로 들어가야되기 때문에 다른 데이터 타입의 값이 들어가는 것을 방지해준다 (컴파일 에러 발생)
.select(new QMemberDTO(member.username, member.age, member.Address))
(※ member.Address 는 파라미터로 들어갈 수 없음)
Projections.객체 사용 시 DTO 의 생성자와 필드에 직접 접근하여 데이터를 조회할 수 있다.
1) DTO 필드 주입 방식 (Projections.field(DTO.class, 파라미터 값.as(필드명) ...))
@Test
public void findUserDto() throws Exception {
List<UserDTO> result = queryFactory
.select(Projections.fields(UserDTO.class,
member.username.as("name"), // alias 를 사용하여 UserDTO 의 필드명과 일치 시킨다.
member.age))
.from(member)
.fetch();
for (UserDTO userDTO : result) {
System.out.println("userDTO = " + userDTO);
}
}
2) DTO 생성자 주입 방식 (Projections.constructor(DTO.class, 파라미터 값...))
@Test
public void soloTestQueryConstructor() throws Exception {
List<UserDTO> userDto = queryFactory
.select(Projections.constructor(UserDTO.class,
member.username.as("name"),
member.age))
.from(member)
.fetch();
for (UserDTO userDTO : userDto) {
System.out.println("userDTO = " + userDTO);
}
}
'Spring > Spring Data JPA' 카테고리의 다른 글
[QueryDsl] Repository(custom) 구현 (0) | 2024.11.18 |
---|---|
EntityManager & EntityManagerFactory (0) | 2024.09.20 |
JPQL(Named 쿼리) @NamedQuery (0) | 2024.05.09 |
JPQL (JPQL기본기능, 결과조회 API, 쿼리의종류, 파라미터 바인딩) (0) | 2024.05.08 |
값 타입 (+ 기본값, 임베디드, 불변객체 타입) (0) | 2024.05.08 |