Kotlin Data Class와 불변성

한 줄 정의

Kotlin data class는 값 전달 객체를 간결하게 만들기 위한 문법이고, 불변성은 객체 상태 변경을 제한해 예측 가능한 코드를 만드는 설계 원칙이다.

실무에서 왜 중요한가

백엔드에서는 request, response, command, event 같은 값 전달 객체가 많다. Kotlin은 이를 간결하게 만들지만 다음 주의점이 있다.

  • data class를 JPA Entity로 사용하면 자동 생성된 equals, hashCode, toString이 문제를 만들 수 있다.
  • val은 재할당을 막지만 참조하는 객체 내부 변경까지 막지는 않는다.
  • List는 읽기 전용 인터페이스이지 완전한 불변 컬렉션이 아니다.
  • copy()는 얕은 복사라 nested mutable 객체는 공유될 수 있다.
  • default value가 Jackson 역직렬화와 validation 의미를 흐릴 수 있다.

data class 사용처

data class는 DTO나 command처럼 값 전달이 목적인 객체에 적합하다.

data class OrderResponse(
    val orderId: Long,
    val status: String,
    val totalPrice: Long
)

자동으로 다음 메서드가 생성된다.

  • equals
  • hashCode
  • toString
  • copy
  • componentN

이 자동 생성이 장점이지만, Entity처럼 식별자와 생명주기가 있는 객체에는 위험할 수 있다.

JPA Entity에 data class를 피하는 이유

data class Order(
    val id: Long?,
    val status: String
)

JPA Entity에 data class를 사용하면 다음 문제가 생길 수 있다.

  • class와 method가 기본적으로 final이라 proxy 생성과 충돌한다.
  • equals/hashCode가 모든 생성자 프로퍼티를 기준으로 만들어진다.
  • 양방향 연관관계가 toString에 포함되면 순환 참조가 생길 수 있다.
  • 지연 로딩 필드 접근이 의도치 않게 발생할 수 있다.

JPA Entity는 일반 class로 만들고, DTO는 data class로 분리하는 편이 안전하다.

val과 불변성

val은 변수 재할당을 막는다.

val items: MutableList<String> = mutableListOf()
items.add("order") // 가능

하지만 참조하는 객체가 mutable이면 내부 상태 변경은 가능하다. 완전한 불변성을 원하면 mutable 타입을 외부로 노출하지 않아야 한다.

class Cart(
    items: List<String>
) {
    private val _items = items.toMutableList()
    val items: List<String>
        get() = _items.toList()
}

자주 나는 실수

  • DTO와 Entity를 모두 data class로 만든다.
  • val이면 객체가 완전히 불변이라고 생각한다.
  • copy()가 deep copy라고 오해한다.
  • mutable collection을 외부에 그대로 노출한다.
  • default value로 필수 입력 누락을 숨긴다.

실무 판단 기준

상황권장
API request/responsedata class
service command/querydata class
domain value objectdata class 가능, 불변성 확인
JPA Entity일반 class 우선
외부 노출 collection읽기 전용 타입 + 방어적 복사 검토

확인 방법

  • JPA Entity가 data class인지 확인한다.
  • toString, equals, hashCode가 lazy loading 필드를 건드리지 않는지 확인한다.
  • val 프로퍼티가 mutable collection을 가리키는지 확인한다.
  • copy() 후 nested 객체 변경이 원본에 영향을 주는지 테스트한다.

핵심 요약

Kotlin data class는 DTO, command, event처럼 값 전달이 목적인 객체에 적합하다.

JPA Entity는 식별자, proxy, lazy loading, 생명주기 문제가 있어 data class를 피하는 편이 안전하다.

val은 재할당을 막지만 객체 내부 변경까지 막지는 않는다.

Kotlin의 List는 읽기 전용 인터페이스일 뿐 완전한 불변 컬렉션을 보장하지 않는다.

불변성을 실무에서 얻으려면 mutable 상태를 외부에 노출하지 않고, 필요한 경우 방어적 복사를 사용해야 한다.

꼬리 질문

점검 퀴즈

아래 문항은 개념을 실제로 설명할 수 있는지 점검하기 위한 것이다. 선택지를 누르면 정답 여부와 이유가 표시된다.

객관식data class가 특히 적합한 백엔드 용도는?

OXval 프로퍼티만 사용하면 객체 내부 상태까지 항상 완전한 불변이 된다.

객관식JPA Entity에 data class를 피하는 이유로 가장 적절한 것은?

관련 문서