영속성 전이(Cascade)
- 영속성 전이란 특정 엔티티의 영속성 상태를 변경할 때 그 엔티티와 연관된 엔티티의 영속성에도 영향을 미쳐 영속성 상태를 변경하는 것
- 예를 들어 아래 코드에서 @OneToMany 어노테이션의 인터페이스를 살펴보면 cascade()라는 요소를 볼 수 있다.
public @interface OneToMany {
Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default FetchType.LAZY;
String mappedBy() default "";
boolean orphanRemoval() default false;
}
- 영속성 전이에 사용되는 타입은 엔티티 생명주기와 연관이 있음
- 한 엔티티가 cascade 요소의 값으로 주어진 영속 상태의 변경이 일어나면 매핑으로 연관된 엔티티에도 동일한 동작이 일어나도록 전이를 발생시키는 것
- cascade() 요소의 리턴 타입은 배열 형식인데, 이는 개발자가 사용하고자 하는 cascade 타입을 골라 각 상황에 적용할 수 있다는 것이다.
영속성 전이 타입의 종류
ALL: 모든 영속 상태 변경에 대해 영속성 전이를 적용
PERSIST: 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화
MERGE: 엔티티를 영속성 컨텍스트에 병합할 때 연관된 엔티티도 병합
REMOVE: 엔티티를 제거할 때 연관된 엔티티도 제거
REFRESH: 엔티티를 새로고침할 때 연관된 엔티티도 새로고침
DETACH: 엔티티를 영속성 컨텍스트에서 제외하면 연관된 엔티티도 제외
영속성 전이 적용
예시 코드로 사용할 Product 엔티티와 `Provider` 엔티티는 다대일 양방향 연관관계로, 여러 Product는 하나의 Provider에 속할 수 있고, 하하나의 Provider는 여러 Product를 가질 수 있습니다.
@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Table(name = "product")
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long number;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer price;
@Column(nullable = false)
private Integer stock;
@ManyToOne
@JoinColumn(name = "provider_id")
@ToString.Exclude
private Provider provider;
}
@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Table(name = "provider")
public class Provider extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST, orphanRemoval = true)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
- Provider 클래스에서 Product 리스트에 대한 필드는 cascade 타입중 PERSIST를 적용했다.
- PERSIST는 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화 하는 것으로, Provider 엔티티가 DB에 저장되면 연관된 Product 엔티티까지 함께 저장된다.
아래는 테스트 코드이다.
@Test
void cascadeTest() {
Provider provider = new Provider();
provider.setName("New Provider");
Product product1 = savedProduct("상품1", 1000, 1000);
Product product2 = savedProduct("상품2", 500, 1500);
Product product3 = savedProduct("상품3", 750, 500);
// 연관관계 설정
product1.setProvider(provider);
product2.setProvider(provider);
product3.setProvider(provider);
provider.getProductList().addAll(Lists.newArrayList(product1, product2, product3));
providerRepository.save(provider);
}
private Product savedProduct(String name, Integer price, Integer stock) {
Product product = new Product();
product.setName(name);
product.setPrice(price);
product.setStock(stock);
return product;
}
위 코드에서는 product를 직접적으로 저장하지 않고, provider에 필드인 productList에 product들을 추가하여 provider를 저장했다. 그러면 아래와 같은 Hibernate 쿼리가 발생한다.
Hibernate:
insert
into
provider
(created_at, name, updated_at)
values
(?, ?, ?)
Hibernate:
insert
into
product
(created_at, name, price, provider_id, stock, updated_at)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
insert
into
product
(created_at, name, price, provider_id, stock, updated_at)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
insert
into
product
(created_at, name, price, provider_id, stock, updated_at)
values
(?, ?, ?, ?, ?, ?)
- insert 문으로 Product 객체들이 함께 쿼리가 발생한 것을 볼 수 있다.
- 이처럼 CascadeType.PERSIST 을 설정하면 영속 상태의 변화에 따라 연관된 엔티티들의 동작도 함께 수행하게 된다.
- 다만 REMOVE, ALL 같은 타입을 무분별하게 사용하면 연관된 엔티티에 의도치 않은 영향을 미칠 수 있기 때문에 사이드 이펙트를 고려해서 사용해야 한다.
고아 객체(Orphan)
- JPA에서 고아(orphan)란 부모 엔티티와 연관관계가 끊어진 엔티티를 의미한다.
- JPA에는 이러한 고아 객체를 자동으로 제거하는 기능이 있다.
아래 Provider 엔티티에서 productList 필드에 대한 orphanRemoval = true 옵션은 고아 객체를 제거하는 기능이다.
@OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST, orphanRemoval = true)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
아래 테스트 코드로 동작을 확인해보자
@Test
@Transactional
void orphanRemovalTest() {
Provider provider = savedProvider("New Provider");
Product product1 = savedProduct("Product1", 1000, 1000);
Product product2 = savedProduct("Product2", 500, 1500);
Product product3 = savedProduct("Product3", 750, 500);
// 연관관계 설정
product1.setProvider(provider);
product2.setProvider(provider);
product3.setProvider(provider);
provider.getProductList().addAll(Lists.newArrayList(product1, product2, product3));
providerRepository.saveAndFlush(provider);
System.out.println("## Before Removal ##");
System.out.println("## provider list ##");
providerRepository.findAll().forEach(System.out::println);
System.out.println("## product list ##");
productRepository.findAll().forEach(System.out::println);
// 연관관계 제거
Provider foundProvider = providerRepository.findById(1L).get();
foundProvider.getProductList().remove(0);
System.out.println("## After Removal ##");
System.out.println("## provider list ##");
providerRepository.findAll().forEach(System.out::println);
System.out.println("## product list ##");
productRepository.findAll().forEach(System.out::println);
}
- Provider 엔티티를 가져온 후 첫번째로 매핑되어 있는 Product 엔티티의 연관관계를 제거하였다.
- 결국 첫번째로 매핑되어 있는 Product1 엔티티는 연관관계가 사라져 고아 객체가 되어버린다.
- 이때 orphanRemoval = true를 적용하였으므로 이 객체는 삭제되어야 한다.
실제로 저 테스트 코드를 실행하면 아래와 같은 로그가 발생한다.
Product(super=BaseEntity(createdAt=2024-03-05T23:46:33.838355, updatedAt=2024-03-05T23:46:33.838355), number=1, name=상품1, price=1000, stock=1000, productDetail=null)
Product(super=BaseEntity(createdAt=2024-03-05T23:46:33.842978, updatedAt=2024-03-05T23:46:33.842978), number=2, name=상품2, price=500, stock=1500, productDetail=null)
Product(super=BaseEntity(createdAt=2024-03-05T23:46:33.844293, updatedAt=2024-03-05T23:46:33.844293), number=3, name=상품3, price=750, stock=500, productDetail=null)
## After Removal ##
## provider list ##
Hibernate:
select
p1_0.id,
p1_0.created_at,
p1_0.name,
p1_0.updated_at
from
provider p1_0
Provider(super=BaseEntity(createdAt=2024-03-05T23:46:33.812593, updatedAt=2024-03-05T23:46:33.812593), id=1, name=새로운 공급업체)
## product list ##
Hibernate:
delete
from
product
where
number=?
Hibernate:
select
p1_0.number,
p1_0.created_at,
p1_0.name,
p1_0.price,
p1_0.provider_id,
p1_0.stock,
p1_0.updated_at
from
product p1_0
Product(super=BaseEntity(createdAt=2024-03-05T23:46:33.842978, updatedAt=2024-03-05T23:46:33.842978), number=2, name=상품2, price=500, stock=1500, productDetail=null)
Product(super=BaseEntity(createdAt=2024-03-05T23:46:33.844293, updatedAt=2024-03-05T23:46:33.844293), number=3, name=상품3, price=750, stock=500, productDetail=null)
Hibernate에 의해 delete 쿼리가 실행되어 Product1이 삭제된 것을 확인할 수 있다.
참고
- 스프링 부트 핵심 가이드 "스프링 부트를 활용한 애플리케이션 개발 실무" , 장정우, 2022
- https://github.com/wikibook/springboot/tree/main/chapter9_relationship/src/main/java/com/springboot/relationship/data/entity
'Development > Spring' 카테고리의 다른 글
[Spring] 스프링 부트의 예외 처리 방식 (0) | 2024.03.08 |
---|---|
[Spring] 유효성 검사와 Hibernate Validator (0) | 2024.03.07 |
[Spring] 연관 관계 매핑하기 (1) | 2024.03.05 |
[Spring] JPA Auditing, BaseEntity (0) | 2024.03.03 |
[Spring] QueryDSL (2) | 2024.03.01 |