이전 포스트에서 다루었던 CascadeType.REMOVE 와 orphanRemoval = true 옵션이 각각 고아객체를 어떻게 처리하는지 알아보도록 하겠습니다.
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 가리킵니다.
CascadeType.REMOVE 와 orphanRemoval = true 옵션이 각각 고아객체를 어떻게 처리하는지 알아보기 위하여, Team 과 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
}
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();
}
}
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
옵션은
부모 엔티티가 삭제되면 자식 엔티티도 삭제됩니다.
즉, 부모가 자식의 삭제 생명 주기를 관리합니다.
부모 엔티티와 자식 엔티티 사이의 연관관계를 제거해도, 자식 엔티티는 삭제되지 않고 그대로 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
옵션은
부모 엔티티가 삭제되면 자식 엔티티도 삭제됩니다.
즉, 부모가 자식의 삭제 생명 주기를 관리합니다.
부모 엔티티와 자식 엔티티 사이의 연관관계를 제거하면, 자식 엔티티는 고아 객체
로취급되어 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.REMOVE
와 orphanRemoval = true
옵션 모두CascadeType.REMOVE
옵션은 자식 엔티티가 DB에 삭제되지 않고 남아있으며, 외래키 값만 변경됩니다.orphanRemoval = true
옵션은 자식 엔티티가 고아 객체로 취급되어 DB에서 삭제됩니다.CascadeType.REMOVE
와 orphanRemoval = true
옵션 모두두 케이스 모두 자식 엔티티에 딱 하나의 부모 엔티티가 연관되어 있는 경우에만 사용해야 합니다.
예를 들어 1개의 자식 엔티티와 서로 다른 2개 이상의 부모 엔티티가 연관관계를 갖고있다면,
CascadeType.REMOVE
또는 orphanRemoval = true
옵션 사용을 조심해야합니다.
그러므로 @OneToMany
에서 CascadeType.REMOVE
또는 orphanRemoval = true
옵션 사용을 신중히 생각해야 합니다.
예제 코드
글 잘 봤습니다! 질문이 있어 댓글 남깁니다!
부모 엔티티와 자식 엔티티 사이의 연관관계 변경하는 부분에서 (orphanRemoval=true, 1:1 매핑 시),
새로운 자식 엔티티를 추가하고 기존의 부모 엔티티의 연관관계를 새로운 자식 엔티티로 변경하는 경우에,
기존의 자식 엔티티는 연관관계가 끊겨 DB에서 삭제가 되는 것 아닌가요?
'CascadeType.REMOVE와 orphanRemoval = true 옵션 모두
자식 엔티티가 DB에 삭제되지 않고 남아있으며, 외래키 값만 변경됩니다.' 라고 적혀있는 부분에 대해서 질문입니다!