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

Truestar님의 프로필 이미지
Truestar

작성한 질문수

실전! 코틀린과 스프링 부트로 도서관리 애플리케이션 개발하기 (Java 프로젝트 리팩토링)

16강. 서비스 계층을 Kotlin으로 변경하기 - UserService.java

재활용 극대화를 위한 상속구조 + Mixin(!?) 구성에 대한 질문입니다.

해결된 질문

작성

·

549

·

수정됨

2

한 3일동안 산넘고 물건너 아직 끝이 어딘지도 모르고 해매고 있습니다 ㅎㅎ 안녕하세요 강사님.

상황:

제목에서도 알 수 있듯, Kotlin Jpa Mixin 을 소개하는 글로 부터 힌트를 얻고, 소스코드:BitBucket 자료를 참고삼아 강의 16강 정도의 예제를 업그레이드 하고 강의를 계속 이어가려다, 돌부리에 걸려 자빠져 입원한듯, 강의를 이어갈수 없게 지체되어서 몇일째 끙끙 앓고있답니다.
제가 참고한 자료 자체가 너무 오래된 5년전 자료라, 최신자료를 찾아봐도 찾아지지 않아서 이 예제를 참고했는데, 이것조차 유효한 자료인지 가늠이 안되었지만 이걸 변형해서라도 조립형태로 가야겠다는 목표가 생겨서 이 가이드를 응용하기로 했습니다..

Kotlin 에서 지원하는 Mixin 기법(맞는지 모르겠습니다)을 도전하게 되었어요. 엔티티, 테이블 모두 생성은 되는데, Mixin 속성은 적용이 안되어서, 테이블을 보면 덩그러니 ID 만 보입니다. 원인과 관련 자료가 너무 없어서 관둬야 하나... 하다가 핼프요청 하게 되었어요.

제가 16강 예제와 와 위에 Mixin 예제로 재구성한 제 Gist(entities.kt) 를 보시면 아시겠지만 구조를 보여드리면 이렇습니다.


entities.kt

다이어그램:

아래는..
조립의 끝단에 있는 @Entity AuditableTestEntity 코드입니다.

/* Implementation */

@Entity
class AuditableTestEntity constructor(
    name: String,
    age: Int?,
) : Identifier<Long>(),
    AccessTimeAuditable by AccessTimeAudit(),
    AccessorAuditable by AccessorAudit(),
    Nameable by Named(name),
    Ageable by Aged(age)

코드를 보면 위의 부품(인터페이스 by 구현체)들이 조립된 형태로 되어있습니다. 컴파일에도 문제가 없어 잘 되는가 싶었지만, [컴파일> 서버 기동] 이후 결과는 다음과 같습니다.

DB 테이블 상황

ID를 제외한 나머지(name, age, created..., 등) 속성이 전혀 반영이 안되었지요. 아래는 서버 구동시 테이블 생성 쿼리 로그입니다.

Hibernate: 
    create table auditable_test_entity (
        id bigint generated by default as identity,
        primary key (id)
    )

질문:

@MappedSuperclass 를 @Entity 를 제외한 모든 클래스에 붙여도 보고, 필드용 에노테이션 역시 Interface 로 옮겨도 보고, Use-site (@get, @filed 등)를 붙여옮봐도, 인식이 안되는건 여전하더라구요. 그래서 혹시 이방식이 동작하지 않는 원인이라던지, Kotlin 설정이나 플러긴 버전의 차이 같은 누락 요인이라던지, 더 나은 방식이 있다면 조언을 얻고자 질문하게 되었습니다.

 

읽어주셔서 감사합니다.

답변 1

3

최태현님의 프로필 이미지
최태현
지식공유자

안녕하세요, Truestar님! 질문 올려주셔서 감사드립니다! 😊

저도 사용해보지 않은 방식이라 정확한 가이드를 드리기는 어렵겠지만, 왜 현재 필드가 나오고 있는지 정도는 설명드려 보겠습니다.

 

사용하신 방법은 이렇습니다.

코틀린에서 제공하는 클래스 위임을 이용해, 한 Entity의 field들을 다른 클래스가 갖고 있게한다.

클래스 위임을 사용하게 되면 다음과 같이 compile이 되게 됩니다. (직접 확인해보셔도 좋을 것 같아요!)

@Entity
A : Nameable by Named(name)

-->

@Entity
public class A {
  private Nameable $$delegate;
  public A(String name) {
    $$delegate = new Named(name)
  }

  public String getName() {
    return this.name;
  }
}

 

자 그럼 위 상황에서 Hibernate은 A에 대한 column 구성을 어떻게 할까요?!

기본적으로 Hibernate은 아시다시피 필드를 기반으로 Table을 구성하게 됩니다. (@Transient 가 붙어 있지 않은 필드죠!) 어디보자~~ 어머 Nameable 필드가 있네요! 그런데 이 필드는 날짜, 숫자, 문자열 등이 아니고 @Embeddable로 구성되어 있는 nested 필드도 아닙니다. 따라서 이 필드는 column으로 옮기고 싶어도 옮길 수가 없죠~ 자 다음으로는... 필드가 없네요!

이렇게 동작하기 때문에 클래스 A는 자동 생성되는 테이블에 name 필드가 빠져 있게 되는 겁니다.

 

자 그렇다면 동작하게 만드는 방법이 있을까요?!

네! 있긴 있습니다... 이 getter에 @Column 어노테이션이 올 수 있도록 intereface 쪽에서 @get:Column 을 적절히 달아주고 JPA의 옵션 중 하나인 '프로퍼티 접근'을 사용하면 됩니다.

(프로퍼티 접근 참고 자료 : https://velog.io/@cmsskkk/JPA-Access 제가 작성한건 아니고 인터넷에 잘 정리되어 있어 가져왔어요!! ㅎㅎㅎ)

@Entity
@Access(AccessType.PROPERTY)
A : Nameable by Named(name)

// 인터페이스에도 column 추가
interface Nameable {
  @get:Column(columnDefinition = "varchar(20)", nullable = false, updatable = true, name = "name")
  var name: String
}

자 이렇게 되면, Hibernate가 필드를 구성할 때 프로퍼티, 즉 getter를 보고도 column을 만들기 때문에 name이라는 DDL이 정상적으로 확인 되실겁니다. (setter도 있으니 조회할 때 값이 올바르게 매핑 될 것도 같고요.. 물론 확인은 필요합니다)

 

다만, 위의 자료에서 확인하실 수 있는 것처럼 "프로퍼티 접근"은 지양해야 하는 방법 중 하나입니다! 🥺

또한 JPA가 Kotlin의 "위임 패턴"을 잘활용할 수 있는 기술은 아니다보니 이러한 mix-in 형태를 갖는 것에는 어려움이 있을 것 같아요! 만약 한 Entity를 여러 객체로 적절히 분리하고 싶은게 목표셨다면 (DDD 느낌 처럼..) 적절한 @Embeddable 으로 분리해보는 것도 좋을 것 같습니다.

예를 들어 위의 예제는 다음과 같이 분리할 수 있습니다.

@Embeddable
class Named(
  override var name: String,
) : Nameable

@Entity
class A(name: String) {
  @Embedded
  private val name = Named(name)
}

이러한 방법은 위임만 사용하지 않았을 뿐이지, 한 Entity가 적절한 다른 값 객체로 분할되고, 해당 값 객체는 다양한 엔티티에서 여전히 재활용 가능하다는 특징이 있습니다. 😊

 

제 답변이 도움이 되었으면 좋겠습니다.

감사합니다!! 🙇

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

어쩐지 자료가 없어도 너~무없어서 이건 무리다 싶었는데 역시나 군요.
오우... 저의 방황하는 뉴런에 다리역할의 시넵스를 놓아준 강사님께 리스팩을 보냅니다.🙌

객체지향 적으로 풀어낼때 Embeddable 을 쓰는지 전혀...감이 안왔습니다.
부실한 기초를 다질 시간입니다.ㅎㅎ

친절한 답변 고맙습니다.👍

Truestar님의 프로필 이미지
Truestar

작성한 질문수

질문하기