오늘의하루

[JPA] CascadeType과 orphanRemoval 설정 이해하고 사용하기 본문

Spring/JPA

[JPA] CascadeType과 orphanRemoval 설정 이해하고 사용하기

오늘의하루_master 2024. 11. 27. 14:40
반응형

Cascade Type 속성

Cascade는 부모 객체의 상태 변화가 자식 객체에게 영향을 미칠지 여부를 설정하는 기능이며 양방향 관계에서 외래 키를 가지지 않는 객체를 부모라고 칭할 때 각 Cascade 유형의 의미는 다음과 같습니다.

@Entity
public class Team {
    ...
    @OneToMany(mapped by = "team")
    private List<Member> members = new ArrayList<>();
    ...
}

1. PERSIST

부모 객체를 저장할 때 자식 객체도 자동으로 DB에 저장할 수 있습니다.

예를 들어, members.add(...)와 같이 부모 객체에 자식 객체를 추가한 후 Team 객체를 저장하면 Member 객체도 함께 저장됩니다.


주의 사항: 자식 테이블에서 부모를 참조할 수 있도록 양방향 세팅을 해줘야 자식 테이블에 부모 참조값이 들어갑니다.

PERSIST 예시

@Test
void persistTest() {
    Team team = new Team();
    team.setTeamName("teamA");
    Member member = new Member("memberA");
    team.getMembers().add(member);
    member.setTeam(team);
    Team saveTeam = teamRepository.save(team);
    em.flush();
    em.clear();

    Team findTeam = teamRepository.findById(saveTeam.getId()).get();
    Member findMember = memberRepository.findById(saveTeam.getMembers().get(0).getId()).get();

    assertThat(findMember.getName()).isEqualTo("memberA");
    assertThat(findTeam.getTeamName()).isEqualTo("teamA");
}

2. MERGE

준영속 상태의 부모 객체를 병합할 때 자식 객체도 자동으로 영속성 컨테스트에 병합하여 DB예 반영 할 수 있습니다.

MERGE 예시

@Test
void mergeTest() {
    Team team = new Team();
    team.setTeamName("teamA");
    Member member = new Member("memberA");
    team.getMembers().add(member);
    member.setTeam(team);

    em.persist(team);
    em.flush();
    em.clear();

    // 준영속 상태
    Team detachedTeam = new Team();
    // 식별자 필수
    detachedTeam.setId(team.getId());
    detachedTeam.setTeamName("teamB");
    Member newMember = new Member("new-member");
    detachedTeam.getMembers().add(newMember);
    newMember.setTeam(detachedTeam);

    // 영속성 전이 MERGE : O ( Team 과 Member Left Join 조회 후 Update )
    // 영속성 전이 MERGE : X ( Team 만 조회 후 Update )
    Team detachedTeamSave = teamRepository.save(detachedTeam);

    assertThat(em.contains(detachedTeamSave)).isTrue();
    assertThat(em.contains(detachedTeamSave.getMembers().get(0))).isTrue();
    assertThat(detachedTeamSave.getMembers().get(0).getName()).isEqualTo("new-member");
}

3. REFRESH

영속성 컨텍스트에 존재하는 Entity의 상태를 DB의 최신 상태로 갱신하는 기능입니다.

이는 1차 캐시에 저장된 데이터를 DB 값으로 덮어쓰는 방식으로 동작하며 스냅샷을 갱신합니다.

REFRESH 예시

@Test
void refreshTest() {
    Team team = new Team();
    team.setTeamName("teamA");
    Member member = new Member("memberA");
    team.getMemberes().add(member);
    member.setTeam(team);
    teamRepository.save(team);
    em.flush();

    member.setName("new-member");
    team.setTeamName("new-team");

    em.refresh(team);

    List<Team> findTeams = teamRepository.findAll();

    assertThat(findTeams.get(0).getTeamName()).isEqualTo("teamA");
    assertThat(findTeams.get(0).getMembers().get(0).getName()).isEqualTo("memberA");
}

4. DETACH

준영속 상태로 변경할 때 부모 객체를 변경하게 되면 연관된 자식 객체들도 모두 준영속 상태로 변경됩니다.

 

사용 목적: 불필요한 특정 Entity를 분리해서 Dirty Checking이 발생하지 않도록 하고 싶을 때 사용됩니다.

DETACH 예시

@Test
void detachTest() {
    Team team = new Team();
    Member member = new Member("memberA");
    team.setTeamName("teamA");
    team.getMembers().add(member);
    member.setTeam(team);
    Team saveTeam = teamRepository.save(team);

    em.flush();

    assertThat(em.contains(saveTeam)).isTrue();
    assertThat(em.contains(saveTeam.getMembers().get(0))).isTrue();

    em.detach(saveTeam);

    assertThat(em.contains(saveTeam)).isFalse();
    assertThat(em.contains(saveTeam.getMembers().get(0))).isFalse();
}

5. REMOVE

부모 객체를 제거했을 때 연관 된 자식 객체들을 함께 삭제합니다.

REMOVE 예시

@Test
void removeTest() {
    Team team = new Team();
    team.setTeamName("teamA");
    Member member = new Member("memberA")
    team.getMembers().add(member);
    member.setTeam(team);
    Team savedTeam = teamRepository.save(team);
    
    teamRepository.delete(savedTeam);
    em.flush();
    em.clear();

    List<Team> teams = teamRepository.findAll();
    List<Member> members = memberRepository.findAll();

    assertThat(teams.size()).isEqualTo(0);
    assertThat(members.size()).isEqualTo(0);
}

REMOVE 옵션 없이 진행한 잘못된 예시

CascadeType.REMOVE와 orphanRemoval를 false일 경우 아래와 같이 진행 시 org.hibernate.TransientObjectException가 발생합니다.

@Test
void removeTestWithoutCascadeAndOrphanRemovalEnabled() {
    Team team = new Team();
    team.setTeamName("teamA");
    Member member = new Member("memberA");
    team.getMembers().add(member);
    member.setTeam(team);
    Team savedTeam = teamRepository.save(team);
    em.flush();

    teamRepository.delete(savedTeam);
    Assertions.assertThrows(IllegalStateException.class, () -> em.flush());
}

예외가 발생하는 이유는 자식 테이블에서 부모 값을 참조하고 있는 상황에서 부모를 삭제하려고 했기 때문입니다.

6. ALL

PERSIST, MERGE, REMOVE, REFRESH, DETACH를 모두 포함하는 옵션입니다.

orphanRemoval 설정

해당 옵션은 이름처럼 orphan(고아)를 어떻게 처리할 것인지에 대해 설정해 주는 옵션입니다.

고아 객체란 참조하던 부모 객체가 사라졌을 때 해당 자식 객체를 삭제할지 말지를 결정할 수 있습니다.

1. orphanRemoval = true

부모 객체 입장에서 연관되어 있는 자식 객체를 제거하는 경우 해당 자식 레코드를 삭제합니다.

 

주의 사항: 특정 자식 엔티티를 삭제하지 않고 단순히 관계만 제거하려는 경우에는 해당 설정은 의도치 않은 삭제를 발생시킬 수 있습니다.

orphanRemoval = true 예시

@Test
void orphanRemovalTest() {
    Team team = new Team();
    team.setTeamName("teamA");
    Member memberA = new Member("memberA");
    team.getMembers().add(memberA);
    memberA.setTeam(team);
    Member memberB = new Member("memberB");
    team.getMembers().add(memberB);
    memberB.setTeam(team);
    Member memberC = new Member("memberC");
    team.getMembers().add(memberC);
    memberC.setTeam(team);
    Team savedTeam = teamRepository.save(team);
    em.flush();

    savedTeam.getMembers().removeIf(m -> m.getName().equals("memberB"));
    em.flush();
    em.clear();

    List<Member> members = memberRepository.findAll();
    assertThat(members.size()).isEqualTo(2);
    assertThat(members.get(0).getName()).isEqualTo("memberA");
    assertThat(members.get(1).getName()).isEqualTo("memberC");
}
반응형
Comments