본문 바로가기
자바/JPA

[JPA/Java] 영속성 전이 : Cascade, 고아 객체

by drCode 2023. 3. 3.
728x90
반응형

어떤 쇼핑몰 사이트에서 주문 정보가 들어왔다.

 

주문 테이블 구조는 Master, Detail 2개의 테이블 구조이고,

 

주문 정보가 들어올 때, 주문 상세 정보가 같이 들어올 때,

 

데이터를 일일이 넣어주는 소스를 넣는 것은 개발자 입장으로서는 참으로 불편한 일이 아닐 수 없다.

 

이럴 때 영속성 전이를 하여 CASCADE 속성을 넣어주면 편하다

 

다만 CASCADE는 하나의 마스터 테이블만을 참조할 때만 사용할 수 있다.

 

영속성 전이 CASCADE 속성은 하나의 Master 테이블을 참조할 때만 사용해야한다.

  • 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속상태로 만들고 싶을 때 CASCADE를 사용한다.
  • 예 : 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하는 형태

 

 

※ 영속성 전이: 저장

@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)

영속성 전이 : 저장

하나의 부모 구조에 여러 자식의 구조인 경우, 저장 역시 CASCADE를 사용할 수 있다.

 

※ Cascade 주의사항

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.

 

★ 예시

Parent.java

package hellojpa;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

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

    private String name;

    @OneToMany(mappedBy = "parent")
    private List<Child> childList = new ArrayList<>();

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }

    public List<Child> getChildList() {
        return childList;
    }

    public void setChildList(List<Child> childList) {
        this.childList = childList;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

Child.java

package hellojpa;

import javax.persistence.*;

@Entity
public class Child {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;

    public Parent getParent() {
        return parent;
    }

    public void setParent(Parent parent) {
        this.parent = parent;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

만약 Parent에 cascade 옵션이 없다면.

 

JpaMain.java

try {
    Child child1 = new Child();
    Child child2 = new Child();
    
    Parent parent = new Parent();
    parent.addChild(child1);
    parent.addChild(child2);
    
    em.persist(parent);
    em.persist(child1);
    en,persist(child2);

    tx.commit();
}

와 같이 child1, child2 에 대해 일일이  persist 를 실행해줘야 한다.

 

하지만, 

Parent에

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

위와 같이 cascade 설정이 들어가 있다면

 

JpaMain 에 이렇게 쓸 수 있다.

try {
    Child child1 = new Child();
    Child child2 = new Child();
    
    Parent parent = new Parent();
    parent.addChild(child1);
    parent.addChild(child2);
    
    em.persist(parent);

    tx.commit();
}

위 소스를 실행하면 아래와 같이 결과가 나온다.

 

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Parent
        */ insert 
        into
            Parent
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, PARENT_ID, id) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, PARENT_ID, id) 
        values
            (?, ?, ?)
3월 03, 2023 10:04:26 오전 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/jpaStudy2]

Process finished with exit code 0

child1, child2 에 대해서 persist를 하지 않아도 영속성이 전이되어 데이터가 입력되는 것을 알 수 있다.

 

CASCADE의 종류는 아래와 같다.

  • ALL : 모두 적용
  • PERSIST : 영속
  • REMOVE : 삭제
  • MERGE : 병합
  • REFRESH : REFRESH
  • DETACH : DETACH

이 중에서 자주 쓰는 것은 ALL, PERSIST, REMOVE 정도 되겠다.

 

고아 객체

 

고아 객체 제거는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 것이다.

 

옵션은 orphanRemoval = true 를 사용하면 된다.

 

Parent parent1 = em.find(Parent.class, id);

parent1.getChildren().remove(0);
// 자식 엔티티를 컬렉션에서 제거

위와 같이 사용할 때, 자식 중 0번째 인덱스의 자식을 제거할 수 있다.

 

Parent.java

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

 

JpaMain.java

try {
    Child child1 = new Child();
    Child child2 = new Child();

    Parent parent = new Parent();
    parent.addChild(child1);
    parent.addChild(child2);

    em.persist(parent);

    em.flush();
    em.clear();

    Parent findParent = em.find(Parent.class, parent.getId());
    findParent.getChildList().remove(0);

    tx.commit();
}

위의 소스를 실행하면

 

아래와 같이 나온다.

 

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Parent
        */ insert 
        into
            Parent
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, PARENT_ID, id) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, PARENT_ID, id) 
        values
            (?, ?, ?)
Hibernate: 
    select
        parent0_.id as id1_8_0_,
        parent0_.name as name2_8_0_ 
    from
        Parent parent0_ 
    where
        parent0_.id=?
Hibernate: 
    select
        childlist0_.PARENT_ID as PARENT_I3_2_0_,
        childlist0_.id as id1_2_0_,
        childlist0_.id as id1_2_1_,
        childlist0_.name as name2_2_1_,
        childlist0_.PARENT_ID as PARENT_I3_2_1_ 
    from
        Child childlist0_ 
    where
        childlist0_.PARENT_ID=?
Hibernate: 
    /* delete hellojpa.Child */ delete 
        from
            Child 
        where
            id=?
3월 03, 2023 10:21:24 오전 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/jpaStudy2]

Process finished with exit code 0

 

JPA가 em.find로 찾은 부모 객체에서 0번째 자식이 사라진 것을 탐지하고

 

알아서 쿼리를 실행해서 자식을 제거한다.

 

고아 객체의 주의사항으로는

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능을 한다.
  • 무조건 참조하는 곳이 하나일 때 사용해야 한다.
  • 특정 엔티티를 개인 소유할 때 사용해야 한다.
  • @OneToOne, @OneToMany 만 가능하다

※ 참고 : 개념적으로 부모를 제거하면 자식은 고아가 된다.

따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다.

이것은 CascadeType.REMOVE 처럼 동작한다.

 

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

  • CascadeType.ALL + orphanRemoval = true 와 같이 사용한다.
  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거할 수 있다.
  • 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
  • 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.

JpaMain.java

try {
    Child child1 = new Child();
    Child child2 = new Child();

    Parent parent = new Parent();
    parent.addChild(child1);
    parent.addChild(child2);

    em.persist(parent);

    em.flush();
    em.clear();

    Parent findParent = em.find(Parent.class, parent.getId());
    em.remove(findParent);

    tx.commit();
}

위의 소스를 실행하면 아래와 같이 결과가 나온다 

 

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Parent
        */ insert 
        into
            Parent
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, PARENT_ID, id) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert hellojpa.Child
        */ insert 
        into
            Child
            (name, PARENT_ID, id) 
        values
            (?, ?, ?)
Hibernate: 
    select
        parent0_.id as id1_8_0_,
        parent0_.name as name2_8_0_ 
    from
        Parent parent0_ 
    where
        parent0_.id=?
Hibernate: 
    select
        childlist0_.PARENT_ID as PARENT_I3_2_0_,
        childlist0_.id as id1_2_0_,
        childlist0_.id as id1_2_1_,
        childlist0_.name as name2_2_1_,
        childlist0_.PARENT_ID as PARENT_I3_2_1_ 
    from
        Child childlist0_ 
    where
        childlist0_.PARENT_ID=?
Hibernate: 
    /* delete hellojpa.Child */ delete 
        from
            Child 
        where
            id=?
Hibernate: 
    /* delete hellojpa.Child */ delete 
        from
            Child 
        where
            id=?
Hibernate: 
    /* delete hellojpa.Parent */ delete 
        from
            Parent 
        where
            id=?
3월 03, 2023 10:37:17 오전 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/jpaStudy2]

Process finished with exit code 0

부모를 삭제하면 일괄적으로

 

연관된 자식들을 모두 지우는 내용을 확인할 수 있다.

 

※ 맺음말

이처럼 cascade, orphanRemoval 속성을 실무에서 많이 사용한다

1:N 인 모든 경우에 다 사용하지는 않는데

하나의 부모가 자식들을 관리할 때는 굉장히 유용하다.

하나의 게시판에 여러 개 첨부파일을 하나의 경로로 사용할 때는 유용하지만

파일을 여러 개의 경로에서 관리할 때는 쓰면 안된다.

단일 사용자의 경우에도 cascade와 orphanRemoval 속성을 사용할 때 유용하다

 

728x90
반응형

댓글