내가 casacade = CasacadeType.REMOVE를 적용한 이유

Kevin·2024년 2월 22일
0
post-thumbnail

그 때 당시에는 Spring은 물론 Java에 대한 개념도 매우 약할 때라 지금도 약하지만

레퍼런스의 코드를 가져다 사용하기에 급급했다.

당시의 나는 해당 개념에 대한 정확한 이해보다는 기능 구현이 우선이 되었기에, 내가 코드에 적용했던 다른 코드나 기술들에 대해 정확히 왜 사용하고, 어떤 기능인지에 대한 것들은 모두 미뤄뒀었다.

그래서 지금부터 내가 잘 모르고 작성했던 코드들에 대한 공부를 차근히 진행하고자 한다.

나는 아직도 너무 무지하고, 개념을 확실히 알고 넘어 가야 한다는 습관이 완벽히 자리 잡지 않았기에

이 시리즈는 한동안 쭉 작성하지 않을까 싶다.

이 시리즈의 여섯번째 글의 주제는 casacade = CasacadeType.REMOVE를 왜 사용했을까라는 주제이다.

나는 단순히 casacade = CasacadeType.REMOVE가 부모가 삭제될 때 연관된 자식을 제거한다고만 이해를 하고 있었고,

정확히 왜 casacade = CasacadeType.REMOVE를 사용하는 건지에 대해서는 이해하지 못하고 있었다.

이제부터 왜 casacade = CasacadeType.REMOVE를 사용하였는지에 대해서 알아보자.

이번에 내가 겪게된 문제는 다음과 같다.

부모 엔티티인 Room 엔티티에 cascade = CascadeType.ALL 옵션을 맨 처음 적용했었을 때는 아래의 Service 코드에서 제대로 DB에 저장이 되는 것이었다.

// 안에서 쪽지 보내기
    @Transactional
    public Letter sendLetterFromIn(LetterRequestDTO letterRequestDTO, Member user, Room room) {
        Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
        // 양방향 연관 관계
        letter.addLetter(room);

        return letter;
    }

그러나 cascade = CascadeType.REMOVE 옵션으로 변경을 하자 위 코드가 동작하지 않고, 아래와 같이 repository.save()를 하여야 제대로 DB에 저장이 되는 것이었다.

// 안에서 쪽지 보내기
    @Transactional
    public Letter sendLetterFromIn(LetterRequestDTO letterRequestDTO, Member user, Room room) {
        Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
        // 양방향 연관 관계
        letter.addLetter(room);
				
				letterRepository.save(letter);

        return letter;
    }

아래는 부모 엔티티인 Room과 자식 엔티티인 Letter, 그리고 Service인 LetterManagementService의 한 메서드에 대한 코드들이다.

Room

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Room {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long roomId;

    @OneToMany(mappedBy = "room", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
    @Builder.Default
    private List<Letter> letterList = new ArrayList<>();

    @ManyToOne
    @JoinColumn(name = "writer_id") // writer_id는 Writer(작성자)의 식별자 컬럼 이름입니다.
    private Member writer;

    @ManyToOne
    @JoinColumn(name = "receiver_id") // receiver_id는 Receiver(수신자)의 식별자 컬럼 이름입니다.
    private Member receiver;

    @CreatedDate
    private LocalDateTime createAt;

    @CreatedDate
    private LocalDateTime updatedAt;

    // 최신화
    public void updateTime(){
        this.updatedAt = LocalDateTime.now();
    }

}

Letter

@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Letter {

    @Id
    @Column(name = "letter_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String content;

    @ManyToOne
    @JoinColumn(name = "writer_id") // writer_id는 Writer(작성자)의 식별자 컬럼 이름입니다.
    private Member writer;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "room_id")
    private Room room;

    @CreatedDate
    private Date createdAt;

    // letterList에 추가하기
    public void addLetter(Room room){
        if (this.room != null) {
            this.room.getLetterList().remove(this);
        }
        this.room = room;
        room.getLetterList().add(this);
    }
}

LetterManagementService

// 안에서 쪽지 보내기
    @Transactional
    public Letter sendLetterFromIn(LetterRequestDTO letterRequestDTO, Member user, Room room) {
        Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
        // 양방향 연관 관계
        letter.addLetter(room);

        letterRepository.save(letter);

        return letter;
    }

문제가 생겼던 이유를 알기전에 영속성 전이에 대해서 먼저 알아볼 필요가 있다.

영속성 전이란?

  • 영속성 전이를 사용하면 부모만 영속 상태로 만들면 연관된 자식까지 한 번에 영속상태로 만들 수 있고, 영속성 전이는 CASACADE 옵션을 통해서 적용할 수 있다.

  • 만약 영속성 전이를 하지않는다면 아래와 같이 코드를 작성해야 한다.

    // 부모 저장
    Room room = new Room();
    em.persist(room);
    
    // 1번 자식 저장
    Letter letter1 = new Letter();
    letter1.addLetter(room);
    em.persist(letter1);
    
    // 2번 자식 저장
    Letter letter2 = new Letter();
    letter2.addLetter(room);
    em.persist(letter2);
    • 부모 엔티티를 먼저 영속 상태로 만들고, 자식 엔티티도 각각 영속 상태로 만들어야 한다.

  • 위 Service 코드를 또 다른 예시로 들어보자.
    // 안에서 쪽지 보내기
        @Transactional
        public Letter sendLetterFromIn(LetterRequestDTO letterRequestDTO, Member user, Room room) {
            Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
            // 양방향 연관 관계
            letter.addLetter(room);
    
            letterRepository.save(letter);
    
            return letter;
        }
    • 만약 부모인 Room과 Letter간 영속성 전이가 설정되어있지 않다면, 영속 상태인 room과 달리 자식인 letter는 비영속 상태이기에 letterRepository.save(letter); 코드 없이는 DB에 저장되지 않을 것이다.

  • 그러나 영속성 전이를 설정하였다면 아래와 같은 코드도 작성할 수 있다.
    // 안에서 쪽지 보내기
        @Transactional
        public Letter sendLetterFromIn(LetterRequestDTO letterRequestDTO, Member user, Room room) {
            Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
            // 양방향 연관 관계
            letter.addLetter(room);
    
            return letter;
        }
    • 인자로 넘겨받은 부모인 room이 영속성 컨텍스트에 존재하는 영속 상태이기에 자식인 letter 또한 한번에 영속 상태가 되므로, 최종 DB에 flush가 된다.

영속성 전이의 여러가지 옵션들을 알아보자.

  • 영속성 전이 저장, casacade = CasacadeType.PERSIST
    • 이 옵션을 통해서 부모와 자식 엔티티를 한번에 영속화 할 수 있다.
      @Entity
      public class Room {
      
          @OneToMany(mappedBy = "room", cascade = CascadeType.PERSIST)
          private List<Letter> letterList = new ArrayList<>();
    • 아래와 같이 부모만 저장하면 연관된 자식도 동시에 저장된다.
      Room room = new Room();
      
      Letter letter1 = new Letter();
      letter1.addLetter(room);
      
      Letter letter2 = new Letter();
      letter2.addLetter(room);
      
      // 부모, 자식 1, 2 모두 영속성 컨텍스트에 저장
      em.persist(room);
      • 여기서 알아야할 것은 영속성 전이는 연관관계를 매핑하는 것과는 아무 관련이 없다. 단지 영속화 할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐이다. → 그래서 위 코드에서 연관 관계를 설정한 후 영속 상태로 만드는 것을 알 수 있다.

  • 영속성 전이 삭제, casacade = CasacadeType.REMOVE
    • 이 옵션을 통해서 부모 엔티티만 삭제하면 연관된 자식 엔티티도 함께 삭제된다.
      @Entity
      public class Room {
      
          @OneToMany(mappedBy = "room", cascade = CascadeType.REMOVE)
          private List<Letter> letterList = new ArrayList<>();
    • 아래와 같이 부모만 삭제하면 연관된 자식도 모두 삭제된다.
      Room room = em.find(Room.class, 1L);
      
      // 부모와 부모와 연관된 자식 1, 2 모두 삭제된다.
      em.remove(room);
      • 삭제 순서는 외래 키 제약 조건을 고려해서 자식을 먼저 삭제하고, 부모를 삭제한다.

    • 만약 casacade = CasacadeType.REMOVE 을 설정하지 않고, 삭제하면 어떻게 될까?
      • 부모 엔티티만 삭제되며, 자식 엔티티에 걸려있는 외래 키 제약 조건으로 인해서, 데이터베이스에서 외래키 무결성 예외가 발생한다.

  • 이 외에도 아래와 같은 옵션들이 존재한다.
    • ALL → 아래의 옵션들을 모두 적용
    • PERSIST → 영속
    • MERGE → 병합
    • REMOVE → 삭제
    • REFRESH → REFRESH
    • DETACH → DETACH

  • CasacadeType.PERSIST, CasacadeType.REMOVE 옵션들은 em.persist(), em.remove() 를 실행할 때 바로 전이가 발생하는게 아니라 플러시를 호출할 때 전이가 발생한다.

그러면 이제 다시 본론으로 들어가보자!


왜 CasacadeType.ALL에서 CasacadeType.REMOVE 옵션으로 변경했을 때repository.save() 메서드가 필요했을까?

맨 처음 CasacadeType.ALL 옵션이 부모 엔티티인 Room에 적용 되어 있었을 때의 코드 흐름을 알아보자.

  1. 먼저 Handler에서 Room 객체를 find해서 영속 상태의 엔티티를 Service에게 넘겨준다.

    @Transactional
        public Page<LetterResponseDTO> execute(int page, int size, Long userId, Long roomPK) {
    
            // DB로부터 영속 상태의 room을 가져옴.
    				Room room = roomService.findByRoomId(roomPK);
    
            Member user = memberService.getMember(userId);
    
            Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
    
            Page<LetterResponseDTO> letterResponseDTOPage = letterManagementService.getLettersByOne(user, room, pageable);
    
            return letterResponseDTOPage;
        }
  2. Service는 영속 상태인 부모 Room 객체와 비영속 상태인 자식 Letter 객체와 연관 관계를 매핑 시켜준다.

    // 안에서 쪽지 보내기
        @Transactional
        public Letter sendLetterFromIn(LetterRequestDTO letterRequestDTO, Member user, Room room) {
            Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
            // 양방향 연관 관계
            letter.addLetter(room);
    
            return letter;
        }
  3. 영속 상태인 Room 객체는 @Tranactional 어노테이션에 의해서 flush 될 때 연관된 자식인 letter 또한 영속성 전이를 통해서 DB에 저장시킨다.


위의 과정 덕분에 별도의 repository.save() 코드 없이도 저장을 할 수 있었던 것이다.

그리고 CasacadeType.REMOVE 옵션이 부모 엔티티인 Room에 적용 되어 있었을 때의 코드 흐름을 알아보자.

  1. 먼저 Handler에서 Room 객체를 find해서 영속 상태의 엔티티를 Service에게 넘겨준다.

    @Transactional
        public Page<LetterResponseDTO> execute(int page, int size, Long userId, Long roomPK) {
    
            // DB로부터 영속 상태의 room을 가져옴.
    				Room room = roomService.findByRoomId(roomPK);
    
            Member user = memberService.getMember(userId);
    
            Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
    
            Page<LetterResponseDTO> letterResponseDTOPage = letterManagementService.getLettersByOne(user, room, pageable);
    
            return letterResponseDTOPage;
        }
  2. Service는 영속 상태인 부모 Room 객체와 비영속 상태인 자식 Letter 객체와 연관 관계를 매핑 시켜준다.

    // 안에서 쪽지 보내기
        @Transactional
        public Letter sendLetterFromIn(LetterRequestDTO letterRequestDTO, Member user, Room room) {
            Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
            // 양방향 연관 관계
            letter.addLetter(room);
    
            return letter;
        }
  3. 영속 상태인 Room 객체는 @Tranactional 어노테이션에 의해서 flush 되더라도 비영속 상태인 Letter 객체는 영속성 전이가 되지 않기에 DB에 저장되지 않는다.

그렇기에 repository.save() 코드를 통해 자식 객체인 Letter를 별도로 DB에 저장을 시켜야 한다.


내가 CasacadeType.REMOVE 옵션을 사용할 때 repository.save()를 통해서 자식 객체를 저장시켰던 이유

답은 CasacadeType.ALL 에서 CasacadeType.REMOVE 옵션으로 변경을 하였기에, 영속 상태인 부모 객체를 영속성 전이를 통해서 비영속 상태인 자식 객체 또한 영속 상태로 만들지 못했기 때문이다.

그렇기에 repository.save()를 통해서 따로 자식 객체를 저장시켜야했다.

profile
Hello, World! \n

0개의 댓글