[JPA] 영속성 전이와 고아 객체 관리

3Beom's 개발 블로그·2022년 12월 3일
0

SpringJPA

목록 보기
11/21
post-custom-banner

출처

본 글은 인프런의 김영한님 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 수강하며 기록한 필기 내용을 정리한 글입니다.

-> 인프런
-> 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의


0. 예시 설정

  • 다음과 같이 엔티티 두 개를 예시로 설정한다.
    • 글 (Post)
    • 댓글 (Comment)
  • 두 엔티티는 다음과 같이 다대일 연관관계로 설정되어 있다.
    • Comment : Post = N : 1
  • Post 엔티티는 Comment 객체의 리스트 필드를 갖고 있다.
    • @OneToMany
  • 각 엔티티는 다음과 같이 구현되어 있다.

(GETTER, SETTER 생략)

< 글 (Post) >

  • 연관관계 편의 메소드 추가
    • addCommentAndSetPost(Comment comment)
@Entity
public class Post {
    @Id @GeneratedValue
    @Column(name = "POST_ID")
    private Long id;

    @Column(name = "TITLE")
    private String title;

    @Column(name = "CONTENT")
    private String content;
    
    @OneToMany(mappedBy = "post")
    private List<Comment> comments = new ArrayList<>();
    
    ...
    
    public void addCommentAndSetPost(Comment comment) {
      this.comments.add(comment);
      comment.setPost(this);
    }

}

< 댓글 (Comment) >

@Entity
public class Comment {
    @Id @GeneratedValue
    @Column(name = "COMMENT_ID")
    private Long id;

    @Column(name = "CONTENT")
    private String content;

    @ManyToOne
    @JoinColumn(name = "POST_ID")
    private Post post;

}

1. 영속성 전이? 고아 객체?

  • 만약 하나의 글에 두 개의 댓글이 달린다고 할 때, 다음과 같이 구현될 수 있을 것이다.
// Post 생성 후 DB 저장
Post post1 = new Post();
post1.setTitle("Post Title");
post1.setContent("Post Content");
entityManager.persist(post1);

// Comment 생성 후 DB 저장
Comment comment1 = new Comment();
comment1.setContent("comment1");
post1.addCommentAndSetPost(comment1);
entityManager.persist(comment1);

// Comment 생성 후 DB 저장
Comment comment2 = new Comment();
comment2.setContent("comment2");
post1.addCommentAndSetPost(comment2);
entityManager.persist(comment2);
  • 위 코드를 보면 Post와 각각의 Comment들이 모두 따로 persist 되는 것을 볼 수 있다.
  • 여기서 다음과 같이 가정해 보자.
  • 댓글은 글에만 달리고, 다른 곳에는 활용되지 않는다.
    • 댓글이 유저 프로필에 달리거나, 주문 목록에 달리진 않을 것이다.
  • 그렇다면 Comment는 온전히 Post만을 FK로 가리키고 있을 것이다.
    • Post 외에는 연관관계를 맺는 엔티티가 없다.
  • 본 상황을 통해 영속성 전이고아 객체의 개념을 파악할 수 있다.

< 영속성 전이 >

  • 본 상황에서 Post 엔티티가 Comment 엔티티를 관리해도 될 것이다.
  • 예를 들어 굳이 여러 개의 Comment 들을 하나하나 persist하는게 아니라, Post 의 comments 필드에 넣어놓고 Post만 persist 하면 자동으로 해당 Comment들도 DB에 저장되도록 하는 것이다.
    => 본 과정이 영속성 전이에 해당한다.

< 고아 객체 >

  • 만약 위 코드와 같이 comment1, comment2 데이터가 post1 데이터를 가리키고 있는 상황에서 post1 데이터가 삭제된다면, comment1과 comment2는 가리키고 있던 데이터를 잃어버리게 된다.
    • Comment 엔티티는 온전히 Post 엔티티만을 FK로 가리키고 있는데, 유일하게 가리키고 있던 데이터가 사라졌으므로
  • 본 상황에서 comment1, comment2의 데이터는 어느 곳에서도 참조할 수 없게 된다.
    • post1 데이터만이 comment1, comment2 데이터를 참조할 수 있었는데, post1 데이터가 삭제되어 버린 것이다.
  • 이처럼 길을 잃어버린 객체를 고아 객체라 한다.

2. 영속성 전이 (CASCADE)

  • 영속성 전이를 구현하기 위해서는 cascade 속성을 활용해야 한다.

< Post >

...

@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
private List<Comment> comments = new ArrayList<>();

...
  • 앞서 다루었듯, Post 엔티티가 Comment 엔티티를 관리해야 하므로, Post 엔티티의 comments 필드에 속성을 걸어주는 것이라 생각하면 편하다.
  • 이렇게 cascade 속성을 적용하면 다음과 같이 post1 객체만을 persist 해도 comment1, comment2 객체까지 DB에 저장된다.
Post post1 = new Post();
post1.setTitle("Post Title");
post1.setContent("Post Content");

Comment comment1 = new Comment();
comment1.setContent("comment1");
post1.addCommentAndSetPost(comment1);

Comment comment2 = new Comment();
comment2.setContent("comment2");
post1.addCommentAndSetPost(comment2);

entityManager.persist(post1);
  • 즉, Post 엔티티 객체 post1을 persist 할 때, post1과 연관되어 있는 comment1, comment2 객체까지 한번에 자동으로 persist 되는 것이다.

2-1. CASCADE 종류

  • CascadeType.ALL
    • persist, remove, detach 등 모든 상태에 대해 관리한다.
  • CascadeType.PERSIST
    • persist 할 때만 관리한다.
  • CascadeType.REMOVE
    • remove 할 때만 관리한다.
  • CascadeType.DETACH
    • detach 할 때만 관리한다.
  • CascadeType.MERGE
    • merge 과정만 관리한다.
  • CascadeType.REFRESH
    • refresh 과정만 관리한다.

2-2. 영속성 전이 유의 사항

  • 영속성 전이는 연관관계 매핑과는 아무런 관련이 없다.
    • 연관된 엔티티도 함께 관리될 수 있도록 편리함을 제공해 주는 것이다.
  • 영속성 전이는 연관된 엔티티가 오직 본인이랑만 연관되어 있을 때 활용하는 것이 좋다.
    • 위에서 가정했듯, Post 엔티티가 관리하는 Comment 엔티티는 다른 엔티티와는 연관관계를 갖지 않을 때 활용하는 것이 좋다.
      (온전히 Post에게만 종속되어 있을 때!)
    • 만약 다른 엔티티와도 연관되어 있는데 활용할 경우, 예상치 못하게 삭제되거나 저장될 수 있다.
  • 라이프 사이클이 거의 유사한 경우에 활용하는 것이 좋다.

3. 고아 객체

  • 앞서 다루었던 고아 객체는 제거되어야 할 것이다.
    • post1이 삭제된 comment1과 comment2는 존재 이유가 없다.
  • 이렇게 고아 객체를 자동으로 삭제해 주는 기능이 있다.
    • orphanRemoval = true
@OneToMany(mappedBy = "post", orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
  • 이렇게 설정하고 다음과 같이 post1을 삭제하면 comment1과 comment2도 삭제된다.
...

Post findPost = entityManager.find(Post.class, post1.getId());
entityManager.remove(findPost);

entityTransaction.commit();

  • 위 사진과 같이 comment1, comment2, post1에 모두 delete쿼리가 전달되는 것을 확인할 수 있다.

3-1. 고아 객체 제거 유의 사항

  • 일단 자동으로 삭제하는 기능이므로, 기본적으로 조심히 사용해야 한다.
  • 본 기능은 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 판단하고, 삭제하는 기능이다.
    • Post 엔티티 객체와의 참조가 끊어지면, 해당 Comment 엔티티 객체를 참조하는 다른 엔티티가 아무 것도 없다고 판단하여 제거하는 것이다.
  • 따라서 본 기능도 참조하는 곳이 하나일 때만 사용해야 한다.
  • 특정 엔티티가 개인 소유할 때 사용해야 한다.
  • @OneToOne, @OneToMany만 사용할 수 있다.

4. CascadeType.REMOVE vs orphanRemoval = true

  • 여기서 의문점이 하나 생긴다.
  • CascadeType.REMOVEorphanRemoval = true 모두 Post를 제거하면 Comment도 제거된다.
  • 그렇다면 둘의 차이점이 있을까?
    -> 본 해답을 다음 글에서 확인할 수 있었다.
    -> JPA CascadeType.REMOVE vs orphanRemoval = true
  • 자세한 내용은 본문에서 확인할 수 있다.
  • 요약하면 다음과 같다.

<CascadeType.REMOVE>

  • 상위 엔티티(Post)가 제거되면 하위 엔티티(Comment)도 제거된다.
  • 상위 엔티티와 하위 엔티티의 참조가 끊어지는 경우에는 하위 엔티티가 제거되지 않는다.
    • post1의 comments 필드에서 comment1을 제거하면 참조는 끊어지지만 comment1은 제거되지 않는다.

<orphanRemoval = true>

  • 상위 엔티티가 제거되면 하위 엔티티도 제거된다.
  • 상위 엔티티와 하위 엔티티의 참조가 끊어지는 경우, 하위 엔티티가 제거된다.
    • post1의 comments 필드에서 comment1을 제거하면, comment1을 고아 객체로 분류하여 제거하는 것이다.
  • 즉, orphanRemoval = true는 기본적으로 고아 객체를 제거하는 기능임을 인지하면 될 것이다.

5. 영속성 전이 + 고아 객체 제거

...

@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();

...
  • 위 코드와 같이 CascadeType.ALL + orphanRemoval = true로 함께 활용할 경우, 상위 엔티티를 통해 하위 엔티티의 생명 주기를 관리할 수 있게 된다.
  • 즉, 예시에서 Comment 엔티티의 생명 주기를 Post 엔티티만으로 관리할 수 있게 되는 것이다.
  • CascadeType.ALL이 아닌 다른 타입과도 상황에 맞추어 적절히 활용될 수 있을 것이다.
profile
경험과 기록으로 성장하기
post-custom-banner

0개의 댓글