JPA - CascadeType.REMOVE vs orphanRemoval = true

이유석·2023년 3월 8일
3

JPA - Entity

목록 보기
13/14
post-thumbnail

이전 포스트에서 다루었던 CascadeType.REMOVE 와 orphanRemoval = true 옵션이 각각 고아객체를 어떻게 처리하는지 알아보도록 하겠습니다.

고아객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 가리킵니다.

  • 부모가 제거될때, 부모와 연관되어있는 모든 자식 엔티티들은 고아객체가 됩니다.
  • 부모 엔티티와 자식 엔티티 사이의 연관관계를 삭제할때, 해당 자식 엔티티는 고아객체가 됩니다.

공통 예제 코드

CascadeType.REMOVE 와 orphanRemoval = true 옵션이 각각 고아객체를 어떻게 처리하는지 알아보기 위하여, Team 과 Member 엔티티를 바탕으로 예제 코드를 작성해보겠습니다.

먼저 두 옵션에 대한 차이점을 제외하고, 공통되는 사항은 아래와 같습니다.

  • Team(One) 과 Member(Many) 는 다대일 양방향 관계입니다.
  • 연관관계의 주인은 외래키(team_id)를 관리하는 Member 입니다.

위 조건에 맞춰서 작성한 Entity 코드는 아래와 같습니다.

Member Entity 코드

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    // custructor

    // 연관관계 편의 메서드
    public void setTeam(Team team) {

        // 기존 팀과 연관관계를 제거
        if (this.team != null) {
            this.team.getMembers().remove(this);
        }

        // 새로운 연관관계 설정
        this.team = team;
        if (team != null) {
            team.getMembers().add(this);
        }
    }

}
  • 다대일 양방향 연관관계의 순수한 객체 상황을 고려하기 위해, 편의메서드인 setTeam을 추가해주었습니다.

Team Entity 코드

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private Long id;

    @Column(name = "NAME")
    private String name;

    @OneToMany(
            mappedBy = "team",
            cascade = CascadeType.PERSIST
    )
    private List<Member> members = new ArrayList<>();

    // custructor

}
  • 부모(Team) 엔티티가 자식(Member) 엔티티에게 영속성을 전달해주기 위해서,
    cascade = CascadeType.PERSIST 옵션을 지정해주었습니다.

테스트 코드
두 옵션에 대해서 테스트 하기 위해서, Repository Unit 테스트를 위해 사용되는 @DataJpaTest를 사용하도록 하겠습니다.

@DataJpaTest : Annotation for a JPA test that focuses only on JPA components.

// 내장 DB (가짜 DB)로 테스트를 수행 - 단위 테스트
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) 
@DataJpaTest // @Transactional 포함하고 있기 때문에, 각 테스트 종료 시 Rollback
public class JpaTest {

    @Autowired
    private EntityManager entityManager;

    @BeforeEach
    public void initTest() {
        Team team = new Team(0L, "팀1");
        entityManager.persist(team);

        Member member1 = new Member(0L, "회원1");
        Member member2 = new Member(1L, "회원2");

        // 연관관계의 주인에 값 설정
        member1.setTeam(team);
        member2.setTeam(team);

        // CascadeType.PERSIST 로 인하여 영속성 전이
//        entityManager.persist(member1);
//        entityManager.persist(member2);

        // 영속성 컨텍스트의 변경 내용을 DB에 반영
        entityManager.flush();
    }
}
  • @BeforeEach 를 사용하여 각 테스트에 필요한 데이터를 사전에 추가하여 줍니다.
  • 부모(Team) 엔티티에 설정해둔 CascadeType.PERSIST 옵션으로 인하여, Team 엔티티 영속화시 하위 엔티티인 Member 엔티티[member1, member2] 역시 영속화 됩니다.
  • entityManager.flush(); 를 통해, 영속성 컨텍스트의 변경 내용을 DB에 반영하도록 합니다.

  • 즉, 각 테스트를 실행하기 전에 실행되는 SQL 은 아래와 같습니다.

INSERT INTO TEAM (TEAM_ID, NAME) VALUES (0, '팀1');
INSERT INTO MEMBER (MEMBER_ID, USERNAME, TEAM_ID) VALUES (0, '회원1', 0);
INSERT INTO MEMBER (MEMBER_ID, USERNAME, TEAM_ID) VALUES (1, '회원2', 0);

CascadeType.REMOVE

CascadeType.REMOVE 옵션은

  • 부모 엔티티가 삭제되면 자식 엔티티도 삭제됩니다.
    즉, 부모가 자식의 삭제 생명 주기를 관리합니다.

  • 부모 엔티티와 자식 엔티티 사이의 연관관계를 제거해도, 자식 엔티티는 삭제되지 않고 그대로 DB에 남아있습니다.

기존에 생성해둔 부모 엔티티(Team)에 CascadeType.REMOVE 옵션을 추가해주도록 하겠습니다.

...
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private Long id;

    @Column(name = "NAME")
    private String name;

    @OneToMany(
            mappedBy = "team",
            cascade = {CascadeType.REMOVE, CascadeType.PERSIST}
    )
    private List<Member> members = new ArrayList<>();

    // custructor

}

부모 엔티티 삭제

@DisplayName("부모 엔티티(Team)을 삭제하는 경우")
@Test
public void cascadeType_REMOVE_Parent() {
    // when
    Team team = entityManager.find(Team.class, 0L);
    entityManager.remove(team); // 부모 엔티티 삭제

    entityManager.flush();

    // then
    List<Team> teamList = entityManager.createQuery("select t from Team t", Team.class).getResultList();
    Assertions.assertEquals(0, teamList.size());

    List<Member> memberList = entityManager.createQuery("select m from Member m", Member.class).getResultList();
    Assertions.assertEquals(0, memberList.size());
}

위 테스트 코드를 통해 부모 엔티티(Team)를 삭제하게 되면, 이와 연관된 자식 엔티티(member1, member2)도 삭제되는 것 을 확인할 수 있습니다.

해당 테스트 코드를 실행하였을 때, 실행되는 SQL문은 아래와 같습니다.

DELETE FROM MEMBER WHERE MEMBER_ID = 0;
DELETE FROM MEMBER WHERE MEMBER_ID = 1;
DELETE FROM TEAM WHERE TEAM_ID = 0;

총 3번의 DELETE 쿼리가 실행되는 것 을 확인할 수 있습니다.

부모 엔티티와 자식 엔티티 사이의 연관관계 제거

@DisplayName("고아객체 - 부모 엔티티(Team)에서 자식 엔티티(Member)와 연관관계를 끊는 경우")
@Test
public void cascadeType_REMOVE_Persistence_Remove() {
    // when
    Team team = entityManager.find(Team.class, 0L);
    team.getMembers().get(0).setTeam(null);

    entityManager.flush();

    // then
    List<Team> teamList = entityManager.createQuery("select t from Team t", Team.class).getResultList();
    Assertions.assertEquals(1, teamList.size());

    List<Member> memberList = entityManager.createQuery("select m from Member m", Member.class).getResultList();
    Assertions.assertEquals(2, memberList.size());
}

위 테스트 코드를 통해 부모 엔티티(Team)와 자식 엔티티(Member) 사이의 연과관계를 끊게 되어도, 자식 엔티티는 삭제되지 않는 것 을 확인할 수 있습니다.

해당 테스트 코드를 실행하였을 때, 실행되는 SQL문은 아래와 같습니다.

UPDATE MEMBER SET TEAM_ID = NULL WHERE MEMBER_ID = 0;

DELETE 쿼리가 전혀 실행되지 않은 것 을 확인할 수 있습니다.

부모 엔티티와 자식 엔티티 사이의 연관관계 변경 시

@DisplayName("자식 엔티티의 연관관계 변경 시")
@Test
public void change_persistence_child() {
    // given
    Team team = new Team(1L, "팀2");
    entityManager.persist(team);

    // when
    Member member1 = entityManager.find(Member.class, 0L);
    member1.setTeam(team); // UPDATE 쿼리 수행
    entityManager.flush();

    // then
    Team team1 = entityManager.createQuery("select t from Team t where t.id = 0", Team.class).getSingleResult();
    Assertions.assertEquals(1L, team1.getMembers().get(0).getId());

    Team team2 = entityManager.createQuery("select t from Team t where t.id = 1", Team.class).getSingleResult();
    Assertions.assertEquals(0L, team2.getMembers().get(0).getId());

    List<Member> memberList = entityManager.createQuery("select m from Member m", Member.class).getResultList();
    Assertions.assertEquals(2, memberList.size());
}

위 테스트 코드를 통해 부모 엔티티(Team)과 자식 엔티티(Member) 사이의 연관관계가 잘 변경되었음을 확인할 수 있습니다.

해당 테스트 코드를 실행하였을 때, 실행되는 SQL문은 아래와 같습니다.

INSERT TEAM (TEAM_ID, NAME) VALUES (1,2);
UPDATE MEMBER SET TEAM_ID = 1 WHERE MEMBER_ID = 0;

DELETE 쿼리가 실행되지 않고, UPDATE 쿼리가 실행되는 것 을 확인할 수 있습니다.

orphanRemoval = true

orphanRemoval = true 옵션은

  • 부모 엔티티가 삭제되면 자식 엔티티도 삭제됩니다.
    즉, 부모가 자식의 삭제 생명 주기를 관리합니다.

  • 부모 엔티티와 자식 엔티티 사이의 연관관계를 제거하면, 자식 엔티티는 고아 객체로취급되어 DB에서 삭제됩니다.

기존에 생성해둔 부모 엔티티(Team)에 orphanRemoval = true 옵션을 추가해주도록 하겠습니다.

...
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private Long id;

    @Column(name = "NAME")
    private String name;

    @OneToMany(
            mappedBy = "team",
            orphanRemoval = true,
            cascade = CascadeType.PERSIST
    )
    private List<Member> members = new ArrayList<>();

    // custructor

}

부모 엔티티 삭제

@DisplayName("부모 엔티티(Team)을 삭제하는 경우")
@Test
public void orphanRemoval_true_Parent() {
    // when
    Team team = entityManager.find(Team.class, 0L);
    entityManager.remove(team);

    entityManager.flush();

    // then
    List<Team> teamList = entityManager.createQuery("select t from Team t", Team.class).getResultList();
    Assertions.assertEquals(0, teamList.size());

    List<Member> memberList = entityManager.createQuery("select m from Member m", Member.class).getResultList();
    Assertions.assertEquals(0, memberList.size());

}

위 테스트 코드를 통해 부모 엔티티(Team)를 삭제하게 되면, 이와 연관된 자식 엔티티(member1, member2)도 삭제되는 것 을 확인할 수 있습니다.

해당 테스트 코드를 실행하였을 때, 실행되는 SQL문은 아래와 같습니다.

DELETE FROM MEMBER WHERE MEMBER_ID = 0;
DELETE FROM MEMBER WHERE MEMBER_ID = 1;
DELETE FROM TEAM WHERE TEAM_ID = 0;

총 3번의 DELETE 쿼리가 실행되는 것 을 확인할 수 있습니다.

부모 엔티티와 자식 엔티티 사이의 연관관계 제거

@DisplayName("고아객체 - 부모 엔티티(Team)에서 자식 엔티티(Member)와 연관관계를 끊는 경우")
@Test
public void orphanRemoval_true_Persistence_Remove() {
    // when
    Team team = entityManager.find(Team.class, 0L);
    team.getMembers().get(0).setTeam(null);

    entityManager.flush();

    // then
    List<Team> teamList = entityManager.createQuery("select t from Team t", Team.class).getResultList();
    Assertions.assertEquals(1, teamList.size());

    List<Member> memberList = entityManager.createQuery("select m from Member m", Member.class).getResultList();
    Assertions.assertEquals(1, memberList.size());
    }

위 테스트 코드를 통해 부모 엔티티(Team)와 자식 엔티티(Member) 사이의 연과관계를 끊게 되어도, 해당 자식 엔티티가 고아객체로 취급되어 삭제되는 것 을 확인할 수 있습니다.

해당 테스트 코드를 실행하였을 때, 실행되는 SQL문은 아래와 같습니다.

DELETE FROM MEMBER WHERE MEMBER_ID = 0;

총 1번의 DELET 쿼리가 실행되는 것 을 확인할 수 있습니다.

부모 엔티티와 자식 엔티티 사이의 연관관계 변경 시

@DisplayName("자식 엔티티의 연관관계 변경 시")
@Test
public void change_persistence_child() {
    // given
    Team team = new Team(1L, "팀2");
    entityManager.persist(team);

    // when
    Member member1 = entityManager.find(Member.class, 0L);
    member1.setTeam(team); // UPDATE 쿼리 수행
    entityManager.flush();

    // then
    Team team1 = entityManager.createQuery("select t from Team t where t.id = 0", Team.class).getSingleResult();
    Assertions.assertEquals(1L, team1.getMembers().get(0).getId());

    Team team2 = entityManager.createQuery("select t from Team t where t.id = 1", Team.class).getSingleResult();
    Assertions.assertEquals(0L, team2.getMembers().get(0).getId());

    List<Member> memberList = entityManager.createQuery("select m from Member m", Member.class).getResultList();
    Assertions.assertEquals(2, memberList.size());
}

위 테스트 코드를 통해 부모 엔티티(Team)과 자식 엔티티(Member) 사이의 연관관계가 잘 변경되었음을 확인할 수 있습니다.

해당 테스트 코드를 실행하였을 때, 실행되는 SQL문은 아래와 같습니다.

INSERT TEAM (TEAM_ID, NAME) VALUES (1,2);
UPDATE MEMBER SET TEAM_ID = 1 WHERE MEMBER_ID = 0;

DELETE 쿼리가 실행되지 않고, UPDATE 쿼리가 실행되는 것 을 확인할 수 있습니다.

연관관계 변경 시, 자식 엔티티는 새로운 부모 엔티티와의 연관관계를 갖게 되어 고아객체가 되지 않기 때문입니다.

비교 결과

  • 부모 엔티티 삭제
    • CascadeType.REMOVEorphanRemoval = true 옵션 모두
      부모 엔티티를 삭제하면, 자식 엔티티도 삭제됩니다.
  • 부모 엔티티와 자식 엔티티 사이의 연관관계 제거
    • CascadeType.REMOVE 옵션은 자식 엔티티가 DB에 삭제되지 않고 남아있으며, 외래키 값만 변경됩니다.
    • orphanRemoval = true 옵션은 자식 엔티티가 고아 객체로 취급되어 DB에서 삭제됩니다.
  • 부모 엔티티와 자식 엔티티 사이의 연관관계 변경
    • CascadeType.REMOVEorphanRemoval = true 옵션 모두
      자식 엔티티가 DB에 삭제되지 않고 남아있으며, 외래키 값만 변경됩니다.'

주의점

두 케이스 모두 자식 엔티티에 딱 하나의 부모 엔티티가 연관되어 있는 경우에만 사용해야 합니다.

예를 들어 1개의 자식 엔티티와 서로 다른 2개 이상의 부모 엔티티가 연관관계를 갖고있다면,
CascadeType.REMOVE 또는 orphanRemoval = true 옵션 사용을 조심해야합니다.

  • 자식 엔티티를 삭제할 상황이 아니어도, 연관되어 있는 1개의 부모 엔티티 삭제 또는 해당 연관관계를 제거하여 자식 엔티티가 삭제될 수 있기 때문입니다.

그러므로 @OneToMany 에서 CascadeType.REMOVE 또는 orphanRemoval = true 옵션 사용을 신중히 생각해야 합니다.

예제 코드

profile
소통을 중요하게 여기며, 정보의 공유를 통해 완전한 학습을 이루어 냅니다.

2개의 댓글

comment-user-thumbnail
2024년 4월 30일

글 잘 봤습니다! 질문이 있어 댓글 남깁니다!
부모 엔티티와 자식 엔티티 사이의 연관관계 변경하는 부분에서 (orphanRemoval=true, 1:1 매핑 시),
새로운 자식 엔티티를 추가하고 기존의 부모 엔티티의 연관관계를 새로운 자식 엔티티로 변경하는 경우에,
기존의 자식 엔티티는 연관관계가 끊겨 DB에서 삭제가 되는 것 아닌가요?

'CascadeType.REMOVE와 orphanRemoval = true 옵션 모두
자식 엔티티가 DB에 삭제되지 않고 남아있으며, 외래키 값만 변경됩니다.' 라고 적혀있는 부분에 대해서 질문입니다!

1개의 답글