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

화이팅님의 프로필 이미지
화이팅

작성한 질문수

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

코틀린 JPA 질문이 있습니다

작성

·

433

2

안녕하세요.

Sto라는 부모 엔티티가 있습니다.

자식으로는 Realestate 라는 엔티티가 있습니다.

해당 엔티티의 관계는 Sto + Realestate 이렇게 1개의 쌍을 이루어 생성이 됩니다.

저는 @OneToOne 양뱡향 관계를 사용하고 있습니다.

cascade 속성을 이용하여, Sto를 save 했을 때 하위 엔티티까지 함께 저장하려고 하고 있습니다.

이때, 모든 엔티티를 val를 사용하려고 했는데 Sto 생성자 호출 시 하위 엔티티에서 Sto 엔티티의 값을 알 수가 없어서 생성이 불가한 상태입니다.
물리적으로 Sto + Realestate는 한쌍인데 방법이 없을까 싶어 문의드립니다.

var + null을 사용하면, 어떻게든 할 수 있을 것 같은데 양방향 설정을 위해 이렇게 풀어서 사용해야 하나 궁금하네요..

@Entity
@Table(name = "sto")
class Sto(
    @Id
    @Column(name = "sto_id", nullable = false)
    private val id: String,

    @OneToOne(
        mappedBy = "sto",
        cascade = [CascadeType.PERSIST]
    )
    val realestate: Realestate,

    @OneToOne(
        mappedBy = "sto",
        cascade = [CascadeType.PERSIST]
    )
    val youtube: Youtube,
)

@Entity
class Realestate(
    @Id
    @Column(name = "realestate_id", nullable = false)
    private val id: String,

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "sto_id", nullable = false)
    val sto: Sto,
)

fun main() {
    Sto(
        id = "sto_id",
        realestate = Realestate(
            id = "realestate_id",
            sto = 여기가 문제네요..
        )
    )
}

 

 

답변 1

1

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

안녕하세요, 민영님! 좋은 질문 감사드립니다! 🙏 결론부터 말씀드리면 방법은 있습니다... 예시로 코드를 조금 간소화 해서 A / B 클래스로 보여드리겠습니다.

@Entity
class A(
  @Id
  val id: String,

  bId: String,
) {
  @OneToOne(
    mappedBy = "a",
    cascade = [CascadeType.PERSIST] // [주의 2]
  )
  val b: B = B(bId, this) // [주의 1]
}

@Entity
class B(
  @Id
  private val id: String,

  @OneToOne(fetch = FetchType.LAZY)
  val a: A,
)

 

코드로 느낌이 오시겠지만, 방법을 말씀드리자면,

  • 실제 프로퍼티 선언은 class body에 하고

    val b: B = B(bId, this)
  • B 클래스에서 필요한 필드를 생성자에 넘겨, B 인스턴스 자체를 A class body 안에서 만드는

방법입니다!

 

만약 1 : N 관계라면, class body에 MutableList를 만들고 부모 클래스를 인스턴스화 한 다음 자식 클래스들을 addAll 함으로써 nullable하지 않게 코드를 작성할 수 있습니다.

 

다만 제가 주의 1, 2를 적어두었는데요!

 

[주의 1]

클래스 A는 Entity 이고, Entity 클래스일 경우 보통 allOpen 설정을 해두니 final 클래스가 아닙니다.

final이 아닌 코틀린 클래스를 인스턴스화 하는 과정에서 this 를 사용하면 위험할 수 있는데요!

(관련 강의 : <자바 개발자를 위한 코틀린 입문 : 10강>)

image

(관련 문서 : https://kotlinlang.org/docs/inheritance.html#derived-class-initialization-order)

  1. 현재 코드는 A 클래스의 하위 클래를 인스턴스화 하고 있지도 않고

  2. JPA가 데이터를 가져와 Entity를 만들 때는 기본적으로 필드에 직접 값을 넣어주기에

문제될 부분이 없지만, IDE에서는 경고를 주고 있어 찜찜하실 수 있습니다. Hibernate 버전이 올라가면서 2번의 동작이 변경될 수도 있고요!

image

 

[주의 2]

이 부분은 Kotlin과 관련된 부분은 아니고 Entity에서 @Id를 not null String으로 갖고 있는 경우 발생할 수 있는데요!

PERSIST 만 casecade 옵션에 주신 것은

  • 최초 save 하는 과정에서 새로운 Entity 이면 PERSIST 를 사용하기 때문

으로 생각됩니다.

image

다만, 여기에 최대 함정이 있는데요! isNew(entity) 에서 not null String은 false 가 나온다는 것입니다!!! 즉, SimpleJpaRepository.save 메소드는 isNew 를 하는 과정에서 PK를 확인하는데, PK 값이

  • null 이면 새로운 Entity

  • 숫자타입이고 0이면 새로운 Entity

  • 위의 두 경우 모두 아니다~ 그럼 새롭지 않은 Entity

로 판단해버려요!

 

관련 로직은 AbstractEntityInformation 에서 확인해보실 수 있습니다.

image

따라서, 사실상 DB에 영속화 되지 않은 새로운 Entity이더라도, 보내주신 것처럼 PK가 String으로 미리 설정되어 있다면, MERGE 까지 cascade 옵션을 켜주셔야 의도하시는 동작이 가능할 겁니다!

 

아이고~ 글이 너무 길었네요! 요약 드려보겠습니다!

  1. JPA + 코틀린으로 원하시는 바 가능하긴 합니다! 다만 사람에 따라 살짝 찜찜할지도...

  2. Cascade 옵션에 PERSIST 말고 MERGE를 함께 주셔야할 수 있어요!

 

궁금한 점이 해소되셨으면 좋겠습니다~ 감사합니다!! 🙇🙇

화이팅님의 프로필 이미지
화이팅
질문자

궁금한 점이 해소되는 답글이였습니다!
가끔 질문할 때마다 좋은 답변을 제시해주셔서 감사합니다~!

추후 강의 계획이 있다면 궁금하네요!
코틀린 심화[코루틴 등등]!

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

아이고~~ 여쭤봐주셔서 감사해요!!! ☺️☺️

넵넵! 현재

  • 코틀린 고급편

    • 제네릭 / 위임, 지연 / 복잡한 함수형 프로그래밍

    • DSL / 어노테이션 및 리플렉션 / 기타 꿀팁

  • 2시간 안에 끝내는 코루틴

2가지 강의를 준비중에 있습니다! ☺️

두 주제를 하나로 합쳐 오픈 할까 하다가, 강의 가격도 더 낮추고, 원하시는 파트만 집중해서 보실 수 있게끔 분리하는 선택을 해보았습니다.

두 강의 모두 늦어도 8월 말까지는 선보일 수 있도록, 열심히 준비해보겠습니다.

감사합니다!! 🙇

화이팅님의 프로필 이미지
화이팅

작성한 질문수

질문하기