호스트를 위한 간편 예약 관리 솔루션의 백엔드 서버입니다.
이벤트 생성 → 예약 폼 공유 → 선착순 예약 → QR 체크인까지 하나의 플로우로 처리합니다.
서비스 링크 : https://www.form-pass.life
프론트엔드 : https://github.com/LEEDONGH00N/form-pass-client
| 분류 | 기술 |
|---|---|
| Language | Java 17 |
| Framework | Spring Boot 3.4.12 |
| ORM | Spring Data JPA (Hibernate) |
| Database | MySQL 8.0 (RDS), H2 (테스트) |
| Auth | Spring Security + JWT (jjwt 0.11.5) |
| Infra | AWS EC2, RDS, S3 |
| CI/CD | GitHub Actions |
| Monitoring | Spring Actuator + Micrometer Prometheus |
| API Docs | Springdoc OpenAPI 2.7.0 (Swagger UI) |
| Spring Mail (Google SMTP) | |
| Cache | Caffeine (이메일 인증코드 TTL 관리) |
| Build | Gradle |
[Client - Vercel]
│
▼ HTTPS
[EC2 Instance]
├── Nginx (Reverse Proxy, SSL)
└── Spring Boot (ticket-form.jar)
├── RDS MySQL (데이터 저장)
├── S3 (이미지 저장)
└── Google SMTP (메일 발송)
┌──────────┐ ┌──────────────┐ ┌─────────────────┐
│ hosts │ │ events │ │ event_schedules │
├──────────┤ ├──────────────┤ ├─────────────────┤
│ id (PK) │──1:N─▶│ id (PK) │──1:N─▶│ id (PK) │
│ email │ │ host_id (FK) │ │ event_id (FK) │
│ password │ │ title │ │ startTime │
│ name │ │ location │ │ endTime │
│ role │ │ description │ │ maxCapacity │
└──────────┘ │ eventCode │ │ reservedCount │
│ isPublic │ └────────┬────────┘
└──────┬───────┘ │
│ │ 1:N
│ 1:N ▼
┌──────┴───────┐ ┌─────────────────┐
│form_questions│ │ reservations │
├──────────────┤ ├─────────────────┤
│ id (PK) │ │ id (PK) │
│ event_id(FK) │ │ schedule_id(FK) │
│ questionText │ │ guestName │
│ questionType │ │ guestPhone(AES) │
│ isRequired │ │ ticketCount │
└──────┬───────┘ │ qrToken (UUID) │
│ │ isCheckedIn │
│ │ status │
│ └────────┬────────┘
│ │ 1:N
│ ┌────────┴────────┐
│ │ form_answers │
└──────────────▶├─────────────────┤
N:1 │ id (PK) │
│ reservation_id │
│ question_id(FK) │
│ answerText │
└─────────────────┘
┌──────────────┐
│ event_images │
├──────────────┤
│ id (PK) │
│ event_id(FK) │ ◀── events 1:N
│ imageUrl │
│ orderIndex │
└──────────────┘
Enum 타입:
Role: HOSTReservationStatus: CONFIRMED, CANCELLEDQuestionType: TEXT, CHECKBOX, RADIO
요청 A ──▶ ReservationFacade
└─ LockExecutor.executeWithLock("schedule:1")
└─ ReservationService (@Transactional 내부)
└─ reservedCount 검증 → 증가 → 예약 생성
요청 B ──▶ Lock 대기 후 순차 처리
InMemoryLockExecutor(ReentrantLock) + Facade 패턴으로 락과 트랜잭션 분리ConcurrentHashMap<String, ReentrantLock>— 키별 공정 락 (FIFO),tryLock(5, SECONDS)- 락은
@Transactional바깥에서 획득하여 DB 커넥션 점유 시간 최소화 LockExecutor인터페이스 기반 설계로 추후 분산 락(Redis 등) 전환 가능reservedCount >= maxCapacity시IllegalStateException(409)- 동일 스케줄 + 전화번호 중복 예약 거부
예약 확정 → UUID 기반 qrToken 발급
→ 게스트가 QR 코드 제시
→ 호스트가 QR 스캔 (POST /api/host/checkin)
→ 또는 수동 체크인 (PATCH /api/host/reservations/{id}/checkin)
→ isCheckedIn = true 업데이트
1. 클라이언트 → POST /api/host/s3/presigned-url (fileName, contentType)
2. 서버 → S3 Presigned PUT URL + 최종 fileUrl 반환
3. 클라이언트 → S3에 직접 PUT 업로드
4. 클라이언트 → 이벤트 생성 시 fileUrl을 images 목록에 포함
| 버전 | 동시성 제어 방식 | 문제점 / 개선 이유 |
|---|---|---|
| v1 | @Lock(PESSIMISTIC_WRITE) 비관적 락 |
DB row-level 락으로 트랜잭션 전체 구간 동안 커넥션 점유 → 고부하 시 커넥션 풀 고갈 위험 |
| v2 | Redis + Redisson 분산 락 | 단일 인스턴스 환경에 Redis 인프라 운영 비용이 과도, 장애 포인트 증가 |
| v3 (현재) | 인메모리 ReentrantLock + Facade 패턴 |
락과 트랜잭션 분리로 커넥션 점유 최소화, 인프라 의존성 제거, LockExecutor 인터페이스로 확장성 유지 |
각 의사결정의 상세 근거: docs/adr/
| 계층 | 전략 | 테스트 수 |
|---|---|---|
| Service (단위) | @ExtendWith(MockitoExtension.class) + Mock |
46개 |
| Repository (통합) | @DataJpaTest + H2 + QueryDSL |
8개 |
| Controller (슬라이스) | @WebMvcTest + @MockBean |
10개 |
| Context Load | @SpringBootTest |
1개 |
| 합계 | 65개 |
./gradlew clean test # 전체 테스트 실행- Java 17+
- MySQL 8.0 (local 프로필) 또는 H2 (test 프로필)
- Gradle 8+
PR 생성 → GitHub Actions → ./gradlew clean test → 테스트 결과 PR 코멘트
main 병합 → GitHub Actions → JAR 빌드 → SCP로 EC2 전송 → deploy 스크립트 실행
| 엔드포인트 | 용도 |
|---|---|
/actuator/health |
서버 상태 (DB, Disk 포함) |
/actuator/prometheus |
Prometheus 메트릭 수집 |
수집 메트릭: HTTP 요청 응답시간 분포 (P95, P99), JVM 메모리/GC, Tomcat 스레드 풀
k6를 사용하여 예약 생성 API의 동시성 안정성을 검증합니다.
# Spike 테스트 (50 → 100 VU)
k6 run k6/reservation-test.js
# Constant 테스트 (50 VU 고정)
k6 run k6/reservation-constant-test.js| 시나리오 | VU | 총 요청 | 평균 응답 | p95 | 에러율 | 초과 예약 |
|---|---|---|---|---|---|---|
| Spike (100 VU 피크) | 0→100 | 11,200건 | 706ms | 1,000ms | 0% | 0건 |
| Constant (50 VU) | 50 | 8,152건 | 236ms | 567ms | 0% | 0건 |
상세 결과: docs/reports/load-test-report.md, docs/reports/constant-test-report.md