문제 상황

시도한 작업과 문제 발생 상황

예매 상태의 티켓을 취소 상태로 변환하는 API({{dev_url}}/api/tickets/{{티켓id}})에서 티켓 정보와 함께 쿼리가 하나 더 발생하는 현상이 있었습니다. 쿼리 발생 문제는 티켓을 취소 상태로 업데이트하는 과정에서 발생했습니다.

관련 코드

문제가 발생한 부분은 TicketControllerTicketServiceImpl입니다.

// TicketController.class
@DeleteMapping("/{ticketId}")
  public ApiResponse<Void> cancelTicket(
      @AuthenticationPrincipal Member member, @PathVariable(name = "ticketId") long ticketId) {
    ticketService.cancelTicket(member.getEmail(), ticketId);
    return ApiResponse.ok();
}

// TicketServiceImpl.class
 @Override
  @Transactional(transactionManager = "jpaTransactionManager")
  public void cancelTicket(String email, Long ticketId) {
    Ticket ticket = ticketRepository.findByMember(ticketId, email)
        .orElseThrow(() -> new CustomException(TICKET_NOT_FOUND_ERROR));
    ticket.isCancellable();
    updateTicketStatus(ticket, TicketStatus.CANCELED);
  }

TicketServiceImpl에서 ticketRepository.findByMember(ticketId, email)를 호출하여 티켓 정보를 조회한 후, 티켓 상태를 업데이트합니다.

//TicketEntity
...
@Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "member_id")
  private Member member;

  @ManyToOne
  @JoinColumn(name = "musical_id")
  private Musical musical;

  @Column
  @Enumerated(value = EnumType.STRING)
  private TicketStatus ticketStatus;

  @OneToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "seat_id")
  private Seat seat;

  @Column(updatable = false)
  @CreatedDate
  private LocalDateTime createdAt;

  @Column
  private LocalDateTime validUntil;

  @Column
  private LocalDateTime cancelUntil;

  @Column
  private String deviceId;
...

문제 발생 상황

Hibernate 쿼리를 확인한 결과, 티켓 정보와 함께 Musical 테이블에 대한 쿼리도 실행되고 있었습니다.

-- Query 1: Fetching ticket information
SELECT
    ticket0_.id AS id1_7_,
    ticket0_.cancel_until AS cancel_u2_7_,
    ticket0_.created_at AS created_3_7_,
    ticket0_.device_id AS device_i4_7_,
    ticket0_.member_id AS member_i7_7_,
    ticket0_.musical_id AS musical_8_7_,
    ticket0_.seat_id AS seat_id9_7_,
    ticket0_.ticket_status AS ticket_s5_7_,
    ticket0_.valid_until AS valid_un6_7_
FROM
    ticket ticket0_
CROSS JOIN
    member member1_
WHERE
    ticket0_.member_id = member1_.id
    AND ticket0_.id = ?
    AND member1_.email = ?

-- Query 2: Fetching musical information
SELECT
    musical0_.id AS id1_3_0_,
    musical0_.detail_image_url AS detail_i2_3_0_,
    musical0_.notice_image_url AS notice_i3_3_0_,
    musical0_.place AS place4_3_0_,
    musical0_.place_detail AS place_de5_3_0_,
    musical0_.place_image_url AS place_im6_3_0_,
    musical0_.poster_image_url AS poster_i7_3_0_,
    musical0_.ranking AS ranking8_3_0_,
    musical0_.running_time AS running_9_3_0_,
    musical0_.ticketing_end_date AS ticketi10_3_0_,
    musical0_.ticketing_start_date AS ticketi11_3_0_,
    musical0_.title AS title12_3_0_
FROM
    musical musical0_
WHERE
    musical0_.id = ?

원인 분석

처음에는 N+1 문제로 의심했으나, 이 문제는 아니었습니다.

원인은 Ticket 엔터티의 musical 필드가 기본적으로 Eager로 설정되어 있었기 때문입니다.

@ManyToOne
@JoinColumn(name = "musical_id")
private Musical musical;

위 코드에서 FetchType.LAZY를 명시하지 않았기 때문에 Musical 객체가 함께 조회된 것입니다.

해결 방법

FetchType.LAZY로 변경하여 해결했습니다.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "musical_id")
private Musical musical;

해결 후 결과