ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [내일배움캠프] 3월 9일 목요일 TIL 회고록
    카테고리 없음 2023. 3. 9. 23:28

    프로젝션과 결과 반환 - DTO 조회

     

    예제 1 : 순수 JPA에서 DTO 조회

    @Test
    public void findDtoByJPQL() {
      List<MemberDto> result = em.createQuery(
          // 패키지 명을 다 적어야 하기에 불편하다.
              "select new study.querydsl_study.dto.MemberDto(m.username,m.age) from Member m",
              MemberDto.class)
          .getResultList();
      for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
      }
    }

    순수 JPA에서 DTO 조회할 때는 new 명령어를 사용해야한다. (select new study~~와 같이)

    DTO에 package 이름을 다 적어줘야해서 지저분하다. 

    생성자 방식만 지원한다. ( setter를 이용하거나 , 필드에 값을 바로 주입을 해주는게 불가능하다.)

     

    Querydsl은 이 문제를 다 극복하는 깔끔한 방법을 지원한다.

    1. 프로퍼티 접근

    2. 필드 직접 접근

    3. 생성자 사용

    총 세가지를 지원한다.

     

    1. 프로퍼티 접근 예시 :

    @Test
    public void findDtoBySetter() {
      List<MemberDto> result = queryFactory
          .select(Projections.bean(MemberDto.class,
              member.username,
              member.age))
          .from(member)
          .fetch();
      for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
      }
    }

    select 절에 Projections.bean을 사용해서 MemberDto.class를 불러온다.

    그 후 Getter, Setter를 사용해서 값이 생성이 된다.

    public String getUsername() {
      return username;
    }
    
    public void setUsername(String username) {
      this.username = username;
    }
    
    public int getAge() {
      return age;
    }
    
    public void setAge(int age) {
      this.age = age;
    }

     

    이렇게 실행하면 에러가 나온다.

     해결 방법은 MemberDto를 가보면

    @Data
    public class MemberDto {
    
      private String username;
      private int age;
    
      public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
      }

    username과 age 필드가 있고, 생성자를 만들었다. 또 @Data 어노테이션이 있는데 이 어노테이션은

    이거들을 다 추가해준다. 하지만 기본생성자를 만들어 주지 않는다. 위에 저 오류는 기본생성자가 없어서 생기는 오류이다.

    그래서 MemberDto에 아래와 같이 기본 생성자를 만들어 주거나 @NoArgsConstructor 어노테이션을 추가해주면 된다.

    public MemberDto() {
    }
    @Data
    @NoArgsConstructor
    public class MemberDto {
    
      private String username;
      private int age;
      
      public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
      }

    테스트 코드를 실행해보면 오류 없이 테스트를 성공한다.

     

    2. 필드 직접 접근 예시 :

    @Test
    public void findDtoByField() {
      List<MemberDto> result = queryFactory
          .select(Projections.fields(MemberDto.class,
              member.username,
              member.age))
          .from(member)
          .fetch();
      for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
      }
    }

    필드 직접 접근은 Projections.fields를 사용한다. 위에 프로퍼티 접근과는 달리 Getter,Setter를 사용하지 않고(무시하고) 바로 값이

    DTO에 필드에 값이 꽂힌다.

     

    3. 생성자 사용 예시 :

    @Test
    public void findDtoByConstructor() {
      List<MemberDto> result = queryFactory
          .select(Projections.constructor(MemberDto.class,
              member.username,
              member.age))
          .from(member)
          .fetch();
      for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
      }
    }

    Projections.constructor를 사용하고 MemberDto에 있는 필드와 순서가 일치해야한다.

     

    MemberDto.java

    private String username;
    private int age;
    
      public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
      }
    MemberDto 생성자를 사용한다.
     

    만약 순서가 일치하지 않으면?

    @Test
    public void findDtoByConstructor() {
      List<MemberDto> result = queryFactory
          .select(Projections.constructor(MemberDto.class,
              member.age,
              member.username))
          .from(member)
          .fetch();
      for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
      }
    }

    이렇게 테스트 실패가 나오며 오류가 출력된다.

    오류 내용을 보면 원래 순서는 String (username) , int (age) 순서여야 하는데 int (age) , String (username) 순서여서 오류가 나온다는 내용이다.


    프로젝션과 결과 반환 - @QueryProjection

    사용법 : MemberDto에 바로 @QueryProjection 어노테이션을 추가한다.

    @QueryProjection
    public MemberDto(String username, int age) {
      this.username = username;
      this.age = age;
    }

    그 다음 QMemberDto 클래스를 만든다. (그냥 어플리케이션 실행하니까 생겼다.)

    @Test
    public void findByQueryProjection() {
      List<MemberDto> result = queryFactory
          .select(new QMemberDto(member.username, member.age))
          .from(member)
          .fetch();
    
      for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
      }
    }

    QMemberDto를 사용해서 쿼리를 만든다. 얼핏 보면 위에 생성자를 사용한 방식과 같아보이는데 생성자로 사용한 방식의 쿼리에 

    member.id를 넣고 실행해보면 여기서는 오류가 안나오지만 실행하면 런타임 오류가 나오면서 테스트가 실패한다.

    @Test
    public void findDtoByConstructor() {
      List<MemberDto> result = queryFactory
          .select(Projections.constructor(MemberDto.class,
              member.username,
              member.age,
              member.id))

    생성자로 만드는 쿼리에 단점은 실제 유저가 이 코드를 실행하는 순간이 되서야 문제를 찾을 수 있다. 

     

    이번에는 @QueryProjection으로 만든 쿼리에 똑같이 member.id를 넣으려고 하면 컴파일 오류가 나온다.

    QMemberDto.java

    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 생성자에 이미 데이터를 <String> username, <Integer> age 두가지만 받도록 만들어져서 두개인 상태에서 한개가 더 들어오려하면 에러가 난다.

     

    @QueryProjection는 Projections.constructor (생성자 사용)과 비슷하지만 컴파일 오류로 많은 것을 잡을 수 있다.

    단점이 있는데, Q파일을 생성헤야 하는 것 (DTO에)이고 기존 DTO에는 QueryDsl에 대한 의존성이 없었지만 @QueryProjection을 사용함으로써 Querydsl에 의존성을 가지게 된다.


    동적 쿼리 - BooleanBuilder 사용

     

    사용 예시 : 

    @Test
      public void dynamicQuery_BooleanBuilder() {
        String usernameParam = "member1";
        Integer ageParam = 10;
    
        List<Member> result = searchMember1(usernameParam,ageParam);
        assertThat(result.size()).isEqualTo(1);
      }
    
      private List<Member> searchMember1(String usernameCond, Integer ageCond) {
    
        // 멤버의 유저네임을 null로 넘어오지 않을려고 하려면 이렇게 따면 된다. age도 마찬가지
    //    BooleanBuilder booleanBuilder = new BooleanBuilder(member.username.eq(usernameCond));
        BooleanBuilder booleanBuilder = new BooleanBuilder();
        // usernameCond가 null이 아니면
        if (usernameCond != null) {
          // booleanBuilder에 and 조건을 하나 넣어주고 멤버의 이름이 usernameCond와 같은지 확인한다.
          booleanBuilder.and(member.username.eq(usernameCond));
        }
        // ageCond가 null이 아니면
        if (ageCond != null) {
          // booleanBuilder에 and 조건을 하나 넣어주고 멤버의 나이가 ageCond와 같은지 확인한다.
          booleanBuilder.and(member.age.eq(ageCond));
        }
    
        return queryFactory
            .selectFrom(member)
            .where(booleanBuilder)
            .fetch();
      }

    동적 쿼리 : Where 다중 파라미터 사용

    어제 댓글 전체 조회 및 검색 기능을 만들면서 사용했던 기능인데 따로 메서드를 만들어서 where문에서 보기 편하게 만들 수 있다.

    @Test
    public void dynamicQuery_WhereParam() {
      String usernameParam = "member1";
      Integer ageParam = 10;
    
      List<Member> result = searchMember2(usernameParam,ageParam);
      assertThat(result.size()).isEqualTo(1);
    
    }

    먼저 dynamicQuery_WhereParam 메서드를 만들고 그 안에  usernameParam 필드와 agaParam 필드를 만들었다.

    private List<Member> searchMember2(String usernameCond, Integer ageCond) {
      return queryFactory
          .selectFrom(member)
          .where(usernameEq(usernameCond),ageEq(ageCond))
          .fetch();
    }

    그 다음 쿼리문을 작성을 했다. searchMember2 메서드를 만들고 매개변수로 위에서 만든 usernameCond, ageCond 를 받는다.

    .where문에 usernameEq, ageEq 메서드가 사용이 되는데 이 두가지 기능은 이렇게 만들었다.

    // usernameCond가 null이면 위에 where문에서 usernameEq(usernameCond)는 null로 바뀌어져서
    // 쿼리를 수행할때 그냥 무시가 되버린다. (아무 역할을 하지 않음) 그래서 동적 쿼리가 만들어진다.
    private BooleanExpression usernameEq(String usernameCond) {
      // 삼항연산자 사용 usernameCond가 null이 아니면 member.username.eq 문 실행, null이면 null 반환
      return usernameCond != null ? member.username.eq(usernameCond) : null;
    }

    BooleanExpression을 리턴 타입으로 받고 usernameCond를 매개변수로 받는 usernameEq 메서드를 만들었다.

    usernameEq 메서드는 호출시에 usernameCond가 null이 아니면 member.username.eq(usernameCond) 조건이 실행되고

    null 일경우에는 null이 반환된다.

    만약 usernameCond가 null일 경우 searchMember2 메서드에 where 절에는 이렇게 실행 될 것이다.

    private List<Member> searchMember2(String usernameCond, Integer ageCond) {
      return queryFactory
          .selectFrom(member)
          .where(null,ageEq(ageCond))
          .fetch();
    }

    이 경우엔 null은 그냥 무시된다. 즉 아무 역할도 하지 않아 동적 쿼리가 완성이 된다.

     

    그 다음 똑같이 BooleanExpression을 리턴 타입으로 받고 ageCond를 매개변수로 받는 ageEq 메서드를 만들었다. 

    private BooleanExpression ageEq(Integer ageCond) {
      // ageCond가 null이 아니면 member.age.eq(ageCond)문 정상 실행, null이면 null 반환
      return ageCond != null ? member.age.eq(ageCond) : null;
    }

    여기도 마찬가지로 ageEq 메서드가 호출되면 ageCond가 null이 아니면 member.age.eq(ageCond) 조건이 실행되고, 

    ageCond가 null일 경우에는 null이 반환된다. 

    여기서도 만약 ageCond가 null일 경우 searchMember2 메서드에 where 절에는 이렇게 실행 될 것이다.

    private List<Member> searchMember2(String usernameCond, Integer ageCond) {
      return queryFactory
          .selectFrom(member)
          .where(usernameEq(usernameCond),null)
          .fetch();
    }

    이 경우에도 null은 그냥 무시된다. 즉 아무 역할도 하지 않아 동적 쿼리가 완성이 된다.

    이 두 메서드를 사용해서 쿼리문을 작성하면 훨씬 깔끔하고 보기도 편하고, 메서드를 다른 쿼리에도 재사용할 수 있다. 

    그리고 이 두 메서드를 합칠수도 있다.

      // 메서드로 구현해놓으면 이렇게 조합을 할 수 있다. 조합을 할 경우 따로 null 체크를 챙겨줘야한다.
        private BooleanExpression allEq(String usernameCond, Integer ageCond) {
        return usernameEq(usernameCond).and(ageEq(ageCond));
      }

    이렇게 만들면 훨씬 더 깔끔해진다. 하지만 이렇게 만들 경우 null 체크를 따로 해줘야한다.


     

Designed by Tistory.