본문 바로가기
Java/spring

QueryDsl에 대해 알아보자.

by 티코딩 2024. 5. 27.

오랜만에 포스팅이다. 그간 취업이안돼서 방황도해보고 자격증공부도 해보고 혼자 웹페이지 프로젝트도 해보고 그런시간을 보냈다.

요즘 다시 열정이 돌아와 열심히 취업을 준비중이다.
전에 편하게 쓰던 Jpa를 보다가 jpa n+1 문제에 대한 해법으로 QueryDsl 에 대해 알게 되었고, 동영상 강의가 있길래 한번 정리해본다.
QueryDSL은 간단하게 설명하자면 자바 기반의 데이터베이스 쿼리 언어로, 타입안전한 방식으로 쿼리를 작성할 수 있게 도와주는 기술이다. 기존의 JPQL이나 SQL같은 문자열 기반 쿼리 언어의단점을 보완하기 위해 설계되었다. 장점으로는 컴파일 시에 쿼리 오류를 잡을 수 있어 더 안전하고 유지보수가 쉬워진다는 점이있다.


먼저 QueryDsl을 쓰기위해 build.gradle에 

이렇게 의존성을 추가해주자.
QueryDsl은 컴파일 시 별도의 객체를 만들어준다. 객체를 이용해 코드형태로 sql을 짠다. annotaitonProcessor가 없으면 생성한 객체를 인식을 못하기때문에 반드시 넣어줘야한다. 이 annotaitonProcessor은 컴파일 시에 어노테이션 프로세싱을 수행할 때 필요한 라이브러리를 추가하기 위해 사용한다.

 

ㅇ JPA의 N+1문제?

한 번의 쿼리로 가져온 데이터가 N개의 추가 쿼리를 발생시켜서 성능 저하를 일으키는 상황을 말하는데, 예를 들어보자.

'회원'과 '주문' 엔티티가 있고, 각 회원은 여러 개의 주문을 가질 수 있습니다. 이를 JPA를 사용하여 조회할 때 N+1 문제가 어떻게 발생하는지 보자.

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "member")
    private List<Order> orders;
}

@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;
    private String product;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;
}

 

public List<Member> findAllMembersWithOrders() {
    List<Member> members = em.createQuery("SELECT m FROM Member m", Member.class)
                              .getResultList();
    for (Member member : members) {
        System.out.println(member.getOrders().size()); // 여기서 N+1 문제 발생
    }
    return members;
}

 

위 코드에서 findAllMembersWithOrders 메서드는 Member를 조회하는데, 여기서 발생하는 첫번째 쿼리는
SELECT m FROM Member m;

이 쿼리로 전체 회원리스트를 가져오고, 각 회원의 orders를 호출할 때마다 추가 쿼리가 발생한다.

SELECT o FROM Order o WHERE o.member.id = ?;

 

사실 N+1은 Fetch Join을 사용해 해결가능하기도 하다.

public List<Member> findAllMembersWithOrders() {
    List<Member> members = em.createQuery(
        "SELECT m FROM Member m JOIN FETCH m.orders", Member.class)
                              .getResultList();
    for (Member member : members) {
        System.out.println(member.getOrders().size()); // 추가 쿼리 발생하지 않음
    }
    return members;
}

 

이러면

SELECT m FROM Member m JOIN FETCH m.oreders;
이렇게 간단히 해결가능하기도 하다.



ㅇ DTO의 필요성

DTO는 계층 간 데이터 전송을 위한 객체로서 순수하게 데이터 전송만을 목적으로 한다. DTO를 사용함으로써 장점으로는, 데이터베이스 엔터티를 프레젠테이션 계층과 분리할 수 있어 각 계층 간의존성을 줄이고 유지보수성을 높히며, 민감한 데이터나 불필요한 데이터를 숨길 수 있고, 다양한 형태의 데이터를 변환해 전송할 수 있어, 클라이언트가 요구하는 형식에 맞출 수 있다는 장점이 있다.
내가 사용하는 dto를 보여주자면 아래와 같다.

public class UserDto {

    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Signup {
        private String username;
        private String password;
        private String email;
    }
    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Login {
        private String username;
        private String password;
    }

}

 

@PostMapping("/signup")
    public ResponseEntity<?> signup(@RequestBody UserDto.Signup requestBody) {
        logger.info("Signup attempt with email: {}, username: {}", requestBody.getEmail(), requestBody.getUsername());
        try {
            userService.signup(userMapper.userDtoSignupToUser(requestBody));
            return ResponseEntity.ok("회원가입이 성공적으로 완료되었습니다.");
        } catch (BusinessLogicException e) {
            return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage());
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Internal Server Error");
        }
    }

매퍼를 썼지만 Controller에서 각 api에 파라미터로 넣어서 클라이언트에게 응답을 보낸다.

 

ㅇ QueryDSL 로 데이터를 조회해보자.

ㅁ Member 엔터티와 Order 엔터티

import javax.persistence.*;
import java.util.List;

@Getter
@Setter
@Entity
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "member")
    private List<Order> orders;
}

@Getter
@Setter
@Entity
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String product;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

}

 

ㅁ MemberRepository

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;

@Repository
public class MemberRepository {

    private final JPAQueryFactory queryFactory;

    @Autowired
    public MemberRepository(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

    public List<Member> findAllMembersWithOrders() {
        QMember member = QMember.member;
        QOrder order = QOrder.order;

        return queryFactory.selectFrom(member)
                           .leftJoin(member.orders, order).fetchJoin()
                           .fetch();
    }

    public Member findMemberByName(String name) {
        QMember member = QMember.member;

        return queryFactory.selectFrom(member)
                           .where(member.name.eq(name))
                           .fetchOne();
    }
}

 

  • selectFrom(entity): 조회할 엔티티를 지정한다.
  • leftJoin(entity1, entity2).fetchJoin(): 엔티티 간의 조인을 수행한다. fetchJoin()을 사용하여 Fetch Join을 수행함.
  • where(predicate): 조건을 지정함. 예를 들어, member.name.eq(name)은 회원의 이름이 특정 값과 같은지를 조건으로 함.
  • fetch(): 쿼리를 실행하여 결과를 리스트로 반환함.
  • fetchOne(): 쿼리를 실행하여 단일 결과를 반환함.

ㅁ MemberService

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public List<Member> getAllMembersWithOrders() {
        return memberRepository.findAllMembersWithOrders();
    }

    public Member getMemberByName(String name) {
        return memberRepository.findMemberByName(name);
    }
}

 

ㅁ MemberController

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    @GetMapping("/members")
    public List<Member> getMembersWithOrders() {
        return memberService.getAllMembersWithOrders();
    }

    @GetMapping("/member")
    public Member getMemberByName(@RequestParam String name) {
        return memberService.getMemberByName(name);
    }
}

 

QueryDSL로 조회결과에 대해 정렬하는 방법

위와같이 엔터티를 정의하고 repository를 바꾼다.

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;

@Repository
public class MemberRepository {

    private final JPAQueryFactory queryFactory;

    @Autowired
    public MemberRepository(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

    public List<Member> findAllMembersWithOrdersSortedByName() {
        QMember member = QMember.member;
        QOrder order = QOrder.order;

        return queryFactory.selectFrom(member)
                           .leftJoin(member.orders, order).fetchJoin()
                           .orderBy(member.name.asc())  // 이름 오름차순 정렬
                           .fetch();
    }

    public List<Order> findAllOrdersSortedByPriceDesc() {
        QOrder order = QOrder.order;

        return queryFactory.selectFrom(order)
                           .orderBy(order.price.desc())  // 가격 내림차순 정렬
                           .fetch();
    }
}

findAllMembersWithOrdersSortedByName 메서드는 Member 엔터티를 조회하면서 Order엔터티를 Fetch Join 하고, 회원의 이름을 기준으로 오름차순 정렬한다. 

findAllOrdersSortedByPriceDesc 메서드는 Order엔터티를 조회하고 가격을 기준으로 내림차순 정렬한다.
이런식으로 자바코드로 쿼리를 만든다는게 신기했다. 지금하고있는 프로젝트에서도 한번 써봐야겠다.