그 때 당시에는 Spring은 물론 Java에 대한 개념도 매우 약할 때라
지금도 약하지만레퍼런스의 코드를 가져다 사용하기에 급급했다.
당시의 나는 해당 개념에 대한 정확한 이해보다는 기능 구현이 우선이 되었기에, 내가 코드에 적용했던 다른 코드나 기술들에 대해 정확히 왜 사용하고, 어떤 기능인지에 대한 것들은 모두 미뤄뒀었다.
그래서 지금부터 내가 잘 모르고 작성했던 코드들에 대한 공부를 차근히 진행하고자 한다.
나는 아직도 너무 무지하고, 개념을 확실히 알고 넘어 가야 한다는 습관이 완벽히 자리 잡지 않았기에
이 시리즈는 한동안 쭉 작성하지 않을까 싶다.
이 시리즈의 다섯번째 글의 주제는 repository.save()를 왜 사용했을까라는 주제이다.
나는 단순히 repository.save()가 객체를 데이터베이스에 저장을 시켜준다고만 이해를 하고 있었고,
정확히 왜 repository.save()을 사용하는 건지에 대해서는 이해하지 못하고 있었다.
이제부터 왜 repository.save()을 사용하였는지에 대해서 알아보자.
내가 해당 개념들에 대해서 무지하게 된 사건의 전말을 이야기해보겠다.
나는 우선 아래의 Service를 통해서 LetterRequestDTO를 Letter 엔티티로 변환 후 별도의 repository.save() 없이, 컨트롤러로 엔티티를 리턴해주었다.
이 때 테스팅을 통해서 Letter Entity가 출력이 되는 것이었다. 물론 출력 된 것이 데이터베이스에 저장된 것이 아니라 메모리에 존재했다는 것을 나중에 알게 되었지만...
이 때 나는 그러면 repository.save()가 왜 필요했던 것이고, 어떻게 데이터베이스에 저장이 되었던 것인지 궁금해서 공부한 결과 너무 부끄러워져버렸다.
나의 부끄러움을 천하에 알리고자 이 글을 작성하게 되었다.
// 밖에서 쪽지 보내기
@Transactional
public Letter sendLetterFromOut(LetterRequestDTO letterRequestDTO, Member user, Room room) {
Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
// 양방향 연관 관계
letter.addLetter(room);
return 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;
}
@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);
}
}
@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();
}
}
내가 왜 repository.save()를 사용했어야 했는지를 알기 전에 JPA의 기본적인 영속성 개념들을 반드시 알고가야한다.
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태이어야 한다.
영속성이란 무엇일까?
JPA에서의 영속성이란 무엇일까?
JPA의 엔티티 매니저가 활성화된 상태로 트랜잭션(@Transactional) 안에서 DB에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태이다.
// 밖에서 쪽지 보내기
@Transactional
public Letter sendLetterFromOut(LetterRequestDTO letterRequestDTO, Member user, Room room) {
Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
// 양방향 연관 관계
letter.addLetter(room);
letterRepository.save(letter);
return letter;
}
위 코드에서 save()를 통해 저장한 Letter 객체는 영속성 컨텍스트에 저장된 것이다.
영속성 컨텍스트에서 엔티티는 4개의 생명 주기를 거친다.
엔티티 객체를 생성했지만, 아직 순수한 객체 상태이며 저장하지 않은 상태 → em.persist()
호출 전, 비영속 상태
// Room을 생성한 상태(비영속)
Room room = new Room();
엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장한 상태 → em.persist()
호출 후, 영속 상태
영속 상태라는 것은 영속성 컨텍스트에 의해 관리된다.
em.find()
나 JPQL을 사용해서 조회한 엔티티도 영속성 컨텍스트가 관리하는 영속 상태다.
// Room을 생성한 상태(비영속)
Room room = new Room();
// 객체를 영속성 컨텍스트에 저장한 상태(영속)
em.persist(room);
// (영속)
Room room = roomRepository.findById(1L);
em.detach()
호출 후, 준영속 상태em.remove()
호출 후, 삭제JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영하는데 이것을 플러시(flush)라 한다.
// 밖에서 쪽지 보내기
@Transactional
public Letter sendLetterFromOut(LetterRequestDTO letterRequestDTO, Member user, Room room) {
Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
// 양방향 연관 관계
letter.addLetter(room);
letterRepository.save(letter);
return letter;
}
@Transactional
이 적용되어 있는 경우에는 해당 메서드가 트랜잭션으로 묶여있어서 메서드가 끝나는 지점에 트랜잭션 커밋이 발생하고, flush가 자동으로 작동한다.```java
// 밖에서 쪽지 보내기
@Transactional
public Letter sendLetterFromOut(LetterRequestDTO letterRequestDTO, Member user, Room room) {
Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
// 양방향 연관 관계
letter.addLetter(room);
return letter;
}
```
// 쪽지 보내기
letterManagementService.sendLetterFromIn(letter1, member1, room1);
letterManagementService.sendLetterFromIn(letter2, member2, room1);
letterManagementService.sendLetterFromIn(letter3, member1, room1);
letterManagementService.sendLetterFromIn(letter4, member2, room1);
System.out.println("answer : " + room1.getId());
System.out.println("answer : " + room1.getLetterList().size());
System.out.println("answer : " + letter.getId());
Pageable pageable = PageRequest.of(0, 3);
Page<LetterResponseDTO> lists = letterManagementService.getLettersByOne(member1, room1, pageable);
// 0 출력된다. -> 매핑이 안되고 있다는 것
for (LetterResponseDTO dto : lists) {
System.out.println("answer : " + dto.getId());
System.out.println("answer : " + dto.getNickname());
System.out.println("answer : " + dto.isWriter());
}
sendLetterFromIn()
메서드에서 letter 객체를 생성하고, getLettersByOne()
메서드에서 letter들을 찾아오고 있다.sendLetterFromIn()
메서드를 먼저 살펴보자.// 안에서 쪽지 보내기
@Transactional
public Letter sendLetterFromIn(LetterRequestDTO letterRequestDTO, Member user, Room room) {
Letter letter = LetterMapper.INSTANCE.toEntity(letterRequestDTO, user, new Date());
// 양방향 연관 관계
letter.addLetter(room);
return letter;
}
getLettersByOne()
메서드를 먼저 살펴보자.@Transactional(readOnly = true)
public Page<LetterResponseDTO> getLettersByOne(Member user, Room room, Pageable pageable) {
// Room의 letters들을 가져온다.
List<Letter> letters = room.getLetterList();
// Letter의 시작 범위와 끝 범위만큼 페이징해서 반환
int start = (int) pageable.getOffset();
int end = Math.min((start + pageable.getPageSize()), letters.size());
List<Letter> sublist = letters.subList(start, end);
List<LetterResponseDTO> letterResponseDTOS = LetterMapper.INSTANCE.toDTOs(sublist, user);
// 역순으로 정렬
Collections.reverse(letterResponseDTOS);
return new PageImpl<>(letterResponseDTOS, pageable, letters.size());
}
List<Letter> letters = room.getLetterList();
의 코드를 통해서 DB에서 값을 가져오는게 아니라 메모리에서 가져왔기 때문에 가능한 것이었다!!답은 비영속 상태인 객체를 영속 상태로 바꿔준 후 DB에 flush 하기 위해서였다.
Spirng Data JPA의 save() 메서드는 비영속적인 객체를 persist하여서 영속 상태로 만들어준다. 그 후 save() 메서드에 붙어있는 @Transactional 어노테이션을 통해서 flush를 하게 된다.
Spring Data JPA의 save() 메서드 구현 코드
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}