레넌의 개발 일기

영속성 전이 - CascadeType.PERSIST와 CascadeType.REMOVE 본문

Spring Data

영속성 전이 - CascadeType.PERSIST와 CascadeType.REMOVE

brorae 2022. 8. 17. 02:05

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

 

CascadeType.PERSIST

아래와 같이 Article 엔티티와 Aritcle과 Tag간의 중간 테이블인 ArticleTag 엔티티가 있다.

@Entity
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Embedded
    private Title title;

    @Embedded
    private Content content;

    @Enumerated(value = EnumType.STRING)
    @Column(nullable = false)
    private Category category;

    @OnDelete(action = OnDeleteAction.CASCADE)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id", nullable = false)
    private Member member;

    @Embedded
    private Views views;

    @Embedded
    private ArticleTags articleTags;

    @Column(nullable = false)
    private boolean isAnonymous;

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
    
    public void addTag(Tag tag) {
       articleTags.add(this, tag);
    }
}
@Entity
public class ArticleTag {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "article_id")
    private Article article;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "tag_id")
    private Tag tag;
}
@Embeddable
public class ArticleTags {

    @OneToMany(mappedBy = "article")
    private List<ArticleTag> value;
    
    public void add(Article article, Tag tag) {
        value.add(new ArticleTag(article, tag));
    }
}
@Entity
public class Tag {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false, length = MAX_NAME_LENGTH)
    private String name;
}

게시글에 대한 태그를 2개 저장한다고 하면 아래와 같이 코드를 작성할 수 있다. 

@Test
void test() {
    // 게시글 저장
    Article article = articleRepository.save(new Article("제목", "content", Category.QUESTION, member, false));

    // 첫번째 태그 저장
    Tag spring = new Tag("SPRING");
    tagRepository.save(spring);
    articleTagRepository.save(new ArticleTag(article, spring));
    article.addTag(spring);

    // 두번째 태그 저장
    Tag java = new Tag("JAVA");
    tagRepository.save(java);
    articleTagRepository.save(new ArticleTag(article, java));
    article.addTag(java);
}

Article을 영속상태로 만들고, ArticleTag도 영속상태로 만들어야 한다. 하지만 이럴 때 영속성 전이를 사용하면 Article만 영속 상태로 만들면 연관된 ArticleTag까지 한번에 영속 상태로 만들 수 있다. cascade = CascadeType.PERSIST 옵션을 설정해주었다.

@Embeddable
public class ArticleTags {

    @OneToMany(mappedBy = "article", cascade = CascadeType.PERSIST)
    private List<ArticleTag> value;
}
@Test
void test() {
    // 게시글 저장
    Article article = articleRepository.save(new Article("제목", "content", Category.QUESTION, member, false));

    // 첫번째 태그 저장
    Tag spring = new Tag("SPRING");
    tagRepository.save(spring);
    article.addTag(spring);

    // 두번째 태그 저장
    Tag java = new Tag("JAVA");
    tagRepository.save(java);
    article.addTag(java);

    testEntityManager.flush();
    testEntityManager.clear();
}

 

Cascade 옵션을 적용하기 전 ArticleTagRepository를 통해 값을 저장했을 때와 Cascade를 적용한 후, 동일하게 쿼리가 나가는 것을 확인할 수 있다.

 

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

 

CascadeType.REMOVE

저장한 Article과 ArticleTag 엔티티를 모두 제거하기 위해서는 아래와 같이 각각의 엔티티를 하나씩 제거해야한다.

@Test
void test() {
    // 게시글 저장
    Article article = articleRepository.save(new Article("제목", "content", Category.QUESTION, member, false));

    // 첫번째 태그 저장
    Tag spring = new Tag("SPRING");
    tagRepository.save(spring);
    article.addTag(spring);

    // 두번째 태그 저장
    Tag java = new Tag("JAVA");
    tagRepository.save(java);
    article.addTag(java);

    // 영속성 컨텍스트 비움
    testEntityManager.flush();
    testEntityManager.clear();

    Article foundArticle = articleRepository.findById(article.getId()).get();
    ArticleTag firstArticleTag = articleTagRepository.findById(1L).get();
    ArticleTag secondArticleTag = articleTagRepository.findById(2L).get();

    articleRepository.delete(foundArticle);
    articleTagRepository.delete(firstArticleTag);
    articleTagRepository.delete(secondArticleTag);

    // 영속성 컨텍스트 비움
    testEntityManager.flush();
    testEntityManager.clear();
}

 

CascadeType.REMOVE로 옵션을 설정하고 아래 코드처럼 Article 엔티티만 삭제하면 연관된 ArticleTag 엔티티도 함께 삭제된다.

@Embeddable
public class ArticleTags {

    @OneToMany(mappedBy = "article", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    private List<ArticleTag> value;
}
@Test
void test() {
    // 게시글 저장
    Article article = articleRepository.save(new Article("제목", "content", Category.QUESTION, member, false));

    // 첫번째 태그 저장
    Tag spring = new Tag("SPRING");
    tagRepository.save(spring);
    article.addTag(spring);

    // 두번째 태그 저장
    Tag java = new Tag("JAVA");
    tagRepository.save(java);
    article.addTag(java);

    // 영속성 컨텍스트 비움
    testEntityManager.flush();
    testEntityManager.clear();

    Article foundArticle = articleRepository.findById(article.getId()).get();

    articleRepository.delete(foundArticle);

    // 영속성 컨텍스트 비움
    testEntityManager.flush();
    testEntityManager.clear();
}

Cascade 옵션을 적용하기 전 ArticleTagRepository를 통해 값을 제거했을 때와 Cascade를 적용한 후, 동일하게 쿼리가 나가는 것을 확인할 수 있다. 삭제 순서는 외래 키 제약조건을 고려해서 자식을 먼저 삭제하고 부모를 삭제한다.

 

CascadeType.REMOVE를 설정하지 않고 위의 코드를 실행하면 어떻게 될까?

Article 엔티티만 삭제된다. 하지만 데이터베이스의 Article 로우를 삭제하는 순간 자식 테이블에 걸려 있는 외래 키 제약조건으로 인해, 외래키 무결성 예외가 발생한다.

 

Cascade의 종류

public enum CascadeType { 

    /** Cascade all operations */
    ALL, 

    /** Cascade persist operation */
    PERSIST, 

    /** Cascade merge operation */
    MERGE, 

    /** Cascade remove operation */
    REMOVE,

    /** Cascade refresh operation */
    REFRESH,

    /**
     * Cascade detach operation
     *
     * @since 2.0
     * 
     */   
    DETACH
}

CascadeType 코드를 보면 다양한 옵션이 있는 것을 확인할 수 있다.

 

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

 

CascadeType.PERSIST, CascadeType.REMOVE는 플러시를 호출할 때 전이가 발생한다.

'Spring Data' 카테고리의 다른 글

@EnableJpaAuditing을 분리해야하는 이유  (1) 2022.07.14