λ³Έ νλ‘μ νΈλ νλ‘λνΈ μμ§λμ΄ μ±μ© κ³Όμ μ€ BE-A. μκ° μ μ² μμ€ν μ ꡬνν λ°±μλ μ ν리μΌμ΄μ μ λλ€.
ν¬λ¦¬μμ΄ν°κ° κ°μλ₯Ό κ°μ€νκ³ , ν΄λμ€λ©μ΄νΈκ° κ°μμ μκ° μ μ²ν λ€ κ²°μ νμ μ ν΅ν΄ μκ°μ νμ νλ νλ¦μ μ 곡ν©λλ€.
κ°μ μν μ μ΄, μκ° μ μ² μν μ μ΄, μ μ μ΄κ³Ό λ°©μ§, λμμ± μ μ΄μ κ°μ λΉμ¦λμ€ κ·μΉμ Spring Boot κΈ°λ° REST APIλ‘ κ΅¬ννλ κ²μ λͺ©νλ‘ ν©λλ€.
μ£Όμ κΈ°λ₯μ λ€μκ³Ό κ°μ΅λλ€.
- κ°μ μμ±, μμ , λͺ¨μ§ μμ, λͺ¨μ§ λ§κ°
- κ°μ λͺ©λ‘ μ‘°ν λ° μμΈ μ‘°ν
- κ°μ μν μ μ΄ κ΄λ¦¬
DRAFTOPENCLOSED
- μκ° μ μ² μμ±, κ²°μ νμ , μκ° μ·¨μ
- μκ° μ μ² μν μ μ΄ κ΄λ¦¬
PENDINGCONFIRMEDCANCELLED
- κ²°μ λκΈ° μν μλ μ·¨μ(μ€μΌμ€λ¬)
- κ²°μ νμ μμ μ μκ° νμ μ²λ¦¬
- κ°μ μ μ μ΄κ³Ό λ°©μ§
- λμ μ μ² μν©μμ λ°μ΄ν° μ ν©μ± 보μ₯
- κ°μλ³ μκ°μ λͺ©λ‘ μ‘°ν(ν¬λ¦¬μμ΄ν° μ μ©)
- λ΄ μκ° μ μ² λͺ©λ‘ νμ΄μ§λ€μ΄μ
- Redis κΈ°λ° λκΈ°μ΄ κ΄λ¦¬(μκ° λ¨μΌλ©΄ ꡬν μμ )
- Docker Compose κΈ°λ° μ€ν νκ²½ μ 곡
- ν μ€νΈ μ½λλ₯Ό ν΅ν ν΅μ¬ λΉμ¦λμ€ κ·μΉ κ²μ¦
- Java 21
- Spring Boot 3.5.14
- Spring Data JPA
- JPA / Hibernate
- Bean Validation
- SpringDoc OpenAPI
- Flyway
- MySQL 8.4
- Gradle
- Docker
- Docker Compose
- JUnit 5
- Spring Boot Test
- Spring MVC Test / MockMvc
- Spring Data JPA Test
- AssertJ
- Mockito
- Testcontainers
- MySQL 8.4 κΈ°λ° Repository / Integration / Concurrency Test
λ³Έ νλ‘μ νΈλ Docker Composeλ₯Ό ν΅ν΄ Spring Boot μ ν리μΌμ΄μ κ³Ό MySQLμ ν¨κ» μ€νν μ μλλ‘ κ΅¬μ±νμ΅λλ€. Docker Desktopμ μ¬μ©νλ κ²½μ° λ³λ μ€μΉ μμ΄ μ€ν κ°λ₯ν©λλ€.
git clone https://github.com/devJin11/liveclass-assignment.git
cd liveclass-assignmentνκ°μκ° λ³λμ λ‘컬 Java, Gradle, MySQL μ€μΉ μμ΄ μ€νν μ μλλ‘ Docker Compose κΈ°λ° μ€ν νκ²½μ μ 곡ν©λλ€.
docker compose up --buildλ°±κ·ΈλΌμ΄λμμ μ€ννλ €λ©΄ λ€μ λͺ λ Ήμ΄λ₯Ό μ¬μ©ν©λλ€.
docker compose up --build -dhttp://localhost:8080
Docker Compose λ΄λΆμμ Spring Boot μ ν리μΌμ΄μ μ λ€μ μ£Όμλ‘ MySQLμ μ°κ²°ν©λλ€.
jdbc:mysql://mysql:3306/enrollment_db
λ‘컬 PCμμ DataGrip, DBeaver, MySQL Workbench λ±μΌλ‘ μ§μ μ μν κ²½μ°μλ λ€μ μ 보λ₯Ό μ¬μ©ν©λλ€.
Host: localhost
Port: 13306
Database: enrollment_db
Username: enrollment_user
Password: enrollment_password
Root Password: root_password
http://localhost:8080/swagger-ui/index.html
ν μ€νΈλ JUnit 5, Spring Boot Test, MockMvc, AssertJ, Mockito, Testcontainers, MySQL 8.4 κΈ°λ°μΌλ‘ ꡬμ±λμ΄ μμ΅λλ€.
ν μ€νΈ μ€ν μ Docker Desktopμ΄ μ€ν μ€μ΄μ΄μΌ ν©λλ€.
Linux / macOS νκ²½:
./gradlew clean testWindows PowerShell νκ²½:
.\gradlew.bat clean testμ μ ν΅κ³Ό μ λ€μκ³Ό κ°μ κ²°κ³Όκ° μΆλ ₯λ©λλ€.
BUILD SUCCESSFUL
docker compose downDB λ°μ΄ν°κΉμ§ ν¨κ» μμ νλ €λ©΄ λ€μ λͺ λ Ήμ΄λ₯Ό μ¬μ©ν©λλ€.
docker compose down -vμ μΆ λ° μ€ν κ²μ¦μ Docker Compose κΈ°μ€μΌλ‘ ꡬμ±νμ΅λλ€.
λ€λ§ λ‘컬 κ°λ° μμλ λ€μ λ°©μμΌλ‘ κ°λ°νμμ΅λλ€.
- Spring Boot μ ν리μΌμ΄μ μ IntelliJμμ μ§μ μ€ν
- MySQL 8.4λ Docker 컨ν μ΄λλ‘ λ¨λ μ€ν
- Spring profileμ
localμ¬μ©
λ‘컬 κ°λ°μ© μ€μ νμΌμ λ€μ νμΌμ μ¬μ©ν©λλ€.
src/main/resources/application-local.yml
ν΄λΉ νμΌμ κ°μΈ λ‘컬 νκ²½ μ€μ μ΄λ―λ‘ Gitμ ν¬ν¨νμ§ μμμ΅λλ€.
λ³Έ νλ‘μ νΈλ μꡬμ¬νμμ λͺ μλ κ°μ μμ±, μκ° μ μ², κ²°μ νμ , μ·¨μ, μ μ κ΄λ¦¬ κΈ°λ₯μ κΈ°λ°μΌλ‘ μ€μ μλΉμ€μμ λ°μν μ μλ μν μ μ΄, μ’μ μ μ , λμμ± μ μ΄, κ²°μ λκΈ° λ§λ£, ν¬λ¦¬μμ΄ν° κΆν λ²μ, λκΈ°μ΄ μ μ± μ κ³ λ €νμ¬ λ€μκ³Ό κ°μ΄ μꡬμ¬νμ ν΄μνμ΅λλ€.
κ³Όμ μꡬμ¬νμμ μΈμ¦/μΈκ°λ κ°λ΅ν μ²λ¦¬ κ°λ₯νλ€κ³ μλ΄λμ΄ μμΌλ―λ‘, λ³Έ νλ‘μ νΈμμλ Spring Security κΈ°λ° λ‘κ·ΈμΈ/μΈμ¦μ ꡬννμ§ μμ΅λλ€.
λμ API μμ² μ λ€μ μλ³μλ₯Ό μμ² λ°λ λλ μμ² νλΌλ―Έν°λ‘ μ λ¬λ°μ κΆνμ λ¨μ κ²μ¦ν©λλ€.
- ν¬λ¦¬μμ΄ν° κΆν API:
creatorId - ν΄λμ€λ©μ΄νΈ κΆν API:
classmateId
μλ₯Ό λ€μ΄ κ°μ μμ , κ°μ λͺ¨μ§ μμ/λ§κ°, κ°μλ³ νμ μκ°μ λͺ©λ‘ μ‘°νλ μμ²ν creatorIdκ° ν΄λΉ κ°μμ ν¬λ¦¬μμ΄ν°μΈμ§ κ²μ¦ν©λλ€.
μκ° μ μ² μμΈ μ‘°ν, κ²°μ νμ , μκ° μ·¨μλ μμ²ν classmateIdκ° ν΄λΉ μ μ²μ μμ μμΈμ§ κ²μ¦ν©λλ€.
κ³΅ν΅ APIμμλ role νλΌλ―Έν°λ₯Ό λ°μ κ²μ¦ν©λλ€.
role = CREATOR
role = CLASSMATE
λ³Έ κ³Όμ μ ν΅μ¬μ νμ κ΄λ¦¬κ° μλλΌ κ°μμ μκ° μ μ² νλ¦μ΄λ―λ‘, creator, classmateμ λν CRUD APIλ λ³λλ‘ κ΅¬ννμ§ μμ΅λλ€.
λ€λ§ DB μ ν©μ±μ μν΄ class_room, enrollmentμμ FKλ‘ μ°Έμ‘°νλ―λ‘ μν°ν°μ Repositoryλ μ μ§ν©λλ€.
ν μ€νΈ λ° API κ²μ¦μ μν΄ Flyway seed SQLλ‘ μ΄κΈ° λ°μ΄ν°λ₯Ό μ 곡ν©λλ€.
ν¬λ¦¬μμ΄ν°λ κ°μλ₯Ό μμ±ν μ μμ΅λλ€.
κ°μ μμ± μ λ€μ μ 보λ₯Ό μ λ ₯ν©λλ€.
- κ°μ μ λͺ©
- κ°μ μ€λͺ
- κ°κ²©
- μ΅λ μκ° μ μ
- μκ° μμμΌ
- μκ° μ’ λ£μΌ
κ°μλ μ΅μ΄ μμ± μ νμ DRAFT μνλ‘ μμ±λ©λλ€.
μ΅μ΄ κ°μ μν = DRAFT
DRAFT μνλ μ΄μ μνμ΄λ―λ‘ μκ°μμ ν΄λΉ κ°μμ μκ° μ μ²ν μ μμ΅λλ€.
κ°μλ λ€μ μνλ₯Ό κ°μ§λλ€.
| μν | μ€λͺ | μκ° μ μ² κ°λ₯ μ¬λΆ |
|---|---|---|
DRAFT |
μ΄μ | λΆκ° |
OPEN |
λͺ¨μ§ μ€ | κ°λ₯ |
CLOSED |
λͺ¨μ§ λ§κ° | λΆκ° |
νμ©λλ κ°μ μν μ μ΄λ λ€μκ³Ό κ°μ΅λλ€.
DRAFT -> OPEN
OPEN -> CLOSED
νμ©νμ§ μλ μν μ μ΄λ λ€μκ³Ό κ°μ΅λλ€.
DRAFT -> CLOSED
OPEN -> DRAFT
CLOSED -> OPEN
CLOSED -> DRAFT
ν¬λ¦¬μμ΄ν°λ DRAFT μνμ κ°μλ₯Ό OPEN μνλ‘ λ³κ²½νμ¬ μκ° μ μ²μ λ°μ μ μμ΅λλ€.
λν μ μμ΄ λͺ¨λ μ°¨μ§ μμλλΌλ ν¬λ¦¬μμ΄ν°λ μ΄μ νλ¨μ λ°λΌ OPEN μνμ κ°μλ₯Ό CLOSED μνλ‘ λ³κ²½νμ¬ λͺ¨μ§μ λ§κ°ν μ μμ΅λλ€.
κ°μ μμ μ DRAFT μνμμλ§ νμ©ν©λλ€.
OPEN λλ CLOSED μνμ κ°μλ μμ ν μ μμ΅λλ€.
μ΄λ κ² μ€κ³ν μ΄μ λ λ€μκ³Ό κ°μ΅λλ€.
- μ΄λ―Έ λͺ¨μ§ μ€μΈ κ°μμ κ°κ²©, κΈ°κ°, μ μμ΄ λ³κ²½λλ©΄ κΈ°μ‘΄ μ μ²μμκ² μν₯μ μ€ μ μμ΅λλ€.
- λͺ¨μ§ λ§κ° μ΄ν κ°μ μ λ³΄κ° λ³κ²½λλ©΄ μ μ² λΉμμ μ 보μ μ€μ κ°μ μ 보 μ¬μ΄μ μ ν©μ±μ΄ κΉ¨μ§ μ μμ΅λλ€.
- λ³Έ κ³Όμ μμλ λ³κ²½ μ΄λ ₯ κ΄λ¦¬κΉμ§ μꡬνμ§ μμΌλ―λ‘, λ°μ΄ν° μ ν©μ±κ³Ό ꡬν λͺ
νμ±μ μν΄
DRAFTμνμμλ§ μμ κ°λ₯νλλ‘ μ ννμ΅λλ€.
μ€λ¬΄ μλΉμ€μμλ κ°μ μκ°κΈ, μλ΄ λ¬Έκ΅¬, μΈλ€μΌ λ± μΌλΆ λΉν΅μ¬ νλλ§ OPEN μνμμλ μμ κ°λ₯νκ² μ€κ³ν μ μμ§λ§, λ³Έ νλ‘μ νΈμμλ ν΅μ¬ μꡬμ¬ν ꡬνμ μ§μ€νκΈ° μν΄ μμ κ°λ₯ μνλ₯Ό DRAFTλ‘ μ ννμ΅λλ€.
κ°μ λͺ©λ‘ μ‘°νλ ν¬λ¦¬μμ΄ν°μ μκ°μ λͺ¨λ μ¬μ©ν μ μλ κ³΅ν΅ APIλ‘ μ 곡ν©λλ€.
λ€λ§ μν μ λ°λΌ μ‘°ν κ°λ₯ν κ°μ μνλ₯Ό λ€λ₯΄κ² μ νν©λλ€.
ν¬λ¦¬μμ΄ν°λ λ³ΈμΈμ΄ κ°μ€ν κ°μλ₯Ό κ΄λ¦¬ν΄μΌ νλ―λ‘ λ€μ μνλ₯Ό λͺ¨λ νν°λ§νμ¬ μ‘°νν μ μμ΅λλ€.
DRAFT
OPEN
CLOSED
ν¬λ¦¬μμ΄ν°μ κ°μ λͺ©λ‘ μ‘°νλ λ³ΈμΈμ΄ μμ±ν κ°μλ₯Ό κΈ°μ€μΌλ‘ ν©λλ€.
classRoom.creatorId == μμ² creatorId
λ°λΌμ ν¬λ¦¬μμ΄ν°λ μμ μ΄ μμ±ν μ΄μ κ°μ, λͺ¨μ§ μ€ κ°μ, λͺ¨μ§ λ§κ° κ°μλ₯Ό λͺ¨λ μ‘°νν μ μμ΅λλ€.
μκ°μμ κ³΅κ° κ°λ₯ν κ°μλ§ μ‘°νν μ μμ΅λλ€.
μκ°μμ΄ νν°λ§ν μ μλ μνλ λ€μκ³Ό κ°μ΅λλ€.
OPEN
CLOSED
DRAFT μνλ μμ§ κ³΅κ° μ μ΄μμ΄λ―λ‘ μκ°μ λͺ©λ‘ μ‘°ν λμμμ μ μΈν©λλ€.
μκ°μ κ΄μ μμ κ° μνμ μλ―Έλ λ€μκ³Ό κ°μ΅λλ€.
| μν | μ€λͺ |
|---|---|
OPEN |
νμ¬ μκ° μ μ² κ°λ₯ν κ°μ |
CLOSED |
λͺ¨μ§μ΄ λ§κ°λμ΄ μ μ²μ λΆκ°λ₯νμ§λ§ μ‘°ν κ°λ₯ν κ°μ |
μκ°μμ΄ DRAFT μνλ‘ νν°λ§μ μμ²νλ©΄ μλͺ»λ μμ²μΌλ‘ μ²λ¦¬ν©λλ€.
κ°μ μμΈ μ‘°νλ ν¬λ¦¬μμ΄ν°μ μκ°μ λͺ¨λ μ¬μ©ν μ μλ κ³΅ν΅ APIλ‘ μ 곡ν©λλ€.
λ€λ§ μμΈ μ‘°ν μμ μν μ λ°λΌ μ κ·Ό κ°λ₯ν μνλ₯Ό λ€λ₯΄κ² μ νν©λλ€.
ν¬λ¦¬μμ΄ν°λ λ³ΈμΈμ΄ κ°μ€ν κ°μλΌλ©΄ λ€μ μνλ₯Ό λͺ¨λ μμΈ μ‘°νν μ μμ΅λλ€.
DRAFT
OPEN
CLOSED
λ¨, λ€λ₯Έ ν¬λ¦¬μμ΄ν°κ° μμ±ν κ°μμ DRAFT μμΈ μ 보λ μ‘°νν μ μμ΅λλ€.
μκ°μμ λ€μ μνμ κ°μλ§ μμΈ μ‘°νν μ μμ΅λλ€.
OPEN
CLOSED
DRAFT μνμ κ°μλ κ³΅κ° μ μ΄μμ΄λ―λ‘ μκ°μμκ² λ
ΈμΆνμ§ μμ΅λλ€.
μμΈ μ‘°ν μλ΅μλ νμ¬ μ μ² μΈμμ ν¬ν¨ν©λλ€.
νμ¬ μ μ² μΈμ = PENDING μν μ μ² μ + CONFIRMED μν μ μ² μ
λ³Έ νλ‘μ νΈμμλ μκ° μ μ²κ³Ό μκ° νμ μ μλ‘ λ€λ₯Έ λΉμ¦λμ€ νλ¦μΌλ‘ ν΄μνμ΅λλ€.
μκ° μ μ² = μ’μμ ν보νκ³ κ²°μ λ₯Ό λκΈ°νλ νμ
μκ° νμ = κ²°μ μλ£ ν μκ° μνλ₯Ό νμ νλ νμ
μκ° μ μ²μ΄ μ±κ³΅νλ©΄ Enrollmentκ° PENDING μνλ‘ μμ±λ©λλ€.
PENDING μνλ λ¨μ μμ λ°μ΄ν°κ° μλλΌ, μ’μμ μ μ ν κ²°μ λκΈ° μνλ‘ κ°μ£Όν©λλ€.
κ²°μ νμ APIκ° νΈμΆλλ©΄ PENDING μνμ μ μ²μ΄ CONFIRMED μνλ‘ λ³κ²½λ©λλ€.
μΈλΆ κ²°μ μμ€ν μ°λμ κ³Όμ μꡬμ¬νμ ν¬ν¨λμ΄ μμ§ μμΌλ―λ‘, λ³Έ νλ‘μ νΈμμλ κ²°μ νμ APIλ₯Ό ν΅ν΄ λ¨μ μν λ³κ²½μΌλ‘ λ체ν©λλ€.
μκ° μ μ²μ λ€μ μνλ₯Ό κ°μ§λλ€.
| μν | μ€λͺ | μ’μ μ μ μ¬λΆ |
|---|---|---|
PENDING |
μ μ² μλ£, κ²°μ λκΈ° | μ μ |
CONFIRMED |
κ²°μ μλ£, μκ° νμ | μ μ |
CANCELLED |
μ·¨μλ¨ | λ―Έμ μ |
κΈ°λ³Έ μν μ μ΄λ λ€μκ³Ό κ°μ΅λλ€.
PENDING -> CONFIRMED
PENDING -> CANCELLED
CONFIRMED -> CANCELLED
CANCELLED -> PENDING
CANCELLED -> PENDING μ μ΄λ μΌλ°μ μΈ λ³΅κ΅¬κ° μλλΌ, μ¬μ©μμ λͺ μμ μΈ μ¬μ μ² μμ²μ μν΄μλ§ νμ©λ©λλ€.
κ° μνμ μλ―Έλ λ€μκ³Ό κ°μ΅λλ€.
PENDING: μκ° μ μ²μ μλ£λμμ§λ§ μμ§ κ²°μ νμ μ μΈ μνμ λλ€. μ΄ μνμμλ μ’μμ μ μ ν©λλ€.CONFIRMED: κ²°μ κ° μλ£λμ΄ μ΅μ’ μκ°μ΄ νμ λ μνμ λλ€.CANCELLED: μ μ²μ΄ μ·¨μλμ΄ μ’μμ μ μ νμ§ μλ μνμ λλ€.
κ°μμ νμ¬ μ μ² μΈμμ μ’μμ μ μ νλ μ μ² μνλ₯Ό κΈ°μ€μΌλ‘ κ³μ°ν©λλ€.
enrollment_countλ μ’μμ μ μ νλ μ μ² μλ₯Ό μλ―Ένλ€.
μ¦, PENDING + CONFIRMED μνμ Enrollment μμ λμΌν΄μΌ νλ€.
νμ¬ μ μ² μΈμ(enrollment_count) = PENDING μν μ μ² μ + CONFIRMED μν νμ μ
CANCELLED μνλ μ’μμ μ μ νμ§ μμΌλ―λ‘ μ μ κ³μ°μμ μ μΈν©λλ€.
μ μμ΄ μ΄κ³Όλ κ²½μ° μΌλ° μκ° μ μ²μ μ€ν¨ν©λλ€.
μκ° μ μ²μ λμμ μ¬λ¬ μ¬μ©μκ° μμ²ν μ μμΌλ―λ‘, μ’μμ λν΄ κ²½μ μ‘°κ±΄μ΄ λ°μν μ μμ΅λλ€.
μλ₯Ό λ€μ΄ μ μμ΄ 10λͺ μ΄κ³ νμ¬ μ μ² μΈμμ΄ 9λͺ μΈ μν©μμ λ μ¬μ©μκ° λμμ μ μ²νλ©΄, λ¨μ μ‘°ν ν μ μ₯ λ°©μμΌλ‘λ 11λͺ μ΄ μ μ²λλ λ¬Έμ κ° λ°μν μ μμ΅λλ€.
μ΄λ₯Ό λ°©μ§νκΈ° μν΄ μκ° μ μ² μ λ€μ λ°©μμΌλ‘ λμμ±μ μ μ΄ν©λλ€.
enrollment_count = PENDING μν μ μ² μ + CONFIRMED μν μ μ² μ
λ°μ΄ν° μ ν©μ±κ³Ό λμμ± μ μ΄λ₯Ό μν΄ μκ° μ μ² μ class_room rowλ₯Ό μμμ μΌλ‘ μ‘°κ±΄λΆ UPDATEν©λλ€.
UPDATE class_room
SET enrollment_count = enrollment_count + 1
WHERE class_room_id = ?
AND status = 'OPEN'
AND enrollment_count < capacity;
μκ° μ μ² μ²λ¦¬ νλ¦μ λ€μκ³Ό κ°μ΅λλ€.
1. μκ° μ μ² μμ²
2. ν΄λμ€λ©μ΄νΈ μ‘΄μ¬ μ¬λΆ νμΈ
3. class_room.enrollment_count μ‘°κ±΄λΆ μ¦κ° μλ
4. μ¦κ° μ€ν¨ μ κ°μ μμ, λͺ¨μ§ μ€ μλ, μ μ μ΄κ³Ό μ€ νλλ‘ μμΈ μ²λ¦¬
5. λμΌ κ°μ/λμΌ ν΄λμ€λ©μ΄νΈ Enrollmentλ₯Ό FOR UPDATEλ‘ μ‘°ν
6. κΈ°μ‘΄ μ μ²μ΄ PENDING λλ CONFIRMEDμ΄λ©΄ μ€λ³΅ μ μ² μμΈ
7. κΈ°μ‘΄ μ μ²μ΄ CANCELLEDμ΄λ©΄ κΈ°μ‘΄ rowλ₯Ό PENDING μνλ‘ μ¬νμ±ν
8. κΈ°μ‘΄ μ μ²μ΄ μμΌλ©΄ μ κ· Enrollment μμ±
9. νΈλμμ
컀λ°
λμ μμ² μν©μμ λͺ¨λ νΈλμμ
μ΄ λ¨Όμ class_room rowμ λν΄ μ‘°κ±΄λΆ UPDATEλ₯Ό μννλλ‘ λ½ νλ μμλ₯Ό ν΅μΌνμ΅λλ€.
μ΄λ₯Ό ν΅ν΄ enrollment μ‘°ν μ κΈκ³Ό class_room κ°±μ μ κΈμ΄ κ΅μ°¨νλ©΄μ λ°μν μ μλ MySQL InnoDB λ°λλ½ κ°λ₯μ±μ μ€μ΄κ³ , λμμ μ¬λ¬ μμ²μ΄ λ€μ΄μλ μ΅μ’
μ’μ μ μ μκ° μ μμ μ΄κ³Όνμ§ μλλ‘ λ³΄μ₯ν©λλ€.
μ€λ³΅ μ μ² λ±μΌλ‘ λΉμ¦λμ€ μμΈκ° λ°μνλ κ²½μ° μ 체 νΈλμμ
μ΄ rollbackλλ―λ‘, μμ μνλ enrollment_count μ¦κ°λ ν¨κ» rollbackλ©λλ€.
PENDING μνλ μ’μμ μ μ νλ μνμ
λλ€.
λ°λΌμ μ¬μ©μκ° μκ° μ μ²λ§ νκ³ κ²°μ λ₯Ό μλ£νμ§ μμΌλ©΄ μ’μμ΄ κ³μ μ μ λλ λ¬Έμ κ° λ°μν μ μμ΅λλ€.
μ΄λ₯Ό λ°©μ§νκΈ° μν΄ λ³Έ νλ‘μ νΈμμλ κ²°μ λκΈ° λ§λ£ μ μ± μ μ μ©ν©λλ€.
μκ° μ μ² ν 10λΆ μ΄λ΄ κ²°μ νμ μ΄ λμ§ μμΌλ©΄ μλ μ·¨μν©λλ€.
μκ° μ μ²μ΄ μμ±λ λ κ²°μ λ§λ£ μκ°μ ν¨κ» μ μ₯ν©λλ€.
paymentExpiredAt = enrollmentCreatedAt + 10λΆ
1λΆ λ¨μ μ€μΌμ€λ¬κ° λ§λ£λ PENDING μ μ²μ μ‘°ννμ¬ CANCELLED μνλ‘ λ³κ²½ν©λλ€.
μ€ν μ£ΌκΈ°: 1λΆ
λμ: status = PENDING and paymentExpiredAt <= now
μ²λ¦¬: PENDING -> CANCELLED
μλ μ·¨μλ μ μ²μ μ’μμ λ μ΄μ μ μ νμ§ μμΌλ―λ‘ enrollmentCountκ° κ°μν©λλ€.
μ΄μ νκ²½μμ μ¬λ¬ μ ν리μΌμ΄μ μΈμ€ν΄μ€κ° λμμ μ€νλλ κ²½μ°μλ μ€μΌμ€λ¬ μ€λ³΅ μ€νμ λ§κΈ° μν΄ λΆμ° λ½μ΄ νμν μ μμ΅λλ€. λ³Έ κ³Όμ μμλ λ¨μΌ μ ν리μΌμ΄μ μΈμ€ν΄μ€ μ€νμ κΈ°μ€μΌλ‘ ꡬνν©λλ€.
μκ°μμ PENDING μνμ μκ° μ μ²μ κ²°μ νμ ν μ μμ΅λλ€.
κ²°μ νμ μ μνλ λ€μκ³Ό κ°μ΄ λ³κ²½λ©λλ€.
PENDING -> CONFIRMED
κ²°μ νμ μ κ²μ¦ κ·μΉμ λ€μκ³Ό κ°μ΅λλ€.
- μ μ² λ΄μμ΄ μ‘΄μ¬ν΄μΌ ν©λλ€.
- μμ²ν μκ°μμ΄ ν΄λΉ μ μ²μ μμ μμ¬μΌ ν©λλ€.
- μ μ² μνκ°
PENDINGμ΄μ΄μΌ ν©λλ€. - κ²°μ λκΈ° λ§λ£ μκ°μ΄ μ§λμ§ μμμΌ ν©λλ€.
- μ΄λ―Έ
CONFIRMEDλλCANCELLEDμνμΈ μ μ²μ κ²°μ νμ ν μ μμ΅λλ€. - κ²°μ νμ μ paymentExpiredAt μ΄μ κΉμ§λ§ κ°λ₯νλ©°, paymentExpiredAt μκ°μ΄ λλνλ©΄ λ§λ£λ κ²μΌλ‘ κ°μ£Όν©λλ€.
κ²°μ νμ μμ μλ μ μ κ²μ¬λ₯Ό λ€μ μννμ§ μμ΅λλ€.
κ·Έ μ΄μ λ μκ° μ μ² μμ μ μ΄λ―Έ PENDING μνλ‘ μ’μμ μ μ νκΈ° λλ¬Έμ
λλ€.
μκ°μμ λ³ΈμΈμ μκ° μ μ²μ μ·¨μν μ μμ΅λλ€.
μ·¨μ μ μ± μ μ μ² μνμ λ°λΌ λ€λ₯΄κ² μ μ©ν©λλ€.
PENDING μνλ κ²°μ μ μνμ΄λ―λ‘ μΈμ λ μ·¨μν μ μμ΅λλ€.
PENDING -> CANCELLED
μ¬μ©μκ° μ§μ μ·¨μνμ§ μλλΌλ κ²°μ λκΈ° μκ°μ΄ 10λΆμ μ΄κ³Όνλ©΄ μ€μΌμ€λ¬μ μν΄ μλ μ·¨μλ©λλ€.
CONFIRMED μνλ κ²°μ μλ£ μνμ΄λ―λ‘ κ²°μ νμ ν 7μΌ μ΄λ΄μλ§ μ·¨μν μ μμ΅λλ€.
CONFIRMED -> CANCELLED
μ·¨μ κ°λ₯ κΈ°κ°μ λ€μ κΈ°μ€μΌλ‘ νλ¨ν©λλ€.
νμ¬ μκ° <= confirmedAt + 7μΌ
κ²°μ νμ ν 7μΌμ΄ μ§λ μ μ²μ μ·¨μν μ μμ΅λλ€.
μ΄λ―Έ CANCELLED μνμΈ μ μ²μ λ€μ μ·¨μν μ μμ΅λλ€.
λμΌν μκ°μμ λμΌν κ°μμ λν΄ λμμ μ¬λ¬ κ°μ νμ± μ μ²μ κ°μ§ μ μμ΅λλ€.
νμ± μ μ²μ λ€μ μνλ₯Ό μλ―Έν©λλ€.
PENDING
CONFIRMED
λ°λΌμ λμΌ μκ°μμ΄ κ°μ κ°μμ λν΄ PENDING λλ CONFIRMED μνμ μ μ²μ μ΄λ―Έ κ°μ§κ³ μλ€λ©΄ μ€λ³΅ μ μ²μ λΆκ°λ₯ν©λλ€.
λ€λ§ κΈ°μ‘΄ μ μ²μ΄ CANCELLED μνλΌλ©΄ μ’μμ μ μ νμ§ μμΌλ―λ‘, μ μμ΄ λ¨μ μλ κ²½μ° λ€μ μ μ²ν μ μμ΅λλ€.
ꡬν λ¨μνλ₯Ό μν΄ λμΌ κ°μμ λμΌ μκ°μ μ‘°ν©μ λν΄μλ νλμ Enrollment rowλ₯Ό μ μ§νκ³ , κΈ°μ‘΄ μνκ° CANCELLEDμΈ κ²½μ° ν΄λΉ rowλ₯Ό λ€μ PENDING μνλ‘ λ³κ²½νλ λ°©μμΌλ‘ μ¬μ μ²μ μ²λ¦¬ν©λλ€.
CANCELLED -> PENDING
μ΄ μ μ΄λ μΌλ°μ μΈ μν λ³΅κ΅¬κ° μλλΌ, μ¬μ©μμ λͺ μμ μΈ μ¬μ μ² μμ²μ μν΄μλ§ νμ©λ©λλ€.
ν¬λ¦¬μμ΄ν°λ λ³ΈμΈμ΄ κ°μ€ν κ°μμ μκ°μ λͺ©λ‘μ μ‘°νν μ μμ΅λλ€.
λ¨, λ€λ₯Έ ν¬λ¦¬μμ΄ν°κ° κ°μ€ν κ°μμ μκ°μ λͺ©λ‘μ μ‘°νν μ μμ΅λλ€.
κ²μ¦ κ·μΉμ λ€μκ³Ό κ°μ΅λλ€.
classRoom.creatorId == μμ² creatorId
μΌμΉνμ§ μλ κ²½μ° FORBIDDEN μμΈλ₯Ό λ°νν©λλ€.
μκ°μ λͺ©λ‘ μ‘°ν λμμ κΈ°λ³Έμ μΌλ‘ PENDING, CONFIRMED, CANCELLED μνλ₯Ό λͺ¨λ ν¬ν¨ν μ μμ΅λλ€.
λ€λ§ μ€μ μκ° νμ μλ§ λ³΄κ³ μΆμ κ²½μ°λ₯Ό μν΄ status νν°λ₯Ό μ 곡ν©λλ€.
μμ:
GET /api/class-rooms/{classRoomId}/enrollments?creatorId=1&status=CONFIRMED&page=0&size=20
μκ°μμ λ³ΈμΈμ μκ° μ μ² λͺ©λ‘μ μ‘°νν μ μμ΅λλ€.
GET /api/enrollments/me?classmateId=1&page=0&size=20
μ‘°ν κ²°κ³Όμλ λ€μ μνμ μ μ²μ΄ ν¬ν¨λ μ μμ΅λλ€.
PENDINGCONFIRMEDCANCELLED
μλ΅μλ μ μ²ν κ°μ μ 보μ μ μ² μνλ₯Ό ν¨κ» μ 곡ν©λλ€.
λͺ¨λ λͺ©λ‘ μ‘°ν APIλ νμ΄μ§ λ²νΈ κΈ°λ° νμ΄μ§λ€μ΄μ μ μ¬μ©ν©λλ€.
μ μ© λμμ λ€μκ³Ό κ°μ΅λλ€.
- κ°μ λͺ©λ‘ μ‘°ν
- λ΄ μκ° μ μ² λͺ©λ‘ μ‘°ν
- κ°μλ³ μκ°μ λͺ©λ‘ μ‘°ν
μμ² νλΌλ―Έν°λ λ€μ νμμ μ¬μ©ν©λλ€.
page=0&size=20
κΈ°λ³Έκ°μ λ€μκ³Ό κ°μ΅λλ€.
page = 0
size = 20
μ΅λ sizeλ κ³Όλν μ‘°νλ₯Ό λ°©μ§νκΈ° μν΄ μ νν μ μμ΅λλ€.
max size = 50
Javaμμ Classλ μ΄λ―Έ μ‘΄μ¬νλ νμ
μ΄λ¦μ΄λ©° λλ©μΈ ν΄λμ€λͺ
μΌλ‘ μ¬μ©νκΈ° λΆμ μ ν©λλ€.
λ°λΌμ λ³Έ νλ‘μ νΈμμλ κ°μλ₯Ό μλ―Ένλ λλ©μΈλͺ
μΌλ‘ ClassRoomμ μ¬μ©νμ΅λλ€.
DRAFT μνλ ν¬λ¦¬μμ΄ν°κ° μμ§ κ³΅κ°νμ§ μμ μ΄μμ
λλ€.
λ°λΌμ μκ°μμ DRAFT μνμ κ°μλ₯Ό λͺ©λ‘ μ‘°ννκ±°λ μμΈ μ‘°νν μ μμ΅λλ€.
ν¬λ¦¬μμ΄ν°λ λ³ΈμΈμ΄ κ°μ€ν κ°μλ₯Ό κ΄λ¦¬ν΄μΌ νλ―λ‘ DRAFT, OPEN, CLOSED μνλ₯Ό λͺ¨λ μ‘°νν μ μμ΅λλ€.
λ³Έ νλ‘μ νΈμμλ PENDING μνλ₯Ό λ¨μ μμ μ μ²μ΄ μλλΌ μ’μμ μ μ ν κ²°μ λκΈ° μνλ‘ ν΄μνμ΅λλ€.
μ΄λ κ² μ€κ³ν μ΄μ λ λ€μκ³Ό κ°μ΅λλ€.
- μ¬μ©μκ° μκ° μ μ²μ μ±κ³΅νλ€λ©΄ μ’μμ ν보λμλ€κ³ 보λ κ²μ΄ μμ°μ€λ½μ΅λλ€.
PENDINGμ μ μμ ν¬ν¨νμ§ μμΌλ©΄ μ μλ³΄λ€ λ§μ μ¬μ©μκ° κ²°μ λκΈ° μνκ° λ μ μμ΅λλ€.- κ²°μ νμ μμ μ μ μ μ΄κ³Όλ‘ μ€ν¨νλ νλ¦μ μ¬μ©μ κ²½νμ λΆμμ°μ€λ½μ΅λλ€.
- λμμ± μ μ΄ μ§μ μ μκ° μ μ² μμ μΌλ‘ μ§μ€μν¬ μ μμ΅λλ€.
μκ° μ μ²μ λμμ μ¬λ¬ μ¬μ©μκ° μμ²ν μ μλ κΈ°λ₯μ λλ€.
μλ₯Ό λ€μ΄ μ μμ΄ 10λͺ
μ΄κ³ νμ¬ μ’μ μ μ μκ° 9λͺ
μΈ μν©μμ λ μ¬μ©μκ° λμμ μ μ²νλ©΄, λ¨μν νμ¬ μ μ² μλ₯Ό μ‘°νν λ€ Enrollmentλ₯Ό μ μ₯νλ λ°©μμΌλ‘λ λ μμ²μ΄ λͺ¨λ μ±κ³΅νμ¬ μ μμ μ΄κ³Όν μ μμ΅λλ€.
λ³Έ νλ‘μ νΈμμλ μ΄λ₯Ό λ°©μ§νκΈ° μν΄ class_room ν
μ΄λΈμ enrollment_count 컬λΌμ λκ³ , ν΄λΉ κ°μ νμ¬ μ’μ μ μ μλ‘ κ΄λ¦¬ν©λλ€.
enrollment_count = PENDING μν μ μ² μ + CONFIRMED μν μ μ² μ
PENDINGμ κ²°μ λκΈ° μνμ΄μ§λ§ μ΄λ―Έ μ’μμ ν보ν μνλ‘ ν΄μνλ―λ‘ μ μ κ³μ°μ ν¬ν¨ν©λλ€. CANCELLED μνλ μ’μμ μ μ νμ§ μμΌλ―λ‘ μ μ κ³μ°μμ μ μΈν©λλ€.
μκ° μ μ² μμλ λ€μκ³Ό κ°μ μ‘°κ±΄λΆ UPDATEλ₯Ό μ€νν©λλ€.
UPDATE class_room
SET enrollment_count = enrollment_count + 1
WHERE class_room_id = ?
AND status = 'OPEN'
AND enrollment_count < capacity;
μ΄ UPDATEλ DB row λ¨μλ‘ μμμ μΌλ‘ μ€νλ©λλ€.
λ°λΌμ λμμ μ¬λ¬ μ¬μ©μκ° λ§μ§λ§ μ’μμ μ μ²νλλΌλ enrollment_count < capacity 쑰건μ λ§μ‘±ν μμ²λ§ μ±κ³΅ν©λλ€.
μ²λ¦¬ κ²°κ³Όλ affected row μλ‘ νλ¨ν©λλ€.
PENDING μνκ° μ’μμ μ μ νκΈ° λλ¬Έμ κ²°μ λ₯Ό μλ£νμ§ μμ μ μ²μ΄ κ³μ λ¨μ μμΌλ©΄ λ€λ₯Έ μ¬μ©μκ° μ μ²ν μ μμ΅λλ€.
μ΄λ₯Ό λ°©μ§νκΈ° μν΄ κ²°μ λκΈ° μκ°μ 10λΆμΌλ‘ μ ννκ³ , 1λΆ λ¨μ μ€μΌμ€λ¬κ° λ§λ£λ PENDING μ μ²μ μλμΌλ‘ CANCELLED μνλ‘ λ³κ²½ν©λλ€.
λͺ©λ‘ μ‘°νμ μμΈ μ‘°νλ μν°ν°λ₯Ό μ§μ λ°ννμ§ μκ³ DTO ProjectionμΌλ‘ μ‘°νν©λλ€.
μ΄λ κ² μ€κ³ν μ΄μ λ λ€μκ³Ό κ°μ΅λλ€.
- μλ΅μ νμν νλλ§ μ‘°νν μ μμ΅λλ€.
- λΆνμν μν°ν° λ‘λ©μ μ€μΌ μ μμ΅λλ€.
- Lazy LoadingμΌλ‘ μΈν N+1 λ¬Έμ λ₯Ό λ°©μ§ν μ μμ΅λλ€.
- API μλ΅ κ΅¬μ‘°μ μν°ν° ꡬ쑰λ₯Ό λΆλ¦¬ν μ μμ΅λλ€.
- λ¬Όλ‘ , λ³Έ κ³Όμ μμλ λ¨μν μλ΅ νλ ꡬ쑰λ‘, Lazy LoadingμΌλ‘ μΈν N+1 λ¬Έμ μν©μ λ°μνμ§ μμ΅λλ€.
| κΈ°λ₯ | μ€λͺ |
|---|---|
| κ°μ μμ± | ν¬λ¦¬μμ΄ν°κ° κ°μλ₯Ό μμ±ν©λλ€. μμ± μ μνλ DRAFTμ
λλ€. |
| κ°μ μμ | DRAFT μνμμλ§ κ°μ μ 보λ₯Ό μμ ν μ μμ΅λλ€. |
| κ°μ λͺ¨μ§ μμ | ν¬λ¦¬μμ΄ν°κ° DRAFT μνμ κ°μλ₯Ό OPEN μνλ‘ λ³κ²½ν©λλ€. |
| κ°μ λͺ¨μ§ λ§κ° | ν¬λ¦¬μμ΄ν°κ° OPEN μνμ κ°μλ₯Ό CLOSED μνλ‘ λ³κ²½ν©λλ€. |
| κ°μ λͺ©λ‘ μ‘°ν | μν λ³ μν νν°μ νμ΄μ§λ€μ΄μ μ μ§μν©λλ€. |
| κ°μ μμΈ μ‘°ν | νμ¬ μ μ² μΈμμ ν¬ν¨ν κ°μ μμΈ μ 보λ₯Ό μ‘°νν©λλ€. |
| κΈ°λ₯ | μ€λͺ |
|---|---|
| μκ° μ μ² | OPEN μνμ κ°μμ μ μ²ν©λλ€. μ±κ³΅ μ PENDING μνκ° λ©λλ€. |
| κ²°μ νμ | PENDING μνμ μ μ²μ CONFIRMED μνλ‘ λ³κ²½ν©λλ€. |
| μκ° μ·¨μ | PENDING λλ CONFIRMED μνμ μ μ²μ CANCELLED μνλ‘ λ³κ²½ν©λλ€. |
| μλ μ·¨μ | κ²°μ λκΈ° μκ°μ΄ 10λΆμ μ΄κ³Όν PENDING μ μ²μ μ€μΌμ€λ¬κ° μλ μ·¨μν©λλ€. |
| λ΄ μ μ² λͺ©λ‘ μ‘°ν | μκ°μ λ³ΈμΈμ μ μ² λͺ©λ‘μ νμ΄μ§λ€μ΄μ μΌλ‘ μ‘°νν©λλ€. |
| κΈ°λ₯ | Method | URL | μ€λͺ |
|---|---|---|---|
| κ°μ λ±λ‘ | POST |
/api/class-rooms |
ν¬λ¦¬μμ΄ν°κ° κ°μλ₯Ό μμ±ν©λλ€. μμ±λ κ°μλ DRAFT μνμ
λλ€. |
| κ°μ μμ | PUT |
/api/class-rooms/{classRoomId} |
DRAFT μνμ κ°μλ§ μμ ν μ μμ΅λλ€. |
| κ°μ λͺ¨μ§ μμ | PATCH |
/api/class-rooms/{classRoomId}/open |
DRAFT μνμ κ°μλ₯Ό OPEN μνλ‘ λ³κ²½ν©λλ€. |
| κ°μ λͺ¨μ§ λ§κ° | PATCH |
/api/class-rooms/{classRoomId}/close |
OPEN μνμ κ°μλ₯Ό CLOSED μνλ‘ λ³κ²½ν©λλ€. |
| κ°μ λͺ©λ‘ μ‘°ν | GET |
/api/class-rooms |
μν λ³λ‘ μ‘°ν κ°λ₯ν κ°μ λͺ©λ‘μ νμ΄μ§λ€μ΄μ μΌλ‘ μ‘°νν©λλ€. |
| κ°μ μμΈ μ‘°ν | GET |
/api/class-rooms/{classRoomId} |
κ°μ μμΈ μ 보μ νμ¬ μ μ² μΈμμ μ‘°νν©λλ€. |
| κ°μλ³ μκ°μ λͺ©λ‘ μ‘°ν | GET |
/api/class-rooms/{classRoomId}/enrollments |
ν¬λ¦¬μμ΄ν°κ° λ³ΈμΈ κ°μμ μκ°μ λͺ©λ‘μ μ‘°νν©λλ€. |
| κΈ°λ₯ | Method | URL | μ€λͺ |
|---|---|---|---|
| μκ° μ μ² | POST |
/api/class-rooms/{classRoomId}/enrollments |
μκ°μμ΄ OPEN μνμ κ°μμ μ μ²ν©λλ€. μ±κ³΅ μ PENDING μνκ° λ©λλ€. |
| κ²°μ νμ | PATCH |
/api/enrollments/{enrollmentId}/confirm |
PENDING μνμ μ μ²μ CONFIRMED μνλ‘ λ³κ²½ν©λλ€. |
| μκ° μ·¨μ | PATCH |
/api/enrollments/{enrollmentId}/cancel |
PENDING λλ CONFIRMED μνμ μ μ²μ CANCELLED μνλ‘ λ³κ²½ν©λλ€. |
| λ΄ μκ° μ μ² λͺ©λ‘ μ‘°ν | GET |
/api/enrollments/me |
μκ°μ λ³ΈμΈμ μ μ² λͺ©λ‘μ νμ΄μ§λ€μ΄μ μΌλ‘ μ‘°νν©λλ€. |
| μκ° μ μ² μμΈ μ‘°ν | GET |
/api/enrollments/{enrollmentId} |
μκ°μ λ³ΈμΈμ μ μ² μμΈ μ 보λ₯Ό μ‘°νν©λλ€. |
POST /api/class-rooms
Content-Type: application/json{
"creatorId": 1,
"title": "Spring Boot μ
λ¬Έ",
"description": "Spring Boot κΈ°λ° REST API κ°λ° κ°μ",
"price": 50000,
"capacity": 30,
"startAt": "2026-05-01T00:00:00",
"endAt": "2026-06-01T00:00:00"
}201 Created
Location: /api/class-rooms/1body μμ.
PUT /api/class-rooms/{classRoomId}
Content-Type: application/json{
"creatorId": 1,
"title": "Spring Boot μ€μ μ
λ¬Έ",
"description": "Spring Boot κΈ°λ° REST API μ€μ κ°μ",
"price": 60000,
"capacity": 25,
"startAt": "2026-05-01T00:00:00",
"endAt": "2026-06-01T00:00:00"
}204 No Content
PATCH /api/class-rooms/{classRoomId}/open
Content-Type: application/json{
"creatorId": 1
}204 No Content
PATCH /api/class-rooms/{classRoomId}/close
Content-Type: application/json{
"creatorId": 1
}
204 No Content
ν¬λ¦¬μμ΄ν°λ λ³ΈμΈμ΄ μμ±ν κ°μλ₯Ό DRAFT, OPEN, CLOSED μνλ‘ νν°λ§νμ¬ μ‘°νν μ μμ΅λλ€.
GET /api/class-rooms?role=CREATOR&creatorId=1&status=DRAFT&page=0&size=20{
"content": [
{
"classRoomId": 1,
"creatorId": 1,
"creatorName": "creator1",
"title": "Spring Boot μ
λ¬Έ",
"price": 50000,
"capacity": 30,
"enrollmentCount": 0,
"status": "DRAFT",
"startAt": "2026-05-01T00:00:00",
"endAt": "2026-06-01T00:00:00"
}
],
"page": 0,
"size": 20,
"totalElements": 1,
"totalPages": 1
}μκ°μμ OPEN, CLOSED μνμ κ°μλ§ νν°λ§νμ¬ μ‘°νν μ μμ΅λλ€.
GET /api/class-rooms?role=CLASSMATE&status=OPEN&page=0&size=20{
"content": [
{
"classRoomId": 1,
"creatorId": 1,
"creatorName": "creator1",
"title": "Spring Boot μ
λ¬Έ",
"price": 50000,
"capacity": 30,
"enrollmentCount": 0,
"status": "DRAFT",
"startAt": "2026-05-01T00:00:00",
"endAt": "2026-06-01T00:00:00"
}
],
"page": 0,
"size": 20,
"totalElements": 1,
"totalPages": 1
}GET /api/class-rooms/1?role=CREATOR&creatorId=1λλ μκ°μ μ‘°ν:
GET /api/class-rooms/1?role=CLASSMATE{
"classRoomId": 1,
"creatorId": 1,
"creatorName": "creator_1",
"title": "Spring Boot μ
λ¬Έ",
"description": "Spring Boot κΈ°λ° REST API κ°λ° κ°μ",
"price": 51000,
"capacity": 30,
"enrollmentCount": 0,
"status": "CLOSED",
"startAt": "2026-05-01T00:00:00",
"endAt": "2026-06-01T00:00:00",
"createdAt": "2026-04-28T04:20:13",
"updatedAt": "2026-04-28T04:25:21"
}POST /api/class-rooms/1/enrollments
Content-Type: application/json{
"classmateId": 10
}201 Created
Location: /api/enrollments/1{
"enrollmentId": 2,
"classRoomId": 2,
"classmateId": 9,
"status": "PENDING",
"paymentExpiredAt": "2026-04-28T04:56:32",
"confirmedAt": null,
"cancelledAt": null,
"createdAt": "2026-04-28T04:46:32"
}PATCH /api/enrollments/1/confirm
Content-Type: application/json{
"classmateId": 10
}{
"enrollmentId": 2,
"classRoomId": 2,
"classmateId": 9,
"status": "CONFIRMED",
"paymentExpiredAt": "2026-04-28T04:56:32",
"confirmedAt": "2026-04-28T04:47:28",
"cancelledAt": null,
"createdAt": "2026-04-28T04:46:32"
}PATCH /api/enrollments/1/cancel
Content-Type: application/json{
"classmateId": 10
}{
"enrollmentId": 2,
"classRoomId": 2,
"classmateId": 9,
"status": "CANCELLED",
"paymentExpiredAt": "2026-04-28T04:56:32",
"confirmedAt": "2026-04-28T04:47:28",
"cancelledAt": "2026-04-28T04:48:10",
"createdAt": "2026-04-28T04:46:32"
}GET /api/enrollments/me?classmateId=10&page=0&size=20{
"content": [
{
"enrollmentId": 1,
"classRoomId": 1,
"classRoomTitle": "Spring Boot μ
λ¬Έ",
"price": 50000,
"status": "CONFIRMED",
"createdAt": "2026-04-26T10:00:00",
"confirmedAt": "2026-04-26T10:05:00",
"cancelledAt": null
}
],
"page": 0,
"size": 20,
"totalElements": 1,
"totalPages": 1
}ν¬λ¦¬μμ΄ν°λ λ³ΈμΈμ΄ κ°μ€ν κ°μμ μκ°μ λͺ©λ‘λ§ μ‘°νν μ μμ΅λλ€.
GET /api/class-rooms/2/enrollments?creatorId=1&status=CANCELLED&page=0&size=20{
"content": [
{
"enrollmentId": 2,
"classmateId": 9,
"classmateName": "classmate_9",
"status": "CANCELLED",
"paymentExpiredAt": "2026-04-28T04:56:32",
"confirmedAt": "2026-04-28T04:47:28",
"cancelledAt": "2026-04-28T04:48:10",
"createdAt": "2026-04-28T04:46:32"
},
{
"enrollmentId": 1,
"classmateId": 10,
"classmateName": "classmate_10",
"status": "CANCELLED",
"paymentExpiredAt": "2026-04-28T04:48:06",
"confirmedAt": null,
"cancelledAt": "2026-04-28T04:49:00",
"createdAt": "2026-04-28T04:38:06"
}
],
"page": 0,
"size": 20,
"totalElements": 2,
"totalPages": 1
}DB μ€ν€λ§μ DDLμ λ³Έ νλ‘μ νΈμ resources/db/migration/ *.sql νμΌμ μ 곡νμ¬ νμΈν μ μμ΅λλ€.
ERD cloud URL
λ€μ URLμ ν΅ν΄ ERDλ₯Ό νμΈν μ μμ΅λλ€.
https://www.erdcloud.com/d/39WesCmjRoB5w2Gpf
λ³Έ νλ‘μ νΈλ μκ° μ μ² μμ€ν μ ν΅μ¬ λλ©μΈμ λ€μ 4κ° μν°ν°λ‘ ꡬμ±ν©λλ€.
Creator: κ°μλ₯Ό κ°μ€νλ ν¬λ¦¬μμ΄ν°Classmate: κ°μμ μκ° μ μ²νλ ν΄λμ€λ©μ΄νΈClassRoom: ν¬λ¦¬μμ΄ν°κ° κ°μ€ν κ°μEnrollment: ν΄λμ€λ©μ΄νΈμ μκ° μ μ² λ΄μ
Creatorμ Classmateλ λ³Έ κ³Όμ μμ νμκ°μ
/λ‘κ·ΈμΈ κΈ°λ₯μ λμμ΄ μλλ©°, class_room, enrollmentμ FK μ°Έμ‘°μ ν
μ€νΈ λ°μ΄ν° ꡬμ±μ μν΄ μ¬μ©ν©λλ€.
ν¬λ¦¬μμ΄ν° μ 보λ₯Ό μ μ₯νλ μν°ν°μ λλ€.
λ³Έ κ³Όμ μμλ ν¬λ¦¬μμ΄ν° CRUD APIλ₯Ό λ³λλ‘ μ 곡νμ§ μκ³ , Flyway seed λ°μ΄ν°λ₯Ό ν΅ν΄ μ΄κΈ° λ°μ΄ν°λ₯Ό μ 곡ν©λλ€.
| νλ | νμ | μ€λͺ |
|---|---|---|
id |
Long |
ν¬λ¦¬μμ΄ν° ID |
name |
String |
ν¬λ¦¬μμ΄ν° μ΄λ¦ |
email |
String |
ν¬λ¦¬μμ΄ν° μ΄λ©μΌ |
status |
CreatorStatus |
ν¬λ¦¬μμ΄ν° μν |
createdAt |
LocalDateTime |
μμ± μκ° |
updatedAt |
LocalDateTime |
μμ μκ° |
ν¬λ¦¬μμ΄ν° μνλ λ€μ enumμΌλ‘ κ΄λ¦¬ν©λλ€.
public enum CreatorStatus {
ACTIVE,
DELETED
}ν΄λμ€λ©μ΄νΈ μ 보λ₯Ό μ μ₯νλ μν°ν°μ λλ€.
λ³Έ κ³Όμ μμλ ν΄λμ€λ©μ΄νΈ CRUD APIλ₯Ό λ³λλ‘ μ 곡νμ§ μκ³ , Flyway seed λ°μ΄ν°λ₯Ό ν΅ν΄ μ΄κΈ° λ°μ΄ν°λ₯Ό μ 곡ν©λλ€.
| νλ | νμ | μ€λͺ |
|---|---|---|
id |
Long |
ν΄λμ€λ©μ΄νΈ ID |
name |
String |
ν΄λμ€λ©μ΄νΈ μ΄λ¦ |
email |
String |
ν΄λμ€λ©μ΄νΈ μ΄λ©μΌ |
status |
ClassmateStatus |
ν΄λμ€λ©μ΄νΈ μν |
createdAt |
LocalDateTime |
μμ± μκ° |
updatedAt |
LocalDateTime |
μμ μκ° |
ν΄λμ€λ©μ΄νΈ μνλ λ€μ enumμΌλ‘ κ΄λ¦¬ν©λλ€.
public enum ClassmateStatus {
ACTIVE,
DELETED
}κ°μ μ 보λ₯Ό μ μ₯νλ μν°ν°μ λλ€.
ν¬λ¦¬μμ΄ν°κ° κ°μλ₯Ό μμ±νλ©΄ μ΅μ΄ μνλ DRAFTκ° λ©λλ€. μ΄ν ν¬λ¦¬μμ΄ν°κ° λͺ¨μ§ μμ APIλ₯Ό νΈμΆνλ©΄ OPEN μνκ° λλ©°, OPEN μνμ κ°μμλ§ μκ° μ μ²ν μ μμ΅λλ€.
| νλ | νμ | μ€λͺ |
|---|---|---|
id |
Long |
κ°μ ID |
creator |
Creator |
κ°μλ₯Ό μμ±ν ν¬λ¦¬μμ΄ν° |
title |
String |
κ°μ μ λͺ© |
description |
String |
κ°μ μ€λͺ |
price |
Long |
κ°μ κ°κ²© |
capacity |
int |
μ΅λ μκ° μ μ |
enrollmentCount |
int |
νμ¬ μ’μ μ μ μ |
status |
ClassRoomStatus |
κ°μ μν |
startAt |
LocalDateTime |
μκ° μμ μΌμ |
endAt |
LocalDateTime |
μκ° μ’ λ£ μΌμ |
createdAt |
LocalDateTime |
μμ± μκ° |
updatedAt |
LocalDateTime |
μμ μκ° |
κ°μ μνλ λ€μ enumμΌλ‘ κ΄λ¦¬ν©λλ€.
public enum ClassRoomStatus {
DRAFT,
OPEN,
CLOSED
}μκ° μ μ² μ 보λ₯Ό μ μ₯νλ μν°ν°μ λλ€.
ν΄λμ€λ©μ΄νΈκ° κ°μμ μκ° μ μ²νλ©΄ PENDING μνμ Enrollmentκ° μμ±λ©λλ€. PENDING μνλ κ²°μ λκΈ° μνμ΄μ§λ§ μ’μμ μ μ ν©λλ€. κ²°μ νμ μ CONFIRMED μνκ° λκ³ , μκ° μ·¨μ λλ κ²°μ λκΈ° λ§λ£ μ CANCELLED μνκ° λ©λλ€.
| νλ | νμ | μ€λͺ |
|---|---|---|
id |
Long |
μκ° μ μ² ID |
classRoom |
ClassRoom |
μ μ²ν κ°μ |
classmate |
Classmate |
μ μ²ν ν΄λμ€λ©μ΄νΈ |
status |
EnrollmentStatus |
μ μ² μν |
paymentExpiredAt |
LocalDateTime |
κ²°μ λκΈ° λ§λ£ μκ° |
confirmedAt |
LocalDateTime |
κ²°μ νμ μκ° |
cancelledAt |
LocalDateTime |
μ·¨μ μκ° |
cancelReason |
CancelReason |
μ·¨μ μ¬μ |
createdAt |
LocalDateTime |
μμ± μκ° |
updatedAt |
LocalDateTime |
μμ μκ° |
μ μ² μνλ λ€μ enumμΌλ‘ κ΄λ¦¬ν©λλ€.
public enum EnrollmentStatus {
PENDING,
CONFIRMED,
CANCELLED
}μ·¨μ μ¬μ λ λ€μ enumμΌλ‘ κ΄λ¦¬ν©λλ€.
public enum CancelReason {
USER_CANCELLED,
PAYMENT_EXPIRED
}λμΌν ν΄λμ€λ©μ΄νΈλ λμΌν κ°μμ λν΄ μ¬λ¬ κ°μ νμ± μ μ²μ κ°μ§ μ μμ΅λλ€.
νμ± μ μ²μ λ€μ μνλ₯Ό μλ―Έν©λλ€.
PENDING
CONFIRMED
DBμμλ λ€μ unique μ μ½μΌλ‘ λμΌ κ°μμ λμΌ ν΄λμ€λ©μ΄νΈ μ‘°ν©μ νλμ rowλ‘ μ νν©λλ€.
UNIQUE (class_room_id, classmate_id)
μ΄ μ μ½μ λ°λΌ λμΌ ν΄λμ€λ©μ΄νΈκ° κ°μ κ°μμ λ€μ μ μ²νλ κ²½μ°μλ μ rowλ₯Ό μμ±νμ§ μκ³ , κΈ°μ‘΄ CANCELLED μνμ rowλ₯Ό PENDING μνλ‘ μ¬νμ±νν©λλ€.
ClassRoom.capacityλ μ΅λ μκ° μ μμ μλ―Έν©λλ€.
ClassRoom.enrollmentCountλ νμ¬ μ’μ μ μ μλ₯Ό μλ―Ένλ©°, λ€μ 쑰건μ λ§μ‘±ν΄μΌ ν©λλ€.
0 <= enrollmentCount <= capacity
μκ° μ μ² μ enrollmentCount < capacity 쑰건μ ν¬ν¨ν UPDATEλ₯Ό μ¬μ©νμ¬ μ μμ μ΄κ³Όνμ§ μλλ‘ λ³΄μ₯ν©λλ€.
PENDING μνλ μ’μμ μ μ νλ―λ‘, κ²°μ κ° μ₯μκ° μλ£λμ§ μμΌλ©΄ λ€λ₯Έ μ¬μ©μκ° μκ° μ μ²ν μ μλ λ¬Έμ κ° λ°μν μ μμ΅λλ€.
μ΄λ₯Ό λ°©μ§νκΈ° μν΄ μκ° μ μ² μμ± μ κ²°μ λ§λ£ μκ°μ μ μ₯ν©λλ€.
paymentExpiredAt = μ μ² μκ° + 10λΆ
1λΆ λ¨μ μ€μΌμ€λ¬κ° λ€μ 쑰건μ μ μ²μ μλ μ·¨μν©λλ€.
status = PENDING
paymentExpiredAt <= now
μλ μ·¨μλ μ μ²μ CANCELLED μνκ° λλ©°, cancelReasonμ PAYMENT_EXPIREDλ‘ μ μ₯λ©λλ€.
PENDING μνμ μ μ²μ κ²°μ μ μνμ΄λ―λ‘ μΈμ λ μ§μ μ·¨μν μ μμ΅λλ€.
CONFIRMED μνμ μ μ²μ κ²°μ νμ ν 7μΌ μ΄λ΄μλ§ μ·¨μν μ μμ΅λλ€.
νμ¬ μκ° <= confirmedAt + 7μΌ
μ·¨μ κ°λ₯ κΈ°κ°μ΄ μ§λ CONFIRMED μ μ²μ μ·¨μν μ μμ΅λλ€.
Creator 1 : N ClassRoom
ClassRoom 1 : N Enrollment
Classmate 1 : N Enrollment
κ΄κ³ μ€λͺ μ λ€μκ³Ό κ°μ΅λλ€.
- νλμ
Creatorλ μ¬λ¬ κ°μClassRoomμ μμ±ν μ μμ΅λλ€. - νλμ
ClassRoomμλ μ¬λ¬ κ°μEnrollmentκ° μμ±λ μ μμ΅λλ€. - νλμ
Classmateλ μ¬λ¬ κ°μμ λν΄Enrollmentλ₯Ό κ°μ§ μ μμ΅λλ€. - λ¨, λμΌν
Classmateλ λμΌνClassRoomμ λν΄ νλμEnrollmentrowλ§ κ°μ§ μ μμ΅λλ€.
μ‘°ν μ±λ₯κ³Ό μ€μΌμ€λ¬ μ²λ¦¬λ₯Ό μν΄ λ€μ μΈλ±μ€λ₯Ό μ¬μ©ν©λλ€.
| μΈλ±μ€ | λͺ©μ |
|---|---|
uk_creator_email |
ν¬λ¦¬μμ΄ν° μ΄λ©μΌ μ€λ³΅ λ°©μ§ |
uk_classmate_email |
ν΄λμ€λ©μ΄νΈ μ΄λ©μΌ μ€λ³΅ λ°©μ§ |
idx_class_room_creator_status_created_at |
ν¬λ¦¬μμ΄ν°μ κ°μ λͺ©λ‘ μ‘°ν |
idx_class_room_status_created_at |
μκ°μμ κ³΅κ° κ°μ λͺ©λ‘ μ‘°ν |
uk_enrollment_class_room_classmate |
λμΌ κ°μ μ€λ³΅ μ μ² λ°©μ§ |
idx_enrollment_classmate_created_at |
λ΄ μκ° μ μ² λͺ©λ‘ μ‘°ν |
idx_enrollment_class_room_status_created_at |
κ°μλ³ μκ°μ λͺ©λ‘ μ‘°ν |
idx_enrollment_status_payment_expired_at |
κ²°μ λκΈ° λ§λ£ μ€μΌμ€λ¬ μ‘°ν |
λ³Έ νλ‘μ νΈλ ν΅μ¬ λΉμ¦λμ€ κ·μΉκ³Ό λ°μ΄ν° μ ν©μ±μ κ²μ¦νκΈ° μν΄ κ³μΈ΅λ³ ν μ€νΈλ₯Ό ꡬμ±νμ΅λλ€.
ν μ€νΈλ λ¨μ μ±κ³΅ μΌμ΄μ€λΏ μλλΌ μν μ μ΄ μ€ν¨, κΆν κ²μ¦, Validation μ€ν¨, Repository Query κ²μ¦, MySQL κΈ°λ° λμμ± μ μ΄κΉμ§ ν¬ν¨ν©λλ€.
μ 체 ν μ€νΈλ λ€μ λͺ λ Ήμ΄λ‘ μ€νν μ μμ΅λλ€.
./gradlew clean testWindows νκ²½μμλ λ€μ λͺ λ Ήμ΄λ₯Ό μ¬μ©ν μ μμ΅λλ€.
gradlew.bat clean testνΉμ ν μ€νΈλ§ μ€νν μλ μμ΅λλ€.
./gradlew test --tests "*ControllerTest"
./gradlew test --tests "*RepositoryTest"
./gradlew test --tests "*IntegrationTest"
./gradlew test --tests "*ConcurrencyTest"Windows νκ²½μμλ λ€μκ³Ό κ°μ΄ μ€νν©λλ€.
gradlew.bat test --tests "*ControllerTest"
gradlew.bat test --tests "*RepositoryTest"
gradlew.bat test --tests "*IntegrationTest"
gradlew.bat test --tests "*ConcurrencyTest"ν μ€νΈλ μ€μ μ΄μ DBμ μ μ¬ν νκ²½μμ κ²μ¦νκΈ° μν΄ H2κ° μλ MySQL 8.4 Testcontainersλ₯Ό μ¬μ©ν©λλ€.
Repository ν μ€νΈμ Service ν΅ν© ν μ€νΈλ κ°κ° λ 립λ MySQL Testcontainerλ₯Ό μ¬μ©ν©λλ€.
| ν μ€νΈ κ΅¬λΆ | ν μ€νΈ νκ²½ |
|---|---|
| Controller Test | @WebMvcTest, MockMvc, Mock Service |
| Entity Test | Spring Context μμ΄ JUnit 5, AssertJ, Mockito |
| Repository Test | @DataJpaTest, MySQL Testcontainers |
| Service Integration Test | @SpringBootTest, MySQL Testcontainers |
| Concurrency Test | @SpringBootTest, MySQL Testcontainers, ExecutorService, CountDownLatch |
Repository ν
μ€νΈμμλ @AutoConfigureTestDatabase(replace = NONE) μ€μ μ μ¬μ©νμ¬ Spring Bootκ° ν
μ€νΈ DBλ₯Ό H2λ‘ λ체νμ§ μλλ‘ νμ΅λλ€.
λν JPA Auditing νλμΈ createdAt, updatedAtμ΄ Repository slice testμμλ μ μ λμνλλ‘ JpaAuditingConfigλ₯Ό ν
μ€νΈ 컨ν
μ€νΈμ λͺ
μμ μΌλ‘ importνμ΅λλ€.
Testcontainersλ ν μ€νΈ ν΄λμ€ λ¬Άμ μ€ν μ 컨ν μ΄λ μλͺ μ£ΌκΈ°μ Spring TestContext μΊμκ° μΆ©λνμ§ μλλ‘ singleton container λ°©μμΌλ‘ ꡬμ±νμ΅λλ€.
ν μ€νΈ κ° λ°μ΄ν° κ°μμ λ°©μ§νκΈ° μν΄ κ° ν μ€νΈ μ€ν μ λ€μ ν μ΄λΈμ μ΄κΈ°νν©λλ€.
enrollment
class_room
creator, classmate ν
μ΄λΈμ Flyway seed λ°μ΄ν°λ₯Ό μ¬μ©νλ―λ‘ ν
μ€νΈ μ΄κΈ°ν λμμμ μ μΈν©λλ€.
ν μ€νΈ μ λ΅μ λ€μ κΈ°μ€μΌλ‘ λλμμ΅λλ€.
| κ³μΈ΅ | λͺ©μ |
|---|---|
| Entity Test | λλ©μΈ μν μ μ΄μ λΉμ¦λμ€ κ·μΉμ λΉ λ₯΄κ² κ²μ¦ |
| Controller Test | HTTP μμ²/μλ΅, Validation, μμΈ μλ΅ κ΅¬μ‘° κ²μ¦ |
| Repository Test | JPQL, Native Query, DTO Projection, μ‘°κ±΄λΆ UPDATE κ²μ¦ |
| Service Integration Test | μ€μ DB κΈ°λ° μ 체 λΉμ¦λμ€ νλ¦ κ²μ¦ |
| Concurrency Test | μ μ μ΄κ³Ό λ°©μ§μ μ€λ³΅ μ μ² λ°©μ§μ λμμ± κ²μ¦ |
Controller ν
μ€νΈλ @WebMvcTestμ MockMvcλ₯Ό μ¬μ©ν©λλ€.
Service κ³μΈ΅μ Mock κ°μ²΄λ‘ λ체νκ³ , Controller κ³μΈ΅μ μ± μλ§ κ²μ¦ν©λλ€.
μ£Όμ κ²μ¦ νλͺ©μ λ€μκ³Ό κ°μ΅λλ€.
- μ μ μμ² μ HTTP status code κ²μ¦
- μμ± APIμ
Locationμλ΅ ν€λ κ²μ¦ - Bean Validation μ€ν¨ μ 400 μλ΅ κ²μ¦
- JSON νμ μ€λ₯ λ°μ μ 400 νμ μ€λ₯ μλ΅ κ²μ¦
- Service κ³μΈ΅ μμΈ λ°μ μ μ μ μμΈ μλ΅ κ΅¬μ‘° κ²μ¦
- νμ΄μ§λ€μ΄μ μλ΅μμ νμν νλλ§ λ ΈμΆλλμ§ κ²μ¦
κ°μ λͺ©λ‘ μ‘°ν APIλ Spring Data Pageλ₯Ό κ·Έλλ‘ μλ΅νμ§ μκ³ PageResponseλ‘ λ³νν©λλ€.
λ°λΌμ ν μ€νΈμμλ λ€μ νλλ§ μλ΅λλμ§ κ²μ¦ν©λλ€.
{
"content": [],
"page": 0,
"size": 10,
"totalElements": 1,
"totalPages": 1
}λν PageImpl κΈ°λ³Έ μ§λ ¬ν νλκ° μΈλΆ API μλ΅μ λ
ΈμΆλμ§ μλμ§ κ²μ¦ν©λλ€.
pageable
sort
first
last
number
numberOfElements
empty
Entity ν μ€νΈλ Spring Context μμ΄ JUnit 5, AssertJ, Mockitoλ§ μ¬μ©ν©λλ€.
μ΄λ₯Ό ν΅ν΄ λλ©μΈ κ°μ²΄μ μν μ μ΄ κ·μΉμ λΉ λ₯΄κ² κ²μ¦ν©λλ€.
κ²μ¦ νλͺ©μ λ€μκ³Ό κ°μ΅λλ€.
- κ°μ μμ± μ μ΅μ΄ μνλ
DRAFT - κ°μ μμ± μ μ μ² μΈμμ 0λͺ
DRAFTμνμ κ°μλOPENμνλ‘ λ³κ²½ κ°λ₯DRAFTκ° μλ κ°μλ λͺ¨μ§ μμ λΆκ°OPENμνμ κ°μλCLOSEDμνλ‘ λ³κ²½ κ°λ₯OPENμ΄ μλ κ°μλ λͺ¨μ§ λ§κ° λΆκ°DRAFTμνμ κ°μλ§ μμ κ°λ₯- κ°μ μμ μκ°μ΄ μ’ λ£ μκ°λ³΄λ€ μ΄μ μ΄ μλλ©΄ μμ λΆκ°
- μμ²ν
creatorIdκ° κ°μ μμ μμΈμ§ κ²μ¦
κ²μ¦ νλͺ©μ λ€μκ³Ό κ°μ΅λλ€.
- μκ° μ μ² μμ± μ
PENDINGμν - μκ° μ μ² μμ± μ κ²°μ λ§λ£ μκ°μ μ μ² μκ° κΈ°μ€ 10λΆ λ€
- κ²°μ λ§λ£ μ
PENDINGμ μ²μCONFIRMEDμνλ‘ νμ κ°λ₯ PENDINGμ΄ μλ μ μ²μ κ²°μ νμ λΆκ°- κ²°μ κ°λ₯ μκ°μ΄ λ§λ£λ μ μ²μ κ²°μ νμ λΆκ°
PENDINGμ μ²μ μ¬μ©μ μ·¨μ κ°λ₯CONFIRMEDμ μ²μ νμ ν 7μΌ μ΄λ΄ μ·¨μ κ°λ₯- νμ ν 7μΌμ΄ μ§λλ©΄ μ·¨μ λΆκ°
- μ΄λ―Έ μ·¨μλ μ μ²μ λ€μ μ·¨μ λΆκ°
- κ²°μ λ§λ£ μκ°μ΄ μ§λ
PENDINGμ μ²μ μλ μ·¨μ κ°λ₯ - κ²°μ λ§λ£ μκ°μ΄ μ§λμ§ μμ μ μ²μ μλ μ·¨μ λΆκ°
CANCELLEDμνμ μ μ²μ μ¬μ μ² μPENDINGμνλ‘ λ³΅κ΅¬ κ°λ₯CANCELLEDμνκ° μλλ©΄ μ¬μ μ² λΆκ°- μμ²ν
classmateIdκ° μ μ² μμ μμΈμ§ κ²μ¦
Repository ν
μ€νΈλ @DataJpaTestμ MySQL Testcontainersλ₯Ό μ¬μ©ν©λλ€.
H2κ° μλ MySQLμμ ν μ€νΈνλ μ΄μ λ λ€μκ³Ό κ°μ΅λλ€.
- MySQL SQL λ¬Έλ²κ³Ό μ€μ μ€ν νκ²½μ κ°κΉμ΄ 쑰건μμ κ²μ¦
SELECT ... FOR UPDATEκΈ°λ° μ κΈ μΏΌλ¦¬ κ²μ¦- Native Query κΈ°λ° μ‘°κ±΄λΆ UPDATE κ²μ¦
- Flyway migrationκ³Ό seed data μ μ© κ²μ¦
- μ€μ DB μ μ½μ‘°κ±΄κ³Ό μΈλ±μ€ λμ κ²μ¦
κ²μ¦ νλͺ©μ λ€μκ³Ό κ°μ΅λλ€.
- ν¬λ¦¬μμ΄ν°μ κ°μ λͺ©λ‘ DTO Projection μ‘°ν
- μκ°μμ κ³΅κ° κ°μ λͺ©λ‘ DTO Projection μ‘°ν
- κ°μ μμΈ DTO Projection μ‘°ν
- κ°μλ³ μκ° μ μ² λͺ©λ‘ DTO Projection μ‘°ν
OPENμνμ΄κ³ μ μμ΄ λ¨μ μμΌλ©΄enrollment_countμ¦κ° μ±κ³΅- μ μμ΄ κ°λ μ°¨λ©΄
enrollment_countμ¦κ° μ€ν¨
κ²μ¦ νλͺ©μ λ€μκ³Ό κ°μ΅λλ€.
- κ°μ IDμ ν΄λμ€λ©μ΄νΈ IDλ‘ μκ° μ μ² μ‘°ν
- λμΌ κ°μ/λμΌ ν΄λμ€λ©μ΄νΈ μ μ² μ‘°ν μ
FOR UPDATEμ κΈ μ¬μ© - λ΄ μκ° μ μ² λͺ©λ‘ DTO Projection μ‘°ν
- κ²°μ λ§λ£λ
PENDINGμ μ² λͺ©λ‘ μ‘°ν - μκ° μ μ² μμΈ DTO Projection μ‘°ν
Service ν΅ν© ν
μ€νΈλ @SpringBootTestμ MySQL Testcontainersλ₯Ό μ¬μ©ν©λλ€.
Controllerλ₯Ό κ±°μΉμ§ μκ³ Service κ³μΈ΅μ μ§μ νΈμΆνμ¬ μ€μ DBμ νΈλμμ κΈ°λ° λΉμ¦λμ€ νλ¦μ κ²μ¦ν©λλ€.
κ²μ¦ νλͺ©μ λ€μκ³Ό κ°μ΅λλ€.
- κ°μ μμ± μ
DRAFTμνλ‘ μ μ₯ - μ‘΄μ¬νμ§ μλ ν¬λ¦¬μμ΄ν°λ‘ κ°μ μμ± μ 404 μμΈ
- κ°μ μμ μλ
DRAFTμνμ κ°μ μμ κ°λ₯ - κ°μ μμ μκ° μλλ©΄ μμ λΆκ°
- κ°μ μμ μλ
DRAFTκ°μλ₯ΌOPENμνλ‘ λ³κ²½ κ°λ₯ OPENκ°μλCLOSEDμνλ‘ λ³κ²½ κ°λ₯- ν¬λ¦¬μμ΄ν°λ λ³ΈμΈ κ°μμ
DRAFTμμΈ μ‘°ν κ°λ₯ - μκ°μμ
DRAFTκ°μ μμΈ μ‘°ν λΆκ° - μκ°μ κ°μ λͺ©λ‘ μ‘°νμμ
DRAFTμν νν°λ νμ©λμ§ μμ - ν¬λ¦¬μμ΄ν° κ°μ λͺ©λ‘ μ‘°νμμ μ‘΄μ¬νμ§ μλ
creatorIdλ 404 μμΈ
κ²μ¦ νλͺ©μ λ€μκ³Ό κ°μ΅λλ€.
OPENκ°μμ μκ° μ μ²νλ©΄PENDINGμ μ² μμ±- μκ° μ μ² μ±κ³΅ μ
enrollment_countμ¦κ° - μ‘΄μ¬νμ§ μλ ν΄λμ€λ©μ΄νΈλ‘ μ μ² μ 404 μμΈ
- μ‘΄μ¬νμ§ μλ κ°μμ μ μ² μ 404 μμΈ
OPENμνκ° μλ κ°μμλ μ μ² λΆκ°- μ μμ΄ κ°λ μ°¬ κ°μμλ μ μ² λΆκ°
- μ΄λ―Έ μ’μμ μ μ μ€μΈ μ μ²μ΄ μμΌλ©΄ μ€λ³΅ μ μ² λΆκ°
CANCELLEDμ μ²μ΄ μμΌλ©΄ κ°μ rowλ₯Ό μ¬μ¬μ©νμ¬ μ¬μ μ²- λ³ΈμΈ μ μ²μ΄κ³ κ²°μ κ°λ₯ μκ°μ΄ μ§λμ§ μμμΌλ©΄ κ²°μ νμ κ°λ₯
- λ€λ₯Έ ν΄λμ€λ©μ΄νΈμ μ μ²μ κ²°μ νμ λΆκ°
- μ μ² μ·¨μ μ
CANCELLEDμνλ‘ λ³κ²½λκ³enrollment_countκ°μ - λ΄ μκ° μ μ² λͺ©λ‘ μ‘°ν μ μ‘΄μ¬νμ§ μλ ν΄λμ€λ©μ΄νΈλ 404 μμΈ
- μκ° μ μ² μμΈ μ‘°ν μ μμ μκ° μλλ©΄ 403 μμΈ
- κ²°μ λ§λ£λ
PENDINGμ μ²μ μλ μ·¨μλκ³enrollment_countκ°μ
λμμ± ν
μ€νΈλ μ€μ MySQL Testcontainers νκ²½μμ ExecutorServiceμ CountDownLatchλ₯Ό μ¬μ©νμ¬ μ¬λ¬ μμ²μ λμμ λ°μμν΅λλ€.
μκ° μ μ²μ λ€μ 쿼리λ₯Ό ν΅ν΄ μ μ μ΄κ³Όλ₯Ό λ°©μ§ν©λλ€.
UPDATE class_room
SET enrollment_count = enrollment_count + 1
WHERE class_room_id = ?
AND status = 'OPEN'
AND enrollment_count < capacity;
λμμ± ν μ€νΈμμλ λ€μ μλ리μ€λ₯Ό κ²μ¦ν©λλ€.
μ μ 5λͺ μΈ κ°μμ 10λͺ μ ν΄λμ€λ©μ΄νΈκ° λμμ μ μ²νλλΌλ μ±κ³΅μλ 5λͺ λ§ λ°μν΄μΌ ν©λλ€.
κ²μ¦ νλͺ©μ λ€μκ³Ό κ°μ΅λλ€.
- μ±κ³΅ μμ² μλ 5건
- μ€ν¨ μμ² μλ 5건
class_room.enrollment_countλ 5enrollmentrow μλ 5
λμΌ ν΄λμ€λ©μ΄νΈκ° κ°μ κ°μμ λμμ 10λ² μ μ²νλλΌλ νλμ νμ± μ μ²λ§ μμ±λμ΄μΌ ν©λλ€.
κ²μ¦ νλͺ©μ λ€μκ³Ό κ°μ΅λλ€.
- μ±κ³΅ μμ² μλ 1건
- μ€ν¨ μμ² μλ 9건
class_room.enrollment_countλ 1enrollmentrow μλ 1- μ€ν¨ μμ²μ μ€λ³΅ μ μ² μμΈλ‘ μ²λ¦¬
λ³Έ ν μ€νΈ μ½λλ λ€μ ν΅μ¬ λΉμ¦λμ€ κ·μΉμ κ²μ¦ν©λλ€.
DRAFT -> OPEN
OPEN -> CLOSED
DRAFTμνμμλ§ κ°μ μμ κ°λ₯- μκ°μμ
DRAFTκ°μ μ‘°ν λΆκ° OPENμν κ°μλ§ μκ° μ μ² κ°λ₯
PENDING -> CONFIRMED
PENDING -> CANCELLED
CONFIRMED -> CANCELLED
CANCELLED -> PENDING
PENDING,CONFIRMEDλ μ’μ μ μ μνCANCELLEDλ μ’μ λ―Έμ μ μνCANCELLEDμ μ²μ μ¬μ μ² κ°λ₯- μ΄λ―Έ μ’μμ μ μ μ€μΈ μ μ²μ μ€λ³΅ μ μ² λΆκ°
enrollment_count = PENDING μν μ μ² μ + CONFIRMED μν μ μ² μ
- μ μ² μ±κ³΅ μ
enrollment_count + 1 - μ·¨μ λλ κ²°μ λ§λ£ μλ μ·¨μ μ
enrollment_count - 1 - μ μ μ΄κ³Ό μ μ μ² μ€ν¨
- λμ μ μ² μν©μμλ μ μ μ΄κ³Ό λΆκ°
paymentExpiredAt = μ μ² μκ° + 10λΆ
- κ²°μ λ§λ£ μ μλ κ²°μ νμ κ°λ₯
- κ²°μ λ§λ£ μκ°μ΄ λλνλ©΄ κ²°μ νμ λΆκ°
- λ§λ£λ
PENDINGμ μ²μ μλ μ·¨μ λμ - μλ μ·¨μ μ μ’μ μ μ μ κ°μ
- ν¬λ¦¬μμ΄ν°λ λ³ΈμΈ κ°μλ§ μμ , λͺ¨μ§ μμ, λͺ¨μ§ λ§κ°, μκ°μ λͺ©λ‘ μ‘°ν κ°λ₯
- ν΄λμ€λ©μ΄νΈλ λ³ΈμΈ μκ° μ μ²λ§ μμΈ μ‘°ν, κ²°μ νμ , μ·¨μ κ°λ₯
- κΆνμ΄ μλ μμ²μ 403 μμΈλ‘ μ²λ¦¬
μ΅μ’ μ μΌλ‘ λ€μ λͺ λ Ήμ΄ κΈ°μ€ μ 체 ν μ€νΈ ν΅κ³Όλ₯Ό νμΈνμ΅λλ€.
./gradlew clean testWindows νκ²½μμλ λ€μ λͺ λ Ήμ΄ κΈ°μ€ μ 체 ν μ€νΈ ν΅κ³Όλ₯Ό νμΈνμ΅λλ€.
gradlew.bat clean testν μ€νΈ μ±κ³΅ μ Gradle μΆλ ₯μ λ€μκ³Ό κ°μ΅λλ€.
BUILD SUCCESSFUL
- ν μ€νΈλ Docker Desktopμ΄ μ€ν μ€μΈ νκ²½μ μ μ λ‘ ν©λλ€.
- Repository / Integration / Concurrency ν μ€νΈλ MySQL Testcontainersλ₯Ό μ¬μ©νλ―λ‘ μ΅μ΄ μ€ν μ MySQL Docker image pull μκ°μ΄ λ°μν μ μμ΅λλ€.
creator,classmateν μ€νΈ λ°μ΄ν°λ Flyway seed dataμ μμ‘΄ν©λλ€.enrollment,class_roomλ°μ΄ν°λ ν μ€νΈ κ° κ²©λ¦¬λ₯Ό μν΄ κ° ν μ€νΈ μ μ μ΄κΈ°νν©λλ€.- μΈλΆ κ²°μ μμ€ν μ°λμ μμΌλ―λ‘ κ²°μ νμ μ μν λ³κ²½ λ‘μ§μΌλ‘λ§ κ²μ¦ν©λλ€.
- Redis λκΈ°μ΄ κΈ°λ₯μ λ³λ μ ν ꡬν μμμ΄λ©°, νμ¬ ν΅μ¬ ν μ€νΈ λ²μλ DB κΈ°λ° κ°μ/μκ° μ μ² μ ν©μ± κ²μ¦μ μ§μ€ν©λλ€.
- μ€μ νμκ°μ λ° λ‘κ·ΈμΈ κΈ°λ₯μ ꡬννμ§ μμμ΅λλ€.
- Spring Security κΈ°λ° μΈμ¦/μΈκ°λ μ μ©νμ§ μμμ΅λλ€.
- μμ²μ
creatorId,classmateIdκ°μ ν΅ν΄ μ¬μ©μλ₯Ό μλ³ν©λλ€. - μ€μ μΈλΆ κ²°μ μμ€ν μ°λμ ꡬννμ§ μμμ΅λλ€.
- κ²°μ νμ μ μΈλΆ κ²°μ μΉμΈ κ²°κ³Όλ₯Ό κ²μ¦νμ§ μκ³ μ μ² μνλ₯Ό CONFIRMEDλ‘ λ³κ²½νλ λ°©μμΌλ‘ λ체νμ΅λλ€.
- μ΄μ νκ²½μμ μ¬λ¬ μΈμ€ν΄μ€κ° μ€μΌμ€λ¬λ₯Ό λμμ μ€ννλ κ²½μ°μλ λΆμ° λ½μ΄ νμνμ§λ§, λ³Έ νλ‘μ νΈλ λ¨μΌ μΈμ€ν΄μ€ μ€νμ κΈ°μ€μΌλ‘ ꡬννμ΅λλ€.
- Redis λκΈ°μ΄μ μκ°μ μ μ½μ΄ μμ΄ κ΅¬ννμ§ μμμ΅λλ€.
- λμΌ μκ°μμ΄ κ°μ κ°μμ μ΅μ΄ μ μ²μ λμμ μ¬λ¬ λ² μμ²νλ κ·Ήλ¨μ μΈ κ²½μ° DBμ unique μ μ½μ΄ μ΅μ’ μ€λ³΅ λ°©μ΄μ μΌλ‘ λμν©λλ€. μ΄ κ²½μ° μ ν리μΌμ΄μ μμλ μ€λ³΅ μ μ² μμΈλ‘ λ³ννμ¬ μ²λ¦¬ν©λλ€.
λ³Έ κ³Όμ μν κ³Όμ μμ AI λꡬλ₯Ό λ€μ λ²μλ‘ νμ©νμ΅λλ€.
- μꡬμ¬ν λΆμ λ° κΈ°λ₯ λͺ μΈ μ 리
- README ꡬ쑰 μ΄μ μμ± λ° μ΅μ’ κ²ν
- ν μ€νΈ μΌμ΄μ€ λμΆ
- ν μ€νΈ μ½λ μ€κ³
- μ½λ κ²μ¦
AIκ° μμ±ν λ΄μ©μ κ·Έλλ‘ μ μΆνμ§ μκ³ , νλ‘μ νΈ μꡬμ¬νκ³Ό ꡬν μ½λμ λ§κ² μ§μ μμ νκ³ κ²μ¦νμ΅λλ€.
μ΅μ’ μ€κ³, ꡬν, ν μ€νΈ, μμΈ μ²λ¦¬ λ°©μμ μ§μ νλ¨νμ¬ λ°μνμ΅λλ€.