JPA Casecade 사용 방법

YeonCloud·2024년 7월 7일
0

Spring

목록 보기
1/3
post-thumbnail

팀 프로젝트를 하던 중 좋아요 기능을 구현할 때였다. 따로 로직을 구현하여 위시리스트를 만들려고 하던 중, JPA의 CascadeType.REMOVE 기능을 알게 되었다. CascadeType.REMOVE를 사용하면 해당 엔티티를 삭제할 때 연관된 엔티티도 함께 삭제된다는 것이다. 내가 구현하려는 좋아요 기능에 딱 맞는다고 생각하고 바로 적용했다. 그러나, 바로 오류를 경험했다. 특정 좋아요를 삭제하면 관련된 상품이 함께 사라지는 현상이 발생한 것이다. 곧바로 CascadeType.REMOVE를 검색해보니, 사용 시 주의해야 한다는 점이 강조되어 있었다. 이 경험을 통해 Cascade 기능을 제대로 이해하고 사용하기로 결심하고 공부를 시작했다.

JPA Cascade의 정의

JPA Cascade는 한 엔티티의 상태 변화가 연관된 다른 엔티티로 전파되도록 하는 설정입니다. CascadeType에는 여러 가지 옵션이 있다

  • CascadeType 옵션: ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH

먼저 casecadeType.persist는 부모 엔티티가 영속화 될 때 자식 엔티티도 영속화 된다. 즉, 새로운 부모 엔티티가 생기면 자식 엔티티도 같이 데이터베이스에 저장할고자 할 때 사용한다.

@Entity
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(cascade = CascadeType.PERSIST)
    private List<Child> children = new ArrayList<>();

    // getters and setters
}

@Entity
public class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // Other fields, getters and setters
}

// 사용 예시
Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();

parent.getChildren().add(child1);
parent.getChildren().add(child2);

entityManager.persist(parent); // 이 시점에 child1과 child2도 자동으로 영속화됨

실제 코드에서는 entityManager는 JPA repository에서 데이터를 넣으면 entityManager.persist이 자동으로 실행된다.

그 다음은 CasecadeType.REMOVE이다.
좋아요 기능으로 예시를 들어보겠다 .

import javax.persistence.*;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private List<Like> likes = new ArrayList<>();

    // getters and setters
}

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private List<Like> likes = new ArrayList<>();

    // getters and setters
}

@Entity
public class Like {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne
    @JoinColumn(name = "product_id")
    private Product product;

    // getters and setters
}

여기서 orphanRemoval = true는 무엇일까? orphanRemoval = true는 JPA에서 엔티티가 부모 엔티티와의 관계에서 제거될 때 데이터베이스에서도 해당 엔티티를 자동으로 삭제하도록 설정하는 옵션입니다. 이는 CascadeType.REMOVE와 자주 함께 사용되어, 부모 엔티티에서 자식 엔티티가 삭제되면 데이터베이스에서도 해당 자식 엔티티가 삭제되도록 보장합니다.

orphanRemoval = trueCascadeType.REMOVE의 차이점은 다음과 같습니다:

  • CascadeType.REMOVE: 부모 엔티티가 삭제될 때 연관된 자식 엔티티도 함께 삭제됩니다.
  • orphanRemoval = true: 부모 엔티티의 컬렉션에서 자식 엔티티가 제거될 때, 해당 자식 엔티티가 데이터베이스에서도 자동으로 삭제됩니다.

그렇다면 Casecade를 사용할 떄 무엇을 조심해야하는가?
기본 개념은 크게 어렵지 않다 오히려 JPA가 알아서 삭제해주니 구현해야할 코드가 줄어들어 편리해보인다. 하지만 연관관계 매핑에서 잘못사용하게 되면 의도하지 않은 결과가 나타나다.

CascadeType.REMOVE 혹은 CascadeType.ALL 옵션을 사용하면 엔티티 삭제 시 연관된 엔티티들이 전부 삭제되고 이로 인해 참조 무결성 제약조건을 위반할 가능성이 있다..

예를 들어서 위 좋아요 기능에서

@Test
void remove_bad_case() {
    // 새로운 사용자와 상품 생성
    User user = new User();
    user.setName("John Doe");

    Product product = new Product();
    product.setName("Awesome Gadget");

    // 새로운 좋아요 생성
    Like like1 = new Like();
    like1.setUser(user);
    like1.setProduct(product);

    Like like2 = new Like();
    like2.setUser(user);
    like2.setProduct(product);

    // 엔티티 저장
    userRepository.save(user);
    productRepository.save(product);
    likeRepository.save(like1);
    likeRepository.save(like2);

    // 좋아요 삭제
    likeRepository.delete(like1); // 이 시점에 user와 product도 삭제

    // flush 시점에 참조 무결성 제약조건 위반 예외 발생
    assertThatThrownBy(() -> entityManager.flush())
            .isInstanceOf(PersistenceException.class);
    // like2가 참조하는 user와 product가 삭제되었으므로 외래키 관련 예외 발생
}

위 예제에서 like1을 삭제했을 뿐인데, CascadeType.REMOVE 설정 때문에 userproduct가 함께 삭제되었습니다. 이로 인해 like2가 참조하는 userproduct가 없어져 참조 무결성 제약조건 위반 예외가 발생하게 됩니다.

문제 해결 방법: CascadeType.REMOVE와 orphanRemoval 사용 시 주의

잘못된 사용을 피하기 위해 CascadeType.REMOVEorphanRemoval를 조심스럽게 사용해야 합니다. 좋아요 기능에서는 Like 엔티티만 삭제하고 UserProduct는 유지해야 합니다.

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "user", orphanRemoval = true)
    private List<Like> likes = new ArrayList<>();

    // getters and setters
}

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "product", orphanRemoval = true)
    private List<Like> likes = new ArrayList<>();

    // getters and setters
}

@Entity
public class Like {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne
    @JoinColumn(name = "product_id")
    private Product product;

    // getters and setters
}
@Test
void remove_good_case() {
    // 새로운 사용자와 상품 생성
    User user = new User();
    user.setName("John Doe");

    Product product = new Product();
    product.setName("Awesome Gadget");

    // 새로운 좋아요 생성
    Like like1 = new Like();
    like1.setUser(user);
    like1.setProduct(product);

    Like like2 = new Like();
    like2.setUser(user);
    like2.setProduct(product);

    // 엔티티 저장
    userRepository.save(user);
    productRepository.save(product);
    likeRepository.save(like1);
    likeRepository.save(like2);

    // 좋아요 삭제
    likeRepository.delete(like1);

    // flush 시점에서 예외가 발생하지 않음
    assertThatNoException().isThrownBy(() -> entityManager.flush());
    // user와 product는 여전히 존재
    assertThat(userRepository.existsById(user.getId())).isTrue();
    assertThat(productRepository.existsById(product.getId())).isTrue();
    // like1은 삭제되었고, like2는 여전히 존재
    assertThat(likeRepository.existsById(like1.getId())).isFalse();
    assertThat(likeRepository.existsById(like2.getId())).isTrue();
}

이 예제에서는 orphanRemoval을 사용하여 Like 엔티티가 UserProduct 엔티티와의 관계에서 제거될 때 자동으로 삭제되도록 했습니다. 하지만 CascadeType.REMOVE를 제거하여 UserProduct 엔티티가 Like 엔티티 삭제 시 함께 삭제되지 않도록 했습니다.

이렇게 함으로써 참조 무결성 제약조건을 위반하지 않으면서 원하는 기능을 구현할 수 있습니다.

언제 Cascade를 사용해야 할까?

  • 부모-자식 구조가 명확한 경우:
    • 게시물(Post)와 댓글(Comment)의 관계처럼 명확한 부모-자식 구조가 있을 때 Cascade를 사용하는 것이 좋습니다.
  • 엔티티의 생명 주기가 부모와 일치할 때:
    • 자식 엔티티의 생명 주기가 부모 엔티티의 생명 주기와 일치할 때 사용하세요. 예를 들어, 주문(Order)과 주문 항목(OrderItem)에서는 주문이 삭제되면 모든 주문 항목도 함께 삭제되어야 합니다.

결론

Cascade는 올바르게 사용하면 매우 편리한 기술이다. 그러나 이를 오용하면 데이터의 일관성과 무결성이 손상될 수 있다. 따라서 Cascade를 사용할 때는 항상 엔티티 간의 관계를 명확히 이해하고, 적절한 CascadeType을 선택하며, 필요 시 orphanRemoval을 활용하여 데이터의 일관성을 유지해야한다.

좋아요 기능과 같은 예제에서는 Like 엔티티만 삭제하고 UserProduct 엔티티는 유지하는 것이 맞습니다. 이런 경우에는 CascadeType.REMOVE를 사용하지 않고 orphanRemoval만 사용하는 것이 바람직합니다. Cascade는 편의성을 높여주는 도구이지만, 사용에 따른 부작용을 항상 염두에 두고 신중하게 적용해야 한다.

0개의 댓글