반응형

코틀린 강의를 들으면서 한가지 기능에 대해 가장 쉽고, 빠르고, 코틀린만의 문법규칙을 사용하지 않고 작성하여 기능을 확인한 뒤 점진적으로 리팩토링 하며 코틀린 문법에 익숙해지려 한다.

 

강의를 들으면서 3단계에 걸쳐 동일한 기능을 리팩토링 하였고 그 과정을 기억해보려 한다.

[기능] 기능 설명

카테고리가 있는 책들의 현재 대출 현황에 대한 갯수를 보여준다.

{
    "COMPUTER": 1,
    "SCIENCE": 2,
    "SOCIATY": 5
}

카테고리 종류는 더 많지만 대출중인 유효 갯수가 있는 카테고리만 반환한다.

> bookRepository

현재 도서관이 보유중이 책 목록을 조회할 수 있다.

 

> BookStatResponse

카테고리별 대출중인 책의 갯수를 담는 DTO 이다.

 

[1] for loop 로 조회하기

- serivce

@Transactional(readOnly = true)
fun getBookStatistics_v1(): List<BookStatResponse> {
    val results = mutableListOf<BookStatResponse>()
    val books = bookRepository.findAll()
    for (book in books) {
        val targetDto = results.firstOrNull { dto -> book.type == dto.type }
        if (targetDto == null) {
            results.add(BookStatResponse(book.type, 1))
        } else {
            targetDto.plusOne()
        }
    }
    return results
}

 

- BookStatResponse

data class BookStatResponse(
        val type: BookType,
        var count: Int,
) {
    fun plusOne() {
        count++
    }
}

 

> 문제점

- 코드가 너무 길다

- BookStatResponse의 count가 가변 필드로 되어 의도치 않은 수정이 일어날 수 있다.

 

[2] ?. 와 ?: 사용하기

- service

@Transactional(readOnly = true)
fun getBookStatistics_v2(): List<BookStatResponse> {
    val results = mutableListOf<BookStatResponse>()
    val books = bookRepository.findAll()
    for (book in books) {
        results.firstOrNull { dto -> book.type == dto.type }?.plusOne()
                ?: results.add(BookStatResponse(book.type, 1))
    }
    return results
}

 

> 문제점

- call chain이 길어져 디버깅이 쉽지 않다.

- BookStatResponse의 count가 가변 필드로 되어 의도치 않은 수정이 일어날 수 있다.

 

[3] group by 사용하기

- service

@Transactional(readOnly = true)
fun getBookStatistics(): List<BookStatResponse> {
    return bookRepository.findAll() //List<Book>
            .groupBy{ book -> book.type }  // Map<BookType, List<Book>>
            .map { (type, books) -> BookStatResponse(type, books.size)} // List<BookStatResponse>
}

 

- BookStatResponse

data class BookStatResponse(
        val type: BookType,
        val count: Int,
)

 

> 코드 설명

1) findAll() 을 사용해 도서관에 등록되어 있는 모든 책을 조회한다 `List<Book>`

2) groupBy 를 사용해 카테고리 별 Map 으로 만들어준다 `Map<BookType, List<Book>>`

3) map 을 사용해 BookStatResponse DTO에 카테고리별 갯수를 담아준다 `List<BookStatResponse`

 

> 해결한 것

- BookStatResponse data class의 count 필드가 불변 필드로 되어 의도치 않은 수정을 막을 수 있다.

- 코드가 간결해 졌다.

 

> 문제점

도서관에 등록된 책의 전체 목록을 조회하여 그 갯수를 세는 과정이므로 db부하 등을 걱정해야 한다.

(전체 데이터 쿼리 => 메모리 로딩 + grouping)

 

[4] JPQL + Spring data JPA 활용하기 (예제 2개)

1] (위 예제와 이어지는 예제) 예제 1 : Book 기능

bookRepository에 커스텀 query function을 작성한다.

//jpql
@Query("select new com.group.libraryapp.dto.book.response.BookStatResponse(b.type, count(b.id)) " +
        "from Book b group by b.type")
fun getStats(): List<BookStatResponse>

 

위 메소드를 사용하면 service에서는 매우 간결해진다.

@Transactional(readOnly = true)
fun getBookStatistics(): List<BookStatResponse> = bookRepository.getStats()

 

해당 쿼리를 통해 조회하면 h2 query 출력은 아래와 같이 나온다

select
    book0_.type as col_0_0_,
    count(book0_.id) as col_1_0_ 
from
    books book0_ 
group by
    book0_.type

 

> 해결한 것

groupBy 쿼리를 사용하여 db로부터 전체 데이터를 메모리에 로드하지 않고 필요한 정보만을 가져오게 되었다.

-> index를 사용해 튜닝 (성능 최적화) 할수있는 여지가 잇다.

> 문제점

- query 구문에서 외부 객체를 사용하기 위해서는 절대 경로를 전부 입력해 주어야 한다...

- 오타와 띄어쓰기를 매우 주의해야 한다..

 

2] 예제 2 : User 기능

유저와 대출히스토리 table이 각각 있는 db구조에서 유저와 해당 유저의 전체 대출 히스토리 기록을 한번에 조회하는 기능이 있다. 이때 대출기록이 없는 유저일지라도 유저목록으로는 반환되야 한다. 

- service

@Transactional(readOnly = true)
fun getUserLoanHistories(): List<UserLoanHistoryResponse> {
    return userRepository.findAllWithHistories().map(UserLoanHistoryResponse::of)
}

 

- repository

@Query("select distinct u from User u left join fetch u.userLoanHistories")
fun findAllWithHistories(): List<User>

 

> 문제점

- 오타 문제

- 조건이 추가된다면 또 다른 jpql 문을 작성해야 한다.

- 어떤것을 참조하는지 타고 들어가 봐야 안다.

 

[5] Querydsl (예제 2개)

0] 코프링에 querydsl 적용하기

1) gradle 추가

먼저 코프링 환경에서 querydsl을 사용하기 위해서 gradle 에 추가해주어야 한다.

// plugin
id "org.jetbrains.kotlin.kapt" version "1.6.21"

// dependecy
implementation("com.querydsl:querydsl-jpa:5.0.0")
kapt("com.querydsl:querydsl-apt:5.0.0:jpa")
kapt("org.springframework.boot:spring-boot-configuration-processor")

참고로 환경은 스프링 부트 3버전 / intelliJ 23.1버전 / kotlin 1.16 버전 이다.

 

설정 변경후 빌드를 하면 `build > generated > source > kapt > main > .....{프로젝트 루트} ..... > domain` 에 User와 Book이 아닌 QUser QBook 처럼 앞에 Q가 붙은 클래스들이 생성된걸 볼 수 있다.

 

2) 스프링 빈 등록

 

그리고 나서 QuerydslConfig 파일을 만들어 Component로 등록하고 JPAQueryFactory를 Bean으로 생성한다.

import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import javax.persistence.EntityManager

@Configuration
class QuerydslConfig(
        private val em: EntityManager
) {

    @Bean
    fun querydsl(): JPAQueryFactory {
        return JPAQueryFactory(em)

    }
}

 

다음으로 querydsl을 사용할 interface와 구현체 클래스를 작성한다. 이는 기존의 repository 는 jpql + jpa를 사용하기 때문에 분리하기 위함이다.

 

3) querydsl 인터페이스, 구현체 생성

 

- interface : UserRepositoryCustom

interface UserRepositoryCustom { ... }

 

- class : UserRepositoryCustomImpl

class UserRepositoryCustomImpl(
        private val queryFactory: JPAQueryFactory
) : UserRepositoryCustom{ ... }

 

 

4) 기존 repository interface에 방금만든 것 상속

그리고 기존의 userRepository interface에서 Custom interface를 상속받도록 한다. 기존에 상속받고 있던 JPARepository는 그대로.

interface UserRepository : JpaRepository<User, Long>, UserRepositoryCustom{ ... }

 

1] 예제 2 : User 기능 => interface로 만들기

querydsl을 활용해 repository를 작성한다.

- UserRepositoryCustom

interface UserRepositoryCustom {
    fun findWithHistories(): List<User>
}

 

- UserRepositoryCustomImpl

import com.group.libraryapp.domain.user.QUser.user
import com.group.libraryapp.domain.user.loanhistory.QUserLoanHistory.userLoanHistory
import com.querydsl.jpa.impl.JPAQueryFactory

class UserRepositoryCustomImpl(
        private val queryFactory: JPAQueryFactory
) : UserRepositoryCustom{
    override fun findWithHistories(): List<User> {
        // querydsl 로 작성
        return queryFactory.select(user).distinct()
                .from(user)
                .leftJoin(userLoanHistory).on(userLoanHistory.user.id.eq(user.id)).fetchJoin()
                .fetch()
    }
}

 

의존성 주입을 위해 생성자에 JPAQueryFactory를 적어주어야 한다. query 구문에서는 기존 Entity class가 아닌 Q클래스를 사용한다.

 

2] (위 예제와 이어지는 예제) 예제 1 : Book 기능 => class로 만들기

BookQuerydslRepository 라는 클래스를 정의한다.

@Component
class BookQuerydslRepository(
        private val queryFactory: JPAQueryFactory,
) {
    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;

 

위 쿼리문을 해석하면 아래와 같다.

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

 

반응형

+ Recent posts