그 때 당시에는 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의 한 메서드에 대한 코드들이다.
@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();
}
}
@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);
}
}
// 안에서 쪽지 보내기
@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);
// 안에서 쪽지 보내기
@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;
}
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;
}
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
→ REFRESHDETACH
→ DETACHCasacadeType.PERSIST
, CasacadeType.REMOVE
옵션들은 em.persist()
, em.remove()
를 실행할 때 바로 전이가 발생하는게 아니라 플러시를 호출할 때 전이가 발생한다.그러면 이제 다시 본론으로 들어가보자!
맨 처음 CasacadeType.ALL
옵션이 부모 엔티티인 Room에 적용 되어 있었을 때의 코드 흐름을 알아보자.
먼저 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;
}
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;
}
영속 상태인 Room 객체는 @Tranactional 어노테이션에 의해서 flush 될 때 연관된 자식인 letter 또한 영속성 전이를 통해서 DB에 저장시킨다.
위의 과정 덕분에 별도의 repository.save() 코드 없이도 저장을 할 수 있었던 것이다.
그리고 CasacadeType.REMOVE
옵션이 부모 엔티티인 Room에 적용 되어 있었을 때의 코드 흐름을 알아보자.
먼저 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;
}
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;
}
영속 상태인 Room 객체는 @Tranactional 어노테이션에 의해서 flush 되더라도 비영속 상태인 Letter 객체는 영속성 전이가 되지 않기에 DB에 저장되지 않는다.
그렇기에 repository.save() 코드를 통해 자식 객체인 Letter를 별도로 DB에 저장을 시켜야 한다.
답은
CasacadeType.ALL
에서CasacadeType.REMOVE
옵션으로 변경을 하였기에, 영속 상태인 부모 객체를 영속성 전이를 통해서 비영속 상태인 자식 객체 또한 영속 상태로 만들지 못했기 때문이다.그렇기에 repository.save()를 통해서 따로 자식 객체를 저장시켜야했다.