Cascade

Cascade : 종속

한 객체가 영속성 상태일 때 연관된 엔티티도 함께 영속성 상태로 만들고 싶다면 영속성 전이를 사용한다. JPA는 cascade 옵션으로 영속성 전이를 사용할 수 있다.

땡쿠팀의 도메인

현재 땡쿠팀은 cascade 를 한 곳에서 사용하고 있습니다.

MeetingMember가 다대다 관계이고 그 사이에 중간 MeetingMember라는 중간 테이블이 존재합니다.

스크린샷 2022-08-31 오후 3 59 56

연관 관계

서비스적인 구조를 살펴보면 Meeting이 생성되기 위해선 반드시 서비스에 가입한 Member가 먼저 저장되어 있어야 합니다.

따라서 MeetingMember가 누군지 알고, 검증해야 합니다. 반대로 MemberMeeting이라는 존재를 몰라도 됩니다.

이러면 의존방향이 MeetingMember 단방향으로 흐르게 됩니다.

따라서 Meeting을 통해 MeetingMember를 생성하도록 구현했습니다.

스크린샷 2022-08-31 오후 4 01 21

Meeting 객체가 스스로 MeetingMembers를 생성합니다.

생성된 MeettingMember 객체는 Transient 상태기 때문에 Meeting 저장될 때 함께 영속상태가 되어야 합니다. 따라서 CascadeType.PERSIST 옵션을 사용했습니다.

@OneToMany(mappedBy = "meeting", cascade = **CascadeType.PERSIST**, orphanRemoval = true)
private List<MeetingMember> meetingMembers = new ArrayList<>();

Cascade.PERSIST

CascadeType.PERSIST: 엔티티를 영속화할 때, 연관된 엔티티도 함께 유지

@Test
void persist() {
    Member sender = memberRepository.save(Member.builder().name("호호").build());
    Member receiver = memberRepository.save(Member.builder().name("후니").build());

    Coupon coupon = 쿠폰_저장(sender, receiver);

    // 현재 PERSIST 옵션
    Meeting meeting = meetingRepository.save(Meeting.builder()
            .coupon(coupon)
            .members(List.of(sender, receiver))
            .meetingStatus(ON_PROGRESS)
            .meetingTime(LocalDate.now())
            .build());
}

스크린샷 2022-08-31 오후 4 10 38

MeetingMember는 영속 상태가 아니다. 미팅 객체가 생성되는 시점에 생성자안에서 생성되는 객체이지만 PERSIST 옵션 덕분에 비영속 객체도 영속 객체로 전이를 시켰다.

스크린샷 2022-08-31 오후 4 10 54

cascade 옵션을 사용하지 않는다면 meetingMember는 insert 되지 않는다.

Cascade.REMOVE

CascadeType.REMOVE: 엔티티를 제거할 때, 연관된 엔티티도 모두 제거

땡쿠팀은 아직 삭제 기능이 존재하지 않는다. 앞으로 삭제 기능은 생기게 될텐데 정책에 맞춰 어디까지 삭제를 해야할지 고민해야 한다.

  1. 회원이 탈퇴할 경우 현재 예약, 쿠폰, 미팅을 모두 삭제해야 하는지
  2. 쿠폰 삭제가 정책상 필요한건지

우선 몇가지 경우의 수로 예제를 만들어보자.

회원이 탈퇴할 경우 관련된 모든 것을 삭제하겠다는 정책이 나온경우.

@Test
void deleteMember() {
    Member sender = memberRepository.save(Member.builder().name("호호").build());
    Member receiver = memberRepository.save(Member.builder().name("후니").build());

    Coupon coupon = 쿠폰_저장(sender, receiver);

    Meeting meeting = meetingRepository.save(Meeting.builder()
            .coupon(coupon)
            .members(List.of(sender, receiver))
            .meetingStatus(ON_PROGRESS)
            .meetingTime(LocalDate.now())
            .build());

    memberRepository.deleteById(sender.getId());    // 예외 발생
}

스크린샷 2022-08-31 오후 4 11 38

미팅 멤버와 연관된 회원때문에 삭제하지 못 한다.

삭제하기 위해선 미팅을 먼저 삭제하고 미팅 멤버를 삭제한 뒤 회원을 삭제해야 한다. 쿠폰의 경우 간접참조를 하고 있어서 이번 예제에서는 제외하겠다. 방법은 2가지가 있을 것 같다.

  1. 미팅을 삭제하기 전 미팅 멤버를 모두 삭제 한 뒤 미팅을 삭제 한다.
그러기 위해선 MeetingMeberRepository도 필요해진다.
  1. Cascade의 REMOVE 옵션을 사용하여 자식 객체도 함께 삭제한다.
Member sender = memberRepository.save(Member.builder().name("호호").build());
Member receiver = memberRepository.save(Member.builder().name("후니").build());

Coupon coupon = 쿠폰_저장(sender, receiver);

Meeting meeting = meetingRepository.save(Meeting.builder()
        .coupon(coupon)
        .members(List.of(sender, receiver))
        .meetingStatus(ON_PROGRESS)
        .meetingTime(LocalDate.now())
        .build());

meetingRepository.delete(meeting);
memberRepository.delete(sender);

스크린샷 2022-08-31 오후 4 13 02

  • 부모 엔티티 삭제
    • CascadeType.REMOVE와 orphanRemoval = true는 부모 엔티티를 삭제하면 자식 엔티티도 삭제한다.
  • 부모 엔티티에서 자식 엔티티 제거
    • CascadeType.REMOVE는 자식 엔티티가 그대로 남아있는 반면, orphanRemoval = true는 자식 엔티티를 제거한다.

카테고리:

업데이트: