개요
이전에 다대일 관계를 다룰 때 다(이하 자식)와 일(이하 부모)이 양방향 연관관계일 때를 생각해 보자.
JPA에서 엔티티를 저장할 땐 연관된 모든 엔티티가 영속 상태여야 하기 때문에 저장되는 자식들과 부모 모두 영속 상태여야 하고, 객체 그래프를 고려해 자식에서 부모 설정, 부모에서 자식 설정을 모두 해줘야 했고, 이는 번거롭고 휴먼 에러가 나기 쉽다.
또한 다대일 관계에선 생명주기를 같이 하는 경우가 있다. 즉 부모가 삭제되면, 자식도 같이 삭제되어야 할 때가 있는데 이때도 부모와 해당 부모의 자식을 모두 찾아 삭제해 줘야 해서 번거롭다.
부모가 삭제되는 것이 아닌 부모를 이용해 자식을 삭제하는 작업을 할 때도 객체 그래프를 고려해 부모의 자식 컬렉션에서 자식을 삭제하고, entityManager를 이용해 해당 자식을 삭제해줘야 하는데 이 또한 번거롭다.
JPA에선 위 작업들을 편리하게 해 줄 수 있는 영속성 전이(cascade)와 고아 객체 삭제 기능을 제공한다.
영속성 전이
간단하게 부모와 자식을 구현해 보자.
Parent.java
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> children = new ArrayList<>();
public List<Child> getChildren() {
return children;
}
}
편의를 위해 CascadeType은 ALL로 지정했다. CascadeType의 다른 타입은 뒤에서 살펴보자.
Child.java
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Parent parent;
public void setParent(final Parent parent) {
this.parent = parent;
}
}
저장
영속성 전이를 사용하지 않았을 때 부모, 자식을 저장하려면 아래와 같이 했어야 했다.
public static void withoutCascade(final EntityManager entityManager) {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.getChildren().add(child1);
parent.getChildren().add(child2);
child1.setParent(parent);
child2.setParent(parent);
entityManager.persist(child1);
entityManager.persist(child1);
entityManager.persist(parent);
}
영속성 전이를 사용하면 말 그대로 부모의 영속 상태를 자식에게 전이시킬 수 있다.
public static void cascade(final EntityManager entityManager) {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.getChildren().add(child1);
parent.getChildren().add(child2);
child1.setParent(parent);
child2.setParent(parent);
entityManager.persist(parent);
}
따라서 위처럼 자식을 persist()하는 작업을 생략할 수 있다.
덕분에 위처럼 자동으로 Child가 INSERT 됐다. 이를 도식화하면 아래와 같다.
이때 JPA가 외래 키를 관리하는 자식 엔티티 입장에서 연관관계를 추가하는 작업(setParent)을 대신해주는 것이 아닌 부모 엔티티의 영속 상태를 자식 엔티티에게 전이시켜 같이 영속화한다는 것을 주의해야 한다.
삭제
위에서 저장한 부모와 자식 엔티티를 모두 제거하려면 원래는 아래와 같이 했어야 했다.
public static void removeWithoutCascade(final EntityManager entityManager) {
Parent parent = entityManager.find(Parent.class, 1L);
Child child1 = entityManager.find(Child.class, 1L);
Child child2 = entityManager.find(Child.class, 2L);
entityManager.remove(parent);
entityManager.remove(child1);
entityManager.remove(child2);
}
영속성 전이는 저장뿐만 아니라 삭제할 때도 사용할 수 있다. 영속성 전이를 이용하면 아래와 같이 하면 된다.
public static void remove(final EntityManager entityManager) {
Parent parent = entityManager.find(Parent.class, 1L);
entityManager.remove(parent);
}
부모만 삭제하면 아래와 같이 외래 키 제약조건을 고려해 연관된 자식들을 먼저 삭제하고 부모를 삭제한다.
CASCADE의 종류
CascadeType을 보면 아래와 같이 다양한 옵션이 있다.
public enum CascadeType {
ALL,
PERSIST,
MERGE,
REMOVE,
REFRESH,
DETACH;
}
영속성 전이는 말 그대로 엔티티의 상태를 연관 객체에게 전이시키는 것을 말한다. 엔티티의 상태는 아래 기록을 참고하자.
고아 객체
부모 객체의 자식 컬렉션에서 자식의 참조를 제거하면, 객체 그래프에서만 삭제될 뿐 해당 자식 엔티티에서의 연관 관계가 끊기는 것이 아니다. 따라서 자식 엔티티에서 부모 엔티티 연관 관계를 삭제하는 작업까지 해줘야 한다. 만약 참조를 제거한 이유가 단순히 자식 엔티티 자체를 제거하려는 의도였다면 문제가 심각해질 수 있다.
JPA에선 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제되도록 해주는 고아 객체 제거 기능을 제공한다.
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<>();
public List<Child> getChildren() {
return children;
}
}
부모 객체를 위와 같이 수정해 보자. 아래와 같이 children 필드를 이용해 특정 자식 엔티티를 삭제하면, 해당 자식 엔티티가 삭제될 것이다.
Parent parent = entityManager.find(Parent.class, 1L);
parent.getChildren().remove(0);
위에서 다뤘던 영속성 전이와 고아 객체 제거를 같이 사용하면, 부모 엔티티를 이용해 자식 엔티티의 생명주기를 관리할 수 있다.
마무리
잘못된 정보에 대한 지적이나 피드백은 환영입니다. 감사합니다.
참고
'Backend > JPA' 카테고리의 다른 글
[JPA] 값 타입(Embeddable, Embedded) (0) | 2024.05.17 |
---|---|
[JPA] 프록시와 연관관계 관리(즉시로딩, 지연로딩) (0) | 2024.05.15 |
[JPA] 다대다 연관관계 매핑 (0) | 2024.05.10 |
[JPA] 다대일 연관관계 매핑 (0) | 2024.05.08 |
[JPA] 영속성 관리(영속성 컨텍스트, 엔티티 매니저) (0) | 2024.05.04 |