[DB 기초] 12. JPA의 영속성 전이 옵션(CascadeType)
1. CascadeType
:: 연관된 Entity에도 동일한 영속성 작업을 전달시킬지 여부를 설정하는 옵션
:: EntityManager(em)에 따른 부모 엔티티에 대한 조작이 자식 엔티티에 영향을 끼치는 것.
:: 예시로, 부모 엔티티를 저장할 때, 자식 엔티티도 함께 저장되게 만들고 싶을 때 사용
:: 영속성 전이의 경우 양방향 관계에서는 한쪽에만 설정
:: 추가로, CascadeType.PERSIST / CascadeType.ALL 설정시, 부모를 persist()하면 자식도 같이 persist()됨
:: 이후 flush() / commit 시점에 DB에 반영
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST) // cascade로 설정
private List<Child> children = new ArrayList<>();
em.persist(parent) == em.persist(parent) + em.persist(child1) + em.persist(child2)
1.1 CascadeType.PERSIST
:: 부모 엔티티를 persist()할 때, 연관된 자식 엔티티도 함께 저장(자식 엔티티를 같이 저장해야 할 때 사용)
:: 설정 X 시, persist(child)를 따로 해줘야 함
:: CascadeType.PERSIST 나 CascadeType.ALL 의 경우 flush 시 자식 엔티티에 persist 발생
Parent parent = new Parent();
Child child = new Child();
child.setParent(parent);
parent.getChildren().add(child);
// 부모만 저장
entityManager.persist(parent); // => 자식도 자동 저장됨!
1.2 CascadeType.MERGE
:: 부모 엔티티를 merge()할 때, 자식도 자동으로 병합
:: detached 상태에서도 자식 변경을 반영하고 싶을 때 사
:: 설정 X 시, 자식 변경 사항은 반영 X
Parent detachedParent = entityManager.find(Parent.class, id);
entityManager.detach(detachedParent);
detachedParent.setName("Updated");
Child detachedChild = detachedParent.getChildren().get(0);
detachedChild.setName("Child updated");
// 병합
entityManager.merge(detachedParent); // => 자식도 함께 merge됨
1.3 CascadeType.REMOVE
:: 부모 엔티티를 remove()할 때, 연관된 자식도 함께 삭제
:: 설정 X 시, 자식은 Orphan(고아) 상태 or FK 제약으로 삭제 실패가 발생 가능
Parent parent = entityManager.find(Parent.class, id);
// 삭제
entityManager.remove(parent); // => 자식도 자동 삭제!
2. orphanRemoval vs CascadeType.REMOVE 정리
2.1 개념 차이
항목 | 설명 |
CascadeType.REMOVE | :: 부모 엔티티가 삭제될 때, 자식 엔티티도 함께 삭제됨 :: 삭제 행위 자체가 전파됨 |
orphanRemoval = true | :: 부모의 컬렉션에서 자식 엔티티를 `remove()` 하거나, 연관 필드를 null 로 만들면 삭제됨 (영속성 컨텍스트 상에서 자동 remove → flush 시 DELETE 발생) :: 부모가 자식과의 연관관계를 끊으면, 자식 엔티티는 고아 객체로 간주되어 삭제됨 :: 자식 입장에서 연관관계가 사라지면, 더 이상 사용할 수 없는 객체로 판단되어 삭제됨 |
2.2 주의점
- 다대다 혹은 다대일 관계에서 삭제 전파 주의
1) 자식 엔티티가 @OneToMany, @ManyToMany, @ManyToOne 등의 관계를 가질 경우, 복수의 부모를 가질 수 있음
2) orphanRemoval은 연관관계의 주인에서만 동작함 → mappedBy 속성을 가진 쪽에서는 orphanRemoval 효과 없음
3) 이 때 한 부모가 자식과 관계를 끊거나 삭제될 경우, 자식이 다른 부모와의 관계도 유지 중인데 삭제될 위험 있음
Ex) Member 엔티티가 Team, Club, School이라는 3개의 부모 엔티티와 연결
만약 Team에서 Member 제거 + orphanRemoval = true 사용 시,
→ 다른 부모(Club, School) 입장에선 이유 없이 Member가 삭제되어 데이터 손실 발생
3. JPA에서 Flush
:: Transaction 개념에서의 commit이 아닌,
:: EntityManager관점에서의 Flush => 영속성 컨텍스트(Persistence Context)의 변경 사항을 DB에 반영(싱크)
:: 즉, Flush는 현재 영속성 컨텍스트의 상태를 DB와 동기화하는 작업(commit이 아니므로 Rollback 가능)
- em.persist 하면 데이터베이스에 저장되는게 아니다.
- em.persist 하면 Entity(POJO 데이터)를 영속성 컨텍스트에 저장하는 것일 뿐
- em.persist 하면 영속성 컨텍스트 내 '쓰기 지연 SQL 저장소'에 SQL 문 만들어 저장
- transaction.commit() 을 해야 데이터베이스에 앞서만든 SQL 실제 쿼리가 수행됨 .. But, 아직 commit X이기에 실제 적용 X
- 그냥 한줄 정리하자면,,, 영속성 컨텍스트 → DB 로 변경사항 반영 / 영속성 컨텍스트와 DB간의 정합성을 맞추기 위함
3.1 FlushModeType
- FlushModeType.COMMIT
:: DB 에 SQL 쿼리 전달 횟수는 줄지만, 데이터 일관성 문제 - FlushModeType.ALL
:: persist() 혹은 flush() 시 데이터베이스에 싱크
:: JPA의 기본 모드는 JPQL 쿼리 실행 시, flush() 자동으로 먼저 실행 - native query는 flush 하지 않음 → 주의 필요
3.2 Flush의 동작 방식
발생 시점 | 설명 |
em.flush() | 명시적으로 호출 |
transaction.commit() | 트랜잭션 커밋 시 자동 호출 |
JPQL 실행 전 | JPQL은 DB에서 직접 조회하므로, flush로 동기화 후 실행됨 |
- 영속성 컨텍스트를 Flush하는 방법
- 직접 호출 :: em.flush()
- 자동 호출(1) :: transcation.commit()
- 자동 호출(2) :: JPQL 쿼리 실행 시, flush()를 자동으로 실행 (DB 상태 기준으로 쿼리 수행하기 위함)
+ 참고로, @Query 사용한 경우도 내부적으로 JPQL → flush 발생함 - 스프링 프레임워크에서 영속성 컨텍스트는 어떻게 동작하는가??
- 트랜잭션 범위에 영속성 컨텍스트를 할당한다.- @Transactional 이 생성된다 = 영속성 컨텍스트가 생성된다.
- @Transactional 이 종료된다 = 영속성 컨텍스트를 날린다.
- @Transactional 이 같다 = 항상 같은 영속성 컨텍스트를 참조한다.
- em.persist() 했을 때
1) Entity는 DB에 저장 X
2) 쓰기 지연 SQL저장소에 쿼리 저장(Write-behind storage)
3) 이후, flush() or commit() 시점에 실제 DB에 전송 - Flush가 발생하면,
1) Dirty Checking 수행 :: 변경된 필드가 있는지 확인
2) 쓰기 지연 SQL 저장소에 쿼리 생성
3) DB에 SQL 실행 (INSERT, UPDATE, DELETE)
3.3 FlushModeType 종류
옵션 | 설명 |
FlushModeType.COMMIT (기본값) | 트랜잭션 commit 시 flush 수행 일반적으로 Commit이 성능이 더 좋지만, 데이터의 정합성이 깨질 수 있음 |
FlushModeType.AUTO / ALL | JPQL 실행 시도 전에 flush 자동 수행 Auto의 경우 JPQL이 실행되기 전에 flush되므로, 정합성 유지에 유리 |
참고
asac 수업자료