강의

멘토링

로드맵

Inflearn brand logo image

인프런 커뮤니티 질문&답변

woonge님의 프로필 이미지
woonge

작성한 질문수

토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1

엔티티 식별자와 JPA 엔티티

Kotlin 에서 JPA Entity 생성시 질문

해결된 질문

작성

·

298

·

수정됨

1

안녕하세요. 토비님!

현재 저는 강의 내용을 Kotlin springboot 로 따라가고 있습니다.

JPA Entity 클래스를 생성할 때, 자바에선 롬복까지 이용해서 Getter 만 만들어놓고 setter 는 닫아놓는 게 쉽게 되는데, 이걸 코틀린에서는 롬복을 사용하지 않다보니 코틀린스러우면서도 깔끔하게 사용하는 방법에 대해서 애를 먹고 있습니다.

 

찾아보니 3가지 방법 있는 것 같습니다.

방법1. 자바랑 가장 비슷하게, 필드를 모두 private 으로 생성하고 getter 는 롬복 대신 직접 선언.

@Entity
open class Member(
    @Column(name = "email", unique = true, nullable = false)
    private var email: String,
    @Column(name = "nickname", nullable = false)
    private var nickname: String,
    @Column(name = "passwordHash", nullable = false)
    private var passwordHash: String,
    @Column(name = "status", nullable = false)
    @Enumerated(EnumType.STRING)
    private var status: MemberStatus = MemberStatus.PENDING,
) {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L

    fun getEmail(): String = email

    fun getNickname(): String = nickname

    fun getPasswordHash(): String = passwordHash

    fun getStatus(): MemberStatus = status
}

 

방법2. getter 를 좀 더 코틀린스럽게 사용하기 위해 내부 필드를 _를 붙여서 선언 

@Entity
class Member2(
    @Column(name = "email", unique = true, nullable = false)
    private var _email: String,
    @Column(name = "nickname", nullable = false)
    private var _nickname: String,
    @Column(name = "passwordHash", nullable = false)
    private var _passwordHash: String,
    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    private var _status: MemberStatus = MemberStatus.PENDING,
) {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L

    val email: String get() = _email

    val nickname: String get() = _nickname

    val passwordHash: String get() = _passwordHash

    val status: MemberStatus get() = _status
}


방법3. protected set 사용

@Entity
open class Member3(
    email: String,
    nickname: String,
    passwordHash: String,
    status: MemberStatus = MemberStatus.PENDING,
) {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L

    @Column(name = "email", unique = true, nullable = false)
    var email: String = email
        protected set

    @Column(name = "nickname", nullable = false)
    var nickname: String = nickname
        protected set

    @Column(name = "passwordHash", nullable = false)
    var passwordHash: String = passwordHash
        protected set

    @Column(name = "status", nullable = false)
    @Enumerated(EnumType.STRING)
    var status: MemberStatus = status
        protected set
}

 

방법4(?). 전부 public val 로 선언하고, 변경시 새로운 객체 생성

@Entity
@Table(name = "members")
class Member4(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,
    @Column(name = "email", unique = true, nullable = false)
    val email: String,
    @Column(name = "nickname", nullable = false)
    val nickname: String,
    @Column(name = "passwordHash", nullable = false)
    val passwordHash: String,
    @Column(name = "status", nullable = false)
    @Enumerated(EnumType.STRING)
    val status: MemberStatus = MemberStatus.PENDING,
) {
    fun updateNickname(newNickname: String): Member4 {
        require(newNickname.isNotBlank()) { "Nickname cannot be blank" }
        return Member4(
            id = this.id,
            email = this.email,
            nickname = newNickname,
            passwordHash = this.passwordHash,
            status = this.status,
        )
    }
}

 

어느 방식을 선택하는게 현명할까요? 토비님은 평소에 어떻게 하시는지 궁금합니다.

답변 2

5

토비님의 프로필 이미지
토비
지식공유자

일반적으로 3번이 많이 권장되는 방법입니다. protected일 필요는 없고 private set으로 하면 됩니다.

그런데 저도 처음에 저렇게 사용했다가, 장황한 코드를 보고 갑갑해서 언제부터인가는 그냥 setter 막지 않고 primary property로 선언하고 씁니다. 개발팀에게는 모든 변경은 생성자 또는 팩토리 메소드, 또 도메인 관점에서 변경이 일어나는 단위로 함수를 정의해서 쓰라고 하고, setter 쓰지 말라고 합니다.

private으로 막아두는 것은 누군가 그걸 가져다가 그냥 setter로 값을 변경하지 못하게 강제로 차단하는 것인데, 엔티티가 공개 되어서 많은 사람들이 사용하는 코드도 아니고 우리 팀 내부에서만 쓰는 거라면, 정책을 정해서 강제하는 것으로 충분하다고 봅니다. 도메인 클래스를 사용하는 코드는 항상 꼼꼼하게 리뷰하게 되고, 누군가 생각없이 실수하면 지적을 하고 고치면 되겠죠. 그나마도 불안하다면 간단한 규칙을 체크하는 정적 분석 도구를 사용하면 됩니다. @Entity가 붙은 클래스의 set 메소드 호출이 있으면 에러가 나면 되겠죠.

지금까지 몇 년을 여러 개발팀에서 이 방식으로 개발했는데 초반에 단단히 교육해두니 단 한번도 이를 무시하고 setter를 써서 로직을 외부에서 처리하는 개발자를 본 적이 없습니다.

사실 setter를 막는게 전부가 아니죠. updateXXX, applyXXX, changeXXX 같은 도메인 로직에 따라 만들어지는 메소드에서 변경이 일어나는 건 항상 안전하고 완벽할까요? 도메인 모델 설계나 구현이 틀어지면 setter를 그냥 쓰는 것 못지 않게 불변식과 일관성이 깨지거나, 불필요하게 한번에 많은 변경이 일어나거나, 일부분만 변경되는 코드가 되기도 합니다.

Kotlin과 비슷한 시기에 만들어져 서로 영향을 주고 받았다는 Swift는 primary constructor에 프로퍼티를 정의할 때 setter만 private으로 지정하는 간단한 문법이 제공된다고 들었습니다. 하지만 코틀린 언어를 만드는 사람들은 꽤 많은 제안과 요구가 있었음에도 그런 간단한 문법 설탕 정도도 허용할 뜻이 없다고 여러번 밝혔죠.

그래서 언어의 철학이 그렇다면, 정책으로 제약을 거는 것이 낫다고 봅니다.

현실적으로 private, protected, final, open 같은 접근 제어나 한정을 제공하는 문법이 모든 문제를 완벽하게 해결해주지 못합니다. 이런 게 없던 시절에 선배 개발자들은 변수 네이밍 룰을 이용해서, 예를 들어 _로 시작하는 건 private이니까 직접 수정하지 말자는 네이밍 규칙이다, 이렇게 해서도 좋은 설계의 코드를 만들었습니다.

자바 클래스를 만들 때 코틀린처럼 기본을 final로 선언하는 경우가 별로 없습니다. 그러면 더 좋음에도 자바 언어의 디폴트가 그렇지 않은데 그런 제어 코드를 덕지덕지 붙이는 게 대단히 좋을 게 없어보이니까 잘 안 썼고, 그래서 특별히 문제가 생기지도 않았습니다. 이제는 sealed도 나오고 함수형 스타일로 확장의 제한이 중요해질 수도 있겠지만 일반적으로는 그다지 필요하지도 않죠.

생성자 DI 받는 필드에 final을 불이기 시작한지도 얼마 안 됐습니다. 스프링 개발자들이 만든 예제에도 final을 잘 쓰지 않았습니다. 그냥 트렉드, 유행이니까 붙여서 씁니다. 혹은 롬복 때문에 쓰기는 하는데, private final 등이 덕지덕지 붙은 자바 코드가 별로 읽기 편하지 않습니다. 왜 자바 언어를 만든 사람들이 그런 한정자를 안붙이는, 요즘엔 package protected 등의 스펙에도 없는 이름을 억지로 붙여서 부르지만, 원래 이름은 그냥 natural이었습니다. 그런거 빡빡하게 따지지 말고 간결하게 쓰라는 거죠. 누가 같은 패키지에 클래스 만들어서 natural(default) 접근 제어를 가진 다른 클래스의 필드를 바꾸는 코드를 작성하겠어요. 그냥 상식이고 관례입니다.

코틀린은 나름 좋은 관례를 따라서 디폴트 설정을 자바와 꽤나 다르게, 편하게 만들었지만 그래도 private set을 쓰려면 일일히 프로퍼티 선언을 바디에 해야하는 것도 비슷하겠죠. 다음 언어가 나온다면 그때는 또 그게 반영될지 모르겠습니다만.

아무튼 저는 코드가 깔끔해서 읽을 때 인지 부하를 적게 주고 간결하게 이해되는 걸 선호합니다.

woonge님의 프로필 이미지
woonge
질문자

좋은 답변 감사합니다!

현실적인 얘기를 많이 해주셔서 도움이 많이 되네요. 🙂

2

저도 비슷한 상황인데요 ㅎㅎ..
개인적으로 의견을 남겨드리자면

방법1. 자바랑 가장 비슷하게, 필드를 모두 private 으로 생성하고 getter 는 롬복 대신 직접 선언.
-> 이것은 사실상 JVM 필드처럼 선언하는 방식인데 코틀린스럽지않아서 넘어가면 되지 않을까 생각합니다.

방법2. getter 를 좀 더 코틀린스럽게 사용하기 위해 내부 필드를 _를 붙여서 선언 
-> 이방식도 1과 마찬가지로 이유에서 넘어가면 좋지 않을까 생각합니다.

방법3. protected set 사용
-> 이렇게 세터에 대한 접근제어자만 두는 것이 합리적이라고 생각합니다.

방법4(?). 전부 public val 로 선언하고, 변경시 새로운 객체 생성
-> 이방식은.. 도메인 모델을 다루는데 있어서 맞는지는 잘 모르겠습니다. 도메인 엔티티가 마치 데이터처럼 다뤄지는 것 같아서 조금 방식에 차이가 있네요. 이방식은 엔티티보다는 VO (Embeddable) 에 좀 더 적합한 방식이 아닌가 싶네요. 이미 data class가 있기도 하고요.


저는 아래 블로그를 많이 참고해서 엔티티를 만드는 편입니다. 공유드려요.
https://veluxer62.github.io/explanation/kotlin-jpa-entity/

(저도 토비님께서 또는 이글을 보고계시는 여러 코틀린 유저분께서 어떻게 엔티티를 작성하시는지 궁금합니다.)

woonge님의 프로필 이미지
woonge
질문자

캬 감사합니다. 🙂 사실 저는 jpa 가 결국 자바 표준이니까 자바랑 가장 비슷한 방식으로 가는 게 맞지 않을까 했는데, 블로그글 읽고 보니 3번 방식도 좋아보이네요.

woonge님의 프로필 이미지
woonge

작성한 질문수

질문하기