DEV ℧ Developer Diary

[JPA] 영속성 전이(CASCADE)와 고아 객체

해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 듣고 정리한 글입니다.

영속성 전이(CASCADE)와 고아 객체

영속성 전이: CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때
  • 예: 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장.

cascade

영속성 전이 사용법

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

위의 코드와 같이 연관관계를 선언한 어노테이션에 cascade 속성을 부여하면 된다.

cascade

영속성 전이를 왜 사용할까?

간단한 예시를 들어보자, 예시는 Parent에 Child가 여러개 들어가는 구조를 작성할 것이다.

@Entity
@Getter @Setter
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);
    }
}
@Entity
@Getter
@Setter
public class Child {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String name;

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

해당 Parent - Child 구조를 JPA를 통해 DB에 저장해 보자.

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

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

entityManager.persist(parent);

만약 Child를 Parent에 넣어 줬으니 Parent만 영속성 컨텍스트에 넣어서 저장하면 어떻게 될까?

cascade2

위의 사진처럼 Parent의 데이터만 DB에 저장 되고 Child의 데이터는 들어가지 않는다.

이렇게 영속성 컨텍스트에 모든 데이터를 넣어주기 위해서는 아래의 소스와 같이 entityManager.persist(); 를 넣어줄 객체만큼 호출 해야한다.

entityManager.persist(parent);
entityManager.persist(child1);
entityManager.persist(child2);

이러한 불편함을 해결하기 위해 Cascade가 있다.

Parent클래스의 childList 필드에 Cascade를 추가해보도록 하자.

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

그리고 위의 parent만 영속성에 넣어준 예제코드를 다시 실행해 보도록 하자.

cascade2

위의 결과처럼 Child까지 모두 들어간것을 확인 할 수 있다.

이렇게 cascade를 선언한 부모객체의 연관관계를 모두 저장 할 것인지 설정하고자 하는것이 cascade라고 볼 수 있다.

영속성 전이: CASCADE - 주의!

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음.
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
  • CASCADE는 Parent - Child와 같은 단일관계 외에 만약 Child가 다른쪽에서도 연관관계로 사용중일 경우는 cascade를 사용하는것을 지양해야 한다.

CASCADE의 종류

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

고아객체

  • 고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티 를 자동으로 삭제
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST, orphanRemoval = true)

고아객체 제거 예제

컬렉션 객체에서 remove같은 메서드를 통해 제거되는 데이터를 종속적으로 DELETE 쿼리를 만들어 날려준다.

@Entity
@Getter @Setter
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    /* orphanRemoval = true 고아객체 제거 추가 */
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Child> childList = new ArrayList<>();

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }
}
@Entity
@Getter
@Setter
public class Child {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String name;

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

Entity는 orphanRemoval = true 속성만 추가하고 나머지는 그대로 사용해준다.

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

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

entityManager.persist(parent);

entityManager.flush();
entityManager.clear();

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

그리고 해당 Parent에 child의 데이터를 두개 넣고 하나를 findParent.getChildList().remove(0); 를 통해 0번째 인덱스에 있는 child 데이터를 지워준다.

만약 orphanRemoval = true 속성이 선언되어 있을 경우.

cascade2

위의 결과와 같이 0번째 인덱스인 id가 2인 child 데이터가 삭제된 것을 확인 할 수 있다.

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

만약 위의 소스와 같이 parent를 삭제한다면 연관된 child까지 모두 지워진다.

고아 객체 - 주의 할점

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 하나일 때 사용해야함!
  • 특정 엔티티가 개인 소유할 때 사용
  • @OneToOne, @OneToMany만 가능
  • 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.
  • 예: 게시판의 첨부파일 등

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

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

Aggregate Root : CascadeType.ALL + orphanRemovel=true 해당 속성으로 생명주기를 관리할 수 있기 때문에 Root에 들어가는 Entity 외에는 Repository 를 만들지 않는다는 뜻이다.