매번 연관된 엔티티를 영속 상태로 만들어야 할까?
이번 글은 영속성 전이와 고아 객체에 대해 알아보겠습니다.
연관된 엔티티를 영속화하고 싶을때, 어떻게 해야 할까요?
코드를 통해 살펴보겠습니다.
Team 엔티티와 Member엔티티가 아래 코드와 같이 일대다 관계를 맺고 있습니다.
(Team 엔티티를 부모 엔티티, Member 엔티티를 자식 엔티티를 의미합니다.)
Team 엔티티와 Member엔티티를 저장하는 테스트 코드는 아래와 같습니다.
(참고, member엔티티의 setTeam 메서드는 연관관계 편의 메서드입니다.)
@SpringBootTest
public class PersistencePropagationTest {
@PersistenceContext
private EntityManager em;
@Test
@Transactional
@Commit
void persistence_propagation_test() throws Exception {
//given
Team team = new Team();
em.persist(team);
Member member = new Member("박상윤");
member.setTeam(team);
em.persist(member);
//when
member.setTeam(team);
//then
em.persist(member);
}
}
로그에 insert쿼리가 team과 member각각 잘 실행이 되었습니다.
H2 DB에도 저장이 된 것을 확인할 수 있습니다.
JPA에서 부모 엔티티를 저장할 때 연관된 모든 자식 엔티티는 코드와 같이 전부 영속 상태여야 합니다.
만약, 연관된 자식 엔티티가 200개의 엔티티가 존재하는 경우엔 어떨까요?
아래 코드와 같이 200개 전부 비영속 상태이므로, 영속화를 해주어야 합니다.
@Test
@Transactional
void one_team_200_member() throws Exception {
//given
Team team = new Team();
em.persist(team);
for(int i = 0; i < 200; i++){
Member member = new Member("박상윤");
member.setTeam(team);
em.persist(member);
}
}
연관된 자식 엔티티의 갯수가 많아질수록 부모와 자식 엔티티를 개별적으로 영속화하는 작업이 많아지고 실수할 가능성이 커집니다.
연관된 엔티티까지 한번에 영속화할 수 있는 방법은 없을까요?
영속성 전이를 사용하면 연관된 자식 엔티티를 자동으로 영속 상태로 만들어줍니다.
영속성 전이
영속성 전이(Cascade)는 부모 엔티티가 영속 상태가 될 때, 연관된 자식 엔티티도 자동으로 영속 상태가 되도록 하는 기능입니다.
영속성 전이는 여러 옵션이 있지만, 그중 Persist옵션을 사용해서 아래 코드와 같이 부모와 자식 엔티티를 한번에 영속화할 수 있습니다.
영속성 전이를 적용한 코드는 다음과 같습니다.
@Entity
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "team", cascade = CascadeType.PERSIST)
List<Member> members = new ArrayList<>();
public void addMember(Member member){
this.members.add(member);
}
}
@Test
@Transactional
@Commit
void persistence_propagation_test_cascade() throws Exception {
//given
Team team = new Team();
em.persist(team);
Member member = new Member("박상윤");
member.setTeam(team);
//then
em.persist(team);
}
부모 엔티티만 영속화 했지만, 자식 엔티티들에 대해서도 insert 쿼리문이 나가게 됩니다.
H2 DB에서 저장이 된것을 확인할 수 있습니다.
저장이 되면 삭제도 가능한거 아닌가요?
가능합니다.
영속성 전이 - 삭제
먼저, 영속성 전이를 사용하지 않고 자식 엔티티를 삭제한 코드는 다음과 같습니다.
@Test
@Transactional
@Commit
void persistence_propagation_test_delete() throws Exception {
Member findMember = em.find(Member.class, 1L);
em.remove(findMember);
Team findTeam = em.find(Team.class, 1L);
em.remove(findTeam);
}
2개의 delete 쿼리가 나가는 것을 로그로 확인할 수 있습니다.
Team 엔티티와 Member 엔티티는 DB에서 외래 키(FK)로 연결되어 있기 때문에, 부모 엔티티인 Team을 삭제하기 전에 먼저 자식 엔티티인 Member를 삭제해야 합니다. 그렇지 않으면 데이터베이스에서 참조 무결성 제약 조건 위반으로 예외가 발생할 수 있습니다.
Team엔티티를 먼저 삭제해본 결과 아래와 같은 예외가 발생합니다.
org.hibernate.TransientObjectException: persistent instance references an unsaved transient instance of 'com.example.spring_practice.entity.Team' (save the transient instance before flushing) org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientObjectException: persistent instance references an unsaved transient instance of 'com.example.spring_practice.entity.Team' (save the transient instance before flushing)
영속성 엔티티 전이를 사용하지 않아서 자식 엔티티를 따로 제거해야 합니다.
영속성 엔티티 전이를 적용한 코드는 아래와 같습니다.
@Entity
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "team", cascade = CascadeType.REMOVE)
List<Member> members = new ArrayList<>();
public void addMember(Member member){
this.members.add(member);
}
}
cascade 옵션을 PERSIST에서 REMOVE로 변경해줍니다.
@Test
@Transactional
@Commit
void persistence_propagation_test_delete() throws Exception {
Team findTeam = em.find(Team.class, 1L);
em.remove(findTeam);
}
부모 엔티티인 Team 엔티티만 제거해줬음에도 자식 엔티티인 Member엔티티도 함께 제거되었습니다.
cascade의 옵션 중 PERSIST, REMOVE말고 어떤 옵션이 있을까요?
ALL, MERGE, REFRESH, DETACH와 같은 옵션이 있습니다.
Cascade 옵션
cascade 옵션은 6가지가 존재합니다.
- PERSIST : 연관된 자식 엔티티를 자동으로 persist() 되어 저장됩니다.
- REMOVE : 연관된 자식 엔티티를 자동으로 삭제됩니다.
- MERGE : 연관된 자식 엔티티를 자동으로 병합됩니다.
- REFRESH : 연관된 자식 엔티티를 DB에서 다시 불러와 최신 상태로 갱신됩니다.
- DETACH : 연관된 자식 엔티티를 함께 영속성 컨텍스트에서 분리됩니다.
- ALL : 위의 모든 옵션(PERSIST, REMOVE, MERGE, REFRESH, DETACH)을 적용합니다.
고아 객체
고아 객체는 부뫄 엔티티와 연관관계가 끊어진 엔티티를 의미합니다.
JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공합니다.
연관관계가 끊어진 다는 것은 어떤 의미인가요?
코드적으로 접근한다면, 연관관계를 끊는다는 것은 자식 엔티티가 부모 엔티티의 컬렉션에서 제거되거나, 부모 엔티티의 필드에서 더 이상 참조되지 않는 것을 의미합니다. 즉, 부모 객체에서 자식 객체를 List 또는 Set 같은 컬렉션에서 remove() 하거나, 연관 필드를 null로 설정하는 경우를 말합니다.
고아 객체 제거 기능을 사용하기 위해선 아래와 같이 설정을 해줍니다.
@Entity
@Getter
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "team", orphanRemoval = true)
List<Member> members = new ArrayList<>();
public void addMember(Member member){
this.members.add(member);
}
}
orphanRemoval = true로 설정해주면 됩니다.
테스트 코드로 고아 객체가 잘 지워지는지 확인해 보겠습니다.
@Test
@Transactional
@Commit
void persistence_propagation_test_delete() throws Exception {
Team findTeam = em.find(Team.class, 2L);
findTeam.getMembers().removeFirst();
}
em.remove()메서드를 사용해 제거해주지 않았음에도 불구하고, Member엔티티가 제거되었습니다.
고아 객체 제거 기능은 영속성 컨텍스트를 플러시 하는 시점에 DELETE SQL이 실행됩니다.
고아 객체 제거 기능은 부모 엔티티에서 자식 엔티티의 관계를 끊으면, 자식 엔티티가 자동으로 삭제되므로 매우 편리합니다.
하지만 단점은 없을까요?
고아 객체 제거 기능은 한 엔티티만 자식을 참조하는 경우에만 사용해야 합니다. 만약 여러 엔티티가 같은 자식을 참조하는 상황이라면, 의도치 않게 데이터가 삭제될 위험이 있습니다.
쉽게 말해, '여러 곳에서 참조한다'는 것은 부모 엔티티가 여러 개 존재한다는 의미입니다.
이 경우, A라는 부모 엔티티가 자식과의 관계를 끊으면, B라는 부모 엔티티도 해당 자식을 사용할 수 없게 됩니다.
@OneToOne 관계나 @OneToMany 관계에서 사용할 수 있습니다.
만약, Cascade 옵션인 ALL과 고아 객체 기능을 함께 사용하면 어떻게 될까요?
부모 엔티티를 통해서 자식 엔티티의 생명주기를 관리할 수 있게 됩니다.
영속성 전이(ALL) + 고아 객체
영속성 전이(ALL)과 고아 객체 자동 제거 기능을 함께 사용하게 되면,
자식 엔티티의 생성과 삭제를 부모 엔티티를 통해서 관리가 가능해집니다.
영속성 전이(ALL) + 고아 객체를 적용한 코드는 아래와 같습니다.
@Entity
@Getter
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
List<Member> members = new ArrayList<>();
public void addMember(Member member){
this.members.add(member);
}
}
자식 엔티티를 생성하는 코드는 아래와 같습니다.
@Test
@Transactional
@Commit
void persistence_propagation_test_cascade_orphan_object() throws Exception {
// 생성
Team team = new Team();
team.addMember(new Member("박상윤"));
em.persist(team);
}
member가 잘 insert된 것을 확인할 수 있습니다.
자식 엔티티를 제거하는 코드는 다음과 같습니다.
@Test
@Transactional
@Commit
void persistence_propagation_test_cascade_orphan_object() throws Exception {
// 삭제
Team findTeam = em.find(Team.class,1L);
findTeam.getMembers().removeFirst();
}
정리
- 객체(부모 엔티티)를 저장하거나 삭제할 때 연관된 객체(자식 엔티티)를 영속성 전이를 사용해서 함께 저장하거나 삭제할 수 있습니다.
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하려면 고아 객체 제거 기능을 사용하면 됩니다.