- Java21
- Spring Boot 4.0.6, Spring Framework 7.0.7
- Spring Data JPA, MySQL
docker-compose up -d- src/docs/asciidoc/index.adoc
- Spring Rest Docs로 작성한 API 문서
./gradlew asciidoctorasciidoctor태스크는test에 의존하므로 테스트 실행 → REST Docs 스니펫 생성(build/generated-snippets) → 문서 조립 순으로 수행됩니다.- 빌드 결과물:
build/docs/asciidoc/index.html
src/main/java/com/liveklass/jxmen_assignment/
├── domain/ # JPA 엔티티 + 비즈니스 규칙
│ ├── exceptions/ # 도메인 예외 클래스
│ ├── Class.java # 강의 (DRAFT → OPEN → CLOSED)
│ ├── ClassMate.java # 수강생
│ ├── ClassStatus.java
│ ├── Creator.java # 강사
│ ├── Enrollment.java # 수강 신청 (WAITING/PENDING → CONFIRMED / CANCELLED)
│ └── EnrollmentStatus.java
├── usecase/ # 쓰기 유스케이스 (@Transactional)
│ ├── dto/ # 유스케이스 입력 Command 객체
│ ├── EnrollmentUseCase.java
│ ├── CancelEnrollmentUseCase.java
│ └── ...
├── query/ # 읽기 전용 조회 (@Transactional readOnly=true)
│ ├── ListClassesQuery.java
│ ├── MyEnrollmentsQuery.java
│ └── ...
└── infrastructure/
├── web/ # @RestController, DTO, ApiExceptionHandler
│ └── dto/
├── persistence/ # Spring Data JPA Repository 인터페이스
└── config/
기본적으로는 클린 아키텍처를 지향하였으나, 단순한 CRUD 어플리케이션을 고려하여 어느정도 타협하였습니다.
레이어별 설명
- domain
- 비즈니스 로직을 담은 메서드 및 JPA 엔티티 설정 등이 포함되어 있습니다.
- JPA 의존성이 있기에 POJO는 아닙니다. POJO 형태로 만들 경우 entity/domain 클래스를 분리하고 mapeer를 사용해야 합니다.
- usecase
- 트랜잭션 관리, 도메인으로 비즈니스 로직 처리 위임
- 인터페이스가 아닌 구현체 클래스를 그대로 사용했습니다. 만약 구현 변경이 필요하다면, 인터페이스 추출 후 구현체 클래스를 사용하는 방식으로 리팩토링 하는 것이 좋다고 판단했습니다.
- query: 조회용 서비스
- infrastructure:
- 외부 구현체 - web, database 등
| 엔티티 | 설명 |
|---|---|
Creator |
강사. 강의를 생성한다 |
Class |
강의. 생성 시 DRAFT, 수동으로 OPEN → CLOSED 전이 |
ClassMate |
수강생. enroll() 호출로 수강 신청 |
Enrollment |
수강 신청. 정원 미달 시 PENDING, 초과 시 WAITING 생성. 취소 시 대기열 선두가 자동으로 PENDING 승격 |
- 인증/사용자 식별: 별도 인증 미들웨어 없이 요청 헤더(
X-Creator-Id,X-ClassMate-Id)로 식별합니다. 실제 서비스라면 JWT 등으로 대체해야 하나, 과제 범위에서는 단순화하였습니다. - 대기열(WAITING) 동작:
- 정원이 초과된 상태에서 신청하면 거부하지 않고
WAITING상태로 생성합니다.
- 정원이 초과된 상태에서 신청하면 거부하지 않고
- 취소 가능 기간: 명세의 "예: 7일"을 그대로 채택하여, 결제 확정(
confirmedAt) 후 7일이 지나면 취소 불가로 처리 - 중복 신청 방지: 동일 수강생이 동일 강의에 활성 신청(
WAITING/PENDING/CONFIRMED)을 동시에 두 건 이상 가질 수 없도록 제약합니다. 취소 후 재신청은 허용합니다. - 강의별 수강생 목록 조회: 크리에이터 전용으로 제한하며, 강의 소유자가 아니면 403을 반환합니다.
동시성 테스트 코드 - EnrollmentUseCaseConcurrencyTest.java
- 수강 신청 동시성 처리의 경우 원자적 업데이트를 사용했습니다. 구현이 간단하고 성능이 뛰어나기 때문입니다.
- 대기열 기능의 경우, 수강 신청 상태 중 별도의
WAITING이라는 상태로 설정하게 하였습니다. 그리고 누군가 결제 후 취소(CONFIRMED->CANCELLED)를 한다면, 가장 먼저 WAITING 상태의 수강생이 PENDING 상태가 되도록 구현하였습니다.- 취소 시 상태 변경도 capacity를 넘지 않기 위해 비관적 락을 사용했으나, 충돌이 많을 것으로 예상되는 부분은 아니라 낙관적 락도 적절한 것 같습니다.
./gradlew test./gradlew test jacocoTestReport- HTML 리포트:
build/reports/jacoco/test/html/index.html - XML 리포트:
build/reports/jacoco/test/jacocoTestReport.xml test태스크가 끝나면jacocoTestReport가 자동 실행되도록 설정되어 있습니다(finalizedBy).
현재 커버리지 (line 기준):
| 패키지 | Line | 비율 |
|---|---|---|
| domain | 106/112 | 94.6% |
| domain/exceptions | 16/18 | 88.9% |
| usecase | 43/64 | 67.2% |
| usecase/dto | 1/2 | 50.0% |
| query | 29/32 | 90.6% |
| infrastructure/web | 48/55 | 87.3% |
| infrastructure/web/dto | 64/64 | 100.0% |
| 전체 | 309/351 | 88.0% |
- 완전한 클린 아키텍처가 아님
- API 예외 시 별도 상태 응답 코드 추가하지 못함 (예: {code: INVALID_INPUT})
- 사용자 등록 API 없음:
Creator/ClassMate는 DB에 미리 존재한다고 가정하며, 가입 플로우는 구현되어 있지 않습니다- data.sql을 통해 더미 데이터 생성
- 대기열 부가 기능 미구현: 대기 순번 조회, 사용자 본인의
WAITING이탈(취소)등은 구현되어 있지 않습니다. - 시나리오/E2E 테스트 부재: 단위·동시성 테스트는 작성되어 있으나, 여러 API를 엮은 다단계 시나리오 테스트는 작성하지 못했습니다.
모델 정보: Claude Code Sonnet 4.6 (medium)
반복적으로 작성되는 아래 항목의 초기 템플릿 생성을 자동화했습니다.
- Controller 테스트
- REST Docs snippet
- HTTP example
- DTO skeleton
비즈니스 로직 및 도메인 규칙은 직접 구현했습니다.
- 테스트 누락 방지
- API 문서 형식 일관화
- 반복 작업 감소
- API 설계 적절성
- 예외 처리
- 상태 전이 로직
- 도메인 규칙
- 테스트 통과 여부
스킬 파일: SKILL.md
