TicketRaider] 티켓 예매 과정 정리

JUNHYUK CHANG·2024년 3월 20일
0

TIL

목록 보기
31/33

이번 프로젝트에서 가장 핵심이 되는 부분을 꼽자면 바로 티켓 예매 메서드라고 할 수 있다.

단순히 티켓 필드를 하나 추가하면 되는 것이 아니라, 동시성 제어, 캐싱, 정합성 검사 등의 과정이 여럿 포함되기 때문이다.

설명에 들어가기 전, 간략히 내용을 요약하자면 아래의 순서로 진행된다.

  1. 멤버id, 예매하고자 하는 이벤트id/날짜/좌석 정보 리스트(좌석등급/좌석번호) 를 request 로 받아온다. ( 최대 4 티켓까지 한번에 생성 가능 )
  2. request 정보를 통해 각 좌석마다 Redis Lock 이 설정된다. ( around 어노테이션으로 구현 )
  3. createTicket() 메서드 실행
    2-1. event 객체 불러오기
    2-2. event 내 좌석정보 테이블을 호출하여 예약 가능 상태를 확인한다.
    2-3. 예약 가능 좌석을 하나씩 확인
    2-3-1. 캐싱을 통해
    2-3-2. DB를 통해
  4. 티켓 객체 생성
  5. 캐시에 등록
  6. 예약 가능 상태 업데이트
  7. createTicket() 메서드 종료.
  8. 각 좌석 락 해제.

  1. 이후 제한시간 안에 결제를 완료하지 않으면 자동으로 티켓이 취소됨.

   @Transactional
       override fun createTicket(memberId: Long, request: CreateTicketRequest) {
        // 속도가 빠른 캐싱체크를 가장 먼저 해야함
        request.seatList.map { seat ->
            //캐싱 체크
            val key = "${request.eventId}_${request.date}_${seat.ticketGrade}_${seat.seatNumber}"
            if (redisCacheService.chkCache(CacheTarget.TICKET, key))
                throw TicketReservationFailedException("${seat.seatNumber} 번 좌석은 선택할 수 없습니다. ( 이미 예약된 좌석 in Cache )")
        }

        val event = eventRepository.findByIdOrNull(request.eventId)
            ?: throw ModelNotFoundException("event", request.eventId)

        Hibernate.initialize(event.availableSeats)   // 컬렉션을 명시적으로 초기화 ( LAZY 모드 )

        // 예약 날짜 체크
        if (request.date < event.startDate || request.date > event.endDate || request.date < LocalDate.now()){
            throw TicketReservationFailedException("예매일(${request.date})이 올바르지 않습니다.")
        }

        // 좌석 예약 가능 상태 확인
        val availableSeat = event.availableSeats.find {
            it.date == request.date && it.bookable == Bookable.OPEN
        } ?:let{
            throw TicketReservationFailedException("예매일(${request.date}) 의 예약이 불가능한 상태 입니다.")
        }


        //DB 체크 - 캐싱관리가 제대로 되고 있다면 생략해도 되는 과정
       request.seatList.map{ seat ->
           val isReserved = ticketRepository.chkTicket(
                request.eventId,
                request.date,
                seat.ticketGrade,
                seat.seatNumber
           )
           if (isReserved != null) {
               throw TicketReservationFailedException("${seat.seatNumber} 번 좌석은 선택할 수 없습니다. ( 이미 예약된 좌석 in DB )")
           }
        }

        // 티켓 생성
        request.seatList.map{seat ->

            val member = memberRepository.findByIdOrNull(memberId)
                ?: throw ModelNotFoundException("member", memberId)

            val ticket = Ticket(
                date = request.date,
                grade = seat.ticketGrade,
                seatNo = seat.seatNumber,
                event = event,
                member = member,
                price = when (seat.ticketGrade) {
                    TicketGrade.R -> event.price!!.seatRPrice
                    TicketGrade.S -> event.price!!.seatSPrice
                    TicketGrade.A -> event.price!!.seatAPrice
                },
                place = event.place.name
            )

            ticketRepository.save( ticket )

            // 캐시에 등록
            redisCacheService.putCache(CacheTarget.TICKET,
                "${request.eventId}_${request.date}_${seat.ticketGrade}_${seat.seatNumber}",
                TicketResponse.from(ticket))

            // 좌석 예약 수 수정
            availableSeat.increaseSeat(seat.ticketGrade)
            if (availableSeat.isFull()) {
                availableSeat.close()
            }
            availableSeatRepository.save(availableSeat)
        }
        eventRepository.save(event)
        //캐시 내 이벤트 항목 최신화 하지 않아도 됨. Response 에는 변동사항 없음
    }

이 메서드를 개발하며 가장 고민을 많이 했던 부분은 '어떻게해야 응답시간을 줄이고 빠르게 락을 해제할 수 있을까' 에 대한 것이었다.

기껏 오래걸리는 확인작업을 다 마쳤는데 중복좌석이거나 예약할 수 없는 좌석이면 몹시 불쾌해질것 같았기 때문이다. 그래서 가능한 빨리 확인할 수 있는 작업부터 수행하고 에러처리를 하도록 구성해보았다. 시간 측정은 logger 를 사용하여 구간별 로그를 남기는 것으로 시간을 측정하였는데 역시 레포지토리를 통한 조회가 가장 시간이 700~1000 사이로 오래 걸려 뒷쪽으로 빼놓기로 했다.
캐싱으로 데이터를 확인하는 시간은 약 8ms 정도. 컨디션에 따라 100배 정도 빠를 수 있는 것이니 캐싱을 사용해야하는 이유를 확실히 느끼게 되었다.

기타 단순 값 비교 과정은 0~3ms 정도 소요되었다.

그리고 [예매하기] 후 [결제하기] 까지의 과정은 별도의 유예시간을 주어 손이 좀 느리더라도 좌석 선택까지만 성공하면 나머지는 조금 천천히 진행할 수 있도록 하였다.


이제 남은 일은 이 과정을 View 로 잘 표현해내고 배포하는 일만 남았다..!

0개의 댓글