코딩은 마라톤

[Kotlin] 데이터 클래스의 숨겨진 함정: copy() 메서드와 불변성 본문

Language/Kotlin

[Kotlin] 데이터 클래스의 숨겨진 함정: copy() 메서드와 불변성

anxi 2025. 9. 20. 16:29

데이터 클래스란?

코틀린의 데이터 클래스는 컴파일러가 사용자가 정의하지 않은 equals, hashcode, toString 메서드를 자동으로 대신 생성한다.

 

data class Person(val name: String) {
    var age: Int = 0
}

 

사용 방법은 클래스 생성 방식과 유사하며, class 앞에 data 변경자만 붙이면 데이터 클래스를 사용할 수 있다.

 

🧐 [Kotlin] 데이터 클래스 == [Java] 레코드

코틀린을 처음 학습할 때, 데이터 클래스는 자바의 레코드(record) 클래스와 동일하다고 생각했다.

 

레코드는 불변 객체를 생성할 때 사용하고, 보통 DTO(Data Transfer Object)나 밸류 타입에서 주로 사용했다.

코틀린 프로젝트를 찾아보니 자바 프로젝트의 레코드와 유사하게 사용하길래 "둘은 같은 거구나"라고 생각했다.

 

하지만 직접 사용해보니 둘 사이에는 아주 큰 차이점이 있었다. 바로 불변성(Immutability)에 대한 차이가 있었다.


❌ 데이터 클래스는 '진정한 불변'을 보장하지 않는다.

1. var이 사용 가능하다.

자바의 레코드는 모든 필드가 final로 선언되어 완전한 불변 객체를 생성한다. 그러나 코틀린의 데이터 클래스는 valvar를 모두 사용 가능하다.

// 자바 - 레코드
public record Person(String name) {}

// 생성
Person person = new Person("anxi");

// 수정
person.setName("bnxi"); // ❌
person.name = "bnxi"; // ❌

 

위 자바 레코드와 달리, 코틀린 데이터 클래스는 var로 선언된 프로퍼티를 변경할 수 있다.

data class Person(val name: String) {
    var age: Int = 0
}

val person = Person("성민")
person.age = 25 // ✅

 

그렇다면 val(가변)이 아닌 val(불변)으로 프로퍼티를 선언하면 불변을 보장할 수 있을까?


2. copy() 메서드의 함정: 얕은 복사(Shallow Copy)

데이터 클래스는 equals, hashcode, toString 메서드뿐만 아니라 copy 메서드도 자동으로 생성해준다.

copy 메서드는 기존 객체의 모든 속성을 복사해 새로운 객체를 생성하는데, 이때 원하는 속성만 변경해서 생성할 수도 있다.

 

하지만 이 과정에서 얕은 복사(Shallow Copy)때문에 의도치 않게 불변성이 깨질 수 있다.

copy()는 객체 자체는 복사하지만, 객체 내부에 포함된 참조 타입(Reference Type) 프로퍼티는 그 주소만 복사하기 때문에, 원본과 복사본이 같은 가변 객체를 공유하게 된다.

 

data class Employee(val name: String, val roles: MutableList<String>)

fun main() {
    val original = Employee("성민", mutableListOf("Java-Developer"))
    val duplicate = original.copy() // 얕은 복사(Shallow Copy)

    duplicate.roles.add("Kotlin-Developer") // 복사본의 roles를 수정

    println(original)    
    // Employee(name=성민, roles=[Java-Developer, Kotlin-Developer])
    
    println(duplicate)   
    // Employee(name=성민, roles=[Java-Developer, Kotlin-Developer])
}

 

위 코드처럼, duplicate의 roles 리스트에 항목을 추가했더니 원본인 original의 roles도 변경되었다. 즉, 두 객체가 동일한 MutableList 가변 인스턴스를 공유하고 있기 때문이다.


✅ 결론: 데이터 클래스는 불변을 보장하지 않는다!

위 Employee 예시처럼, copy()는 val 프로퍼티의 값을 직접 변경하지는 않는다.

다만, 내부에 가변 객체(MutableList 등)가 포함되어 있을 경우, copy()가 원본과 복사본의 불변성을 동시에 깨뜨릴 수 있어 불변을 보장하지 않는다.

 

따라서 데이터 클래스는 가변 객체를 포함하지 않을 때만 불변성을 보장할 수 있다. copy()를 사용하여 불변 객체를 생성할 때는 가변 객체를 깊은 복사(Deep Copy)를 통해 새로운 인스턴스를 만들어야 한다.

 

data class Employee(val name: String, val roles: MutableList<String>)

fun main() {
    val original = Employee("성민", mutableListOf("Java-Developer"))
    val duplicate = original.copy(roles = original.roles.toMutableList()) // 깊은 복사 (Deep Copy)

    duplicate.roles.add("Kotlin-Developer") // 복사본의 roles를 수정

    println(original)    
    // Employee(name=성민, roles=[Java-Developer])
    
    println(duplicate)   
    // Employee(name=성민, roles=[Java-Developer, Kotlin-Developer])
}

 


참고