한정된 강의 좌석을 여러 사용자가 동시에 신청하는 상황을 다루는 수강 신청 백엔드입니다. 좌석 확보, 결제 대기 만료, 대기열 승격이 경쟁 상황에서도 일관되게 이어지는 데 집중합니다.
| 영역 | 기술 |
|---|---|
| Language | Kotlin 2.2.21, Java 21 |
| Application | Spring Boot 4.0.6, Spring MVC, Spring Security |
| Persistence | MySQL 8.4, Spring Data JPA, JDBC, Liquibase |
| Queue / Token | Redis 7.4, Redis Sorted Set, Lua |
| Test | JUnit 5, Testcontainers, k6, JaCoCo |
| Observability | Prometheus, Loki, Promtail, Grafana |
| Build | Gradle Kotlin DSL, ktlint |
- 좌석 초과 방지: MySQL 조건부 갱신으로 남은 좌석이 있을 때만 원자적으로 차감합니다.
- 좌석 선점 독점 방지: 신청 직후 좌석은
PENDING상태로 10분간만 확보됩니다. - 빈 좌석 자동 재배정: 취소·결제 대기 만료로 반환된 좌석은 Redis 대기열의 다음 신청자에게 배정됩니다.
- 재처리 가능한 흐름: 대기열 복구 스케줄러가 좌석과 대기열 불일치를 다시 처리합니다.
stateDiagram-v2
[*] --> PENDING: 좌석 확보
PENDING --> CONFIRMED: 결제 확정
PENDING --> CANCELLED: 신청 취소
PENDING --> EXPIRED: 결제 대기 만료
CANCELLED --> PENDING: 재신청
EXPIRED --> PENDING: 재신청 또는 대기열 승격
Course가 좌석과 모집 상태를, Enrollment가 신청 생명주기를 담당합니다. MySQL은 좌석·신청의 기준 데이터이며, Redis는 순서가 필요한 대기열과 인증 토큰을 담당합니다.
기능을 계층별 Gradle 모듈로 분리했습니다. service는 infrastructure의 저장소 포트에만 의존하고, JPA·JDBC·Redis 모듈이 포트를 구현합니다. application은 실행 시 필요한 어댑터를 조립합니다.
flowchart LR
Client[Client] --> API[course:api]
subgraph Core
API --> Service[course:service]
Service --> Model[course:model]
Service --> Ports[course:infrastructure<br/>Repository Ports]
Ports --> Model
end
subgraph Driven Adapters
JPA[course:repository-jpa<br/>단건 조회 OR 쓰기]
JDBC[course:repository-jdbc<br/>벌크 좌석 AND 만료 처리]
Redis[course:repository-redis<br/>대기열 AND 토큰]
Schema[course:schema<br/>Liquibase]
end
JPA --> Ports
JDBC --> Ports
Redis --> Ports
JDBC --> Schema
App[course:application] --> API
App --> JPA
App --> JDBC
App --> Redis
common:*은 도메인과 무관한 API 응답, 보안, 부트스트랩, JPA 기반 기능만 제공합니다.
도메인 규칙은 course:model, 유스케이스 조율은 course:service, 기술 구현은 저장소 어댑터에 남깁니다.