안녕하세요 오늘은 일명 코프링
에서 JPA Entity 를 사용하는 방법에 대해 알아보겠습니다 ❗️
현재 진행중인 Kafka 를 이용한 Multi-Module 심사 대출 프로젝트 를 진행하다가 Java 로 스프링을 개발하면서 사용했던 관습들이 Kotlin 을 이용한 스프링에서 문제가 발생할 수 있다는 점을 알게 되었습니다.
특히 Domain 에서 Entity 를 설계하는 도중 많은 궁금중이 생겨 이번 포스팅을 통해 정리해보려고 합니다 ❗️
해당 포스팅에서는 Parent Entity
와 Child 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
}
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
를 사용하자.
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 키워드를 포함합니다.
객체 지향 관점에서 상속
은 굉장한 기능입니다. 하지만 무분별한 상속
은 오히려 악영향을 초래할 수 있습니다.
간단하게 이야기 하면 상속 기반이 되는 클래스가 변경이 빈번한 경우 해당 클래스를 기반으로 상속이 잦은 경우 취약한 기반 클래스(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
을 사용하여 프로젝트를 진행한다면 자동으로 추가가 됩니다 👍
Kotlin 에서는 Data Class
라는 Class 형태를 제공합니다.
Data Class
란 데이터를 저장하고 전달하기 위한 간단한 클래스입니다.
해당 클래스는 기본 기능을 자동으로 제공하여 간결하고 효율적인 코드를 설계할 수 있습니다.
Data Class
가 제공하는 기본적인 기능에는 toString()
,equals()
,hashCode()
그리고 copy()
를 제공합니다.
여기서 특히 copy()
는 객체의 불변성을 유지하기 위해 제공합니다.
불변성은 객체의 안정성을 보장하기 때문에 굉장히 중요한 성질입니다.
Data Class
는 몇가지 특징을 가집니다 👨💻
그렇다면 이러한 데이터 중심의 성격을 지닌 Data Class
와 JPA Entity 가 찰떡궁합이라고 생각할 수 있습니다. ❗️
하지만, 아쉽게도 JPA Entity Class 에 Data Class
는 사용하지 못합니다.
왜그럴까요? 여기에는 여러가지 이유가 있습니다.
Data class
는 상속이 불가하다.
이부분은 앞과 동일한 원리입니다.
Data class
는 불변성을 유지하는 것을 지향한다.
하지만 Entity 는 상태를 변경하는 경우가 있습니다. 이때 Data class
의 이러한 특징은 문제가 될 수 있습니다.
JPA Entity 에 대해 요구되는 여러가지 요구사항을 Data class
가 반영하기 힘들다.
Data class
는 키워드 제약이 많기 때문에 다양한 성격의 JPA Entity 로 사용하기에 여러가지 제약이 있습니다.
엔티티간 동등성 문제 발생
Data class
는 equals()
와 hashCode()
를 통해 자동으로 객체의 동등성을 검사합니다. 따라서 이때문에 만약 Entity 를 Data class
로 설계한다면 엔티티 식별에 문제가 생길 수 있습니다. -> 프로퍼티가 모두 같다면 같은 엔티티로 인지하는 오류 발생 가능성 O
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가 노출되는 것을 방지합니다.
@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란