이번에 새로운 사이드 프로젝트를 진행하면서 Kotlin, JPA, Spring Boot를 활용하게 되었다. 관련 예제 코드를 찾아보니, Kotlin과 JPA로 작성된 Entity 클래스는 보통 아래와 같은 형태로 많이 작성된다.
@Entity
data class Article(
var slug: String = "",
var title: String = "",
var description: String = "",
var body: String = "",
@ManyToMany
val tagList: MutableList<Tag> = mutableListOf(),
var createdAt: OffsetDateTime = OffsetDateTime.now(),
var updatedAt: OffsetDateTime = OffsetDateTime.now(),
@ManyToMany
var favorited: MutableList<User> = mutableListOf(),
@ManyToOne
var author: User = User(),
@Id @GeneratedValue(strategy = GenerationType.AUTO)
var id: Long = 0
) {
fun favoritesCount() = favorited.size
}
Kotlin의 data class에서는 생성자에 var와 val 키워드를 사용하면, 생성자 파라미터와 클래스 변수를 동시에 선언할 수 있다. 이를 통해 작성해야 할 코드 양을 줄일 수 있는 장점이 있다. 그러나 이 방식은 엔티티의 값을 외부에서 쉽게 변경할 수 있는 가능성을 열어두기 때문에, 좋은 설계라고 볼 수 없다. 그렇다면, 어떻게 엔티티 클래스를 작성하는 것이 더 나은 방식일까?
var와 val의 차이점
Kotlin에서 var는 변수의 값을 변경할 수 있게 만들고, val은 초기화된 후 값을 변경할 수 없도록 만든다. 따라서, 변경되지 않아야 할 필드에는 val을 사용하고, 외부에서 변경이 가능한 필드는 var를 사용하게 된다.
변경이 불필요한 필드는 val을 사용하여 불변성을 유지하는 것이 좋지만, JPA Entity에서는 모든 필드를 val로 선언할 수는 없다. 예를 들어, 엔티티의 식별자(id)는 데이터베이스에서 자동 생성되므로, 코드에서 변경할 수 없게 막을 수 없다. 또한, JPA는 기본 생성자와 함께 동작하는 특성이 있으므로, data class보다는 일반 클래스를 사용하는 것이 더 적합할 때가 많다.
개선된 설계1: 불변성을 고려한 엔티티 작성
아래는 JPA와 Kotlin에서 엔티티 설계를 개선한 예시다. var를 최소화하고, setter는 외부에서 호출할 수 없도록 protected로 제한하여 불필요한 변경을 방지한다.
@Entity
class Article(
slug: String = "",
title: String = "",
description: String = "",
body: String = "",
@ManyToMany
val tagList: MutableList<Tag> = mutableListOf(),
createdAt: OffsetDateTime = OffsetDateTime.now(),
updatedAt: OffsetDateTime = OffsetDateTime.now(),
@ManyToOne
val author: User = User()
) {
var slug: String = slug
protected set
var title: String = title
protected set
var description: String = description
protected set
var createdAt: OffsetDateTime = createdAt
protected set
var updatedAt: OffsetDateTime = updatedAt
protected set
@ManyToMany
var favorited: MutableList<User> = mutableListOf()
protected set
@Id @GeneratedValue(strategy = GenerationType.AUTO)
var id: Long = 0
protected set
fun favoritesCount() = favorited.size
}
이 방식은 외부에서 함부로 필드 값을 변경하지 못하게 막으면서, 엔티티의 불변성을 유지할 수 있다. slug, title, description 등은 외부에서 접근은 가능하지만, 값을 변경할 수는 없다. 대신, 비즈니스 로직 내에서 값이 필요할 때만 변경할 수 있다. 하지만 val을 사용하는 변수만 클래스의 생성자에 두는 것은 코드의 일관성이 떨어지게 만들 수 있다 생각한다.
개선된 설계2: 모든 필드를 클래스 내부에서 선언하기
아래는 val과 var 모두를 생성자가 아닌 클래스 내부에 선언하여 관리하는 방식의 예시다. 이 방식은 코드의 구조를 더 명확하게 하고, 엔티티의 불변성을 강력하게 유지할 수 있다.
@Entity
class Article(
slug: String = "",
title: String = "",
description: String = "",
body: String = "",
tagList: MutableList<Tag> = mutableListOf(),
createdAt: OffsetDateTime = OffsetDateTime.now(),
updatedAt: OffsetDateTime = OffsetDateTime.now(),
author: User = User()
) {
var slug: String = slug
protected set
var title: String = title
protected set
var description: String = description
protected set
var body: String = body
protected set
@ManyToMany
val tagList: MutableList<Tag> = tagList
protected set
var createdAt: OffsetDateTime = createdAt
protected set
var updatedAt: OffsetDateTime = updatedAt
protected set
@ManyToMany
var favorited: MutableList<User> = mutableListOf()
protected set
@ManyToOne
var author: User = author
protected set
@Id @GeneratedValue(strategy = GenerationType.AUTO)
var id: Long = 0
protected set
fun favoritesCount() = favorited.size
}
위와 같이 모든 필드를 클래스 내부에서 선언하면, 외부에서 접근할 수 있는 경로를 명확히 제한할 수 있다. 불필요한 수정이 불가능해지며, 엔티티의 일관성이 더욱 강화된다.
'JPA' 카테고리의 다른 글
LazyInitializationException과 트랜잭션, 영속성 컨텍스트에 대한 이해 (0) | 2024.12.01 |
---|---|
자바 ORM 표준 JPA 프로그래밍 기본편 정리 1탄 (0) | 2022.08.07 |
@Enumerated (0) | 2022.07.25 |