새소식

JPA

두 엔티티 간에 영속성 전이가 제대로 이루어지려면?

  • -

현재 진행하고 있는 개인 프로젝트에서 다음과 같이 엔티티들의 연관관계가 설정되어 있다.

 

Member -- (OneToMany) -- History -- (OneToMany) -- Question

 

그리고 저장 및 테스트를 용이하게 하기 위해 Member에서 Question까지 CascadeType.ALL을 통해  영속성 전이를 설정해 놓았다.

 

그리고 Member, History 그리고 Question을 저장하고 Member가 올린 총 Question의 수를 구해오는 테스트를 작성하였다.

 

@DisplayName("유저가 존재하면 유저의 이메일, 닉네임, 가입날짜, 해당 유저의 질문 개수를 반환한다.")
@Test
void getMyInfoWithExistMemberId() {
    // given
    String email = "email@email.com";
    String password = "password";

    Member member = createMember(email, password);

    for (int i = 0; i < 100; i++) {
        History history = new History();
        for(int j=0;j<10;j++){
            history.addQuestion(new Question());
        }
        member.addHistory(history);
    }

    Member savedMember = memberRepository.save(member);

    // when
    MemberResponse myInfo = memberService.getMyInfo(savedMember.getId());

    // then
    assertThat(myInfo).isNotNull()
        .extracting("email", "questionCount")
        .contains(email, 1000);
}

 

그런데 자꾸 해당 테스트에서 멤버의 총 질문수가 100*10=1000개가 아닌 0개로 나왔었다.

 

 처음에는 repository의 save 이후 flush가 안되어서 생긴 문제인 것 같아 별도로 테스트코드에서 @PersistenceContext로 엔티티 매니저를 주입받아 flush 해보았지만 결과는 똑같았다.

 

그래서 영속성 전이에 대해 내가 뭔가 잘못 이해하고 있는 것 같아 다시 책을 살펴보니 정말 어이없는 실수를 했다는 것을 알았다.

 

처음 엔티티들의 연관관계에 대해 공부할 때, 그리고 엔티티들의 연관관계를 설계할 때 정말 고민을 많이 했던 부분이 있었다. 바로 연관관계 편의 메서드인데, 연관관계 편의 메서드는 실제로 있는 단어가 아닌 김영한 님이 만드신 단어라고 한다.

 

 

연관관계 편의 메서드에 관련 문의 - 인프런 | 커뮤니티 질문&답변

누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.

www.inflearn.com

 

 처음에 연관관계 편의 메서드를 쓰는 이유는 엔티티가 영속화되고 아직 디비에 반영이 되지 않은 상태에서 객체의 연관관계로 매핑되어 있는 다른 엔티티를 꺼내써야 하는 경우 매핑을 해주어야 하기 때문이라고 이해했다. 그래서 과연 두 엔티티 간에 편의 메서드를 어디에 두어야 하는가에 대한 고민을 한 결과 위 질문글에서 말해주신 대로 정답은 없고 어떤 객체를 중심으로 비즈니스 로직이 진행되는지에 따라 두면 된다라고 결론을 내렸다.

 

 하지만 연관관계 편의 메서드를 쓰는 더 근본적인 이유를 까먹고 그냥 작성을 아얘 하지 않아 버렸다. 당연히 일단은 두 객체간에 연관관계를 맺기 위함인데

 

@OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<History> histories = new ArrayList<>();

public void addHistory(History history) {
    history.setMember(this);
    this.getHistories().add(history);
}

 

 위와 같이 Member 측에서 history를 추가하는 것 외에도 history측에서도 member에 해당 Member를 할당을 안해주니까 당연히 영속성 전이에 문제가 생겼던 것이었다. 물론 이제 영속성 전이라는 것이 저장을 하면 Member에 저장이 되고 History도 저장은 되겠지만 내 테스트 코드와 같이 Member를 통해 History 조회시 Lazy 로딩이 되어있기 때문에 조회를 위해 별도 쿼리가 한번 더 나갈 것이고 여기에서 Member는 History와 연결이 되어있지만 History는 Member와 연결이 되어있지 않기 때문에 당연히 찾아질 수가 없다.

 

 

 위 사진과 같이 History의 memberId로 Member와 join을 하려고 했지만 History에는 저장된 memberId가 없기 때문에 계속 테스트의 결과가 예상과는 달리 0이 뜨는 것이었다.

 

그리고 다시 테스트를 실행하면

 

 

성공하는 것을 볼 수 있다.

 

물론 이렇게 부모 엔티티에서 자식 엔티티를 관리하는 구조로 설계 시 orphanRemoval을 통한 고아 객체 제거도 잊으면 안된다!

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.