JPA Entity 심층탐구 (1) Entity에 Kotlin Data class를 써도 될까?

HEYDAY7·2022년 11월 21일
1

Learn Kotlin + Spring

목록 보기
24/25
post-custom-banner

시작하며

프로젝트를 돌아보기 시작하며 Entity부터 시작했다. 그래서 Entity에 대하여 기본 학습을 마치고 나서 처음부터 하나하나 뜯어보기 시작했다. 그러다가 Entity에 내가 아무 생각없이 data class를 쓰고있는 것을 발견하고, 그 이유를 찾기 위해 이 글을 쓰기 시작했다.

공부

Kotlin Data class

Data class란 무엇인가? kotlin에서 제공하는 class 이다. 가장 큰 특징이자 편의성이라하자면 기본적으로 제공하는 여러 method 들에 있다.

  • equals() / hashcode()
  • toString()
  • copy()

이렇게 기본적으로 제공하는 객체지향 method들 덕분에 내가 안드로이드 개발을 하던 시절에는 매우 애용했었다. 헌데 여러 자료들을 보다보니 JPA에서는 이 data class를 쓰는 것을 추천하지 않는다고 한다.

Hibernate(JPA 구현체)에서 요구하는 Entity

본 내용은 Hibernate 관련 문서를 참고했다.
Hibernate에서는 아래 항목들이 requirement로 나와있다.

  • @Entity annotation이 달려야 한다.
  • public or protected no-argument constructor가 필요하다.
  • final이면 안된다.(이는 java 기준으로 작성된 문서이며, java에서의 final은 불변의 의미를 갖는다.)
  • Identifier를 제공해야 하며, nullabe/non-primitive 인 것이 권고된다.
  • equals와 hashcode를 구현해야한다.

Entity에 Data class를 쓰면 안되는 이유

바로 앞서서 Entity가 어떻게 만들어져야(구성되야?) 되는지를 알아보았다. 그렇다면 왜 Data class를 쓰면 안되는 것인가?

Lazy Loading을 위해서

처음으로는 Lazy Loading을 위해서 이다. JPA 관련 글에서 찾아봤듯이 프록시 Lazy loading을 이용하면 관련된 다른 object를 필요한 시점에 불러와서 사용할 수 있다.
그런데 Hibernate의 문서를 보면 이런 문장이 있다.

You can still persist final classes that do not implement such an interface with Hibernate, but you will not be able to use proxies for fetching lazy associations, therefore limiting your options for performance tuning.
(출처는 여기다.)

즉 final class로도 코드가 돌아는 가지만 proxy를 통한 lazy loading은 할 수 없어서, 성능 향상의 한 도구를 사용할 수 없다고 한다.

여기서 Kotlin의 클래스와 프로퍼티, 함수는 기본적으로 final이라서 이 문제를 해결하기 위해서 공식 문서에서는 allopen 플러그인을 쓰라고 권장한다.
이러면 문제가 다 해결됐나 싶지만 결국 제일 마지막 해결할 수 없는 문제에 다다르게 된다.

Kotlin의 Data class는 결국 이 방식을 통해서도 open이 되지 않는다.

따라서 Data class를 쓰면 안된다.

equal, hashcode, toString 등 기본 제공 함수 때문에...

Kotlin data class를 만들면 equal, hashcode, toString 등 method를 기본으로 제공한다. 이는 매우 편리한 기능이지만, 단점을 갖는다.

equal, hashcode

이 두 기본 제공 method는 constructor 내부에 정의된 값에 대해서만 비교를 시행한다. 예시는 다음과 같다.

위 사진과 같이 Person data class를 만들었다고 생각해보자. 그리고 constructor에 포함되지 않는 var 변수가 하나 있다고 해보자. 이 경우 아래 3개의 pritln의 결과가 어떻게 나올까? 다음을 봐보자

처음에 동일했던 person1과 person2는 person2.age = 20 이후에도 이 두개를 동일하다고 판단하고 있다. 결국 앞서 언급했던 것 처럼 equal과 hashcode는 age에 대해서는 비교를 하지 않기 때문이다.

toString

toString 또한 data class가 기본적으로 제공하는 method이다. 이게 문제를 일으키는 순간은 N:1 relation ship이 쌍방향으로 연결될 때 이다. 아래 예시를 보자

@Entity
data class Book(	
    @OneToMany
    val pages: List<Page>
)

@Entity
data class Page(
	@ManyToOne
    val book: Book
)

위와 같은 두 entity가 있다고 해보자.(다른 여러 옵션들은 생략했다. relation 구조만 보자) 이때 하나의 Book Entity에서 toString이 호출되었다고 생각해보자. 이 경우 Book의 경우 pages attribute를 만들기 위해 Page의 toString을 호출했을 것이고, 또 Page를 구성하기 위해 book의 toString을 호출하고..... 이렇게 무한반복이 되다 결국 StackOverflowException이 터지게 된다.

내가 며칠전에 겪었던 문제도 결국 이거였던 것 같다... 이렇게 알게되서 너무 기쁘다.

그래서 기본 제공 함수는...?

위에 나온 문제들을 피하려면 해당 method들을 override 해서 상황에 맞게 사용해주면 된다. 그런데 data class를 사용하는 이유가 효율적이고 좋은 method들을 자동생성해주기 때문인데, 이를 모두 override 해야 한다면 결국 사용할 이유가 하나 준 것과 마찬가지이다.

결론

성능 향상을 위한 lazy loading도 활용이 불가능하고, data class를 선택하는 이유들 마저 퇴색되어버리는 것들을 보며 data class를 사용할 이유가 전혀 없다는 것을 깨닫게 된다.

마치며

Data class를 Entity로 사용하지 않아야 된다는 것을 깨달았으니 다음 순서는 명확하다. 현재 data class로 적어둔 entity들을 어떻게 변경시켜야 할 지 찾아보는 것이다.

참고자료

profile
(전) Junior Android Developer (현) Backend 이직 준비생
post-custom-banner

1개의 댓글

"Kotlin의 Data class는 결국 이 방식(All-open 플러그인)을 통해서도 open이 되지 않는다."라고 적혀 있습니다.
하지만 이는 사실이 아닙니다.
아마도 All-open 플러그인의 기본 대상 중에 javax.persistence.Entity 가 빠져있기 때문일 것 입니다.
관련해서는 https://wave1994.tistory.com/154 블로그를 참고하시기 바랍니다.

답글 달기