[Kotlin] DTO 구현하기 Java to Kotlin

easyone·2026년 4월 25일

코딩 테스트

목록 보기
12/12
post-thumbnail

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);
    }
}

방법1 : 일반 클래스로 변환

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)
        }
    }
}

val과 var

private final 필드와 public getter, 생성자 할당을 합친 게 val이다.
var로 선언하게 되면 private 필드, public getter/setter 이렇게 생성된다.

companion object

자바의 public static class 대용으로 사용한다고 보면 된다. 클래스 안에 companion object를 두고, 그 안에 메서드를 구현한다.
호출 방식은 자바 static 메서드와 똑같다.

이 방식도 돌아가기는 하지만 코틀린스럽지는 않다. 코틀린 장점을 잘 못 살렸다고 볼 수 있는 코드다.

방법2: 데이터 클래스로 변환

자바에서는 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가 있었던 것이다.
커서 페이징에서 변환 로직 같은게 아니라면.. 이제 생략해도 될 것 같다.

profile
백엔드 개발자 지망 대학생

0개의 댓글