JPA Entity 심층탐구 (2) Kotlin으로 Entity를 작성하는 알맞은 방법

HEYDAY7·2022년 11월 22일
0

Learn Kotlin + Spring

목록 보기
25/25

시작하며

지난 글에 이어서 이제는 그렇다면 어떻게 Entity를 적어야 하는지 알아보겠다.

Kotlin으로 JPA Entity 작성하기

이 파트는 spoqa 기술 블로그에 올라온 남경호님의 "스포카에서 Kotlin으로 JPA Entity를 정의하는 방법" 글을 참고하여 작성하였다.

필요한 plugin 추가하기

Kotlin으로 JPA를 사용할 때 Entity에 allopen 옵션과 no-args constructor 옵션을 줘야한다. 이는 많은 자료들에서 찾아볼 수 있다.(위 링크에서도 나와 있음) 따라서 해당 작업을 먼저 해준다.

## build.gradle.kts 
// 아래 코드를 추가해준다.
~~
plugins {
	~~~
	kotlin("plugin.allopen") version "1.6.21"
	kotlin("plugin.noarg") version "1.6.21"
}

allOpen {
	annotation("javax.persistence.Entity")
	annotation("javax.persistence.MappedSuperclass")
	annotation("javax.persistence.Embeddable")
}

PrimaryKeyEntity

앞 글에서 data class를 사용하지 않는 문제들 중 equal, hashcode가 적절치 않기 때문도 있었다. 이를 극복하기 위해 PrimaryKeyEntity라는 추상 클래스를 만들어서 사용하자.

## PrimaryKeyEntity.kt
@MappedSuperclass
abstract class PrimaryKeyEntity: Persistable<Long> {
    @Id @GeneratedValue
    private val id: Long? = null

    @Transient
    private var _isNew = true

    override fun getId(): Long? = id

    override fun isNew(): Boolean = _isNew

    override fun equals(other: Any?): Boolean {
        if (other == null) {
            return false
        }

        if (other !is HibernateProxy && this::class != other::class) {
            return false
        }

        return id == getIdentifier(other)
    }

    private fun getIdentifier(obj: Any): Long? {
        return if (obj is HibernateProxy) {
            (obj.hibernateLazyInitializer.implementation as PrimaryKeyEntity).id
        } else {
            (obj as PrimaryKeyEntity).id
        }
    }

    override fun hashCode() = Objects.hashCode(id)

    @PostPersist
    @PostLoad
    protected fun load() {
        _isNew = false
    }
}

코드를 보면 앞서 언급했든 equals와 hashcode를 정의해 공통으로 활용할 수 있도록 하였다. 코드의 부분 부분을 살펴보자

  • @MappedSuperclass : 여러 Entity 별로 공통 필드가 존재하는 경우 중복코드를 제거해주기 위해서 사용한다. 따로 object를 생성할 필요가 없다면 추상 클래스로 만들어주면 된다.

  • id : Long Type으로 만들어줬으며 @GeneratedValue를 줘 자동으로 key가 생성되게 하였다.

이 코드의 핵심이 될 수 있는 equals를 따로 살펴보자

equals()

코드 자체는 어려울게 없다.

override fun equals(other: Any?): Boolean {
	if (other == null) {
    	return false
    }

	if (other !is HibernateProxy && this::class != other::class) {
		return false
    }

	return id == getIdentifier(other)
}

private fun getIdentifier(obj: Any): Long? {
	return if (obj is HibernateProxy) {
    	(obj.hibernateLazyInitializer.implementation as PrimaryKeyEntity).id
    } else {
    	(obj as PrimaryKeyEntity).id
    }
}

우선 첫번째 if는 null과의 비교시 false를 return 해 주도록 하는 당연한 코드이다.
두번째 if의 경우 비교 대상이 HibernateProxy가 아닐때, 해당 class와 비교 대상의 class가 동일하지 않다면 false를 return해 준다. 이를 통해 불필요하게 다른 class들 끼리 우연히 id가 겹쳐 일어날 수 있는 문제를 해결해준다.

핵심은 마지막 return 과 getIdentifier이다. getIdentifier function을 보면 obj가 HibernateProxy이면 해당 실제 entity의 id를, 아닐경우 obj(entity)의 id를 return 해주도록 되어있다. 이는 지연로딩 시 Proxy를 마주하게 되었을 때 id 식별자를 잘 return 해주기 위함이다.

물론 여기서 spoqa의 글을 보고 하다 나는 Id를 Long으로 하고 싶어 코드를 약간 수정한 부분이 있다. 따라서 로직 자체가 더 복잡해지거나 비효율적으로 바뀌었을 수는 있다.

이렇게 equals를 정의해주는 것으로 lazy loading 또한 효과적으로 이뤄질 수 있게 작성하였다.

실제 Entity 적용

여기서는 내가 진행하던 프로젝트에서 수정한 Entity 하나를 예시로 들겠다.

@Entity
class Diary(
    @Column(nullable = false)
    var title: String = "",

    @Column(nullable = false)
    var body: String = "",

    @Column(nullable = false)
    var isPrivate: Boolean = false,
    writer: User
): PrimaryKeyEntity() {
    @OneToMany(
        cascade = [CascadeType.ALL],
        orphanRemoval = true
    )
    private val _actions: MutableList<Action> = mutableListOf()
    val actions: List<Action> get() = _actions.toList()

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    var writer: User = writer
        protected set

    fun addAction(action: Action) {
        _actions.add(action)
    }
    fun removeAction(action: Action) {
        _actions.remove(action)
    }
}

var 사용하기

Entity를 작업하면서는 필드들을 어떻게 나타낼까~ 하는 고민을 많이 했다. 우선 Entity의 경우 중간 과정에서 상태(데이터)들이 바뀔 수 있기 때문에 1차적으로는 var로 나타내는 것을 최우선으로 했다.

변경 불가능한 필드 정의하기

1차적으로는 다 var로 나타냈다. 그 이후에 드는 생각은 수정이 불가능한 field 또한 존재할 수 있는데 이때는 어떻게 처리해야 할까? 였다. 여러 글에서 추천하는 방법은 property의 private set(protected set) 기능을 활용하는 것이다. 이를 활용하면 setter를 막을 수 있다.

헌데 코드를 보면 private set이 아닌 protected set이 적혀있는 것을 볼 수 있다. 그 이유는 앞서 Entity 클래스에 대해 allOpen 설정을 했기 때문이다. open property에 대해서는 private setter가 허용되지 않기 때문에 protected set을 통해서 setter를 제한하는 것이다.

list 필드 정의하기(연관관계)

복잡하고 데이터가 많은 Entity를 만들다보면 필연적으로 list 형태의 필드를 정의하게 된다. 이 list를 어떻게 나타낼까 고민 하다가 kotlin의 backing properties convention을 이용하기로 했다.

@OneToMany(
	cascade = [CascadeType.ALL],
	orphanRemoval = true
)
private val _actions: MutableList<Action> = mutableListOf()
val actions: List<Action> get() = _actions.toList()
    
fun addAction(action: Action) {
	_actions.add(action)
}
fun removeAction(action: Action) {
	_actions.remove(action)
}
  1. 우선 _ + 필드명으로 mutableList를 정의해준다.
  2. 그 다음 원하는 필드명으로 List를 정의하고, getter를 통해 _필드명 값을 .toList()로 변환하여 같는다.
  3. Entity 상태 변경(데이터 변경)을 위해 필요한 method를 정의하고, 내부적으로 _필드 를 통해 값을 변경해준다.

이렇게 나타내는 것으로 연관관계 수정도 가능하면서, 외부에 직접적으로 노출되는것은 Immutable 하게 관리할 수 있게 된다.

마치며

이렇게 Kotlin에서 Entity를 어떻게 적어야 하는지 배워가는 것 같다. 그럼에도 위 코드에 남아있는 persist 관련 코드라거나 확신이 없는 것들이 많다. 일단은 현 상태에서도 entity 학슴이 꽤나 되었다 생각된다. 이제 프로젝트에 직접 적용을 해보자!

profile
(전) Junior Android Developer (현) Backend 이직 준비생

0개의 댓글