μμ μμ½/λκΈ°/체ν¬μΈ μμ€ν
# Docker νκ²½ μμ
chmod +x docker-commands.sh
./docker-commands.sh up
# λλ μλμΌλ‘
docker-compose up -d./gradlew bootRun- API Documentation: http://localhost:8080/swagger-ui.html
λͺ¨λ κ³μ λΉλ°λ²νΈ: 12341234
| μ΄λ©μΌ | μ΄λ¦ | μν |
|---|---|---|
| member1@example.com | νμ1 | ACTIVE |
| member2@example.com | νμ2 | ACTIVE |
| member3@example.com | νμ3 | ACTIVE |
| member4@example.com | νμ4 | ACTIVE |
| member5@example.com | νμ5 | ACTIVE |
| member6@example.com | νμ6 | INACTIVE |
- μ΄λ©μΌ: admin@lessonrsvn.com
- λΉλ°λ²νΈ: admin123!@#
- κΆν: ADMIN (κ΄λ¦¬μ μ μ© API μ κ·Ό κ°λ₯)
- μ μ© μλν¬μΈνΈ:
/api/admin/**
μμ μμ½, λκΈ°μ΄ κ΄λ¦¬, 체ν¬μΈ κΈ°λ₯μ μ 곡νλ λ°±μλ μμ€ν μ λλ€.
- μμ κ΄λ¦¬: μμ μμ±, μ‘°ν, μμ , μμ , μ μ μ¦κ°
- μμ½ μμ€ν : μμ μμ½ μμ± λ° μ·¨μ, μ€λ³΅ μμ½ λ°©μ§
- λκΈ°μ΄ κ΄λ¦¬: μ μ μ΄κ³Ό μ μλ λκΈ°μ΄ λ±λ‘ λ° μΉκ²© μ²λ¦¬
- 체ν¬μΈ: μμ½μ μΆμ μ²λ¦¬, μ€λ³΅ 체ν¬μΈ λ°©μ§
- λμμ± μ μ΄: Redis λΆμ°λ½μ νμ©ν μμμ μ²λ¦¬
- μΊμ μ΅μ ν: Redis κΈ°λ° μ±λ₯ μ΅μ ν λ° Self-healing
- μ΄λ²€νΈ λ리λΈ: μμ μ·¨μ/μ μ μ¦κ°/νμ μν λ³κ²½ μ μ°κ΄ λ°μ΄ν° μλ μ²λ¦¬
- λ©±λ±μ± 보μ₯: API λ 벨 λ©±λ±μ± λ° μ€λ³΅ μμ² μ²λ¦¬
- Language: Java 21
- Framework: Spring Boot 3.2.10, Spring Security, Spring Data JPA
- Authentication: JWT (JSON Web Token)
- Database: MySQL 8.0 (Flyway λ§μ΄κ·Έλ μ΄μ )
- Cache & Lock: Redis 7.0, Redisson (λΆμ°λ½)
- Build Tool: Gradle
- Testing: JUnit 5, AssertJ, Mockito, Testcontainers
- Documentation: OpenAPI 3.0 (Swagger)
- Containerization: Docker, Docker Compose
com.lessonRsvn
βββ applicationInfra/ # κ³΅ν΅ μΈνλΌ
β βββ config/ # μ€μ (Security, Redis, Async)
β βββ exception/ # μ μ μμΈ μ²λ¦¬
β βββ idempotency/ # λ©±λ±μ± νν°
β βββ redis/ # Redis μλΉμ€ (μΊμ, λΆμ°λ½)
βββ lesson/ # μμ
λλ©μΈ
β βββ application/ # μ ν리μΌμ΄μ
μλΉμ€
β βββ domain/ # λλ©μΈ λͺ¨λΈ, μ΄λ²€νΈ, λ ν¬μ§ν 리
β βββ dto/ # λ°μ΄ν° μ μ‘ κ°μ²΄
β βββ interfaces/ # REST 컨νΈλ‘€λ¬
βββ reservation/ # μμ½ λλ©μΈ (CQRS μ μ©)
β βββ application/ # μ ν리μΌμ΄μ
μλΉμ€
β β βββ command/ # λͺ
λ Ή μλΉμ€ (μν λ³κ²½)
β β βββ query/ # μ‘°ν μλΉμ€ (μ½κΈ° μ μ©)
β βββ checkin/ # 체ν¬μΈ μλΈλλ©μΈ
β βββ domain/ # λλ©μΈ λͺ¨λΈ, μλΉμ€, μ΄λ²€νΈ
β βββ dto/ # λ°μ΄ν° μ μ‘ κ°μ²΄
β βββ interfaces/ # REST 컨νΈλ‘€λ¬ (Command/Query λΆλ¦¬)
βββ member/ # νμ λλ©μΈ
βββ application/ # μΈμ¦/νμ μλΉμ€
βββ domain/ # λλ©μΈ λͺ¨λΈ, μ΄λ²€νΈ
βββ systemNotice/ # μμ€ν
μλ¦Ό μλΈλλ©μΈ (TODO)
μμ½ λλ©μΈμ CQRS ν¨ν΄ μ μ©μΌλ‘ νμ₯μ±κ³Ό μ±λ₯ μ΅μ ν:
- μν : μμ½ μμ±, μ·¨μ, λκΈ°μ΄ μΉκ²© λ± μν λ³κ²½ μμ
- νΈλμμ
: μ°κΈ° νΈλμμ
(
@Transactional) - λμμ± μ μ΄: Redis λΆμ°λ½μΌλ‘ λ°μ΄ν° μΌκ΄μ± 보μ₯
- μ΄λ²€νΈ λ°ν: λλ©μΈ μ΄λ²€νΈλ₯Ό ν΅ν λΆκ° μμ μ²λ¦¬
- μν : μμ½ λͺ©λ‘, λκΈ°μ΄, ν΅κ³ λ± λ€μν μ‘°ν μμ
- νΈλμμ
: μ½κΈ° μ μ© (
@Transactional(readOnly = true)) - μ±λ₯ μ΅μ ν: Redis μΊμ μ°μ μ‘°ν, N+1 λ¬Έμ ν΄κ²°
- νμ₯μ±: μ‘°ν μ μ© μ΅μ ν λ° λ 립μ μ€μΌμΌλ§ κ°λ₯
- MySQL: localhost:3307/lesson_rsvn (lesson_rsvn/lesson_rsvn)
- Redis: localhost:6380 (password: redispw)
./docker-commands.sh up # νκ²½ μμ
./docker-commands.sh down # νκ²½ μ’
λ£
./docker-commands.sh restart # νκ²½ μ¬μμ
./docker-commands.sh logs # λ‘κ·Έ νμΈ
./docker-commands.sh status # μν νμΈ
./docker-commands.sh clean # λ°μ΄ν° μμ - μ€λ³΅ μμ½ λ°©μ§: λμΌ νμμ λμΌ μμ μ€λ³΅ μμ½ μ°¨λ¨
- μ μ κ΄λ¦¬: Redis μΊμ κΈ°λ° μ€μκ° μ μ νμΈ
- μν κ²°μ : μ μ μ¬μ μ μ¦μ νμ , μ΄κ³Ό μ λκΈ°μ΄ λ±λ‘
- μ¬μμ½ μ§μ: μ·¨μλ μμ½μ μν λ³κ²½μ ν΅ν ν¨μ¨μ μ¬μμ½
- λμμ± μ μ΄: Redis λΆμ°λ½μΌλ‘ μμ ν μ·¨μ/μΉκ²© μ²λ¦¬
- μλ μΉκ²©: νμ μμ½ μ·¨μ μ λκΈ°μ΄ μ²« λ²μ§Έ νμ μλ μΉκΈ
- μμ 보μ₯: Redis Sorted Set κΈ°λ° λκΈ° μμ κ΄λ¦¬
- μν λκΈ°ν: μΊμ-DB μΌκ΄μ± μ μ§
- μ격 κ²μ¦: νμ μμ½ νμλ§ μ²΄ν¬μΈ κ°λ₯
- μκ° μ ν: μμ μμ 10λΆ μ λΆν° μ’ λ£κΉμ§λ§ νμ©
- μ€λ³΅ λ°©μ§: λμΌ μμ½μ λν μ€λ³΅ 체ν¬μΈ μ°¨λ¨
- μμ μ·¨μ: ν΄λΉ μμ μ λͺ¨λ μμ½ μλ μ·¨μ
- μ μ μ¦κ°: λκΈ°μ΄μμ μ¦κ°λλ§νΌ μλ μΉκΈ
- νμ λΉνμ±ν: ν΄λΉ νμμ λͺ¨λ μμ½ μλ μ·¨μ
- Redisson νμ©: μμ ν λΆμ° νκ²½ λμμ± μ μ΄
- λ½ λ²μ: μμ λ³ μμ½ μμ±/μ·¨μ/μΉκΈ μ²λ¦¬
- μ€μ κ°λ₯: λκΈ° μκ° λ° μλ μκ° νκ²½λ³ μ€μ
- λμ: μμ λ³ νμ μμ½ μ, λκΈ°μ΄ μ 보
- Self-healing: μΊμ λ―Έμ€ μ DB κΈ°λ° μλ 볡ꡬ
- TTL κ΄λ¦¬: μμ μμ μκ°μ μλ λ§λ£
- μ κΈ° λκΈ°ν: μ€μΌμ€λ¬ κΈ°λ° μΊμ-DB μΌκ΄μ± μ μ§
- μΈλ±μ€ μ λ΅: 쿼리 ν¨ν΄ κΈ°λ° λ³΅ν© μΈλ±μ€ μ€κ³
- N+1 ν΄κ²°: @EntityGraph νμ©ν νμΉ μ‘°μΈ
- μ½κΈ° μ΅μ ν: CQRS ν¨ν΄μ Query μλΉμ€ λΆλ¦¬
./gradlew test -Dspring.profiles.active=test./gradlew test --tests "*Integration*" -Dspring.profiles.active=testcontainers./gradlew test./gradlew jacocoTestReport
# build/reports/jacoco/test/html/index.htmlμμ νμΈ- λ¨μ ν μ€νΈ: λλ©μΈ μν°ν°, μλΉμ€λ³ λ¨μ ν μ€νΈ
- ν΅ν© ν μ€νΈ: Redis, MySQL ν¬ν¨ν μ€μ νκ²½ ν μ€νΈ
- λμμ± ν μ€νΈ: λΆμ°λ½ λ° λκΈ°μ΄ μ²λ¦¬ κ²μ¦
- μμ€ν ν μ€νΈ: End-to-End μ 체 νλ‘μ° κ²μ¦
POST /api/auth/login # λ‘κ·ΈμΈ
POST /api/auth/refresh # ν ν° κ°±μ
POST /api/auth/refresh-from-cookie # μΏ ν€ κΈ°λ° ν ν° κ°±μ
POST /api/auth/logout # λ‘κ·Έμμ
=== μΌλ° νμμ© ===
POST /api/members/signup # νμ κ°μ
GET /api/members/me # λ΄ μ 보 μ‘°ν
PUT /api/members/me # λ΄ μ 보 μμ
PATCH /api/members/me/password # λΉλ°λ²νΈ λ³κ²½
DELETE /api/members/me # νμ νν΄
=== κ΄λ¦¬μμ© ===
GET /api/admin/members # νμ λͺ©λ‘ μ‘°ν
GET /api/admin/members/{id} # νμ μμΈ μ‘°ν
PATCH /api/admin/members/{id}/status # νμ μν λ³κ²½
=== κ³΅ν΅ ===
GET /api/lessons # μμ
λͺ©λ‘ μ‘°ν
GET /api/lessons/{id} # μμ
μμΈ μ‘°ν
GET /api/lessons/reservable # μμ½ κ°λ₯ν μμ
μ‘°ν
GET /api/lessons/date # νΉμ λ μ§ μμ
μ‘°ν
=== κ΄λ¦¬μμ© ===
POST /api/admin/lessons # μμ
μμ±
PUT /api/admin/lessons/{id} # μμ
μ 보 μμ
DELETE /api/admin/lessons/{id} # μμ
μμ (μ·¨μ)
PATCH /api/admin/lessons/{id}/capacity # μμ
μ μ μ¦κ°
POST /api/reservations # μμ½ μμ±
DELETE /api/reservations/{id} # μμ½ μ·¨μ
=== κ°μΈ μμ½ μ‘°ν ===
GET /api/reservations/my # λ΄ μμ½ λͺ©λ‘
GET /api/reservations/my/status # μνλ³ λ΄ μμ½
GET /api/reservations/my/active # μ·¨μ κ°λ₯ν λ΄ μμ½
GET /api/reservations/my/date-range # κΈ°κ°λ³ λ΄ μμ½
GET /api/reservations/my/today # μ€λ νμ λ λ΄ μμ½
GET /api/reservations/my/checkin-eligible # 체ν¬μΈ κ°λ₯ν μμ½
GET /api/reservations/my/waiting-position/{lessonId} # λ΄ λκΈ° μλ²
=== μμ
λ³ μμ½ μ‘°ν (κ΄λ¦¬μ) ===
GET /api/admin/reservations/{id} # μμ½ μμΈ
GET /api/admin/reservations/lessons/{id} # μμ
λ³ μμ½ λͺ©λ‘
GET /api/admin/reservations/lessons/{id}/waiting # μμ
λ³ λκΈ°μ΄
GET /api/admin/reservations/lessons/{id}/confirmed # μμ
λ³ νμ μμ½
GET /api/admin/reservations/date # νΉμ λ μ§ λͺ¨λ μμ½
POST /api/checkins # 체ν¬μΈ μ²λ¦¬
GET /api/checkins/my # λ΄ μ²΄ν¬μΈ νν©
GET /api/admin/checkins/lessons/{id} # μμ
λ³ μ²΄ν¬μΈ νν© (κ΄λ¦¬μ)
GET /api/admin/checkins/reservations/{id} # μμ½λ³ 체ν¬μΈ μ‘°ν (κ΄λ¦¬μ)
- HTTP ν€λ:
X-Idempotency-KeyκΈ°λ° μ€λ³΅ μμ² μ²λ¦¬ - μΊμ μ μ₯: Redis 30μ΄ TTLλ‘ μλ΅ μΊμ±
- μλ λ°ν: λμΌ ν€ μμ² μ μ΄μ μλ΅ λ°ν
- ꡬ쑰νλ μλ΅: μΌκ΄λ μλ¬ ν¬λ§· μ 곡
- λΉμ¦λμ€ μμΈ: λλ©μΈλ³ λͺ νν μλ¬ λ©μμ§
- Fallback μ²λ¦¬: Redis μ₯μ μ DB κΈ°λ° μλΉμ€ μ μ§
- JWT μΈμ¦: μ‘μΈμ€/리νλ μ ν ν° κΈ°λ°
- μν κΈ°λ° μ κ·Ό μ μ΄: μΌλ° νμ/κ΄λ¦¬μ κΆν λΆλ¦¬
- νκ²½λ³ λ³΄μ μ€μ : κ°λ°/μ΄μ νκ²½ μ°¨λ³ν
μμΈν μ€κ³ λ° κ΅¬ν λ΄μ©μ λ€μ λ¬Έμλ₯Ό μ°Έκ³ νμΈμ:
- μ€κ³ κ²°μ κΈ°λ‘: νΈλμμ κ²½κ³, λμμ± μ μ΄, μΌκ΄μ± λͺ¨λΈ, μλ¬ μ²λ¦¬ λ± ν΅μ¬ μ€κ³ κ²°μ μ¬ν
- λλ©μΈ μ€κ³: DDD κΈ°λ° λλ©μΈ λͺ¨λΈ λ° λΉμ¦λμ€ κ·μΉ μμΈ μ€λͺ
- μμ λ‘κ·Έ: νλ‘μ νΈ κ°λ° κ³Όμ λ° μ£Όμ ꡬν λ΄μ© κΈ°λ‘
- TODO 리μ€νΈ: ꡬν νλͺ©
- κ΄λ¦¬μ λ³ κΆνμ μ΄
- μμ μν μλ λ³κ²½ (μμ/μ’ λ£ μκ° κΈ°μ€)
- 체ν¬μΈ 리λ§μΈλ μλ¦Ό
- μΊμ μ ν©μ± μ κΈ° μ κ²
- μ€μκ° μλ¦Ό (Server-Sent Events)
- μμ½ νμ /μ·¨μ/μΉκ²© μλ¦Ό
- μμ 리λ§μΈλ μλ¦Ό
- λΉμ¦λμ€ λ©νΈλ¦ μμ§
- μ±λ₯ μ§ν λͺ¨λν°λ§
- μλ¦Ό λ° λμ보λ ꡬμΆ