ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JPA] cascadeType
    JPA 2024. 9. 5. 14:13

    엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공한다

     

    cascade 옵션은 부모 엔티티가 단일 소유자일 때, 부모와 자식의 라이프 사이클을 맞추고 싶을때 사용한다

    ex) 게시글과 댓글

    Cascade의 뜻이 ‘폭포수가 흐르다’ 라는 의미도 갖고 있는걸 알면 직관적으로 파악하기 쉽다

     


    cascadeType.PERSIST

    부모가 persist될때 연관된 자식들도 persist됨

    부모가 자식의 전체 생명주기를 관리

     

    casacdeType.REMOVE

    부모가 삭제되면 자식도 삭제됨

    but

    부모와 자식의 관계가 끊어졌다고 해서 자식이 삭제되는 것은 아님!

    관계가 있는 부모가 삭제되면 연쇄적으로 해당 엔티티와 연관된 자식들도 전부 삭제되는거임!

    cf) orphanRemoval = true 와 잘 구분해야한다!

     

    cascadeType.ALL

    위 두개를 합쳐놓은거라고 보면 됨

     


    [casacde를 안썼을시!]

    @Entity
    @Getter
    public class Member {
        @Id @GeneratedValue
        @Column(name="member_id")
        private int id;
        
        private String name;
    }
    @Entity
    @Getter
    public class Team {
        @Id @GeneratedValue
        @Column(name="team_id")
        private int id;
    
        @OneToMany(cascade = CascadeType.PERSIST)
        @JoinColumn(name = "team_id")
        private List<Member> members = new ArrayList<>();
    }

     

    이런 경우에 Team의 members에 cascade = CascadeType.PERSIST를 적용을 하지 않으면

        @Test
        @DisplayName("일대다 단방향 멤버 추가 성공")
        public void addMemberToTeamSuccess(){
            Member member = new Member("gakpo");
    
            Team team = new Team("liverpool");
            team.addMember(member);
            teamRepository.save(team);
    
            // @OneToMany(cascade = CascadeType.ALL) 로 해서 이게 통과가 되는거임
            // 만약 cascade 명시 안했으면 member는 자동 저장이 안됨!
            Member savedMember = memberRepository.findById(member.getId()).orElseThrow();
            Assertions.assertThat(savedMember.getId()).isEqualTo(member.getId());
    
            // Team 쪽에서 member가 memberList에 저장되었는지 확인
            Team savedTeam = teamRepository.findById(team.getId()).orElseThrow();
            Assertions.assertThat(savedTeam.getMembers().contains(member)).isTrue();
        }

     

    이 테스트에서 savedMember를 조회할때 에러가 난다

    왜냐하면 영속성 전파/전이가 전혀 되지 않아

    team의 members에 Member 엔티티 객체를 집어넣어도 Member 엔티티가 persist 즉, 영속된 상태는 아니기 때문!

     


    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Builder
    @Entity
    public class Member {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String name;
    
        @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
        @JoinColumn(name = "teamId")
        private Team team;
    
        public Member(String name) {
            this(null, name, null);
        }
    }

     

    cascade는 꼭 List를 들고 있는 쪽에만 작성하지 않아도 됨

    하지만 주인쪽에서 하면 이상한 일 많이 일어나서 주인쪽에는 안적어주는게 좋음

     

    주인쪽인 Member Entity에 선언된 Team과의 연관관계에서, cascade.ALL 옵션이 보이는가?

    해당 옵션 때문에 Member Entity에 가해진 변화(삭제)가 Team까지 전파되었다

    즉, 영속성 전이는 부모에서 자식에게만 발생하는 것이 아닌, 참조하고 있는 Entity에 대해서 cascade 옵션이 활성화 되어 있다면 발생하는 것이다

    그래서 쌩뚱맞게도 의도와는 전혀 다르게 Team Entity가 삭제된 것이다

     


    이 외에도 여러 테스트를 작성해서 실험해본 결과, Entity에 생긴 변화는 cascade 옵션이 활성화되어 있다면 영속성에 생긴 변화는 무조건 전파된다(PERSIST, REMOVE에 대해서만 해보긴 했지만). 부모이건 자식이건, 단방향이건 양방향이건 상관없다

     

    자식에 cascade가 걸려있는 상태로 부모에서 자식에게 해당 전파 행위를 하면 자식이 부모에게 역으로(?) 전파를 하는, 역류 현상을 맛볼 수 있다

     

    cascade 옵션은 객체의 입장에서, 연관관계를 맺고 있는 대상 엔티티에게 "야, 너도 같이 가야지?"하고 같이 끌고가도록(?) JPA에게 알려주는 것과 같다고 생각하면 될 것 같다

     

    자식이 부모와만 연관관계가 있다면 다행이지만, 부모는 자식 외에도 다른 엔티티들과 연관관계를 맺을 확률이 높으므로 자식(반드시 자식이라고 하기엔 조금 그렇지만, 대상을 소유하고 있는 주체를 모두 아울러서)에게는 cascade 옵션을 쥐어주지 않는 것이 좋아보인다

     

    실무에서는 delete 쿼리를 사용하지 않고 상태값으로 데이터를 관리하는 경우가 많다고 한다. 그러나 주의해서, 잘 알고 사용하도록 하자


    [만약 cascade 적용시에 이미 자식 테이블에 해당 id 값이 존재하는 경우에는?]

     

     


    [cascadeType.ALL을 사용하면 안되는 상황]

     

    @Entity
    class Circle{
        
        @OneToMany(mappedBy = "circle", cascade = CascadeType.ALL, orphanRemoval = true)
        private List<UserCircle> userCircleList;
    }
    
    @Entity
    class Users{
    
        @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
        private List<UserCircle> userCircleList;
    }
    
    @Entity
    class UserCircle{
    
        @ManyToOne(fetch = FetchType.LAZY)
        private Circle circle;
    
        @ManyToOne(fetch = FetchType.LAZY)
        private Users users;
    }

     

    위와 같은 경우에는 Usercircle이라는 자식을 Circle 부모 객체와 Users 부모 객체가 알고 있는 상황입니다

     

    이러한 경우에는 주의해야 합니다

     

    자식 엔티티를 삭제할 상황이 아닌 경우 즉, Circle이나 Users 중 어느 한쪽에서 부모 엔티티를 삭제하거나 부모와 한쪽의 연관관계를 제거했을 경우 자식인 UserCircle이 아예 삭제되는 대참사가 발생할 수도 있습니다

     

    여기서 중요한 점은 Cascade 옵션이 유용해 보이지만

    어떤 연관관계를 맺은 엔티티들이 함께 저장되고 관리가 될지 계속해서 추적해 보고 생각해야 한다는 점입니다

     

    Cascade옵션을 사용할 경우 고려해야 할 점을 참고해보면 '통상적으로 권장하는 cascade 범위는, 완전히 개인 소유하는 엔티티일 때, 예를 들어서 게시판과 첨부파일이 있을 때 첨부파일은 게시판 엔티티만 참조하므로, 개인 소유하는 경우에는 사용 가능'이라고 합니다.

    https://www.inflearn.com/community/questions/31969/cascade-%EC%98%B5%EC%85%98-%EC%A7%88%EB%AC%B8

     

    cascade 옵션 질문 - 인프런 | 커뮤니티 질문&답변

    누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.

    www.inflearn.com

     

    위 경우는 개인 소유가 아닙니다. UserCircle이 Circle과 Users에서 공동 소유를 하고 있기 때문이죠.

     

    따라서 신중하게 판단하고 되도록이면 한쪽에서만 사용해야 합니다.

     

    아니면 Circle에서 UserCircle을 삭제할 경우 Users가 알게 하고, Users에서 UserCircle을 삭제할 경우 Circle이 알게 해야 합니다.

     

    5-2. CascadeType.ALL을 남발하면 안 되는 이유

     

    CascadeType.ALL과 orphanRemoval=true 옵션을 모두 사용했을 경우, 부모 엔티티를 통해서 자식의 모든 생명 주기를 관리할 수 있다는 내용이 타 블로그에 많이 올라와있습니다.

     

    하지만 제가 생각했을 때는 5-1에서 언급했듯이, 부모 엔티티가 자식의 모든 생명 주기를 관리하는 것이 무조건적으로 좋지만은 않습니다.

    'JPA' 카테고리의 다른 글

    [JPA] 즉시로딩 지연로딩 (FetchType)  (0) 2025.04.26
    [JPA] orphanRemoval  (0) 2024.09.10
    [JPA] @JoinColumn의 이해  (0) 2024.09.01
    [JPA] JPA의 이해  (0) 2024.07.28
    연관관계 편의 메서드  (0) 2024.07.08