danuri
오늘의 기록
danuri
전체 방문자
오늘
어제
  • 오늘의 기록 (307)
    • java (150)
      • java (33)
      • spring (63)
      • jpa (36)
      • querydsl (7)
      • intelliJ (9)
    • kotlin (8)
    • python (24)
      • python (10)
      • data analysis (13)
      • crawling (1)
    • ddd (2)
    • chatgpt (2)
    • algorithm (33)
      • theory (9)
      • problems (23)
    • http (8)
    • git (8)
    • database (5)
    • aws (12)
    • devops (10)
      • docker (6)
      • cicd (4)
    • book (44)
      • clean code (9)
      • 도메인 주도 개발 시작하기 (10)
      • 자바 최적화 (11)
      • 마이크로서비스 패턴 (0)
      • 스프링으로 시작하는 리액티브 프로그래밍 (14)
    • tistory (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

인기 글

태그

  • Kotlin
  • JPA
  • Saving Plans
  • RDS
  • 도메인 주도 설계
  • Database
  • 자바 최적화
  • gitlab
  • nuribank
  • S3
  • AWS
  • 등가속도 운동
  • Spring
  • Security
  • PostgreSQL
  • DDD
  • ChatGPT
  • reactive
  • 트랜잭션
  • POSTGIS
  • CICD
  • SWAGGER
  • Bitmask
  • Java
  • Jackson
  • docker
  • connection
  • 마이크로서비스패턴
  • Thymeleaf
  • mockito

최근 댓글

최근 글

hELLO · Designed By 정상우.
danuri

오늘의 기록

kotlin

[Kotlin] Private primary constructor is exposed via the generated 'copy()' method of a 'data' class.

2024. 7. 20. 11:43

문제

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

 

Confusing data class copy with private constructor : KT-11914

When the primary constructor of data classes is private, copy() probably should be private too? Otherwise you can create confusing things like this: data class User private constructor(val name: String, val id: Int){ companion object{ fun of(name:String, i

youtrack.jetbrains.com

 

최근 Kotlin 2.0.20-Beta2 버전에서 data class가 생성하는 copy() 메서드를 data class의 주 생성자와 visibility가 동일하도록 수정해줬다.

https://kotlinlang.org/docs/whatsnew-eap.html#data-class-copy-function-to-have-the-same-visibility-as-constructor

 

What's new in Kotlin 2.0.20-Beta2 | Kotlin

 

kotlinlang.org

 

정확히는, 2.0.20에서는 non-public한 주 생성자를 가진 data class의 copy() 메서드를 사용하면 warning 메시지를 띄어주기 까지만 하고, 향후 점진적으로 아얘 copy() 사용도 못하도록 막으려는 것 같다.

 

아무튼 2.0.20은 정식 release 버전은 아니지만, kotlin에서도 이를 인지하고 개선해주고 있으니,

앞으로 data class + 정적 팩토리 메서드 + private 생성자 조합을 편하게 사용할 수 있는 날이 오겠거니 싶다.

-> kotlin 버전이 업그레이드되면서 이 글이 지워지는 날이 오면 좋겠다.

 

저작자표시 비영리 동일조건 (새창열림)

'kotlin' 카테고리의 다른 글

[Kotlin] @JvmOverloads - 생성자/함수 손쉽게 오버로딩  (0) 2025.02.23
[Kotlin] JPA 플러그인 정리  (0) 2024.07.19
[Kotlin] 추가적으로 알아두어야 할 코틀린 특성  (0) 2023.06.20
[Kotlin] 코틀린에서의 FP  (0) 2023.06.04
[Kotlin] 코틀린에서의 OOP  (0) 2023.05.07
    'kotlin' 카테고리의 다른 글
    • [Kotlin] @JvmOverloads - 생성자/함수 손쉽게 오버로딩
    • [Kotlin] JPA 플러그인 정리
    • [Kotlin] 추가적으로 알아두어야 할 코틀린 특성
    • [Kotlin] 코틀린에서의 FP
    danuri
    danuri
    IT 관련 정보(컴퓨터 지식, 개발)를 꾸준히 기록하는 블로그입니다.

    티스토리툴바