Kotlin으로의 전환을 연구하면서, DTO는 어떻게 구현해야하는지 생각해보게 됐다.
자바 코드
다음과 같은 자바 코드를 Kotlin으로 전환해 볼 것이다.
@Getter
public class CursorResponse<T, C> {
private final List<T> items;
private final C nextCursor;
private final boolean hasNext;
public CursorResponse(
List<T> items,
C nextCursor,
boolean hasNext
) {
this.items = items;
this.nextCursor = nextCursor;
this.hasNext = hasNext;
}
public static <T, C> CursorResponse<T, C> of(
List<T> items,
C nextCursor,
boolean hasNext
) {
return new CursorResponse<>(items, nextCursor, hasNext);
}
}
class CursorResponse<T, C>(
val items: List<T>,
val nextCursor: C,
val hasNext: Boolean,
) {
companion object {
fun <T, C> of(
items: List<T>,
nextCursor: C,
hasNext: Boolean,
): CursorResponse<T, C> {
return CursorResponse(items, nextCursor, hasNext)
}
}
}
private final 필드와 public getter, 생성자 할당을 합친 게 val이다.
var로 선언하게 되면 private 필드, public getter/setter 이렇게 생성된다.
자바의 public static class 대용으로 사용한다고 보면 된다. 클래스 안에 companion object를 두고, 그 안에 메서드를 구현한다.
호출 방식은 자바 static 메서드와 똑같다.
이 방식도 돌아가기는 하지만 코틀린스럽지는 않다. 코틀린 장점을 잘 못 살렸다고 볼 수 있는 코드다.
자바에서는 record 클래스 방식이랑 유사하다. 단순 데이터를 담는 Response 이므로 data class가 조금 더 적합하다고 판단했다.
data class CursorResponse<T, C>(
val items: List<T>,
val nextCursor: C? = null,
val hasNext: Boolean = nextCursor != null,
) {
companion object {
/**
* size + 1 만큼 조회된 결과로부터 다음 커서를 추출하여 응답을 만든다.
* 마지막 항목이 cutoff 역할
*/
fun <T, C> from(
items: List<T>,
pageSize: Int,
cursorExtractor: (T) -> C,
): CursorResponse<T, C> {
val hasNext = items.size > pageSize
val pagedItems = if (hasNext) items.take(pageSize) else items
val nextCursor = if (hasNext) cursorExtractor(pagedItems.last()) else null
return CursorResponse(pagedItems, nextCursor, hasNext)
}
}
}
data class를 생성하면 컴파일러가 자동으로 만들어준다.
equals() / hashCode() : 필드 기반 비교toString() — CursorResponse(items=[...], nextCursor=..., hasNext=true)copy() : 일부 필드만 바꿔서 복사 (response.copy(hasNext = false))componentN() : 구조 분해 (val (items, cursor, hasNext) = response)java의 Lombok @Data와 비슷한 역할이다.
자바에서는 of, create와 같은 정적 팩터리 메서드를 생성하는 방식을 주로 사용한다. new 키워드로 생성하지 않고 기본값을 고정해두기 위해서다. 그런데 코틀린에서는 이게 불필요해서 제거해도 된다.
왜냐.. 코틀린에서는 new 키워드가 없다.
그래서 생성자 호출 자체가 깔끔해지고, 제네릭 추론 자체가 자바보다 더 유연하다. 람다 안에서 하더라도 명시를 딱히 안해줘도 되고, 양방향 추론도 된다. 자바는 거꾸로 쓰면 안된다 ...
코틀린
val list = listOf(1, 2, 3) // List<Int>로 추론
val map = mapOf("a" to 1, "b" to 2) // Map<String, Int>로 추론
val pair = "key" to 42 // Pair<String, Int>로 추론
자바
자바는 List, Map 선언 시 타입 명시가 강제된다.
List<Integer> list = List.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2);
이 점 때문에 of가 있었던 것이다.
커서 페이징에서 변환 로직 같은게 아니라면.. 이제 생략해도 될 것 같다.