20240313 (수) 최종 프로젝트 티켓레이더 3주차 - 시연 테스트코드

2024. 3. 14. 20:56TIL

발표에 들어갈 시연을 위해 테스트코드를 둘러보았다.

그중 테스트 코드에 문제가 있었다.

 

TicketServiceTest 코드 수정

 

티켓 테스트에 락을 구현한뒤 동시성 테스트코드를 실행하였을 때 테스트 실패가 나왔다.

사실 어제 저녁에부터 실패를 확인하였지만 어제는 시간도 시간이여서 적당히 둘러보다가 갔다.

오늘에서야 오류를 해결하였고 그 과정을 적어본다.

테스트 조건은 이렇다.

val threadCount = 50
        val executorService = Executors.newFixedThreadPool(10)
        val countDownLatch = CountDownLatch(threadCount)

        var success = 0
        var fail = 0
        val createTicketReq = CreateTicketRequest(
            date = LocalDate.now(),
            eventId = getEvent!!.id!!,
        )
        //when 총 50개의 스레드로 동시에 10명이 티켓을 구매했을때
        repeat(threadCount) {
            executorService.submit {
                try {
                    createTicketReq.seatList.add(SeatInfo(TicketGrade.R, 1))
                    ticketService.createTicket(member.id!!, createTicketReq)
                    success++
                } catch (e: TicketReservationFailedException) {
                    fail++
                } finally {
                    countDownLatch.countDown()
                }
            }
        }
        countDownLatch.await()

        println("success : $success")
        println("fail : $fail")

        //then 성공1 실패49 이여야한다.
        Assertions.assertThat(success).isEqualTo(1)
        Assertions.assertThat(fail).isEqualTo(49)

총 50번의 티켓을 요청하고 R1이란 자리는 1개니까

테스트 코드를 실행하면 성공은 1 실패는 49가 되어야한다. 하지만 결과는..

성공 2 실패 0..?

티켓 테스트를 돌려보고 H2를 확인해본 결과 이 코드 이전에 적혀있는 이벤트 생성까지는 제대로 되는것을 확인했고

이 이후에 생기는 문제라고 판단하였다. (아래코드)

        val createTicketReq = CreateTicketRequest(
            date = LocalDate.now(),
            eventId = getEvent!!.id!!,
        )
        //when 총 50개의 스레드로 동시에 10명이 티켓을 구매했을때
     repeat(threadCount) {
            executorService.submit {
                try {
                    createTicketReq.seatList.add(SeatInfo(TicketGrade.R, 1))
                    ticketService.createTicket(member.id!!, createTicketReq)
                    success++
                } catch (e: TicketReservationFailedException) {
                    fail++
                } finally {
                    countDownLatch.countDown()
                }
            }
        }
        countDownLatch.await()

 

그렇다면 왜 문제가 생기는 것일까?

 

테스트 결과도 조금 이상하다. 총 50번 실행했을 테지만 48번이 fail을 거치지도 않고 finally로 가버렸다.

우선 createTicket부터 스웨거로 실행해봤을시에 예매 성공과 예매 실패조건을 둘다 실행해봤을때 문제 없이 잘 되는걸 확인했다.

이번엔 레디스 인사이트라는 레디스에 어떤 정보가 들어오는지도 확인해보았다. 근데 여기서 티켓예매 실패 조건 이후 락키가 사라지지않고 그대로 남아있는것을 확인하였다!!

그럼 락 점유시간과 대기시간의 문제인것일까? 그래서 락 점유시간을 기존 10초에서 3초로 짧게 줄여보았다. 하지만 그럼에도 테스트 결과에 변화는 없었다..

그렇다면 락 해제의 시점이 언제인지 확인해보았다.

createTicket의 마지막부분에서 정상적으로 락해제를 해주지만 어째선지 여기에 닿지를 않는 상황.. 그러던중 갑자기 번뜩이면서 해당 코드가 보였다.

 

바로 throw 이것이다.

2번째 요청부터 이미 예약된 자리가 있어서 티켓 예매 실패를 안내해주며throw를 던짐과 동시에 createTicket의 마지막에 있는 unlock()까지 진행이 되질 않았고,

따라서 다음 진행 순서 스레드들은 락 점유만을 기다리다가 성공도 실패도 아닌채로 끝나버린 것이다.

예상한대로 락의 점유와 해제를 createTicket밖에서 다뤄주니까 제대로 성공하는것을 볼수 있었다.

그렇다면 바뀐 코드를 바탕으로 테스트코드를 다시 수정해주었고 해당 테스트 결과도 잘 나오는것을 확인하였다.

  val threadCount = 50
        val executorService = Executors.newFixedThreadPool(10)
        val countDownLatch = CountDownLatch(threadCount)

        var success = 0
        var total = 0
        val createTicketReq = CreateTicketRequest(
            date = LocalDate.now(),
            eventId = getEvent!!.id!!,
        )
        //when 10개의 스레드로 동시에 티켓을 구매했을때
        repeat(threadCount) {
            executorService.submit {
                try {
                    createTicketReq.seatList.add(SeatInfo(TicketGrade.R, 1))
                    ticketService.createTicket(member.id!!, createTicketReq)
                    success++
                }  finally {
                    total++
                    countDownLatch.countDown()
                }
            }
        }
        countDownLatch.await()

        // 시도한 횟수랑 = 실패 + 성공 횟수 같아야함.
        // 성공 횟수가 10이 아닌거는 동시성 문제해결.
        println("success : $success")
        println("total : $total")

        //then 성공1 실패9 이여야한다.
        Assertions.assertThat(success).isEqualTo(1)
        Assertions.assertThat(total).isEqualTo(50)

 


뭔가 오랜만에 제대로된 원인 - 결과 형식의 문제를 직면해서 재밌었다.