본문 바로가기

TIL

트랜잭션 경계 설정중 외부 트랜잭션 객체 Lazy 로딩 안됨.

이전 팀프로젝트 중 발생 했던 이슈에 대해서 다뤄 보고자한다.

어찌 보면 간단한 이슈인데 꽤나 중요하다고 생각해서 작성한다.

 

근데 지금 쓰는거면 너무 늦은거 아니냐 할 수 있는데.

알아.

문제 상황

좌석 예약 시 주문정보를 생성하는 메서드를 제작중인데. 아래와 같은 에러가 발생했습니다.

Could not initialize proxy [com.tixy.api.event.entity.Event#251] - no session

 

해당 이슈는 트랜잭션경계 밖에서 event를 lazy로딩 하려고해서 발생한 이슈

근데 코드를 보면 최상단에 Transactional 을 넣어서 하나의 트랜잭션 경계인데 왜 이런 이슈가 발생했을까?

@Transactional
public CreateOrderResponse order(Long eventSessionId, List<Long> seatIds, Long memberId){
    Member member = memberService.findById(memberId);
    member.checkMemberWallet();
    SeatHoldResponse seatHoldResponse = seatHoldService.seatHold(eventSessionId, seatIds, memberId);
    try {
        OrderRequest orderRequest = new OrderRequest(
                seatIds.size(),
                member,
                seatHoldResponse.ticketType()
        );
        Long totalPrice = orderService.saveOrder(orderRequest);
        EventSession eventSession = seatHoldResponse.ticketType().getEventSession();
        return new CreateOrderResponse(
                totalPrice,
                depositAddress,
                eventSession.getEvent().getTitle(), // seatHoldResponse 가 해당 트랜잭션 외부에서 만들어져서 lazy로딩 에러 발생..
                seatHoldResponse.ticketType().getSeatSection().getSectionName(),
                seatHoldResponse.seatLabels(),
                seatHoldResponse.seatLabels().size(),
                eventSession.getSessionOpenDate(),
                eventSession.getSessionCloseDate()
        );
    }catch (Exception e){
        // 주문생성 실패 시 보상트랜잭션
        seatHoldService.releaseSeatHold(eventSessionId, seatIds);
        log.error("주문생성 에러 발생 : {} ", e.getMessage());
        throw new OrderException(CREAT_ORDER_FAILED);
    }
}

getEvent 의 경우 seatHold 의 반환값인 SeatHoldResponse 에 담겨온 ticketType 정보로 조회 하는데,

seatHold 메서드가 Propagation.REQUIRES_NEW 로 별도의 트랜잭션에서 동작하기 때문이다.

@RedisLock(key = SEAT_HOLD_PREFIX, idx = 1,timeout = 10)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public SeatHoldResponse seatHold(Long eventSessionId, List<Long> seatIds, Long userId) {
    List<String> seatLabels = new ArrayList<>();
    eventSessionService.checkSessionSaleOpen(eventSessionId);
    for (Long seatId : seatIds) {
        SeatSession seatSession = seatSessionService.getSeatSession(eventSessionId, seatId);
        seatSession.setHeld(userId);
        seatLabels.add(seatSession.getSeat().getRowLabel());
    }

    Seat seat = seatService.getBySeatId(seatIds.get(0));
    TicketType ticketType =  ticketTypeService.getTicketTypeByEventSessionId(eventSessionId ,seat.getSeatSection().getId());
    return new  SeatHoldResponse(seatLabels,ticketType);
}

 

즉 seatHoldResponse.ticketType() 에서 가져온 eventSession 은 order 트랜잭션이아닌 별도의 트랜잭션에서 가져온 데이터이기 때문에 lazy로딩을 못하는것이다.

그러면 getEventSession 이 부분에선 왜 에러가 발생하지 않을까? 그 이유는 seatHold(별도 트랜잭션) 에서 이미 해당 데이터를 한번 조회를 했기 때문이다.checkSessionSaleOpen 메서드 안을 까보면

public void checkSessionSaleOpen(Long sessionId) {
        EventSession eventSession = getBySessionId(sessionId);
        eventSession.checkOpenSale();
    }

public EventSession getBySessionId(Long sessionId) {
	  return eventSessionRepository.findById(sessionId).orElseThrow(
	          () -> new EventServiceException(EventErrorCode.EVENT_NOT_FOUND)
	  );
}

이 안에서 한번 접근해서 조회가 가능한 것이다.

해결 방법은 간단하다 객체를 담지말고 조회에 필요한 데이터를 담아서 반환해주면 된다.


해결 과정

Step 1.

seatHold 에서 필요한 모든 데이터를 미리 조회해서 반환해준다

@RedisLock(key = SEAT_HOLD_PREFIX, idx = 1,timeout = 10)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public SeatHoldResponse seatHold(Long eventSessionId, List<Long> seatIds, Long userId) {
    List<String> seatLabels = new ArrayList<>();
    eventSessionService.checkSessionSaleOpen(eventSessionId);
    for (Long seatId : seatIds) {
        SeatSession seatSession = seatSessionService.getSeatSession(eventSessionId, seatId);
        seatSession.setHeld(userId);
        seatLabels.add(seatSession.getSeat().getRowLabel());
    }

    Seat seat = seatService.getBySeatId(seatIds.get(0));
    TicketType ticketType =  ticketTypeService.getTicketTypeByEventSessionId(eventSessionId ,seat.getSeatSection().getId());
    EventSession eventSession = ticketType.getEventSession();
    Event event = eventSession.getEvent();
    return new  SeatHoldResponse(
            seatLabels,
            ticketType,
            event.getTitle(),
            ticketType.getSeatSection().getSectionName(),
            eventSession.getSessionOpenDate(),
            eventSession.getSessionCloseDate()
            );
}

Step 2.

→ 근데 생각해보면 지금 lazy조회를 3번해서 N+1처럼 한번 조회하면 쿼리가 3번이 더 나간다. fetch join으로 변경해주자.

@Query("""
    SELECT tt FROM TicketType tt
    JOIN FETCH tt.eventSession es
    JOIN FETCH es.event
    WHERE tt.eventSession.id = :eventSessionId
    AND tt.seatSection.id = :seatSectionId
    """)
    Optional<TicketType> findByEventSessionAndSeatSectionId(@Param("eventSessionId") Long eventSessionId, @Param("seatSectionId") Long seatSectionId);

결과가 잘 나오는 것을 확인.

 

배운점.

일단 특정 이런 복잡한 여러 데이터가 연관되는 상황, 그리고 그걸 책임 분리를 위해서 여러 메서드에서 데이터를 조합해서 가져올때, 다른 API통신으로 데이터를 가져오는게 아니라 지금같이 같은 모듈내에서 데이터를 불러온다면, 필요한 데이터를 미리 다 가져와서 DTO에 담아 넘겨주는 것이 안전하다.