내가 repository.save()를 통해서 객체를 저장시켰던 이유

Kevin·2023년 6월 29일
1

반성합니다.

목록 보기
5/9
post-thumbnail

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

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

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

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

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

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

이 시리즈의 다섯번째 글의 주제는 repository.save()를 왜 사용했을까라는 주제이다.

나는 단순히 repository.save()가 객체를 데이터베이스에 저장을 시켜준다고만 이해를 하고 있었고,

정확히 왜 repository.save()을 사용하는 건지에 대해서는 이해하지 못하고 있었다.

이제부터 왜 repository.save()을 사용하였는지에 대해서 알아보자.

내가 해당 개념들에 대해서 무지하게 된 사건의 전말을 이야기해보겠다.
나는 우선 아래의 Service를 통해서 LetterRequestDTO를 Letter 엔티티로 변환 후 별도의 repository.save() 없이, 컨트롤러로 엔티티를 리턴해주었다.

이 때 테스팅을 통해서 Letter Entity가 출력이 되는 것이었다. 물론 출력 된 것이 데이터베이스에 저장된 것이 아니라 메모리에 존재했다는 것을 나중에 알게 되었지만...

이 때 나는 그러면 repository.save()가 왜 필요했던 것이고, 어떻게 데이터베이스에 저장이 되었던 것인지 궁금해서 공부한 결과 너무 부끄러워져버렸다.

나의 부끄러움을 천하에 알리고자 이 글을 작성하게 되었다.


LetterManagementService

// 밖에서 쪽지 보내기
    @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;
    }

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);
    }
}

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();
    }

}

내가 왜 repository.save()를 사용했어야 했는지를 알기 전에 JPA의 기본적인 영속성 개념들을 반드시 알고가야한다.

기본적인 JPA에서의 영속성에 관한 개념들

  • JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태이어야 한다.

    • 즉 부모와 자식 모두 영속상태이어야 한다.
  • 영속성이란 무엇일까?

    • 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말한다.
    • 영속성을 갖지 않으면 데이터는 메모리에서만 존재하게 되고 프로그램이 종료되면 해당 데이터는 모두 사라지게 된다.
    • 그래서 우리는 데이터를 파일이나 DB에 영구 저장함으로써 데이터에 영속성을 부여한다.
  • 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 객체는 영속성 컨텍스트에 저장된 것이다.


  • 영속성 컨텍스트란 무엇일까?
    • 엔티티를 영구 저장하는 환경이라는 뜻이다.
    • 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고, 관리한다.
      • persist() 메서드는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장한다.
    • 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어지며, 엔티티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있고, 관리할 수 있다.

  • 엔티티 매니저란?
    • 개발자 입장에서 엔티티 매니저는 엔티티를 저장하는 가상의 데이터베이스이다.
    • 데이터베이스를 하나만 사용하는 애플리케이션은 일반적으로 하나의 EntityManagerFactory를 생성하고, 필요할 때마다 EntityManagerFactory에서 EntityManger를 생성한다.
      • EntityManagerFactory를 만드는 것은 비용이 많이든다.
    • EntityManger는 데이터베이스 연결이 꼭 필요한 시점까지 DB 커넥션을 얻지 않는다.
      • 보통 트랜잭션을 시작할 때 DB 커넥션을 얻는다.

  • 영속성 컨텍스트에서 엔티티는 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가 자동으로 작동한다.
      - 적용된 범위에서는 프록시 객체가 생성되어 자동으로 커밋 또는 롤백을 진행한다.

    • 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;
          }
      ```
      • 그렇기에 letter 객체는 비영속이므로 flush 되지 않기에 데이터베이스에 저장되지 않는다.

    • 그런데 왜 저장이 되었었지…;
      • 저장이 되었던게 아니다. 아래 코드의 흐름을 보자.
        		// 쪽지 보내기
        		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;
              }
          • 보면 DTO를 엔티티로 변환한 후 persist를 하지 않은 상태로 순수한 자바 객체이고, 비영속 상태인 letter의 상태를 변경하였다. 그렇기에 DB에 flush 되지않고 단순히 메모리에만 남아있는 상태이다.
        • 그다음으로 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에서 값을 가져오는게 아니라 메모리에서 가져왔기 때문에 가능한 것이었다!!


내가 repository.save()를 통해서 객체를 저장시켰던 이유

답은 비영속 상태인 객체를 영속 상태로 바꿔준 후 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);
		}
	}

[Spring JPA] 영속 환경 ( Persistence Context )

[JPA] 영속성(persistence)이란?

@Transactional 사용과 영속성 컨텍스트(persistence context)

profile
Hello, World! \n

0개의 댓글