본문 바로가기

Kotlin

코틀린과 Hibernate, CGLIB, Proxy 오해와 재대로된 사용법 - (4)

앞선 글에서 proxy 객체를 사용하기 위해 allOpen 플러그인을 사용하였습니다.

 

이 allOpen 을 조금 더 알아봅시다.

 

0. allOpen vs 그냥 open class

발단은 이거였습니다.

 

제가 이 주제로 글을 적기 전, 메모해놨던 것이 있는데 아래와 같이 적혀 있었습니다.

 

allOpen 플러그인을 적용하면서 기존 코드의 private set 을 사용하지 못함 -> protected set 을 고려해보자.

이를 시연해보기 위해 다음과 같이 생각했죠.

 

allOpen 이 open 클래스라고 했으니 그냥 open class 만들어서 재현해볼까?

 

open class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,
    val email: String,
    var picture: String
) {
    var name: String = ""
        private set
}

 

그러나 왠걸. private set 이 잘 되는겁니다.

 

그래서 allOpen을 적용해보니 또 컴파일에러가 나는겁니다.

 

 

이런 고민은 stackOverflow 에서 쉽게 찾을 수 있었습니다.

https://stackoverflow.com/questions/45153998/what-is-an-open-property-why-i-cant-make-its-setter-to-private

 

What is an open property? Why I can't make its setter to private?

What is the difference between a Property and Open Property in Kotlin? The code below complains on me declaring the setter private and Intellij says private setters are not allowed for open propert...

stackoverflow.com

 

 

차이는 이거였습니다.

  • kotlin 에서는 클래스 뿐만 아니라 인터페이스, 어노테이션 클래스, enum, var 변수, 파라미터, mutable 프로퍼티를 제외하면 모두 final.
    • 즉, method 도 자동으로 final
    • 단, var 변수와 파라미터는 effectivily-final
  • 그리고 allOpen 이 붙은 클래스는, 클래스가 open 으로 바뀌는 것 뿐만 아니라 모든 프로퍼티와 메서드를 open 으로 만듭니다.
  • 반면 open class 는 클래스만 open.

 

  • private set 은 final 메서드에서만 쓸 수 있다.
  • allOpen 은 메서드조차 open 으로 만들기에, allOpen 에서만 private set 을 쓸 수 없었던것.

 

여기서 첫번째 문제가 발생합니다.

 

 

 

1. allOpen 을 사용하면 private set 을 사용하지 못함

위에서 본 그대로입니다.

 

물론 외부에서 set 에 접근하지 못하도록 만들고 싶지만, 기술적인 한계인 듯 합니다.

 

차선책으로는, 최대한 접근범위를 줄인

 

protected set 을 사용하는 방법 이 있을 수 있겠습니다.

 

(더 좋은 방법이 있을까요 .. ?)

 

 

 

 

이후로, allOpen 을 적용하며 겪은 몇가지 문제를 소개합니다.

2. @OneToMany 에 List<E> 를 못씀

결과를 먼저 말하자면, MutableList<E> 를 써야 합니다.

 

이를 알기 위해서는 List<E> 와 MutableList<E> 의 구현을 봐야 합니다.

 

public interface MutableList<E>

public interface List<out E>

 

차이가 보이시나요 ?

 

List<out E> 는 자바로 치면 List<? extends E> 와 같습니다.

 

제네릭을 공부해보면 알 수 있는 것들 중 하나는 제네릭이 불공변이라는 것입니다.

쉽게 말하면,
A 가 B 의 상위 클래스라도,
<A> 는 <B> 의 상위 클래스가 되지 않는다는 것입니다. (같이 변하지 않는다. 불공변.)

Effective Java 책에 자세해 나와 있습니다.

 

예를 들어 아래와 같은 코드가 있다 치면,

@OneToMany
val books: List<Book>

자바로 컴파일 될 때 아래와 같이 변하는 겁니다.

@OneToMany
private List<? extends Book> books;

 

 

뭔가 Book 을 상속한 하위 클래스 아무거나라고 해서 Book 처럼 써서 가능한거 아닐까 ? 라고 생각할 수 있지만,

 

불공변의 특성을 생각해보면 제네릭에서 <? extends Book> 은 <Book> 의 하위 클래스가 아니라는 겁니다.

 

따라서, Book 리스트를 써야 하는 자리에서 컴파일 에러가 나는 것이죠.

 

 

해결 방법은 MutableList<E> 를 사용하는 것입니다.

위의 구현을 보시면 아시겠지만, 불공변이고 뭐고 할 것 없이 그냥 <E> 이기 때문이죠.

 

 

3. inline 함수

allOpen 을 적용하였더니 inline 메서드에서도 에러가 발생하였습니다.

 

inline 메서드도 final 이 아니게 되었고, final 키워드를 명시적으로 붙여주었습니다.

 

 

 

 

4. data class 를 사용하지 말자.

https://github.com/spring-guides/tut-spring-boot-kotlin#persistence-with-jpa

 

위 링크의 중간을 보면 아래와 같은 문구가 나옵니다.

 

Here we don’t use data classes with val properties because JPA is not designed to work with immutable classes or the methods generated automatically by data classes. If you are using other Spring Data flavor, most of them are designed to support such constructs so you should use classes like data class User(val login: String, …​) when using Spring Data MongoDB, Spring Data JDBC, etc.

 

뭔가 해석하기가 어려웠지만, 해석해보면

 

JPA 에서 data class (val properties 를 가진) 를 사용하지 마라.

  • 불변 클래스 (data class) 와 JPA 가 같이 사용되도록 설계되지 않았다.
    • 앞 글에서 살펴본 proxy 문제
  • data class 로 자동생성된 메서드를 사용하도록 설계되지 않았다.
    • data class 에서 자동 생성된 toString(), equals() 메서드에서 순환참조가 일어날 수 있음
  • val properties 를 가진 data class 를 사용하지 마라
    • 이 내용이 이번에 새로 알게  된 내용인데, 아래에서 자세히 정리해 보겠습니다.

 

4-1. data class 와 val property

data class 가 자동 생성해주는 메서드 중 copy() 는 equals(), hashCode() 와는 달리 Object 클래스에 있는 메서드가 아닙니다.

 

data class 에서 만들어주는 메서드인데, data class 와 copy() 메서드를 이용한다면 val 변수도 바꿀 수 있습니다.

 

정확하게 말하면, val 인것과 상관없이 새로운 변수를 할당한 새로운 객체로 복사할 수 있습니다.

 

 

아래 코드를 봅시다.

fun main() {
    val p1 = Point(1, 2, 3)
    println(p1)
    
    val copiedP1 = p1.copy(id = 3)
    println(copiedP1)
}

data class Point(
    val id: Int,
    val x: Int,
    val y: Int
)

Point 라는 data class 가 있고, 모든 프로퍼티가 val 로 변경 불가능입니다.

 

그런데 id 를 바꾸고 싶다고 해 봅시다.

그럼 위와 같이 id 값만 새로 할당해서 기존 p1 을 복사하면 됩니다.

 

결과는 아래와 같습니다.

 

 

4-2. copy() 와 dirty check

위와 같이, val 프로퍼티의 변경을 copy() 를 통해 흉내낼 수 있었습니다.

 

하지만 이렇게 값을 변경하면, JPA 와 사용할때 문제가 생깁니다.

 

JPA 에서는 dirty check 이라는 것이 있습니다.

dirty check

같은 트랜잭션 내에서, 엔티티의 변경이 일어났을때 save() 와 같이 명시적으로 저장하는 메서드가 없어도
읽어왔을때의 스냅샷과 비교하여 엔티티 객체의 변경점을 확인하여
자동으로 영속성 컨텍스트에 update 쿼리가 날라가서 DB에 반영되는 기능.

 

 

하지만 copy() 를 이용하여 프로퍼티의 변화를 시도한 것은 흉내일 뿐, 결국 새로운 객체를 만든것에 불과합니다.

 

따라서 dirty check 이 발생하지 않고, 명시적인 save() 를 해줘야 합니다.

 

위의 data classes with val properties 는 해당 부분에서 발생할 수 있는 착각 때문에, JPA 에서는 이렇게 쓰도록 설계되지 않았다라고 언급하지 않았나 생각이듭니다.

 

결론적으로는 data class 를 사용하지 말고, 변경할 일이 있는 프로퍼티는 그냥 var 로 만드는게 좋겠습니다.

 

 

 

 

 

 

5. 다음 글 주제

다음 글이 시리즈의 마지막이 될 것 같습니다.

 

다음 글은 약간 번외인게, 이전 글들에서 kotlin 에서 entity 객체를 생성할때 기본생성자를 사용하지 않아도 됨을 알았습니다.

 

자바에서 기본 생성자가 꼭 필요했던 경우를 생각해보면, @RequestBody 에서 받는 객체도 기본 생성자가 필요했죠.

 

그럼 Kotlin에서는 어떨지 알아보도록 하겠습니다.