이번에 새로운 사이드 프로젝트를 진행하면서 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
}

위와 같이 모든 필드를 클래스 내부에서 선언하면, 외부에서 접근할 수 있는 경로를 명확히 제한할 수 있다. 불필요한 수정이 불가능해지며, 엔티티의 일관성이 더욱 강화된다.

 

 

+ Recent posts