조급하면 모래성이 될뿐

Spring Data Jpa Insert 할 때 Select가 나가네.. 본문

TroubleShooting/데브코스

Spring Data Jpa Insert 할 때 Select가 나가네..

Pawer0223 2022. 7. 1. 02:03

문제 상황


  • 설계한 Entity의 id가 Auto Increament값이 아니다.
  • 생성자가 호출되는 시점에 fk의 조합으로 생성된다.
    • makeReservedSeatId 함수에서 만들어진다.
@Entity
@Table(name = "reserved_seat")
public class ReservedSeat {

	public static final String ID_SEPARATOR = "_";

	@Id
	@Column(name = "id")
	private String id;
    
    ...
    
	private void makeReservedSeatId(Long scheduleId, Long seatId) {
		this.id = new StringBuilder()
			.append(scheduleId)
			.append(ID_SEPARATOR)
			.append(seatId)
			.toString();
	}

	public ReservedSeat(Ticket ticket, Seat seat) {
		makeReservedSeatId(ticket.getSchedule().getId(), seat.getId());
		this.ticket = ticket;
		this.seat = seat;
	}
}

 

  • 그리고 아무것도 안 하고 ReservedSeat을 save만 하는데 select 쿼리가 날아갔다.
	@Test
	@DisplayName("insert 하기 전에 select가 나간다.")
	void testInsertBeforeSelect() {
		Ticket ticket = saveTicket();
		Seat seat = saveSeat();

		ReservedSeat reservedSeat = new ReservedSeat(ticket, seat);

		log.info("=== 1 ===");
		reservedSeatRepository.save(reservedSeat);
		log.info("=== 2 ===");
	}

  • 보면 reservedSeat을 insert 하기 전에.. reservedSeat에 select가 날아갔다...

 

문제의 원인


  • 보통 auto increament로 id를 지정하게 되면 id값이 insert 할 때 생성된다.
  • 하지만 지금의 경우는, insert 하기 전에 id값이 결정된다.
  • 그래서 Spring Data JPA를 통해 save 할 때, @id 필드에 값을 보고 DB에 존재하는 데이터로 간주한다.
  • 그다음 update항목이 있는지 확인을 위해 select를 실행하는 것이다.
  • 하지만 저장된 게 없으니 이후 insert 또한 날아간다.
데이터가 많아질수록 성능도 저하될 것이다. 1억 개를 저장하려면 1억 번의 select가 나간다..

 

save 처리방식


  • CrudRepository 구현체인 SimpleJpaRepository를 보면 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);
		}
	}
  • 새로운 객체 isNew(entity) = true 이면 persist
  • 아니면 merge 한다
    • merge는 한번 persist -> detach -> persist상태가 될 때를 의미한다.
    • 참조

 

해결방법


  • isNew조건에 타게 하면 된다.
  • isNew이 true가 되는 기준을 id 존재 여부가 아닌
    • 영속성 콘텍스트에서 조회되지 않았을 때
    • 영속 상태가 되기 전
  • 으로 변경하면 된다.

isNew라는 상태 값을 하나 만들고 default값을 true로 준다. 그러면 처음 객체를 만들 때는 isTrue에 탈것이다.

 

그다음

 

  • 영속성 콘텍스트에서 조회되지 않았을 때를 판단하기 위해
    • 영속성 컨텍스트에서 조회가 되었다면? 상태를 false로 바꾼다.
  • 영속 상태가 되기 전을 판단하기 위해
    • 영속 상태가 되기 직전에 상태를 false로 바꾼다.

 

첫 번째를 처리할 수 있는 어노테이션이 @PostLoad 두 번째가 @PrePersist이다.

즉, 상태를 false로 변경하는 메서드를 추가한다.

 

다음으로 isNew의 기준을 변경하기 위해 Persistable 인터페이스를 구현해서, 오버 라이딩해준다.

추가적으로 해당 인터페이스를 구현함으로써 getId함수도 오버 라이딩해주면 된다.

 

최종 코드는 아래와 같다.

 

@Entity
@Table(name = "reserved_seat")
public class ReservedSeat implements Persistable<String> {
	@Id
	@Column(name = "id")
	private String id;

	...

	@Transient
	private boolean isNew = true;

	protected ReservedSeat() {
	}

	private void makeReservedSeatId(Long scheduleId, Long seatId) {
		this.id = new StringBuilder()
			.append(scheduleId)
			.append(ID_SEPARATOR)
			.append(seatId)
			.toString();
	}

	public ReservedSeat(Ticket ticket, Seat seat) {
		makeReservedSeatId(ticket.getSchedule().getId(), seat.getId());
		this.ticket = ticket;
		this.seat = seat;
	}

	@Override
	public boolean isNew() {
		return isNew;
	}

	@PrePersist
	@PostLoad
	void markNotNew() {
		this.isNew = false;
	}

	@Override
	public String getId() {
		return this.id;
	}
    
    ...

}

 

참조


반응형