문제
kotlin에서는 toString(), equals(), hashCode()를 편하게 사용하기 위해 data class를 많이 사용한다.
또한, 어떤 객체에 정적 팩토리 메서드를 추가하면, 보통 생성자는 private 처리하는 경우가 많다.
다음은 data class + 정적 팩토리 메서드 + private 생성자를 적용한 예제이다.
data class Customer private constructor(
val id: String,
val name: String,
) {
companion object {
fun of(id: String, name: String): Customer {
verifyIdFormat(id)
return Customer(id, name)
}
private fun verifyIdFormat(id: String) =
require(isCustomerIdFormat(id)) { "Invalid customer id: $id" }
private fun isCustomerIdFormat(id: String) =
Regex("^[a-z0-9]+$").matches(id)
}
}
그런데 이렇게 코드를 작성하면 "private constructor" 코드에서 다음과 같은 warning이 발생한다.
Private primary constructor is exposed via the generated 'copy()' method of a 'data' class.
원인
저런 warning이 발생하는 이유는 data class는 사실 copy()라는 메서드도 자동으로 생성하기 때문이다.
copy() 메서드는 어떤 객체를 복사할 때 사용할 수 있고, 복사하면서 일부 데이터를 변경할 수 있다.
val customer1 = Customer.of("123", "han")
val customer2 = customer1.copy(name = "kim")
그런데, copy()는 내부적으로 객체의 주 생성자를 호출하게 되는데,
앞서 주 생성자를 private으로 지정했기 때문에, "private 생성자로 지정했어도 어차피 copy()에 의해 생성자 노출될거야" 정도의 warning이라고 보면 된다.
copy() 메서드를 사용한 코드는 문제 없이 돌아가는 것처럼 보일 수 있지만,
예제와 같은 정적 팩토리 메서드를 적용했다는 것은 Customer를 생성할 때, id가 포맷에 맞는지 검증하고 객체를 생성하고 싶다는 것인데,
copy()를 사용하면 이러한 검증 로직을 수행하지 않고, Customer 객체를 생성할 수 있어서 코드가 의도한 대로 동작하지 않을 수 있다.
해결
그럼 이 문제를 해결할까?
copy() 메서드가 정적 팩토리 메서드를 호출하도록 할 수 없나?
data class가 copy() 메서드를 생성하지 않도록 할 수 없나?
-> 아쉽게도 현재로서는 방법이 없다.
다른 방법을 찾아보자면,
1. 정적 팩토리 메서드를 없애고, public 생성자 + init 블럭 조합을 사용.
data class Customer(
val id: String,
val name: String,
) {
init {
verifyIdFormat(id)
}
private fun verifyIdFormat(id: String) =
require(isCustomerIdFormat(id)) { "Invalid customer id: $id" }
private fun isCustomerIdFormat(id: String) =
Regex("^[a-z0-9]+$").matches(id)
}
2. data class를 사용하지 않고, 직접 toString(), equals(), hashCode()를 적용.
class Customer private constructor(
val id: String,
val name: String,
) {
companion object {
fun of(id: String, name: String): Customer {
verifyIdFormat(id)
return Customer(id, name)
}
private fun verifyIdFormat(id: String) =
require(isCustomerIdFormat(id)) { "Invalid customer id: $id" }
private fun isCustomerIdFormat(id: String) =
Regex("^[a-z0-9]+$").matches(id)
}
override fun toString(): String {
return "Customer(id='$id', name='$name')"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Customer
if (id != other.id) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
return result
}
}
3. 기다리는 방법
기다려라? 이게 무슨 말이지..?
-> 사실 이 이슈는 오래 전부터 논의되고 있었고, 많은 kotlin 개발자들이 불편함을 겪고 있었는듯 하다.
https://youtrack.jetbrains.com/issue/KT-11914
최근 Kotlin 2.0.20-Beta2 버전에서 data class가 생성하는 copy() 메서드를 data class의 주 생성자와 visibility가 동일하도록 수정해줬다.
정확히는, 2.0.20에서는 non-public한 주 생성자를 가진 data class의 copy() 메서드를 사용하면 warning 메시지를 띄어주기 까지만 하고, 향후 점진적으로 아얘 copy() 사용도 못하도록 막으려는 것 같다.
아무튼 2.0.20은 정식 release 버전은 아니지만, kotlin에서도 이를 인지하고 개선해주고 있으니,
앞으로 data class + 정적 팩토리 메서드 + private 생성자 조합을 편하게 사용할 수 있는 날이 오겠거니 싶다.
-> kotlin 버전이 업그레이드되면서 이 글이 지워지는 날이 오면 좋겠다.
'kotlin' 카테고리의 다른 글
[Kotlin] JPA 플러그인 정리 (0) | 2024.07.19 |
---|---|
[Kotlin] 추가적으로 알아두어야 할 코틀린 특성 (0) | 2023.06.20 |
[Kotlin] 코틀린에서의 FP (0) | 2023.06.04 |
[Kotlin] 코틀린에서의 OOP (0) | 2023.05.07 |
[Kotlin] 코틀린에서 코드를 제어하는 방법 (4) | 2023.05.07 |