Skip to content

왜 var로 선언된 property는 private set이 안 될까?

이진혁 edited this page Jun 4, 2021 · 7 revisions

개요

코틀린에서는 var로 선언된 property(프로퍼티)에 대해 setter를 막는 private set 기능을 제공하지 않습니다.
스프링 + 코틀린 환경에서 개발하다보면 var로 선언된 프로퍼티의 setter를 막아야 하는 경우가 존재합니다.
하지만 왜 코틀린에서는 프로퍼티의 private set 기능을 제공하지 않을까요?

왜 var로 선언된 프로퍼티에 대해 private set 기능이 필요한가요?

이에 대한 예제는 JPA를 사용하기 위한 Entity를 구성하는데 있어서 발생합니다.
처음 코틀린으로 엔티티를 작성하면 보통 다음과 같이 작성하게 됩니다.

@Entity
@Table(name = "person")
class Person(

    @Column(name = "name")
    val name: String,

    @Column(name = "age")
    val age: Int,
) {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    var id: Long? = null
}

근데 이렇게 엔티티로 생성한 Person 객체의 상태를 변경하려면 어떻게 해야 할까요?
nameage 프로퍼티는 val로 선언했으니 setter 없이 getter만 존재하게 됩니다.
그래서 Person 객체의 상태를 변경할 수가 없습니다.

그렇다고 var로 선언하자니 아무 의미 없는 setter가 생겨 JPA의 규칙에 어긋납니다.
그럼 varprivate set을 해야 하는데 다음 코드는 컴파일 에러가 발생합니다.

@Entity
@Table(name = "person")
class Person(

    @Column(name = "name")
    var name: String
        private set,        // compile error

    @Column(name = "age")
    var age: Int
        private set,        // compile error
) {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    var id: Long? = null
}

제목에서 말했듯이 코틀린에서는 프로퍼티에 대해 private set 기능을 제공하지 않습니다.
그러면 실제로 개발할 때는 어떻게 우회하여 해결할까요?

해결책

1. 실제 선언을 클래스의 body에 두어 필드로 관리하기

@Entity
@Table(name = "person")
class Person(
    name: String,
    age: Int,
) {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    var id: Long? = null

    @Column(name = "name")
    var name = name
        private set

    @Column(name = "age")
    var age = age
        private set

    fun eatRiceCakeSoup() {
        age++
    }
}

본 해결책은 제가 개발할 때 사용하는 방식으로 가장 합리적으로 우회했다고 생각하는 방식입니다.
하지만 생성자에도 변수를 작성해야하고, 클래스의 바디에도 작성해야 하기 때문에
단순한 엔티티가 길어지는 문제점을 가지고 있습니다.

2. 변경시 새로운 엔티티를 반환하기

@Entity
@Table(name = "person")
class Person(

    @Column(name = "name")
    val name: String,

    @Column(name = "age")
    val age: Int,
) {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    var id: Long? = null

    fun eatRiceCakeSoup(): Person {
        val person = Person(
            name = name,
            age = age + 1,
        )
        person.id = id
        return person
    }
}

이렇게 엔티티를 변경할 때마다 새로운 엔티티를 생성하게 되면 불변성 유지도 되고
setter를 막을 수도 있지만, JPA의 더티 체킹 기능을 사용할 수 없다는 최대의 문제점이 있습니다.
더티 체킹을 이용하지 않으면 엔티티가 변경될 때 UPDATE문을 날리기 위해서
엔티티의 변경마다 save() 메소드를 호출해야 합니다.
이는 로직을 더럽히는데 주축을 담당하게 될 수도 있습니다.

3. Backing Field 사용하기

Backing Field를 사용하는 방법도 존재하지만 사용하지 않는 것을 추천합니다.

그래서 코틀린에서는 왜 프로퍼티에 private set 기능을 제공 안 하나요?

아래 참조한 문서들을 살펴보면 코틀린 공식 사이트에서 왜 안되게 해두었는가를 두고 싸우고 있는 것을 볼 수 있습니다.
owner 키워드를 제공하여 private set인 프로퍼티를 만들 수 있게 해야한다는 주장도 있고
다른 개발자들도 위 해결책-1을 이용하여 해결하는 것 같으며,
코틀린의 공식 입장도 없기 때문에 딱히 이유는 없는 것 같습니다.
추후에 업데이트를 통해 해결되었으면 좋겠습니다.

참조

Private setter for var in primary constructor
How to use custom setter in Kotlin class constructor body
In kotlin, how to make the setter of properties in primary constructor private?