[Kopring] 코프링에게 JPA Entity 란

최동근·2023년 5월 24일
2

안녕하세요 오늘은 일명 코프링 에서 JPA Entity 를 사용하는 방법에 대해 알아보겠습니다 ❗️

🧠 Kotlin 그리고 JPA Entity

현재 진행중인 Kafka 를 이용한 Multi-Module 심사 대출 프로젝트 를 진행하다가 Java 로 스프링을 개발하면서 사용했던 관습들이 Kotlin 을 이용한 스프링에서 문제가 발생할 수 있다는 점을 알게 되었습니다.
특히 Domain 에서 Entity 를 설계하는 도중 많은 궁금중이 생겨 이번 포스팅을 통해 정리해보려고 합니다 ❗️

해당 포스팅에서는 Parent EntityChild Entity 를 예시로 들어보겠습니다.

🧠 필수적 매개변수 & 선택적 매개변수

클래스 생성자에 전달되는 매개변수에도 필수적 매개변수선택적 매개변수 로 나뉠 수 있다.
간단하게 이야기해서 필수적 매개변수란 default 값이 없는 매개변수이고 선택적 매개변수란 default 값이 존재하는 매개변수입니다.

Kotlin 을 이용한 JPA Entity 설계에서 필수적 매개변수는 상단에 선택적 매개변수는 하단에 위치하도록 합시다 ❗️

@Entity
class Parent(
	var name: String // 필수적 매개변수
    var age: Integer // 필수적 매개변수
    var weight: Double? = null // 선택적 매개변수
) {
	@Id@GeneratedValue(strategy = GenerationValue.IDENTITY)
    var id: Long? = null 
}

🧠 var/val 는 개발자의 판단과 상황으로 결정하자

Entity 는 변경 가능성이 많은 클래스이지만 동시에 무분별한 변경은 방지해야합니다.
그렇다면 프로퍼티는 var/val 중 어떤 것으로 설계해야할까요?

답은 개발자의 판단과 상황에 따라 다르다입니다 😅
하지만 일반적으로 Entity class 는 var 를 사용하여 가변 프로퍼티를 선언하는 것이 일반적입니다. 이는 JPA 에서 Entity class 상태를 변경할 수 있는 기능을 활용하기 위한 선택입니다.

하지만 때로는 val 를 사용해서 불변 프로퍼티를 선언하기도 하는데, 불변 프로퍼티는 말 그대로 한번 값을 지정하면 수정을 하지 못하기에 만약 개발자가 판단했을 때 해당 프로퍼티의 변경 가능성이 없다고 생각했을 때는 안전하게 val 를 사용하는 것이 좋습니다.

개인적인 생각으로는 var 를 선언해서 가변성을 챙기는 동시에 무분별한 변경을 막기 위해 protected setter 를 설정하는 것이 가장 Best 한 선택인것 같습니다.
protected setter 에 대해서는 밑에서 다룰 예정이며 해당 궁금중에 대해 실제로 Chat GPT 에게 여쭈어보았습니다(?)

나: 그렇다면 Entity 에서 var 를 기본적으로 설정하고 setter 를 protected 로 설계해서 무분별한 변경을 막으면 어떨까?

GPT 님 : Entity 클래스에서 var를 기본적으로 사용하고 setter를 protected로 설정하여 무분별한 변경을 막는 것은 일반적으로 좋은 방법입니다. 이렇게 설계함으로써 명시적으로 접근 제한을 설정하고, 불변성과 데이터의 일관성을 유지할 수 있습니다❗️
일반적으로 Entity 클래스의 프로퍼티는 변경이 필요한 경우가 많기 때문에 var를 사용하는 것이 일반적입니다. 그러나 모든 프로퍼티를 외부에서 자유롭게 변경할 수 있도록 허용하면 무분별한 변경이 발생할 수 있고, 데이터의 무결성을 유지하기 어려울 수 있습니다.

결론 -> var + protected setter 사용하자. 단 특정 프로퍼티는 전혀 변경 가능성이 없다면 val 를 사용하자.

🧠 JPA Entity 는 기본 생성자가 필요하다

Spring 프레임워크를 이용해본 사람이라면 Entity 를 구현할 때 기본 생성자가 필수로 필요하다는 것을 알고 있어야합니다. Reflection API 때문인데요.
Reflection API 를 통해 런타임시에 데이터베이스에 있는 데이터를 해당하는 클래스 프로퍼티에 주입할 수 있습니다. (참고)

Java 를 이용한 Spring 에서는 NoArgsConstructor 을 통해 기본 생성자를 손쉽게 구현할 수 있었습니다. 하지만 Kotlin 에서는 Lombok 사용을 하지 못합니다.
Kotlin 에서 lombok 사용을 하지 못하는 이유 을 참고해주세요 ❗️
그런데 여기서 문제가 발생합니다. JPA Entity 는 데이터베이스 테이블 칼럼과 매핑되는 프로퍼티를 가지는 객체이기 때문에 해당 프로퍼티를 매개변수로 가지는 생성자를 가지게 되고 자연스럽게 기본 생성자를 가지지 못합니다.

여기서 우리는 Kotlin 에 별도의 플러그인plugin.jpa을 추가합니다.

plugin {
	kotlin("plugin.jpa") version "1.6.0"
}

해당 플로그인은 @Entity 를 사용하면 적용이 되며 사실 Spring Initializer 를 통해 개발을 한다면 자동으로 추가되는 플러그인입니다👍

🧠 Kotlin Class 는 기본적으로 final 이다.

Kotlin Class 는 기본적으로 final 키워드를 포함합니다.
객체 지향 관점에서 상속 은 굉장한 기능입니다. 하지만 무분별한 상속 은 오히려 악영향을 초래할 수 있습니다.
간단하게 이야기 하면 상속 기반이 되는 클래스가 변경이 빈번한 경우 해당 클래스를 기반으로 상속이 잦은 경우 취약한 기반 클래스(fragile base class) 문제에 직면할 수 있습니다 🥲
따라서 Java 와 달리 코틀린에서는 따로 명시하지 않으면 final 이 붙은 형태의 Class 가 정의됩니다. 참고

만약 상속을 하기에 적합한 클래스라면 open 키워드를 붙여 상속을 가능케 할 수 있습니다. 이를 통해 Kotlin 이 얼마나 잘 설계된 언어인지 이해할 수 있습니다.
하지만 Kotlin 을 이용해 JPA Entity 를 설계시 오히려 이는 문제가 될 수 있습니다 ❗️

잠시 JPA Entity 에 대해 떠올려 볼까요?
JPA 에서 Proxy 라는 개념은 굉장히 중요합니다.
Proxy는 실제 Entity 를 상속한 클래스이며 실제로 해당 객체가 사용될 때 데이터베이스에 접근하기 위해 사용됩니다. 이는 비교적 느린 데이터베이스의 접근을 최소화 혹은 메모리 공간을 효율적으로 사용할 수 있게 합니다 🔥

하지만 앞서 이야기 했다 싶이 Proxy 에 대상이 되는 Entity 클래스는 상속이 가능해야 하지만 Kotlin을 이용한 Class 는 상속이 불가합니다 🚫

그렇다면 모든 Entity Class 에 open 키워드를 붙여줘야 할까요?
물론 가능합니다만 우리는 plugin.spring 플러그인을 통해 일괄적으로 open 키워드를 추가할 수 있습니다.

plugin {
	kotlin("plugin.spring") version "1.6.10"
}

해당 플러그인 역시 앞서 봤던 plugin.jpa 와 마찬가지로 Spring Intializer 을 사용하여 프로젝트를 진행한다면 자동으로 추가가 됩니다 👍

🧠 Data Class 를 이용한 JPA Entity 설계는 금지

Kotlin 에서는 Data Class 라는 Class 형태를 제공합니다.
Data Class 란 데이터를 저장하고 전달하기 위한 간단한 클래스입니다.
해당 클래스는 기본 기능을 자동으로 제공하여 간결하고 효율적인 코드를 설계할 수 있습니다.
Data Class 가 제공하는 기본적인 기능에는 toString(),equals(),hashCode() 그리고 copy() 를 제공합니다.

여기서 특히 copy() 는 객체의 불변성을 유지하기 위해 제공합니다.
불변성은 객체의 안정성을 보장하기 때문에 굉장히 중요한 성질입니다.

Data Class 는 몇가지 특징을 가집니다 👨‍💻

  • 1개 이상의 생성자 프로퍼티를 가져야합니다.
  • 상속이 불가합니다.
  • 생성자 프로퍼티에 정의된 프로퍼티에 대해서만 기본 기능을 제공합니다.
  • 불변성을 제공합니다.
  • 모든 생성자 프로퍼티에는 val 또는 var 로 선언해야 합니다.
  • abstract, open, sealed, inner 같은 키워드 사용이 불가합니다.

그렇다면 이러한 데이터 중심의 성격을 지닌 Data Class 와 JPA Entity 가 찰떡궁합이라고 생각할 수 있습니다. ❗️

하지만, 아쉽게도 JPA Entity Class 에 Data Class 는 사용하지 못합니다.
왜그럴까요? 여기에는 여러가지 이유가 있습니다.

  1. Data class 는 상속이 불가하다.

    이부분은 앞과 동일한 원리입니다.

  2. Data class 는 불변성을 유지하는 것을 지향한다.

    하지만 Entity 는 상태를 변경하는 경우가 있습니다. 이때 Data class 의 이러한 특징은 문제가 될 수 있습니다.

  3. JPA Entity 에 대해 요구되는 여러가지 요구사항을 Data class 가 반영하기 힘들다.

    Data class 는 키워드 제약이 많기 때문에 다양한 성격의 JPA Entity 로 사용하기에 여러가지 제약이 있습니다.

  4. 엔티티간 동등성 문제 발생

    Data classequals()hashCode() 를 통해 자동으로 객체의 동등성을 검사합니다. 따라서 이때문에 만약 Entity 를 Data class 로 설계한다면 엔티티 식별에 문제가 생길 수 있습니다. -> 프로퍼티가 모두 같다면 같은 엔티티로 인지하는 오류 발생 가능성 O

🧠 Setter

JPA Entity 는 데이터베이스의 데이터 변경성을 최소화하기 위해 Setter 를 노출시키는것을 지양해야 하며 또한 이것이 관행입니다.
따라서, Java 를 이용한 Spring 에서는 Setter 를 캡슐화하고 특정 필드만을 수정할 수 있는 즉 수정의 의도가 명확한 public 메소드를 설계하는 편입니다.

그렇다면, Kotlin 은 프로퍼티를 선언시 자동으로 Getter/Setter 가 생기는데 Entity 에서 Getter/Setter 를 어떻게 설계해야할까요? 🤔

여기에는 사용할 수 있는 여러가지 옵션이 있습니다.

1. 프로퍼티를 private 으로 설정

@Entity
public class Parent(
	private var name: String,
    private var age:  Int 
    
) {

	@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null
}

얼핏 보았을때는 해당 방법이 능사인것 같으나, 프로퍼티 자체를 private으로 선언해 버리면 Getter 또한 캡슐화가 되기 때문에 의도했던 기능을 하지 못할 수 있습니다.

2. Setter 를 private 으로 설정

@Entity
public class Parent (
	name: String,
    age: Int
) {
	@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null
    
    val name: String = name
    	private set // private setter
    
    val age: Int = age
    	private set // private setter
        
    fun modifyProperties(name: String, age: Int) {
    	this.name = name
        this.age = age
    }
 }

해당 방법은 Compile Error 를 초래합니다 🥲
그이유가 뭘까요? 앞서 살펴보았듯이, Spring Initializer 를 통해 코프링 프로젝트를 진행하면 두개의 플러그인이 자동으로 주입됩니다. 그 중 하나가 plugin.spring 이였는데 해당 플러그인은 모든 클래스에 open 키워드를 자동으로 추가해줌으로써 JPA Entity 의 Proxy 기능을 허용합니다.

이렇게 추가된 키워드는 클래스 뿐만 아니라 클래스 내부에 프로퍼티에도 적용됩니다.
open 이 프로퍼티에 추가된다는 것은 private setter 를 선언할 수 없음을 의미합니다 🤔

3. Setter 를 protected 으로 설정

protected 는 open 프로퍼티에도 적용할 수 있는 접근제어지시자입니다.
또한 public 하게 setter가 노출되는 것을 방지합니다.

  • Kotlin 에서 protected 접근 제어 지시자의 범위
    • protected 가 선언된 프로퍼티는 해당 클래스를 상속하는 하위 클래스에서 접근 가능하다.
    • 동일한 패키지 내의 접근 가능하다. 여기서 동일한 패키지란 파일 시스템에서의 패키지 구조를 기준으로 정의된다.

@Entity
class Parent(
   name: String,
   age: Int
){
	@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
   val id: Long? = null

   val name: String = name
       protected set // protected setter

   val age: Int = age
       protected set // protected setter

   fun modifyProperties(name: String, age: Int) {
       this.name = name
       this.age = age
   }
}

하지만 이방법 또한 Best 한 방법은 아닙니다. 왜냐하면 protected로 접근 가능하다면 무분별하게 Setter를 사용할 수 있기 때문입니다.
코프링에서는 Java 를 이용한 Spring 처럼 @Setter 를 단순히 사용만 하지 않으면 무분별한 setter 를 방지할 수 있는 방법이 아직 없는 것 같습니다.
따라서, 코프링에서는 여전히 개선해야할 부분이 존재하는 것 같습니다 ❗️


참고

코틀린에서 하이버네이트를 사용할 수 있을까?
Kotlin + Spring 시작하기(3) - Entity
Kotlin JPA 사용하기
[Kotlin] 코틀린 Data Class란

profile
비즈니스가치를추구하는개발자

0개의 댓글