[JPA] Cascade

6720·2023년 12월 19일
0

이거 모르겠어요

목록 보기
37/38

Cascade

영속성 전이

부모 엔티티가 관리하는 자식 엔티티 관리를 부모 엔티티를 관리할 때 같이 관리하는 것.

ex) Order와 OrderItem
이때 Order와 OrderItem은 일대다 양방향 연관관계

@Entity
@Getter
@Setter
public class Order {
	@Id
	@GeneratedValue
	private Long id;

	private String name;

	@OneToMany(mappedBy = "order")
	private List<OrderItem> orderItems = new ArrayList<>();

	public void addOrderItem(OrderItem orderItem) {
		orderItems.add(orderItem);
		orderItem.setOrder(this);
	}
}
@Entity
@Getter
@Setter
public class OrderItem {
	@Id
	@GeneratedValue
	private Long id;

	private int value;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "order_id")
	private Order order;
}

이때 부모 엔티티인 Order를 관리한다고 하고, 부모인 order의 orderItem에는 OrderItem 타입의 orderItemA, orderItemB, orderItemC가 담겨 있다고 하자.

OrderItem orderItemA = new OrderItem();
OrderItem orderItemB = new OrderItem();
OrderItem orderItemC = new OrderItem();
Order order = new Order();

order.addOrderItem(orderItemA);
order.addOrderItem(orderItemB);
order.addOrderItem(orderItemC);

em.persist(order);

em.persist(orderItemA);
em.persist(orderItemB);
em.persist(orderItemC);

엔티티 구성이 위와 같다면 이론상 부모인 order를 먼저 DB에 저장한 후에 자식으로 포함하고 있는 orderItemA, orderItemB, orderItemC를 DB에 저장할 수 있음.

하지만 OrderItem 즉, 자식이 늘어나게 된다면? 그만큼 손수 persist를 해줘야 함.
-> 해당 코드를 편리하게 줄이기 위해 나온 것이 Cascade, 영속성 전이라고 함.

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();

다음처럼 @OneToMany의 속성 cascade의 값을 CascadeType.ALL로 변경하면 자식의 경우, 부모가 관리됨과 동시에 같이 관리됨.

OrderItem orderItemA = new OrderItem();
OrderItem orderItemB = new OrderItem();
OrderItem orderItemC = new OrderItem();
Order order = new Order();

order.addOrderItem(orderItemA);
order.addOrderItem(orderItemB);
order.addOrderItem(orderItemC);

em.persist(order);

// 필요 없음.
//em.persist(orderItemA);
//em.persist(orderItemB);
//em.persist(orderItemC);

그렇다면 Cascade 타입의 종류는 뭐가 있으며, 저 상황에서는 왜 ALL을 사용해야 할까?

타입의 종류

Cascade 타입은 총 6가지이며, 타입을 사용하고 싶다면 앞에 CascadeType.을 붙여주면 됨.

MERGE

  • 하위 엔티티까지 병합 작업을 지속
    • 엔티티 상태를 병합할 때 연관된 엔티티도 함께 병합함.
  • 잘 사용하지는 않음.
Order order = new Order();
OrderItem orderItem = new OrderItem();

order.setName("name1");
orderItem.setValue(10);

order.addOrderItem(orderItem);

em.persist(order);
em.persist(orderItem);

em.flush();
em.clear(); // 영속성 컨텍스트로부터 분리하여 값 수정이 가능하도록 함.

order.setName("name2");
orderItem.setValue(20);

em.merge(order); // 이때 orderItem도 변경사항이 반영됨.

REFRESH

  • 데이터베이스로부터 인스턴스 값을 다시 읽어오기
    • 엔티티를 새로고침할 때, 연관된 엔티티들도 함께 새로고침함.
    • 새로고침: 데이터베이스로부터 실제 레코드의 값을 즉시 로딩해 덮어씌움.
  • 잘 사용하지는 않음.
transaction.begin();

Order order = new Order();
OrderItem orderItem = new OrderItem();

order.setName("name1");
orderItem.setValue(10);

order.addOrderItem(orderItem);

em.persist(order);
em.persist(orderItem);

em.flush();

transaction.commit(); // 트랜잭션 커밋을 통해 DB에 값 저장

order.setName("name2");
orderItem.setValue(20);

em.refresh(order); // 이때 데이터베이스로부터 orderItem의 원본 값도 즉시 로딩

System.out.println(order.getName()); // name1
System.out.println(orderItem.getValue()); // 10

refresh() 자체를 사용할 때는 transaction.commit()의 영향을 받지 않음.


+) transaction.begin()transaction.commit()
?) 이 상황에서의 transaction.begin()transaction.commit()은 무슨 역할을 하냐?
!) 영속성 컨텍스트 포스팅
엔티티 매니저는 트랜잭션을 커밋하기 직전까지 내부 쿼리 저장소에 insert sql을 모아둠.
그러므로 transaction.commit()을 하기 전까지는 DB에 적용되지 않음.
위의 코드에서의 em.refresh()과 맨 밑의 출력문이 제대로 나오기 위해선 첫번째로 값을 넣었던 name1과 10이 이미 데이터베이스 안에 저장돼야 함.
그러므로 트랜잭션 커밋에서 em.flush()가 완료된 후에야 refresh()를 사용해서 원하는 값을 얻을 수 있음.

++) transaction과 DB와 영속성 컨텍스트
transaction.commit()의 경우 DB에 해당 값을 적용하는 것일 뿐임.
트랜잭션이 없으면 영속성 컨텍스트 안에서만 값 변동이 일어남.
위의 경우는 DB에 값이 name1, 10인 데이터가 있어야 했기 때문에 트랜잭션이 들어갔을 뿐, 영속성 컨텍스트 안에서만 놀아도 되는 경우라면 트랜잭션이 굳이 필요가 없음.

DETACH

  • 영속성 컨텍스트에서 엔티티 분리
    • 이때 연관된 엔티티들도 분리됨.
  • 잘 사용하지는 않음.
Order order = new Order();
OrderItem orderItem = new OrderItem();

order.setName("name1");
orderItem.setValue(10);

order.addOrderItem(orderItem);

em.persist(order);
em.persist(orderItem);

em.flush();

em.detach(order); // 이때 orderItem도 영속성 컨텍스트에서 분리됨.

+) flush()와 detach()
?) flush()를 사용하면 데이터베이스에 값이 저장되는 것 아니냐, 이 상태에서 detach()를 사용해도 이미 DB에 저장되지 않았냐?
!) 영속성 컨텍스트 포스팅
flush 설명을 보면 영속성 컨텍스트에 있는 데이터를 DB에 동기화 할 뿐이지, 영속성 컨텍스트에 있는 데이터가 사라지지는 않음.
즉, DB에 값이 저장된 상태이면서, 영속성 컨텍스트에 아직 데이터가 남아있음.

그러므로 엔티티를 영속성 컨텍스트에서 분리시킬 수 있는 detach()를 사용할 수 있다는 것.
-> DB에는 저장된 상태이지만 detach()를 사용하여 영속성 컨텍스트에서 엔티티를 분리한 상황 + CascadeType.DETACH를 통해 orderItem도 같이 분리된 상황

PERSIST

  • 하위 엔티티까지 영속성 전달
    • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 옵션임.
Order order = new Order();
OrderItem orderItem = new OrderItem();

order.addOrderItem(orderItem);

em.persist(order); // 이때 orderItem도 영속화됨.

REMOVE

  • 하위 엔티티까지 제거 작업을 지속
    • 엔티티를 제거할 때 연관된 엔티티들도 함게 제거됨.
```java
Order order = new Order();
OrderItem orderItem = new OrderItem();

order.addOrderItem(orderItem);

em.persist(order);
em.persist(orderItem);

em.remove(order); // 이때 orderItem도 제거됨.

ALL

  • 위에서 언급한 모든 타입들이 적용됨.
    • 이걸 사용하면 persist도, remove도, merge도, refresh도, detach도 모두 하위 엔티티도 함께 적용됨.

주의사항

REMOVE와 ALL과 remove()

두 타입은 em.remove()를 사용하여 하위 엔티티까지 전부 삭제할 수 있음.
어떤 면에서는 편리해보일 수 있지만 굉장히 위험하기도 함.

연관된 엔티티가 전부 삭제된다면 참조 무결성 제약조건을 위반할 수 있음.

참조 무결성 제약조건: RDS에서 릴레이션은 참조할 수 없는 FK를 가져서는 안된다는 것.
위를 위반할 시 데이터의 모순이 발생됨.
ex) 자식의 FK는 부모의 PK와 동일해야 하며, 자식의 값이 변경될 때 부모의 제약을 받음.
-> 각 사과가 고유 번호를 가진다고 할 때, 사과 나무는 사과 번호에서 존재하지 않는 값을 입력할 수 없음.

?) 그래서 그게 무슨 뜻인데?
!) 너가 사과를 따서 하나 먹었다고 하자. 그런데 사과 나무가 없어지고, 달려있던 다른 사과가 전부 바닥에 떨어졌다고 하자. 이렇게 바닥에 떨어진 사과는 하늘에서 떨어진건지 땅에서 솟은건지 알 수 없어짐. 사과가 참조하고 있던 사과 나무가 없어진 이 상황을 참조 무결성 제약조건이라고 함.

@Entity
public class Apple {
	...
	@ManyToOne(cascade = CascadeType.REMOVE)
	@JoinColumn(name = "tree_id")
	private AppleTree appleTree;
}
AppleTree tree = new AppleTree();
Apple apple1 = new Apple();
Apple apple2 = new Apple();

apple1.setAppleTree(tree);
apple2.setAppleTree(tree);

em.persist(tree);
em.persist(apple1);
em.persist(apple2);

em.remove(apple1); // 이때 tree도 같이 삭제됨.
em.flush(); // 참조 무결성 제약조건 위반

그러므로 REMOVE나 ALL을 사용할 때 em.remove()를 사용한다면 주의해서 사용해야 함.


+) 왜 flush()에서 참조 무결성 제약조건이 위반됐나?
영속성 컨텍스트 내에선 FK 개념이 없음. 그러므로 flush()를 통해 DB로 동기화하기 위해 이동한 순간부터 제약조건을 위반하게 된 것.

PERSIST와 양방향 연관관계 매핑 시 충돌 가능성

다시 사과 나무와 사과 얘기로 해보자.
사과 나무와 사과의 관계는 일대다이며, 이때 연관관계의 주인은 @JoinColumn을 가지게 되는 사과가 될 것임.

다음 상황에서 사과 나무의 사과 필드에 cascade 타입을 CascadeType.PERSIST로 설정해보자

@Entity
public class AppleTree {
	...
	@OneToMany(mappedBy = "appleTree", cascade = CascadeType.PERSIST)
	private List<Apple> apples = new ArrayList<>();
}
AppleTree tree = new AppleTree();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");

apple1.setAppleTree(tree);
apple2.setAppleTree(tree);

em.persist(apple1);
em.remove(apple1); // apple1 삭제

em.persist(tree);

em.flush();

Apple apple = em.find(Apple.class, "apple1"); // null이 아닌 apple1이 들어감.

분명 apple1을 삭제했지만 tree persist 부분에서 이미 apple1이 필드로 포함되어 있었기 때문에 tree, apple1, apple2가 같이 생성됨. (CascadeType.PERSIST 원인)

영속성 전이(cascade)는 관리하는 부모가 단 하나일 때 사용해야 한다는 주장이 나온 배경도 같은 맥락임.

그러므로 만약 다음과 같은 상황에서 apple1을 없애고 싶다면 deleteApple()을 appleTree 안에 메소드로 만들어주면 됨.

@Entity
public class AppleTree {
	public void deleteApple(Apple apple) {
		apples.remove(apple);
	}
}
AppleTree tree = new AppleTree();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");

apple1.setAppleTree(tree);
apple2.setAppleTree(tree);

tree.deleteApple(apple1); // tree와 apple1의 연결 끊음
em.persist(tree); // apple1이 포함되지 않은 tree 저장
em.remove(apple1); // 영속성 컨텍스트에 남아있는(영속 상태인) apple1 삭제

em.flush();

Apple apple = em.find(Apple.class, "apple1"); // null이나 EntityNotFoundException이 뜰 것

사용 시기

  • 사과나무-사과처럼 부모-자식 구조가 명확할 때
  • 농부-사과나무처럼 하나의 자식에 여러 부모가 대응되는 경우는 X
    • 한 농장에 농부가 여럿이고 딱히 담당 구역이 없다고 하면 여러 농부가 사과나무를 관리하지 않을까
    • 여러 부모가 대응하게 되면 예상하지 못한 문제가 발생할 수 있음.

+) remove와 orphanRemoval=true

이름부터 살벌한 고아제거=true

주의사항에서 설명했던 remove()에서 사과 나무가 사라져 바닥으로 떨어진 사과는 참조할 사과 나무가 없어서 참조 무결성 제약조건에 위반된다고 했는데 이렇게 떨어진 사과를 고아 객체라고 부름.

orphanRemoval=true를 사용하게 되면 이렇게 떨어진 사과(고아 객체)를 자동으로 삭제해줌.

주의사항

위의 사용 시기처럼 사과나무와 사과의 관계는 부모-자식 구조가 명확하고 완전히 개인 소유 엔티티이기 때문에 사용해도 되지만, 농부-사과나무처럼 개인 소유 엔티티 영역을 벗어나게 되면 의도치 않은 삭제가 발생할 수 있음.

++) remove와 N+1 현상

N+1 현상은 간단하게만 말해서 연관관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 현상임.

ex) 사과나무만 select 하고 싶었는데 거기에 달린 사과 정보 N개를 전부 select 해왔다는 의미 -> 사과나무 1 + 사과 N

?) 하지만 remove의 경우는 삭제이므로 과연 select처럼 N+1 현상이 치명적이고 의?미있게 나타날까?
!) delete문이 N+1만큼 실행되는게 select문과는 다른 의미로 치명적일 "수는" 있음.
select문의 N+1 현상이 치명적인 이유는 과도한 "조회"를 하는 것이지만, delete 문에서는 조회를 하지 않음. 단지 delete 문이 N+1 만큼 나갈 뿐임.

delete문에 대한 최적화 방법도 존재함. Batch Delete라던가..
결론적으로 delete문의 N+1과 select문의 N+1은 같은 맥락은 아님.

참고 자료

profile
뭐라도 하자

0개의 댓글