본문 바로가기
코틀린

QueryDsl 코틀린으로 안전하게 쓰기

by RWriter 2022. 1. 11.
반응형

예제소스는 깃허브에 있습니다.

 

코틀린을 사용하면 확장함수를 통해서 유용한 기능을 만들 수 있고, 이를 통해 안전하고 가독성 있는 코드를 짤 수 있어서 정말 편리하다.

사내에서도 코틀린을 사용한 유틸 함수들을 만들어 사용하고 있다. 오늘은 QueryDSL 에 관한 내용이다.

 

JPA + QueryDSL 을 사용하다 보면 검색조건에 따른 dynamic query 를 작성하는 일이 잦다.

 

Dynamic query는 아래와 같은 모습이 된다.

어떤 조건이나 파라미터의 유무에 따라서 query 의 where절을 붙여주는 형태이다.

값의 유무에 따른 로직은 코틀린의 ? (물음표 null safety) 으로 사용할 수 있지만 자바에서 사용하던 대로 if 문을 사용했다.

// 검색 파라미터
data class Param(
    val id: Long?,
    val title: String?,
    val body: String?,
    val writerId: Long?
)

override fun doQuery_java_style(param: Param): List<Post> {
    val qPost = QPost.post

    val query = from(qPost)
    if (param.id != null) {
        query.where(qPost.id.eq(param.id))
    }
    if (param.title.isNullOrBlank()) {
        query.where(qPost.title.eq(param.title))
    }
    if (param.body.isNullOrBlank()) {
        query.where(qPost.body.eq(param.body))
    }
    if (param.writerId != null) {
        query.where(qPost.writerId.eq(param.writerId))
    }
    return query.fetch() ?: emptyList()
}

익숙한 형태이고 이렇게 사용하는 것이 나쁘다 라고 하기는 어렵다. 요구조건의 변경에도 수정이 간편하고 if 문 block 을 void나 Unit 함수로 분리해서 관리하기도 편하다.

 

개인적으로 코틀린을 사용하며 조금 바뀐 코딩 습관이 있는데 if 문을 쓰면 else 를 꼭 붙여서 state(문 또는 절) 보다는 expression (식) 을 사용하는 것인데, 예를 들어 if 문이 상당히 중첩되었을 때는 로직을 파악하기 어렵고 잘못하다간 구멍이 생길 수 있기 때문이다.

 

 

코틀린을 사용하면 if else, when 절에 대해서도 expression 이 가능해 함수 시그니처에 바로 =(equals) 를 붙여서 값으로 반환할 수 있다.

 

이러한 관점에서 early return, fast return 에 대한 생각도 좀 달라졌다.

 

if 문이 중첩된 함수.

else 를 쓰지 않게 되면 로직이 복잡해졌을 때 구멍을 찾기 어려워진다.

fun complicated() {
  if (a) return;
  if (b) {
    if (c) return;
  }
  if (d) {
    if (e) {
      println("hello")
    }
  }
}

 

이를 expression 으로 바꾸면 이렇게 된다.

// = 로 함수를 시작하게 되면 if 에는 무조건 else 를 붙이게 되어있다.
// if empty body 인 경우에 warning 을 없앨 수 있다.
@SuppressWarnings("ControlFlowWithEmptyBody")
fun complicated(): Unit = 
    if (a) {} 
    else if (b) {
      if (c) {} 
      else {}
    } 
    else if (d) {
      if (e) {
        println("hello")
      } else {}
    } else {} // 위에서 등장하지 않았던 마지막 else 가지도 발라내었다.

else 를 쓰지 않았던 분들에게는 조금 어색할 수 있지만 함수의 분기점을 빠짐없이 발라내서 미쳐 고려하지 못했던 로직에 구멍을 찾을 수 있으니 그런 관점에서는 else 를 강제하는 것이 안전한 프로그래밍이라고 할 수도 있다.

 

 

QueryDSL 로 돌아와서 else 가 없는 쿼리문도 if 문 보다는 좀 더 로직 블럭 간 떨어짐이 없이 expression 으로 반환될 수 있게 할 수 있으면 좋겠다고 생각했다.

 

코틀린의 확장 함수를 사용하면 이를 해결할 수 있는데 이런 식으로 변환할 수 있다.

override fun doQuery_kotlin_my_style(param: Param): List<Post> {
    val qPost = QPost.post

    return from(qPost)
        .whereNotEmpty(param.id) { qPost.id.eq(it) }
        .whereNotEmpty(param.title) { qPost.title.eq(it) }
        .whereNotEmpty(param.body) { qPost.body.eq(it) }
        .whereNotEmpty(param.writerId) { qPost.writerId.eq(it) }
        .fetchList()
}

 

 

from(qPost) 의 결과 객체인 JPQLQuery 에 확장함수를 붙여서 if 중심이 아닌 Query 중심으로 체이닝 연산을 하여 쿼리 조건들이 안전하게 엮여있는 모습이다.

 

whereNotEmpty 함수의 구현이다.

fun <C, T> JPQLQuery<C>.whereNotEmpty(
    param: T?,
    predicate: (T) -> Predicate?,
): JPQLQuery<C> {
    param.notEmpty {
        this.where(predicate(it))
    }
    return this
}

inline fun <T> T?.notEmpty(block: (T) -> Unit) {
    if (this == null || (this is String && this.trim() === "")) return

    block(this)
}

주로 String 또는 Not String 클래스가 오는데, String은 null 체크 뿐만 아니라 blank("", " ") 여부까지 확인을 해야 안전하기 때문에 두가지 종류의 클래스를 판단해줄 notEmpty 확장함수를 추가했다.

 

쿼리 결과 안전하게 받기

QueryDSL 쿼리를 통해 최종으로 List 객체를 받아올 수 있는데 코틀린에서는 주의할 점이 있다.

 

Fetchable 의 fetch() 함수는 List 를 응답한다. 하지만 자바 타입으로 응답을 하기 때문에 코틀린 입장에서는 nullable 여부를 알 수가 없다.

 

 

 

! 타입은 nullable 여부를 알 수 없는 타입이기 때문에 코틀린에서는 nullable, not null 타입 모두 취급할 수 있다.

 

그럴때는 아래와 같이 List 자체가 null 인 경우 ?: (엘비스 연산자) 를 통해 emptyList() 로 최종 리턴을 해주면 된다.

override fun doQuery_kotlin_my_style(param: Param): List<Post> {
    val qPost = QPost.post

    return from(qPost)
        .whereNotEmpty(param.id) { qPost.id.eq(it) }
        .whereNotEmpty(param.title) { qPost.title.eq(it) }
        .whereNotEmpty(param.body) { qPost.body.eq(it) }
        .whereNotEmpty(param.writerId) { qPost.writerId.eq(it) }
        .fetch() ?: emptyList()
}

 

코틀린에서는 확장 함수를 통해 이를 더 간결하게 표현할 수 있다.

fun <T> Fetchable<T>.fetchList(): List<T> = this.fetch() ?: emptyList()
override fun doQuery_kotlin_my_style(param: Param): List<Post> {
  val qPost = QPost.post

  return from(qPost)
      .whereNotEmpty(param.id) { qPost.id.eq(it) }
      .whereNotEmpty(param.title) { qPost.title.eq(it) }
      .whereNotEmpty(param.body) { qPost.body.eq(it) }
      .whereNotEmpty(param.writerId) { qPost.writerId.eq(it) }
      .fetchList()
}
반응형

댓글