포인트를 충전하고 콘서트의 좌석을 예매하는 API
- Milestone
- Projects
- 시퀀스 다이어그램
- ERD
- API 명세서
- Query 분석 및 캐싱 전략 설계
- 수행되는 쿼리 수집과 인덱스를 통한 성능개선
- 트랜잭션 범위의 이해와 MSA로 분리시 트랜잭션 처리의 한계와 해결방안
sequenceDiagram
사용자 ->> POST /api/queue/token: 토큰 발급 요청
POST /api/queue/token ->> 대기열: 사용자 정보 전달
대기열 ->> 유저: 유효한 사용자인지 확인
유저 -->> 사용자: 유효하지 않은 사용자입니다.
대기열 ->> 대기열: 해당 유저의 토큰 조회
opt 만료되지 않은 토큰이 존재하면
대기열 -->> 사용자: 토큰, 상태, 순번 반환
end
opt 토큰이 존재하지 않으면
대기열 ->> 대기열: 토큰 생성
대기열 -->> 사용자: 토큰, 상태, 순번 반환
end
sequenceDiagram
사용자 ->> GET /api/queue/token/validate: 토큰 유효성 체크
GET /api/queue/token/validate ->> 대기열: 토큰 정보 전달
대기열 ->> 대기열: 토큰 정보 조회
opt 토큰이 존재하지 않으면
대기열 -->> 사용자: 토큰이 존재하지 않습니다.
end
opt 토큰이 만료되었다면
대기열 -->> 사용자: 이미 만료된 토큰입니다.
end
opt 토큰이 존재하면
대기열 -->> 사용자: 토큰, 상태, 순번 반환
end
sequenceDiagram
사용자 ->> PUT /api/queue/token: 토큰 유효기간 연장 요청
PUT /api/queue/token ->> 대기열: 토큰 정보 전달
대기열 ->> 대기열: 토큰 정보 조회
opt 토큰이 존재하지 않으면
대기열 -->> 사용자: 토큰이 존재하지 않습니다.
end
opt 만료된 토큰이라면
대기열 -->> 사용자: 이미 만료된 토큰입니다.
end
opt 토큰이 존재하면
대기열 ->> 대기열: 토큰 만료시간을 연장
대기열 -->> 사용자: 토큰, 상태, 순번 반환
end
sequenceDiagram
loop 1분마다 반복
대기열 ->> 대기열: 상태가 working인 토큰의 개수를 확인
opt working 상태인 토큰의 개수가 허용개수 보다 작으면
대기열 ->> 대기열: 전체 허용개수 - 현재 활성 토큰 수 만큼 상태변경 및 만료시간 20분 부여
end
end
sequenceDiagram
사용자 ->> Get /api/concerts: 공연 조회
Get /api/concerts ->> 공연: 토큰 & 요청 전달
공연 ->> 대기열: 유효한 토큰인지 확인
대기열 -->> 사용자: 유효하지 않은 토큰입니다.
공연 ->> 공연: 예약 오픈일과 종료일 사이에 있는 공연 정보 조회
공연 -->> 사용자: 공연 정보(이름, 잔여좌석) 반환
sequenceDiagram
사용자 ->> Get /api/concerts/schedules/{scheduleId}/seats: 좌석 목록 조회
Get /api/concerts/schedules/{scheduleId}/seats ->> 공연: 토큰 & 공연 스케줄 id 전달
공연 ->> 대기열: 유효한 토큰인지 확인
대기열 -->> 사용자: 유효하지 않은 토큰입니다.
공연 ->> 공연: 예약 가능한 해당 공연 스케줄의 좌석 목록 조회
공연 -->> 사용자: 좌석 목록 반환
sequenceDiagram
사용자 ->> POST /api/reservations: 예약 요청
POST /api/reservations ->> 예약: 토큰 & 요청 전달
예약 ->> 대기열: 유효한 토큰인지 확인
activate 대기열
대기열 -->> 사용자: 유효하지 않은 토큰입니다.
대기열 ->> 예약: 사용자 정보 반환
deactivate 대기열
예약 ->> 공연: 좌석 예약처리 요청
activate 공연
공연 ->> 공연: 요청한 공연 스케줄의 좌석 상태 조회 및 예약 가능 상태 조회
공연 -->> 사용자: 유효하지 않거나 이미 선택된 좌석입니다.
공연 ->> 예약: 성공여부 반환
deactivate 공연
예약 ->> 예약: 좌석 예약 생성 처리 (expiredAt을 5분후로 지정)
예약 -->> 사용자: 예약 정보 반환
sequenceDiagram
loop 1분마다 반복
예약 ->> 예약: 예약시간이 만료된 예약 목록 조회
예약 ->> 공연: 해당 예약의 좌석 상태를 예약가능으로 변경 요청
end
sequenceDiagram
사용자 ->> POST /api/payments: 결제 요청
POST /api/payments ->> 결제: 토큰 & 요청 전달
결제 ->> 대기열: 유효한 토큰인지 확인
activate 대기열
대기열 -->> 사용자: 유효하지 않은 토큰입니다.
대기열 ->> 결제: 사용자 정보 반환
deactivate 대기열
결제 ->> 예약: 예약정보 조회
activate 예약
예약 -->> 사용자: 유효하지않은 예약입니다.
예약 ->> 결제: 예약정보 반환
deactivate 예약
결제 ->> 유저: 포인트 차감 요청
activate 유저
유저 -->> 사용자: 포인트가 부족합니다.
유저 ->> 유저: 유저 포인트 차감 내역 생성
deactivate 유저
결제 ->> 예약: 예약 상태를 결제로 변경 요청
결제 ->> 공연: 예약했던 공연 스케줄의 좌석의 상태를 결제로 변경 요청
결제 ->> 대기열: 토큰 삭제 요청
결제 -->> 사용자: 결제 정보 반환
sequenceDiagram
사용자 ->> GET /api/users/{userId}/point: 조회 요청
GET /api/users/{userId}/point ->> 유저: 요청 전달
activate 유저
유저 -->> 사용자: 존재하지 않는 유저입니다.
유저 ->> 유저: 유저의 포인트 정보 조회
유저 -->> 사용자: 현재 포인트 정보 반환
deactivate 유저
sequenceDiagram
사용자 ->> PATCH /api/users/{userId}/point/charge: 충전 요청
PATCH /api/users/{userId}/point/charge ->> 유저: 요청 전달
activate 유저
유저 -->> 사용자: 존재하지 않는 유저입니다.
유저 ->> 유저: 포인트 충전 & 포인트 충전 내역 생성
유저 -->> 사용자: 충전 결과 반환
deactivate 유저
| Table | Verbose | Description |
|---|---|---|
| user | 사용자 | 사용자 |
| queue | 대기열 | 사용자의 대기열 토큰 정보 |
| point | 포인트 | 사용자의 포인트 정보 |
| point_history | 포인트 내역 | 포인트 충전, 사용 내역 |
| concert | 공연 | |
| concert_schedule | 공연 스케줄 | 공연 날짜 및 잔여좌석 정보 |
| concert_seat | 공연 좌석 | 공연 스케줄의 좌석 정보 |
| reservation | 예약 | 사용자의 공연 예약 정보 |
| payment | 결제정보 | 사용자의 공연 예약 정보 |
| concert_meta_data | 공연 메타데이터 | 공연 예약시 생성되는 공연 meta data |
erDiagram
user {
id int pk
name string
}
point {
id int pk
updated_at datetime
user_id int fk "1:1"
amount int
}
point_history {
id int pk
created_at datetime
user_id int fk "N:1"
amount int
type enum
}
queue {
id int pk
expired_at datetime
token uuid
user_id int fk "N:1"
status enum "wait, working, expired"
}
erDiagram
concert {
id int pk
name string
}
concert ||--o{ concert_schedule: one2many
concert_schedule {
id int pk
date datetime
ticket_open_at datetime
ticket_close_at datetime
left_seat int
}
concert_schedule ||--o{ concert_seat: one2many
concert_seat {
id int pk
concert_schedule int fk
number int
price int
status enum "closed, able, reserved, soldout"
}
concert_meta_data {
id int pk
concert_id int fk
concert_name string
concert_schedule_id int fk
concert_schedule_date datetime
concert_seat_id int fk
concert_seat_number int
concert_seat_price int
}
concert ||--o{ concert_meta_data: one2many
concert_schedule ||--o{ concert_meta_data: one2many
concert_seat ||--o{ concert_meta_data: one2many
reservation {
id int pk
created_at datetime
updated_at datetime
expired_at datetime
user_id int fk
status enum "reserved, expired, paid"
concert_meta_data_id int fk
}
reservation ||--|| concert_meta_data: one2one
payment {
id int pk
created_at datetime
reservation_id int fk "1:1"
user_id int fk
}
reservation ||--|| payment: one2one
목차
- 대기열
- 공연
- 공연 결제
- 포인트
현재 유저에게 대기열 토큰을 발급합니다.
-
POST /api/queue/token -
Body
Key type Description userId int 사용자 id -
Response
// 201 Created { "token": "d07edb0f-3ac1-45a3-8972-7d263958b59d", // uuid "status": "wait", // wait, working "remain": 1 } // 401 Unauthorized { "message": "유효하지 않은 사용자입니다." }
현재 유저의 대기열 정보를 조회합니다.
-
GET /api/queue/token/validate -
Header
Key Description Authorization 대기열 토큰 -
Response
// 200 OK { "token": "d07edb0f-3ac1-45a3-8972-7d263958b59d", // uuid "status": "wait", // wait, working "remain": 1 } // 404 Not Found { "message": "토큰이 존재하지 않습니다." } // 401 Unauthorized { "message": "이미 만료된 토큰입니다." }
현재 유저의 대기열 토큰의 유효기간을 연장합니다.
-
PUT /api/queue/token -
Header
Key Description Authorization 대기열 토큰 -
Response
// 200 OK { "token": "d07edb0f-3ac1-45a3-8972-7d263958b59d", // uuid "status": "wait", // wait, working "remain": 1 } // 404 Not Found { "message": "토큰이 존재하지 않습니다." } // 401 Unauthorized { "message": "이미 만료된 토큰입니다." }
예약 가능한 공연 목록을 조회합니다. 티켓 오픈날짜와 종료날짜로 검증합니다.
-
GET /api/concerts -
Header
Key Description Authorization 대기열 토큰 -
Response
// 200 OK [ { "id": 1, "name": "카리나의 왁자지껄", "schedule": [ { "id": 1, "date": "2023-04-12T14:30:00+09:00", "ticketOpenAt": "2023-04-12T14:30:00+09:00", "ticketCloseAt": "2023-04-12T14:30:00+09:00", "leftSeat": 50, } ] } ] // 401 Unauthorized { "message": "유효하지 않은 토큰입니다." } // 403 Forbidden { "message": "유효하지 않은 토큰입니다." }
해당 공연의 좌석 목록을 조회합니다.
-
GET /api/concerts/schedules/:scheduleId/seats -
Header
Key Description Authorization 대기열 토큰 -
Path Parameter
Key Description scheduleId 공연 스케줄 id -
Response
// 200 OK [ { "id": 1, "number": 1, "price": 50000, "status": "Able" // Able, Reserved, SoldOut } ] // 401 Unauthorized { "message": "유효하지 않은 토큰입니다." } // 403 Forbidden { "message": "유효하지 않은 토큰입니다." }
공연을 예약합니다.
-
POST /api/reservations -
Header
Key Description Authorization 대기열 토큰 -
Request Body
Key Type Description scheduleId int 공연 날짜 id seatId int 좌석 id { "scheduleId": 1, "seatId": 1 } -
Response
// 201 Created { "id": 1, "createdAt": "2023-04-12T14:30:00+09:00", "updatedAt": "2023-04-12T14:30:00+09:00", "expiredAt": "2023-04-12T14:30:00+09:00", "status": "reserved", // reserved, expired, paid "concertMetaData": { "concertName": "카리나의 왁자지껄", "concertScheduleData": "2023-04-12T14:30:00+09:00", "concertSeatNumber": 1, "concertSeatPrice": 50000, } } // 400 Bad Request { "message": "이미 선택된 좌석입니다." } // 401 Unauthorized { "message": "유효하지 않은 토큰입니다." } // 403 Forbidden { "message": "유효하지 않은 토큰입니다." }
사용자가 예약한 공연을 결제합니다.
-
POST /api/payments -
Header
Key Description Authorization 대기열 토큰 -
Body
Key Type Description reservationId int 예약 id -
Response
// 201 Created { "id": 1, "createdAt": "2023-04-12T14:30:00+09:00", "reservation": { "id": 1, "createdAt": "2023-04-12T14:30:00+09:00", "updatedAt": "2023-04-12T14:30:00+09:00", "expiredAt": "2023-04-12T14:30:00+09:00", "status": "reserved", // reserved, expired, paid "concertMetaData": { "concertName": "카리나의 왁자지껄", "concertScheduleDate": "2023-04-12T14:30:00+09:00", "concertSeatNumber": 1, "concertSeatPrice": 50000, } }, } // 400 Bad Request { "message": "포인트가 부족합니다." } { "message": "유효하지 않은 접근입니다." } // 401 Unauthorized { "message": "유효하지 않은 토큰입니다." } // 403 Forbidden { "message": "유효하지 않은 토큰입니다." }
현재 유저의 포인트 정보를 조회합니다.
GET /api/users/:userId/point- Path Parameter
Key Description userId 사용자 id - Response
// 200 Ok { "amount": 100000 }
현재 유저의 포인트를 충전합니다.
-
PATCH /api/users/:userId/point/charge -
Path Parameter
Key Description userId 사용자 id -
Request Body
Key Type Description amount int 충전 금액 { "amount": 50000 } -
Response
// 200 Ok { "amount": 100000 }
- Slow Query가 발생할 수 있는 경우
- 대량의 데이터를 불러오는 경우
- 전체 테이블 스캔이 발생하는 경우 (Join)
- 잠금 문제 (비관락)
- Index가 잘못 선정된 경우
- 콘서트 예약 서비스에서 Slow Query가 발생할 수 있는 API
- 공연 목록 조회
- 남은 좌석 수를
select count(*) from schedule_seat where status=open으로 처리하는 경우 - 좌석은 예약에서 락이 발생하기 때문
- status에 index가 없으면 조회 속도가 느릴 수 있기 때문
- 남은 좌석 수를
- 공연 스케줄 좌석 조회
- 좌석, 예약에 대한 비관적 락에 의한 경우
- 공연 목록 조회
- 한 유저가 콘서트 조회를 시작하고 결제까지 완료되는 시간을 파악
- 목록 조회 -> 좌석 조회 -> 예약 -> 포인트 충전 -> 결제
- 평균 1분
- DB에 동시에 접근할 수 있는 트래픽의 최대치를 계산
- TPS가 1000이라고 가정 -> 1분당 60,000
- 1분간 유저가 호출하는 API의 수 2. 목록 조회, 좌석 조회, 예약, 포인트 충전, 결제 : 5 3. 재시도 계수 : 2 4. 총 호출 API 수 : 10
- 분당 처리할 수 있는 동시접속자 수
60,000 / 10= 1분당 6,000명- N = 10초마다 M = 1,000명씩 유효한 토큰으로 전환
- 나의 대기열 순번이 1,160,000번(브루노마스 티켓팅 동접자 수)이라면? -> 19분 18초
접기/펼치기
- 포인트 조회
-- user의 point 정보 조회
SELECT * FROM "point" WHERE "user_id" = $1 LIMIT 1- 포인트 충전
START TRANSACTION
-- pessimistic_write으로 user의 point 조회
SELECT * FROM "point" WHERE "user_id" = $1 LIMIT 1 FOR UPDATE
-- 사용된 amount를 저장
UPDATE "point" SET "id" = $1, "user_id" = $2, "amount" = $3, "updated_at" = CURRENT_TIMESTAMP WHERE "id" IN ($4)
COMMIT- 예약 가능한 공연 날짜 조회
-- 공연 목록 조회
SELECT * FROM "concert"
-- 해당 공연의 예매가 가능한 스케줄 목록 조회
SELECT * FROM "concert_schedule" WHERE "concert_id" IN ($1, $2) AND "ticket_open_at" <= $3 AND $4 < "ticket_close_at"- 공연 좌석 조회
-- 공연 스케줄 조회
SELECT * FROM "concert_schedule" WHERE "id" = $1 LIMIT 1
-- 해당 스케줄의 좌석 전체 조회
SELECT * FROM "concert_seat" WHERE "concert_schedule_id" = $1- 예약
START TRANSACTION
-- pessimistic_write으로 좌석 조회
SELECT * FROM "concert_seat" WHERE "id" = $1 FOR UPDATE
-- 해당 공연의 상태를 에약으로 변경
UPDATE "concert_seat" SET "status" = $1 WHERE "id" = $2
COMMIT
-- 스케줄의 잔여좌석 수 업데이트를 위해 조회
SELECT * FROM "concert_schedule" WHERE "id" = $1 LIMIT 1
-- 스케줄의 잔여좌석 수 업데이트
UPDATE "concert_schedule" SET "left_seat" = $1 WHERE "id" = $2
-- 공연 메타 데이터 생성을 위해 조회
SELECT * FROM "concert_schedule" WHERE "id" = $1 LIMIT 1
SELECT * FROM "concert" WHERE "id" = $1 LIMIT 1
SELECT * FROM "concert_seat" "ConcertSeatEntity" WHERE "id" = $1 LIMIT 1
-- 공연 메타 데이터 생성
START TRANSACTION
INSERT INTO "concert_meta_data"("concert_id", "concert_name", "concert_schedule_id", "concert_schedule_date", "concert_seat_id", "concert_seat_number", "concert_seat_price") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id"
COMMIT
-- 예약 정보 생성
START TRANSACTION
INSERT INTO "reservation"("created_at", "updated_at", "expired_at", "user_id", "status", "concert_meta_data_id") VALUES (DEFAULT, DEFAULT, $1, $2, $3, $4) RETURNING "id", "created_at", "updated_at"
COMMIT- 예약 결제
-- 유효한 예약인지 확인하기 위해서 조회
SELECT * FROM "reservation" WHERE "id" = $1 LIMIT 1
-- 예약의 메타 데이터 조회
SELECT * FROM "concert_meta_data" WHERE "id" = $1 LIMIT 1
START TRANSACTION
-- 사용자의 포인트를 차감하기 위해 pessimistic_write으로 조회
SELECT * FROM "point" WHERE "user_id" = $1 LIMIT 1 FOR UPDATE
UPDATE "point" SET "id" = $1, "user_id" = $2, "amount" = $3, "updated_at" = CURRENT_TIMESTAMP WHERE "id" IN ($4)
-- 포인트 사용내역을 생성
INSERT INTO "point_history"("created_at", "user_id", "amount", "type") VALUES (DEFAULT, $1, $2, $3) RETURNING "id", "created_at"
-- 예약의 상태를 paied로 변경
UPDATE "reservation" SET "status" = $1, "updated_at" = CURRENT_TIMESTAMP WHERE "id" IN ($2)
-- 공연 좌석의 상태를 soldout으로 변경
UPDATE "concert_seat" SET "status" = $1 WHERE "id" IN ($2)
-- 결제 정보 생성
INSERT INTO "payment"("created_at", "reservation_id", "user_id") VALUES (DEFAULT, $1, $2) RETURNING "id", "created_at"
COMMIT-- 해당 공연의 예매가 가능한 스케줄 목록 조회
SELECT * FROM "concert_schedule" WHERE "concert_id" IN ($1, $2) AND "ticket_open_at" <= $3 AND $4 < "ticket_close_at"
--- 예약 가능한 공연만 불러오도록 join
EXPLAIN ANALYZE
SELECT
c.id AS concert_id,
c.name AS concert_name,
json_agg(
json_build_object(
'id', cs.id,
'date', cs.date,
'ticket_open_at', cs.ticket_open_at,
'ticket_close_at', cs.ticket_close_at,
'left_seat', cs.left_seat
)
) AS schedules
FROM
concert AS c
LEFT JOIN concert_schedule AS cs ON cs.concert_id = c.id
WHERE
cs.ticket_open_at <= '2024-08-09'
AND '2024-08-09' < cs.ticket_close_at
GROUP BY
c.id,
c.name;- Mac M2 24GB
- concert 테이블 rows : 2,107,986
- concert_schedule 테이블 rows : 6,127,076
- concert 1개당 concert_schedule 1~5개씩 적용해서 카디널리티를 높게 설정
GroupAggregate (cost=231574.94..438329.98 rows=1515061 width=53) (actual time=276.608..418.593 rows=59920 loops=1)
Group Key: c.id
-> Gather Merge (cost=231574.94..408028.76 rows=1515061 width=53) (actual time=276.556..293.918 rows=102063 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Sort (cost=230574.92..232153.11 rows=631275 width=53) (actual time=259.831..261.428 rows=34021 loops=3)
Sort Key: c.id
Sort Method: quicksort Memory: 3328kB
Worker 0: Sort Method: quicksort Memory: 3857kB
Worker 1: Sort Method: external merge Disk: 2584kB
-> Parallel Hash Join (cost=38596.81..148180.67 rows=631275 width=53) (actual time=222.664..254.079 rows=34021 loops=3)
Hash Cond: (cs.concert_id = c.id)
-> Parallel Seq Scan on concert_schedule cs (cost=0.00..92882.77 rows=631275 width=36) (actual time=19.971..88.827 rows=34021 loops=3)
" Filter: ((ticket_open_at <= '2024-08-09 00:00:00'::timestamp without time zone) AND ('2024-08-09 00:00:00'::timestamp without time zone < ticket_close_at))"
Rows Removed by Filter: 2088552
-> Parallel Hash (cost=22367.47..22367.47 rows=883947 width=21) (actual time=123.050..123.050 rows=707523 loops=3)
Buckets: 131072 Batches: 32 Memory Usage: 4704kB
-> Parallel Seq Scan on concert c (cost=0.00..22367.47 rows=883947 width=21) (actual time=0.024..46.242 rows=707523 loops=3)
Planning Time: 0.656 ms
JIT:
Functions: 39
Options: Inlining false, Optimization false, Expressions true, Deforming true
Timing: Generation 2.653 ms, Inlining 0.000 ms, Optimization 1.530 ms, Emission 30.404 ms, Total 34.587 ms
Execution Time: 421.568 msGroupAggregate (cost=13.60..344082.46 rows=1514851 width=53) (actual time=204.256..1266.483 rows=59920 loops=1)
Group Key: c.id
-> Merge Join (cost=13.60..313785.44 rows=1514851 width=53) (actual time=204.224..1134.455 rows=102063 loops=1)
Merge Cond: (c.id = cs.concert_id)
" -> Index Scan using ""PK_c96bfb33ee9a95525a3f5269d1f"" on concert c (cost=0.43..68648.83 rows=2121473 width=21) (actual time=0.012..309.503 rows=2122568 loops=1)"
" -> Index Scan using ""IDX_71925060d6beea8f2c47ccc55f"" on concert_schedule cs (cost=0.43..231495.18 rows=1514851 width=36) (actual time=136.819..718.581 rows=102064 loops=1)"
" Filter: ((ticket_open_at <= '2024-08-09 00:00:00'::timestamp without time zone) AND ('2024-08-09 00:00:00'::timestamp without time zone < ticket_close_at))"
Rows Removed by Filter: 6265651
Planning Time: 0.603 ms
JIT:
Functions: 12
Options: Inlining false, Optimization false, Expressions true, Deforming true
Timing: Generation 1.783 ms, Inlining 0.000 ms, Optimization 0.453 ms, Emission 9.652 ms, Total 11.889 ms
Execution Time: 1269.795 msGroupAggregate (cost=17.28..408986.11 rows=1514851 width=53) (actual time=176.811..1108.005 rows=59920 loops=1)
Group Key: c.id
-> Merge Join (cost=17.28..378689.09 rows=1514851 width=53) (actual time=176.779..976.979 rows=102063 loops=1)
Merge Cond: (c.id = cs.concert_id)
" -> Index Scan using ""PK_c96bfb33ee9a95525a3f5269d1f"" on concert c (cost=0.43..68648.83 rows=2121473 width=21) (actual time=0.025..307.517 rows=2122568 loops=1)"
" -> Index Scan using ""IDX_996be3ab1528fabf37221c9ac7"" on concert_schedule cs (cost=0.43..299460.20 rows=1514851 width=36) (actual time=106.822..563.610 rows=102064 loops=1)"
" Index Cond: (ticket_open_at <= '2024-08-09 00:00:00'::timestamp without time zone)"
" Filter: ('2024-08-09 00:00:00'::timestamp without time zone < ticket_close_at)"
Rows Removed by Filter: 4020833
Planning Time: 0.797 ms
JIT:
Functions: 14
Options: Inlining false, Optimization false, Expressions true, Deforming true
Timing: Generation 1.612 ms, Inlining 0.000 ms, Optimization 0.716 ms, Emission 11.267 ms, Total 13.596 ms
Execution Time: 1111.131 msGroupAggregate (cost=16.86..401596.38 rows=1514851 width=53) (actual time=94.731..788.253 rows=59920 loops=1)
Group Key: c.id
-> Merge Join (cost=16.86..371299.36 rows=1514851 width=53) (actual time=94.706..658.382 rows=102063 loops=1)
Merge Cond: (c.id = cs.concert_id)
" -> Index Scan using ""PK_c96bfb33ee9a95525a3f5269d1f"" on concert c (cost=0.43..68648.83 rows=2121473 width=21) (actual time=0.010..321.743 rows=2122568 loops=1)"
" -> Index Scan using ""IDX_2e3196fb484c72aa1655073b2d"" on concert_schedule cs (cost=0.43..291721.92 rows=1514851 width=36) (actual time=19.505..225.619 rows=102064 loops=1)"
" Index Cond: ((ticket_open_at <= '2024-08-09 00:00:00'::timestamp without time zone) AND (ticket_close_at > '2024-08-09 00:00:00'::timestamp without time zone))"
Planning Time: 0.208 ms
JIT:
Functions: 12
Options: Inlining false, Optimization false, Expressions true, Deforming true
Timing: Generation 0.790 ms, Inlining 0.000 ms, Optimization 0.390 ms, Emission 5.129 ms, Total 6.309 ms
Execution Time: 790.640 ms| 항목 | 인덱스 추가 전 | 인덱스 추가 후 | 변화 |
|---|---|---|---|
| 실행 시간 | 421.568 ms | 790.640 ms | 실행 시간이 증가함 |
| Seq Scan | 3x Parallel Seq Scan (총 88.827 ms) | 없음 | Seq Scan 없음 (인덱스 사용) |
| 인덱스 스캔 | 없음 | 2x Index Scan (총 225.619 ms) | 인덱스 스캔이 도입됨 |
| 정렬 방법 | Workers Planned: 2, Workers Launched: 2 | 정렬 없음 | 정렬 단계가 없음 |
| 조인 방법 | Parallel Hash Join | Merge Join | 조인 방법이 Hash Join에서 Merge Join으로 변경됨 |
| 정렬 및 메모리 사용 | Sort Method: quicksort, Memory: 3328kB (Worker 0) / 3857kB (Worker 1) | 없음 | 정렬 작업이 사라짐 |
| 버킷 수 및 메모리 사용 | Buckets: 131072, Memory Usage: 4704kB | 없음 | 버킷 수와 메모리 사용이 없음 |
| Plan Cost | 231574.94..438329.98 | 16.86..401596.38 | Plan Cost가 크게 감소함 |
분석 및 개선 사항:
-
실행 시간 증가: 인덱스 추가 후 실행 시간이 증가하였습니다. 인덱스 스캔이 Seq Scan보다 느릴 수 있는 상황이거나 인덱스 스캔이 비효율적일 수 있습니다.
-
Seq Scan 제거: 인덱스 추가 후 Seq Scan이 제거되었으며, 이는 인덱스가 제대로 작동하고 있음을 나타냅니다.
-
인덱스 스캔 도입: 인덱스 스캔이 도입되었으나, 예상보다 더 오랜 시간이 소요되었습니다. 이는 인덱스의 선택성이나 인덱스의 구조가 쿼리와 잘 맞지 않을 수 있습니다.
-
정렬 및 메모리 사용: 인덱스 추가 후 정렬이 사라지면서 메모리 사용이 줄어들었습니다. 이는 인덱스가 정렬을 대체했음을 의미합니다.
-
조인 방법 변경: Hash Join에서 Merge Join으로 변경되었습니다. Merge Join은 인덱스가 정렬된 데이터에 대해 더 효율적이므로 조인 성능이 개선될 수 있습니다.
-
Plan Cost 감소: Plan Cost가 크게 감소하였지만 실제 실행 시간은 증가했습니다. 이는 인덱스 추가가 단순히 비용 측정에 영향을 미쳤을 가능성이 있습니다.
결론: Plan cost는 크게 감소했지만 실행속도 자체는 오히려 증가하면서 성능 개선이 되지 않았다. 스케줄에서 공연 id가 중복되는 카디널리티가 높게 데이터를 넣었음에도 결과가 이런 것으로 보아 Query 자체의 튜닝과 Index 설정에 대한 검토가 필요할 것 같다.
기존 설계에서는 각 Facade에서 여러 service를 호출해서 사용해서 사용 중이기 때문에, 기능이 추가되었을때 트랜잭션의 범위가 커질 수있다. 특히 결제 API에서 포인트, 예약, 좌석과 같이 연관된 서비스가 많은 API가 존재한다. 사용자에게 예약 정보를 전달하거나 하는 외부 API를 사용하는 기능이 추가될 수 있기 때문에 Event를 이용해 트랜잭션을 분산시킬 필요가 있다.
- 포인트
- 대기열
- 공연 (공연, 스케줄, 좌석)
- 예약
- 결제
- 포인트 - 포인트 충전
- transaction
- 포인트 조회
- 포인트 업데이트
- transaction
- 예약 - 좌석 예약
- transaction
- 좌석 조회
- 좌석 상태 업데이트
- transaction
- 결제 - 예약 결제
- 예약정보 조회
- transaction
- 사용자 포인트 차감
- 예약 상태를 결제로 변경
- 예약 좌석 상태를 결제로 변경
- 결제 내역 생성
- 사용자 토큰 삭제
flowchart TD
ApplicationGateway([ApplicationGateway])
broker(Message Broker)
대기열[[대기열]]
포인트[[포인트]]
공연[[공연]]
예약[[예약]]
결제[[결제]]
대기열DB[(Redis)]
포인트DB[(Database)]
공연DB[(Database)]
예약DB[(Database)]
결제DB[(Database)]
ApplicationGateway --> broker
broker <--> 대기열
broker <--> 포인트
broker <--> 공연
broker <--> 예약
broker <--> 결제
대기열 --- 대기열DB
포인트 --- 포인트DB
공연 --- 공연DB
예약 --- 예약DB
결제 --- 결제DB
- MSA 패턴 : Choreography Saga 패턴 -> 현재 서비스의 복잡도가 높지 않지 않기 때문
- 트랜잭션 : Redis를 이용한 분산락을 통해 구현 -> Redis 통한 낙관락도 있지만 이슈 발생시 롤백 처리되는 케이스가 많을 것으로 예상해서 분산락을 선택
- 에러 이벤트 발생시 각 서비스에서 보상 트랜잭션을 통해 롤백 실행
- 예약 정보 검증
- 결제 내역 생성
- 결제 완료 생성 이벤트 발생
- 포인트 2. 포인트 조회 3. 포인트 차감
- 공연
- 공연 및 좌석 정보를 metadata로 저장
- 좌석 상태를 결제로 변경
- 예약
- 예약 상태를 결제로 변경
- 대기열
- 토큰 삭제
- 포인트, 공연 서비스에서 에러 발생시 실패 이벤트 발행
- 포인트
- 포인트 조회
- 포인트 충전
- 공연
- 좌석 상태를 예약으로 변경
- 예약
- 예약 상태를 예약으로 변경
- 대기열
- 해당 토큰 복구
- 포인트