개요
우리가 만든 콘서트 예약 시스템에 대해 유저 토큰 발급부터 시작하여 대기열, 콘서트 조회, 예약, 결제에 이르기까지의 과정을 통해 사용자들이 콘서트 예약을 수행하게 된다. 해당 시나리오는 콘서트 예약의 핵심 시나리오인만큼, 서비스가 커질수록 더 많은 사용자가 호출하게 될 것이다.
따라서 해당 시나리오를 구체적으로 어떤 API가 불리고 각 요청과 응답이 어떻게 변화하는지 확인할 필요가 있다. 그에 더해 시나리오에서 수행되는 각 API에 대해 성능 테스트를 수행해보고 그 결과를 확인해본다.
시나리오
먼저 핵심 시나리오에 대해 순서대로 다시 분석해보자.
1.
유저 토큰 발급
•
POST http://localhost:8080/users/{userId}
•
발급 시 쿠키에 user-access-token 으로 UUID 저장되어 응답
•
응답 : 204 Created
2.
대기열 토큰 발급
•
POST http://localhost:8080/tokens
•
요청 header : 쿠키에 user-access-token
•
발급 시 쿠키에 queue-access-token으로 UUID 저장되어 응답
•
응답 : 204 Created
3.
대기열 순번 조회 및 대기
•
GET http://localhost:8080/tokens
•
요청 header : 쿠키에 user-access-token 및 queue-access-token
•
응답 : 200 OK
{
"waitingOrder": 203
"expectedWaitingSeconds": 22934
}
JSON
복사
•
waitingOrder가 0이면 대기열 통과하여 아래의 프로세스 진행
•
그렇지 않다면 0이 될 때까지 대기열 순번 메서드를 3초마다 조회
4.
대기열 통과 시
a.
콘서트 정보 조회
•
GET http://localhost:8080/concerts
•
요청 header : 쿠키에 user-access-token 및 queue-access-token
•
응답 : 200 OK
{
concerts: [
{
"concertId": 1,
"title": "콘서트1",
"provider": "가수1"
},
...
]
}
JSON
복사
b.
콘서트 일정 조회
•
GET http://localhost:8080/concerts/{concertId}/schedules
•
요청 header : 쿠키에 user-access-token 및 queue-access-token
•
응답 : 200 OK
{
concertSchedules: [
{
"concertId": 1,
"concertScheduleId": 123,
"totalSeat": 50,
"startAt": 2025-02-26'T'18:00:00.000000,
"endAt": 2025-02-26'T'20:00:00.000000,
},
...
]
}
JSON
복사
c.
콘서트 좌석 조회
•
GET http://localhost:8080/concerts/{concertId}/schedules/{scheduleId}/seats
•
요청 header : 쿠키에 user-access-token 및 queue-access-token
•
응답 : 200 OK
{
concertSeats: [
{
"concertId": 1,
"concertScheduleId": 123,
"concertSeatId": 99,
"seatNumber": 3,
"price": 150000,
},
...
]
}
JSON
복사
d.
콘서트 좌석 예약
•
POST http://localhost:8080/reservations
•
요청 header : 쿠키에 user-access-token 및 queue-access-token
•
요청 payload
{
"concertId": 1,
"concertScheduleId": 123,
"concertSeatId": 99
}
JSON
복사
•
응답 : 200 OK
e.
콘서트 좌석 예약 실패 시 위 반복
f.
콘서트 좌석 예약 성공 시 결제 진행
•
POST http://localhost:8080/reservations/{reservationId}/pay
•
요청 header : 쿠키에 user-access-token 및 queue-access-token
•
요청 payload
{
"concertId": 1,
"concertScheduleId": 123,
"concertSeatId": 99
}
JSON
복사
데이터 세팅
위의 시나리오를 테스트 하기 위해서는 일정 이상의 데이터가 저장되어있어야 할 것이다.
1.
User
CREATE TABLE `user` (
`id` BIGINT PRIMARY KEY NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`user_uuid` VARCHAR(255) NOT NULL UNIQUE,
`balance` BIGINT NOT NULL,
`version` BIGINT NOT NULL,
`created_at` TIMESTAMP(6) NOT NULL,
`updated_at` TIMESTAMP(6) NOT NULL
);
SQL
복사
유저의 경우 테스트를 수행할 1500명에 대한 정보만 생성하였다. 잔고가 없어 결제를 못하는 경우에 대한 테스트는 배제하여 balance에 대한 값은 전부 충분하도록 설정하였다.
2.
Concert
CREATE TABLE `concert` (
`id` BIGINT PRIMARY KEY NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`provider` VARCHAR(255) NOT NULL,
`finished` BIT NOT NULL,
`created_at` TIMESTAMP(6) NOT NULL,
`updated_at` TIMESTAMP(6) NOT NULL
);
SQL
복사
콘서트의 경우에는 10000개의 데이터를 생성하였고, 9990개의 완료된(이미 끝난) 콘서트와 10개의 예정된 콘서트로 구성하였다.
데이터 세팅 쿼리
3.
ConcertSchedule
CREATE TABLE `concert_schedule` (
`id` BIGINT PRIMARY KEY NOT NULL AUTO_INCREMENT,
`concert_id` BIGINT NOT NULL,
`total_seat` INT NOT NULL,
`start_at` TIMESTAMP(6) NOT NULL,
`end_at` TIMESTAMP(6) NOT NULL,
`created_at` TIMESTAMP(6) NOT NULL,
`updated_at` TIMESTAMP(6) NOT NULL,
INDEX idx_concert_id (concert_id)
);
SQL
복사
콘서트 일정은 위에서 생성한 콘서트의 id를 기반으로, 하나의 콘서트마다 2~3개의 일정을 생성했다. 또한 동일 콘서트에서는 서로 다른 일정의 start_at과 end_at의 기간이 서로 겹치지 않도록 하였다.
위의 콘서트에서 예정된 콘서트가 10개이고, 이에 기반하여 25개의 예정된 콘서트 일정을 추가하도록 설정하였다.
데이터 세팅 쿼리
4.
ConcertSeat
CREATE TABLE `concert_seat` (
`id` BIGINT PRIMARY KEY NOT NULL AUTO_INCREMENT,
`concert_schedule_id` BIGINT NOT NULL,
`seat_number` INT NOT NULL,
`price` INT NOT NULL,
`reserved_until` TIMESTAMP(6),
`created_at` TIMESTAMP(6) NOT NULL,
`updated_at` TIMESTAMP(6) NOT NULL,
INDEX idx_concert_schedule_id (concert_schedule_id)
);
SQL
복사
콘서트 좌석의 경우 각 일정별로 50개씩 설정하였고, 이미 지난 콘서트(일정)에 대해서는 전부 매진(null) 상태와 예정 콘서트에 대해서는 전부 판매되지 않은 상태로 설정하였다.
데이터 세팅 쿼리
테스트 환경 구축
위 시나리오를 테스트 하기 위해 테스트 환경을 구축해야한다.
docker run -d \
--name docker-influxdb-grafana \
-p 3003:3003 \
-p 3004:8083 \
-p 8086:8086 \
-v /Users/jiwon/hanghae/hhplus-week3/src/test/resources/k6/influxdb:/var/lib/influxdb \
-v /Users/jiwon/hanghae/hhplus-week3/src/test/resources/k6/grafana:/var/lib/grafana \
philhawthorne/docker-influxdb-grafana:latest
Shell
복사
먼저 k6의 결과를 시계열 데이터베이스에 저장하여 그라파나를 통해 볼 수 있도록, 위의 influxdb-grafana를 묶어 docker 컨테이너로 올려둔 오픈 소스를 활용하였다.
그 후 시나리오 기반으로 테스트 스크립트를 작성하였다. 테스트는 15명의 유저가 100씩 요청을 반복하여, 각 요청 시에 서로 다른 id를 통해 요청을 수행하도록 하였다.
스크립트
스크립트를 실행시키게 되면,
k6 run ./src/test/resources/k6/script.js --console-output "./src/test/resources/k6/console.log" --out influxdb=http://localhost:8086/k6
Shell
복사
이처럼 테스트가 수행되고 그 결과를 출력하게 된다. 추가적으로 명령어로 설정한 influxdb에 저장된다.
결과 확인 및 성능 분석
저장된 데이터를 그라파나를 통해 확인을 해보면 다음과 같다.
결과 분석
•
시간이 지날수록 초당 요청의 수 증가
◦
대기열에 막혀 자기 차례가 오기전까지 주기적으로 풀링 요청을 하기 때문에, 시간이 지날수록 풀링 요청 비율이 늘어남
◦
시나리오 상 일정 선택 후 빈 좌석을 선택하여 예약을 시도하지만, 해당 일정에 빈 좌석이 없다면 일정을 다시 조회 후 좌석 조회를 반복(getConcertSchedule 4035 / getConcertSeats 3243). 때문에 예약된 좌석이 많아질수록 반복하는 요청의 증가
•
p99(최대 지연 시간)
◦
makePayment : 202ms
◦
getUserToken : 244ms
◦
위 두 최대 지연 시간은 서버의 부하나 다른 이유로 인해 순간적으로 응답 지연이 발생한 것으로 예상된다.
◦
실제 테스트를 반복했을 때 reserveSeat의 최대 지연 시간 길게 나오거나 getConcertSeat의 최대 지연 시간이 길게 나오는 등 예측 불가능하게 보여짐
•
성능 지표
◦
예외적으로 튀는 경우를 배제한다면 평균 지연 시간 25ms 이내, 최대 지연 시간 111ms 이내로 합리적인 성능 지표를 보여줌
한계점
•
k6 테스트 시나리오
◦
순간적으로 요청이 몰리는 예약 시스템의 상황과 유사하게 구현하고 싶었지만, const-arrival-rate 같은 시나리오를 선택하여 동시에 많은 요청을 보내면 수많은 요청 실패가 발생하게된다. 요청 실패가 발생하지 않는 지점을 찾아 per-vu-iteration을 통해 15명의 vu로 설정하여 테스트를 수행했는데, 실제 사용자들의 요청이 몰리는 것과는 차이가 보일 것으로 예상된다.
•
로컬 테스트 환경
◦
시나리오에 대한 전반적인 성능 확인은 가능하지만, 로컬 테스트 환경이라는 한계가 있기 때문에 절대적으로 신뢰하기에는 어렵다.
◦
다만 이를 통해 다른 환경에서의 성능 예측의 기준이 될 수는 있을 것 같다.
•
에러 지표의 부재
◦
에러 지표에 대한 설정을 테스트가 다 끝나고 보고서를 작성하며 발견하여, 테스트 전체적인 에러의 지표를 확인할 수 없었다.