본문 바로가기
번역/Spring Boot 공식문서

[JPA] Cascade Type 비교 | 공식 문서 번역

by 독서왕뼝아리 2023. 3. 30.
영어 공부 && 취미로 공식 문서 번역합니다.
의역, 오역 주의!!!!!!!!
영어 못함

원문 : https://www.baeldung.com/jpa-cascade-types

 

Overview of JPA/Hibernate Cascade Types. | Baeldung

A quick and practical overview of JPA/Hibernate Cascade Types.

www.baeldung.com

 


1. Overview

이 문서에서는 JPA/Hibernate의 의미를 따라서 다양한 cascade 타입을 다룰 것입니다.

 

2. Cascading이란?

엔티티는 주로 다른 엔티티 존재에 의존하게 됩니다. 예를 들어 Person-Address 관계처럼요. Person 엔티티가 존재하지 않는다면 Address 엔티티는 그자체로 어떠한 의미를 갖지 않습니다. 우리가 Person 엔티티를 제거했을 때 연관된 Address 엔티티도 삭제되어야 합니다.

Cascading으로 이 문제를 해결할 수 있습니다. 타겟 엔티티에 어떤 액션을 취했을 때 동일한 액션이 연관된 엔티티에 적용될 것입니다.

 

2.1. JPA Cascade Type

모든 JPA의 cascade 작업은 javax.persistence 안에 표현됩니다. Cascade 타입은 열거형 타입으로 다음과 같습니다:

  • ALL
  • PERSIST
  • MERGE
  • REMOVE
  • REFRESH
  • DETACH

 

2.2. Hibernate Cascade Type

Hibernate는 JPA에서 제공하는 cascade 타입에 3가지 타입을 추가적으로 더 제공합니다. org.hibernate.annotations.CascadeType 으로 사용가능합니다.

  • REPLICATE
  • SAVE_UPDATE
  • LOCK

 

3. 각 Cascade 타입 차이점

3.1. CascadeType.ALL

CascadeType.ALL은 Hibernate 특화 옵션을 포함해, 부모에서 자식으로 전해지는 모든 작업을 전파합니다. 

예시를 함께 봅시다:

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String name;
    @OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
    private List<Address> addresses;
}

cascade 타입을 명시한 OneToMany 결합을 기억하세요. 

그럼 Person과 연관된 Address 엔티티를 봅시다:

@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String street;
    private int houseNumber;
    private String city;
    private int zipCode;
    @ManyToOne(fetch = FetchType.LAZY)
    private Person person;
}

 

3.2. CascadeType.PERSIST

Persist 작업은 일시적인 인스턴스를 영속화 합니다 (?) Cascade Type PERSIST는 부모에서 자식 엔티티로 영속화 작업을 전파합니다. Person 엔티티를 저장했을 때, Address 엔티티 또한 같이 저장됩니다.

테스트 케이스를 통해 예시를 함께 봅시다.:

@Test
public void whenParentSavedThenChildSaved() {
    Person person = new Person();
    Address address = new Address();
    
    address.setPerson(person);
    person.setAddresses(Arrays.asList(address));
    session.persist(person); // person만 저장!
    
    session.flush();
    session.clear();
}

테스트 코드를 실행하면 다음과 같이 쿼리가 발생합니다:

Hibernate: insert into Person (name, id) values (?, ?)
Hibernate: insert into Address (
    city, houseNumber, person_id, street, zipCode, id) values (?, ?, ?, ?, ?, ?)

 

3.3. CascadeType.MERGE

Merge 작업은 주어진 객체의 상태를 같은 식별자를 가진 영속된 객체에 복사하는 작업입니다. CascadeType.MERGE 부모에서 자식 엔티티로 병합 작업을 전파합니다.

테스트 코드로 살펴봅시다:

@Test
public void whenParentSavedThenMerged() {
    int addressId;
    Person person = buildPerson("devender");
    Address address = buildAddress(person);
    person.setAddresses(Arrays.asList(address));
    
    session.persist(person);
    session.flush();
    
    addressId = address.getId();
    session.clear();

    Address savedAddressEntity = session.find(Address.class, addressId);
    Person savedPersonEntity = savedAddressEntity.getPerson();
    savedPersonEntity.setName("devender kumar");
    savedAddressEntity.setHouseNumber(24);
    
    session.merge(savedPersonEntity);
    session.flush();
}

코드를 실행하면 SQL 쿼리가 다음처럼 발생합니다:

Hibernate: select address0_.id as id1_0_0_, address0_.city as city2_0_0_, address0_.houseNumber as houseNum3_0_0_, address0_.person_id as person_i6_0_0_, address0_.street as street4_0_0_, address0_.zipCode as zipCode5_0_0_ from Address address0_ where address0_.id=?
Hibernate: select person0_.id as id1_1_0_, person0_.name as name2_1_0_ from Person person0_ where person0_.id=?
Hibernate: update Address set city=?, houseNumber=?, person_id=?, street=?, zipCode=? where id=?
Hibernate: update Person set name=? where id=?

Merge 작업 결과로, 첫 번째로 Address와 Person 엔티티가 select 되었고, 그 다음 둘 다 업데이트 된 것을 확인할 수 있습니다.

(person 저장 후 → address.getId()한 값으로 저장된 address 엔티티 불러옴 → address와 연관된 person 불러옴 → person 필드 값 변경, address 필드 값 변경 → MERGE 작업 → person과 address 모두 업데이트)

 

3.4. CascadeType.REMOVE

이름에서도 알 수 있 듯, remove 작업은 데이터베이스에서 튜플이 제거 되었을 때, 연관된 영속 컨텍스트에서도 제거시킵니다.

CascadeType.REMOVE은 부모에서 자식 엔티티까지 제거 작업을 전파합니다. Hibernate에서는 JPA의 CascadeType.REMOVE  작업과 동일한 CascadeType.DELETE 타입을 제공합니다. 

그럼 CascadeType.Remove를 테스트해봅시다:

@Test
public void whenParentRemovedThenChildRemoved() {
    int personId;
    Person person = buildPerson("devender");
    Address address = buildAddress(person);
    person.setAddresses(Arrays.asList(address));
    session.persist(person);
    session.flush();
    
    personId = person.getId();
    session.clear();
	
    // person 삭제
    Person savedPersonEntity = session.find(Person.class, personId);
    session.remove(savedPersonEntity);
    session.flush();
}

테스트 코드를 실행시키면 다음 SQL이 실행됩니다:

Hibernate: delete from Address where id=?
Hibernate: delete from Person where id=?

CascadeType.REMOVE의 결과로 person 엔티티와 연관된 address 엔티티가 연쇄적으로 삭제됐습니다.

 

3.5. CascadeType.DETACH

Detach 작업은 영속 컨텍스트에서 엔티티를 제거합니다. 

The detach operation removes the entity from the persistent context. Type.DETACH를 사용하면 자식 엔티티는 영속 컨텍스트에서 제거될 것입니다. 

살펴볼까요?:

@Test
public void whenParentDetachedThenChildDetached() {
    Person person = buildPerson("devender");
    Address address = buildAddress(person);
    person.setAddresses(Arrays.asList(address));
    session.persist(person);
    session.flush();
    
    assertThat(session.contains(person)).isTrue();
    assertThat(session.contains(address)).isTrue();

    session.detach(person);
    assertThat(session.contains(person)).isFalse();
    assertThat(session.contains(address)).isFalse();
}

person 엔티티를 제거(detaching)한 후 person도 address도 영속 컨텍스트에 존재하지 않는 것을 확인할 수 있습니다.

 

3.6. CascadeType.LOCK

본의 아니게, CascadeType.LOCK는 엔티티와 연관된 자식 엔티티와 재결합시킵니다.

CascadeType.LOCK를 이해하기 위해 테스트를 해보죠:

@Test
public void whenDetachedAndLockedThenBothReattached() {
    Person person = buildPerson("devender");
    Address address = buildAddress(person);
    person.setAddresses(Arrays.asList(address));
    session.persist(person);
    session.flush();
    
    assertThat(session.contains(person)).isTrue();
    assertThat(session.contains(address)).isTrue();

    session.detach(person);
    assertThat(session.contains(person)).isFalse();
    assertThat(session.contains(address)).isFalse();
    
    session.unwrap(Session.class)
      .buildLockRequest(new LockOptions(LockMode.NONE))
      .lock(person);

    assertThat(session.contains(person)).isTrue();
    assertThat(session.contains(address)).isTrue();
}

CascadeType.LOCK을 사용했을 때 person 엔티티와 연관된 address 엔티티가 다시 영속 컨텍스트에 결합된 것을 확인할 수 있습니다!

 

3.7. CascadeType.REFRESH

Refresh 작업은 데이터베이스에서 인스턴스 값을 다시 읽어오게 합니다. 데이터 저장 후 인스턴스를 변경이 필요하지만 나중에 변경을 취소해야 할 때같은 상황에서 유용할 것입니다. Cascade Type REFRESH 작업을 사용하면 부모 엔티티가 변경될 때마다 자식 엔티티를 데이터베이스에서 다시 읽어오게 됩니다.

이해를 돕기 위해 테스트 케이스를 살펴봅시다:

@Test
public void whenParentRefreshedThenChildRefreshed() {
    Person person = buildPerson("devender");
    Address address = buildAddress(person);
    person.setAddresses(Arrays.asList(address));
    session.persist(person);
    session.flush();
    
    person.setName("Devender Kumar");
    address.setHouseNumber(24); // 부모 changed
    session.refresh(person); // 부모 refreshed, 자식도 refreshed
    
    assertThat(person.getName()).isEqualTo("devender");
    assertThat(address.getHouseNumber()).isEqualTo(23); // 초깃값과 동일
}

이미 저장한 person과 address 엔티티가 변경했습니다만, person 엔티티를 refresh 했을 때 address도 refresh된 것을 확인할 수 있습니다.

 

3.8. CascadeType.REPLICATE

Replicate 작업은 둘 이상의 데이터 소스가 있고 데이터 동기화하려는 경우에 사용됩니다.

또한 CascadeType.REPLICATE를 사용하면 동기화 작업은 부모 엔티티가 어떤 행동을 하더라도 자식 엔티티에 전파됩니다.

CascadeType.REPLICATE를 테스트해봅시다:

@Test
public void whenParentReplicatedThenChildReplicated() {
    Person person = buildPerson("devender");
    person.setId(2);
    Address address = buildAddress(person);
    address.setId(2);
    person.setAddresses(Arrays.asList(address));
    session.unwrap(Session.class).replicate(person, ReplicationMode.OVERWRITE);
    session.flush();
    
    assertThat(person.getId()).isEqualTo(2);
    assertThat(address.getId()).isEqualTo(2);
}

CascadeType.REPLICATE 덕분에 person 엔티티를 복제했을 때 연관된 address 엔티티 또한 변경된 식별자(=2)로 복제되었습니다

 

3.9. CascadeType.SAVE_UPDATE

CascadeType.SAVE_UPDATE는 부모의 작업을 자식 엔티티에게도 전파합니다. Hibernate에서는 save, update, saveOrUpdate 메서드로 제공합니다.

 

CascadeType.SAVE_UPDATE가 어떻게 작동하는지 봅시다:

@Test
public void whenParentSavedThenChildSaved() {
    Person person = buildPerson("devender");
    Address address = buildAddress(person);
    person.setAddresses(Arrays.asList(address));
    
    session.saveOrUpdate(person);
    session.flush();
}

CascadeType.SAVE_UPDATE 덕분에 테스트를 실행하면 person과 address가 같이 저장됩니다.

SQL 결과는 다음과 같습니다:

Hibernate: insert into Person (name, id) values (?, ?)
Hibernate: insert into Address (
    city, houseNumber, person_id, street, zipCode, id) values (?, ?, ?, ?, ?, ?)

 

 

4. 결론

이 문서에서 cascading이 무엇인지, JPA와 Hibernate에서 사용 가능한 cascade 옵션을 살펴보았습니다.

해당 소스코드는 available on GitHub.에서 확인 가능합니다.