0. 이 글을 쓰게 된 이유
프로젝트에 앞서 JPA 복습을 하면서 영속성 전이와 고아 객체 부분이 명확하지 않다는 느낌이 들어 정리를 하게 되었다.
1. 영속성 전이(CASCADE)
다음과 같은 엔티티 연관 관계가 있다고 가정하자
@Entity
@Getter
@Setter
public class Parent {
@Id
private Long id;
@OneToMany(mappedBy = "parent")
private List<Child> children = new ArrayList<>();
}
========================================================
@Entity
@Getter
@Setter
public class Child {
@Id
private Long id;
@ManyToOne
private Parent parent;
}
만약 JPA에서 위 엔티티들을 저장하라면 다음과 같이 해야한다.
final Parent parent = new Parent();
em.persist(parent);
final Child child1 = new Child();
child1.setParent(parent);
parent.getChildren().add(child1);
em.persist(child1);
final Child child2 = new Child();
child2.setParent(parent);
parent.getChildren().add(child2);
em.persist(child2);
JPA에서는 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 저장이 가능하다.
그래서 부모 엔티티와 자식 엔티티를 모두 저장하고 싶다면 각각 영속상태로 만들어야한다.
이럴 때 영속성 전이를 이용하면 부모만 영속 상태로 만들면 연관된 엔티티까지 한번에 영속 상태로 만들 수 있다.
2. 영속성 전이의 종류
영속성 전이에는 여러가지 옵션이 있고 그에 따라 연관된 엔티티가 영속화되는지, 어떤 상태에서 영속화가 되는지 다르다. 이 옵션들을 알고 적절히 활용하면 객체 지향적인 코드를 짜는데에 많은 도움이 될 것이다.
2.1 영속성 전이:PERSIST(저장)
Parent 객체의 영속성 전이 옵션을 다음과 같이 변경해보자.
@Entity
@Getter
@Setter
public class Parent {
@Id
private Long id;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<>();
}
이렇게 하면 부모, 자식 엔티티를 모두 저장하려고 할 때 부모 엔티티만 영속화하면 자식 엔티티까지 모두 영속화한다.
저장 코드는 다음과 같다.
final Child child1 = new Child();
final Child child2 = new Child();
final Parent parent = new Parent();
child1.setParent(parent);
child2.setParent(parent);
parent.getChildren().add(child1);
parent.getChildren().add(child2);
em.persist(parent);
부모만 영속화하면 CascadeType.PERSIST로 설정한 자식 엔티티까지 함께 영속화해서 저장한다.
2.2 영속성 전이:REMOVE(삭제)
CascadeType.REMOVE는 연관된 엔티티의 저장이 아닌 삭제 시 함께 삭제하기 위한 옵션이다.
만약 저장된 부모와 자식 엔티티를 모두 제거하려면 다음과 같이 각각의 엔티티를 하나씩 제거해야한다.
final Parent parent = em.find(Parent.class, 1L);
final Child child1 = em.find(Child.class, 1L);
final Child child2 = em.find(Child.class, 2L);
em.remove(parent);
em.remove(child1);
em.remove(child2);
Parent 객체의 영속성 전이 옵션을 다음과 같이 변경해보자.
@Entity
@Getter
@Setter
public class Parent {
@Id
private Long id;
@OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
private List<Child> children = new ArrayList<>();
}
그렇다면 다음과 같이 부모 엔티티만 삭제 했을 때, 자식 엔티티도 모두 삭제된다.
final Parent parent = em.find(Parent.class, 1L);
em.remove(parent);
만약 CascadeType.REMOVE 설정을 하지 않고 이 코드를 실행시킨다면 부모 로우만 삭제될 것이다.
그렇게 되면 데이터베이스에서 외래키 무결성 예외가 발생한다.
2.3 영속성 전이(CASCADE) 종류
이 외에도 다양한 CASCADE 종류가 있다.
public enum CascadeType {
/** Cascade all operations */
ALL,
/** Cascade persist operation */
PERSIST,
/** Cascade merge operation */
MERGE,
/** Cascade remove operation */
REMOVE,
/** Cascade refresh operation */
REFRESH,
/**
* Cascade detach operation
*
* @since 2.0
*
*/
DETACH
}
참고로 CascadeType.PERSIST, CascadeType.REMOVE는 em.persist(), em.remove()를 실행할 때 바로 전이가 발생되지 않고 플로시를 호출할 때 전이가 발생한다.
2.4 영속성 전이(CASCADE) 주의 사항
- 영속성 전이는 엔티티를 영속화할 때 함께 영속화하는 편리함을 제공할 뿐 그 이상 그 이하도 아니다. 그러므로 이는 연관관계 매핑하는 것과는 아무런 상관이 없다.
- 논리적으로 하나의 부모가 자식을 관리할 때만 의미가 있음. 즉, 자식의 라이프사이클이 부모와 완벽히 일치할 때만 사용해야함. 만약 자식이 다른 객체와 관련이 있다면 부모 객체와 라이프 사이클이 일치하지 않을 수도 있으니 주의 해야한다.
예를 들어 하나의 게시물과 해당 게시물에 첨부된 파일이라는 관계가 있다고 가정하자. 게시물(부모)이 저장되면 첨부된 파일(자식)도 함께 저장 되어야 논리적으로 맞다. 삭제될 때에도 마찬가지이다. 이런 경우에는 영속성 전이를 사용해도 좋다.
하지만 카테고리와 상품이라는 관계가 있다. 하나의 카테고리(부모)가 삭제될 때 해당 상품(자식)도 삭제된다면 문제가 될 수 있다. 왜냐하면 해당 상품은 다른 카테고리에도 속할 수 있기 때문이다. 이럴 때에는 사용을 자제해야한다.
3. 고아(ORPHAN) 객체
JPA에서는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체 제거라고 한다.
이 기능을 사용하면 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다.
Parent의 옵션을 다음과 같이 변경했다.
@Entity
@Getter
@Setter
public class Parent {
@Id
private Long id;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<>();
}
이때 다음과 같은 코드를 실행하면 해당 CHILD를 삭제하는 쿼리가 나간다.
final Parent parent = em.find(Parent.class, 1L);
parent.getChildren().remove(0);
orphanRemoval = true 옵션으로 인해 컬렉션에서 엔티티를 제거하면 데이터베이스의 데이터도 삭제된다.
고아 객체 기능은 영속성 컨텍스트를 플러시할 때 적용되므로 플러시 시점에 삭제 쿼리가 나가게 된다.
그리고 다음과 같이 부모가 삭제되면 자식도 같이 제거된다. 개념적으로 볼 때, 부모가 없으면 자식은 참조를 잃기 때문에 고아가 되기 때문이다.
final Parent parent = em.find(Parent.class, 1L);
em.remove(parent);
CascadeType.REMOVE를 설정한 것과 완전히 같은 기능이라고 볼 수 있다.
3.1 고아 객체 주의 사항
고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다. 따라서 이 기능은 참조하는 곳이 하나일 때만 사용해야한다. 쉽게 이야기해서 특정 엔티티가 개인 소유하는 엔티티에만 이 기능을 적용해야한다.
만약 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생할 수 있다. 이런 이유로 orphanRemoval은 @OneToOne, @OneToMany에만 사용할 수 있다.
4. 영속성 전이(CASCADE) + 고아 객체, 생명주기
CascadeType.ALL + orphanRemoval = true를 동시에 사용하면 다음과 같은 효과를 낼 수 있다.
- 자식을 저장하려면 부모에 등록만 하면된다.(CASCADE)
final Parent parent = em.find(Parent.class, 1L); parent.addChild(child);
- 자식을 삭제하려면 부모에서 제거하면 된다.(orphanRemoval)
final Parent parent = em.find(Parent.class, 1L); parent.removeChild(child);
- 즉, 자식 엔티티의 생명주기를 부모 엔티티를 통해서 100% 관리할 수 있다.
'JPA' 카테고리의 다른 글
[JPA] 1 : N 연관관계에서 limit 사용 시 OutOfMemory가 발생하는 문제 해결 (1) | 2023.10.05 |
---|---|
[JPA] 기본키 생성 전략 (0) | 2023.07.07 |
[JPA] N + 1 문제 2 (fetch join 최적화) (0) | 2022.09.28 |
[JPA] N + 1 문제 1 (0) | 2022.09.28 |