- 프로젝트 개요
- 프로젝트 목표 및 Milestone
- 시퀀스 다이어그램
- 플로우 차트
- ERD 설계
- API 명세
- 기술 스택 및 기본 패키지 구조
- 동시성 이슈와 해결책
- 조회 성능 개선과 캐싱 전략
- 쿼리 성능 개선 보고서
- 서비스 확장성을 위한 아키텍처 설계
- 카프카 그리고 테스트 컨테이너 그 사이 어딘가
콘서트 예약 서비스는 유저가 대기열을 통해 좌석을 예약하고 결제를 완료하는 시스템입니다. 사용자는 잔액을 충전하여 좌석을 예약하고, 대기열 시스템을 통해 예약 요청을 처리할 수 있습니다.
- TDD와 클린 아키텍처를 적용한 콘서트 예약 서비스 개발.
- 유저 대기열 시스템 및 좌석 예약/결제 기능 구현.
- 동시성 문제를 고려한 안전한 예약 시스템 구축.
- 시스템 요구사항 분석 및 클린 아키텍처 설계.
- ERD 설계 및 유저 토큰 발급, 잔액 충전/조회 API 구현.
- 유닛 테스트 작성.
- 예약 가능 날짜/좌석 조회 API 구현.
- 좌석 예약 요청 API 구현 및 대기열 시스템 구축.
- 통합 테스트 작성.
- 결제 API 구현.
- 좌석 예약 및 결제 통합 테스트.
- 최종 프로젝트 배포.
erDiagram
User ||--o{ Reservation : "makes"
Reservation ||--|| Payment : "is paid by"
User ||--o{ Queue : "waits in"
Concert_Schedule ||--o{ Reservation : "includes"
Concert_Schedule ||--o{ Seat : "has"
User ||--o{ Balance_History : "has"
User {
Long id
String name
String email
Double balance
}
Reservation {
Long id
Long user_id
Long concert_schedule_id
Integer seat_number
Date reservation_date
String status
Timestamp expiration_time
}
Payment {
Long id
Long user_id
Long reservation_id
Double amount
DateTime payment_date
String status
}
Queue {
Long id
Long user_id
Integer queue_position
Timestamp created_at
String status
}
Concert_Schedule {
Long id
Long concert_id
Date concert_date
Integer total_seats
}
Concert {
Long id
String title
String description
}
Seat {
Long id
Long concert_schedule_id
Integer seat_number
String status
}
Balance_History {
Long id
Long user_id
Double amount
DateTime transaction_date
String transaction_type
}
- User (유저)
id
: Primary Key, 유저 IDname
: 유저 이름email
: 유저 이메일balance
: 유저 잔액
- Concert (콘서트)
id
: Primary Key, 콘서트 IDtitle
: 콘서트 제목description
: 콘서트 설명
- Concert_Schedule (콘서트 일정)
id
: Primary Key, 콘서트 일정 IDconcert_id
: Concert 테이블과의 Foreign Keyconcert_date
: 콘서트 날짜total_seats
: 총 좌석 수
- Seat (좌석)
id
: Primary Key, 좌석 IDconcert_schedule_id
: Concert_Schedule 테이블과의 Foreign Keyseat_number
: 좌석 번호status
: 좌석 상태 (available
,pending
,reserved
등)
- Reservation (좌석 예약)
id
: Primary Key, 예약 IDuser_id
: User 테이블과의 Foreign Keyconcert_schedule_id
: Concert_Schedule 테이블과의 Foreign Keyseat_number
: 좌석 번호status
: 예약 상태 (pending
,reserved
,expired
등)expiration_time
: 예약 만료 시간
- Payment (결제)
id
: Primary Key, 결제 IDuser_id
: User 테이블과의 Foreign Keyreservation_id
: Reservation 테이블과의 Foreign Keyamount
: 결제 금액payment_date
: 결제 일시status
: 결제 상태 (completed
,failed
등)
- Queue (대기열)
id
: Primary Key, 대기열 IDuser_id
: User 테이블과의 Foreign Keyqueue_position
: 대기열 순서created_at
: 대기열 생성 시간status
: 대기열 상태 (waiting
,completed
등)
- Balance_History (잔액 거래 내역)
id
: Primary Key, 거래 내역 IDuser_id
: User 테이블과의 Foreign Keyamount
: 거래 금액transaction_date
: 거래 일시transaction_type
: 거래 유형 (charge
,payment
등)
- 링크 : http://localhost:8080/concert-reservation-swagger/ (로컬 환경에서 확인 가능)
-
Endpoint:
POST /api/users/token
-
Request:
- 없음
-
Response:
{ "userToken": "unique-user-token" }
-
Description: 사용자가 서비스를 이용하기 위한 유저 토큰을 발급받는 API.
-
Error:
500 Internal Server Error
:유저 토큰 발급 오류
-
Authorization: 없음
-
Endpoint:
GET /api/concerts/schedules
-
Request:
- 없음
-
Response:
{ "availableSchedules": [ { "concertScheduleId": 1, "concertDate": "2024-10-12", "concertTitle": "콘서트 A" }, { "concertScheduleId": 2, "concertDate": "2024-10-13", "concertTitle": "콘서트 B" } ] }
-
Description: 예약 가능한 콘서트 일정 목록을 반환.
-
Error:
500 Internal Server Error
:예약 가능한 일정을 가져올 수 없어요.
-
Authorization: 없음
-
Endpoint:
GET /api/concerts/schedules/{scheduleId}/seats
-
Request Parameters:
scheduleId
(Path Variable): 조회할 콘서트 일정 ID
-
Response:
{ "concertScheduleId": 1, "availableSeats": [ { "seatNumber": 1, "status": "available" }, { "seatNumber": 2, "status": "available" }, { "seatNumber": 3, "status": "reserved" } ] }
-
Description: 특정 콘서트 일정의 좌석 상태를 반환.
-
Error:
400 Bad Request
:잘못된 일정 ID에요.
500 Internal Server Error
:좌석 정보를 가져올 수 없어요.
-
Authorization:
Bearer {userToken}
-
Endpoint:
POST /api/reservations
-
Request:
{ "userToken": "unique-user-token", "concertScheduleId": 1, "seatNumber": 3 }
-
Response:
{ "reservationId": 123, "status": "PENDING", "expirationTime": "2024-10-12T10:15:00" }
-
Description: 사용자가 좌석을 예약하고, 일정 시간 동안 해당 좌석을
PENDING
상태로 유지하는 API. -
Error:
400 Bad Request
:잘못된 좌석 번호이거나 일정 ID가 입력되었어요.
401 Unauthorized
:잘못되거나 만료된 유저 토큰이에요.
409 Conflict
:이미 예약된 좌석이에요.
500 Internal Server Error
:예약 시스템 오류
-
Authorization:
Bearer {userToken}
-
Endpoint:
POST /api/users/balance/charge
-
Request:
{ "userToken": "unique-user-token", "amount": 50000 }
-
Response:
{ "status": "SUCCESS", "currentBalance": 100000 }
-
Description: 사용자의 잔액을 충전하는 API.
-
Error:
400 Bad Request
:유효하지 않은 금액이에요.
401 Unauthorized
:잘못되거나 만료된 유저 토큰이에요.
500 Internal Server Error
:잔액 충전 오류
-
Authorization:
Bearer {userToken}
-
Endpoint:
GET /api/users/balance
-
Request:
- 없음
-
Response:
{ "currentBalance": 100000, "balanceHistory": [ { "transactionDate": "2024-10-10T12:00:00", "transactionType": "charge", "amount": 50000 }, { "transactionDate": "2024-10-11T15:30:00", "transactionType": "payment", "amount": -30000 } ] }
-
Description: 사용자의 현재 잔액 및 거래 내역을 조회하는 API.
-
Error:
401 Unauthorized
:잘못되거나 만료된 유저 토큰이에요.
500 Internal Server Error
:잔액 조회 오류
-
Authorization:
Bearer {userToken}
-
Endpoint:
POST /api/payments
-
Request:
{ "userToken": "unique-user-token", "reservationId": 123, "paymentAmount": 50000 }
-
Response:
{ "status": "COMPLETED", "paymentId": 789 }
-
Description: 예약된 좌석에 대해 결제를 처리하는 API.
-
Error:
400 Bad Request
:잘못된 결제 정보에요.
401 Unauthorized
:잘못되거나 만료된 유저 토큰이에요.
409 Conflict
:이미 결제된 예약이에요.
500 Internal Server Error
:결제 처리 오류
-
Authorization:
Bearer {userToken}
- Backend: Kotlin, Spring Boot
- Database: MySQL
- ORM: Spring Data JPA
- In-memory DB (for testing): H2
- API Documentation: SpringDoc OpenAPI
- Testing: JUnit5, AssertJ, Mockito
- Dependency Management: Gradle
/src
├── main
│ ├── kotlin
│ │ └── io
│ │ └── hhplus
│ │ └── concertreservationservice
│ │ ├── ConcertReservationServiceApplication.kt
│ │ ├── application (Application 계층)
│ │ │ ├── TimeOverException.kt
│ │ │ ├── auth
│ │ │ │ └── AuthFacade.kt
│ │ │ ├── balance
│ │ │ │ └── BalanceFacade.kt
│ │ │ ├── concert
│ │ │ │ └── ConcertFacade.kt
│ │ │ ├── payment
│ │ │ │ └── PaymentFacade.kt
│ │ │ ├── queue
│ │ │ │ └── QueueFacade.kt
│ │ │ ├── reservation
│ │ │ ├── scheduler
│ │ │ │ ├── QueueScheduler.kt
│ │ │ │ └── ReservationCleanupScheduler.kt
│ │ │ └── user
│ │ ├── config
│ │ │ └── SwaggerConfig.kt
│ │ ├── domain (Domain 계층)
│ │ │ ├── balance
│ │ │ │ ├── BalanceHistory.kt
│ │ │ │ └── BalanceService.kt
│ │ │ ├── common
│ │ │ │ └── Auditable.kt
│ │ │ ├── concert
│ │ │ │ ├── Concert.kt
│ │ │ │ ├── ConcertRepository.kt
│ │ │ │ ├── ConcertSchedule.kt
│ │ │ │ ├── ConcertScheduleRepository.kt
│ │ │ │ ├── ConcertScheduleService.kt
│ │ │ │ └── ConcertService.kt
│ │ │ ├── payment
│ │ │ │ ├── Payment.kt
│ │ │ │ ├── PaymentRepository.kt
│ │ │ │ ├── PaymentService.kt
│ │ │ │ └── PaymentStatus.kt
│ │ │ ├── queue
│ │ │ │ ├── QueueEntry.kt
│ │ │ │ ├── QueueRepository.kt
│ │ │ │ ├── QueueService.kt
│ │ │ │ └── QueueStatus.kt
│ │ │ ├── reservation
│ │ │ │ ├── Reservation.kt
│ │ │ │ ├── ReservationRepository.kt
│ │ │ │ ├── ReservationService.kt
│ │ │ │ └── ReservationStatus.kt
│ │ │ ├── seat
│ │ │ │ ├── Seat.kt
│ │ │ │ ├── SeatAlreadyReservedException.kt
│ │ │ │ ├── SeatRepository.kt
│ │ │ │ ├── SeatService.kt
│ │ │ │ └── SeatStatus.kt
│ │ │ └── user
│ │ │ ├── User.kt
│ │ │ ├── UserRepository.kt
│ │ │ └── UserService.kt
│ │ ├── infrastructure (Persistence 계층)
│ │ │ ├── concert
│ │ │ │ ├── ConcertRepositoryImpl.kt
│ │ │ │ ├── ConcertScheduleRepositoryImpl.kt
│ │ │ │ ├── JpaConcertRepository.kt
│ │ │ │ └── JpaConcertScheduleRepository.kt
│ │ │ ├── payment
│ │ │ │ ├── JpaPaymentRepository.kt
│ │ │ │ └── PaymentRepositoryImpl.kt
│ │ │ ├── queue
│ │ │ │ ├── JpaQueueRepository.kt
│ │ │ │ └── QueueRepositoryImpl.kt
│ │ │ ├── reservation
│ │ │ │ ├── JpaReservationRepository.kt
│ │ │ │ └── ReservationRepositoryImpl.kt
│ │ │ ├── seat
│ │ │ │ ├── JpaSeatRepository.kt
│ │ │ │ └── SeatRepositoryImpl.kt
│ │ │ └── user
│ │ │ ├── JpaUserRepository.kt
│ │ │ └── UserRepositoryImpl.kt
│ │ └── interfaces (Presentation 계층)
│ │ ├── QueueController.kt
│ │ ├── api
│ │ │ ├── balance
│ │ │ │ └── BalanceController.kt
│ │ │ ├── concert
│ │ │ ├── payment
│ │ │ │ └── PaymentController.kt
│ │ │ ├── reservation
│ │ │ │ └── ReservationController.kt
│ │ │ └── user
│ │ │ ├── AuthController.kt
│ │ │ └── UserController.kt
│ │ ├── common
│ │ │ └── AuthUtil.kt
│ │ └── dto
│ │ ├── ApiResponse.kt
│ │ ├── AuthResponse.kt
│ │ ├── BalanceRequest.kt
│ │ ├── ChargeRequest.kt
│ │ ├── ConcertScheduleDto.kt
│ │ ├── LoginRequest.kt
│ │ ├── PaymentRequest.kt
│ │ ├── PaymentResponse.kt
│ │ ├── QueueRegistrationRequest.kt
│ │ ├── QueueTokenResponse.kt
│ │ ├── ReservationRequest.kt
│ │ ├── ReservationResponse.kt
│ │ ├── SeatAvailabilityResponse.kt
│ │ ├── SeatDto.kt
│ │ └── TokenResponse.kt
└── resources
├── application-mysql.yml
└── application.yml
프로젝트에서 발생할 수 있는 동시성 이슈와 적용한 동시성 제어 방식들, 그리고 각각의 장단점 분석은 아래의 문서에서 자세히 확인하실 수 있습니다.
프로젝트의 조회 성능 개선을 위한 캐싱 대상 분석, Redis를 이용한 성능 개선 방안, 그리고 각각의 이유와 구현 계획은 아래의 문서에서 자세히 확인하실 수 있습니다.
프로젝트의 쿼리 성능 개선 시도' 작업에 대한 자세한 슬픈 이야기는 아래 문서에서 확인하실 수 있습니다.