Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
35cddde
feat: add API documentation for movie registration endpoint
YeaChan05 Aug 19, 2025
1b75d05
feat: add documentation on Spring Security filter registration process
YeaChan05 Aug 21, 2025
0c5530f
feat: refactor package structure for web API components
YeaChan05 Aug 23, 2025
9ea7971
feat: implement movie registration functionality and update related c…
YeaChan05 Aug 23, 2025
0b37e9c
feat: add Jackson configuration for custom date formatting and timezone
YeaChan05 Aug 23, 2025
3c32e8d
feat: implement custom authentication and access denial handling
YeaChan05 Aug 26, 2025
433057e
feat: update TestResult to use ApiResponse for success handling
YeaChan05 Aug 26, 2025
7d3a96a
feat: simplify JWT authentication by removing unused provider and enh…
YeaChan05 Aug 26, 2025
22b5043
feat: enhance custom authentication by adding authority handling in t…
YeaChan05 Aug 26, 2025
75d628c
feat: add test case for unauthorized access handling in movie registr…
YeaChan05 Aug 27, 2025
600cded
feat: improve error handling in TestResult for response validation
YeaChan05 Aug 27, 2025
137e7c6
feat: enhance movie registration validation and error handling
YeaChan05 Aug 27, 2025
baba91a
feat: add validation for runtimeMinutes to ensure non-negative values
YeaChan05 Aug 27, 2025
6340078
feat: update releaseDate validation to require yyyy-MM-dd format
YeaChan05 Aug 27, 2025
dabe352
feat: mark releaseDate validation as implemented in movie registration
YeaChan05 Aug 27, 2025
ddf0560
feat: enhance JWT token generation to include user authorities
YeaChan05 Aug 28, 2025
93f78ac
feat: enhance integration tests for JWT authentication and user role …
YeaChan05 Aug 28, 2025
a3b3dff
feat: implement movie registration functionality with validation and …
YeaChan05 Aug 28, 2025
50a2129
feat: remove unnecessary checks in authentication entry point and res…
YeaChan05 Aug 28, 2025
606a3f2
feat: add NOT_FOUND status and handle NoHandlerFoundException in glob…
YeaChan05 Aug 29, 2025
0edecd0
feat: enhance authentication handling with improved error messaging a…
YeaChan05 Aug 29, 2025
f00d14a
feat: enhance authentication handling with improved error messaging a…
YeaChan05 Aug 29, 2025
2e42b79
feat: update API documentation for movie registration and member regi…
YeaChan05 Aug 29, 2025
60c4708
feat: refactor member and movie repository interfaces for consistency…
YeaChan05 Sep 1, 2025
e987bbb
feat: add AI assistant rules file to .gitignore
YeaChan05 Sep 1, 2025
70f77db
feat: add architecture and authentication policy documentation
YeaChan05 Sep 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.gradle/
/build
.idea
.aiassistant/rules/AGENTS.md
268 changes: 268 additions & 0 deletions AGENTS.md

Large diffs are not rendered by default.

122 changes: 122 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# booking — 영화 예매 시스템 (개요 중심 README)

이 문서는 프로젝트를 어떻게 실행하는지보다는, 이 프로젝트가 무엇인지, 어떤 구성과 아키텍처를 가지는지, 어떤 기술을 왜 선택했는지, 그리고 어떤 방식으로 개발하는지를 설명합니다. 모든 주장은 저장소 내 문서/코드에 대한 링크로 근거를 제시합니다.

- 저장소 루트: 단일 모듈(Spring Boot) 프로젝트

---

## 1. 프로젝트 소개
영화 예매(Booking) 도메인을 다루는 학습 목적의 Spring Boot 애플리케이션입니다. 헥사고날 아키텍처(Ports & Adapters)를 채택하여 도메인 규칙을 프레임워크로부터 분리하고, 보안/웹/영속성 같은 어댑터 계층을 통해 외부 세계와 상호작용합니다.

- 도메인 개요: [docs/specs/domain.md](docs/specs/domain.md)
- 아키텍처/개발 규칙: [docs/specs/policy/application.md](docs/specs/policy/application.md)
- 테스트 규칙: [docs/specs/policy/test.md](docs/specs/policy/test.md)
---

## 2. 핵심 기능
- 추후 작성 예정

테스트로 검증되는 수용 기준은 각 API 문서 하단의 체크리스트를 참고하세요.

---

## 3. 아키텍처 개요 (Hexagonal)
헥사고날 아키텍처를 적용하여 다음과 같은 레이어 규칙을 따릅니다.

- domain: 순수한 도메인 모델과 비즈니스 규칙. 프레임워크 의존 금지.
- app: 유스케이스 서비스, 입력/출력 포트, 트랜잭션 경계, 검증, AOP.
- adapter: 웹 API, 보안, 영속성 등 외부 인터페이스.

근거와 세부 규칙
- 정책 문서: [docs/specs/policy](docs/specs/policy)
- 레이어 테스트: `src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java`
- 패키지 구조 예
- 도메인: `src/main/java/org/mandarin/booking/domain/*`
- 앱/포트/영속 어댑터: `src/main/java/org/mandarin/booking/app/*` (`app/persist` 포함)
- 웹/보안 어댑터: `src/main/java/org/mandarin/booking/adapter/{webapi,security}/*`

텍스트 다이어그램: [Controllers/Security/External] → adapter → app(ports, services) → domain

---

## 4. 도메인 모델 요약
- Movie (Aggregate Root): 제목, 감독, 장르, 상영시간, 개봉일, 등급, 줄거리, 포스터URL, 출연진 등. 팩토리/커맨드 기반 생성.
- Member (Aggregate Root): 닉네임, userId, email, passwordHash, 권한 목록. 비밀번호 해시 일치 검증.

자세한 속성과 규칙: [docs/specs/domain.md](docs/specs/domain.md)

---

## 5. 기술 스택과 선택 근거
- 추후 작성 예정

선택 이유(요지)
- Hexagonal: 테스트 용이성과 변경 격리를 위해 계층 경계를 명확히. 또한, 추후 모듈화 or MSA 전환시 이점을 위해 애플리케이션 아키텍처를 영역에 따라 구분.
- Spring Security + JWT: 무상태(stateless) API 인증과 확장성.
- JPA + RDB(H2/MySQL): 표준 ORM과 빠른 테스트 사이클.

---

## 6. 개발 방식과 테스트 전략
- 테스트 주도 개발(TDD) 지향: 테스트 우선, 기능 추가 시 관련 스펙 테스트 동반.
- 테스트 정책 문서: [docs/specs/policy/test.md](docs/specs/policy/test.md)
- 통합 테스트: Spring Context 기동, 보안 필터/컨트롤러/JPA 연동을 포함한 경로 검증.
- 예시: `src/test/java/org/mandarin/booking/webapi/**/POST_specs.java`
- 아키텍처 테스트: 레이어 규칙 준수 확인.
- 예시: `src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java`

Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profiles, JUnit Platform, ByteBuddy javaagent).

---

## 7. 보안 개요
- 필터 기반 JWT 인증: `JwtFilter`가 Authorization `Bearer <token>`을 파싱해 SecurityContext 설정.
- 경로별 권한: `SecurityConfig`의 `@Order(1) apiChain`
- 공개: `POST /api/member`, `/api/auth/login`, `/api/auth/reissue`
- 권한 필요: `POST /api/movie`는 `ROLE_DISTRIBUTOR`
- 예외 처리: `CustomAuthenticationEntryPoint`, `CustomAccessDeniedHandler`

근거: `src/main/java/org/mandarin/booking/adapter/security/*`

---

## 8. 데이터/환경 구성
- 프로필: `local`(기본), `test`, `prod(비어있음)`
- 근거: `src/main/resources/application.yml` 및 `application-*.yml`
- local: MySQL + JPA `ddl-auto: create`, JWT 시크릿/TTL 설정
- 근거: `application-local.yml`, Docker Compose: [compose.yaml](compose.yaml)
- test: H2 메모리 + MySQL 호환 모드 + JPA `ddl-auto: create`
- 근거: `application-test.yml`

민감정보는 운영 환경에서 환경변수로 주입하는 것을 권장합니다(로컬에 예시 값 존재).

---

## 9. API 문서
- 로그인: [docs/specs/api/login.md](docs/specs/api/login.md)
- 회원 가입: [docs/specs/api/member_register.md](docs/specs/api/member_register.md)
- 토큰 재발급: [docs/specs/api/reissue.md](docs/specs/api/reissue.md)
- 영화 등록: [docs/specs/api/movie_register.md](docs/specs/api/movie_register.md)

각 문서 하단의 테스트 체크리스트가 수용 기준입니다.

---

## 10. 프로젝트 상태 및 향후 계획
- CI/CD, 코드 포매터, 마이그레이션 도구(Flyway/Liquibase)는 현재 문서/설정 부재로 "확인 불가" 상태입니다.
- TODO/메모: [docs/devlog/*](docs/devlog), [docs/todo.md](docs/todo.md)
- 권장 향후 작업
- prod 프로필 구성과 비밀 주입 전략 수립
- CI 파이프라인(.github/workflows) 도입
- DB 마이그레이션 도구 채택 및 규약 수립
- 인증/인가 정책 문서 구체화: [docs/specs/policy/authentication.md](docs/specs/policy/authentication.md), [docs/specs/policy/authorization.md](docs/specs/policy/authorization.md)

---

## 11. 버전/도구 근거 링크
- Spring Boot/Java/Gradle 버전: [build.gradle](build.gradle), [gradle-wrapper.properties](gradle/wrapper/gradle-wrapper.properties)
- 애플리케이션 엔트리포인트: `src/main/java/org/mandarin/booking/BookingApplication.java`
- 보안 설정/필터: `src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java`, `src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java`
- 아키텍처 규칙: [docs/specs/policy/application.md](docs/specs/policy/application.md)
- 테스트 정책: [docs/specs/policy/test.md](docs/specs/policy/test.md)
79 changes: 37 additions & 42 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,48 +23,43 @@ configurations {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

implementation("org.projectlombok:lombok:1.18.36")
annotationProcessor('org.projectlombok:lombok:1.18.36')

// Spring web
implementation 'org.springframework.boot:spring-boot-starter-web'

// Spring data JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

//h2 database
testRuntimeOnly 'com.h2database:h2'

developmentOnly 'org.springframework.boot:spring-boot-docker-compose'

implementation 'com.mysql:mysql-connector-j:8.3.0'

// Spring validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// Spring security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// Mockito inline 사용 시 필요한 Byte Buddy Agent
byteBuddyAgent "net.bytebuddy:byte-buddy-agent:1.17.6"

// spotbugs
spotbugs 'com.github.spotbugs:spotbugs:4.9.3'

// p6spy
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'

// jjwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
testImplementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
// ---- Spring Boot Core ----
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-aop'

// ---- Data & Database ----
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.mysql:mysql-connector-j:8.3.0'
testRuntimeOnly 'com.h2database:h2'
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'

// ---- Security & Auth ----
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
testImplementation 'io.jsonwebtoken:jjwt-impl:0.12.6'

// ---- Lombok ----
implementation 'org.projectlombok:lombok:1.18.36'
annotationProcessor 'org.projectlombok:lombok:1.18.36'

// ---- Dev Only ----
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'

// ---- Code Quality / Tooling ----
spotbugs 'com.github.spotbugs:spotbugs:4.9.3'

// ---- Testing ----
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
byteBuddyAgent 'net.bytebuddy:byte-buddy-agent:1.17.6'
testImplementation 'org.mockito:mockito-inline:5.2.0'
}

tasks.named('test') {
Expand Down
11 changes: 11 additions & 0 deletions docs/devlog/250821.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## 예찬
Spring Security의 filter **등록** 방식에 대한 이해가 부족하다고 느껴 실제로 등록하는 과정을 디버깅을 통해 확인해봤다. Spring Bea으로 등록된 filter는 기본적으로 servlet context에 의해 filter chain에 등록되지만 상세한 filter 우선순위를 결정하는데에 불편함이 있다. 그래서 Spring Security는 해당 filter의 우선순위를 보다 명시적으로 지정하기 위해 `SecurityFilterChain`을 사용해 filter의 우선순위를 명시하고, `DelegatingFilterChainProxy`가 이를 적용해준다. 그 과정에서 bean등록이 된 filter를 `SecurityFilterChain`에 명시하게 된다면 이 과정이 두번 동작하기 때문에 등록 과정이 두번 실행된다. 헷갈리지 말아야 할 개념은 실제 bean filter를 `DelegatingFilterChainProxy`가 두개 등록할 수는 없다.(애초에 Spring Singleton Bean이라서 인스턴스가 하나밖에 없기도 하다.) 다만,`DelegatingFilterChainProxy`가 `ApplicationContext`에서 빈을 가져올때 filter의 `beanName`으로 인스턴스를 레퍼런싱 하다보니 중복 등록을 제재할 방법은 없는것이다. 하지만 filter를 직접 적용하는 과정에서 동일 bean임이 인식되기 때문에 실질적으로 동작하는 인스턴스는 한번뿐인 것이다. 정리하자면 등록할 filter을 bean으로 등록하게 되면 자동으로 filter chain에 추가되기 때문에 가능하면 이 경우에는 `SecurityFilterChain`에 등록을 하지 말던가 아니면 bean으로 등록하지 않은 상태에서 `SecurityFilterChain`에 등록하도록 객체를 생성해야 중복 등록이 발생하지 않는다.

## 휘동
@Component로 filter를 스프링 빈으로 만들고, filter chain에 등록하면 filter가 중복 등록되는 것을 확인함.
filter를 빈으로 만들면 자동으로 servletfilterchain에 등록되는데, securityfilterchain에 빈을 주입받아 수동으로 등록하면서 중복됨.
실제 filter 동작은 한번만 일어나다.
GenericFilterBean을 상속받은 filter는 요청/응답 시 한번씩 총 2번 동작하고,
OncePerRequestFilter를 상속받은 filter는 요청에만 동작한다.
filter의 동작이 끝날 때 filterchain.doFilter()가 없으면 다음 필터(응답 시 동작해야할 필터 포함)는 작동하지 않는다.
filterchain.doFilter()의 유무로 필터의 중단을 결정하지 말고, GenericFilterBean와 OncePerRequestFilter를 적절히 사용해 필터 적용 시기를 조절함이 바람직하다.
6 changes: 6 additions & 0 deletions docs/devlog/250829.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## 예찬
전반적인 인증 흐름에 대한 이해가 이번 기능 구현 과정에서 미흡했던것을 인지함. 이후 Spring Security의 인증 흐름을 학습하고, 그 과정에서 기존에 구현하려 했던 방식에 문제점을 발견했고, 이를 해결하기 위해 직접 제어 가능한 수준의 인증정보 제공자를 직접 구현하는 방식으로 기능 구현, 성공적인 테스트 케이스 통과가 가능했다.
이후에 해야할건 영화 조회 아닐까 싶다. 영화 조회 기능도 일단 빠르게 Spring Data JPA의 도움을 받아서 구현하고 이후 최적화 과정을 거치는 것이 좋지 않을까 하는 생각.
추가로, 현재까지 개발을 하는 과정에서 정책을 일부 작성해왔었는데, 이부분이 좀 누락된거 같다. 작성해두는게 앞으로 문제 발생 가능성을 줄이는데에 도움이 되지 않을까...

## 휘동
6 changes: 3 additions & 3 deletions docs/specs/api/login.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
-H 'Content-Type: application/json' \
-d '
{
"userId": "string",
"password": "string"
"userId": "test1234",
"password": "myPassword123"
}'
```

Expand Down Expand Up @@ -55,4 +55,4 @@
- [x] 성공적인 로그인 후 응답에 accessToken과 refreshToken가 포함되어야 한다
- [x] 전달된 토큰은 유효한 JWT 형식이어야 한다
- [x] 전달된 토큰은 만료되지 않아야한다
- [x] 전달된 토큰에는 사용자의 올바른 userId가 포함되어야 한다
- [x] 전달된 토큰에는 사용자의 올바른 userId가 포함되어야 한다
2 changes: 1 addition & 1 deletion docs/specs/api/member_register.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
curl 명령 예시

```bash
curl -i -X POST 'http://localhost:8080/api/members' \
curl -i -X POST 'http://localhost:8080/api/member' \
-H 'Content-Type: application/json' \
-d '{
"nickName": "test",
Expand Down
64 changes: 64 additions & 0 deletions docs/specs/api/movie_register.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
### 요청

- 메서드: `POST`
- 경로: `/api/movies`
- 헤더

```
Content-Type: application/json
Authorization: Bearer <accessToken>
```

- 본문

```json

```


- curl 명령 예시

```bash
curl -i -X POST 'http://localhost:8080/api/movie' \
-H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0MTIzNCIsInJvbGVzIjoiUk9MRV9ESVNUUklCVVRPUiIsInVzZXJJZCI6InRlc3QxMjM0Iiwibmlja05hbWUiOiJ0ZXN0IiwiaWF0IjoxNzU2NDM4MjIzLCJleHAiOjE3NTY0Mzg4MjN9.DN0wZb8BdKY-7Grd0KAALXf88KX3iF_tg6UmcfotkFOlbRoRnSuY1nNVUFfZk2TxP0hvju3A8AglK3mt_hnutQ' \
-H 'Content-Type: application/json' \
-d '{
"title": "인셉션",
"director": "크리스토퍼 놀란",
"runtimeMinutes": 148,
"genre": "SF",
"releaseDate": "2010-07-21",
"rating": "AGE12",
"synopsis": "타인의 꿈속에 진입해 아이디어를 주입하는 특수 임무를 수행하는 이야기.",
"posterUrl": "https://example.com/posters/inception.jpg",
"casts": [
"레오나르도 디카프리오",
"조셉 고든레빗",
"엘렌 페이지"
]
}'
```

### 응답

- 상태코드: `200 OK`
- 본문

```json
{
"status": "SUCCESS",
"data": {
"movieId": 1
},
"timestamp": "2024-06-10T12:34:56.789Z"
}
```

### 테스트

- [x] 올바른 요청을 보내면 status가 SUCCESS이다
- [ ] 올바른 요청을 보내면 응답 본문에 movieId가 존재한다
- [x] Authorization 헤더에 유효한 accessToken이 없으면 status가 UNAUTHORIZED이다
- [x] title, director, runtimeMinutes, genre, releaseDate, rating이 비어있으면 BAD_REQUEST이다
- [x] runtimeMinutes은 0 미만이면 BAD_REQUEST이다
- [x] releaseDate는 yyyy-MM-dd 형태를 준수하지 않으면 BAD_REQUEST이다
Loading