영속성 전이와 고아 객체

땡글이·2023년 3월 8일
0

JPA

목록 보기
4/9

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이(transitive persistence)를 사용하면 된다.

JPA는 CASCADE 옵션으로 영속성 전이를 제공한다. 쉽게 말해, 영속성 전이를 사용하면, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수도 있고, 부모 엔티티를 삭제할 때 자식 엔티티도 함께 삭제할 수 있다.

CascadeType 종류

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

참고로, PERSIST, REMOVE는 em.persist()em.remove() 를 실행할 때, 바로 전이가 발생하지 않고 플러시를 호출할 때 전이가 발생한다.

영속성 전이 : 저장

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "parent")
    private List<Child> childList = new ArrayList<>();
}
@Entity
public class Child {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}
Parent parent = new Parent();
em.persist(parent);

Child child1 = new Child();
child1.setParent(parent);
parent.getChildList().add(child1);
em.persist(child1);             // 자식1 저장

Child child2 = new Child();
child2.setParent(parent);
parent.getChildList().add(child1);
em.persist(child2);             // 자식2 저장

위의 예제에서는 자식 엔티티도 직접 영속화해줘야 했다. 하지만, 영속성 전이 기능을 활용하면 그렇게 하지 않아도 된다.

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child> childList = new ArrayList<>();
}

Parent 클래스에서 이렇게 CascadeType.PERSIST 로 설정하게 되면, 해당 부모 엔티티와 연관된 자식 엔티티들도 같이 영속성 컨텍스트에 저장된다. 즉, 아래처럼 작성해도 자식 엔티티는 저장된다.

Parent parent = new Parent();

Child child1 = new Child();
child1.setParent(parent);
parent.getChildList().add(child1);

Child child2 = new Child();
child2.setParent(parent);
parent.getChildList().add(child2);

em.persist(parent);

영속성 전이 : 삭제

영속성 전이 기능 없이 부모 엔티티와 자식 엔티티를 모두 제거하려면 다음 코드처럼 각각의 엔티티를 하나씩 제거해야 한다.

Parent findParent = em.find(Parent.class, 1L);
Child findChild1 = em.find(Child.class, 1L);
Child findChild2 = em.find(Child.class, 2L);

em.remove(findParent);
em.remove(findChild1);
em.remove(findChild2);

하지만, CascadeType.REMOVE를 활용한 영속성 전이를 활용한다면, 부모 엔티티만 삭제함으로써 연관된 자식 엔티티도 함께 삭제할 수 있다.

Parent findParent = em.find(Parent.class, 1L);

em.remove(findParent);

코드를 실행하면, DELETE SQL을 3번 실행하고 부모는 물론, 연관된 자식도 모두 삭제된다. 삭제 순서는 외래키 제약조건을 고려해서 자식을 먼저 삭제하고, 부모를 삭제한다.

만약, CascadeType.REMOVE를 설정하지 않고 이 코드를 실행하게 되면 어떻게 될까? 그러면 부모 엔티티만 삭제된다. 하지만, 데이터베이스의 부모 로우만 삭제하는 순간, 자식 테이블에 걸려 있는 외래 키 제약조건으로 인해, 데이터베이스의 외래 키 무결성 예외가 발생한다.

// 데이터베이스의 외래 키 무결성 예외
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation: "FKLH67J1N7X7GT59U0PBKWQH6O6: PUBLIC.CHILD FOREIGN KEY(PARENT_ID) REFERENCES PUBLIC.PARENT(ID) (CAST(1 AS BIGINT))"; SQL statement:
/* delete org.example.domain. [23503-214]

고아 객체

JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데, 이를 고아 객체(ORPHAN) 제거 라고 한다.

이 기능을 사용하면, 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제되도록 할 수 있다.

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> childList = new ArrayList<>();
}
Parent findParent = em.find(Parent.class, 1L);
findParent.getChildList().remove(0);

즉, orphanRemoval = true 옵션을 통해 컬렉션에서 엔티티를 제거하면 데이터베이스의 데이터도 삭제된다. 고아 객체 제거 기능은 영속성 컨텍스트를 플러시할 때 적용되므로 플러시 시점에 DELETE SQL이 실행된다.

고아 객체 제거 기능을 이용할 때, 모든 자식 엔티티를 제거하려면 parent.getChildList().clear(); 처럼 컬렉션을 비우면 된다.

고아 객체 정리 (중요)

고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고, 삭제하는 기능이다. 따라서 이 기능은 참조하는 곳이 하나일 때만 사용해야 한다!!

쉽게 이야기해서, 특정 엔티티가 개인 소유하는 엔티티에만 이 기능을 적용해야 한다. 만약 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생할 수 있다. 이런 이유로 orphanRemoval@OneToOne, @OneToMany 에서만 사용 가능하다.

그리고 고아 객체 제거에는 기능이 한 가지 더 있다. 개념적으로 볼 때, 부모를 제거하면 자식은 고아가 된다. 따라서 부모를 제거하면 자식도 같이 제거된다. 따라서, 고아 객체 제거 기능을 사용하면 CascadeType.REMOVE 기능도 포함되어 있다.

영속성 전이 + 고아 객체 제거, 생명 주기

CascadeType.ALLorphanRemoval = true 옵션을 동시에 사용하면 어떻게 될까? 일반적으로 엔티티는 EntityManager.persist()를 통해 영속화되고 EntityManager.remove()를 통해 제거된다.

  • 이것은 엔티티 스스로 생명주기를 관리한다는 뜻이다.

그런데 두 옵션(CascadeType.ALLorphanRemoval = true)을 모두 활성화하면, 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다. 예를 들면 다음과 같다.

// 자식을 저장하려면 부모에 등록만 하면 된다 (CASCADE.PERSIST)
Parent parent = em.find(Parent.class, 1L);
parent.addChild(child1);
// 자식을 삭제하려면, 부모에서 제거하면 된다 (orphanRemoval)
Parent parent = em.find(Parent.class, 1L);
parent.getChildList.remove(child1);

영속성 전이는 DDD의 Aggregate Root 개념을 구현할 때 사용하면 편리하다.

Aggregate 란?

DDD(Domain Driven Design)에서 Aggregate는 도메인 모델링에서 중요한 개념 중 하나입니다. Aggregate는 데이터와 해당 데이터를 관리하는 로직의 그룹으로 정의됩니다. Aggregate 내부의 데이터와 로직은 외부에서 접근이 가능한 단일 단위로 존재이다.

Aggregate Root 란?

Aggregate root는 Aggregate 내부에서 가장 핵심적인 객체를 의미합니다. Aggregate root는 다른 Aggregate 객체들과 관계를 맺는 진입점 역할을 합니다.
즉, Aggregate root는 Aggregate 내부에서 다른 객체들과의 관계를 정의하고, 해당 Aggregate를 관리하는 데 필요한 비즈니스 규칙과 로직을 구현합니다.

예시

주문 시스템에서 주문(Order)주문 항목(OrderItem)Aggregate로 정의할 수 있습니다. 이때, 주문(Order)Aggregate root가 되며, 주문 항목(OrderItem)은 주문(Order)에 속해있는 객체로 볼 수 있습니다.
Aggregate root인 주문(Order)은 주문 항목(OrderItem)을 관리하며, 주문(Order)의 식별자(OrderId)를 통해 다른 객체들과 관계를 맺습니다.

Reference

자바 ORM 표준 JPA 프로그래밍
https://eocoding.tistory.com/36
https://blog.decorus.io/engineering/domain%20driven%20design/2022/05/06/design-and-management-of-aggregate-root-ddd.html

profile
꾸벅 🙇‍♂️ 매일매일 한발씩 나아가자잇!

0개의 댓글