예제소스는 깃허브에 있습니다.
코틀린을 사용하면 확장함수를 통해서 유용한 기능을 만들 수 있고, 이를 통해 안전하고 가독성 있는 코드를 짤 수 있어서 정말 편리하다.
사내에서도 코틀린을 사용한 유틸 함수들을 만들어 사용하고 있다. 오늘은 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()
}
'코틀린' 카테고리의 다른 글
코루틴을 사용한 동시성, 병렬처리 - 2 (0) | 2022.01.14 |
---|---|
코루틴을 사용한 동시성, 병렬처리 - 1 (2) | 2022.01.14 |
Kotlin Exposed (orm) 사용해보기 (0) | 2022.01.09 |
코틀린으로 Functional 하게 테스트 코드 짜기 (0) | 2021.08.13 |
ResultSet 에서 List<T> 로 쉽게 변환하기 (0) | 2021.08.05 |
댓글