티스토리 뷰

처음 자바를 배울때만해도 getter, setter 는 객체지향 언어인 java 의 캡슐화의 산물이었다. 하지만 개발자로써 경험을 하고, 공부해오면서 getter, setter 로는 충분한 캡슐화를 제공하기 어렵고, 특히 setter 는 캡슐화를 넘어 객체 상태를 변경하기때문에 더 주의깊게 사용하고자 하고있다.

 

객체를 조작하고, 코드를 작성하다보면 객체의 상태를 변경해야하는 경우도 발생하는데 이럴때도 단순히 setter 를 호출하기보다는 좀 더 의미있는 이름의 메서드를 제공하고자했고, 특히 2개 이상의 상태를 변경해야한다면 클라이언트 코드에서 연속적인 setter 를 호출하는것 보다는 필요한 상태를 모두 원자적으로 변경하는 메서드를 제공하는 형태의 코드를 작성하고있다.

 

- setter 를 이용한 상태 변경

public static void main(String[] args) {          
    Person person = personRepository.get();      
    person.setAge(32);                            
    person.setName("LichKing");                   
}                                                 
                                                  
static class Person {                             
    private int age;                              
    private String name;                          
                                                  
    public int getAge() {                         
        return age;                               
    }                                             
                                                  
    public void setAge(int age) {                 
        this.age = age;                           
    }                                             
                                                  
    public String getName() {                     
        return name;                              
    }                                             
                                                  
    public void setName(String name) {            
        this.name = name;                         
    }                                             
}                                                 

- 원자적으로 처리하는 상태 변경

public static void main(String[] args) {           
    Person person = personRepository.get();       
    person.update(32, "LichKing");                 
}                                                  
                                                   
static class Person {                              
    private int age;                               
    private String name;                           
                                                   
    public int getAge() {                          
        return age;                                
    }                                              
                                                   
    public String getName() {                      
        return name;                               
    }                                              
                                                   
    public void update(int age, String name) {     
        this.age = age;                            
        this.name = name;                          
    }                                              
}                                                  

 

아래코드가 위에코드에 비해 어떤 이점이 있는지는 여기서 논하지 않겠다. 이제 kotlin + spring + JPA 환경에서 위에서 추구한 방식들을 그대로 녹여보려고 시도한 몇가지 내용들에 대해 작성하고자한다.

 

먼저 entity 클래스를 하나 정의한다.

@Entity
@Table(name = "person")
class Person(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "person_id")
    val id: Long?,
    @Column(name = "person_age")
    val age: Int,
    @Column(name = "person_name")
    val name: String
)

생성자 내에서 필드들까지 정의했다. 코틀린이 제공해주는 편리한 문법 중 하나이다. 참고로 JPA 스펙에서는 entity 클래스에 대해 파라미터가 없는 기본 생성자를 정의해야한다고 명시하고있다. 그리고 위와 같이 필드들에 대해 초기화 구문이 없으면 코틀린은 기본 생성자를 만들지 않는다. 하지만 별다른 설정을 해주지않아도 코드가 잘 동작하는걸 볼 수 있는데, gradle 플러그인에 "kotlin-noarg" 가 설정되어 있는지 확인해보면 될 것이다. 플러그인 이름에서부터 알 수 있듯이 argument 없는 생성자를 자동으로 만들어준다.

apply plugin: "kotlin-noarg"
// or
plugins {
  id "org.jetbrains.kotlin.plugin.noarg" version "1.4.10"
}

혹시 noarg 가 없다면 "kotlin-jpa" 가 있는건 아닌지 확인해보자. "kotlin-jpa" 플러그인은 "kotlin-noarg" 를 래핑하고있으며, @Entity, @Embeddable, @MappedSuperclass 에 대해 기본 생성자를 생성하도록 설정되어있다.

 

다시 Person 엔티티로 돌아가서, 현재 age 와 name 은 val 로 정의되어 변경 불가능한 read-only 프로퍼티로 정의되어있다. 이 두 값을 변경할 수 있게 바꿔야한다면 어떻게 해야할까?

@Entity
@Table(name = "person")
class Person(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "person_id")
    val id: Long,
    @Column(name = "person_age")
    var age: Int,
    @Column(name = "person_name")
    var name: String
)

일단은 val 을 var 로 바꿔야한다. var 로 바꿔주면 자바로 치면 setter 를 제공한 셈이 되며, 손쉽게 상태를 변경할 수 있다.

@Test                                                              
fun `person 상태변경`() {                                              
    val person = personRepository.findByIdOrNull(1)!!              
    person.age = 32                                                
    person.name = "LichKing"                                       
}                                                                  

하지만 이렇게 변경하는건 의미있는 네이밍을 제공하는 메서드도 없게되고, 2개 필드를 원자적으로 바꾸지도 못한다. 상태를 변경해야하는 곳이 여러군데가 된다면 개발자의 실수를 유발할 수 있다. 원자적인 메서드를 제공해보자.

@Entity
@Table(name = "person")
class Person(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "person_id")
    val id: Long,
    @Column(name = "person_age")
    var age: Int,
    @Column(name = "person_name")
    var name: String
) {
    fun update(age: Int, name: String) {
        this.age = age
        this.name = name
    }
}

이제 age 와 name 을 원자적으로 변경할 수있다.

@Test                                                             
fun `person 상태변경`() {                                             
    val person = personRepository.findByIdOrNull(1)!!             
    person.update(32, "LichKing")                                 
}                                                                 

사실 여기서 끝내도되지만 내게 하나 남아있던 불만은 setter 가 숨겨지지 않았다는 것이다. update() 메서드를 통해 상태를 변경할 수 있지만, 직접 프로퍼티에 접근해 상태를 변경하는 코드도 여전히 컴파일이 되고 실행이 된다. 이걸 막을 수 있을까?

 

1. 프로퍼티를 private 으로 변경

@Entity
@Table(name = "person")
class Person(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "person_id")
    val id: Long,
    @Column(name = "person_age")
    private var age: Int,
    @Column(name = "person_name")
    private var name: String
) {
    fun update(age: Int, name: String) {
        this.age = age
        this.name = name
    }
}

가장 간단한 방법이다. 하지만 이렇게 할 경우 getter 까지 막히게된다. 상태변경 측면에서 필드를 읽기(read-only)만 하는 getter 가 setter 보다 나은건 맞으나 캡슐화 관점에서는 getter 역시 바람직하지는 않으므로 필드들을 private 으로 막는게 가장 간단하면서도 바람직해보이기는 한다. 하지만 getter 가 필요한 경우가 있을 수 있으므로 getter 는 살려두되 setter 는 막는 쪽으로 고민을 좀 더 해봤다.

 

2. setter 만 private

@Entity
@Table(name = "person")
class Person(
    age: Int, name: String
) {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "person_id")
    val id: Long? = null
    @Column(name = "person_age")
    var age: Int = age
    private set
    @Column(name = "person_name")
    var name: String = name
    private set
    
    fun update(age: Int, name: String) {
        this.age = age
        this.name = name
    }
}

필드를 생성자 내에 선언하지않고, 자바 클래스처럼 클래스 바디에 선언하게되면 property 에 대한 개별적인 접근제어자를 부여할 수 있다. 위 코드는 age, name 필드 내에 private set 이라는 구문을 추가했는데, 이렇게 해주면 setter 는 private 이 된다. 그럼 getter 는 공개하면서 setter 는 막는, 처음 의도했던 바를 구현할 수 있다.

 

하지만 이 방법은 2가지 문제가 있는데 첫번째 문제는 코드가 너무 장황해진다는 것이다. 코틀린의 장점 중 하나는 자바의 보일러 플레이트 코드를 상당수 제거해주는 것인데 위 방식은 생성자에 파라미터들도 직접 넣어줘야하고 일일이 초기화 코드를 작성해줘야한다. 물론 이걸 감안해도 다른 곳에서 코틀린의 이점을 살릴 수 있겠지만 적어도 엔티티 클래스에서만큼은 이렇게 할거면 굳이 코틀린을 써야하나? 라는 생각이 들게되기도한다. 자바를 쓰면 그나마 롬복이라도 쓸 수 있는데 코틀린은 그것도 안되니 역설적으로 코드가 더 길어진다는 느낌을 받았다.

 

사실 두번째 문제가 훨씬 크리티컬한데 실제 저 코드는 컴파일 조차 되지않는다( 에러메세지: Private setters are not allowed for open properties ). 첫번째 문제를 감안하기로하고 쓰고싶어도 못 쓰는 코드라는 것이다. 컴파일 되지않는 이유는 무엇일까. 위에 noarg 플러그인을 잠깐 설명하기도했지만, kotlin+spring+jpa 같은 환경에서 개발환경을 구성하다보면 내가 모르는 영역에서 너무 많은 마법들이 일어난다. JPA 는 lazy 로딩과 같은 기법들을 적용하기위해 프록시 객체를 만들게 되는데 이 프록시 객체를 만드는 핵심 기술은 상속이다. 하지만 코틀린은 자바와 반대로 open 이라는 키워드를 붙여주지않는 이상 기본적으로 final class, final method 가 된다. 이를 해결하고자 젯브레인은 또 하나의 gradle 플러그인을 제공하는데 이게 "allopen" 플러그인이다. 이 플러그인으로 인해 property 에도 open 이 붙게되는데 open property 에는 private set 을 지정할 수가 없다. 플러그인의 마법이 없는 플래인 코틀린 코드를 확인해보자.

open class Person(
    age: Int, name: String
) {
    open var age: Int = age
    private set
    open var name: String = name
    private set
}

엔티티에서 나왔던 것과 동일한 에러 메세지가 노출된다. ( Private setters are not allowed for open properties )

 

이 문제를 해결하려면 엔티티 내의 프로퍼티를 private 으로 만들어야 하는데(private 프로퍼티는 open 을 하지않음), 이렇게 되면 다시 첫번째 해결법으로 회귀하게된다.

 

결론

또 다른 방법이 있을지는 모르겠지만 결국 이런 이슈들로 인해 setter 만 막는 방법은 끝내 찾지 못했다. 개인적으로 이 이슈를 확인해보면서 몇가지를 느꼈는데

 

첫번째는 코틀린이 자바에서 불편했던 부분들을 이런저런 신택스 슈가로 해결을 해주긴하는데 코틀린이 제시해주는 길로 강제한다는? 느낌을 받았다. 문법상 문제는 없지만 코틀린이 제시해주는 길과 다른 방향으로 가게되면 코드양이 급격하게 늘기때문에 그 기로에서 굉장히 고민되는 경우를 몇번 만난 것 같다.

 

두번째는 JPA 랑 코틀린이랑 궁합이 안 맞는다는걸 여러가지로 느꼈다. 추후 코틀린용 ORM 이 대중화될지는 잘 모르겠지만..

 

스프링 플러그인 링크로 이 포스팅을 마친다.

 

참고

- gradle plugin: https://kotlinlang.org/docs/reference/compiler-plugins.html

 

Compiler Plugins - Kotlin Programming Language

 

kotlinlang.org

 

댓글
  • 프로필사진 Joshua 결론이 상당히 공감되네요 ㅎㅎ 전 직장에서 옆팀이 JPA대신 Exposed 쓰던게 생각납니다. 2020.09.18 12:43
  • 프로필사진 xephysis 아.. 뭐 찾다보면 결국 이쪽으로 오게 되는군요 ㅎㅎㅎ
    kotlin 이랑 jpa 랑 좀 삐걱거린다는 느낌을 지울수가 없어요(제가 아직 잘 못써서 그렇겠지만요...)
    2021.10.04 00:10
댓글쓰기 폼