ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 6. 네 번째 요구사항 추가하기 - Querydsl
    Kotlin/실전! 코틀린과 스프링 부트로 ... 개발 2023. 8. 8. 20:07

    37. Querydsl 도입하기

    1. JPQL과 Querydsl의 장단점을 이해할 수 있다.

    2. Querydsl을 Kotlin + Spring Boot 와 함께 사용할 수 있다.

    3. Querydsl을 활용해 기존에 존재하던 Repository를 리팩토링 할 수 있다.

     

    JPQL은 무슨 단점이 있을까?

    - 문자열이기 때문에 '버그'를 찾기가 어렵다!

    - JPQL 문법이 일반 SQL와 조금 달라 복잡한 쿼리를 작성할 때마다 찾아보아야 한다.

     

    Spring Data JPA는 무슨 단점이 있을까?

    - 조건이 복잡한 동적쿼리를 작성할 때 함수가 계속해서 늘어난다.

    - 프로덕션 코드 변경에 취약하다.

     

    JPQL과 Spring Data JPA의 단점 정리!

    1. 문자열로 쿼리를 작성하기에 버그를 찾기 어렵다.

    2. 문법이 조금 달라 그때마다 검색해 찾아보아야 한다.

    3. 동적 쿼리 작성이 어렵다.

    4. 도메인 코드 변경에 취약하다.

     

    - Spring Data JPA와 Querydsl을 함께 사용하며 서로를 보완해야 한다!

    - Querydsl : 코드로 쿼리를 작성하게 해주는 도구!

     

    38. Querydsl 사용하기 - 첫 번째 방법

    1. UserRepositoryCustom interface 생성

    2. UserRepositroy 가 UserRepositoryCustom을 상속

    3. UserRepositoryCustom을 상속하는 UserRepositoryCustomImpl 생성

     

    - QuerydslConfig

    @Configuration
    class QuerydslConfig(
        private val em: EntityManager
    ) {
    
        @Bean
        fun querydsl(): JPAQueryFactory {
            return JPAQueryFactory(em)
        }
    }

     

    @Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.userLoanHistories")
    fun findAllWithHistories(): List<User>
    class UserRepositoryCustomImpl(
        private val queryFactory: JPAQueryFactory,
    ) : UserRepositoryCustom {
        override fun findAllWithHistories(): List<User> {
            return queryFactory.select(user).distinct()
                .from(user)
                .leftJoin(userLoanHistory).on(userLoanHistory.user.id.eq(user.id)).fetchJoin()
                .fetch()
        }
    }

     

    - 장점

        ㆍ서비스단에서 UserRepository 하나만 사용하면 된다.

    - 단점

        ㆍ인터페이스와 클래스를 항상 같이 만들어 줘야 하는 것이 부담이고 여러모로 번거롭다

     

    39. Querydsl 사용하기 - 두 번째 방법

    @Query("SELECT NEW com.group.libraryapp.dto.book.reponse.BookStatResponse(b.type, COUNT(b.id)) FROM Book b GROUP BY b.type")
    fun getStats(): List<BookStatResponse>
    fun getStats(): List<BookStatResponse> {
        return queryFactory.select(Projections.constructor(
            BookStatResponse::class.java,
            book.type,
            book.id.count()
        ))
            .from(book)
            .groupBy(book.type)
            .fetch()
    }

    - Projections.constructor : 주어진 DTO의 생성자를 호출한다는 의미!

        ㆍ이때 뒤에 나오는 파라미터들이 생성자로 들어간다.

    select type, count(book.id) from book group by type;

     

    - 장점

        ㆍ클래스만 바로 만들면 되어 간결하다

    - 단점

        ㆍ필요에 따라 두 Repository를 모두 불러와야 한다

     

    * 개인적(최태현님)으로는 2번째 방법을 선호한다.

        ㆍ멀티 모듈을 사용하는 경우 모듈 별로만 Repository를 쓰는 경우가 많기 때문 

     

    40. UserLoanHistoryRepository를 Querydsl으로 리팩토링하기

    - @Query 를 사용하지 않은 Repository 기능도 Querydsl로 옮겨야 할까?

        ㆍ개인적(최태현님)으로는 Querydsl로 옮기는 것을 선호한다!

        ㆍ동적 쿼리의 간편함 때문!!

     

    - 동적쿼리

        ㆍwhere 조건이 동적으로 바뀌는 쿼리는 Queydsl을 이용하면 쉽게 구현이 가능하다!

     

    findByBookName 변경

    fun find(bookName: String): UserLoanHistory? {
        return queryFactory.select(userLoanHistory)
            .from(userLoanHistory)
            .where(
                userLoanHistory.bookName.eq(bookName)
            )
            .limit(1)
            .fetchOne()
    }

    - 함수 이름이 간결해졌다.

    - limit(1) : SQL의 limit 1, 모든 검색 결과에서 1개만 가져온다는 의미이다.

    - fetchOne() : List<Entity> 로 조회 결과를 가져오는 대신 Entity 하나만 가져온다.

     

    findByBookNameAndStatus 변경

    fun find(bookName: String, status: UserLoanStatus? = null): UserLoanHistory? {
        return queryFactory.select(userLoanHistory)
            .from(userLoanHistory)
            .where(
                userLoanHistory.bookName.eq(bookName),
                status?.let { userLoanHistory.status.eq(status) }
            )
            .limit(1)
            .fetchOne()
    }

    - default parameter에 null을 넣어 외부에서는 bookName만 쓸 수도, status까지 같이 쓸 수도 있다.

    - status가 null이 아닌 경우에만 User_loan_history.status = ? 가 들어간다.

    - Where 에 여러 조건이 들어오면 각 조건은 AND 로 연결된다

    - Where 조건에 null이 들어오면 무시하게 된다.

     

    countByStatus 변경

    fun count(status: UserLoanStatus): Long {
        return queryFactory.select(userLoanHistory.count())
            .from(userLoanHistory)
            .where(
                userLoanHistory.status.eq(status)
            )
            .fetchOne() ?: 0L
    }

     - fetchOne() 은 null이 나올 수 있기에 ?: 연산자를 사용해 0L 반환

     

    41. 마지막 요구사항 클리어!

    댓글

Designed by Tistory.