kotlin by 키워드를 사용한 JPA + QueryDsl 통합 Repository 구현
jpa를 사용해서 개발하다 보면 JpaRepository 인터페이스를 구현한 repository와 queryDsl를 사용하는 repoitory를 사용할 일이 잦다.
나는 간단한 조회, 엔티티 저장은 JpaRepository를 사용하고, 배치성 업데이트나 복잡한 쿼리 등은 querydsl을 사용하는 편이다.
그런데 JpaRepsitory, QuerydslRepository가 각각의 클래스로 분리되어 있다보니 서비스 계층에서 주입받아야 하는 클래스가 늘어났다.
@Service
class OrderService(
private val orderJpaRepository: OrderJpaRepository,
private val orderQuerydslRepository: OrderQuerydslRepository
) {
...
}
이런 구조의 단점을 난 다음과 같이 생각했다.
1. 서비스 계층에서 repository가 어떻게 구현된건지 각각에 구현체를 꼭 알아야 할 필요가 없다.
2. 의존성을 주입 받는 객체가 늘어날수록 테스트가 복잡해진다.
그래서 자바 개발하던 당시 이런 문제점을 해소하기위해 다소 복잡하지만 컴포지션을 사용해서 하나의 Repository를 만들었다.
예를 들어 다음과 같은 구조가 된다.
@Repository
class OrderRepository(
private val orderJpaRepository: OrderJpaRepository,
private val orderQuerydslRepository: OrderQuerydslRepository
) {
fun save(order: Order): Order {
return orderJpaRepository.save(order)
}
fun findById(id: Long): Optional<Order> {
return orderJpaRepository.findById(id)
}
...
}
@Service
class OrderService(
private val orderRepository: OrderRepository,
) {
...
}
이렇게되면 서비스에서는 하나의 repository를 보게 된다. OrderRepository는 위임을 통해 함수를 호출한다.
하지만, 이 방식도 단점이 있는데 위임하는 코드를 직접 작성해줘야한다는 것이다. 상당히 귀찮다...
코틀린에서는 by라는 키워드를 제공하는데 이번 게시글의 핵심이다.
간단하게 말하면 by 키워드를 사용하면 위임을 위한 코드를 생성해준다!
by키워드를 사용해서 다음과 같은 코드를 작성해보자.
@Repository
class OrderRepository(
private val orderJpaRepository: OrderJpaRepository,
private val queryFactory: JPAQueryFactory,
) : OrderJpaRepository by orderJpaRepository {
}
이 코드를 자바로 decompile해보면 아래의 코드가 된다.
자세히 들여다보면 this.orderJpaRepository를 호출하여 동작을 위임하는 코드가 생성됐다.
따라서 OrderRepository를 by를 사용해서 JpaRepository 메서드를 위임하고 이 클래스에서 queryDsl을 사용한 코드를 정의하면 하나의 Repository를 사용해서 관리할 수 있다.
interface OrderJpaRepository : JpaRepository<Order, Long> {
fun findByProductName(productName: String): Order?
}
@Repository
class OrderRepository(
private val orderJpaRepository: OrderJpaRepository,
private val queryFactory: JPAQueryFactory,
) : OrderJpaRepository by orderJpaRepository {
fun findLowPriceOrder(): List<Order> {
return queryFactory.selectFrom(QOrder.order)
.where(QOrder.order.price.between(0, 10000))
.fetch()
}
}
@Service
class OrderService(
private val orderRepository: OrderRepository,
) {
fun saveOrder(order: Order) {
orderRepository.save(order) // OrderJpaRepository
}
fun findOrder(productName: String){
orderRepository.findByProductName("티셔츠") // OrderJpaRepository
}
fun findLowPriceOrder() {
orderRepository.findLowPriceOrder() // OrderRepository(querydsl)
}
}
결론
위임을 위한 코드를 작성하지 않고도 코틀린의 by키워드를 통해 JpaRepo + QueryDslRepo를 통합한 Repository를 간단하게 구현할 수 있다.
이 방식은 간단하지만 주의해야 할 점이 있다. 하나의 엔티티에 관련된 Repository가 아닌 여러 엔티티가 join 되거나 불분명한 함수들이 포함된다면 오히려 사용에 헷갈릴 수 있다.
이럴 때는 불분명한 메서드가 섞이게 된다면 custom repository클래스를 따로 만들어서 처리하도록 하는 게 덜 헷갈리는 방식일 것 같다.