From 8485f48ac2ce30dbeaf4d73550a73862b20e3e5a Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Tue, 9 Sep 2025 17:32:02 +0900 Subject: [PATCH 01/36] refactor: reorganize application-prod.yml and update .gitignore --- .gitignore | 2 -- application/.gitignore | 2 ++ domain/.gitignore | 1 + src/main/resources/application-prod.yml | 0 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 application/.gitignore create mode 100644 domain/.gitignore delete mode 100644 src/main/resources/application-prod.yml diff --git a/.gitignore b/.gitignore index 88538ea..379d52b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ -# Gradle 캐시·빌드 정보 .gradle/ /build .idea .aiassistant/rules/AGENTS.md -/src/docs/ diff --git a/application/.gitignore b/application/.gitignore new file mode 100644 index 0000000..a765bfd --- /dev/null +++ b/application/.gitignore @@ -0,0 +1,2 @@ +/docs +/build diff --git a/domain/.gitignore b/domain/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/domain/.gitignore @@ -0,0 +1 @@ +/build diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml deleted file mode 100644 index e69de29..0000000 From 6885d608a2d0769f9772d9db93946084c6e36240 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Tue, 9 Sep 2025 17:33:54 +0900 Subject: [PATCH 02/36] update .gitignore to include relative paths for docs and build directories --- application/.gitignore | 4 ++-- domain/.gitignore | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/application/.gitignore b/application/.gitignore index a765bfd..5046a92 100644 --- a/application/.gitignore +++ b/application/.gitignore @@ -1,2 +1,2 @@ -/docs -/build +./docs +./build diff --git a/domain/.gitignore b/domain/.gitignore index 796b96d..6f65c48 100644 --- a/domain/.gitignore +++ b/domain/.gitignore @@ -1 +1 @@ -/build +./build From e35089dbdb4c97b2e36fe88f63a23871d26fc07f Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Tue, 9 Sep 2025 17:36:42 +0900 Subject: [PATCH 03/36] update .gitignore to include relative paths for docs and build directories --- .gitignore | 2 +- application/.gitignore | 1 - domain/.gitignore | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 379d52b..d899e02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .gradle/ -/build +**/build .idea .aiassistant/rules/AGENTS.md diff --git a/application/.gitignore b/application/.gitignore index 5046a92..2d1477b 100644 --- a/application/.gitignore +++ b/application/.gitignore @@ -1,2 +1 @@ ./docs -./build diff --git a/domain/.gitignore b/domain/.gitignore index 6f65c48..e69de29 100644 --- a/domain/.gitignore +++ b/domain/.gitignore @@ -1 +0,0 @@ -./build From 7247b23b2bba6e2f44b22313e18cdd650ad6e822 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Tue, 9 Sep 2025 17:37:40 +0900 Subject: [PATCH 04/36] update .gitignore to include relative paths for docs and build directories --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d899e02..cb650c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .gradle/ **/build +build .idea .aiassistant/rules/AGENTS.md From 7cf3ba3ee5378c5735b8007ed8b62b7275e385d2 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Wed, 10 Sep 2025 15:32:58 +0900 Subject: [PATCH 05/36] separate domain module from main module --- README.md | 4 +- application/build.gradle | 73 + application/docs/index.adoc | 28 + application/docs/index.html | 2567 +++++++++++++++++ .../mandarin/booking/BookingApplication.java | 0 .../adapter/JacksonCustomizerConfig.java | 0 .../security/CustomAccessDeniedHandler.java | 0 .../CustomAuthenticationEntryPoint.java | 13 +- .../CustomAuthenticationProvider.java | 8 +- .../CustomMemberAuthenticationToken.java | 9 +- .../booking/adapter/security/JwtFilter.java | 3 +- .../adapter/security/SecurityConfig.java | 0 .../adapter/security/package-info.java | 4 + .../booking/adapter/webapi/ApiResponse.java | 2 + .../booking/adapter/webapi/ApiStatus.java | 0 .../adapter/webapi/AuthController.java | 0 .../webapi/CommonHttpMessageConverter.java | 10 +- .../booking/adapter/webapi/ErrorResponse.java | 0 .../webapi/GlobalExceptionHandler.java | 4 +- .../adapter/webapi/MemberController.java | 2 +- .../adapter/webapi/ResponseWrapper.java | 3 +- .../adapter/webapi/ShowController.java | 0 .../adapter/webapi/SuccessResponse.java | 0 .../booking/adapter/webapi/package-info.java | 4 + .../org/mandarin/booking/app/AuthService.java | 4 +- .../app/BCryptSecurePasswordEncoder.java | 0 .../booking/app/HallExistCheckEvent.java | 0 .../org/mandarin/booking/app/HallService.java | 0 .../mandarin/booking/app/JwtTokenUtils.java | 9 +- .../java/org/mandarin/booking/app/Log.java | 0 .../mandarin/booking/app/LoggingAspect.java | 118 + .../booking/app/MemberRegisterValidator.java | 0 .../mandarin/booking/app/MemberService.java | 6 +- .../mandarin/booking/app/QuerydslConfig.java | 0 .../org/mandarin/booking/app/ShowService.java | 6 +- .../org/mandarin/booking/app/TokenUtils.java | 4 +- .../mandarin/booking/app/package-info.java | 0 .../app/persist/HallCommandRepository.java | 0 .../app/persist/HallQueryRepository.java | 0 .../booking/app/persist/HallRepository.java | 0 .../app/persist/MemberCommandRepository.java | 0 .../app/persist/MemberQueryRepository.java | 0 .../booking/app/persist/MemberRepository.java | 0 .../app/persist/ShowCommandRepository.java | 0 .../app/persist/ShowQueryRepository.java | 0 .../booking/app/persist/ShowRepository.java | 0 .../booking/app/port/AuthUseCase.java | 0 .../booking/app/port/MemberRegisterer.java | 0 .../booking/app/port/ShowRegisterer.java | 0 .../src}/main/resources/application-local.yml | 0 .../src}/main/resources/application-test.yml | 0 .../src}/main/resources/application.yml | 0 .../booking/BookingApplicationTests.java | 20 + .../java/org/mandarin/booking/DocsUtils.java | 0 .../org/mandarin/booking/IntegrationTest.java | 0 .../booking/IntegrationTestUtils.java | 3 +- .../booking/IntegrationTestUtilsSpecs.java | 0 .../org/mandarin/booking/JwtTestUtils.java | 0 .../java/org/mandarin/booking/NoRestDocs.java | 0 .../java/org/mandarin/booking/TestConfig.java | 0 .../java/org/mandarin/booking/TestResult.java | 9 +- .../CustomAuthenticationEntryPointTest.java | 0 .../CustomAuthenticationProviderTest.java | 3 +- .../CustomMemberAuthenticationTokenTest.java | 16 + .../adapter/security/JwtFilterTest.java | 6 +- .../CommonHttpMessageConverterTest.java | 25 + .../webapi/GlobalExceptionHandlerTest.java | 0 .../booking/app/LoggingAspectTest.java | 615 ++++ .../arch/HexagonalArchitectureTest.java | 0 .../booking/fixture/MemberFixture.java | 0 .../booking/webapi/auth/login/POST_specs.java | 5 +- .../webapi/auth/reissue/POST_specs.java | 0 .../booking/webapi/member/POST_specs.java | 0 .../booking/webapi/show/POST_specs.java | 1 + .../webapi/show/schedule/POST_specs.java | 19 +- build.gradle | 152 +- domain/build.gradle | 18 + .../booking/domain/AbstractEntity.java | 7 +- .../booking/domain/DomainException.java | 0 .../mandarin/booking/domain/EnumRequest.java | 0 .../booking/domain/EnumRequestValidator.java | 3 + .../booking/domain/member/AuthException.java | 0 .../booking/domain/member/AuthRequest.java | 0 .../booking/domain/member/Member.java | 6 + .../domain/member/MemberAuthority.java | 5 +- .../member/MemberAuthorityConverter.java | 2 - .../domain/member/MemberException.java | 0 .../domain/member/MemberRegisterRequest.java | 2 + .../domain/member/MemberRegisterResponse.java | 0 .../booking/domain/member/ReissueRequest.java | 0 .../domain/member/SecurePasswordEncoder.java | 5 - .../booking/domain/member/TokenHolder.java | 0 .../booking/domain/member/package-info.java | 0 .../mandarin/booking/domain/package-info.java | 0 .../mandarin/booking/domain/show/Show.java | 0 .../booking/domain/show/ShowException.java | 0 .../domain/show/ShowRegisterRequest.java | 0 .../domain/show/ShowRegisterResponse.java | 0 .../booking/domain/show/ShowSchedule.java | 0 .../show/ShowScheduleCreateCommand.java | 0 .../show/ShowScheduleRegisterRequest.java | 0 .../show/ShowScheduleRegisterResponse.java | 0 .../booking/domain/show/package-info.java | 0 .../mandarin/booking/domain/venue/Hall.java | 0 .../booking/domain/venue/HallException.java | 0 .../booking/domain/AbstractEntityTest.java | 3 + .../mandarin/booking/domain/MemberTest.java | 2 +- settings.gradle | 3 + .../mandarin/booking/app/LoggingAspect.java | 94 - .../booking/domain/member/MemberDetails.java | 31 - .../booking/BookingApplicationTests.java | 13 - .../mandarin/booking/TestOnlyController.java | 23 - .../booking/app/LoggingAspectTest.java | 192 -- .../org.mockito.plugins.MockMaker | 1 - 114 files changed, 3639 insertions(+), 526 deletions(-) create mode 100644 application/build.gradle create mode 100644 application/docs/index.adoc create mode 100644 application/docs/index.html rename {src => application/src}/main/java/org/mandarin/booking/BookingApplication.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java (78%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java (85%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java (70%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/security/JwtFilter.java (96%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java (100%) create mode 100644 application/src/main/java/org/mandarin/booking/adapter/security/package-info.java rename {src => application/src}/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java (84%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/webapi/AuthController.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java (90%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java (89%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/webapi/MemberController.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java (90%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/webapi/ShowController.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java (100%) create mode 100644 application/src/main/java/org/mandarin/booking/adapter/webapi/package-info.java rename {src => application/src}/main/java/org/mandarin/booking/app/AuthService.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/BCryptSecurePasswordEncoder.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/HallExistCheckEvent.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/HallService.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/JwtTokenUtils.java (90%) rename {src => application/src}/main/java/org/mandarin/booking/app/Log.java (100%) create mode 100644 application/src/main/java/org/mandarin/booking/app/LoggingAspect.java rename {src => application/src}/main/java/org/mandarin/booking/app/MemberRegisterValidator.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/MemberService.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/QuerydslConfig.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/ShowService.java (93%) rename {src => application/src}/main/java/org/mandarin/booking/app/TokenUtils.java (78%) rename {src => application/src}/main/java/org/mandarin/booking/app/package-info.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/persist/HallCommandRepository.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/persist/HallQueryRepository.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/persist/HallRepository.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/persist/MemberCommandRepository.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/persist/MemberQueryRepository.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/persist/MemberRepository.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/persist/ShowCommandRepository.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/persist/ShowQueryRepository.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/persist/ShowRepository.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/port/AuthUseCase.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/port/MemberRegisterer.java (100%) rename {src => application/src}/main/java/org/mandarin/booking/app/port/ShowRegisterer.java (100%) rename {src => application/src}/main/resources/application-local.yml (100%) rename {src => application/src}/main/resources/application-test.yml (100%) rename {src => application/src}/main/resources/application.yml (100%) create mode 100644 application/src/test/java/org/mandarin/booking/BookingApplicationTests.java rename {src => application/src}/test/java/org/mandarin/booking/DocsUtils.java (100%) rename {src => application/src}/test/java/org/mandarin/booking/IntegrationTest.java (100%) rename {src => application/src}/test/java/org/mandarin/booking/IntegrationTestUtils.java (97%) rename {src => application/src}/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java (100%) rename {src => application/src}/test/java/org/mandarin/booking/JwtTestUtils.java (100%) rename {src => application/src}/test/java/org/mandarin/booking/NoRestDocs.java (100%) rename {src => application/src}/test/java/org/mandarin/booking/TestConfig.java (100%) rename {src => application/src}/test/java/org/mandarin/booking/TestResult.java (95%) rename {src => application/src}/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java (100%) rename {src => application/src}/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java (90%) create mode 100644 application/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java rename {src => application/src}/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java (96%) create mode 100644 application/src/test/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverterTest.java rename {src => application/src}/test/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandlerTest.java (100%) create mode 100644 application/src/test/java/org/mandarin/booking/app/LoggingAspectTest.java rename {src => application/src}/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java (100%) rename {src => application/src}/test/java/org/mandarin/booking/fixture/MemberFixture.java (100%) rename {src => application/src}/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java (99%) rename {src => application/src}/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java (100%) rename {src => application/src}/test/java/org/mandarin/booking/webapi/member/POST_specs.java (100%) rename {src => application/src}/test/java/org/mandarin/booking/webapi/show/POST_specs.java (99%) rename {src => application/src}/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java (95%) create mode 100644 domain/build.gradle rename {src => domain/src}/main/java/org/mandarin/booking/domain/AbstractEntity.java (91%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/DomainException.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/EnumRequest.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/EnumRequestValidator.java (94%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/member/AuthException.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/member/AuthRequest.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/member/Member.java (88%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/member/MemberAuthority.java (56%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/member/MemberAuthorityConverter.java (94%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/member/MemberException.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/member/MemberRegisterRequest.java (90%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/member/MemberRegisterResponse.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/member/ReissueRequest.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java (67%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/member/TokenHolder.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/member/package-info.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/package-info.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/show/Show.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/show/ShowException.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/show/ShowRegisterRequest.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/show/ShowRegisterResponse.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/show/ShowSchedule.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/show/ShowScheduleCreateCommand.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/show/ShowScheduleRegisterRequest.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/show/ShowScheduleRegisterResponse.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/show/package-info.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/venue/Hall.java (100%) rename {src => domain/src}/main/java/org/mandarin/booking/domain/venue/HallException.java (100%) rename {src => domain/src}/test/java/org/mandarin/booking/domain/AbstractEntityTest.java (99%) rename {src => domain/src}/test/java/org/mandarin/booking/domain/MemberTest.java (100%) delete mode 100644 src/main/java/org/mandarin/booking/app/LoggingAspect.java delete mode 100644 src/main/java/org/mandarin/booking/domain/member/MemberDetails.java delete mode 100644 src/test/java/org/mandarin/booking/BookingApplicationTests.java delete mode 100644 src/test/java/org/mandarin/booking/TestOnlyController.java delete mode 100644 src/test/java/org/mandarin/booking/app/LoggingAspectTest.java delete mode 100644 src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/README.md b/README.md index c4bde25..fd8a942 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,9 @@ Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profile --- ## 11. 버전/도구 근거 링크 -- Spring Boot/Java/Gradle 버전: [build.gradle](build.gradle), [gradle-wrapper.properties](gradle/wrapper/gradle-wrapper.properties) + +- Spring Boot/Java/Gradle + 버전: [build.gradle](application/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) diff --git a/application/build.gradle b/application/build.gradle new file mode 100644 index 0000000..428eb0f --- /dev/null +++ b/application/build.gradle @@ -0,0 +1,73 @@ +plugins { + id 'com.epages.restdocs-api-spec' version '0.18.2' + id 'org.asciidoctor.jvm.convert' version '3.3.2' +} + +configurations { + byteBuddyAgent + asciidoctorExt +} + +ext { + set('snippetsDir', file('build/generated-snippets')) +} + +dependencies { + api(project(':domain')) + // ---- 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-aop' + + // ---- Data & Database ---- + implementation 'com.mysql:mysql-connector-j:8.3.0' + runtimeOnly 'com.h2database:h2' + 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' + + // ---- Dev Only ---- + developmentOnly 'org.springframework.boot:spring-boot-docker-compose' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + + // ---- Testing ---- + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' + byteBuddyAgent 'net.bytebuddy:byte-buddy-agent:1.17.6' + testImplementation 'org.mockito:mockito-core:5.19.0' + + + // ---- API Docs (REST Docs + Rest Assured) ---- + testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' + + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:3.1.0' +} + +asciidoctor { + group = 'documentation' + description = 'Converts AsciiDoc files to HTML, including REST Docs snippets.' + dependsOn test + baseDirFollowsSourceDir() + sourceDir = file('src/docs') + sources { + include '**/*.adoc' + } + inputs.dir snippetsDir + attributes 'snippets': snippetsDir + resources { + from(snippetsDir) { into 'snippets' } + } + outputDir = layout.buildDirectory.dir("docs/asciidoc").get().asFile +} + +tasks.named('test') { + jvmArgs "-javaagent:${configurations.byteBuddyAgent.singleFile}" + outputs.dir snippetsDir +} diff --git a/application/docs/index.adoc b/application/docs/index.adoc new file mode 100644 index 0000000..4312d24 --- /dev/null +++ b/application/docs/index.adoc @@ -0,0 +1,28 @@ += Booking API Documentation +:toc: left +:toclevels: 2 +:sectanchors: +:sectnums: +:source-highlighter: highlightjs + +This is the generated API documentation using Spring REST Docs and Asciidoctor. + +== Authentication + +include::{snippets}/post-api-auth-login/http-request.adoc[] +include::{snippets}/post-api-auth-login/http-response.adoc[] + +== Reissue Token + +include::{snippets}/post-api-auth-reissue/http-request.adoc[] +include::{snippets}/post-api-auth-reissue/http-response.adoc[] + +== Member Registration + +include::{snippets}/post-api-member/http-request.adoc[] +include::{snippets}/post-api-member/http-response.adoc[] + +== Show Registration + +include::{snippets}/post-api-show/http-request.adoc[] +include::{snippets}/post-api-show/http-response.adoc[] diff --git a/application/docs/index.html b/application/docs/index.html new file mode 100644 index 0000000..4823abf --- /dev/null +++ b/application/docs/index.html @@ -0,0 +1,2567 @@ + + + + + + + + Booking API Documentation + + + + + + + +
+
+
+
+

This is the generated API documentation using Spring REST Docs and Asciidoctor.

+
+
+
+
+

1. Authentication

+
+
+
+
POST /api/auth/login HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Host: localhost:60312
+Content-Length: 110
+
+{
+  "userId" : "922a834b-52ca-4e10-9ca8-9b851d8f558a",
+  "password" : "d92f16da-ad24-4ca2-97bf-094c460a1794"
+}
+
+
+
+
+
HTTP/1.1 200 OK
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 0
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Type: application/json
+Transfer-Encoding: chunked
+Date: Fri, 05 Sep 2025 07:14:22 GMT
+Keep-Alive: timeout=60
+Connection: keep-alive
+Content-Length: 131
+
+{
+  "status" : "UNAUTHORIZED",
+  "message" : "잘못된 userID 또는 비밀번호",
+  "timestamp" : "2025-09-05T16:14:22.994152"
+}
+
+
+
+
+
+

2. Reissue Token

+
+
+
+
POST /api/auth/reissue HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Host: localhost:60365
+Content-Length: 413
+
+{
+  "refreshToken" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJiNzJhNDU4Ny1kNTVmLTQ1ZTEtOThjOC1kOWRjNTlmZDI0NTciLCJyb2xlcyI6IlJPTEVfVVNFUiIsIm5pY2tOYW1lIjoiMjhmOGQ5MmQtMDdkZi00YWVkLWFkZTktMTc0ZWRhZjQ0ODJiIiwidXNlcklkIjoiYjcyYTQ1ODctZDU1Zi00NWUxLTk4YzgtZDlkYzU5ZmQyNDU3IiwiaWF0IjoxNzU3MDU2NDYzLCJleHAiOjE3NTcwNTY0NjN9.DLMSqhiLIIB_IJrcO-akHmy_siNHtPjWCvexuo_-oOVrpsNRNjBR3VRlFl0hcqpockupX8q1Tm34zpIyV-T32g"
+}
+
+
+
+
+
HTTP/1.1 200 OK
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 0
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Type: application/json
+Transfer-Encoding: chunked
+Date: Fri, 05 Sep 2025 07:14:23 GMT
+Keep-Alive: timeout=60
+Connection: keep-alive
+Content-Length: 131
+
+{
+  "status" : "UNAUTHORIZED",
+  "message" : "토큰 검증에 실패했습니다.",
+  "timestamp" : "2025-09-05T16:14:23.944676"
+}
+
+
+
+
+
+

3. Member Registration

+
+
+
+
POST /api/member HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Content-Type: application/json
+Host: localhost:60312
+Content-Length: 193
+
+{
+  "nickName" : "b214e075-e391-4f44-8f42-ea7e16dabcf4",
+  "userId" : "id",
+  "password" : "ac334c81-c0bc-4812-bc87-13660da6e095",
+  "email" : "9425394d-4517-48df-803e-283cf71afda9@gmail.com"
+}
+
+
+
+
+
HTTP/1.1 200 OK
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 0
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Type: application/json
+Transfer-Encoding: chunked
+Date: Fri, 05 Sep 2025 07:14:24 GMT
+Keep-Alive: timeout=60
+Connection: keep-alive
+Content-Length: 143
+
+{
+  "status" : "INTERNAL_SERVER_ERROR",
+  "message" : "이미 존재하는 회원입니다: id",
+  "timestamp" : "2025-09-05T16:14:24.646164"
+}
+
+
+
+
+
+

4. Show Registration

+
+
+
+
POST /api/show HTTP/1.1
+Accept: application/json, application/javascript, text/javascript, text/json
+Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIwYjhlODRkNC0wNmRjLTRjNDYtYmU3Yy1jZGJmNzkxZGM3NmEiLCJyb2xlcyI6IlJPTEVfQURNSU4iLCJuaWNrTmFtZSI6IjhlYjkwNDhlLWNlNjAtNGJmMy1iNDlkLWJjZmFkZjUyZWI5NyIsInVzZXJJZCI6IjBiOGU4NGQ0LTA2ZGMtNGM0Ni1iZTdjLWNkYmY3OTFkYzc2YSIsImlhdCI6MTc1NzA1NjQ2NSwiZXhwIjoxNzU3MDU3MDY1fQ.qQGd6qo4xb3mHEl95T0j5KJ2LBC24kmZhIxz1ve70MwMQJCtqlYi50eoApQqxgMA_CfFj4NKvl3fl2M4vh8v0g
+Content-Type: application/json
+Host: localhost:60312
+Content-Length: 240
+
+{
+  "title" : "공연 제목",
+  "type" : "MUSICAL",
+  "rating" : "AGE12",
+  "synopsis" : "공연 줄거리",
+  "posterUrl" : "https://example.com/poster.jpg",
+  "performanceStartDate" : "2025-09-05",
+  "performanceEndDate" : "2025-09-04"
+}
+
+
+
+
+
HTTP/1.1 200 OK
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 0
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Type: application/json
+Transfer-Encoding: chunked
+Date: Fri, 05 Sep 2025 07:14:25 GMT
+Keep-Alive: timeout=60
+Connection: keep-alive
+Content-Length: 176
+
+{
+  "status" : "INTERNAL_SERVER_ERROR",
+  "message" : "공연 시작 날짜는 종료 날짜 이후에 있을 수 없습니다.",
+  "timestamp" : "2025-09-05T16:14:25.757063"
+}
+
+
+
+
+
+ + + + + + diff --git a/src/main/java/org/mandarin/booking/BookingApplication.java b/application/src/main/java/org/mandarin/booking/BookingApplication.java similarity index 100% rename from src/main/java/org/mandarin/booking/BookingApplication.java rename to application/src/main/java/org/mandarin/booking/BookingApplication.java diff --git a/src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java b/application/src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java similarity index 100% rename from src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java rename to application/src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java diff --git a/src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java b/application/src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java similarity index 100% rename from src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java rename to application/src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java diff --git a/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java b/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java similarity index 78% rename from src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java rename to application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java index 83df390..c1a85e9 100644 --- a/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java +++ b/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java @@ -22,16 +22,17 @@ public void commence(HttpServletRequest request, HttpServletResponse response, response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); - var exception = (Exception)(request.getAttribute("exception")); - var message = getMessage(authException, exception); + var message = getMessage(request, authException); + var errorResponse = new ErrorResponse(ApiStatus.UNAUTHORIZED, message); objectMapper.writeValue(response.getWriter(), errorResponse); } - private String getMessage(AuthenticationException authException, Exception exception) { - if (exception != null) { - return exception.getMessage(); + private static String getMessage(HttpServletRequest request, AuthenticationException authException) { + if (request.getAttribute("exception") == null) { + return authException.getMessage(); } - return authException.getMessage(); + return request.getAttribute("exception").toString(); } + } diff --git a/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java b/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java similarity index 85% rename from src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java rename to application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java index 0da27ce..a91802f 100644 --- a/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java +++ b/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java @@ -4,10 +4,10 @@ import org.mandarin.booking.app.persist.MemberQueryRepository; import org.mandarin.booking.domain.member.AuthException; import org.mandarin.booking.domain.member.Member; -import org.mandarin.booking.domain.member.MemberDetails; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.User; import org.springframework.stereotype.Component; @Component @@ -34,7 +34,11 @@ public Authentication authenticate(Authentication authentication) throws Authent } private void specifyToken(CustomMemberAuthenticationToken token, Member member) { - MemberDetails details = MemberDetails.from(member); + var details = User.builder() + .username(member.getUserId()) + .authorities(member.getParsedAuthorities()) + .password(member.getPasswordHash()) + .build(); token.setDetails(details);// set user details token.setAuthenticated(true); } diff --git a/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java b/application/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java similarity index 70% rename from src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java rename to application/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java index ac1f9d8..f39bb35 100644 --- a/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java +++ b/application/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java @@ -1,14 +1,18 @@ package org.mandarin.booking.adapter.security; import java.util.Collection; +import org.jspecify.annotations.NullUnmarked; +import org.mandarin.booking.domain.member.MemberAuthority; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; public class CustomMemberAuthenticationToken extends AbstractAuthenticationToken { private final String userId; - public CustomMemberAuthenticationToken(String userId, Collection authorities) { - super(authorities); + public CustomMemberAuthenticationToken(String userId, Collection authorities) { + super(authorities.stream() + .map(authority -> (GrantedAuthority) authority::getAuthority) + .toList()); this.userId = userId; super.setAuthenticated(true); } @@ -19,6 +23,7 @@ public String getName() { } @Override + @NullUnmarked public Object getCredentials() { return null; } diff --git a/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java b/application/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java similarity index 96% rename from src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java rename to application/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java index cc67cd3..38556eb 100644 --- a/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java +++ b/application/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java @@ -8,6 +8,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.jspecify.annotations.Nullable; import org.mandarin.booking.app.TokenUtils; import org.mandarin.booking.domain.member.AuthException; import org.mandarin.booking.domain.member.MemberAuthority; @@ -67,7 +68,7 @@ private List getAuthorities(String token) { .map(MemberAuthority::valueOf).toList(); } - private boolean isTokenBlank(String header) { + private boolean isTokenBlank(@Nullable String header) { return header == null || header.equals("Bearer"); } diff --git a/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java b/application/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java similarity index 100% rename from src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java rename to application/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java diff --git a/application/src/main/java/org/mandarin/booking/adapter/security/package-info.java b/application/src/main/java/org/mandarin/booking/adapter/security/package-info.java new file mode 100644 index 0000000..b212696 --- /dev/null +++ b/application/src/main/java/org/mandarin/booking/adapter/security/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.mandarin.booking.adapter.security; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java similarity index 84% rename from src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java index bcf8995..ea67ea5 100644 --- a/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java @@ -3,9 +3,11 @@ import java.time.LocalDateTime; import lombok.Getter; import lombok.NoArgsConstructor; +import org.jspecify.annotations.NullUnmarked; @Getter @NoArgsConstructor +@NullUnmarked public abstract class ApiResponse { private final LocalDateTime timestamp = LocalDateTime.now(); protected ApiStatus status; diff --git a/src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java similarity index 100% rename from src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java diff --git a/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java similarity index 100% rename from src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java diff --git a/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java similarity index 90% rename from src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java index 4e57897..6230360 100644 --- a/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.nio.charset.StandardCharsets; +import org.jspecify.annotations.Nullable; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; @@ -45,11 +46,8 @@ protected void writeInternal(final Object objectApiResponse, final HttpOutputMes } @Override - protected void addDefaultHeaders(HttpHeaders headers, Object objectApiResponse, MediaType contentType) { - try { - super.addDefaultHeaders(headers, objectApiResponse, contentType); - } catch (IOException e) { - throw new RuntimeException(e); - } + protected void addDefaultHeaders(HttpHeaders headers, Object objectApiResponse, @Nullable MediaType contentType) + throws IOException { + super.addDefaultHeaders(headers, objectApiResponse, contentType); } } diff --git a/src/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java similarity index 100% rename from src/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java diff --git a/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java similarity index 89% rename from src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java index 0892a64..90bd511 100644 --- a/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java @@ -31,8 +31,8 @@ public ErrorResponse handleAuthException(AuthException ex) { @ExceptionHandler(MethodArgumentNotValidException.class) public ErrorResponse handleValidationException(MethodArgumentNotValidException ex) { - return new ErrorResponse(BAD_REQUEST, - requireNonNull(ex.getBindingResult().getFieldError()).getDefaultMessage()); + var defaultMessage = requireNonNull(ex.getBindingResult().getFieldError()).getDefaultMessage(); + return new ErrorResponse(BAD_REQUEST, requireNonNull(defaultMessage)); } @ExceptionHandler(NoHandlerFoundException.class) diff --git a/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java similarity index 100% rename from src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java index 7483a15..948a272 100644 --- a/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java @@ -1,9 +1,9 @@ package org.mandarin.booking.adapter.webapi; import jakarta.validation.Valid; -import org.mandarin.booking.domain.member.MemberRegisterResponse; import org.mandarin.booking.app.port.MemberRegisterer; import org.mandarin.booking.domain.member.MemberRegisterRequest; +import org.mandarin.booking.domain.member.MemberRegisterResponse; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java similarity index 90% rename from src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java index 270fbb1..7339c0c 100644 --- a/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java @@ -1,5 +1,6 @@ package org.mandarin.booking.adapter.webapi; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; @@ -18,7 +19,7 @@ public boolean supports(final MethodParameter returnType, } @Override - public Object beforeBodyWrite(final Object body, final MethodParameter returnType, + public Object beforeBodyWrite(@Nullable final Object body, final MethodParameter returnType, final MediaType selectedContentType, final Class> selectedConverterType, final ServerHttpRequest request, final ServerHttpResponse response) { diff --git a/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java similarity index 100% rename from src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java diff --git a/src/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java similarity index 100% rename from src/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/package-info.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/package-info.java new file mode 100644 index 0000000..c8fa41a --- /dev/null +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.mandarin.booking.adapter.webapi; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/mandarin/booking/app/AuthService.java b/application/src/main/java/org/mandarin/booking/app/AuthService.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/AuthService.java rename to application/src/main/java/org/mandarin/booking/app/AuthService.java index ae122a7..f66d526 100644 --- a/src/main/java/org/mandarin/booking/app/AuthService.java +++ b/application/src/main/java/org/mandarin/booking/app/AuthService.java @@ -2,11 +2,11 @@ import lombok.RequiredArgsConstructor; import org.mandarin.booking.app.persist.MemberQueryRepository; -import org.mandarin.booking.domain.member.SecurePasswordEncoder; -import org.mandarin.booking.domain.member.TokenHolder; import org.mandarin.booking.app.port.AuthUseCase; import org.mandarin.booking.domain.member.AuthException; import org.mandarin.booking.domain.member.Member; +import org.mandarin.booking.domain.member.SecurePasswordEncoder; +import org.mandarin.booking.domain.member.TokenHolder; import org.springframework.stereotype.Service; @Service diff --git a/src/main/java/org/mandarin/booking/app/BCryptSecurePasswordEncoder.java b/application/src/main/java/org/mandarin/booking/app/BCryptSecurePasswordEncoder.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/BCryptSecurePasswordEncoder.java rename to application/src/main/java/org/mandarin/booking/app/BCryptSecurePasswordEncoder.java diff --git a/src/main/java/org/mandarin/booking/app/HallExistCheckEvent.java b/application/src/main/java/org/mandarin/booking/app/HallExistCheckEvent.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/HallExistCheckEvent.java rename to application/src/main/java/org/mandarin/booking/app/HallExistCheckEvent.java diff --git a/src/main/java/org/mandarin/booking/app/HallService.java b/application/src/main/java/org/mandarin/booking/app/HallService.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/HallService.java rename to application/src/main/java/org/mandarin/booking/app/HallService.java diff --git a/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java b/application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java similarity index 90% rename from src/main/java/org/mandarin/booking/app/JwtTokenUtils.java rename to application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java index 4874b77..bd846d3 100644 --- a/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java +++ b/application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java @@ -14,12 +14,12 @@ import java.util.Map; import java.util.stream.Collectors; import javax.crypto.SecretKey; +import org.jspecify.annotations.Nullable; import org.mandarin.booking.domain.member.AuthException; import org.mandarin.booking.domain.member.MemberAuthority; import org.mandarin.booking.domain.member.TokenHolder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; @Component @@ -34,6 +34,7 @@ public class JwtTokenUtils implements TokenUtils { @Value("${jwt.token.refresh}") private long refreshTokenExp; + @Nullable private SecretKey key; @Autowired @@ -43,7 +44,7 @@ public void setKey(@Value("${jwt.token.secret}") String secretKey) { @Override public TokenHolder generateToken(String userId, String nickName, - Collection authorities) { + Collection authorities) { String accessToken = generateTokenInternal(userId, nickName, authorities, accessTokenExp); String refreshToken = generateTokenInternal(userId, nickName, authorities, refreshTokenExp); return new TokenHolder(accessToken, refreshToken); @@ -78,7 +79,7 @@ public Collection getClaims(String token, String claimName) { } private String generateTokenInternal(String userId, String nickName, - Collection authorities, long expiration) { + Collection authorities, long expiration) { long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); Date exp = new Date(nowMillis + expiration); @@ -90,7 +91,7 @@ private String generateTokenInternal(String userId, String nickName, .claims(Map.of( USER_ID, userId, NICK_NAME, nickName, - ROLES, authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")) + ROLES, authorities.stream().map(MemberAuthority::getAuthority).collect(Collectors.joining(",")) )) .issuedAt(now) .expiration(exp) diff --git a/src/main/java/org/mandarin/booking/app/Log.java b/application/src/main/java/org/mandarin/booking/app/Log.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/Log.java rename to application/src/main/java/org/mandarin/booking/app/Log.java diff --git a/application/src/main/java/org/mandarin/booking/app/LoggingAspect.java b/application/src/main/java/org/mandarin/booking/app/LoggingAspect.java new file mode 100644 index 0000000..901bfd9 --- /dev/null +++ b/application/src/main/java/org/mandarin/booking/app/LoggingAspect.java @@ -0,0 +1,118 @@ +package org.mandarin.booking.app; + +import java.lang.reflect.Method; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class LoggingAspect { + + @Around("@within(org.mandarin.booking.app.Log) || @annotation(org.mandarin.booking.app.Log)") + public Object around(ProceedingJoinPoint jp) throws Throwable { + Logger logger = selectTargetLogger(jp); + String level = resolveScope(jp); + + LocalDateTime startAt = LocalDateTime.now(); + long startNs = System.nanoTime(); + String sig = jp.getSignature().toLongString(); + + logAtLevel(logger, level, "START {} at {}", sig, formatTime(startAt)); + Object ret = jp.proceed(); + long elapsedMs = nanosToMillis(System.nanoTime() - startNs); + LocalDateTime endAt = LocalDateTime.now(); + logAtLevel(logger, level, "END {} at {} ({} ms) status={}", sig, formatTime(endAt), elapsedMs, "SUCCESS"); + return ret; + } + + @AfterThrowing( + argNames = "pjp,ex", + pointcut = "@within(org.mandarin.booking.app.Log) || @annotation(org.mandarin.booking.app.Log)", + throwing = "ex" + ) + public void afterThrowing(JoinPoint pjp, Throwable ex) { + Logger logger = selectTargetLogger(pjp); + String sig = pjp.getSignature().toLongString(); + if (logger.isErrorEnabled()) { + logger.error("END {} status=FAIL cause={}", sig, ex.toString()); + } + } + + private Logger selectTargetLogger(JoinPoint joinPoint) { + Class targetClass = getTargetClass(joinPoint.getTarget()); + return LoggerFactory.getLogger(targetClass); + } + + private String resolveScope(ProceedingJoinPoint joinPoint) { + MethodSignature ms = (MethodSignature) joinPoint.getSignature(); + Method method = ms.getMethod(); + Class targetClass = getTargetClass(joinPoint.getTarget()); + try { + Method targetMethod = targetClass.getMethod(method.getName(), method.getParameterTypes()); + Log m = targetMethod.getAnnotation(Log.class); + if (m != null && !m.scope().isBlank()) { + return m.scope(); + } + } catch (NoSuchMethodException ignored) { + } + Log c = targetClass.getAnnotation(Log.class); + String scope = c != null ? c.scope() : "INFO"; + return scope.isBlank() ? "INFO" : scope; + } + + private void logAtLevel(Logger logger, String level, String message, Object... args) { + String up = level.trim().toUpperCase(); + switch (up) { + case "TRACE" -> { + if (logger.isTraceEnabled()) { + logger.trace(message, args); + } + } + case "DEBUG" -> { + if (logger.isDebugEnabled()) { + logger.debug(message, args); + } + } + case "WARN" -> { + if (logger.isWarnEnabled()) { + logger.warn(message, args); + } + } + case "ERROR" -> { + if (logger.isErrorEnabled()) { + logger.error(message, args); + } + } + default -> { + if (logger.isInfoEnabled()) { + logger.info(message, args); + } + } + } + } + + private static Class getTargetClass(Object target) { + return target.getClass(); + } + + private static long nanosToMillis(long ns) { + return ns / 1_000_000L; + } + + private static String formatTime(LocalDateTime time) { + return time.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } +} diff --git a/src/main/java/org/mandarin/booking/app/MemberRegisterValidator.java b/application/src/main/java/org/mandarin/booking/app/MemberRegisterValidator.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/MemberRegisterValidator.java rename to application/src/main/java/org/mandarin/booking/app/MemberRegisterValidator.java diff --git a/src/main/java/org/mandarin/booking/app/MemberService.java b/application/src/main/java/org/mandarin/booking/app/MemberService.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/MemberService.java rename to application/src/main/java/org/mandarin/booking/app/MemberService.java index 2322b34..ba5c9ce 100644 --- a/src/main/java/org/mandarin/booking/app/MemberService.java +++ b/application/src/main/java/org/mandarin/booking/app/MemberService.java @@ -2,12 +2,12 @@ import lombok.RequiredArgsConstructor; import org.mandarin.booking.app.persist.MemberCommandRepository; -import org.mandarin.booking.domain.member.SecurePasswordEncoder; -import org.mandarin.booking.domain.member.MemberRegisterRequest; -import org.mandarin.booking.domain.member.MemberRegisterResponse; import org.mandarin.booking.app.port.MemberRegisterer; import org.mandarin.booking.domain.member.Member; import org.mandarin.booking.domain.member.Member.MemberCreateCommand; +import org.mandarin.booking.domain.member.MemberRegisterRequest; +import org.mandarin.booking.domain.member.MemberRegisterResponse; +import org.mandarin.booking.domain.member.SecurePasswordEncoder; import org.springframework.stereotype.Service; @Service diff --git a/src/main/java/org/mandarin/booking/app/QuerydslConfig.java b/application/src/main/java/org/mandarin/booking/app/QuerydslConfig.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/QuerydslConfig.java rename to application/src/main/java/org/mandarin/booking/app/QuerydslConfig.java diff --git a/src/main/java/org/mandarin/booking/app/ShowService.java b/application/src/main/java/org/mandarin/booking/app/ShowService.java similarity index 93% rename from src/main/java/org/mandarin/booking/app/ShowService.java rename to application/src/main/java/org/mandarin/booking/app/ShowService.java index efc3c53..98bad9a 100644 --- a/src/main/java/org/mandarin/booking/app/ShowService.java +++ b/application/src/main/java/org/mandarin/booking/app/ShowService.java @@ -1,5 +1,7 @@ package org.mandarin.booking.app; +import static java.util.Objects.requireNonNull; + import lombok.RequiredArgsConstructor; import org.mandarin.booking.app.persist.ShowCommandRepository; import org.mandarin.booking.app.persist.ShowQueryRepository; @@ -31,7 +33,7 @@ public ShowRegisterResponse register(ShowRegisterRequest request) { checkDuplicateTitle(show.getTitle()); var saved = commandRepository.insert(show); - return new ShowRegisterResponse(saved.getId()); + return new ShowRegisterResponse(requireNonNull(saved.getId())); } @Override @@ -46,7 +48,7 @@ public ShowScheduleRegisterResponse registerSchedule(ShowScheduleRegisterRequest show.registerSchedule(hallId, command); var saved = commandRepository.insert(show); - return new ShowScheduleRegisterResponse(saved.getId()); + return new ShowScheduleRegisterResponse(requireNonNull(saved.getId())); } private void checkDuplicateTitle(String title) { diff --git a/src/main/java/org/mandarin/booking/app/TokenUtils.java b/application/src/main/java/org/mandarin/booking/app/TokenUtils.java similarity index 78% rename from src/main/java/org/mandarin/booking/app/TokenUtils.java rename to application/src/main/java/org/mandarin/booking/app/TokenUtils.java index 31a7a3d..368a93c 100644 --- a/src/main/java/org/mandarin/booking/app/TokenUtils.java +++ b/application/src/main/java/org/mandarin/booking/app/TokenUtils.java @@ -1,13 +1,13 @@ package org.mandarin.booking.app; import java.util.Collection; +import org.mandarin.booking.domain.member.MemberAuthority; import org.mandarin.booking.domain.member.TokenHolder; -import org.springframework.security.core.GrantedAuthority; public interface TokenUtils { TokenHolder generateToken(String refreshToken); - TokenHolder generateToken(String userId, String nickName, Collection authorities); + TokenHolder generateToken(String userId, String nickName, Collection authorities); String getClaim(String token, String claimName); diff --git a/src/main/java/org/mandarin/booking/app/package-info.java b/application/src/main/java/org/mandarin/booking/app/package-info.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/package-info.java rename to application/src/main/java/org/mandarin/booking/app/package-info.java diff --git a/src/main/java/org/mandarin/booking/app/persist/HallCommandRepository.java b/application/src/main/java/org/mandarin/booking/app/persist/HallCommandRepository.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/persist/HallCommandRepository.java rename to application/src/main/java/org/mandarin/booking/app/persist/HallCommandRepository.java diff --git a/src/main/java/org/mandarin/booking/app/persist/HallQueryRepository.java b/application/src/main/java/org/mandarin/booking/app/persist/HallQueryRepository.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/persist/HallQueryRepository.java rename to application/src/main/java/org/mandarin/booking/app/persist/HallQueryRepository.java diff --git a/src/main/java/org/mandarin/booking/app/persist/HallRepository.java b/application/src/main/java/org/mandarin/booking/app/persist/HallRepository.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/persist/HallRepository.java rename to application/src/main/java/org/mandarin/booking/app/persist/HallRepository.java diff --git a/src/main/java/org/mandarin/booking/app/persist/MemberCommandRepository.java b/application/src/main/java/org/mandarin/booking/app/persist/MemberCommandRepository.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/persist/MemberCommandRepository.java rename to application/src/main/java/org/mandarin/booking/app/persist/MemberCommandRepository.java diff --git a/src/main/java/org/mandarin/booking/app/persist/MemberQueryRepository.java b/application/src/main/java/org/mandarin/booking/app/persist/MemberQueryRepository.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/persist/MemberQueryRepository.java rename to application/src/main/java/org/mandarin/booking/app/persist/MemberQueryRepository.java diff --git a/src/main/java/org/mandarin/booking/app/persist/MemberRepository.java b/application/src/main/java/org/mandarin/booking/app/persist/MemberRepository.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/persist/MemberRepository.java rename to application/src/main/java/org/mandarin/booking/app/persist/MemberRepository.java diff --git a/src/main/java/org/mandarin/booking/app/persist/ShowCommandRepository.java b/application/src/main/java/org/mandarin/booking/app/persist/ShowCommandRepository.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/persist/ShowCommandRepository.java rename to application/src/main/java/org/mandarin/booking/app/persist/ShowCommandRepository.java diff --git a/src/main/java/org/mandarin/booking/app/persist/ShowQueryRepository.java b/application/src/main/java/org/mandarin/booking/app/persist/ShowQueryRepository.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/persist/ShowQueryRepository.java rename to application/src/main/java/org/mandarin/booking/app/persist/ShowQueryRepository.java diff --git a/src/main/java/org/mandarin/booking/app/persist/ShowRepository.java b/application/src/main/java/org/mandarin/booking/app/persist/ShowRepository.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/persist/ShowRepository.java rename to application/src/main/java/org/mandarin/booking/app/persist/ShowRepository.java diff --git a/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java b/application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/port/AuthUseCase.java rename to application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java diff --git a/src/main/java/org/mandarin/booking/app/port/MemberRegisterer.java b/application/src/main/java/org/mandarin/booking/app/port/MemberRegisterer.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/port/MemberRegisterer.java rename to application/src/main/java/org/mandarin/booking/app/port/MemberRegisterer.java diff --git a/src/main/java/org/mandarin/booking/app/port/ShowRegisterer.java b/application/src/main/java/org/mandarin/booking/app/port/ShowRegisterer.java similarity index 100% rename from src/main/java/org/mandarin/booking/app/port/ShowRegisterer.java rename to application/src/main/java/org/mandarin/booking/app/port/ShowRegisterer.java diff --git a/src/main/resources/application-local.yml b/application/src/main/resources/application-local.yml similarity index 100% rename from src/main/resources/application-local.yml rename to application/src/main/resources/application-local.yml diff --git a/src/main/resources/application-test.yml b/application/src/main/resources/application-test.yml similarity index 100% rename from src/main/resources/application-test.yml rename to application/src/main/resources/application-test.yml diff --git a/src/main/resources/application.yml b/application/src/main/resources/application.yml similarity index 100% rename from src/main/resources/application.yml rename to application/src/main/resources/application.yml diff --git a/application/src/test/java/org/mandarin/booking/BookingApplicationTests.java b/application/src/test/java/org/mandarin/booking/BookingApplicationTests.java new file mode 100644 index 0000000..16bc2fd --- /dev/null +++ b/application/src/test/java/org/mandarin/booking/BookingApplicationTests.java @@ -0,0 +1,20 @@ +package org.mandarin.booking; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BookingApplicationTests { + + @Test + void run() { + try (MockedStatic mocked = Mockito.mockStatic(SpringApplication.class)) { + BookingApplication.main(new String[0]); + mocked.verify(() -> SpringApplication.run(BookingApplication.class, new String[0])); + } + } + +} diff --git a/src/test/java/org/mandarin/booking/DocsUtils.java b/application/src/test/java/org/mandarin/booking/DocsUtils.java similarity index 100% rename from src/test/java/org/mandarin/booking/DocsUtils.java rename to application/src/test/java/org/mandarin/booking/DocsUtils.java diff --git a/src/test/java/org/mandarin/booking/IntegrationTest.java b/application/src/test/java/org/mandarin/booking/IntegrationTest.java similarity index 100% rename from src/test/java/org/mandarin/booking/IntegrationTest.java rename to application/src/test/java/org/mandarin/booking/IntegrationTest.java diff --git a/src/test/java/org/mandarin/booking/IntegrationTestUtils.java b/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java similarity index 97% rename from src/test/java/org/mandarin/booking/IntegrationTestUtils.java rename to application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java index fd844b0..0be317a 100644 --- a/src/test/java/org/mandarin/booking/IntegrationTestUtils.java +++ b/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java @@ -24,7 +24,6 @@ import org.mandarin.booking.domain.show.Show.ShowCreateCommand; import org.mandarin.booking.domain.show.ShowRegisterRequest; import org.mandarin.booking.domain.venue.Hall; -import org.springframework.security.core.GrantedAuthority; import org.springframework.test.util.ReflectionTestUtils; public record IntegrationTestUtils(MemberCommandRepository memberRepository, @@ -68,7 +67,7 @@ public String getAuthToken(Member member) { } public TokenHolder getUserToken(String userId, String nickname, - Collection authorities) { + Collection authorities) { return tokenUtils.generateToken(userId, nickname, authorities); } diff --git a/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java b/application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java similarity index 100% rename from src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java rename to application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java diff --git a/src/test/java/org/mandarin/booking/JwtTestUtils.java b/application/src/test/java/org/mandarin/booking/JwtTestUtils.java similarity index 100% rename from src/test/java/org/mandarin/booking/JwtTestUtils.java rename to application/src/test/java/org/mandarin/booking/JwtTestUtils.java diff --git a/src/test/java/org/mandarin/booking/NoRestDocs.java b/application/src/test/java/org/mandarin/booking/NoRestDocs.java similarity index 100% rename from src/test/java/org/mandarin/booking/NoRestDocs.java rename to application/src/test/java/org/mandarin/booking/NoRestDocs.java diff --git a/src/test/java/org/mandarin/booking/TestConfig.java b/application/src/test/java/org/mandarin/booking/TestConfig.java similarity index 100% rename from src/test/java/org/mandarin/booking/TestConfig.java rename to application/src/test/java/org/mandarin/booking/TestConfig.java diff --git a/src/test/java/org/mandarin/booking/TestResult.java b/application/src/test/java/org/mandarin/booking/TestResult.java similarity index 95% rename from src/test/java/org/mandarin/booking/TestResult.java rename to application/src/test/java/org/mandarin/booking/TestResult.java index 4bdc415..eeb978a 100644 --- a/src/test/java/org/mandarin/booking/TestResult.java +++ b/application/src/test/java/org/mandarin/booking/TestResult.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.HashMap; import java.util.Map; +import org.jspecify.annotations.NonNull; import org.mandarin.booking.adapter.webapi.ApiResponse; import org.mandarin.booking.adapter.webapi.ApiStatus; import org.mandarin.booking.adapter.webapi.ErrorResponse; @@ -65,13 +66,9 @@ public ErrorResponse assertFailure() { return response; } - public TestResult withHeader(String headerName, String headerValue) { - headers.put(headerName, headerValue); - return this; - } public TestResult withAuthorization(String token) { - this.withHeader("Authorization", token); + headers.put("Authorization", token); return this; } @@ -141,7 +138,7 @@ private ErrorResponse readErrorResponse() { } } - private SuccessResponse readSuccessResponse(String raw, TypeReference typeRef) { + private SuccessResponse<@NonNull T> readSuccessResponse(String raw, TypeReference typeRef) { try { var inner = objectMapper.getTypeFactory().constructType(typeRef); var wrapper = objectMapper.getTypeFactory().constructParametricType(SuccessResponse.class, inner); diff --git a/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java similarity index 100% rename from src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java rename to application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java diff --git a/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java similarity index 90% rename from src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java rename to application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java index 530bc62..a5e2377 100644 --- a/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java +++ b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java @@ -1,7 +1,8 @@ package org.mandarin.booking.adapter.security; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; import org.mandarin.booking.IntegrationTest; diff --git a/application/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java new file mode 100644 index 0000000..11e7142 --- /dev/null +++ b/application/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java @@ -0,0 +1,16 @@ +package org.mandarin.booking.adapter.security; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mandarin.booking.domain.member.MemberAuthority.USER; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class CustomMemberAuthenticationTokenTest { + + @Test + void getCredentials() { + var toke = new CustomMemberAuthenticationToken("user", List.of(USER)); + assertThat(toke.getCredentials()).isNull(); + } +} diff --git a/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java similarity index 96% rename from src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java rename to application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java index 4f49712..ad96c14 100644 --- a/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java +++ b/application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java @@ -74,7 +74,7 @@ void failToAuth(@Autowired IntegrationTestUtils testUtils) { .withAuthorization(invalidToken) .assertFailure(); assertThat(response.getStatus()).isEqualTo(UNAUTHORIZED); - assertThat(response.getData()).isEqualTo("유효한 토큰이 없습니다."); + assertThat(response.getData()).contains("유효한 토큰이 없습니다."); } @Test @@ -89,7 +89,7 @@ void failWithInvalidBearer(@Autowired IntegrationTestUtils testUtils) { // Assert assertThat(response.getStatus()).isEqualTo(UNAUTHORIZED); - assertThat(response.getData()).isEqualTo("유효한 토큰이 없습니다."); + assertThat(response.getData()).contains("유효한 토큰이 없습니다."); } @Test @@ -122,7 +122,7 @@ void blankTokenWillFailToAuth( // Assert assertThat(response.getStatus()).isEqualTo(UNAUTHORIZED); - assertThat(response.getData()).isEqualTo("토큰이 비어있습니다."); + assertThat(response.getData()).contains("토큰이 비어있습니다."); } @RestController diff --git a/application/src/test/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverterTest.java b/application/src/test/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverterTest.java new file mode 100644 index 0000000..8694a1f --- /dev/null +++ b/application/src/test/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverterTest.java @@ -0,0 +1,25 @@ +package org.mandarin.booking.adapter.webapi; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpInputMessage; + +@ExtendWith(MockitoExtension.class) +class CommonHttpMessageConverterTest { + @InjectMocks + CommonHttpMessageConverter converter; + + @Test + void readInternal_throwsUnsupportedOperation() { + HttpInputMessage msg = mock(HttpInputMessage.class); + assertThrows( + UnsupportedOperationException.class, + () -> converter.readInternal(ApiResponse.class, msg) + ); + } +} diff --git a/src/test/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandlerTest.java b/application/src/test/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandlerTest.java similarity index 100% rename from src/test/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandlerTest.java rename to application/src/test/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandlerTest.java diff --git a/application/src/test/java/org/mandarin/booking/app/LoggingAspectTest.java b/application/src/test/java/org/mandarin/booking/app/LoggingAspectTest.java new file mode 100644 index 0000000..941e84d --- /dev/null +++ b/application/src/test/java/org/mandarin/booking/app/LoggingAspectTest.java @@ -0,0 +1,615 @@ +package org.mandarin.booking.app; + +import static ch.qos.logback.classic.Level.DEBUG; +import static ch.qos.logback.classic.Level.ERROR; +import static ch.qos.logback.classic.Level.INFO; +import static ch.qos.logback.classic.Level.OFF; +import static ch.qos.logback.classic.Level.TRACE; +import static ch.qos.logback.classic.Level.WARN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mandarin.booking.IntegrationTest; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@IntegrationTest +@Import(LoggingAspectTest.TestConfig.class) +class LoggingAspectTest { + + @Autowired + SampleService bean; + @Autowired + BlankMethodOnlyService blankMethodOnlyService; + @Autowired + BlankClassScopeService blankClassScopeService; + @Autowired + WarnClassScopeService warnClassScopeService; + @Autowired + ErrorClassScopeService errorClassScopeService; + private ListAppender la; + + @BeforeEach + void setUp() { + Logger logger = (Logger) LoggerFactory.getLogger(SampleLoggedService.class); + la = new ListAppender<>(); + la.start(); + logger.addAppender(la); + logger.setLevel(TRACE); + } + + @AfterEach + void tearDown() { + Logger logger = (Logger) LoggerFactory.getLogger(SampleLoggedService.class); + logger.detachAppender(la); + } + + @Test + @DisplayName("Class-level @Log는 START/END를 scope 레벨에서 제공한다") + void classLevelLogStartEnd() { + SampleService s = bean; + String res = s.doWork(); + assertThat(res).isEqualTo("ok"); + + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.get(0).getLevel()).isEqualTo(DEBUG); + assertThat(events.get(0).getFormattedMessage()).contains("START").contains("doWork"); + assertThat(events.get(1).getLevel()).isEqualTo(DEBUG); + assertThat(events.get(1).getFormattedMessage()).contains("END").contains("("); + } + + @Test + @DisplayName("Method-level @Log는 클래스 수준을 우선합니다") + void methodLevelOverrides() { + SampleService s = bean; + String res = s.doTraced(); + assertThat(res).isEqualTo("traced"); + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.getFirst().getLevel()).isEqualTo(TRACE); + assertThat(events.getFirst().getFormattedMessage()).contains("START").contains("doTraced"); + } + + @Test + @DisplayName("예외적으로 END는 예외 로그로 남는다") + void exceptionLogging() { + SampleService s = bean; + assertThatThrownBy(s::fail).isInstanceOf(IllegalStateException.class); + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.get(1).getLevel()).isEqualTo(ERROR); + assertThat(events.get(1).getFormattedMessage()).contains("IllegalStateException"); + } + + @Test + @DisplayName("WARN scope는 WARN 로그를 출력한다") + void warnLevelMethod() { + SampleService s = bean; + String res = s.doWarn(); + assertThat(res).isEqualTo("warned"); + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.getFirst().getLevel()).isEqualTo(WARN); + assertThat(events.getFirst().getFormattedMessage()).contains("START").contains("doWarn"); + assertThat(events.get(1).getLevel()).isEqualTo(WARN); + assertThat(events.get(1).getFormattedMessage()).contains("END").contains("("); + } + + @Test + @DisplayName("ERROR scope인 경우 START/END가 기제된다") + void errorLevelMethodSuccessful() { + SampleService s = bean; + String res = s.doErrorLevel(); + assertThat(res).isEqualTo("erred"); + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.get(0).getLevel()).isEqualTo(ERROR); + assertThat(events.get(0).getFormattedMessage()).contains("START").contains("doErrorLevel"); + assertThat(events.get(1).getLevel()).isEqualTo(ERROR); + assertThat(events.get(1).getFormattedMessage()).contains("END").contains("("); + } + + @Test + @DisplayName("알수없는 scope인 경우 기본값인 INFO scope로 로깅된다") + void unknownScopeDefaultsToInfo() { + SampleService s = bean; + String res = s.doCustom(); + assertThat(res).isEqualTo("custom"); + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.getFirst().getLevel()).isEqualTo(INFO); + } + + @Test + @DisplayName("trim 검증") + void infoScopeCoversExplicitInfoCase() { + SampleService s = bean; + String res = s.doInfo(); + assertThat(res).isEqualTo("info"); + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.get(0).getLevel()).isEqualTo(INFO); + assertThat(events.get(0).getFormattedMessage()).contains("START").contains("doInfo"); + assertThat(events.get(1).getLevel()).isEqualTo(INFO); + assertThat(events.get(1).getFormattedMessage()).contains("END"); + } + + @Test + @DisplayName("기본 scope는 INFO") + void blankMethodScopeFallsBackToInfo() { + ListAppender la = attachAppenderForBlankMethodOnly(); + try { + String res = blankMethodOnlyService.blankOnly(); + assertThat(res).isEqualTo("blank"); + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.getFirst().getLevel()).isEqualTo(INFO); + } finally { + detachAppender(la); + } + } + + + @Test + @DisplayName("DEBUG scope일때 로그 레벨이 INFO인 경우 로그가 발생하지 않는다") + void debugScopeButLoggerAtInfoNoLogs() { + SampleService s = bean; + Logger logger = (Logger) LoggerFactory.getLogger(SampleLoggedService.class); + Level prev = logger.getLevel(); + try { + logger.setLevel(INFO); + la.list.clear(); + + String res = s.doWork(); + assertThat(res).isEqualTo("ok"); + + assertThat(la.list).isEmpty(); + } finally { + logger.setLevel(prev); + } + } + + @Test + @DisplayName("로거 레벨이 TRACE scope인 경우 INFO 로그가 없다") + void traceScopeButLoggerAtInfoNoLogs() { + SampleService s = bean; + Logger logger = (Logger) LoggerFactory.getLogger(SampleLoggedService.class); + Level prev = logger.getLevel(); + try { + logger.setLevel(INFO); + la.list.clear(); + + String res = s.doTraced(); + assertThat(res).isEqualTo("traced"); + + assertThat(la.list).isEmpty(); + } finally { + logger.setLevel(prev); + } + } + + @Test + @DisplayName("예외 로그 레벨이 꺼져 있으면 로그가 기록되지 않는다") + void exceptionLoggingSuppressedWhenErrorDisabled() { + SampleService s = bean; + Logger logger = (Logger) LoggerFactory.getLogger(SampleLoggedService.class); + Level prev = logger.getLevel(); + try { + logger.setLevel(OFF); + la.list.clear(); + + String res = s.doErrorLevel(); + + assertThat(res).isEqualTo("erred"); + assertThatThrownBy(s::fail).isInstanceOf(IllegalStateException.class); + + assertThat(la.list).isEmpty(); + } finally { + logger.setLevel(prev); + } + } + + @Test + @DisplayName("Class-level scope이 OFF면 로그가 없다") + void finallySuccessBranchRunsButNoLogWhenOff() { + SampleService s = bean; + Logger logger = (Logger) LoggerFactory.getLogger(SampleLoggedService.class); + Level prev = logger.getLevel(); + try { + logger.setLevel(OFF); + la.list.clear(); + + String res = s.doWork(); + assertThat(res).isEqualTo("ok"); + + assertThat(la.list).isEmpty(); + } finally { + logger.setLevel(prev); + } + } + + + @Test + @DisplayName("Class-level scope가 blank일 때 resolveScope는 INFO로 폴백되어 START/END가 INFO로 찍힌다") + void classBlankScopeFallsBackToInfoOnSuccess() { + ListAppender la = attachAppenderForBlankClass(); + try { + String res = blankClassScopeService.doOk(); + assertThat(res).isEqualTo("ok"); + + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.get(0).getLevel()).isEqualTo(INFO); + assertThat(events.get(0).getFormattedMessage()).contains("START").contains("doOk"); + assertThat(events.get(1).getLevel()).isEqualTo(INFO); + assertThat(events.get(1).getFormattedMessage()).contains("END"); + } finally { + detachAppenderForBlankClass(la); + } + } + + @Test + @DisplayName("proceed에서 예외 발생 시 START(INFO) 후 ERROR 로그가 남는다 (클래스 blank scope)") + void classBlankScopeProceedThrowsLogsError() { + ListAppender la = attachAppenderForBlankClass(); + try { + assertThatThrownBy(() -> blankClassScopeService.fail()) + .isInstanceOf(IllegalStateException.class); + + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.get(0).getLevel()).isEqualTo(INFO); + assertThat(events.get(0).getFormattedMessage()).contains("START").contains("fail"); + assertThat(events.get(1).getLevel()).isEqualTo(ERROR); + assertThat(events.get(1).getFormattedMessage()).contains("END").contains("IllegalStateException"); + } finally { + detachAppenderForBlankClass(la); + } + } + + @Test + @DisplayName("Class-level ERROR scope: START/END가 ERROR로 기록된다 (성공 경로)") + void classErrorScopeLogsAtErrorOnSuccess() { + ListAppender la = attachAppenderForErrorClass(); + try { + String res = errorClassScopeService.doOk(); + assertThat(res).isEqualTo("ok"); + + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.get(0).getLevel()).isEqualTo(ERROR); + assertThat(events.get(0).getFormattedMessage()).contains("START").contains("doOk"); + assertThat(events.get(1).getLevel()).isEqualTo(ERROR); + assertThat(events.get(1).getFormattedMessage()).contains("END"); + } finally { + detachAppenderForErrorClass(la); + } + } + + @Test + @DisplayName("Class-level ERROR scope: proceed 예외 시 ERROR 로그가 기록된다") + void classErrorScopeProceedThrowsLogsError() { + ListAppender la = attachAppenderForErrorClass(); + try { + assertThatThrownBy(() -> errorClassScopeService.fail()) + .isInstanceOf(IllegalStateException.class); + + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.get(0).getLevel()).isEqualTo(ERROR); + assertThat(events.get(0).getFormattedMessage()).contains("START").contains("fail"); + assertThat(events.get(1).getLevel()).isEqualTo(ERROR); + assertThat(events.get(1).getFormattedMessage()).contains("END").contains("IllegalStateException"); + } finally { + detachAppenderForErrorClass(la); + } + } + + @Test + @DisplayName("Class-level WARN scope: START/END가 WARN으로 기록된다") + void classWarnScopeLogsAtWarnOnSuccess() { + ListAppender la = attachAppenderForWarnClass(); + try { + String res = warnClassScopeService.doOk(); + assertThat(res).isEqualTo("ok"); + + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.get(0).getLevel()).isEqualTo(WARN); + assertThat(events.get(0).getFormattedMessage()).contains("START").contains("doOk"); + assertThat(events.get(1).getLevel()).isEqualTo(WARN); + assertThat(events.get(1).getFormattedMessage()).contains("END"); + } finally { + detachAppenderForWarnClass(la); + } + } + + @Test + @DisplayName("logAtLevel: WARN/ERROR 레벨 라우팅이 직접 검증된다") + void logAtLevelWarnAndErrorAreVerifiedDirectly() throws Exception { + Logger logger = (Logger) LoggerFactory.getLogger( + SampleLoggedService.class); + logger.setLevel(TRACE); + la.list.clear(); + var aspect = new LoggingAspect(); + + var m = LoggingAspect.class.getDeclaredMethod("logAtLevel", org.slf4j.Logger.class, String.class, String.class, + Object[].class); + m.setAccessible(true); + m.invoke(aspect, logger, "WARN", "warn {}", new Object[]{"x"}); + + assertThat(la.list).hasSize(1); + assertThat(la.list.getFirst().getLevel()).isEqualTo(WARN); + assertThat(la.list.getFirst().getFormattedMessage()).contains("warn x"); + + la.list.clear(); + m.invoke(aspect, logger, "ERROR", "error {}", new Object[]{"y"}); + + assertThat(la.list).hasSize(1); + assertThat(la.list.getFirst().getLevel()).isEqualTo(ERROR); + assertThat(la.list.getFirst().getFormattedMessage()).contains("error y"); + } + + @Test + @DisplayName("logAtLevel: WARN/INFO 비활성화 시 로그가 발생하지 않는다") + void logAtLevelNoWarnOrInfoWhenDisabled() throws Exception { + Logger logger = (Logger) LoggerFactory.getLogger(SampleLoggedService.class); + la.list.clear(); + var aspect = new LoggingAspect(); + var m = LoggingAspect.class.getDeclaredMethod("logAtLevel", + org.slf4j.Logger.class, + String.class, + String.class, + Object[].class); + m.setAccessible(true); + + logger.setLevel(ERROR); + m.invoke(aspect, logger, "WARN", "warn {}", new Object[]{"x"}); + assertThat(la.list).isEmpty(); + + logger.setLevel(WARN); + m.invoke(aspect, logger, "INFO", "info {}", new Object[]{"y"}); + assertThat(la.list).isEmpty(); + } + + @Test + @DisplayName("finally의 if(success)==false 분기가 명시적으로 검증된다 (END 로그 없음)") + void finallyIfSuccessFalseBranchExplicit() { + SampleService s = bean; + la.list.clear(); + + assertThatThrownBy(s::fail).isInstanceOf(IllegalStateException.class); + + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.get(0).getFormattedMessage()).contains("START"); + assertThat(events.get(1).getLevel()).isEqualTo(ERROR); + assertThat(events.get(1).getFormattedMessage()).contains("END").contains("IllegalStateException"); + } + + @Test + @DisplayName("정상 반환 시 finally의 if(success)==true 분기가 명시적으로 검증된다") + void catchJoinPointExceptionReturnErrorMessageWithFailStatus() { + SampleService s = bean; + la.list.clear(); + + assertThatThrownBy(s::fail).isInstanceOf(IllegalStateException.class); + List events = la.list; + assertThat(events).hasSize(2); + assertThat(events.get(0).getFormattedMessage()).contains("START"); + assertThat(events.get(1).getLevel()).isEqualTo(ERROR); + assertThat(events.get(1).getFormattedMessage()).contains("END").contains("IllegalStateException"); + } + + private ListAppender attachAppenderForBlankClass() { + Logger logger = (Logger) LoggerFactory.getLogger(BlankClassScopeServiceImpl.class); + ListAppender la = new ListAppender<>(); + la.start(); + logger.addAppender(la); + logger.setLevel(TRACE); + return la; + } + + private ListAppender attachAppenderForBlankMethodOnly() { + Logger logger = (Logger) LoggerFactory.getLogger(MethodBlankOnlyService.class); + ListAppender la = new ListAppender<>(); + la.start(); + logger.addAppender(la); + logger.setLevel(TRACE); + return la; + } + + private static void detachAppender(ListAppender la) { + Logger logger = (Logger) LoggerFactory.getLogger(MethodBlankOnlyService.class); + logger.detachAppender(la); + } + + private static void detachAppenderForBlankClass(ListAppender la) { + Logger logger = (Logger) LoggerFactory.getLogger(BlankClassScopeServiceImpl.class); + logger.detachAppender(la); + } + + private static ListAppender attachAppenderForWarnClass() { + Logger logger = (Logger) LoggerFactory.getLogger(WarnClassScopeServiceImpl.class); + ListAppender la = new ListAppender<>(); + la.start(); + logger.addAppender(la); + logger.setLevel(WARN); + return la; + } + + private static void detachAppenderForWarnClass(ListAppender la) { + Logger logger = (Logger) LoggerFactory.getLogger(WarnClassScopeServiceImpl.class); + logger.detachAppender(la); + } + + private static ListAppender attachAppenderForErrorClass() { + Logger logger = (Logger) LoggerFactory.getLogger(ErrorClassScopeServiceImpl.class); + ListAppender la = new ListAppender<>(); + la.start(); + logger.addAppender(la); + logger.setLevel(ERROR); + return la; + } + + private static void detachAppenderForErrorClass(ListAppender la) { + Logger logger = (Logger) LoggerFactory.getLogger(ErrorClassScopeServiceImpl.class); + logger.detachAppender(la); + } + + interface SampleService { + String doWork(); + + String doTraced(); + + void fail(); + + String doWarn(); + + String doErrorLevel(); + + String doCustom(); + + String doInfo(); + } + + interface BlankMethodOnlyService { + String blankOnly(); + } + + interface BlankClassScopeService { + String doOk(); + + void fail(); + + } + + interface WarnClassScopeService { + String doOk(); + + } + + interface ErrorClassScopeService { + String doOk(); + + void fail(); + + } + + @Configuration + @Import({AopAutoConfiguration.class, LoggingAspect.class}) + static class TestConfig { + @Bean + SampleService sampleLoggedService() { + return new SampleLoggedService(); + } + + @Bean + BlankMethodOnlyService blankMethodOnlyService() { + return new MethodBlankOnlyService(); + } + + @Bean + BlankClassScopeService blankClassScopeService() { + return new BlankClassScopeServiceImpl(); + } + + @Bean + WarnClassScopeService warnClassScopeService() { + return new WarnClassScopeServiceImpl(); + } + + @Bean + ErrorClassScopeService errorClassScopeService() { + return new ErrorClassScopeServiceImpl(); + } + } + + static class MethodBlankOnlyService implements BlankMethodOnlyService { + @Log(scope = " ") + public String blankOnly() { + return "blank"; + } + } + + @Log(scope = " ") + static class BlankClassScopeServiceImpl implements BlankClassScopeService { + public String doOk() { + return "ok"; + } + + public void fail() { + throw new IllegalStateException("boom"); + } + } + + @Log(scope = "WARN") + static class WarnClassScopeServiceImpl implements WarnClassScopeService { + public String doOk() { + return "ok"; + } + + } + + @Log(scope = "ERROR") + static class ErrorClassScopeServiceImpl implements ErrorClassScopeService { + public String doOk() { + return "ok"; + } + + public void fail() { + throw new IllegalStateException("boom"); + } + + } + + @Log(scope = "DEBUG") + static class SampleLoggedService implements SampleService { + public String doWork() { + return "ok"; + } + + @Log(scope = "TRACE") + public String doTraced() { + return "traced"; + } + + public void fail() { + throw new IllegalStateException("boom"); + } + + @Log(scope = "WARN") + public String doWarn() { + return "warned"; + } + + @Log(scope = "ERROR") + public String doErrorLevel() { + return "erred"; + } + + @Log(scope = "CUSTOM") + public String doCustom() { + return "custom"; + } + + @Log(scope = " info ") + public String doInfo() { + return "info"; + } + } +} diff --git a/src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java b/application/src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java similarity index 100% rename from src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java rename to application/src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java diff --git a/src/test/java/org/mandarin/booking/fixture/MemberFixture.java b/application/src/test/java/org/mandarin/booking/fixture/MemberFixture.java similarity index 100% rename from src/test/java/org/mandarin/booking/fixture/MemberFixture.java rename to application/src/test/java/org/mandarin/booking/fixture/MemberFixture.java diff --git a/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java similarity index 99% rename from src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java rename to application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java index 519eed0..2998300 100644 --- a/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java @@ -4,11 +4,11 @@ import static org.mandarin.booking.JwtTestUtils.assertJwtFormat; import static org.mandarin.booking.JwtTestUtils.getExpiration; import static org.mandarin.booking.JwtTestUtils.getTokenClaims; -import static org.mandarin.booking.fixture.MemberFixture.PasswordGenerator.generatePassword; -import static org.mandarin.booking.fixture.MemberFixture.UserIdGenerator.generateUserId; import static org.mandarin.booking.adapter.webapi.ApiStatus.BAD_REQUEST; import static org.mandarin.booking.adapter.webapi.ApiStatus.SUCCESS; import static org.mandarin.booking.adapter.webapi.ApiStatus.UNAUTHORIZED; +import static org.mandarin.booking.fixture.MemberFixture.PasswordGenerator.generatePassword; +import static org.mandarin.booking.fixture.MemberFixture.UserIdGenerator.generateUserId; import io.jsonwebtoken.security.Keys; import java.util.Date; @@ -239,6 +239,7 @@ public class POST_specs { assertThat(savedMember).isNotNull(); } + private static AuthRequest[] blankUserIdRequests() { return new AuthRequest[]{ new AuthRequest(null, generatePassword()), diff --git a/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java similarity index 100% rename from src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java rename to application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java diff --git a/src/test/java/org/mandarin/booking/webapi/member/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/member/POST_specs.java similarity index 100% rename from src/test/java/org/mandarin/booking/webapi/member/POST_specs.java rename to application/src/test/java/org/mandarin/booking/webapi/member/POST_specs.java diff --git a/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java similarity index 99% rename from src/test/java/org/mandarin/booking/webapi/show/POST_specs.java rename to application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java index 3a0f685..3d54be5 100644 --- a/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java @@ -161,6 +161,7 @@ public class POST_specs { } + @SuppressWarnings("NonAsciiCharacters") @Test void 중복된_제목의_공연을_등록하면_INTERNAL_SERVER_ERROR가_발생한다( @Autowired IntegrationTestUtils testUtils diff --git a/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java similarity index 95% rename from src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java rename to application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java index 97bb1c3..5b61658 100644 --- a/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java @@ -1,5 +1,6 @@ package org.mandarin.booking.webapi.show.schedule; +import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import static org.mandarin.booking.adapter.webapi.ApiStatus.BAD_REQUEST; import static org.mandarin.booking.adapter.webapi.ApiStatus.FORBIDDEN; @@ -33,7 +34,7 @@ public class POST_specs { var show = testUtils.insertDummyShow(LocalDate.of(2025, 9, 10), LocalDate.of(2025, 12, 31)); var hall = testUtils.insertDummyHall(); var request = generateShowScheduleRegisterRequest( - show, hall.getId(), + show, requireNonNull(hall.getId()), LocalDateTime.of(2025, 9, 10, 19, 0), LocalDateTime.of(2025, 9, 10, 21, 30)); @@ -54,7 +55,7 @@ public class POST_specs { var show = testUtils.insertDummyShow(LocalDate.of(2025, 9, 10), LocalDate.of(2025, 12, 31)); var hall = testUtils.insertDummyHall(); var request = generateShowScheduleRegisterRequest( - show, hall.getId(), + show, requireNonNull(hall.getId()), LocalDateTime.of(2025, 9, 10, 19, 0), LocalDateTime.of(2025, 9, 10, 21, 30)); @@ -75,7 +76,7 @@ public class POST_specs { var show = testUtils.insertDummyShow(LocalDate.of(2025, 9, 10), LocalDate.of(2025, 12, 31)); var hall = testUtils.insertDummyHall(); var request = generateShowScheduleRegisterRequest( - show, hall.getId(), + show, requireNonNull(hall.getId()), LocalDateTime.of(2025, 9, 10, 19, 0), LocalDateTime.of(2025, 9, 10, 21, 30)); @@ -132,7 +133,7 @@ public class POST_specs { @Autowired IntegrationTestUtils testUtils ) { // Arrange - var show = testUtils.insertDummyShow(LocalDate.of(2025, 9, 10), LocalDate.of(2025, 12, 31)); + testUtils.insertDummyShow(LocalDate.of(2025, 9, 10), LocalDate.of(2025, 12, 31)); var request = new ShowScheduleRegisterRequest( 9999L,// 존재하지 않는 showId 10L, @@ -157,7 +158,7 @@ public class POST_specs { // Arrange var show = testUtils.insertDummyShow(LocalDate.of(2025, 9, 10), LocalDate.of(2025, 12, 31)); var request = new ShowScheduleRegisterRequest( - show.getId(), + requireNonNull(show.getId()), 9999L,// 존재하지 않는 hallId LocalDateTime.of(2025, 9, 10, 19, 0), LocalDateTime.of(2025, 9, 10, 21, 30) @@ -181,8 +182,8 @@ public class POST_specs { var show = testUtils.insertDummyShow(LocalDate.of(2025, 9, 10), LocalDate.of(2025, 9, 11)); var hall = testUtils.insertDummyHall(); var request = new ShowScheduleRegisterRequest( - show.getId(), - hall.getId(), + requireNonNull(show.getId()), + requireNonNull(hall.getId()), LocalDateTime.of(2023, 9, 10, 19, 0), LocalDateTime.of(2023, 9, 10, 21, 30) ); @@ -208,7 +209,7 @@ public class POST_specs { LocalDate.now().minusDays(1), LocalDate.now().plusDays(10) ); - var request = generateShowScheduleRegisterRequest(show, hall.getId(), + var request = generateShowScheduleRegisterRequest(show, requireNonNull(hall.getId()), LocalDateTime.now(), LocalDateTime.now().plusHours(2) ); @@ -259,7 +260,7 @@ private ShowScheduleRegisterRequest generateShowScheduleRegisterRequest(Show sho LocalDateTime startAt, LocalDateTime endAt, long hallId) { return new ShowScheduleRegisterRequest( - show.getId(), + requireNonNull(show.getId()), hallId, startAt, endAt diff --git a/build.gradle b/build.gradle index f51f311..768b92f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,113 +1,67 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '3.5.4' - id 'io.spring.dependency-management' version '1.1.7' - id 'com.epages.restdocs-api-spec' version '0.18.2' - id 'org.asciidoctor.jvm.convert' version '3.3.2' -} - -group = 'org.mandarin' -version = '0.0.1-SNAPSHOT' - -ext { - snippetsDir = file('build/generated-snippets') -} +buildscript { + ext { + dependencyManagementVersion = '1.1.7' + springBootVersion = '3.5.4' + } -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } -} + repositories { + mavenCentral() + } -repositories { - mavenCentral() + dependencies { + classpath("io.spring.gradle:dependency-management-plugin:${dependencyManagementVersion}") + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } } -configurations { - byteBuddyAgent - asciidoctorExt +plugins { + id 'base' } -dependencies { - // ---- 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' - - // ---- Querydsl ---- - implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' - annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' - // Ensure APT has Jakarta APIs (some environments require explicit presence) - annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0' - annotationProcessor 'jakarta.annotation:jakarta.annotation-api:2.1.1' - // ---- 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 ---- - compileOnly 'org.projectlombok:lombok:1.18.36' - annotationProcessor 'org.projectlombok:lombok:1.18.36' - testCompileOnly 'org.projectlombok:lombok:1.18.36' - testAnnotationProcessor 'org.projectlombok:lombok:1.18.36' - - // ---- Dev Only ---- - developmentOnly 'org.springframework.boot:spring-boot-docker-compose' - developmentOnly 'org.springframework.boot:spring-boot-devtools' +subprojects { + apply plugin: 'java-library' + apply plugin: 'org.springframework.boot' + apply plugin: 'io.spring.dependency-management' + apply plugin: 'java-test-fixtures' + + group = 'org.yechan' + version = '1.0-SNAPSHOT' + sourceCompatibility = '21' + + configurations { + developmentOnly + runtimeClasspath { + extendsFrom developmentOnly + } + compileOnly { + extendsFrom annotationProcessor + } + } - // ---- 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' + repositories { + mavenCentral() + } - // ---- API Docs (REST Docs + Rest Assured) ---- - testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' + dependencies { + // ---- Lombok ---- + compileOnly 'org.projectlombok:lombok:1.18.36' + annotationProcessor 'org.projectlombok:lombok:1.18.36' + testCompileOnly 'org.projectlombok:lombok:1.18.36' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.36' - asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:3.1.0' + // ---- Testing ---- + testImplementation 'org.mockito:mockito-inline:5.2.0' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - // null safety - implementation 'org.jspecify:jspecify:1.0.0' -} -ext { - set('snippetsDir', file('build/generated-snippets')) -} - -tasks.named('test') { - useJUnitPlatform() - systemProperty 'spring.profiles.active', 'test' - // javaagent 선부착 (Mockito self-attach 방지) - jvmArgs "-javaagent:${configurations.byteBuddyAgent.singleFile}" - outputs.dir snippetsDir -} - -asciidoctor { - group = 'documentation' - description = 'Converts AsciiDoc files to HTML, including REST Docs snippets.' - dependsOn test - baseDirFollowsSourceDir() - sourceDir = file('src/docs') - sources { - include '**/*.adoc' + // null safety + implementation 'org.jspecify:jspecify:1.0.0' } - inputs.dir snippetsDir - attributes 'snippets': snippetsDir - resources { - from(snippetsDir) { into 'snippets' } + + tasks.named('test') { + useJUnitPlatform() + failFast = true + systemProperty 'spring.profiles.active', 'test' } - outputDir = layout.buildDirectory.dir("docs/asciidoc").get().asFile } - diff --git a/domain/build.gradle b/domain/build.gradle new file mode 100644 index 0000000..d4189e2 --- /dev/null +++ b/domain/build.gradle @@ -0,0 +1,18 @@ +bootJar { + enabled = false +} + +jar { + enabled = true +} + +dependencies { + api 'org.springframework.boot:spring-boot-starter-data-jpa' + api 'org.springframework.boot:spring-boot-starter-validation' + + // ---- Querydsl ---- + api 'com.querydsl:querydsl-jpa:5.1.0:jakarta' + annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' + annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0' + annotationProcessor 'jakarta.annotation:jakarta.annotation-api:2.1.1' +} diff --git a/src/main/java/org/mandarin/booking/domain/AbstractEntity.java b/domain/src/main/java/org/mandarin/booking/domain/AbstractEntity.java similarity index 91% rename from src/main/java/org/mandarin/booking/domain/AbstractEntity.java rename to domain/src/main/java/org/mandarin/booking/domain/AbstractEntity.java index 50c428e..d543aa4 100644 --- a/src/main/java/org/mandarin/booking/domain/AbstractEntity.java +++ b/domain/src/main/java/org/mandarin/booking/domain/AbstractEntity.java @@ -2,6 +2,7 @@ import static jakarta.persistence.GenerationType.IDENTITY; +import jakarta.annotation.Nullable; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; @@ -9,14 +10,14 @@ import lombok.Getter; import lombok.ToString; import org.hibernate.proxy.HibernateProxy; -import org.jspecify.annotations.Nullable; +import org.jspecify.annotations.NullUnmarked; +@NullUnmarked @MappedSuperclass @ToString(callSuper = true) public abstract class AbstractEntity { @Id - @Nullable - @Getter + @Getter(onMethod_ = {@Nullable}) @GeneratedValue(strategy = IDENTITY) private Long id; diff --git a/src/main/java/org/mandarin/booking/domain/DomainException.java b/domain/src/main/java/org/mandarin/booking/domain/DomainException.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/DomainException.java rename to domain/src/main/java/org/mandarin/booking/domain/DomainException.java diff --git a/src/main/java/org/mandarin/booking/domain/EnumRequest.java b/domain/src/main/java/org/mandarin/booking/domain/EnumRequest.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/EnumRequest.java rename to domain/src/main/java/org/mandarin/booking/domain/EnumRequest.java diff --git a/src/main/java/org/mandarin/booking/domain/EnumRequestValidator.java b/domain/src/main/java/org/mandarin/booking/domain/EnumRequestValidator.java similarity index 94% rename from src/main/java/org/mandarin/booking/domain/EnumRequestValidator.java rename to domain/src/main/java/org/mandarin/booking/domain/EnumRequestValidator.java index 14b72fa..06e98aa 100644 --- a/src/main/java/org/mandarin/booking/domain/EnumRequestValidator.java +++ b/domain/src/main/java/org/mandarin/booking/domain/EnumRequestValidator.java @@ -2,7 +2,10 @@ import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; +import org.jspecify.annotations.NullUnmarked; + +@NullUnmarked public class EnumRequestValidator implements ConstraintValidator { private Class> clazz; private String message; diff --git a/src/main/java/org/mandarin/booking/domain/member/AuthException.java b/domain/src/main/java/org/mandarin/booking/domain/member/AuthException.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/member/AuthException.java rename to domain/src/main/java/org/mandarin/booking/domain/member/AuthException.java diff --git a/src/main/java/org/mandarin/booking/domain/member/AuthRequest.java b/domain/src/main/java/org/mandarin/booking/domain/member/AuthRequest.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/member/AuthRequest.java rename to domain/src/main/java/org/mandarin/booking/domain/member/AuthRequest.java diff --git a/src/main/java/org/mandarin/booking/domain/member/Member.java b/domain/src/main/java/org/mandarin/booking/domain/member/Member.java similarity index 88% rename from src/main/java/org/mandarin/booking/domain/member/Member.java rename to domain/src/main/java/org/mandarin/booking/domain/member/Member.java index 11aa500..3ff7b30 100644 --- a/src/main/java/org/mandarin/booking/domain/member/Member.java +++ b/domain/src/main/java/org/mandarin/booking/domain/member/Member.java @@ -35,6 +35,12 @@ public static Member create(MemberCreateCommand command, return member; } + public String[] getParsedAuthorities() { + return authorities.stream() + .map(MemberAuthority::getAuthority) + .toArray(String[]::new); + } + public boolean matchesPassword(String rawPassword, SecurePasswordEncoder securePasswordEncoder) { return securePasswordEncoder.matches(rawPassword, this.passwordHash); } diff --git a/src/main/java/org/mandarin/booking/domain/member/MemberAuthority.java b/domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthority.java similarity index 56% rename from src/main/java/org/mandarin/booking/domain/member/MemberAuthority.java rename to domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthority.java index 649d5f4..1dc1701 100644 --- a/src/main/java/org/mandarin/booking/domain/member/MemberAuthority.java +++ b/domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthority.java @@ -1,13 +1,10 @@ package org.mandarin.booking.domain.member; -import org.springframework.security.core.GrantedAuthority; - -public enum MemberAuthority implements GrantedAuthority { +public enum MemberAuthority { USER, DISTRIBUTOR, ADMIN; - @Override public String getAuthority() { return "ROLE_" + name().toUpperCase(); } diff --git a/src/main/java/org/mandarin/booking/domain/member/MemberAuthorityConverter.java b/domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthorityConverter.java similarity index 94% rename from src/main/java/org/mandarin/booking/domain/member/MemberAuthorityConverter.java rename to domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthorityConverter.java index 2e46be4..ca9e2fe 100644 --- a/src/main/java/org/mandarin/booking/domain/member/MemberAuthorityConverter.java +++ b/domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthorityConverter.java @@ -6,7 +6,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; @Converter @@ -20,7 +19,6 @@ public String convertToDatabaseColumn(List attribute) { return ""; } return attribute.stream() - .filter(Objects::nonNull) .map(MemberAuthority::getAuthority) .collect(Collectors.joining(DELIM)); } diff --git a/src/main/java/org/mandarin/booking/domain/member/MemberException.java b/domain/src/main/java/org/mandarin/booking/domain/member/MemberException.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/member/MemberException.java rename to domain/src/main/java/org/mandarin/booking/domain/member/MemberException.java diff --git a/src/main/java/org/mandarin/booking/domain/member/MemberRegisterRequest.java b/domain/src/main/java/org/mandarin/booking/domain/member/MemberRegisterRequest.java similarity index 90% rename from src/main/java/org/mandarin/booking/domain/member/MemberRegisterRequest.java rename to domain/src/main/java/org/mandarin/booking/domain/member/MemberRegisterRequest.java index e52d587..9e90646 100644 --- a/src/main/java/org/mandarin/booking/domain/member/MemberRegisterRequest.java +++ b/domain/src/main/java/org/mandarin/booking/domain/member/MemberRegisterRequest.java @@ -2,7 +2,9 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import org.jspecify.annotations.NullUnmarked; +@NullUnmarked public record MemberRegisterRequest( @NotBlank(message = "Nickname cannot be blank") String nickName, diff --git a/src/main/java/org/mandarin/booking/domain/member/MemberRegisterResponse.java b/domain/src/main/java/org/mandarin/booking/domain/member/MemberRegisterResponse.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/member/MemberRegisterResponse.java rename to domain/src/main/java/org/mandarin/booking/domain/member/MemberRegisterResponse.java diff --git a/src/main/java/org/mandarin/booking/domain/member/ReissueRequest.java b/domain/src/main/java/org/mandarin/booking/domain/member/ReissueRequest.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/member/ReissueRequest.java rename to domain/src/main/java/org/mandarin/booking/domain/member/ReissueRequest.java diff --git a/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java b/domain/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java similarity index 67% rename from src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java rename to domain/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java index fd09987..3cc715e 100644 --- a/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java +++ b/domain/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java @@ -1,11 +1,6 @@ package org.mandarin.booking.domain.member; public interface SecurePasswordEncoder { - /** - * Encodes the given password. - * @param password - * @return - */ String encode(String password); boolean matches(String rawPassword, String encodedPassword); } diff --git a/src/main/java/org/mandarin/booking/domain/member/TokenHolder.java b/domain/src/main/java/org/mandarin/booking/domain/member/TokenHolder.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/member/TokenHolder.java rename to domain/src/main/java/org/mandarin/booking/domain/member/TokenHolder.java diff --git a/src/main/java/org/mandarin/booking/domain/member/package-info.java b/domain/src/main/java/org/mandarin/booking/domain/member/package-info.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/member/package-info.java rename to domain/src/main/java/org/mandarin/booking/domain/member/package-info.java diff --git a/src/main/java/org/mandarin/booking/domain/package-info.java b/domain/src/main/java/org/mandarin/booking/domain/package-info.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/package-info.java rename to domain/src/main/java/org/mandarin/booking/domain/package-info.java diff --git a/src/main/java/org/mandarin/booking/domain/show/Show.java b/domain/src/main/java/org/mandarin/booking/domain/show/Show.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/show/Show.java rename to domain/src/main/java/org/mandarin/booking/domain/show/Show.java diff --git a/src/main/java/org/mandarin/booking/domain/show/ShowException.java b/domain/src/main/java/org/mandarin/booking/domain/show/ShowException.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/show/ShowException.java rename to domain/src/main/java/org/mandarin/booking/domain/show/ShowException.java diff --git a/src/main/java/org/mandarin/booking/domain/show/ShowRegisterRequest.java b/domain/src/main/java/org/mandarin/booking/domain/show/ShowRegisterRequest.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/show/ShowRegisterRequest.java rename to domain/src/main/java/org/mandarin/booking/domain/show/ShowRegisterRequest.java diff --git a/src/main/java/org/mandarin/booking/domain/show/ShowRegisterResponse.java b/domain/src/main/java/org/mandarin/booking/domain/show/ShowRegisterResponse.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/show/ShowRegisterResponse.java rename to domain/src/main/java/org/mandarin/booking/domain/show/ShowRegisterResponse.java diff --git a/src/main/java/org/mandarin/booking/domain/show/ShowSchedule.java b/domain/src/main/java/org/mandarin/booking/domain/show/ShowSchedule.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/show/ShowSchedule.java rename to domain/src/main/java/org/mandarin/booking/domain/show/ShowSchedule.java diff --git a/src/main/java/org/mandarin/booking/domain/show/ShowScheduleCreateCommand.java b/domain/src/main/java/org/mandarin/booking/domain/show/ShowScheduleCreateCommand.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/show/ShowScheduleCreateCommand.java rename to domain/src/main/java/org/mandarin/booking/domain/show/ShowScheduleCreateCommand.java diff --git a/src/main/java/org/mandarin/booking/domain/show/ShowScheduleRegisterRequest.java b/domain/src/main/java/org/mandarin/booking/domain/show/ShowScheduleRegisterRequest.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/show/ShowScheduleRegisterRequest.java rename to domain/src/main/java/org/mandarin/booking/domain/show/ShowScheduleRegisterRequest.java diff --git a/src/main/java/org/mandarin/booking/domain/show/ShowScheduleRegisterResponse.java b/domain/src/main/java/org/mandarin/booking/domain/show/ShowScheduleRegisterResponse.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/show/ShowScheduleRegisterResponse.java rename to domain/src/main/java/org/mandarin/booking/domain/show/ShowScheduleRegisterResponse.java diff --git a/src/main/java/org/mandarin/booking/domain/show/package-info.java b/domain/src/main/java/org/mandarin/booking/domain/show/package-info.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/show/package-info.java rename to domain/src/main/java/org/mandarin/booking/domain/show/package-info.java diff --git a/src/main/java/org/mandarin/booking/domain/venue/Hall.java b/domain/src/main/java/org/mandarin/booking/domain/venue/Hall.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/venue/Hall.java rename to domain/src/main/java/org/mandarin/booking/domain/venue/Hall.java diff --git a/src/main/java/org/mandarin/booking/domain/venue/HallException.java b/domain/src/main/java/org/mandarin/booking/domain/venue/HallException.java similarity index 100% rename from src/main/java/org/mandarin/booking/domain/venue/HallException.java rename to domain/src/main/java/org/mandarin/booking/domain/venue/HallException.java diff --git a/src/test/java/org/mandarin/booking/domain/AbstractEntityTest.java b/domain/src/test/java/org/mandarin/booking/domain/AbstractEntityTest.java similarity index 99% rename from src/test/java/org/mandarin/booking/domain/AbstractEntityTest.java rename to domain/src/test/java/org/mandarin/booking/domain/AbstractEntityTest.java index 55ede8a..a9e7e2f 100644 --- a/src/test/java/org/mandarin/booking/domain/AbstractEntityTest.java +++ b/domain/src/test/java/org/mandarin/booking/domain/AbstractEntityTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.Serial; import java.lang.reflect.Field; import java.util.HashSet; import java.util.Set; @@ -71,6 +72,7 @@ public org.hibernate.proxy.LazyInitializer getHibernateLazyInitializer() { ); } + @Serial @Override public Object writeReplace() { return this; @@ -216,6 +218,7 @@ public org.hibernate.proxy.LazyInitializer getHibernateLazyInitializer() { ); } + @Serial @Override public Object writeReplace() { return this; diff --git a/src/test/java/org/mandarin/booking/domain/MemberTest.java b/domain/src/test/java/org/mandarin/booking/domain/MemberTest.java similarity index 100% rename from src/test/java/org/mandarin/booking/domain/MemberTest.java rename to domain/src/test/java/org/mandarin/booking/domain/MemberTest.java index d9e03fe..07e0ccf 100644 --- a/src/test/java/org/mandarin/booking/domain/MemberTest.java +++ b/domain/src/test/java/org/mandarin/booking/domain/MemberTest.java @@ -4,9 +4,9 @@ import static org.mockito.Mockito.mock; import org.junit.jupiter.api.Test; -import org.mandarin.booking.domain.member.SecurePasswordEncoder; import org.mandarin.booking.domain.member.Member; import org.mandarin.booking.domain.member.Member.MemberCreateCommand; +import org.mandarin.booking.domain.member.SecurePasswordEncoder; class MemberTest { private static final String MAIL = "test@test.com"; diff --git a/settings.gradle b/settings.gradle index 19e1830..66463a8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,4 @@ rootProject.name = 'booking' + +include ':application' +include ':domain' diff --git a/src/main/java/org/mandarin/booking/app/LoggingAspect.java b/src/main/java/org/mandarin/booking/app/LoggingAspect.java deleted file mode 100644 index c24f433..0000000 --- a/src/main/java/org/mandarin/booking/app/LoggingAspect.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.mandarin.booking.app; - -import java.lang.reflect.Method; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.reflect.MethodSignature; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -@Aspect -@Component -@Slf4j -@RequiredArgsConstructor -public class LoggingAspect { - - @Around("@within(org.mandarin.booking.app.Log) || @annotation(org.mandarin.booking.app.Log)") - public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable { - Logger targetLogger = selectTargetLogger(joinPoint); - String level = resolveScope(joinPoint); - - LocalDateTime startAt = LocalDateTime.now(); - long startNs = System.nanoTime(); - String signature = joinPoint.getSignature().toLongString(); - - logAtLevel(targetLogger, level, "START {} at {}", signature, formatTime(startAt)); - - boolean success = false; - try { - Object result = joinPoint.proceed(); - success = true; - return result; - } catch (Throwable t) { - long elapsedMs = nanosToMillis(System.nanoTime() - startNs); - LocalDateTime endAt = LocalDateTime.now(); - if (targetLogger.isErrorEnabled()) { - targetLogger.error("END {} at {} ({} ms) with exception: {}", signature, formatTime(endAt), elapsedMs, t.toString()); - } - throw t; - } finally { - if (success) { - long elapsedMs = nanosToMillis(System.nanoTime() - startNs); - LocalDateTime endAt = LocalDateTime.now(); - logAtLevel(targetLogger, level, "END {} at {} ({} ms)", signature, formatTime(endAt), elapsedMs); - } - } - } - - private Logger selectTargetLogger(ProceedingJoinPoint joinPoint) { - MethodSignature ms = (MethodSignature) joinPoint.getSignature(); - Method method = ms.getMethod(); - Class targetClass = joinPoint.getTarget() != null ? joinPoint.getTarget().getClass() : method.getDeclaringClass(); - return LoggerFactory.getLogger(targetClass); - } - - private String resolveScope(ProceedingJoinPoint joinPoint) { - MethodSignature ms = (MethodSignature) joinPoint.getSignature(); - Method method = ms.getMethod(); - Class targetClass = joinPoint.getTarget() != null ? joinPoint.getTarget().getClass() : method.getDeclaringClass(); - try { - Method targetMethod = targetClass.getMethod(method.getName(), method.getParameterTypes()); - Log m = targetMethod.getAnnotation(Log.class); - if (m != null && !m.scope().isBlank()) return m.scope(); - } catch (NoSuchMethodException ignored) {} - Log c = targetClass.getAnnotation(Log.class); - String scope = c != null ? c.scope() : "INFO"; - return scope.isBlank() ? "INFO" : scope; - } - - private void logAtLevel(Logger logger, String level, String message, Object... args) { - String up = level.trim().toUpperCase(); - switch (up) { - case "TRACE" -> { if (logger.isTraceEnabled()) logger.trace(message, args); } - case "DEBUG" -> { if (logger.isDebugEnabled()) logger.debug(message, args); } - case "WARN" -> { if (logger.isWarnEnabled()) logger.warn(message, args); } - case "ERROR" -> { if (logger.isErrorEnabled()) logger.error(message, args); } - case "INFO" -> { if (logger.isInfoEnabled()) logger.info(message, args); } - default -> { if (logger.isInfoEnabled()) logger.info(message, args); } - } - } - - private static long nanosToMillis(long ns) { - return ns / 1_000_000L; - } - - private static String formatTime(LocalDateTime time) { - return time.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); - } -} diff --git a/src/main/java/org/mandarin/booking/domain/member/MemberDetails.java b/src/main/java/org/mandarin/booking/domain/member/MemberDetails.java deleted file mode 100644 index b302d75..0000000 --- a/src/main/java/org/mandarin/booking/domain/member/MemberDetails.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.mandarin.booking.domain.member; - -import java.util.Collection; -import lombok.Getter; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -@Getter -public class MemberDetails implements UserDetails { - private final String userId; - private final String password; - private final Collection authorities; - - private MemberDetails(String userId, String password, Collection authorities) { - this.userId = userId; - this.password = password; - this.authorities = authorities; - } - - public static MemberDetails from(Member member) { - String userId = member.getUserId(); - String password = member.getPasswordHash(); - Collection authorities = member.getAuthorities(); - return new MemberDetails(userId, password, authorities); - } - - @Override - public String getUsername() { - return userId; - } -} diff --git a/src/test/java/org/mandarin/booking/BookingApplicationTests.java b/src/test/java/org/mandarin/booking/BookingApplicationTests.java deleted file mode 100644 index 65ed601..0000000 --- a/src/test/java/org/mandarin/booking/BookingApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.mandarin.booking; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class BookingApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/org/mandarin/booking/TestOnlyController.java b/src/test/java/org/mandarin/booking/TestOnlyController.java deleted file mode 100644 index ef14c46..0000000 --- a/src/test/java/org/mandarin/booking/TestOnlyController.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.mandarin.booking; - -import java.util.Map; -import org.mandarin.booking.app.persist.MemberQueryRepository; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; - -//@RestController -//@RequestMapping(path = "/test", produces = MediaType.APPLICATION_JSON_VALUE) -public record TestOnlyController(MemberQueryRepository memberQueryRepository) { - - @PostMapping(path = "/echo", consumes = MediaType.APPLICATION_JSON_VALUE) - public Map echo(@RequestBody Map body) { - return body; - } - - @PostMapping(path = "/member/exists", consumes = MediaType.APPLICATION_JSON_VALUE) - public Boolean exists(@RequestBody Map body) { - String userId = body.get("userId"); - return userId != null && memberQueryRepository.existsByUserId(userId); - } -} diff --git a/src/test/java/org/mandarin/booking/app/LoggingAspectTest.java b/src/test/java/org/mandarin/booking/app/LoggingAspectTest.java deleted file mode 100644 index ee8972c..0000000 --- a/src/test/java/org/mandarin/booking/app/LoggingAspectTest.java +++ /dev/null @@ -1,192 +0,0 @@ -package org.mandarin.booking.app; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.read.ListAppender; -import java.util.List; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mandarin.booking.IntegrationTest; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@IntegrationTest -@Import(LoggingAspectTest.TestConfig.class) -class LoggingAspectTest { - - private ListAppender listAppender; - - @Autowired - LoggingAspectTest.SampleService bean; - - @Autowired - LoggingAspectTest.BlankMethodOnlyService blankMethodOnlyService; - - @BeforeEach - void setUp() { - Logger logger = (Logger) LoggerFactory.getLogger(SampleLoggedService.class); - listAppender = new ListAppender<>(); - listAppender.start(); - logger.addAppender(listAppender); - logger.setLevel(Level.TRACE); - } - - @AfterEach - void tearDown() { - Logger logger = (Logger) LoggerFactory.getLogger(SampleLoggedService.class); - logger.detachAppender(listAppender); - } - - private static ListAppender attachAppender(Class target) { - Logger logger = (Logger) LoggerFactory.getLogger(target); - ListAppender la = new ListAppender<>(); - la.start(); - logger.addAppender(la); - logger.setLevel(Level.TRACE); - return la; - } - - private static void detachAppender(Class target, ListAppender la) { - Logger logger = (Logger) LoggerFactory.getLogger(target); - logger.detachAppender(la); - } - - @Configuration - @Import({AopAutoConfiguration.class, LoggingAspect.class}) - static class TestConfig { - @Bean - SampleService sampleLoggedService() { - return new SampleLoggedService(); - } - @Bean - BlankMethodOnlyService blankMethodOnlyService() { return new MethodBlankOnlyService(); } - } - - interface SampleService { - String doWork(); - String doTraced(); - String fail(); - String doWarn(); - String doErrorLevel(); - String doCustom(); - } - - interface BlankMethodOnlyService { String blankOnly(); } - - static class MethodBlankOnlyService implements BlankMethodOnlyService { - @Log(scope = " ") - public String blankOnly() { return "blank"; } - } - - @Log(scope = "DEBUG") - static class SampleLoggedService implements SampleService { - public String doWork() { return "ok"; } - @Log(scope = "TRACE") - public String doTraced() { return "traced"; } - public String fail() { throw new IllegalStateException("boom"); } - @Log(scope = "WARN") - public String doWarn() { return "warned"; } - @Log(scope = "ERROR") - public String doErrorLevel() { return "erred"; } - @Log(scope = "CUSTOM") - public String doCustom() { return "custom"; } - } - - @Test - @DisplayName("Class-level @Log produces START and END logs at configured level, method inherits when not annotated") - void classLevelLog_startEnd() { - SampleService s = bean; - String res = s.doWork(); - assertThat(res).isEqualTo("ok"); - - List events = listAppender.list; - assertThat(events).hasSize(2); - assertThat(events.get(0).getLevel()).isEqualTo(Level.DEBUG); - assertThat(events.get(0).getFormattedMessage()).contains("START").contains("doWork"); - assertThat(events.get(1).getLevel()).isEqualTo(Level.DEBUG); - assertThat(events.get(1).getFormattedMessage()).contains("END").contains("("); - } - - @Test - @DisplayName("Method-level @Log overrides class level") - void methodLevelOverrides() { - SampleService s = bean; - String res = s.doTraced(); - assertThat(res).isEqualTo("traced"); - List events = listAppender.list; - assertThat(events).hasSize(2); - assertThat(events.get(0).getLevel()).isEqualTo(Level.TRACE); - assertThat(events.get(0).getFormattedMessage()).contains("START").contains("doTraced"); - } - - @Test - @DisplayName("On exception, END is logged at error with exception info") - void exceptionLogging() { - SampleService s = bean; - assertThatThrownBy(s::fail).isInstanceOf(IllegalStateException.class); - List events = listAppender.list; - assertThat(events).hasSize(2); - assertThat(events.get(1).getLevel()).isEqualTo(Level.ERROR); - assertThat(events.get(1).getFormattedMessage()).contains("with exception").contains("IllegalStateException"); - } - - @Test - @DisplayName("Method annotated with WARN logs at WARN") - void warnLevelMethod() { - SampleService s = bean; - String res = s.doWarn(); - assertThat(res).isEqualTo("warned"); - List events = listAppender.list; - assertThat(events).hasSize(2); - assertThat(events.get(0).getLevel()).isEqualTo(Level.WARN); - } - - @Test - @DisplayName("Method annotated with ERROR logs START/END at ERROR on success path") - void errorLevelMethod_successful() { - SampleService s = bean; - String res = s.doErrorLevel(); - assertThat(res).isEqualTo("erred"); - List events = listAppender.list; - assertThat(events).hasSize(2); - assertThat(events.get(0).getLevel()).isEqualTo(Level.ERROR); - assertThat(events.get(1).getLevel()).isEqualTo(Level.ERROR); - } - - @Test - @DisplayName("Unknown scope falls back to INFO (default branch)") - void unknownScopeDefaultsToInfo() { - SampleService s = bean; - String res = s.doCustom(); - assertThat(res).isEqualTo("custom"); - List events = listAppender.list; - assertThat(events).hasSize(2); - assertThat(events.get(0).getLevel()).isEqualTo(Level.INFO); - } - - @Test - @DisplayName("Blank method scope with no class annotation falls back to INFO") - void blankMethodScopeFallsBackToInfo() { - // Attach specific appender for MethodBlankOnlyService class - ListAppender la = attachAppender(MethodBlankOnlyService.class); - try { - String res = blankMethodOnlyService.blankOnly(); - assertThat(res).isEqualTo("blank"); - List events = la.list; - assertThat(events).hasSize(2); - assertThat(events.get(0).getLevel()).isEqualTo(Level.INFO); - } finally { - detachAppender(MethodBlankOnlyService.class, la); - } - } -} diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index 1f0955d..0000000 --- a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline From 480fc21cc46730c5a0104948112065b640092a6f Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Wed, 10 Sep 2025 17:05:03 +0900 Subject: [PATCH 06/36] modularize package structure and update imports --- application/build.gradle | 10 +-- .../org/mandarin/booking/app/AuthService.java | 5 +- .../CustomAuthenticationProvider.java | 5 +- .../mandarin/booking/app/JwtTokenUtils.java | 7 +- .../booking/app/port/AuthUseCase.java | 2 +- .../{adapter => }/webapi/AuthController.java | 4 +- .../webapi/MemberController.java | 2 +- .../{adapter => }/webapi/ShowController.java | 2 +- .../src/main/resources/application-test.yml | 1 + .../booking/IntegrationTestUtils.java | 4 +- .../java/org/mandarin/booking/TestConfig.java | 2 +- .../CustomAuthenticationProviderTest.java | 1 + .../adapter/security/JwtFilterTest.java | 1 - .../booking/arch/BaseArchitectureTest.java | 18 +++++ .../arch/HexagonalArchitectureTest.java | 26 ------- .../arch/ModuleDependencyRulesTest.java | 77 +++++++++++++++++++ .../booking/webapi/auth/login/POST_specs.java | 2 +- .../webapi/auth/reissue/POST_specs.java | 6 +- .../booking/webapi/show/POST_specs.java | 2 +- .../webapi/show/schedule/POST_specs.java | 6 +- build.gradle | 2 +- .../org/mandarin/booking}/AuthException.java | 4 +- .../mandarin/booking}/DomainException.java | 2 +- .../mandarin/booking}/MemberAuthority.java | 2 +- .../org/mandarin/booking}/TokenHolder.java | 2 +- domain/build.gradle | 1 + .../booking/domain/member/Member.java | 1 + .../member/MemberAuthorityConverter.java | 1 + .../domain/member/MemberException.java | 2 +- .../booking/domain/show/ShowException.java | 2 +- .../booking/domain/venue/HallException.java | 2 +- .../domain/{ => member}/MemberTest.java | 4 +- .../adapter/JacksonCustomizerConfig.java | 0 .../security/CustomAccessDeniedHandler.java | 0 .../CustomAuthenticationEntryPoint.java | 0 .../CustomMemberAuthenticationToken.java | 2 +- .../booking/adapter/security/JwtFilter.java | 5 +- .../adapter/security/SecurityConfig.java | 1 - .../booking/adapter/security}/TokenUtils.java | 6 +- .../adapter/security/package-info.java | 0 .../booking/adapter/webapi/ApiResponse.java | 0 .../booking/adapter/webapi/ApiStatus.java | 0 .../webapi/CommonHttpMessageConverter.java | 0 .../booking/adapter/webapi/ErrorResponse.java | 0 .../webapi/GlobalExceptionHandler.java | 4 +- .../adapter/webapi/SuccessResponse.java | 0 .../booking/adapter/webapi/package-info.java | 0 .../CustomAuthenticationEntryPointTest.java | 0 .../CustomMemberAuthenticationTokenTest.java | 2 +- settings.gradle | 3 + 50 files changed, 148 insertions(+), 83 deletions(-) rename application/src/main/java/org/mandarin/booking/{adapter/security => app}/CustomAuthenticationProvider.java (92%) rename application/src/main/java/org/mandarin/booking/{adapter => }/webapi/AuthController.java (89%) rename application/src/main/java/org/mandarin/booking/{adapter => }/webapi/MemberController.java (94%) rename application/src/main/java/org/mandarin/booking/{adapter => }/webapi/ShowController.java (96%) create mode 100644 application/src/test/java/org/mandarin/booking/arch/BaseArchitectureTest.java delete mode 100644 application/src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java create mode 100644 application/src/test/java/org/mandarin/booking/arch/ModuleDependencyRulesTest.java rename {domain/src/main/java/org/mandarin/booking/domain/member => common/src/main/java/org/mandarin/booking}/AuthException.java (57%) rename {domain/src/main/java/org/mandarin/booking/domain => common/src/main/java/org/mandarin/booking}/DomainException.java (90%) rename {domain/src/main/java/org/mandarin/booking/domain/member => common/src/main/java/org/mandarin/booking}/MemberAuthority.java (78%) rename {domain/src/main/java/org/mandarin/booking/domain/member => common/src/main/java/org/mandarin/booking}/TokenHolder.java (62%) rename domain/src/test/java/org/mandarin/booking/domain/{ => member}/MemberTest.java (84%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java (100%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java (100%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java (100%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java (94%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java (94%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java (98%) rename {application/src/main/java/org/mandarin/booking/app => internal/src/main/java/org/mandarin/booking/adapter/security}/TokenUtils.java (69%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/security/package-info.java (100%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java (100%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java (100%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java (100%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java (100%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java (94%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java (100%) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/webapi/package-info.java (100%) rename {application => internal}/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java (100%) rename {application => internal}/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java (84%) diff --git a/application/build.gradle b/application/build.gradle index 428eb0f..35b05a6 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -14,10 +14,11 @@ ext { dependencies { api(project(':domain')) + api(project(':common')) + api(project(':internal')) // ---- 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-aop' // ---- Data & Database ---- @@ -26,19 +27,12 @@ dependencies { 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' // ---- Dev Only ---- developmentOnly 'org.springframework.boot:spring-boot-docker-compose' developmentOnly 'org.springframework.boot:spring-boot-devtools' // ---- Testing ---- - testImplementation 'org.springframework.security:spring-security-test' testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' byteBuddyAgent 'net.bytebuddy:byte-buddy-agent:1.17.6' testImplementation 'org.mockito:mockito-core:5.19.0' diff --git a/application/src/main/java/org/mandarin/booking/app/AuthService.java b/application/src/main/java/org/mandarin/booking/app/AuthService.java index f66d526..5fc4166 100644 --- a/application/src/main/java/org/mandarin/booking/app/AuthService.java +++ b/application/src/main/java/org/mandarin/booking/app/AuthService.java @@ -1,12 +1,13 @@ package org.mandarin.booking.app; import lombok.RequiredArgsConstructor; +import org.mandarin.booking.AuthException; +import org.mandarin.booking.TokenHolder; +import org.mandarin.booking.adapter.security.TokenUtils; import org.mandarin.booking.app.persist.MemberQueryRepository; import org.mandarin.booking.app.port.AuthUseCase; -import org.mandarin.booking.domain.member.AuthException; import org.mandarin.booking.domain.member.Member; import org.mandarin.booking.domain.member.SecurePasswordEncoder; -import org.mandarin.booking.domain.member.TokenHolder; import org.springframework.stereotype.Service; @Service diff --git a/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java b/application/src/main/java/org/mandarin/booking/app/CustomAuthenticationProvider.java similarity index 92% rename from application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java rename to application/src/main/java/org/mandarin/booking/app/CustomAuthenticationProvider.java index a91802f..009b643 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java +++ b/application/src/main/java/org/mandarin/booking/app/CustomAuthenticationProvider.java @@ -1,8 +1,9 @@ -package org.mandarin.booking.adapter.security; +package org.mandarin.booking.app; import lombok.RequiredArgsConstructor; +import org.mandarin.booking.AuthException; +import org.mandarin.booking.adapter.security.CustomMemberAuthenticationToken; import org.mandarin.booking.app.persist.MemberQueryRepository; -import org.mandarin.booking.domain.member.AuthException; import org.mandarin.booking.domain.member.Member; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; diff --git a/application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java b/application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java index bd846d3..a0ffd89 100644 --- a/application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java +++ b/application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java @@ -15,9 +15,10 @@ import java.util.stream.Collectors; import javax.crypto.SecretKey; import org.jspecify.annotations.Nullable; -import org.mandarin.booking.domain.member.AuthException; -import org.mandarin.booking.domain.member.MemberAuthority; -import org.mandarin.booking.domain.member.TokenHolder; +import org.mandarin.booking.AuthException; +import org.mandarin.booking.MemberAuthority; +import org.mandarin.booking.TokenHolder; +import org.mandarin.booking.adapter.security.TokenUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; diff --git a/application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java b/application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java index 5be54c9..0e5db1c 100644 --- a/application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java +++ b/application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java @@ -1,6 +1,6 @@ package org.mandarin.booking.app.port; -import org.mandarin.booking.domain.member.TokenHolder; +import org.mandarin.booking.TokenHolder; public interface AuthUseCase { TokenHolder login(String userId, String password); diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java b/application/src/main/java/org/mandarin/booking/webapi/AuthController.java similarity index 89% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java rename to application/src/main/java/org/mandarin/booking/webapi/AuthController.java index a940a93..2749a2d 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java +++ b/application/src/main/java/org/mandarin/booking/webapi/AuthController.java @@ -1,10 +1,10 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.webapi; import jakarta.validation.Valid; +import org.mandarin.booking.TokenHolder; import org.mandarin.booking.app.port.AuthUseCase; import org.mandarin.booking.domain.member.AuthRequest; import org.mandarin.booking.domain.member.ReissueRequest; -import org.mandarin.booking.domain.member.TokenHolder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java b/application/src/main/java/org/mandarin/booking/webapi/MemberController.java similarity index 94% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java rename to application/src/main/java/org/mandarin/booking/webapi/MemberController.java index 948a272..1de821e 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java +++ b/application/src/main/java/org/mandarin/booking/webapi/MemberController.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.webapi; import jakarta.validation.Valid; import org.mandarin.booking.app.port.MemberRegisterer; diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java b/application/src/main/java/org/mandarin/booking/webapi/ShowController.java similarity index 96% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java rename to application/src/main/java/org/mandarin/booking/webapi/ShowController.java index f6480ba..c51e070 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java +++ b/application/src/main/java/org/mandarin/booking/webapi/ShowController.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.webapi; import jakarta.validation.Valid; import org.mandarin.booking.app.port.ShowRegisterer; diff --git a/application/src/main/resources/application-test.yml b/application/src/main/resources/application-test.yml index 71ffcf8..1036f80 100644 --- a/application/src/main/resources/application-test.yml +++ b/application/src/main/resources/application-test.yml @@ -26,3 +26,4 @@ jwt: #logging: # level: # org.springframework.security: TRACE + diff --git a/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java b/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java index 0be317a..0568527 100644 --- a/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java +++ b/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java @@ -11,15 +11,13 @@ import java.util.Collection; import java.util.List; import java.util.UUID; -import org.mandarin.booking.app.TokenUtils; +import org.mandarin.booking.adapter.security.TokenUtils; import org.mandarin.booking.app.persist.HallCommandRepository; import org.mandarin.booking.app.persist.MemberCommandRepository; import org.mandarin.booking.app.persist.ShowCommandRepository; import org.mandarin.booking.domain.member.Member; import org.mandarin.booking.domain.member.Member.MemberCreateCommand; -import org.mandarin.booking.domain.member.MemberAuthority; import org.mandarin.booking.domain.member.SecurePasswordEncoder; -import org.mandarin.booking.domain.member.TokenHolder; import org.mandarin.booking.domain.show.Show; import org.mandarin.booking.domain.show.Show.ShowCreateCommand; import org.mandarin.booking.domain.show.ShowRegisterRequest; diff --git a/application/src/test/java/org/mandarin/booking/TestConfig.java b/application/src/test/java/org/mandarin/booking/TestConfig.java index 2cf1f84..257cf99 100644 --- a/application/src/test/java/org/mandarin/booking/TestConfig.java +++ b/application/src/test/java/org/mandarin/booking/TestConfig.java @@ -1,7 +1,7 @@ package org.mandarin.booking; import com.fasterxml.jackson.databind.ObjectMapper; -import org.mandarin.booking.app.TokenUtils; +import org.mandarin.booking.adapter.security.TokenUtils; import org.mandarin.booking.app.persist.HallCommandRepository; import org.mandarin.booking.app.persist.MemberCommandRepository; import org.mandarin.booking.app.persist.ShowCommandRepository; diff --git a/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java index a5e2377..25ac2cf 100644 --- a/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java +++ b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import org.mandarin.booking.IntegrationTest; +import org.mandarin.booking.app.CustomAuthenticationProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; diff --git a/application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java index ad96c14..6cf29cd 100644 --- a/application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java +++ b/application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java @@ -12,7 +12,6 @@ import org.mandarin.booking.NoRestDocs; import org.mandarin.booking.adapter.security.JwtFilterTest.TestAuthController; import org.mandarin.booking.adapter.security.JwtFilterTest.TestAuthController.TestSecurityConfig; -import org.mandarin.booking.app.TokenUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; diff --git a/application/src/test/java/org/mandarin/booking/arch/BaseArchitectureTest.java b/application/src/test/java/org/mandarin/booking/arch/BaseArchitectureTest.java new file mode 100644 index 0000000..b7d429e --- /dev/null +++ b/application/src/test/java/org/mandarin/booking/arch/BaseArchitectureTest.java @@ -0,0 +1,18 @@ +package org.mandarin.booking.arch; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import org.junit.jupiter.api.BeforeAll; + +class BaseArchitectureTest { + protected static final String BASE = "org.mandarin.booking"; + protected static JavaClasses classes; + + @BeforeAll + static void importClasses() { + classes = new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages(BASE); + } +} diff --git a/application/src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java b/application/src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java deleted file mode 100644 index 4dfec37..0000000 --- a/application/src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.mandarin.booking.arch; - - -import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.importer.ImportOption; -import com.tngtech.archunit.junit.AnalyzeClasses; -import com.tngtech.archunit.junit.ArchTest; -import com.tngtech.archunit.library.Architectures; - -@AnalyzeClasses(packages = "org.mandarin.booking", importOptions = ImportOption.DoNotIncludeTests.class) -public class HexagonalArchitectureTest { - - @ArchTest - void hexagonalArchitectureTest(JavaClasses classes) { - Architectures - .layeredArchitecture() - .consideringAllDependencies() - .layer("adapter").definedBy("..adapter..") - .layer("application").definedBy("..app..") - .layer("domain").definedBy("..domain..") - .whereLayer("adapter").mayNotBeAccessedByAnyLayer() - .whereLayer("application").mayOnlyBeAccessedByLayers("adapter") - .whereLayer("domain").mayOnlyBeAccessedByLayers("adapter", "application") - .check(classes); - } -} diff --git a/application/src/test/java/org/mandarin/booking/arch/ModuleDependencyRulesTest.java b/application/src/test/java/org/mandarin/booking/arch/ModuleDependencyRulesTest.java new file mode 100644 index 0000000..f666bde --- /dev/null +++ b/application/src/test/java/org/mandarin/booking/arch/ModuleDependencyRulesTest.java @@ -0,0 +1,77 @@ +package org.mandarin.booking.arch; + + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static org.mandarin.booking.arch.ModuleDependencyRulesTest.Module.APPLICATION; +import static org.mandarin.booking.arch.ModuleDependencyRulesTest.Module.COMMON; +import static org.mandarin.booking.arch.ModuleDependencyRulesTest.Module.DOMAIN; +import static org.mandarin.booking.arch.ModuleDependencyRulesTest.Module.EXTERNAL; +import static org.mandarin.booking.arch.ModuleDependencyRulesTest.Module.INTERNAL; +import static org.mandarin.booking.arch.ModuleDependencyRulesTest.Module.packages; + +import org.junit.jupiter.api.Test; + +class ModuleDependencyRulesTest extends BaseArchitectureTest { + + @Test + void commonModuleShouldPure() { + noClasses().that().resideInAnyPackage(BASE + ".common..") + .should().dependOnClassesThat().resideInAnyPackage( + BASE + ".application..", + BASE + ".domain..", + BASE + ".internal..", + BASE + ".external.." + ) + .allowEmptyShould(true) + .check(classes); + } + + @Test + void onlyApplicationMayDependOnDomain() { + noClasses() + .that().resideInAnyPackage( + packages(COMMON, INTERNAL, EXTERNAL) + ) + .should().dependOnClassesThat().resideInAnyPackage(packages(APPLICATION, DOMAIN)) + .because("도메인 영역은 오직 application 계층에서만 의존 가능해야 한다") + .allowEmptyShould(true) + .check(classes); + } + + @Test + void domainShouldDependOnlyOnCommonAmongProjectModules() { + noClasses() + .that().resideInAnyPackage(packages(DOMAIN)) + .should().dependOnClassesThat() + .resideInAnyPackage( + packages(APPLICATION, + INTERNAL, + EXTERNAL) + ) + .because("domain은 프로젝트 내부 모듈 중 common만 의존할 수 있다") + .allowEmptyShould(true) + .check(classes); + } + + enum Module { + APPLICATION, + COMMON, + INTERNAL, + EXTERNAL, + DOMAIN; + + @Override + public String toString() { + return BASE + "." + name().toLowerCase(); + } + + static String[] packages(Module... modules) { + String[] packages = new String[modules.length]; + for (int i = 0; i < modules.length; i++) { + packages[i] = modules[i].toString() + ".."; + } + return packages; + } + + } +} diff --git a/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java index 2998300..b50a72f 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java @@ -19,9 +19,9 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mandarin.booking.IntegrationTest; import org.mandarin.booking.IntegrationTestUtils; +import org.mandarin.booking.TokenHolder; import org.mandarin.booking.app.persist.MemberQueryRepository; import org.mandarin.booking.domain.member.AuthRequest; -import org.mandarin.booking.domain.member.TokenHolder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; diff --git a/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java index 15894a2..b08a9f1 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java @@ -3,10 +3,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mandarin.booking.JwtTestUtils.assertJwtFormat; import static org.mandarin.booking.JwtTestUtils.getExpiration; +import static org.mandarin.booking.MemberAuthority.USER; import static org.mandarin.booking.adapter.webapi.ApiStatus.BAD_REQUEST; import static org.mandarin.booking.adapter.webapi.ApiStatus.SUCCESS; import static org.mandarin.booking.adapter.webapi.ApiStatus.UNAUTHORIZED; -import static org.mandarin.booking.domain.member.MemberAuthority.USER; import static org.mandarin.booking.fixture.MemberFixture.NicknameGenerator.generateNickName; import static org.mandarin.booking.fixture.MemberFixture.PasswordGenerator.generatePassword; import static org.mandarin.booking.fixture.MemberFixture.UserIdGenerator.generateUserId; @@ -20,9 +20,9 @@ import org.junit.jupiter.api.Test; import org.mandarin.booking.IntegrationTest; import org.mandarin.booking.IntegrationTestUtils; -import org.mandarin.booking.app.TokenUtils; +import org.mandarin.booking.TokenHolder; +import org.mandarin.booking.adapter.security.TokenUtils; import org.mandarin.booking.domain.member.ReissueRequest; -import org.mandarin.booking.domain.member.TokenHolder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.test.context.TestPropertySource; diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java index 3d54be5..3bd01ce 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java @@ -1,11 +1,11 @@ package org.mandarin.booking.webapi.show; import static org.assertj.core.api.Assertions.assertThat; +import static org.mandarin.booking.MemberAuthority.ADMIN; import static org.mandarin.booking.adapter.webapi.ApiStatus.BAD_REQUEST; import static org.mandarin.booking.adapter.webapi.ApiStatus.INTERNAL_SERVER_ERROR; import static org.mandarin.booking.adapter.webapi.ApiStatus.SUCCESS; import static org.mandarin.booking.adapter.webapi.ApiStatus.UNAUTHORIZED; -import static org.mandarin.booking.domain.member.MemberAuthority.ADMIN; import java.time.LocalDate; import java.util.List; diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java index 5b61658..68c2af9 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java @@ -2,14 +2,14 @@ import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; +import static org.mandarin.booking.MemberAuthority.ADMIN; +import static org.mandarin.booking.MemberAuthority.DISTRIBUTOR; +import static org.mandarin.booking.MemberAuthority.USER; import static org.mandarin.booking.adapter.webapi.ApiStatus.BAD_REQUEST; import static org.mandarin.booking.adapter.webapi.ApiStatus.FORBIDDEN; import static org.mandarin.booking.adapter.webapi.ApiStatus.INTERNAL_SERVER_ERROR; import static org.mandarin.booking.adapter.webapi.ApiStatus.NOT_FOUND; import static org.mandarin.booking.adapter.webapi.ApiStatus.SUCCESS; -import static org.mandarin.booking.domain.member.MemberAuthority.ADMIN; -import static org.mandarin.booking.domain.member.MemberAuthority.DISTRIBUTOR; -import static org.mandarin.booking.domain.member.MemberAuthority.USER; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/build.gradle b/build.gradle index 768b92f..4c536e9 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ subprojects { tasks.named('test') { useJUnitPlatform() - failFast = true +// failFast = true systemProperty 'spring.profiles.active', 'test' } } diff --git a/domain/src/main/java/org/mandarin/booking/domain/member/AuthException.java b/common/src/main/java/org/mandarin/booking/AuthException.java similarity index 57% rename from domain/src/main/java/org/mandarin/booking/domain/member/AuthException.java rename to common/src/main/java/org/mandarin/booking/AuthException.java index c037795..1c53b7e 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/member/AuthException.java +++ b/common/src/main/java/org/mandarin/booking/AuthException.java @@ -1,6 +1,4 @@ -package org.mandarin.booking.domain.member; - -import org.mandarin.booking.domain.DomainException; +package org.mandarin.booking; public class AuthException extends DomainException { public AuthException(String message) { diff --git a/domain/src/main/java/org/mandarin/booking/domain/DomainException.java b/common/src/main/java/org/mandarin/booking/DomainException.java similarity index 90% rename from domain/src/main/java/org/mandarin/booking/domain/DomainException.java rename to common/src/main/java/org/mandarin/booking/DomainException.java index 41d7a26..89afb8a 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/DomainException.java +++ b/common/src/main/java/org/mandarin/booking/DomainException.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.domain; +package org.mandarin.booking; import lombok.Getter; diff --git a/domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthority.java b/common/src/main/java/org/mandarin/booking/MemberAuthority.java similarity index 78% rename from domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthority.java rename to common/src/main/java/org/mandarin/booking/MemberAuthority.java index 1dc1701..cdbc1b8 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthority.java +++ b/common/src/main/java/org/mandarin/booking/MemberAuthority.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.domain.member; +package org.mandarin.booking; public enum MemberAuthority { USER, diff --git a/domain/src/main/java/org/mandarin/booking/domain/member/TokenHolder.java b/common/src/main/java/org/mandarin/booking/TokenHolder.java similarity index 62% rename from domain/src/main/java/org/mandarin/booking/domain/member/TokenHolder.java rename to common/src/main/java/org/mandarin/booking/TokenHolder.java index f91c42a..8be80fa 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/member/TokenHolder.java +++ b/common/src/main/java/org/mandarin/booking/TokenHolder.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.domain.member; +package org.mandarin.booking; public record TokenHolder(String accessToken, String refreshToken) { } diff --git a/domain/build.gradle b/domain/build.gradle index d4189e2..dba45aa 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -7,6 +7,7 @@ jar { } dependencies { + api project(':common') api 'org.springframework.boot:spring-boot-starter-data-jpa' api 'org.springframework.boot:spring-boot-starter-validation' diff --git a/domain/src/main/java/org/mandarin/booking/domain/member/Member.java b/domain/src/main/java/org/mandarin/booking/domain/member/Member.java index 3ff7b30..8203ff0 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/member/Member.java +++ b/domain/src/main/java/org/mandarin/booking/domain/member/Member.java @@ -6,6 +6,7 @@ import java.util.List; import lombok.Getter; import lombok.NoArgsConstructor; +import org.mandarin.booking.MemberAuthority; import org.mandarin.booking.domain.AbstractEntity; @Entity diff --git a/domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthorityConverter.java b/domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthorityConverter.java index ca9e2fe..05d9c9c 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthorityConverter.java +++ b/domain/src/main/java/org/mandarin/booking/domain/member/MemberAuthorityConverter.java @@ -7,6 +7,7 @@ import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; +import org.mandarin.booking.MemberAuthority; @Converter public class MemberAuthorityConverter implements AttributeConverter, String> { diff --git a/domain/src/main/java/org/mandarin/booking/domain/member/MemberException.java b/domain/src/main/java/org/mandarin/booking/domain/member/MemberException.java index 1fa3cf0..e629606 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/member/MemberException.java +++ b/domain/src/main/java/org/mandarin/booking/domain/member/MemberException.java @@ -1,6 +1,6 @@ package org.mandarin.booking.domain.member; -import org.mandarin.booking.domain.DomainException; +import org.mandarin.booking.DomainException; public class MemberException extends DomainException { public MemberException(String message) { diff --git a/domain/src/main/java/org/mandarin/booking/domain/show/ShowException.java b/domain/src/main/java/org/mandarin/booking/domain/show/ShowException.java index a562fae..47165e9 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/show/ShowException.java +++ b/domain/src/main/java/org/mandarin/booking/domain/show/ShowException.java @@ -1,6 +1,6 @@ package org.mandarin.booking.domain.show; -import org.mandarin.booking.domain.DomainException; +import org.mandarin.booking.DomainException; public class ShowException extends DomainException { public ShowException(String message) { diff --git a/domain/src/main/java/org/mandarin/booking/domain/venue/HallException.java b/domain/src/main/java/org/mandarin/booking/domain/venue/HallException.java index e65ebcb..62dd466 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/venue/HallException.java +++ b/domain/src/main/java/org/mandarin/booking/domain/venue/HallException.java @@ -1,6 +1,6 @@ package org.mandarin.booking.domain.venue; -import org.mandarin.booking.domain.DomainException; +import org.mandarin.booking.DomainException; public class HallException extends DomainException { public HallException(String status, String message) { diff --git a/domain/src/test/java/org/mandarin/booking/domain/MemberTest.java b/domain/src/test/java/org/mandarin/booking/domain/member/MemberTest.java similarity index 84% rename from domain/src/test/java/org/mandarin/booking/domain/MemberTest.java rename to domain/src/test/java/org/mandarin/booking/domain/member/MemberTest.java index 07e0ccf..0d3c806 100644 --- a/domain/src/test/java/org/mandarin/booking/domain/MemberTest.java +++ b/domain/src/test/java/org/mandarin/booking/domain/member/MemberTest.java @@ -1,12 +1,10 @@ -package org.mandarin.booking.domain; +package org.mandarin.booking.domain.member; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import org.junit.jupiter.api.Test; -import org.mandarin.booking.domain.member.Member; import org.mandarin.booking.domain.member.Member.MemberCreateCommand; -import org.mandarin.booking.domain.member.SecurePasswordEncoder; class MemberTest { private static final String MAIL = "test@test.com"; diff --git a/application/src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java b/internal/src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java similarity index 100% rename from application/src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java rename to internal/src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java diff --git a/application/src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java b/internal/src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java similarity index 100% rename from application/src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java rename to internal/src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java diff --git a/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java b/internal/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java similarity index 100% rename from application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java rename to internal/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java diff --git a/application/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java b/internal/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java similarity index 94% rename from application/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java rename to internal/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java index f39bb35..0fd64fb 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java @@ -2,7 +2,7 @@ import java.util.Collection; import org.jspecify.annotations.NullUnmarked; -import org.mandarin.booking.domain.member.MemberAuthority; +import org.mandarin.booking.MemberAuthority; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; diff --git a/application/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java b/internal/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java similarity index 94% rename from application/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java rename to internal/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java index 38556eb..22c1cce 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java @@ -9,9 +9,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jspecify.annotations.Nullable; -import org.mandarin.booking.app.TokenUtils; -import org.mandarin.booking.domain.member.AuthException; -import org.mandarin.booking.domain.member.MemberAuthority; +import org.mandarin.booking.AuthException; +import org.mandarin.booking.MemberAuthority; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; diff --git a/application/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java b/internal/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java similarity index 98% rename from application/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java rename to internal/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java index 79e5e73..63b5009 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java @@ -1,7 +1,6 @@ package org.mandarin.booking.adapter.security; import lombok.RequiredArgsConstructor; -import org.mandarin.booking.app.TokenUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; diff --git a/application/src/main/java/org/mandarin/booking/app/TokenUtils.java b/internal/src/main/java/org/mandarin/booking/adapter/security/TokenUtils.java similarity index 69% rename from application/src/main/java/org/mandarin/booking/app/TokenUtils.java rename to internal/src/main/java/org/mandarin/booking/adapter/security/TokenUtils.java index 368a93c..f328911 100644 --- a/application/src/main/java/org/mandarin/booking/app/TokenUtils.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/security/TokenUtils.java @@ -1,8 +1,8 @@ -package org.mandarin.booking.app; +package org.mandarin.booking.adapter.security; import java.util.Collection; -import org.mandarin.booking.domain.member.MemberAuthority; -import org.mandarin.booking.domain.member.TokenHolder; +import org.mandarin.booking.MemberAuthority; +import org.mandarin.booking.TokenHolder; public interface TokenUtils { TokenHolder generateToken(String refreshToken); diff --git a/application/src/main/java/org/mandarin/booking/adapter/security/package-info.java b/internal/src/main/java/org/mandarin/booking/adapter/security/package-info.java similarity index 100% rename from application/src/main/java/org/mandarin/booking/adapter/security/package-info.java rename to internal/src/main/java/org/mandarin/booking/adapter/security/package-info.java diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java b/internal/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java similarity index 100% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java rename to internal/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java b/internal/src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java similarity index 100% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java rename to internal/src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java b/internal/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java similarity index 100% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java rename to internal/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java b/internal/src/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java similarity index 100% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java rename to internal/src/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java b/internal/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java similarity index 94% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java rename to internal/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java index 90bd511..106a669 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java @@ -6,8 +6,8 @@ import static org.mandarin.booking.adapter.webapi.ApiStatus.UNAUTHORIZED; import lombok.extern.slf4j.Slf4j; -import org.mandarin.booking.domain.DomainException; -import org.mandarin.booking.domain.member.AuthException; +import org.mandarin.booking.AuthException; +import org.mandarin.booking.DomainException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java b/internal/src/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java similarity index 100% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java rename to internal/src/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/package-info.java b/internal/src/main/java/org/mandarin/booking/adapter/webapi/package-info.java similarity index 100% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/package-info.java rename to internal/src/main/java/org/mandarin/booking/adapter/webapi/package-info.java diff --git a/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java b/internal/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java similarity index 100% rename from application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java rename to internal/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java diff --git a/application/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java b/internal/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java similarity index 84% rename from application/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java rename to internal/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java index 11e7142..285059d 100644 --- a/application/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java +++ b/internal/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java @@ -1,7 +1,7 @@ package org.mandarin.booking.adapter.security; import static org.assertj.core.api.Assertions.assertThat; -import static org.mandarin.booking.domain.member.MemberAuthority.USER; +import static org.mandarin.booking.MemberAuthority.USER; import java.util.List; import org.junit.jupiter.api.Test; diff --git a/settings.gradle b/settings.gradle index 66463a8..e95bddf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,3 +2,6 @@ rootProject.name = 'booking' include ':application' include ':domain' + +include 'internal' +include 'common' \ No newline at end of file From feb8185d7fb173ea24def0ac54d07281c7da2db7 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Wed, 10 Sep 2025 17:05:17 +0900 Subject: [PATCH 07/36] modularize package structure and update imports --- .../org/mandarin/booking/adapter/webapi/ResponseWrapper.java | 3 +++ 1 file changed, 3 insertions(+) rename {application => internal}/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java (92%) diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java b/internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java similarity index 92% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java rename to internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java index 7339c0c..23f5912 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java @@ -23,6 +23,9 @@ public Object beforeBodyWrite(@Nullable final Object body, final MethodParameter final MediaType selectedContentType, final Class> selectedConverterType, final ServerHttpRequest request, final ServerHttpResponse response) { + if (body == null) { + return new SuccessResponse<>(ApiStatus.SUCCESS, "null"); + } return new SuccessResponse<>(ApiStatus.SUCCESS, body); } } From 6c1404ed6fb72e66182488d6f65a18dcaac231de Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Wed, 10 Sep 2025 17:05:23 +0900 Subject: [PATCH 08/36] configure build.gradle for modularization and add dependencies --- common/build.gradle | 9 +++++++++ internal/build.gradle | 13 +++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 common/build.gradle create mode 100644 internal/build.gradle diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000..6ef52a1 --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,9 @@ +bootJar { + enabled = false +} +jar { + enabled = true +} + +dependencies { +} diff --git a/internal/build.gradle b/internal/build.gradle new file mode 100644 index 0000000..0c3d9af --- /dev/null +++ b/internal/build.gradle @@ -0,0 +1,13 @@ +dependencies { + api(project(':common')) + + // ---- Spring Boot Web ---- + api 'org.springframework.boot:spring-boot-starter-web' + + // ---- Security & Auth ---- + api 'org.springframework.boot:spring-boot-starter-security' + api 'org.springframework.security:spring-security-test' + api 'io.jsonwebtoken:jjwt-api:0.12.6' + api 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' +} From 3c4e7773689999c260430f2aea5199067bc5633c Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 11 Sep 2025 10:23:56 +0900 Subject: [PATCH 09/36] remove null body handling from ResponseWrapper --- .../org/mandarin/booking/adapter/webapi/ResponseWrapper.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java b/internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java index 23f5912..7339c0c 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java @@ -23,9 +23,6 @@ public Object beforeBodyWrite(@Nullable final Object body, final MethodParameter final MediaType selectedContentType, final Class> selectedConverterType, final ServerHttpRequest request, final ServerHttpResponse response) { - if (body == null) { - return new SuccessResponse<>(ApiStatus.SUCCESS, "null"); - } return new SuccessResponse<>(ApiStatus.SUCCESS, body); } } From 70c0158e717779b2d149316c71cc142f862d5776 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 11 Sep 2025 10:24:52 +0900 Subject: [PATCH 10/36] ensure non-null body in ResponseWrapper --- .../org/mandarin/booking/adapter/webapi/ResponseWrapper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java b/internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java index 7339c0c..974e874 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java @@ -1,5 +1,6 @@ package org.mandarin.booking.adapter.webapi; +import java.util.Objects; import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; @@ -23,6 +24,6 @@ public Object beforeBodyWrite(@Nullable final Object body, final MethodParameter final MediaType selectedContentType, final Class> selectedConverterType, final ServerHttpRequest request, final ServerHttpResponse response) { - return new SuccessResponse<>(ApiStatus.SUCCESS, body); + return new SuccessResponse<>(ApiStatus.SUCCESS, Objects.requireNonNull(body)); } } From 7bfa33ff6a283b4a0ac28e2a2b1d845120439f24 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 11 Sep 2025 10:25:11 +0900 Subject: [PATCH 11/36] change dependency from api to implementation for common module --- application/build.gradle | 3 ++- domain/build.gradle | 2 +- internal/build.gradle | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/application/build.gradle b/application/build.gradle index 35b05a6..4d479d9 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -14,7 +14,7 @@ ext { dependencies { api(project(':domain')) - api(project(':common')) + implementation project(':common') api(project(':internal')) // ---- Spring Boot Core ---- implementation 'org.springframework.boot:spring-boot-starter' @@ -63,5 +63,6 @@ asciidoctor { tasks.named('test') { jvmArgs "-javaagent:${configurations.byteBuddyAgent.singleFile}" + jvmArgs "-javaagent:${configurations.testRuntimeClasspath.find { it.name.contains('mockito-core') }}", '-Xshare:off' outputs.dir snippetsDir } diff --git a/domain/build.gradle b/domain/build.gradle index dba45aa..fda90f7 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -7,7 +7,7 @@ jar { } dependencies { - api project(':common') + implementation project(':common') api 'org.springframework.boot:spring-boot-starter-data-jpa' api 'org.springframework.boot:spring-boot-starter-validation' diff --git a/internal/build.gradle b/internal/build.gradle index 0c3d9af..5901252 100644 --- a/internal/build.gradle +++ b/internal/build.gradle @@ -1,5 +1,5 @@ dependencies { - api(project(':common')) + implementation project(':common') // ---- Spring Boot Web ---- api 'org.springframework.boot:spring-boot-starter-web' From 6e522a32fb5bcffaf96368a24607584b5d76e22e Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 11 Sep 2025 10:35:16 +0900 Subject: [PATCH 12/36] remove java-test-fixtures plugin and update test task JVM arguments --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4c536e9..8ae3705 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,6 @@ subprojects { apply plugin: 'java-library' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' - apply plugin: 'java-test-fixtures' group = 'org.yechan' version = '1.0-SNAPSHOT' @@ -62,6 +61,7 @@ subprojects { tasks.named('test') { useJUnitPlatform() // failFast = true + jvmArgs = ['-XX:+EnableDynamicAgentLoading', '-Xshare:off'] systemProperty 'spring.profiles.active', 'test' } } From 6e3384a77e800910ef211b3ddf8ffc8a0af94b20 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 11 Sep 2025 10:43:54 +0900 Subject: [PATCH 13/36] remove unused canScheduleOn method from Hall class --- .../main/java/org/mandarin/booking/domain/venue/Hall.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/domain/src/main/java/org/mandarin/booking/domain/venue/Hall.java b/domain/src/main/java/org/mandarin/booking/domain/venue/Hall.java index 6411e01..6d508c4 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/venue/Hall.java +++ b/domain/src/main/java/org/mandarin/booking/domain/venue/Hall.java @@ -11,11 +11,6 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Hall extends AbstractEntity { -// public boolean canScheduleOn(LocalDateTime startAt, LocalDateTime endAt) { -// return showSchedules.stream() -// .noneMatch(schedule -> schedule.isConflict(startAt, endAt)); -// } - public static Hall create() { return new Hall(); } From ccd6aadad9736b1958d946c9440e5b5612f8a8c3 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 11 Sep 2025 11:16:57 +0900 Subject: [PATCH 14/36] modularize package structure by removing 'webapi' prefix from adapter classes --- .../java/org/mandarin/booking/app/AuthService.java | 2 +- .../booking/app/CustomAuthenticationProvider.java | 2 +- .../org/mandarin/booking/app/JwtTokenUtils.java | 2 +- .../org/mandarin/booking/IntegrationTestUtils.java | 2 +- .../booking/IntegrationTestUtilsSpecs.java | 2 +- .../test/java/org/mandarin/booking/TestConfig.java | 2 +- .../test/java/org/mandarin/booking/TestResult.java | 8 ++++---- .../security/CustomAuthenticationProviderTest.java | 1 + .../booking/adapter/security/JwtFilterTest.java | 8 +++++--- .../booking/webapi/auth/login/POST_specs.java | 6 +++--- .../booking/webapi/auth/reissue/POST_specs.java | 8 ++++---- .../not_found/GET_specs.java} | 9 ++++----- .../mandarin/booking/webapi/show/POST_specs.java | 8 ++++---- .../booking/webapi/show/schedule/POST_specs.java | 10 +++++----- .../booking/adapter/{webapi => }/ApiResponse.java | 2 +- .../org/mandarin/booking/adapter/ApiStatus.java | 10 ++++++++++ .../{webapi => }/CommonHttpMessageConverter.java | 4 ++-- .../{security => }/CustomAccessDeniedHandler.java | 7 +++---- .../CustomAuthenticationEntryPoint.java | 6 ++---- .../CustomMemberAuthenticationToken.java | 2 +- .../adapter/{webapi => }/ErrorResponse.java | 2 +- .../{webapi => }/GlobalExceptionHandler.java | 10 +++++----- .../booking/adapter/JacksonCustomizerConfig.java | 2 +- .../booking/adapter/{security => }/JwtFilter.java | 2 +- .../adapter/{webapi => }/ResponseWrapper.java | 4 ++-- .../adapter/{security => }/SecurityConfig.java | 4 ++-- .../adapter/{webapi => }/SuccessResponse.java | 2 +- .../booking/adapter/{security => }/TokenUtils.java | 2 +- .../booking/adapter/{webapi => }/package-info.java | 2 +- .../booking/adapter/security/package-info.java | 4 ---- .../mandarin/booking/adapter/webapi/ApiStatus.java | 14 -------------- .../adapter}/CommonHttpMessageConverterTest.java | 2 +- .../CustomAuthenticationEntryPointTest.java | 2 +- .../CustomMemberAuthenticationTokenTest.java | 2 +- 34 files changed, 73 insertions(+), 82 deletions(-) rename application/src/test/java/org/mandarin/booking/{adapter/webapi/GlobalExceptionHandlerTest.java => webapi/not_found/GET_specs.java} (78%) rename internal/src/main/java/org/mandarin/booking/adapter/{webapi => }/ApiResponse.java (88%) create mode 100644 internal/src/main/java/org/mandarin/booking/adapter/ApiStatus.java rename internal/src/main/java/org/mandarin/booking/adapter/{webapi => }/CommonHttpMessageConverter.java (94%) rename internal/src/main/java/org/mandarin/booking/adapter/{security => }/CustomAccessDeniedHandler.java (81%) rename internal/src/main/java/org/mandarin/booking/adapter/{security => }/CustomAuthenticationEntryPoint.java (84%) rename internal/src/main/java/org/mandarin/booking/adapter/{security => }/CustomMemberAuthenticationToken.java (95%) rename internal/src/main/java/org/mandarin/booking/adapter/{webapi => }/ErrorResponse.java (89%) rename internal/src/main/java/org/mandarin/booking/adapter/{webapi => }/GlobalExceptionHandler.java (83%) rename internal/src/main/java/org/mandarin/booking/adapter/{security => }/JwtFilter.java (98%) rename internal/src/main/java/org/mandarin/booking/adapter/{webapi => }/ResponseWrapper.java (92%) rename internal/src/main/java/org/mandarin/booking/adapter/{security => }/SecurityConfig.java (98%) rename internal/src/main/java/org/mandarin/booking/adapter/{webapi => }/SuccessResponse.java (86%) rename internal/src/main/java/org/mandarin/booking/adapter/{security => }/TokenUtils.java (90%) rename internal/src/main/java/org/mandarin/booking/adapter/{webapi => }/package-info.java (55%) delete mode 100644 internal/src/main/java/org/mandarin/booking/adapter/security/package-info.java delete mode 100644 internal/src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java rename {application/src/test/java/org/mandarin/booking/adapter/webapi => internal/src/test/java/org/mandarin/booking/adapter}/CommonHttpMessageConverterTest.java (94%) rename internal/src/test/java/org/mandarin/booking/adapter/{security => }/CustomAuthenticationEntryPointTest.java (98%) rename internal/src/test/java/org/mandarin/booking/adapter/{security => }/CustomMemberAuthenticationTokenTest.java (89%) diff --git a/application/src/main/java/org/mandarin/booking/app/AuthService.java b/application/src/main/java/org/mandarin/booking/app/AuthService.java index 5fc4166..b84cd8d 100644 --- a/application/src/main/java/org/mandarin/booking/app/AuthService.java +++ b/application/src/main/java/org/mandarin/booking/app/AuthService.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import org.mandarin.booking.AuthException; import org.mandarin.booking.TokenHolder; -import org.mandarin.booking.adapter.security.TokenUtils; +import org.mandarin.booking.adapter.TokenUtils; import org.mandarin.booking.app.persist.MemberQueryRepository; import org.mandarin.booking.app.port.AuthUseCase; import org.mandarin.booking.domain.member.Member; diff --git a/application/src/main/java/org/mandarin/booking/app/CustomAuthenticationProvider.java b/application/src/main/java/org/mandarin/booking/app/CustomAuthenticationProvider.java index 009b643..cf6f715 100644 --- a/application/src/main/java/org/mandarin/booking/app/CustomAuthenticationProvider.java +++ b/application/src/main/java/org/mandarin/booking/app/CustomAuthenticationProvider.java @@ -2,7 +2,7 @@ import lombok.RequiredArgsConstructor; import org.mandarin.booking.AuthException; -import org.mandarin.booking.adapter.security.CustomMemberAuthenticationToken; +import org.mandarin.booking.adapter.CustomMemberAuthenticationToken; import org.mandarin.booking.app.persist.MemberQueryRepository; import org.mandarin.booking.domain.member.Member; import org.springframework.security.authentication.AuthenticationProvider; diff --git a/application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java b/application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java index a0ffd89..7bb3c5c 100644 --- a/application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java +++ b/application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java @@ -18,7 +18,7 @@ import org.mandarin.booking.AuthException; import org.mandarin.booking.MemberAuthority; import org.mandarin.booking.TokenHolder; -import org.mandarin.booking.adapter.security.TokenUtils; +import org.mandarin.booking.adapter.TokenUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; diff --git a/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java b/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java index 0568527..826e108 100644 --- a/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java +++ b/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java @@ -11,7 +11,7 @@ import java.util.Collection; import java.util.List; import java.util.UUID; -import org.mandarin.booking.adapter.security.TokenUtils; +import org.mandarin.booking.adapter.TokenUtils; import org.mandarin.booking.app.persist.HallCommandRepository; import org.mandarin.booking.app.persist.MemberCommandRepository; import org.mandarin.booking.app.persist.ShowCommandRepository; diff --git a/application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java b/application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java index e5d9472..f5d2296 100644 --- a/application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java +++ b/application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java @@ -1,7 +1,7 @@ package org.mandarin.booking; import static org.assertj.core.api.Assertions.assertThat; -import static org.mandarin.booking.adapter.webapi.ApiStatus.SUCCESS; +import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; import com.fasterxml.jackson.core.type.TypeReference; import java.util.HashMap; diff --git a/application/src/test/java/org/mandarin/booking/TestConfig.java b/application/src/test/java/org/mandarin/booking/TestConfig.java index 257cf99..2e95a6c 100644 --- a/application/src/test/java/org/mandarin/booking/TestConfig.java +++ b/application/src/test/java/org/mandarin/booking/TestConfig.java @@ -1,7 +1,7 @@ package org.mandarin.booking; import com.fasterxml.jackson.databind.ObjectMapper; -import org.mandarin.booking.adapter.security.TokenUtils; +import org.mandarin.booking.adapter.TokenUtils; import org.mandarin.booking.app.persist.HallCommandRepository; import org.mandarin.booking.app.persist.MemberCommandRepository; import org.mandarin.booking.app.persist.ShowCommandRepository; diff --git a/application/src/test/java/org/mandarin/booking/TestResult.java b/application/src/test/java/org/mandarin/booking/TestResult.java index eeb978a..8cc6714 100644 --- a/application/src/test/java/org/mandarin/booking/TestResult.java +++ b/application/src/test/java/org/mandarin/booking/TestResult.java @@ -8,10 +8,10 @@ import java.util.HashMap; import java.util.Map; import org.jspecify.annotations.NonNull; -import org.mandarin.booking.adapter.webapi.ApiResponse; -import org.mandarin.booking.adapter.webapi.ApiStatus; -import org.mandarin.booking.adapter.webapi.ErrorResponse; -import org.mandarin.booking.adapter.webapi.SuccessResponse; +import org.mandarin.booking.adapter.ApiResponse; +import org.mandarin.booking.adapter.ApiStatus; +import org.mandarin.booking.adapter.ErrorResponse; +import org.mandarin.booking.adapter.SuccessResponse; public class TestResult { private Executor executor; diff --git a/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java index 25ac2cf..2b34508 100644 --- a/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java +++ b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import org.mandarin.booking.IntegrationTest; +import org.mandarin.booking.adapter.CustomMemberAuthenticationToken; import org.mandarin.booking.app.CustomAuthenticationProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; diff --git a/application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java index 6cf29cd..d24bbef 100644 --- a/application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java +++ b/application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java @@ -1,15 +1,17 @@ package org.mandarin.booking.adapter.security; import static org.assertj.core.api.Assertions.assertThat; -import static org.mandarin.booking.adapter.webapi.ApiStatus.FORBIDDEN; -import static org.mandarin.booking.adapter.webapi.ApiStatus.SUCCESS; -import static org.mandarin.booking.adapter.webapi.ApiStatus.UNAUTHORIZED; +import static org.mandarin.booking.adapter.ApiStatus.FORBIDDEN; +import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; +import static org.mandarin.booking.adapter.ApiStatus.UNAUTHORIZED; import java.util.List; import org.junit.jupiter.api.Test; import org.mandarin.booking.IntegrationTest; import org.mandarin.booking.IntegrationTestUtils; import org.mandarin.booking.NoRestDocs; +import org.mandarin.booking.adapter.JwtFilter; +import org.mandarin.booking.adapter.TokenUtils; import org.mandarin.booking.adapter.security.JwtFilterTest.TestAuthController; import org.mandarin.booking.adapter.security.JwtFilterTest.TestAuthController.TestSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; diff --git a/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java index b50a72f..b9e907c 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java @@ -4,9 +4,9 @@ import static org.mandarin.booking.JwtTestUtils.assertJwtFormat; import static org.mandarin.booking.JwtTestUtils.getExpiration; import static org.mandarin.booking.JwtTestUtils.getTokenClaims; -import static org.mandarin.booking.adapter.webapi.ApiStatus.BAD_REQUEST; -import static org.mandarin.booking.adapter.webapi.ApiStatus.SUCCESS; -import static org.mandarin.booking.adapter.webapi.ApiStatus.UNAUTHORIZED; +import static org.mandarin.booking.adapter.ApiStatus.BAD_REQUEST; +import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; +import static org.mandarin.booking.adapter.ApiStatus.UNAUTHORIZED; import static org.mandarin.booking.fixture.MemberFixture.PasswordGenerator.generatePassword; import static org.mandarin.booking.fixture.MemberFixture.UserIdGenerator.generateUserId; diff --git a/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java index b08a9f1..1e565b4 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java @@ -4,9 +4,9 @@ import static org.mandarin.booking.JwtTestUtils.assertJwtFormat; import static org.mandarin.booking.JwtTestUtils.getExpiration; import static org.mandarin.booking.MemberAuthority.USER; -import static org.mandarin.booking.adapter.webapi.ApiStatus.BAD_REQUEST; -import static org.mandarin.booking.adapter.webapi.ApiStatus.SUCCESS; -import static org.mandarin.booking.adapter.webapi.ApiStatus.UNAUTHORIZED; +import static org.mandarin.booking.adapter.ApiStatus.BAD_REQUEST; +import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; +import static org.mandarin.booking.adapter.ApiStatus.UNAUTHORIZED; import static org.mandarin.booking.fixture.MemberFixture.NicknameGenerator.generateNickName; import static org.mandarin.booking.fixture.MemberFixture.PasswordGenerator.generatePassword; import static org.mandarin.booking.fixture.MemberFixture.UserIdGenerator.generateUserId; @@ -21,7 +21,7 @@ import org.mandarin.booking.IntegrationTest; import org.mandarin.booking.IntegrationTestUtils; import org.mandarin.booking.TokenHolder; -import org.mandarin.booking.adapter.security.TokenUtils; +import org.mandarin.booking.adapter.TokenUtils; import org.mandarin.booking.domain.member.ReissueRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; diff --git a/application/src/test/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandlerTest.java b/application/src/test/java/org/mandarin/booking/webapi/not_found/GET_specs.java similarity index 78% rename from application/src/test/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandlerTest.java rename to application/src/test/java/org/mandarin/booking/webapi/not_found/GET_specs.java index 33b6069..822fb91 100644 --- a/application/src/test/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandlerTest.java +++ b/application/src/test/java/org/mandarin/booking/webapi/not_found/GET_specs.java @@ -1,7 +1,7 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.webapi.not_found; import static org.assertj.core.api.Assertions.assertThat; -import static org.mandarin.booking.adapter.webapi.ApiStatus.NOT_FOUND; +import static org.mandarin.booking.adapter.ApiStatus.NOT_FOUND; import org.junit.jupiter.api.Test; import org.mandarin.booking.IntegrationTest; @@ -11,10 +11,9 @@ @IntegrationTest @NoRestDocs -class GlobalExceptionHandlerTest { - +public class GET_specs { @Test - void endpointNotFound(@Autowired IntegrationTestUtils testUtils){ + void endpointNotFound(@Autowired IntegrationTestUtils testUtils) { // Act var request = testUtils.get("/not-found") .assertFailure(); diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java index 3bd01ce..68ccd89 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java @@ -2,10 +2,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mandarin.booking.MemberAuthority.ADMIN; -import static org.mandarin.booking.adapter.webapi.ApiStatus.BAD_REQUEST; -import static org.mandarin.booking.adapter.webapi.ApiStatus.INTERNAL_SERVER_ERROR; -import static org.mandarin.booking.adapter.webapi.ApiStatus.SUCCESS; -import static org.mandarin.booking.adapter.webapi.ApiStatus.UNAUTHORIZED; +import static org.mandarin.booking.adapter.ApiStatus.BAD_REQUEST; +import static org.mandarin.booking.adapter.ApiStatus.INTERNAL_SERVER_ERROR; +import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; +import static org.mandarin.booking.adapter.ApiStatus.UNAUTHORIZED; import java.time.LocalDate; import java.util.List; diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java index 68c2af9..8741754 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java @@ -5,11 +5,11 @@ import static org.mandarin.booking.MemberAuthority.ADMIN; import static org.mandarin.booking.MemberAuthority.DISTRIBUTOR; import static org.mandarin.booking.MemberAuthority.USER; -import static org.mandarin.booking.adapter.webapi.ApiStatus.BAD_REQUEST; -import static org.mandarin.booking.adapter.webapi.ApiStatus.FORBIDDEN; -import static org.mandarin.booking.adapter.webapi.ApiStatus.INTERNAL_SERVER_ERROR; -import static org.mandarin.booking.adapter.webapi.ApiStatus.NOT_FOUND; -import static org.mandarin.booking.adapter.webapi.ApiStatus.SUCCESS; +import static org.mandarin.booking.adapter.ApiStatus.BAD_REQUEST; +import static org.mandarin.booking.adapter.ApiStatus.FORBIDDEN; +import static org.mandarin.booking.adapter.ApiStatus.INTERNAL_SERVER_ERROR; +import static org.mandarin.booking.adapter.ApiStatus.NOT_FOUND; +import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/internal/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java b/internal/src/main/java/org/mandarin/booking/adapter/ApiResponse.java similarity index 88% rename from internal/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java rename to internal/src/main/java/org/mandarin/booking/adapter/ApiResponse.java index ea67ea5..7e0dbd2 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/webapi/ApiResponse.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/ApiResponse.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.adapter; import java.time.LocalDateTime; import lombok.Getter; diff --git a/internal/src/main/java/org/mandarin/booking/adapter/ApiStatus.java b/internal/src/main/java/org/mandarin/booking/adapter/ApiStatus.java new file mode 100644 index 0000000..1b899ff --- /dev/null +++ b/internal/src/main/java/org/mandarin/booking/adapter/ApiStatus.java @@ -0,0 +1,10 @@ +package org.mandarin.booking.adapter; + +public enum ApiStatus { + SUCCESS, + BAD_REQUEST, + UNAUTHORIZED, + INTERNAL_SERVER_ERROR, + FORBIDDEN, + NOT_FOUND +} diff --git a/internal/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java b/internal/src/main/java/org/mandarin/booking/adapter/CommonHttpMessageConverter.java similarity index 94% rename from internal/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java rename to internal/src/main/java/org/mandarin/booking/adapter/CommonHttpMessageConverter.java index 6230360..28f70f7 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverter.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/CommonHttpMessageConverter.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.adapter; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; @@ -18,7 +18,7 @@ @Component @Order(Ordered.HIGHEST_PRECEDENCE) -public class CommonHttpMessageConverter extends AbstractHttpMessageConverter { +class CommonHttpMessageConverter extends AbstractHttpMessageConverter { private final ObjectMapper objectMapper; public CommonHttpMessageConverter(ObjectMapper objectMapper) { diff --git a/internal/src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java b/internal/src/main/java/org/mandarin/booking/adapter/CustomAccessDeniedHandler.java similarity index 81% rename from internal/src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java rename to internal/src/main/java/org/mandarin/booking/adapter/CustomAccessDeniedHandler.java index 8ec2f83..335de1b 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/CustomAccessDeniedHandler.java @@ -1,6 +1,6 @@ -package org.mandarin.booking.adapter.security; +package org.mandarin.booking.adapter; -import static org.mandarin.booking.adapter.webapi.ApiStatus.FORBIDDEN; +import static org.mandarin.booking.adapter.ApiStatus.FORBIDDEN; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; @@ -8,7 +8,6 @@ import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.mandarin.booking.adapter.webapi.ErrorResponse; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; @@ -16,7 +15,7 @@ @Slf4j @Component @RequiredArgsConstructor -public class CustomAccessDeniedHandler implements AccessDeniedHandler { +class CustomAccessDeniedHandler implements AccessDeniedHandler { private final ObjectMapper objectMapper; @Override diff --git a/internal/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java b/internal/src/main/java/org/mandarin/booking/adapter/CustomAuthenticationEntryPoint.java similarity index 84% rename from internal/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java rename to internal/src/main/java/org/mandarin/booking/adapter/CustomAuthenticationEntryPoint.java index c1a85e9..40e59e8 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/CustomAuthenticationEntryPoint.java @@ -1,19 +1,17 @@ -package org.mandarin.booking.adapter.security; +package org.mandarin.booking.adapter; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import lombok.RequiredArgsConstructor; -import org.mandarin.booking.adapter.webapi.ApiStatus; -import org.mandarin.booking.adapter.webapi.ErrorResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor -public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { +class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { private final ObjectMapper objectMapper; @Override diff --git a/internal/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java b/internal/src/main/java/org/mandarin/booking/adapter/CustomMemberAuthenticationToken.java similarity index 95% rename from internal/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java rename to internal/src/main/java/org/mandarin/booking/adapter/CustomMemberAuthenticationToken.java index 0fd64fb..db83926 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationToken.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/CustomMemberAuthenticationToken.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.security; +package org.mandarin.booking.adapter; import java.util.Collection; import org.jspecify.annotations.NullUnmarked; diff --git a/internal/src/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java b/internal/src/main/java/org/mandarin/booking/adapter/ErrorResponse.java similarity index 89% rename from internal/src/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java rename to internal/src/main/java/org/mandarin/booking/adapter/ErrorResponse.java index 28f7cdb..7db3cdf 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/webapi/ErrorResponse.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/ErrorResponse.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.adapter; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.ToString; diff --git a/internal/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java b/internal/src/main/java/org/mandarin/booking/adapter/GlobalExceptionHandler.java similarity index 83% rename from internal/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java rename to internal/src/main/java/org/mandarin/booking/adapter/GlobalExceptionHandler.java index 106a669..52f4860 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/webapi/GlobalExceptionHandler.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/GlobalExceptionHandler.java @@ -1,9 +1,9 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.adapter; import static java.util.Objects.requireNonNull; -import static org.mandarin.booking.adapter.webapi.ApiStatus.BAD_REQUEST; -import static org.mandarin.booking.adapter.webapi.ApiStatus.NOT_FOUND; -import static org.mandarin.booking.adapter.webapi.ApiStatus.UNAUTHORIZED; +import static org.mandarin.booking.adapter.ApiStatus.BAD_REQUEST; +import static org.mandarin.booking.adapter.ApiStatus.NOT_FOUND; +import static org.mandarin.booking.adapter.ApiStatus.UNAUTHORIZED; import lombok.extern.slf4j.Slf4j; import org.mandarin.booking.AuthException; @@ -15,7 +15,7 @@ @Slf4j @RestControllerAdvice -public class GlobalExceptionHandler { +class GlobalExceptionHandler { @ExceptionHandler(DomainException.class) public ErrorResponse handleJsonParseError(DomainException ex) { diff --git a/internal/src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java b/internal/src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java index c1a70af..139531d 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/JacksonCustomizerConfig.java @@ -5,7 +5,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -public class JacksonCustomizerConfig { +class JacksonCustomizerConfig { @Bean Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { return builder -> { diff --git a/internal/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java b/internal/src/main/java/org/mandarin/booking/adapter/JwtFilter.java similarity index 98% rename from internal/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java rename to internal/src/main/java/org/mandarin/booking/adapter/JwtFilter.java index 22c1cce..0b7e9a4 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/JwtFilter.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.security; +package org.mandarin.booking.adapter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java b/internal/src/main/java/org/mandarin/booking/adapter/ResponseWrapper.java similarity index 92% rename from internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java rename to internal/src/main/java/org/mandarin/booking/adapter/ResponseWrapper.java index 974e874..0495c3f 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/webapi/ResponseWrapper.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/ResponseWrapper.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.adapter; import java.util.Objects; import org.jspecify.annotations.Nullable; @@ -12,7 +12,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; @RestControllerAdvice -public class ResponseWrapper implements ResponseBodyAdvice { +class ResponseWrapper implements ResponseBodyAdvice { @Override public boolean supports(final MethodParameter returnType, final Class> converterType) { diff --git a/internal/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java b/internal/src/main/java/org/mandarin/booking/adapter/SecurityConfig.java similarity index 98% rename from internal/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java rename to internal/src/main/java/org/mandarin/booking/adapter/SecurityConfig.java index 63b5009..a277d6f 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/SecurityConfig.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.security; +package org.mandarin.booking.adapter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -21,7 +21,7 @@ @Configuration @RequiredArgsConstructor -public class SecurityConfig { +class SecurityConfig { private final TokenUtils tokenUtils; @Bean diff --git a/internal/src/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java b/internal/src/main/java/org/mandarin/booking/adapter/SuccessResponse.java similarity index 86% rename from internal/src/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java rename to internal/src/main/java/org/mandarin/booking/adapter/SuccessResponse.java index 7d7e55f..4465a0b 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/webapi/SuccessResponse.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/SuccessResponse.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.adapter; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/internal/src/main/java/org/mandarin/booking/adapter/security/TokenUtils.java b/internal/src/main/java/org/mandarin/booking/adapter/TokenUtils.java similarity index 90% rename from internal/src/main/java/org/mandarin/booking/adapter/security/TokenUtils.java rename to internal/src/main/java/org/mandarin/booking/adapter/TokenUtils.java index f328911..d9a31f1 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/security/TokenUtils.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/TokenUtils.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.security; +package org.mandarin.booking.adapter; import java.util.Collection; import org.mandarin.booking.MemberAuthority; diff --git a/internal/src/main/java/org/mandarin/booking/adapter/webapi/package-info.java b/internal/src/main/java/org/mandarin/booking/adapter/package-info.java similarity index 55% rename from internal/src/main/java/org/mandarin/booking/adapter/webapi/package-info.java rename to internal/src/main/java/org/mandarin/booking/adapter/package-info.java index c8fa41a..f54caa2 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/webapi/package-info.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.adapter; import org.jspecify.annotations.NullMarked; diff --git a/internal/src/main/java/org/mandarin/booking/adapter/security/package-info.java b/internal/src/main/java/org/mandarin/booking/adapter/security/package-info.java deleted file mode 100644 index b212696..0000000 --- a/internal/src/main/java/org/mandarin/booking/adapter/security/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -@NullMarked -package org.mandarin.booking.adapter.security; - -import org.jspecify.annotations.NullMarked; diff --git a/internal/src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java b/internal/src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java deleted file mode 100644 index bba5da6..0000000 --- a/internal/src/main/java/org/mandarin/booking/adapter/webapi/ApiStatus.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.mandarin.booking.adapter.webapi; - -/** - * Centralizes API status codes to achieve type-safety and remove string duplication. - * JSON representation remains identical via enum.name(). - */ -public enum ApiStatus { - SUCCESS, - BAD_REQUEST, - UNAUTHORIZED, - INTERNAL_SERVER_ERROR, - FORBIDDEN, - NOT_FOUND -} diff --git a/application/src/test/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverterTest.java b/internal/src/test/java/org/mandarin/booking/adapter/CommonHttpMessageConverterTest.java similarity index 94% rename from application/src/test/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverterTest.java rename to internal/src/test/java/org/mandarin/booking/adapter/CommonHttpMessageConverterTest.java index 8694a1f..d5a9f15 100644 --- a/application/src/test/java/org/mandarin/booking/adapter/webapi/CommonHttpMessageConverterTest.java +++ b/internal/src/test/java/org/mandarin/booking/adapter/CommonHttpMessageConverterTest.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.adapter; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; diff --git a/internal/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java b/internal/src/test/java/org/mandarin/booking/adapter/CustomAuthenticationEntryPointTest.java similarity index 98% rename from internal/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java rename to internal/src/test/java/org/mandarin/booking/adapter/CustomAuthenticationEntryPointTest.java index dd81059..b17dc28 100644 --- a/internal/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPointTest.java +++ b/internal/src/test/java/org/mandarin/booking/adapter/CustomAuthenticationEntryPointTest.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.security; +package org.mandarin.booking.adapter; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; diff --git a/internal/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java b/internal/src/test/java/org/mandarin/booking/adapter/CustomMemberAuthenticationTokenTest.java similarity index 89% rename from internal/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java rename to internal/src/test/java/org/mandarin/booking/adapter/CustomMemberAuthenticationTokenTest.java index 285059d..92ec5cd 100644 --- a/internal/src/test/java/org/mandarin/booking/adapter/security/CustomMemberAuthenticationTokenTest.java +++ b/internal/src/test/java/org/mandarin/booking/adapter/CustomMemberAuthenticationTokenTest.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.adapter.security; +package org.mandarin.booking.adapter; import static org.assertj.core.api.Assertions.assertThat; import static org.mandarin.booking.MemberAuthority.USER; From c652a9ea37b0540579225ec52f789024706556e8 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 11 Sep 2025 11:27:20 +0900 Subject: [PATCH 15/36] rename test classes and reorganize package structure --- application/build.gradle | 1 - .../security}/BCryptSecurePasswordEncoder.java | 2 +- .../security}/CustomAuthenticationProvider.java | 2 +- .../java/org/mandarin/booking/app/QuerydslConfig.java | 2 +- .../org/mandarin/booking/{app => }/LoggingAspectTest.java | 3 +-- .../{JwtFilterTest.java => AuthIntegrationTest.java} | 6 +++--- .../security/CustomAuthenticationProviderTest.java | 1 - internal/build.gradle | 1 + .../src/main/java/org/mandarin/booking}/Log.java | 2 +- .../main/java/org/mandarin/booking}/LoggingAspect.java | 8 ++++---- .../java/org/mandarin/booking/adapter}/JwtTokenUtils.java | 3 +-- 11 files changed, 14 insertions(+), 17 deletions(-) rename application/src/main/java/org/mandarin/booking/{app => adapter/security}/BCryptSecurePasswordEncoder.java (93%) rename application/src/main/java/org/mandarin/booking/{app => adapter/security}/CustomAuthenticationProvider.java (97%) rename application/src/test/java/org/mandarin/booking/{app => }/LoggingAspectTest.java (99%) rename application/src/test/java/org/mandarin/booking/adapter/security/{JwtFilterTest.java => AuthIntegrationTest.java} (96%) rename {application/src/main/java/org/mandarin/booking/app => internal/src/main/java/org/mandarin/booking}/Log.java (90%) rename {application/src/main/java/org/mandarin/booking/app => internal/src/main/java/org/mandarin/booking}/LoggingAspect.java (93%) rename {application/src/main/java/org/mandarin/booking/app => internal/src/main/java/org/mandarin/booking/adapter}/JwtTokenUtils.java (98%) diff --git a/application/build.gradle b/application/build.gradle index 4d479d9..e5034f8 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -19,7 +19,6 @@ dependencies { // ---- 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-aop' // ---- Data & Database ---- implementation 'com.mysql:mysql-connector-j:8.3.0' diff --git a/application/src/main/java/org/mandarin/booking/app/BCryptSecurePasswordEncoder.java b/application/src/main/java/org/mandarin/booking/adapter/security/BCryptSecurePasswordEncoder.java similarity index 93% rename from application/src/main/java/org/mandarin/booking/app/BCryptSecurePasswordEncoder.java rename to application/src/main/java/org/mandarin/booking/adapter/security/BCryptSecurePasswordEncoder.java index ef46a77..ad7bc32 100644 --- a/application/src/main/java/org/mandarin/booking/app/BCryptSecurePasswordEncoder.java +++ b/application/src/main/java/org/mandarin/booking/adapter/security/BCryptSecurePasswordEncoder.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app; +package org.mandarin.booking.adapter.security; import lombok.RequiredArgsConstructor; import org.mandarin.booking.domain.member.SecurePasswordEncoder; diff --git a/application/src/main/java/org/mandarin/booking/app/CustomAuthenticationProvider.java b/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java similarity index 97% rename from application/src/main/java/org/mandarin/booking/app/CustomAuthenticationProvider.java rename to application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java index cf6f715..2e81e06 100644 --- a/application/src/main/java/org/mandarin/booking/app/CustomAuthenticationProvider.java +++ b/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app; +package org.mandarin.booking.adapter.security; import lombok.RequiredArgsConstructor; import org.mandarin.booking.AuthException; diff --git a/application/src/main/java/org/mandarin/booking/app/QuerydslConfig.java b/application/src/main/java/org/mandarin/booking/app/QuerydslConfig.java index a622bfb..95f81d4 100644 --- a/application/src/main/java/org/mandarin/booking/app/QuerydslConfig.java +++ b/application/src/main/java/org/mandarin/booking/app/QuerydslConfig.java @@ -7,7 +7,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -public class QuerydslConfig { +class QuerydslConfig { @PersistenceContext private EntityManager entityManager; diff --git a/application/src/test/java/org/mandarin/booking/app/LoggingAspectTest.java b/application/src/test/java/org/mandarin/booking/LoggingAspectTest.java similarity index 99% rename from application/src/test/java/org/mandarin/booking/app/LoggingAspectTest.java rename to application/src/test/java/org/mandarin/booking/LoggingAspectTest.java index 941e84d..dd44be8 100644 --- a/application/src/test/java/org/mandarin/booking/app/LoggingAspectTest.java +++ b/application/src/test/java/org/mandarin/booking/LoggingAspectTest.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app; +package org.mandarin.booking; import static ch.qos.logback.classic.Level.DEBUG; import static ch.qos.logback.classic.Level.ERROR; @@ -18,7 +18,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mandarin.booking.IntegrationTest; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; diff --git a/application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/AuthIntegrationTest.java similarity index 96% rename from application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java rename to application/src/test/java/org/mandarin/booking/adapter/security/AuthIntegrationTest.java index d24bbef..60e431f 100644 --- a/application/src/test/java/org/mandarin/booking/adapter/security/JwtFilterTest.java +++ b/application/src/test/java/org/mandarin/booking/adapter/security/AuthIntegrationTest.java @@ -12,8 +12,8 @@ import org.mandarin.booking.NoRestDocs; import org.mandarin.booking.adapter.JwtFilter; import org.mandarin.booking.adapter.TokenUtils; -import org.mandarin.booking.adapter.security.JwtFilterTest.TestAuthController; -import org.mandarin.booking.adapter.security.JwtFilterTest.TestAuthController.TestSecurityConfig; +import org.mandarin.booking.adapter.security.AuthIntegrationTest.TestAuthController; +import org.mandarin.booking.adapter.security.AuthIntegrationTest.TestAuthController.TestSecurityConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; @@ -35,7 +35,7 @@ @IntegrationTest @NoRestDocs @Import({TestSecurityConfig.class, TestAuthController.class}) -class JwtFilterTest { +class AuthIntegrationTest { private static final String PONG_WITHOUT_AUTH = "pong without auth"; private static final String PONG_WITH_AUTH = "pong with auth"; private static final String WITH_USER_ROLE = "pong with user role"; diff --git a/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java index 2b34508..472f541 100644 --- a/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java +++ b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test; import org.mandarin.booking.IntegrationTest; import org.mandarin.booking.adapter.CustomMemberAuthenticationToken; -import org.mandarin.booking.app.CustomAuthenticationProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; diff --git a/internal/build.gradle b/internal/build.gradle index 5901252..e14cb97 100644 --- a/internal/build.gradle +++ b/internal/build.gradle @@ -10,4 +10,5 @@ dependencies { api 'io.jsonwebtoken:jjwt-api:0.12.6' api 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + implementation 'org.springframework.boot:spring-boot-starter-aop' } diff --git a/application/src/main/java/org/mandarin/booking/app/Log.java b/internal/src/main/java/org/mandarin/booking/Log.java similarity index 90% rename from application/src/main/java/org/mandarin/booking/app/Log.java rename to internal/src/main/java/org/mandarin/booking/Log.java index 39e7990..d9f4d84 100644 --- a/application/src/main/java/org/mandarin/booking/app/Log.java +++ b/internal/src/main/java/org/mandarin/booking/Log.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app; +package org.mandarin.booking; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/application/src/main/java/org/mandarin/booking/app/LoggingAspect.java b/internal/src/main/java/org/mandarin/booking/LoggingAspect.java similarity index 93% rename from application/src/main/java/org/mandarin/booking/app/LoggingAspect.java rename to internal/src/main/java/org/mandarin/booking/LoggingAspect.java index 901bfd9..99e01f6 100644 --- a/application/src/main/java/org/mandarin/booking/app/LoggingAspect.java +++ b/internal/src/main/java/org/mandarin/booking/LoggingAspect.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app; +package org.mandarin.booking; import java.lang.reflect.Method; import java.time.LocalDateTime; @@ -19,9 +19,9 @@ @Component @Slf4j @RequiredArgsConstructor -public class LoggingAspect { +class LoggingAspect { - @Around("@within(org.mandarin.booking.app.Log) || @annotation(org.mandarin.booking.app.Log)") + @Around("@within(org.mandarin.booking.Log) || @annotation(org.mandarin.booking.Log)") public Object around(ProceedingJoinPoint jp) throws Throwable { Logger logger = selectTargetLogger(jp); String level = resolveScope(jp); @@ -40,7 +40,7 @@ public Object around(ProceedingJoinPoint jp) throws Throwable { @AfterThrowing( argNames = "pjp,ex", - pointcut = "@within(org.mandarin.booking.app.Log) || @annotation(org.mandarin.booking.app.Log)", + pointcut = "@within(org.mandarin.booking.Log) || @annotation(org.mandarin.booking.Log)", throwing = "ex" ) public void afterThrowing(JoinPoint pjp, Throwable ex) { diff --git a/application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java b/internal/src/main/java/org/mandarin/booking/adapter/JwtTokenUtils.java similarity index 98% rename from application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java rename to internal/src/main/java/org/mandarin/booking/adapter/JwtTokenUtils.java index 7bb3c5c..04245ab 100644 --- a/application/src/main/java/org/mandarin/booking/app/JwtTokenUtils.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/JwtTokenUtils.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app; +package org.mandarin.booking.adapter; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; @@ -18,7 +18,6 @@ import org.mandarin.booking.AuthException; import org.mandarin.booking.MemberAuthority; import org.mandarin.booking.TokenHolder; -import org.mandarin.booking.adapter.TokenUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; From 50b43311787a2ec367337acc74941c269a99ac87 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 11 Sep 2025 11:35:13 +0900 Subject: [PATCH 16/36] remove public modifier from BCryptSecurePasswordEncoder and CustomAuthenticationProvider classes --- application/build.gradle | 3 ++- .../booking/adapter/security/BCryptSecurePasswordEncoder.java | 2 +- .../booking/adapter/security/CustomAuthenticationProvider.java | 2 +- .../mandarin/booking/domain/member/SecurePasswordEncoder.java | 3 +++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/application/build.gradle b/application/build.gradle index e5034f8..a267113 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -14,8 +14,9 @@ ext { dependencies { api(project(':domain')) - implementation project(':common') api(project(':internal')) + + implementation project(':common') // ---- Spring Boot Core ---- implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-actuator' diff --git a/application/src/main/java/org/mandarin/booking/adapter/security/BCryptSecurePasswordEncoder.java b/application/src/main/java/org/mandarin/booking/adapter/security/BCryptSecurePasswordEncoder.java index ad7bc32..f85cf7f 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/security/BCryptSecurePasswordEncoder.java +++ b/application/src/main/java/org/mandarin/booking/adapter/security/BCryptSecurePasswordEncoder.java @@ -7,7 +7,7 @@ @Component @RequiredArgsConstructor -public class BCryptSecurePasswordEncoder implements SecurePasswordEncoder { +class BCryptSecurePasswordEncoder implements SecurePasswordEncoder { private final BCryptPasswordEncoder bCryptPasswordEncoder; @Override diff --git a/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java b/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java index 2e81e06..30b2c28 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java +++ b/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java @@ -13,7 +13,7 @@ @Component @RequiredArgsConstructor -public class CustomAuthenticationProvider implements AuthenticationProvider { +class CustomAuthenticationProvider implements AuthenticationProvider { private final MemberQueryRepository queryRepository; @Override diff --git a/domain/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java b/domain/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java index 3cc715e..80216ff 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java +++ b/domain/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java @@ -1,5 +1,8 @@ package org.mandarin.booking.domain.member; +import org.jspecify.annotations.NullUnmarked; + +@NullUnmarked public interface SecurePasswordEncoder { String encode(String password); boolean matches(String rawPassword, String encodedPassword); From 77df444c4ab84d62a1cbfdc004baefde0eb449b3 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 11 Sep 2025 11:43:05 +0900 Subject: [PATCH 17/36] refactor build configuration and modularize dependencies --- application/build.gradle | 9 +-------- external/build.gradle | 15 +++++++++++++++ settings.gradle | 3 ++- 3 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 external/build.gradle diff --git a/application/build.gradle b/application/build.gradle index a267113..8e486ac 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -15,19 +15,13 @@ ext { dependencies { api(project(':domain')) api(project(':internal')) + api(project(':external')) implementation project(':common') // ---- Spring Boot Core ---- implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-actuator' - // ---- Data & Database ---- - implementation 'com.mysql:mysql-connector-j:8.3.0' - runtimeOnly 'com.h2database:h2' - testRuntimeOnly 'com.h2database:h2' - implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0' - - // ---- Dev Only ---- developmentOnly 'org.springframework.boot:spring-boot-docker-compose' developmentOnly 'org.springframework.boot:spring-boot-devtools' @@ -35,7 +29,6 @@ dependencies { // ---- Testing ---- testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' byteBuddyAgent 'net.bytebuddy:byte-buddy-agent:1.17.6' - testImplementation 'org.mockito:mockito-core:5.19.0' // ---- API Docs (REST Docs + Rest Assured) ---- diff --git a/external/build.gradle b/external/build.gradle new file mode 100644 index 0000000..cddb303 --- /dev/null +++ b/external/build.gradle @@ -0,0 +1,15 @@ +bootJar { + enabled = false +} + +jar { + enabled = true +} + +dependencies { + // ---- Data & Database ---- + implementation 'com.mysql:mysql-connector-j:8.3.0' + runtimeOnly 'com.h2database:h2' + testRuntimeOnly 'com.h2database:h2' + implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0' +} diff --git a/settings.gradle b/settings.gradle index e95bddf..59439df 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,4 +4,5 @@ include ':application' include ':domain' include 'internal' -include 'common' \ No newline at end of file +include 'common' +include 'external' \ No newline at end of file From 916dc2132cae8200a6d89fda661cc5d459ed9d57 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 11 Sep 2025 12:24:56 +0900 Subject: [PATCH 18/36] clean up code formatting and improve readability --- .github/workflows/gradle.yml | 4 +- AGENTS.md | 224 ++++++++++-------- README.md | 19 +- .../org/mandarin/booking/app/AuthService.java | 3 +- .../booking/app/MemberRegisterValidator.java | 6 +- .../booking/app/port/AuthUseCase.java | 1 + .../booking/webapi/MemberController.java | 2 +- .../booking/IntegrationTestUtilsSpecs.java | 3 +- .../java/org/mandarin/booking/TestResult.java | 11 +- .../CustomAuthenticationProviderTest.java | 2 +- .../webapi/auth/reissue/POST_specs.java | 52 ++-- .../booking/webapi/show/POST_specs.java | 38 +-- .../org/mandarin/booking/DomainException.java | 1 + docs/devlog/250809.md | 4 +- docs/devlog/250819.md | 7 +- docs/devlog/250821.md | 14 +- docs/devlog/250829.md | 4 +- docs/devlog/250908.md | 10 +- docs/specs/api/login.md | 14 +- docs/specs/domain.md | 52 ++++ docs/specs/policy/application.md | 13 +- docs/specs/policy/authentication.md | 23 +- docs/specs/policy/authorization.md | 17 +- docs/specs/policy/test.md | 31 ++- docs/todo.md | 7 +- .../booking/domain/AbstractEntity.java | 20 +- .../booking/domain/member/Member.java | 22 +- .../domain/member/SecurePasswordEncoder.java | 1 + .../mandarin/booking/domain/show/Show.java | 28 +-- .../booking/domain/AbstractEntityTest.java | 99 ++++---- .../booking/adapter/JwtTokenUtils.java | 5 +- 31 files changed, 453 insertions(+), 284 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index cfe4c2b..1c1a3bc 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -9,9 +9,9 @@ name: Java CI with Gradle on: push: - branches: ["main"] + branches: [ "main" ] pull_request: - branches: ["main"] + branches: [ "main" ] jobs: build: diff --git a/AGENTS.md b/AGENTS.md index b270c9f..5611fd2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,62 +9,67 @@ Generated by IntelliJ AI based on repository scan (2025-08-31 16:52) 본 문서는 사람용 README가 아닌, 코드 에이전트(코딩 봇)를 위한 실행 지침서입니다. 이 저장소의 실제 파일과 문서를 스캔해 확인된 사실만 기술합니다. 확인 불가능한 항목은 "확인 불가"로 명시합니다. ## 1) Title & Scope + - 목적: 이 파일은 에이전트를 위한 지침서입니다. 개발/운영 작업 자동화 시 준수 규칙과 명령을 제공합니다. - 적용 범위: 단일 레포지토리(single repo), 단일 모듈 구조 - - 근거: `settings.gradle`에 `rootProject.name = 'booking'`만 정의되어 있으며 서브프로젝트 선언 없음. + - 근거: `settings.gradle`에 `rootProject.name = 'booking'`만 정의되어 있으며 서브프로젝트 선언 없음. ## 2) Documentation Reference (docs 우선) + - 모든 작업 전/중에 `./docs` 디렉토리 문서를 우선적으로 참조하십시오. - 주요 문서: - - 아키텍처 규칙: `docs/specs/policy/application.md` - - 도메인 개요: `docs/specs/domain.md` - - API 명세: `docs/specs/api/*.md` - - 인증/인가 정책 문서 틀: `docs/specs/policy/authentication.md`, `docs/specs/policy/authorization.md` (현재 비어 있음) - - 작업 메모/할 일: `docs/devlog/*`, `docs/todo.md` + - 아키텍처 규칙: `docs/specs/policy/application.md` + - 도메인 개요: `docs/specs/domain.md` + - API 명세: `docs/specs/api/*.md` + - 인증/인가 정책 문서 틀: `docs/specs/policy/authentication.md`, `docs/specs/policy/authorization.md` (현재 비어 있음) + - 작업 메모/할 일: `docs/devlog/*`, `docs/todo.md` - 세부 설계나 정책 충돌 시: `docs/specs/policy/application.md`의 규칙을 최우선으로 따르십시오. ## 3) Project Setup + - JDK/Gradle/Spring Boot 버전 - - Java: 21 (근거: `build.gradle` → `java.toolchain.languageVersion = 21`) - - Gradle Wrapper: 8.14.3 (근거: `gradle/wrapper/gradle-wrapper.properties` → `distributionUrl`) - - Spring Boot: 3.5.4 (근거: `build.gradle` → `id 'org.springframework.boot' version '3.5.4'`) + - Java: 21 (근거: `build.gradle` → `java.toolchain.languageVersion = 21`) + - Gradle Wrapper: 8.14.3 (근거: `gradle/wrapper/gradle-wrapper.properties` → `distributionUrl`) + - Spring Boot: 3.5.4 (근거: `build.gradle` → `id 'org.springframework.boot' version '3.5.4'`) - 빌드/의존성 설치 - - 명령: `./gradlew clean build` - - 테스트는 JUnit Platform 사용, 테스트 프로필은 Gradle test 태스크에서 `spring.profiles.active=test`로 설정됨 (근거: `build.gradle` → tasks.named('test')). + - 명령: `./gradlew clean build` + - 테스트는 JUnit Platform 사용, 테스트 프로필은 Gradle test 태스크에서 `spring.profiles.active=test`로 설정됨 (근거: `build.gradle` → + tasks.named('test')). - 로컬 실행 - - 명령: `./gradlew bootRun` - - 활성 프로필: 기본 `local` (근거: `src/main/resources/application.yml` → `spring.profiles.active: local`) - - DB 및 JWT 설정은 `application-local.yml` 참고. 민감정보는 환경변수로 덮어쓰기를 권장. + - 명령: `./gradlew bootRun` + - 활성 프로필: 기본 `local` (근거: `src/main/resources/application.yml` → `spring.profiles.active: local`) + - DB 및 JWT 설정은 `application-local.yml` 참고. 민감정보는 환경변수로 덮어쓰기를 권장. - 테스트 실행 - - 기본: `./gradlew test` (JUnit5, Spring Boot Test, Mockito, Security Test, ArchUnit 포함) - - 테스트 시 프로필: `test` (근거: `build.gradle` test 태스크 설정) + - 기본: `./gradlew test` (JUnit5, Spring Boot Test, Mockito, Security Test, ArchUnit 포함) + - 테스트 시 프로필: `test` (근거: `build.gradle` test 태스크 설정) - 코드 분석/품질 도구 - - SpotBugs 사용 (근거: `build.gradle` → `com.github.spotbugs` 플러그인). 일반 태스크: `spotbugsMain`, `spotbugsTest`. - - 포매터/린터(Spotless/Checkstyle 등): 확인 불가 (저장소 내 설정/플러그인 없음). + - SpotBugs 사용 (근거: `build.gradle` → `com.github.spotbugs` 플러그인). 일반 태스크: `spotbugsMain`, `spotbugsTest`. + - 포매터/린터(Spotless/Checkstyle 등): 확인 불가 (저장소 내 설정/플러그인 없음). ## 4) Architecture Rules (준수 규칙) + - 전반: 헥사고날 아키텍처(Ports & Adapters) - - 근거: `docs/specs/policy/application.md` 및 `src/test/java/.../arch/HexagonalArchitectureTest.java` + - 근거: `docs/specs/policy/application.md` 및 `src/test/java/.../arch/HexagonalArchitectureTest.java` - 계층 및 패키지 - - domain: `org.mandarin.booking.domain` — 순수 모델/예외/커맨드/요청/응답 (프레임워크 의존 금지) - - app: `org.mandarin.booking.app` — 유스케이스 서비스, 포트(`app/port`), 퍼시스턴스 포트/구현(`app/persist`), AOP 등 - - adapter: `org.mandarin.booking.adapter` — webapi, security 등 외부와의 접점 - - 아키텍처 테스트 강제: `HexagonalArchitectureTest` 층 규칙 참조. + - domain: `org.mandarin.booking.domain` — 순수 모델/예외/커맨드/요청/응답 (프레임워크 의존 금지) + - app: `org.mandarin.booking.app` — 유스케이스 서비스, 포트(`app/port`), 퍼시스턴스 포트/구현(`app/persist`), AOP 등 + - adapter: `org.mandarin.booking.adapter` — webapi, security 등 외부와의 접점 + - 아키텍처 테스트 강제: `HexagonalArchitectureTest` 층 규칙 참조. - DDD/엔티티 규칙(요지) - - Aggregate Root 공개 범위 준수, 도메인 엔티티는 domain 패키지에 위치. - - DTO(요청/응답/커맨드)는 현재 domain에 위치하며 컨트롤러에서 변환하여 사용 (근거: policy 문서). + - Aggregate Root 공개 범위 준수, 도메인 엔티티는 domain 패키지에 위치. + - DTO(요청/응답/커맨드)는 현재 domain에 위치하며 컨트롤러에서 변환하여 사용 (근거: policy 문서). - 영속성(JPA) - - 의존성: `spring-boot-starter-data-jpa`, DB: MySQL(H2 for test), P6Spy (근거: `build.gradle`) - - 저장소 규약: Spring Data Repository 인터페이스는 `app/persist`에 위치 (예: `MemberRepository`, `ShowRepository`). - - 트랜잭션 경계는 app 서비스 (예: `ShowCommandRepository`에 `@Transactional`). + - 의존성: `spring-boot-starter-data-jpa`, DB: MySQL(H2 for test), P6Spy (근거: `build.gradle`) + - 저장소 규약: Spring Data Repository 인터페이스는 `app/persist`에 위치 (예: `MemberRepository`, `ShowRepository`). + - 트랜잭션 경계는 app 서비스 (예: `ShowCommandRepository`에 `@Transactional`). - 보안(Spring Security/JWT) - - JWT 파싱/인증: `adapter/security/JwtFilter`가 Authorization 헤더 `Bearer ` 처리 (근거 파일). - - SecurityFilterChain 설정과 우선순위: `SecurityConfig` - - @Order(1) `apiChain` → 직접 구현한 엔드포인트에 대한 인증/인가 설정. `api/**` 규율을 준수함. - - @Order(2) `publicChain` → 그 외 경로 permitAll. - - AuthenticationProvider: `adapter/security/CustomAuthenticationProvider` (구체 로직은 소스 참조). + - JWT 파싱/인증: `adapter/security/JwtFilter`가 Authorization 헤더 `Bearer ` 처리 (근거 파일). + - SecurityFilterChain 설정과 우선순위: `SecurityConfig` + - @Order(1) `apiChain` → 직접 구현한 엔드포인트에 대한 인증/인가 설정. `api/**` 규율을 준수함. + - @Order(2) `publicChain` → 그 외 경로 permitAll. + - AuthenticationProvider: `adapter/security/CustomAuthenticationProvider` (구체 로직은 소스 참조). - 메시징/캐시/검색 - - Kafka/Redis/Elasticsearch 사용: 확인 불가 (관련 의존성/설정 없음). + - Kafka/Redis/Elasticsearch 사용: 확인 불가 (관련 의존성/설정 없음). 자세한 규칙은 `docs/specs/policy/application.md`를 준수하십시오. @@ -80,18 +85,21 @@ Generated by IntelliJ AI based on repository scan (2025-08-31 16:52) - 테스트 메서드: 시나리오 기반 네이밍 (예: shouldFailWhenPasswordIsInvalid) 5.2 OOP 원칙 + - Tell, don’t ask: 데이터를 꺼내 연산하지 말고 객체에게 메시지를 보내라. - SRP: 클래스는 한 가지 책임만. 메서드는 5~10줄 이내 유지. - 불변 객체 지향: 값 객체(Value Object)는 final 필드와 팩토리 메서드 사용. - 의미 있는 도메인 모델: Map 대신 MemberProfile 같은 타입을 정의. 5.3 글 읽듯이 읽히는 코드 + - 한 메서드는 하나의 이야기(스토리)를 표현해야 함. - 중첩 if/else 최소화 → 조기 반환(early return) 활용. - 숫자/문자열 리터럴은 상수화 (MAX_RETRY_COUNT, DATE_FORMAT_PATTERN). - 체이닝 시 가독성 유지: 각 메서드 호출을 줄바꿈 정렬. 5.4 주석 규칙 + - 기본 원칙: 코드 자체가 의도를 설명할 수 있도록 작성 → 불필요한 주석 금지. - 허용되는 주석 위치/용도 - 인터페이스/추상 클래스: 계약(Contract) 설명 @@ -102,6 +110,7 @@ Generated by IntelliJ AI based on repository scan (2025-08-31 16:52) 작업 시 반드시 `docs/specs/policy/application.md` 규칙을 따르십시오. ## 6) Commands (신뢰 가능한 명령만) + - Gradle Wrapper 사용을 강제합니다. ```bash @@ -125,33 +134,37 @@ docker compose up -d ``` 근거 파일/경로: + - `build.gradle` (plugins, dependencies, test 태스크) - `src/main/resources/application.yml`, `application-local.yml`, `application-test.yml` - `compose.yaml` ## 7) Contribution & PR Rules + - 브랜치/커밋/PR 규약: `.github/pull_request_template.md` 참조 - 권장 체크리스트(제안): - - [ ] 모든 테스트 통과 (`./gradlew test`) - - [ ] SpotBugs 통과 (`./gradlew spotbugsMain spotbugsTest`) - - [ ] 아키텍처 테스트 통과 (`HexagonalArchitectureTest`) - - [ ] docs/specs/* 업데이트 반영 + - [ ] 모든 테스트 통과 (`./gradlew test`) + - [ ] SpotBugs 통과 (`./gradlew spotbugsMain spotbugsTest`) + - [ ] 아키텍처 테스트 통과 (`HexagonalArchitectureTest`) + - [ ] docs/specs/* 업데이트 반영 - 코드 스타일 점검 - - [ ] 코드가 글 읽듯 자연스럽게 읽히는가? (네이밍, 메서드 길이, 역할 분리 확인) - - [ ] 불필요한 주석이 없는가? 필요한 주석만 인터페이스/복잡 규칙에 존재하는가? - - [ ] 모든 테스트/SpotBugs/아키텍처 테스트 통과 여부 + - [ ] 코드가 글 읽듯 자연스럽게 읽히는가? (네이밍, 메서드 길이, 역할 분리 확인) + - [ ] 불필요한 주석이 없는가? 필요한 주석만 인터페이스/복잡 규칙에 존재하는가? + - [ ] 모든 테스트/SpotBugs/아키텍처 테스트 통과 여부 ## 8) Environments + - 프로필 - - local: 기본 활성 (근거: `application.yml`), MySQL 설정 및 JWT 시크릿 포함 (근거: `application-local.yml`). - - test: H2 메모리 DB, JPA DDL auto create, JWT 설정 포함 (근거: `application-test.yml`). - - prod: 파일 존재하나 내용 비어 있음 → 확인 불가. + - local: 기본 활성 (근거: `application.yml`), MySQL 설정 및 JWT 시크릿 포함 (근거: `application-local.yml`). + - test: H2 메모리 DB, JPA DDL auto create, JWT 설정 포함 (근거: `application-test.yml`). + - prod: 파일 존재하나 내용 비어 있음 → 확인 불가. - 필수 환경변수(권장 키) - - `SPRING_DATASOURCE_URL`, `SPRING_DATASOURCE_USERNAME`, `SPRING_DATASOURCE_PASSWORD` - - `JWT_TOKEN_SECRET`, `JWT_TOKEN_ACCESS`, `JWT_TOKEN_REFRESH` - - 위 값은 실제 비밀을 포함하므로, 절대 저장소에 커밋하지 말고 실행 환경에서 주입하십시오. 현재 로컬 yml에는 예시 값이 존재하나, 운영에서는 환경변수로 덮어쓰기를 권장. + - `SPRING_DATASOURCE_URL`, `SPRING_DATASOURCE_USERNAME`, `SPRING_DATASOURCE_PASSWORD` + - `JWT_TOKEN_SECRET`, `JWT_TOKEN_ACCESS`, `JWT_TOKEN_REFRESH` + - 위 값은 실제 비밀을 포함하므로, 절대 저장소에 커밋하지 말고 실행 환경에서 주입하십시오. 현재 로컬 yml에는 예시 값이 존재하나, 운영에서는 환경변수로 덮어쓰기를 권장. ## 9) Limitations / Unknowns + - CI/CD(.github/workflows) 설정: 확인 불가. - 코드 포매팅(Spotless/Checkstyle): 구성 없음 → 확인 불가. - 마이그레이션 도구(Flyway/Liquibase): 확인 불가. @@ -159,6 +172,7 @@ docker compose up -d - 배포(AWS/K8s): 설정/스크립트 부재 → 확인 불가. 향후 TODO(사람이 보완): + - prod 프로필 구성 및 비밀 주입 전략 정의 - CI 파이프라인 도입(.github/workflows) - DB 마이그레이션 도구 채택 및 규약 수립 @@ -166,92 +180,97 @@ docker compose up -d - 코드 포매터 도입 여부 결정(Spotless/Checkstyle 등) ## 10) Appendix + - 모듈/계층 의존 개요(텍스트) - - adapter(webapi, security) → app\(ports, services, persist adapters) → domain - - domain은 프레임워크 비의존, app은 domain에만 의존, adapter는 app의 포트에 의존 + - adapter(webapi, security) → app\(ports, services, persist adapters) → domain + - domain은 프레임워크 비의존, app은 domain에만 의존, adapter는 app의 포트에 의존 - 주요 디렉터리 - - `src/main/java/org/mandarin/booking/BookingApplication.java` — Spring Boot 엔트리포인트 - - `src/main/java/org/mandarin/booking/adapter/webapi/*` — REST 컨트롤러/응답 래핑 - - `src/main/java/org/mandarin/booking/adapter/security/*` — 보안 구성/JWT 필터/프로바이더 - - `src/main/java/org/mandarin/booking/app/*` — 서비스, AOP, 포트, 퍼시스트 어댑터 - - `src/main/java/org/mandarin/booking/domain/*` — 도메인 모델과 DTO/커맨드 - - `src/test/java/...` — 단위/아키텍처/통합 테스트 스위트 + - `src/main/java/org/mandarin/booking/BookingApplication.java` — Spring Boot 엔트리포인트 + - `src/main/java/org/mandarin/booking/adapter/webapi/*` — REST 컨트롤러/응답 래핑 + - `src/main/java/org/mandarin/booking/adapter/security/*` — 보안 구성/JWT 필터/프로바이더 + - `src/main/java/org/mandarin/booking/app/*` — 서비스, AOP, 포트, 퍼시스트 어댑터 + - `src/main/java/org/mandarin/booking/domain/*` — 도메인 모델과 DTO/커맨드 + - `src/test/java/...` — 단위/아키텍처/통합 테스트 스위트 ## 11) Test + - TDD 원칙: 새로운 기능은 테스트부터 작성하고, `./gradlew test`로 검증하십시오. - 테스트 정책 근거: - - JUnit Platform 활성화 및 test 프로필 지정 (근거: `build.gradle` test 태스크) - - 아키텍처 검증: `src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java` - - Web/API 및 보안 단위/통합 테스트 예시: `src/test/java/org/mandarin/booking/webapi/**`, `adapter/security/**` + - JUnit Platform 활성화 및 test 프로필 지정 (근거: `build.gradle` test 태스크) + - 아키텍처 검증: `src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java` + - Web/API 및 보안 단위/통합 테스트 예시: `src/test/java/org/mandarin/booking/webapi/**`, `adapter/security/**` - 단위 vs 통합 테스트 - - 단위: 도메인/서비스 단위 로직 검증, 외부 의존 모킹(Mock) (예: Mockito) - - 통합: Spring Context 기동, 필터/시큐리티/컨트롤러 경로 포함. 테스트 프로필 `test`와 H2 DB 사용. + - 단위: 도메인/서비스 단위 로직 검증, 외부 의존 모킹(Mock) (예: Mockito) + - 통합: Spring Context 기동, 필터/시큐리티/컨트롤러 경로 포함. 테스트 프로필 `test`와 H2 DB 사용. - 자세한 내용은 `./docs/specs/policy/test.md`를 참조. + --- ## 12) Documentation Authoring Rules (문서 작성 규칙) + - 적용 범위: `./docs/specs` 디렉토리의 모든 문서(todo.md 제외). 이 규칙은 문서를 자동으로 작성/갱신하는 에이전트를 위한 것입니다. - 공통 원칙 - - 사실만 기술하고, 확인 불가한 항목은 반드시 "확인 불가"로 명시합니다. 근거 파일/경로를 문서 내에 링크로 첨부합니다. - - 한국어를 기본으로 작성합니다. 코드/명령/경로는 코드블록으로 표시합니다. - - 변경 시 일관된 섹션 순서와 템플릿을 유지합니다. - - 예시 명령은 복사-붙여넣기 즉시 실행 가능한 상태로 제공합니다. + - 사실만 기술하고, 확인 불가한 항목은 반드시 "확인 불가"로 명시합니다. 근거 파일/경로를 문서 내에 링크로 첨부합니다. + - 한국어를 기본으로 작성합니다. 코드/명령/경로는 코드블록으로 표시합니다. + - 변경 시 일관된 섹션 순서와 템플릿을 유지합니다. + - 예시 명령은 복사-붙여넣기 즉시 실행 가능한 상태로 제공합니다. - 파일/이름 규칙 - - API 스펙: `docs/specs/api/_.md` 또는 엔드포인트 의미가 드러나는 snake_case 파일명 사용 - - 근거: `docs/specs/api/login.md`, `member_register.md`, `show_register.md`, `reissue.md` - - 정책 문서: `docs/specs/policy/.md` - - 근거: `docs/specs/policy/application.md`, `authentication.md`, `authorization.md`, `test.md` - - 도메인 설계: `docs/specs/domain.md` + - API 스펙: `docs/specs/api/_.md` 또는 엔드포인트 의미가 드러나는 snake_case 파일명 사용 + - 근거: `docs/specs/api/login.md`, `member_register.md`, `show_register.md`, `reissue.md` + - 정책 문서: `docs/specs/policy/.md` + - 근거: `docs/specs/policy/application.md`, `authentication.md`, `authorization.md`, `test.md` + - 도메인 설계: `docs/specs/domain.md` - API 스펙 문서 템플릿 - 1) 제목 생략 가능(현재 파일들은 섹션 위주). 최상단에 섹션 "요청/응답/테스트"를 포함합니다. - 2) 요청 섹션 구성 - - 메서드, 경로, 헤더 코드블록, 본문 JSON 예시(필수 필드 포맷 포함) - - 실행 가능한 curl 예시를 제공 - - 근거 예시: `docs/specs/api/login.md`, `member_register.md`, `show_register.md`, `reissue.md` - 3) 응답 섹션 구성 - - 상태코드 명시, 응답 JSON 예시 제공(필수 필드 포함) - 4) 테스트 섹션 구성 - - 체크박스 형태의 수용 기준 리스트(`[x]/[ ]`) 사용. 실제 테스트 코드와 동기화합니다. - - 체크리스트 항목은 구체적 조건/결과를 포함합니다. + 1) 제목 생략 가능(현재 파일들은 섹션 위주). 최상단에 섹션 "요청/응답/테스트"를 포함합니다. + 2) 요청 섹션 구성 + - 메서드, 경로, 헤더 코드블록, 본문 JSON 예시(필수 필드 포맷 포함) + - 실행 가능한 curl 예시를 제공 + - 근거 예시: `docs/specs/api/login.md`, `member_register.md`, `show_register.md`, `reissue.md` + 3) 응답 섹션 구성 + - 상태코드 명시, 응답 JSON 예시 제공(필수 필드 포함) + 4) 테스트 섹션 구성 + - 체크박스 형태의 수용 기준 리스트(`[x]/[ ]`) 사용. 실제 테스트 코드와 동기화합니다. + - 체크리스트 항목은 구체적 조건/결과를 포함합니다. - 도메인 문서 템플릿 - - 상단 개요(프로젝트/도메인 목적) - - 도메인 모델 섹션 - - 각 Aggregate/Entity 별로: 표제 → 역할 설명(이탤릭), 속성 목록, 행위 목록, 관련 타입 목록 - - 근거 예시: `docs/specs/domain.md` + - 상단 개요(프로젝트/도메인 목적) + - 도메인 모델 섹션 + - 각 Aggregate/Entity 별로: 표제 → 역할 설명(이탤릭), 속성 목록, 행위 목록, 관련 타입 목록 + - 근거 예시: `docs/specs/domain.md` - 정책 문서 템플릿 - - 번호 있는 대제목(1., 2., 3. …)을 사용하여 규칙을 체계화 - - 레이어/의존/포트/보안/테스트 등 주제별 세부 항목을 불릿으로 상세화 - - 실제 코드/설정 파일 경로를 근거로 명시 - - "확인 불가"를 명확히 표기해 향후 TODO를 남김 - - 근거 예시: `docs/specs/policy/application.md`, `docs/specs/policy/test.md` + - 번호 있는 대제목(1., 2., 3. …)을 사용하여 규칙을 체계화 + - 레이어/의존/포트/보안/테스트 등 주제별 세부 항목을 불릿으로 상세화 + - 실제 코드/설정 파일 경로를 근거로 명시 + - "확인 불가"를 명확히 표기해 향후 TODO를 남김 + - 근거 예시: `docs/specs/policy/application.md`, `docs/specs/policy/test.md` - 링크/근거 표기 규칙 - - 저장소 상대 경로 링크를 사용합니다(예: `build.gradle`, `src/main/...`). - - 문서 말미 또는 섹션 말미에 "근거" 목록을 배치하여 신뢰 가능한 출처를 나열합니다. - - 정책/가이드 문서에는 규칙 옆에 괄호로 근거를 병기해도 됩니다. + - 저장소 상대 경로 링크를 사용합니다(예: `build.gradle`, `src/main/...`). + - 문서 말미 또는 섹션 말미에 "근거" 목록을 배치하여 신뢰 가능한 출처를 나열합니다. + - 정책/가이드 문서에는 규칙 옆에 괄호로 근거를 병기해도 됩니다. - 표기/형식 규칙 - - 헤더 레벨은 H2(##)부터 사용해 문서 내 구조를 안정적으로 유지합니다. - - 코드/JSON/명령은 fenced code block 사용. JSON 예시에는 실제 키를 포함하되 민감정보는 예시 값으로 대체. - - 에러/예외/상태코드는 명시적으로 표기(예: `400 Bad Request`, `401 Unauthorized`). - - 체크박스는 `[x]`(충족) / `[ ]`(미충족) 형식으로 유지. + - 헤더 레벨은 H2(##)부터 사용해 문서 내 구조를 안정적으로 유지합니다. + - 코드/JSON/명령은 fenced code block 사용. JSON 예시에는 실제 키를 포함하되 민감정보는 예시 값으로 대체. + - 에러/예외/상태코드는 명시적으로 표기(예: `400 Bad Request`, `401 Unauthorized`). + - 체크박스는 `[x]`(충족) / `[ ]`(미충족) 형식으로 유지. - 테스트와의 동기화 - - API 문서의 테스트 체크리스트는 실제 테스트(`src/test/java/...`)와 1:1로 대응하도록 작성/갱신합니다. - - 새로운 테스트가 추가되면 해당 API 문서의 체크리스트도 즉시 업데이트합니다. - - 근거: `src/test/java/org/mandarin/booking/webapi/**/POST_specs.java`, `adapter/security/*Test.java`, `arch/HexagonalArchitectureTest.java` + - API 문서의 테스트 체크리스트는 실제 테스트(`src/test/java/...`)와 1:1로 대응하도록 작성/갱신합니다. + - 새로운 테스트가 추가되면 해당 API 문서의 체크리스트도 즉시 업데이트합니다. + - 근거: `src/test/java/org/mandarin/booking/webapi/**/POST_specs.java`, `adapter/security/*Test.java`, + `arch/HexagonalArchitectureTest.java` - 프로필/환경 기술 시 유의사항 - - 실제 yml의 키/값/프로필을 그대로 반영하고, 운영 비밀은 "환경변수로 주입"이라고만 적습니다. - - 근거: `src/main/resources/application.yml`, `application-local.yml`, `application-test.yml` + - 실제 yml의 키/값/프로필을 그대로 반영하고, 운영 비밀은 "환경변수로 주입"이라고만 적습니다. + - 근거: `src/main/resources/application.yml`, `application-local.yml`, `application-test.yml` - 금지 사항 - - 추측성 서술 금지("추정/아마도" 금지). 알 수 없는 항목은 "확인 불가". - - 실행 불가한 모호한 명령 예시 금지. 검증되지 않은 외부 의존성 언급 금지. + - 추측성 서술 금지("추정/아마도" 금지). 알 수 없는 항목은 "확인 불가". + - 실행 불가한 모호한 명령 예시 금지. 검증되지 않은 외부 의존성 언급 금지. - 예시 스니펫 스타일 - curl: 실제 경로/헤더/본문 포함(예: `docs/specs/api/show_register.md`의 curl 예시) @@ -260,6 +279,7 @@ docker compose up -d --- 근거 스니펫 링크/파일 경로 요약: + - `build.gradle`: 플러그인/의존성/테스트 태스크 - `settings.gradle`: 단일 모듈 확인 - `src/main/resources/application*.yml`: 프로필/환경 설정 diff --git a/README.md b/README.md index fd8a942..f0c171a 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,11 @@ - 도메인 개요: [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 문서 하단의 체크리스트를 참고하세요. @@ -22,6 +24,7 @@ --- ## 3. 아키텍처 개요 (Hexagonal) + 헥사고날 아키텍처를 적용하여 다음과 같은 레이어 규칙을 따릅니다. - domain: 순수한 도메인 모델과 비즈니스 규칙. 프레임워크 의존 금지. @@ -29,6 +32,7 @@ - adapter: 웹 API, 보안, 영속성 등 외부 인터페이스. 근거와 세부 규칙 + - 정책 문서: [docs/specs/policy](docs/specs/policy) - 레이어 테스트: `src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java` - 패키지 구조 예 @@ -50,9 +54,11 @@ --- ## 5. 기술 스택과 선택 근거 + - 추후 작성 예정 선택 이유(요지) + - Hexagonal: 테스트 용이성과 변경 격리를 위해 계층 경계를 명확히. 또한, 추후 모듈화 or MSA 전환시 이점을 위해 애플리케이션 아키텍처를 영역에 따라 구분. - Spring Security + JWT: 무상태(stateless) API 인증과 확장성. - JPA + RDB(H2/MySQL): 표준 ORM과 빠른 테스트 사이클. @@ -60,6 +66,7 @@ --- ## 6. 개발 방식과 테스트 전략 + - 테스트 주도 개발(TDD) 지향: 테스트 우선, 기능 추가 시 관련 스펙 테스트 동반. - 테스트 정책 문서: [docs/specs/policy/test.md](docs/specs/policy/test.md) - 통합 테스트: Spring Context 기동, 보안 필터/컨트롤러/JPA 연동을 포함한 경로 검증. @@ -72,6 +79,7 @@ Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profile --- ## 7. 보안 개요 + - 필터 기반 JWT 인증: `JwtFilter`가 Authorization `Bearer `을 파싱해 SecurityContext 설정. - 경로별 권한: `SecurityConfig`의 `@Order(1) apiChain` - 차후 추가 작성 @@ -82,6 +90,7 @@ Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profile --- ## 8. 데이터/환경 구성 + - 프로필: `local`(기본), `test`, `prod(비어있음)` - 근거: `src/main/resources/application.yml` 및 `application-*.yml` - local: MySQL + JPA `ddl-auto: create`, JWT 시크릿/TTL 설정 @@ -89,11 +98,12 @@ Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profile - 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) @@ -104,13 +114,15 @@ Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profile --- ## 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) + - 인증/인가 정책 문서 + 구체화: [docs/specs/policy/authentication.md](docs/specs/policy/authentication.md), [docs/specs/policy/authorization.md](docs/specs/policy/authorization.md) --- @@ -119,6 +131,7 @@ Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profile - Spring Boot/Java/Gradle 버전: [build.gradle](application/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` +- 보안 설정/필터: `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) diff --git a/application/src/main/java/org/mandarin/booking/app/AuthService.java b/application/src/main/java/org/mandarin/booking/app/AuthService.java index b84cd8d..718175d 100644 --- a/application/src/main/java/org/mandarin/booking/app/AuthService.java +++ b/application/src/main/java/org/mandarin/booking/app/AuthService.java @@ -28,8 +28,9 @@ public TokenHolder login(String userId, String password) { @Override public TokenHolder reissue(String refreshToken) { var userId = tokenUtils.getClaim(refreshToken, "userId"); - if(!queryRepository.existsByUserId(userId)) + if (!queryRepository.existsByUserId(userId)) { throw new AuthException("회원이 존재하지 않습니다"); + } return tokenUtils.generateToken(refreshToken); } diff --git a/application/src/main/java/org/mandarin/booking/app/MemberRegisterValidator.java b/application/src/main/java/org/mandarin/booking/app/MemberRegisterValidator.java index 6986254..4fd8ee6 100644 --- a/application/src/main/java/org/mandarin/booking/app/MemberRegisterValidator.java +++ b/application/src/main/java/org/mandarin/booking/app/MemberRegisterValidator.java @@ -11,12 +11,14 @@ public class MemberRegisterValidator { private final MemberQueryRepository queryRepository; void checkDuplicateEmail(String email) { - if(queryRepository.existsByEmail(email)) + if (queryRepository.existsByEmail(email)) { throw new MemberException("이미 존재하는 이메일입니다: " + email); + } } void checkDuplicateUserId(String userId) { - if(queryRepository.existsByUserId(userId)) + if (queryRepository.existsByUserId(userId)) { throw new MemberException("이미 존재하는 회원입니다: " + userId); + } } } diff --git a/application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java b/application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java index 0e5db1c..14afc17 100644 --- a/application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java +++ b/application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java @@ -4,5 +4,6 @@ public interface AuthUseCase { TokenHolder login(String userId, String password); + TokenHolder reissue(String refreshToken); } diff --git a/application/src/main/java/org/mandarin/booking/webapi/MemberController.java b/application/src/main/java/org/mandarin/booking/webapi/MemberController.java index 1de821e..041e105 100644 --- a/application/src/main/java/org/mandarin/booking/webapi/MemberController.java +++ b/application/src/main/java/org/mandarin/booking/webapi/MemberController.java @@ -14,7 +14,7 @@ public record MemberController(MemberRegisterer memberRegisterer) { @PostMapping - public MemberRegisterResponse register(@RequestBody @Valid MemberRegisterRequest request){ + public MemberRegisterResponse register(@RequestBody @Valid MemberRegisterRequest request) { return memberRegisterer.register(request); } } diff --git a/application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java b/application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java index f5d2296..36da8be 100644 --- a/application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java +++ b/application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java @@ -28,7 +28,8 @@ void post_echo_success( // Act var response = integrationUtils.post("/test/echo", payload) - .assertSuccess(new TypeReference>() {}); + .assertSuccess(new TypeReference>() { + }); // Assert assertThat(response.getStatus()).isEqualTo(SUCCESS); diff --git a/application/src/test/java/org/mandarin/booking/TestResult.java b/application/src/test/java/org/mandarin/booking/TestResult.java index 8cc6714..cafe868 100644 --- a/application/src/test/java/org/mandarin/booking/TestResult.java +++ b/application/src/test/java/org/mandarin/booking/TestResult.java @@ -14,19 +14,17 @@ import org.mandarin.booking.adapter.SuccessResponse; public class TestResult { - private Executor executor; - private final String path; private final Object request; private final Map headers = new HashMap<>(); + private Executor executor; + private ObjectMapper objectMapper; public TestResult(String path, Object request) { this.path = path; this.request = request; } - private ObjectMapper objectMapper; - public ApiResponse assertSuccess(Class responseType) { var response = readSuccessResponse( getResponse(), @@ -60,7 +58,7 @@ public ErrorResponse assertFailure() { var response = readErrorResponse(); if (response == null) { throw new AssertionError("Expected Error response, but got: " + null); - }else if (response.getStatus() == ApiStatus.SUCCESS) { + } else if (response.getStatus() == ApiStatus.SUCCESS) { throw new AssertionError("Expected Error response, but got SUCCESS: " + response); } return response; @@ -119,7 +117,8 @@ private ApiResponse readSuccessResponse(String raw, Class dataType) { T data = objectMapper.readValue(raw, dataType); return new SuccessResponse<>(ApiStatus.SUCCESS, data); } catch (Exception fallback) { - fail("Failed to parse SuccessResponse with data type " + dataType.getName() + ": " + primary.getMessage(), primary); + fail("Failed to parse SuccessResponse with data type " + dataType.getName() + ": " + + primary.getMessage(), primary); return null; } } diff --git a/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java index 472f541..61dddc8 100644 --- a/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java +++ b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java @@ -22,7 +22,7 @@ void supports() { } @Test - void supportsFailure(){ + void supportsFailure() { var isSupported = provider.supports(String.class); assertThat(isSupported).isFalse(); } diff --git a/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java index 1e565b4..cab7cfa 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java @@ -120,29 +120,51 @@ public class POST_specs { // Assert assertThat(response.getStatus()).isEqualTo(UNAUTHORIZED); } - + @Test void 요청_body가_누락된_경우_400_Bad_Request가_발생한다( @Autowired IntegrationTestUtils testUtils ) { // Arrange var request = new ReissueRequest(null); - + // Act var response = testUtils.post( "/api/auth/reissue", request ) .assertFailure(); - + // Assert assertThat(response.getStatus()).isEqualTo(BAD_REQUEST); } + @Test + void 존재하지_않는_사용자의_refresh_token을_요청하면_401_Unauthorize가_발생한다( + @Autowired IntegrationTestUtils testUtils, + @Autowired TokenUtils tokenUtils + ) { + // Arrange + var validRefreshToken = tokenUtils.generateToken(generateUserId(), generateNickName(), List.of(USER)) + .refreshToken(); + var request = new ReissueRequest(validRefreshToken); + + // user 생성 안함 + + // Act + var response = testUtils.post( + "/api/auth/reissue", + request + ) + .assertFailure(); + + // Assert + assertThat(response.getStatus()).isEqualTo(UNAUTHORIZED); + } @Nested @TestPropertySource(properties = "jwt.token.refresh=100") - class ReissueShortToken{ + class ReissueShortToken { @Test void 만료된_refresh_token으로_요청하면_401_Unauthorize가_발생한다( @Autowired IntegrationTestUtils testUtils @@ -163,26 +185,4 @@ class ReissueShortToken{ assertThat(response.getStatus()).isEqualTo(UNAUTHORIZED); } } - - @Test - void 존재하지_않는_사용자의_refresh_token을_요청하면_401_Unauthorize가_발생한다( - @Autowired IntegrationTestUtils testUtils, - @Autowired TokenUtils tokenUtils - ) { - // Arrange - var validRefreshToken = tokenUtils.generateToken(generateUserId(), generateNickName(), List.of(USER)).refreshToken(); - var request = new ReissueRequest(validRefreshToken); - - // user 생성 안함 - - // Act - var response = testUtils.post( - "/api/auth/reissue", - request - ) - .assertFailure(); - - // Assert - assertThat(response.getStatus()).isEqualTo(UNAUTHORIZED); - } } diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java index 68ccd89..3732ee5 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java @@ -24,6 +24,25 @@ @DisplayName("POST /api/show") public class POST_specs { + static List nullOrBlankElementRequests() { + return List.of( + new ShowRegisterRequest("", "MUSICAL", "ALL", "공연 줄거리", "https://example.com/poster.jpg", + LocalDate.now(), LocalDate.now().plusDays(1)), + new ShowRegisterRequest("공연 제목", "", "ALL", "공연 줄거리", "https://example.com/poster.jpg", LocalDate.now(), + LocalDate.now().plusDays(1)), + new ShowRegisterRequest("공연 제목", "MUSICAL", "", "공연 줄거리", "https://example.com/poster.jpg", + LocalDate.now(), LocalDate.now().plusDays(1)), + new ShowRegisterRequest("공연 제목", "MUSICAL", "ALL", "", "https://example.com/poster.jpg", + LocalDate.now(), LocalDate.now().plusDays(1)), + new ShowRegisterRequest("공연 제목", "MUSICAL", "ALL", "공연 줄거리", "", LocalDate.now(), + LocalDate.now().plusDays(1)), + new ShowRegisterRequest("공연 제목", "MUSICAL", "ALL", "공연 줄거리", "https://example.com/poster.jpg", null, + LocalDate.now().plusDays(1)), + new ShowRegisterRequest("공연 제목", "MUSICAL", "ALL", "공연 줄거리", "https://example.com/poster.jpg", + LocalDate.now(), null) + ); + } + @Test void 올바른_요청을_보내면_status가_SUCCESS이다( @Autowired IntegrationTestUtils testUtils @@ -191,25 +210,6 @@ public class POST_specs { assertThat(response.getData()).contains("이미 존재하는 공연 이름입니다:"); } - static List nullOrBlankElementRequests() { - return List.of( - new ShowRegisterRequest("", "MUSICAL", "ALL", "공연 줄거리", "https://example.com/poster.jpg", - LocalDate.now(), LocalDate.now().plusDays(1)), - new ShowRegisterRequest("공연 제목", "", "ALL", "공연 줄거리", "https://example.com/poster.jpg", LocalDate.now(), - LocalDate.now().plusDays(1)), - new ShowRegisterRequest("공연 제목", "MUSICAL", "", "공연 줄거리", "https://example.com/poster.jpg", - LocalDate.now(), LocalDate.now().plusDays(1)), - new ShowRegisterRequest("공연 제목", "MUSICAL", "ALL", "", "https://example.com/poster.jpg", - LocalDate.now(), LocalDate.now().plusDays(1)), - new ShowRegisterRequest("공연 제목", "MUSICAL", "ALL", "공연 줄거리", "", LocalDate.now(), - LocalDate.now().plusDays(1)), - new ShowRegisterRequest("공연 제목", "MUSICAL", "ALL", "공연 줄거리", "https://example.com/poster.jpg", null, - LocalDate.now().plusDays(1)), - new ShowRegisterRequest("공연 제목", "MUSICAL", "ALL", "공연 줄거리", "https://example.com/poster.jpg", - LocalDate.now(), null) - ); - } - private ShowRegisterRequest validShowRegisterRequest() { return new ShowRegisterRequest( UUID.randomUUID().toString().substring(0, 10), diff --git a/common/src/main/java/org/mandarin/booking/DomainException.java b/common/src/main/java/org/mandarin/booking/DomainException.java index 89afb8a..ea4a461 100644 --- a/common/src/main/java/org/mandarin/booking/DomainException.java +++ b/common/src/main/java/org/mandarin/booking/DomainException.java @@ -5,6 +5,7 @@ @Getter public class DomainException extends RuntimeException { private String status = "INTERNAL_SERVER_ERROR"; + public DomainException(String message) { super(message); } diff --git a/docs/devlog/250809.md b/docs/devlog/250809.md index 2476bca..4e6d9e3 100644 --- a/docs/devlog/250809.md +++ b/docs/devlog/250809.md @@ -1,8 +1,10 @@ ## 예찬 + 현재까지 작성한 회원가입 구현 과정에서 TDD로 개발을 진행했는데, 외부에 의존된 코드가 통합테스트에 다수 포함되어있음. 이를 최소화할 방안이 필요하다고 느낌. ## 휘동 + 회원가입 요청에 사용되는 `MemberRegisterRequest` 객체의 속성이 하드코딩되어있는 부분이 많음. -이를 메서드로 추출하여 테스트 신뢰성을 높이는 방향으로 개선할 필요가 있음. \ No newline at end of file +이를 메서드로 추출하여 테스트 신뢰성을 높이는 방향으로 개선할 필요가 있음. diff --git a/docs/devlog/250819.md b/docs/devlog/250819.md index 4601e48..a0105d7 100644 --- a/docs/devlog/250819.md +++ b/docs/devlog/250819.md @@ -1,10 +1,11 @@ ## 예찬 -시스템 아키텍처에 대한 개념이 흐리다고 느낌. 특히 정확한 기준 없이 헥사고날 아키텍처를 적용하려 했던 부분이 문제가 된것으로 생각됨. -또한 TDD를 위한 `TestUtils`를 구현하는데에 있어 발생한 문제를 아직 해결하지 못함. 성공적인 응답값을 반환해야 하는 경우에 적절한 에러 메세지를 띄우지 않기 때문에 문제 해결에 어려움이 있음. +시스템 아키텍처에 대한 개념이 흐리다고 느낌. 특히 정확한 기준 없이 헥사고날 아키텍처를 적용하려 했던 부분이 문제가 된것으로 생각됨. +또한 TDD를 위한 `TestUtils`를 구현하는데에 있어 발생한 문제를 아직 해결하지 못함. 성공적인 응답값을 반환해야 하는 경우에 적절한 에러 메세지를 띄우지 않기 때문에 문제 해결에 어려움이 있음. ## 휘동 + 헥사고날의 In/Out 개념을 확실히 하고 넘어가야할 것 같음. 응답 객체에서 원하는 응답 값과 상태를 반환하지 못하는 문제가 있음. -응답 객체를 간소화하거나 래퍼 메소드를 수정할 필요가 있음. \ No newline at end of file +응답 객체를 간소화하거나 래퍼 메소드를 수정할 필요가 있음. diff --git a/docs/devlog/250821.md b/docs/devlog/250821.md index 8a1c434..ed3fce8 100644 --- a/docs/devlog/250821.md +++ b/docs/devlog/250821.md @@ -1,11 +1,21 @@ ## 예찬 -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`에 등록하도록 객체를 생성해야 중복 등록이 발생하지 않는다. + +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를 적절히 사용해 필터 적용 시기를 조절함이 바람직하다. \ No newline at end of file +filterchain.doFilter()의 유무로 필터의 중단을 결정하지 말고, GenericFilterBean와 OncePerRequestFilter를 적절히 사용해 필터 적용 시기를 조절함이 바람직하다. diff --git a/docs/devlog/250829.md b/docs/devlog/250829.md index db3efa7..6785a61 100644 --- a/docs/devlog/250829.md +++ b/docs/devlog/250829.md @@ -1,5 +1,7 @@ ## 예찬 -전반적인 인증 흐름에 대한 이해가 이번 기능 구현 과정에서 미흡했던것을 인지함. 이후 Spring Security의 인증 흐름을 학습하고, 그 과정에서 기존에 구현하려 했던 방식에 문제점을 발견했고, 이를 해결하기 위해 직접 제어 가능한 수준의 인증정보 제공자를 직접 구현하는 방식으로 기능 구현, 성공적인 테스트 케이스 통과가 가능했다. + +전반적인 인증 흐름에 대한 이해가 이번 기능 구현 과정에서 미흡했던것을 인지함. 이후 Spring Security의 인증 흐름을 학습하고, 그 과정에서 기존에 구현하려 했던 방식에 문제점을 발견했고, 이를 해결하기 +위해 직접 제어 가능한 수준의 인증정보 제공자를 직접 구현하는 방식으로 기능 구현, 성공적인 테스트 케이스 통과가 가능했다. 이후에 해야할건 영화 조회 아닐까 싶다. 영화 조회 기능도 일단 빠르게 Spring Data JPA의 도움을 받아서 구현하고 이후 최적화 과정을 거치는 것이 좋지 않을까 하는 생각. 추가로, 현재까지 개발을 하는 과정에서 정책을 일부 작성해왔었는데, 이부분이 좀 누락된거 같다. 작성해두는게 앞으로 문제 발생 가능성을 줄이는데에 도움이 되지 않을까... diff --git a/docs/devlog/250908.md b/docs/devlog/250908.md index 044bf0b..ef5e131 100644 --- a/docs/devlog/250908.md +++ b/docs/devlog/250908.md @@ -1,7 +1,11 @@ ## 예찬 -기존의 개발 방식을 고수하면서 이번에는 Aggregate root간의 의존을 최소화 하기 위해 Application Event를 적당히 활용해봤다. 실무에서도 이렇게 잘 쓰이는지는 모르겠다만, 결국 Spring Context에 의해 관리되는 영역인 만큼 그냥 써도 되지 않을까 하는 생각.. +기존의 개발 방식을 고수하면서 이번에는 Aggregate root간의 의존을 최소화 하기 위해 Application Event를 적당히 활용해봤다. 실무에서도 이렇게 잘 쓰이는지는 모르겠다만, 결국 Spring +Context에 의해 관리되는 영역인 만큼 그냥 써도 되지 않을까 하는 생각.. -그리고 이번에 AR를 적극적으로 활용한 DDD를 하려 하다보니 Entity를 package private로 개발하는 방식을 처음으로 써봤다. 도메인 로직들은 통상 AR를 통해 호출되기 때문에, AR를 제외한 나머지 Entity들은 외부에서 직접 접근할 일이 거의 없다는 점에 착안한 것이다. 물론, 이 방식이 무조건 옳다고 생각하지는 않는다. 다만, 이번 프로젝트에서는 이 방식을 채택함으로써 도메인 모델의 캡슐화가 좀 더 강화된 느낌이다. +그리고 이번에 AR를 적극적으로 활용한 DDD를 하려 하다보니 Entity를 package private로 개발하는 방식을 처음으로 써봤다. 도메인 로직들은 통상 AR를 통해 호출되기 때문에, AR를 제외한 나머지 +Entity들은 외부에서 직접 접근할 일이 거의 없다는 점에 착안한 것이다. 물론, 이 방식이 무조건 옳다고 생각하지는 않는다. 다만, 이번 프로젝트에서는 이 방식을 채택함으로써 도메인 모델의 캡슐화가 좀 더 강화된 +느낌이다. -마침 AR를 사용한 설계의 장점을 극대화할 겸, 어차피 나중에 한번쯤은 손대려고 했던 모듈을 다음 기능 개발에 도입하려 한다. 이때 AR 기준 접근을 하려 했던 기존의 설계가 빛을 바랄거라고 생각되는데, 이 부분은 다음 개발기를 통해 좀 더 자세히 다뤄보도록 하겠다. +마침 AR를 사용한 설계의 장점을 극대화할 겸, 어차피 나중에 한번쯤은 손대려고 했던 모듈을 다음 기능 개발에 도입하려 한다. 이때 AR 기준 접근을 하려 했던 기존의 설계가 빛을 바랄거라고 생각되는데, 이 부분은 +다음 개발기를 통해 좀 더 자세히 다뤄보도록 하겠다. diff --git a/docs/specs/api/login.md b/docs/specs/api/login.md index ad8291b..df6f368 100644 --- a/docs/specs/api/login.md +++ b/docs/specs/api/login.md @@ -3,13 +3,13 @@ - 메서드: `POST` - 경로: `/api/auth/login` - 헤더 - + ``` Content-Type: application/json ``` - + - 본문 - + ```json { "userId": "string", @@ -17,10 +17,10 @@ } ``` - + - curl 명령 예시 - + ```bash curl -i -X POST 'http://localhost:8080/api/auth/login' \ -H 'Content-Type: application/json' \ @@ -30,20 +30,18 @@ "password": "myPassword123" }' ``` - ### 응답 - 상태코드: `200 OK` - 본문 - + ```json { "accessToken": "string", "refreshToken": "string" } ``` - ### 테스트 diff --git a/docs/specs/domain.md b/docs/specs/domain.md index cf2c9f1..e96528e 100644 --- a/docs/specs/domain.md +++ b/docs/specs/domain.md @@ -4,21 +4,26 @@ 이 문서는 공연 예매 시스템의 도메인 모델 설계 및 주요 유스케이스 흐름을 설명합니다. 설계는 도메인 모델 패턴을 따르며, 다음 원칙을 강제합니다. + - AR 내부 연관은 FK 사용 허용 - AR 간 연관은 "간접 참조(식별자)"만 사용(FK 불허, via XId) - 결제 도메인 분리 없음: Reservation AR 내부에 Payment/PaymentAttempt/Refund 포함 - Show 공연 기간 명확화: performanceStartDate, performanceEndDate(또는 값객체 PerformanceWindow) 사용 --- + # 공연 예매 시스템 도메인 설계 --- ## 공연(Show) + _Aggregate Root_ + - 공연 작품 자체 #### 속성 + - 제목(title) - 유형(type: MUSICAL, PLAY, CONCERT, OPERA, DANCE, CLASSICAL, ETC) - 관람등급(rating: ALL, AGE12, AGE15, AGE18) @@ -29,10 +34,12 @@ _Aggregate Root_ - 공연 스케줄(schedules: List\) #### 행위 + - `create(command: ShowCreateCommand)` - `registerSchedule(hallId, startAt, endAt, runtimeOverride)` #### 관련 타입 + - `ShowCreateCommand` - title, type, rating, synopsis, posterUrl, performanceStartDate, performanceEndDate - `ShowRegisterRequest` / `ShowRegisterResponse` @@ -40,10 +47,13 @@ _Aggregate Root_ --- ### 회차(ShowSchedule) + _Entity_ + - 특정 공연이 특정 홀에서 특정 시간에 진행되는 스케줄 #### 속성 + - showId(FK) - hallId - 시작일시(startAt) @@ -53,10 +63,13 @@ _Entity_ --- ### 캐스팅(Casting) + _Entity_ + - 회차별 배역과 출연자 매핑 #### 속성 + - scheduleId(FK) - 배역명(roleName) - 출연자명(personName) @@ -64,10 +77,13 @@ _Entity_ --- ## 공연장(Venue) + _Aggregate Root_ + - 공연 시설 #### 속성 + - 이름(name) - 주소(address) @@ -80,20 +96,26 @@ _Aggregate Root_ --- ### 홀(Hall) + _Entity_ + - 공연장 내부의 개별 공간 #### 속성 + - venueId(FK) - 이름(name) --- ### 좌석(Seat) + _Entity_ + - 홀 내부의 개별 좌석 #### 속성 + - hallId(FK) - 열(rowLabel) - 번호(number) @@ -103,20 +125,26 @@ _Entity_ --- ### 좌석등급(TicketGrade) + _Entity_ + - 홀 단위 좌석 등급 #### 속성 + - hallId(FK) - 이름(name) --- ## 가격표(SchedulePricing) + _Aggregate Root_ + - 회차와 좌석등급 조합에 따른 가격 관리 #### 속성 + - scheduleId - 통화(currency) - 시작일(validFrom) @@ -124,21 +152,26 @@ _Aggregate Root_ - 가격정책(pricingPolicy) #### 행위 + - `createFor(scheduleId, currency, validFrom, validTo)` - `putPrice(ticketGradeId, amount)` - `removePrice(ticketGradeId)` #### 관련 타입 + - `CreateSchedulePricingCommand` - `PutTicketPriceCommand` --- ### 가격행(TicketPriceLine) + _Entity_ + - 가격표의 라인 항목 #### 속성 + - schedulePricingId(FK) - ticketGradeId - 금액(amount) @@ -146,10 +179,13 @@ _Entity_ --- ## 회원(Member) + _Aggregate Root_ + - 서비스를 사용하는 회원 #### 속성 + - 닉네임(nickName) - 아이디(userId, UNIQUE) - 비밀번호(passwordHash) @@ -157,22 +193,27 @@ _Aggregate Root_ - 권한(authorities: USER, DISTRIBUTOR, ADMIN) #### 행위 + - `register(command: MemberRegisterCommand, encoder)` - `changeNickName(newNickName)` - `changeEmail(newEmail)` - `matchesPassword(rawPassword, encoder)` #### 관련 타입 + - `MemberRegisterCommand` - `MemberRegisterRequest` / `MemberRegisterResponse` --- ## 예매(Reservation) + _Aggregate Root_ + - 좌석 보류, 확정, 환불 및 결제 관리 #### 속성 + - memberId - scheduleId - seatId @@ -183,6 +224,7 @@ _Aggregate Root_ - 결제금액(paidAmount) #### 행위 + - `hold(memberId, scheduleId, seatId, ticketGradeId, ttl)` - `readyPayment(merchantUid, totalAmount)` - `confirmPaid(merchantUid, approvedAt)` @@ -190,6 +232,7 @@ _Aggregate Root_ - `requestRefund(amount, reason)` #### 관련 타입 + - `HoldReservationCommand` - `ReadyPaymentCommand` - `ConfirmPaidCommand` @@ -198,10 +241,13 @@ _Aggregate Root_ --- ### 결제(Payment) + _Entity_ + - Reservation에 종속되는 결제 정보 #### 속성 + - reservationId(FK) - 상점거래ID(merchantUid, UNIQUE) - 총액(totalAmount) @@ -211,10 +257,13 @@ _Entity_ --- ### 결제시도(PaymentAttempt) + _Entity_ + - 결제 요청 및 승인/실패 내역 #### 속성 + - paymentId(FK) - 결제수단(method: CARD, ACCOUNT_TRANSFER, MOBILE, VIRTUAL_ACCOUNT, SIMPLE_PAY) - 요청금액(requestedAmount) @@ -227,10 +276,13 @@ _Entity_ --- ### 환불(Refund) + _Entity_ + - 환불 내역 #### 속성 + - paymentId(FK) - 환불금액(amount) - 상태(refundStatus: REQUESTED, PENDING, COMPLETED, FAILED) diff --git a/docs/specs/policy/application.md b/docs/specs/policy/application.md index 62a1b93..0de348a 100644 --- a/docs/specs/policy/application.md +++ b/docs/specs/policy/application.md @@ -1,6 +1,7 @@ # 애플리케이션 아키텍처 규칙(헥사고날 아키텍처) -본 문서는 booking 프로젝트가 채택한 헥사고날 아키텍처(Hexagonal Architecture, Ports & Adapters)의 규칙을 명확히 하기 위한 가이드입니다. 이 문서는 아키텍처 테스트와 코드 리뷰의 근거가 되며, 새로운 기능 추가 시 반드시 준수해야 합니다. +본 문서는 booking 프로젝트가 채택한 헥사고날 아키텍처(Hexagonal Architecture, Ports & Adapters)의 규칙을 명확히 하기 위한 가이드입니다. 이 문서는 아키텍처 테스트와 코드 +리뷰의 근거가 되며, 새로운 기능 추가 시 반드시 준수해야 합니다. ## 1. 계층과 책임 @@ -8,7 +9,8 @@ - domain: 도메인 모델과 비즈니스 규칙의 순수 영역 - 위치: `src/main/java/org/mandarin/booking/domain` - - 포함: 엔티티(를 표현하기 위한 매핑정보), 값 객체, 도메인 서비스(필요시), 도메인 예외, 도메인 전용 인터페이스(예: `SecurePasswordEncoder`), 유스케이스에 전달되는 순수 모델(`*Request`, `*Response`, `*Command` 등) + - 포함: 엔티티(를 표현하기 위한 매핑정보), 값 객체, 도메인 서비스(필요시), 도메인 예외, 도메인 전용 인터페이스(예: `SecurePasswordEncoder`), 유스케이스에 전달되는 순수 모델( + `*Request`, `*Response`, `*Command` 등) - 금지: 프레임워크/외부 라이브러리 의존(JPA/Spring/Web 등), I/O 접근, 인프라 세부 사항 - app: 애플리케이션 서비스(유스케이스)와 포트 인터페이스 @@ -41,9 +43,11 @@ - 입력 포트(inbound port): 유스케이스 인터페이스. 위치: `app/port` 컨트롤러는 입력 포트를 통해서만 유스케이스 호출. - 출력 포트(outbound port): 외부 시스템/리포지토리에 대한 인터페이스. 위치: `app/persist` 또는 `app/port` 하위에 정의 가능. -- 어댑터(adapters): 포트 인터페이스의 구현체. 위치: adapter 하위. 현재 JPA 기반 구현은 `app/persist/*Repository`를 통해 동작하며, 해당 패키지는 어댑터 계층으로 간주합니다. +- 어댑터(adapters): 포트 인터페이스의 구현체. 위치: adapter 하위. 현재 JPA 기반 구현은 `app/persist/*Repository`를 통해 동작하며, 해당 패키지는 어댑터 계층으로 + 간주합니다. 권장 네이밍: + - 입력 포트: UseCase 동사형 + er (예: Registerer, UseCase) - 출력 포트: 리소스 + 동작 + Repository/Gateway (예: ShowCommandRepository) - 그 외에는 해당 인터페이스가 담당한 기능의 추상적 개념을 나타내는 네이밍 @@ -114,6 +118,7 @@ 6) 아키텍처/통합 테스트 통과 확인. 새 어댑터(예: 외부 결제 API) 추가 절차: + 1) app에 출력 포트 인터페이스 추가(예: `PaymentGateway`). 2) adapter 하위에 구현(예: `adapter/external/PaymentGatewayHttpClient`). 3) 구성(Security/Config)과 예외 매핑 추가. @@ -121,12 +126,14 @@ ## 11. 공통 규칙 요약(Do/Don’t) Do + - 유스케이스 입출력은 app 포트를 통해서만 노출/호출한다. - 도메인 모델은 순수하게 유지한다(프레임워크 의존 금지). - 어댑터는 포트 인터페이스를 구현한다. - 트랜잭션과 로깅은 app에서 관리한다. Don’t + - 컨트롤러에서 비즈니스 로직 수행 금지. - app에서 adapter 패키지/구현에 의존 금지. - domain에서 JPA/Spring 등에 의존 금지. diff --git a/docs/specs/policy/authentication.md b/docs/specs/policy/authentication.md index 206ac49..0264c64 100644 --- a/docs/specs/policy/authentication.md +++ b/docs/specs/policy/authentication.md @@ -3,22 +3,26 @@ 본 문서는 booking 프로젝트의 인증(Authentication) 동작과 규칙을 정리합니다. 모든 내용은 저장소의 실제 코드/설정에 근거합니다. 확인 불가한 항목은 "확인 불가"로 표기합니다. 근거 파일/경로: + - 보안 설정: `src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java` - JWT 필터: `src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java` - AuthenticationProvider: `src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java` - 토큰 유틸: `src/main/java/org/mandarin/booking/app/TokenUtils.java` -- 예외 처리기: `src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java`, `CustomAccessDeniedHandler.java` +- 예외 처리기: `src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationEntryPoint.java`, + `CustomAccessDeniedHandler.java` - 프로필/환경: `src/main/resources/application.yml`, `application-local.yml`, `application-test.yml` --- ## 1. 인증 흐름 개요 + - 모든 `/api/**` 요청은 `@Order(1)` 체인의 보호를 받습니다. (근거: SecurityConfig.apiChain) - 인증 헤더: `Authorization: Bearer ` (근거: JwtFilter) - JwtFilter가 토큰을 파싱하여 사용자 식별자와 권한 정보를 추출하고, `AuthenticationManager`(= `CustomAuthenticationProvider`)를 통해 인증 토큰을 완성합니다. - 세션 상태: Stateless (세션 생성 비활성화), CSRF 비활성화. (근거: SecurityConfig.apiChain 설정) 텍스트 시퀀스: + 1) 클라이언트 → API: Authorization 헤더 전달 2) JwtFilter: `Bearer` 타입 여부 확인 → 토큰 파싱 → `userId`, `roles` 추출 3) JwtFilter: `CustomMemberAuthenticationToken(userId, authorities)` 생성 → `AuthenticationManager.authenticate(...)` 위임 @@ -28,12 +32,14 @@ --- ## 2. JWT 토큰 규칙 + - 헤더 키: `Authorization` - 포맷: `Bearer ` - 사용 Claims (근거: JwtFilter): - `userId`: 사용자 식별자 - `roles`: 문자열 리스트. 예: `["ROLE_USER", "ROLE_DISTRIBUTOR"]` -- 권한 매핑: JwtFilter는 `roles`에서 `ROLE_` 접두사를 제거한 뒤 `MemberAuthority` enum으로 변환하여 `CustomMemberAuthenticationToken`에 부여합니다. (근거: JwtFilter.getAuthorities) +- 권한 매핑: JwtFilter는 `roles`에서 `ROLE_` 접두사를 제거한 뒤 `MemberAuthority` enum으로 변환하여 `CustomMemberAuthenticationToken`에 + 부여합니다. (근거: JwtFilter.getAuthorities) - 토큰 서명/TTL 설정: 프로필 별 설정 사용 - `jwt.token.secret`: 서명 시크릿(Base64 인코딩 값) - `jwt.token.access`: Access Token 만료(ms) @@ -41,12 +47,15 @@ - 근거: `application-local.yml`, `application-test.yml` 주의: -- 헤더가 없거나 `Bearer` 접두사만 온 경우 익명 처리되며, 요청 속성 `exception`에 `AuthException`이 설정됩니다. (근거: JwtFilter.isTokenBlank, doFilterInternal) + +- 헤더가 없거나 `Bearer` 접두사만 온 경우 익명 처리되며, 요청 속성 `exception`에 `AuthException`이 설정됩니다. (근거: JwtFilter.isTokenBlank, + doFilterInternal) - 서명 오류/만료/클레임 파싱 실패 시 `AuthException`을 설정하고 SecurityContext를 비웁니다. (근거: JwtFilter 예외 처리) --- ## 3. 인증 컴포넌트 + - JwtFilter - 위치/순서: `UsernamePasswordAuthenticationFilter` 앞 (근거: SecurityConfig.addFilterBefore) - 역할: Authorization 헤더 파싱, 사용자 정보 추출, AuthenticationManager 위임, SecurityContext 설정 @@ -59,13 +68,16 @@ --- ## 4. 인증 예외 처리 + - 인증 실패(미인증) 시: `CustomAuthenticationEntryPoint`가 응답 생성 (상세 형식은 클래스 구현 참고) - 인가 실패(권한 부족) 시: `CustomAccessDeniedHandler`가 응답 생성 -- JwtFilter는 내부적으로 `request.setAttribute("exception", new AuthException(...))`로 실패 사유를 넘기며, 이후 예외 처리기가 이를 사용해 응답을 형성할 수 있습니다. +- JwtFilter는 내부적으로 `request.setAttribute("exception", new AuthException(...))`로 실패 사유를 넘기며, 이후 예외 처리기가 이를 사용해 응답을 형성할 수 + 있습니다. --- ## 5. 공개 엔드포인트와 인증 필요 엔드포인트 + - 공개(permitAll): (근거: SecurityConfig.apiChain) - `POST /api/member` - `POST /api/auth/login` @@ -78,6 +90,7 @@ --- ## 6. 테스트/프로필 연계 + - 테스트 실행 시 프로필 `test` 활성화: Gradle test 태스크에서 설정 (근거: build.gradle) - 테스트 프로필에서 JWT/DB 설정은 `application-test.yml`을 따른다. - 보안 관련 통합/단위 테스트는 다음을 참조: `src/test/java/org/mandarin/booking/adapter/security/*` @@ -85,6 +98,7 @@ --- ## 7. 확장 가이드(인증) + - 새로운 인증 스킴 도입 시 지켜야 할 규칙: - JwtFilter 앞/뒤 필터 추가 시 순서 충돌 주의. 인증 헤더 파싱 필터는 반드시 UsernamePasswordAuthenticationFilter 이전. - CustomAuthenticationProvider는 `supports`/`authenticate` 계약을 준수하여 `Authentication` 토큰 타입을 명확히 구분. @@ -94,6 +108,7 @@ --- ## 8. 알 수 없는 항목(확인 불가) + - 토큰 발급/서명 구현 세부(Access/Refresh 생성 로직) 문서화 수준: 확인 불가 (해당 클래스 상세는 별도 코드 참조 필요) - 키 회전/블랙리스트/토큰 철회 전략: 확인 불가 diff --git a/docs/specs/policy/authorization.md b/docs/specs/policy/authorization.md index 120471b..632a0c4 100644 --- a/docs/specs/policy/authorization.md +++ b/docs/specs/policy/authorization.md @@ -3,20 +3,25 @@ 본 문서는 booking 프로젝트의 인가(Authorization) 규칙을 경로/메서드/권한 기준으로 명확히 기술합니다. 모든 내용은 실제 보안 설정 코드에 근거합니다. 근거 파일/경로: + - 보안 설정: `src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java` - 권한 Enum: `src/main/java/org/mandarin/booking/domain/member/MemberAuthority.java` - JWT 필터: `src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java` -- 예외 처리기: `src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java`, `CustomAuthenticationEntryPoint.java` +- 예외 처리기: `src/main/java/org/mandarin/booking/adapter/security/CustomAccessDeniedHandler.java`, + `CustomAuthenticationEntryPoint.java` --- ## 1. 기본 원칙 + - 인가는 Spring Security의 `SecurityFilterChain` 규칙으로 정의됩니다. -- 권한 문자열은 `ROLE_` 접두사를 가진 형태로 JWT `roles` 클레임에 담깁니다. (예: `ROLE_USER`, `ROLE_DISTRIBUTOR`, `ROLE_ADMIN`) (근거: JwtFilter.getAuthorities, MemberAuthority) +- 권한 문자열은 `ROLE_` 접두사를 가진 형태로 JWT `roles` 클레임에 담깁니다. (예: `ROLE_USER`, `ROLE_DISTRIBUTOR`, `ROLE_ADMIN`) (근거: + JwtFilter.getAuthorities, MemberAuthority) --- ## 2. 경로/메서드 별 인가 규칙 + 아래 표는 `SecurityConfig.apiChain`의 `authorizeHttpRequests` 설정을 반영합니다. - 공개(permitAll): @@ -34,6 +39,7 @@ - `/error`, `/assets/**`, `/favicon.ico`, 및 그 외 `/**`는 permitAll (정적/오류/기타 공개 경로) 근거 코드 스니펫 요약: + - `http.securityMatcher("/api/**")` - `.requestMatchers(HttpMethod.POST, "/api/member").permitAll()` - `.requestMatchers("/api/auth/login").permitAll()` @@ -44,14 +50,17 @@ --- ## 3. 권한 명명 규칙과 매핑 + - Enum: `MemberAuthority`는 다음 권한을 가집니다(코드 참조). - USER, DISTRIBUTOR, ADMIN 등 -- JWT `roles` → Spring Security 권한 변환: JwtFilter는 `roles`에서 `ROLE_` 접두사를 제거한 후 `MemberAuthority.valueOf(...)`로 Enum을 만들어 `CustomMemberAuthenticationToken`에 저장합니다. (근거: JwtFilter.getAuthorities) +- JWT `roles` → Spring Security 권한 변환: JwtFilter는 `roles`에서 `ROLE_` 접두사를 제거한 후 `MemberAuthority.valueOf(...)`로 Enum을 만들어 + `CustomMemberAuthenticationToken`에 저장합니다. (근거: JwtFilter.getAuthorities) - hasAuthority 비교 시에는 문자열 `ROLE_*` 형태를 사용합니다. (근거: SecurityConfig 설정) --- ## 4. 예외/오류 처리 + - 인증 실패(401 Unauthorized): `CustomAuthenticationEntryPoint`가 처리 - 권한 부족(403 Forbidden): `CustomAccessDeniedHandler`가 처리 - JwtFilter는 유효하지 않은 토큰, 누락된 토큰 등에 대해 `request.setAttribute("exception", AuthException)`을 설정하여 원인 정보를 예외 처리기로 전달합니다. @@ -59,6 +68,7 @@ --- ## 5. 확장 가이드(인가 규칙 추가 방법) + - 새로운 엔드포인트 추가 시 규칙 예시: - 공개 엔드포인트(회원가입/로그인 유사): `.requestMatchers(HttpMethod.POST, "/api/xxx").permitAll()` - 역할 제한 엔드포인트: `.requestMatchers(HttpMethod.PUT, "/api/show/{id}").hasAuthority("ROLE_ADMIN")` @@ -69,5 +79,6 @@ --- ## 6. 알 수 없는 항목(확인 불가) + - 경로 별 세부 권한 정책 문서(도메인별 Role Matrix): 현재 저장소에 상세 표 없음 → 확인 불가 - 동적 권한(도메인 데이터 소유권 기반 세분화) 정책: 확인 불가 diff --git a/docs/specs/policy/test.md b/docs/specs/policy/test.md index b401862..d4ab220 100644 --- a/docs/specs/policy/test.md +++ b/docs/specs/policy/test.md @@ -1,8 +1,10 @@ # 테스트 정책 (booking) -본 문서는 booking 프로젝트의 테스트 작성/실행 표준을 정의합니다. 실제 저장소의 테스트 코드, Gradle 설정, 애플리케이션 프로필을 근거로 수립되었습니다. 이 문서는 코드 변경 시 항상 최신 상태로 유지되어야 하며, 테스트 실패는 정책 위반으로 간주할 수 있습니다. +본 문서는 booking 프로젝트의 테스트 작성/실행 표준을 정의합니다. 실제 저장소의 테스트 코드, Gradle 설정, 애플리케이션 프로필을 근거로 수립되었습니다. 이 문서는 코드 변경 시 항상 최신 상태로 +유지되어야 하며, 테스트 실패는 정책 위반으로 간주할 수 있습니다. 근거 파일/경로: + - build.gradle: test 태스크, 라이브러리, JVM args 설정 - src/main/resources/application-test.yml: 테스트 프로필 환경 - src/test/java/**/*: 실제 테스트 코드 일체 @@ -11,11 +13,13 @@ --- ## 1. 목표와 범위 + - 목표: 기능/아키텍처/보안/도메인 규칙을 신뢰성 있게 검증한다. - 범위: 단위 테스트(Unit), 통합 테스트(Integration), 아키텍처 테스트(ArchUnit) 전반. - 테스트 실행 환경은 Gradle test 태스크를 표준으로 한다. 명령: + - 전체 테스트: `./gradlew test` - 테스트 프로필: Gradle가 자동으로 `spring.profiles.active=test`를 설정함 (build.gradle 근거). @@ -24,12 +28,14 @@ ## 2. 테스트 종류와 원칙 ### 2.1 단위 테스트 (Unit Test) + - 목적: 작은 단위(도메인, 유틸, 애플리케이션 서비스의 순수 로직)의 동작을 빠르고 고립적으로 검증. - 프레임워크 의존: 가급적 없음. Spring Context를 기동하지 않는다. - 목킹(Mock): 외부 의존성은 Mockito 등으로 대체. 저장소/네트워크/시큐리티 등 I/O 경계를 모킹한다. - 예시 근거: - 도메인: `src/test/java/org/mandarin/booking/domain/MemberTest.java`, `AbstractEntityTest.java` - - 보안 컴포넌트 단위: `adapter/security/JwtFilterTest.java`, `CustomAuthenticationProviderTest.java`, `CustomAuthenticationEntryPointTest.java` + - 보안 컴포넌트 단위: `adapter/security/JwtFilterTest.java`, `CustomAuthenticationProviderTest.java`, + `CustomAuthenticationEntryPointTest.java` - 공통/web 단위: `adapter/webapi/GlobalExceptionHandlerTest.java` - 라이브러리/설정 근거: - Mockito inline 사용: `build.gradle` → `testImplementation 'org.mockito:mockito-inline:5.2.0'` @@ -37,20 +43,23 @@ - ByteBuddy javaagent 사전 부착: `build.gradle` → `jvmArgs "-javaagent:${configurations.byteBuddyAgent.singleFile}"` 권장 규칙: + - 네이밍: 테스트 클래스는 대상 클래스명 + `Test` 또는 시나리오 중심 스펙 명(`*Specs`)을 사용 가능. - 패키지: 테스트 대상과 유사한 패키지 경로 하위에 배치하여 접근성을 높인다. - given-when-then 주석 또는 메서드명으로 시나리오를 명확히 표현한다. - 외부 시스템/DB 액세스 금지. 필요한 경우 포트/리포지토리를 모킹. ### 2.2 통합 테스트 (Integration Test) + - 목적: Spring Context를 실제로 기동하여, 보안 필터/컨트롤러/시리얼라이저/예외 처리 및 JPA/H2 동작을 포함해 엔드투엔드에 가까운 경로를 검증. - 프로필/환경: `application-test.yml` 사용. H2 메모리 DB, JPA `ddl-auto: create`, JWT 시크릿/TTL 설정 포함. - 보안: 실제 `SecurityConfig`와 `JwtFilter` 동작을 최대한 반영. 필요 시 테스트 전용 컨트롤러/설정 (`TestOnlyController`, `TestConfig`) 사용. -- 유틸리티: `IntegrationTest`, `IntegrationTestUtils`, `IntegrationTestUtilsSpecs`, `JwtTestUtils` 등 공용 유틸을 통해 테스트 준비/토큰 생성/컨텍스트 초기화. +- 유틸리티: `IntegrationTest`, `IntegrationTestUtils`, `IntegrationTestUtilsSpecs`, `JwtTestUtils` 등 공용 유틸을 통해 테스트 준비/토큰 + 생성/컨텍스트 초기화. - 예시 근거: - - 웹 API 스펙 테스트: `src/test/java/org/mandarin/booking/webapi/**/POST_specs.java` + - 웹 API 스펙 테스트: `src/test/java/org/mandarin/booking/webapi/**/POST_specs.java` - 통합 환경 유틸: `src/test/java/org/mandarin/booking/IntegrationTest*.java`, `JwtTestUtils.java` -권장 규칙: + 권장 규칙: - `@IntegrationTest` 커스텀 어노테이션 사용으로 공통 설정 - 각 테스트는 `IntegrationTestUtils`를 사용해 작성 - `IntegrationTestUtils` 사용 방법은 다음과 같음 @@ -64,7 +73,7 @@ assertThat(response.getData()).isEqualTo(PONG_WITHOUT_AUTH); } ``` - - ```java + - ```java @Test void failToAuth(@Autowired IntegrationTestUtils testUtils) { // Arrange @@ -85,6 +94,7 @@ - 검증은 최대한 `assertj`의 `assertThat`을 사용해 검증 통일. ### 2.3 아키텍처 테스트 (ArchUnit) + - 목적: 헥사고날 계층 규칙 준수 보장. - 근거 테스트: `src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java` - 핵심 규칙: @@ -95,6 +105,7 @@ --- ## 3. 테스트-환경 설정 + - Gradle test 태스크가 `spring.profiles.active=test`로 실행됨: `build.gradle` 65~70행 참고. - H2 설정: `application-test.yml` - URL: `jdbc:h2:mem:test;MODE=MySQL;` @@ -108,6 +119,7 @@ ## 4. 작성 규칙 ### 4.1 단위 테스트 작성 체크리스트 + - [ ] 단일 책임/작은 단위만 검증한다. - [ ] 외부 의존은 Mockito로 모킹한다. - [ ] 스프링 컨텍스트를 기동하지 않는다. @@ -115,6 +127,7 @@ - [ ] 경계값, 널/빈값 케이스 포함. ### 4.2 통합 테스트 작성 체크리스트 + - [ ] Spring 컨텍스트 기동 및 필요한 빈 주입 확인. - [ ] 보안 필터/JWT 인증 흐름을 실제로 검증한다. - [ ] 컨트롤러 → 앱 서비스 → 영속성(JPA/H2) 경로를 통해 상태 변화/응답을 확인한다. @@ -122,6 +135,7 @@ - [ ] 테스트 독립성을 보장하고, 데이터 격리를 유지한다. ### 4.3 공통 규칙 + - 테스트명/메서드명은 자연어에 가깝게 시나리오를 드러낸다(한글/영문 허용). - 반복 셋업은 유틸/추상 베이스 클래스로 추출(예: `IntegrationTestUtils`). - 민감정보(비밀번호, 토큰 원문 등)는 로그에 남기지 않는다. @@ -129,6 +143,7 @@ --- ## 5. API 스펙 연동 + - 각 API 문서의 "테스트" 체크박스를 충족하는 테스트를 작성/유지한다. - 로그인: `docs/specs/api/login.md` - 회원가입: `docs/specs/api/member_register.md` @@ -139,6 +154,7 @@ --- ## 6. 실행 방법과 성능 + - 표준 실행: `./gradlew test` - 통합 테스트의 컨텍스트 초기화 비용이 크므로, 로컬 개발 중에는 대상 패키지/클래스만 선별 실행을 권장. - 빠른 피드백: 도메인/유틸 단위 테스트 우선 실행 → 이후 통합 테스트. @@ -146,12 +162,14 @@ --- ## 7. CI 연동 + - GitHub Actions 등 CI 정의는 현재 저장소에서 확인 불가. - 향후 CI 도입 시, 최소 요구: `./gradlew clean test` + ArchUnit + SpotBugs. --- ## 8. 디렉터리/네이밍 가이드 + - 테스트 루트: `src/test/java` - 관례: - 단위 테스트: 대상 패키지에 맞춰 배치, 클래스명 `*Test` @@ -164,6 +182,7 @@ --- ## 9. 부록: 참고 클래스 + - ArchUnit: `src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java` - 보안 테스트: `src/test/java/org/mandarin/booking/adapter/security/*.java` - 웹 API 스펙: `src/test/java/org/mandarin/booking/webapi/**` diff --git a/docs/todo.md b/docs/todo.md index 408b7d3..f0b4d18 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,16 +1,19 @@ - 2025.08.25 + - [x] `올바른_요청을_보내면_status가_SUCCESS이다` 테스트의 거짓 양성 문제 해결 필요? -> 그것보단 테스트 케이스의 부실한 부분을 수정하는게 맞는듯 - - 올바르다의 기준이 명확할 필요가 있음 + - 올바르다의 기준이 명확할 필요가 있음 - [x] `TestResult`에 인증을 위한 토큰 입력부를 추가할 필요가 있음 2025.08.26 + - [x] `TestResult`에 인증을 위한 토큰 입력부 추가 2025.08.27 + - [x] 테스트 케이스 충족 2025.08.28 + - [x] 없는 엔드포인트에 대한 처리 어찌할지 고민 - [x] 리펙터링 diff --git a/domain/src/main/java/org/mandarin/booking/domain/AbstractEntity.java b/domain/src/main/java/org/mandarin/booking/domain/AbstractEntity.java index d543aa4..dae4e25 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/AbstractEntity.java +++ b/domain/src/main/java/org/mandarin/booking/domain/AbstractEntity.java @@ -24,11 +24,21 @@ public abstract class AbstractEntity { @Override public final boolean equals(Object o) { - if (this == o) return true; - if (o == null) return false; - Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); - Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); - if (thisEffectiveClass != oEffectiveClass) return false; + if (this == o) { + return true; + } + if (o == null) { + return false; + } + Class oEffectiveClass = + o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer() + .getPersistentClass() : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) { + return false; + } AbstractEntity that = (AbstractEntity) o; return getId() != null && Objects.equals(getId(), that.getId()); } diff --git a/domain/src/main/java/org/mandarin/booking/domain/member/Member.java b/domain/src/main/java/org/mandarin/booking/domain/member/Member.java index 8203ff0..ec526a8 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/member/Member.java +++ b/domain/src/main/java/org/mandarin/booking/domain/member/Member.java @@ -25,6 +25,16 @@ public class Member extends AbstractEntity { @Convert(converter = MemberAuthorityConverter.class) private List authorities = new ArrayList<>(); + public String[] getParsedAuthorities() { + return authorities.stream() + .map(MemberAuthority::getAuthority) + .toArray(String[]::new); + } + + public boolean matchesPassword(String rawPassword, SecurePasswordEncoder securePasswordEncoder) { + return securePasswordEncoder.matches(rawPassword, this.passwordHash); + } + public static Member create(MemberCreateCommand command, SecurePasswordEncoder securePasswordEncoder) { var member = new Member(); @@ -36,16 +46,6 @@ public static Member create(MemberCreateCommand command, return member; } - public String[] getParsedAuthorities() { - return authorities.stream() - .map(MemberAuthority::getAuthority) - .toArray(String[]::new); - } - - public boolean matchesPassword(String rawPassword, SecurePasswordEncoder securePasswordEncoder) { - return securePasswordEncoder.matches(rawPassword, this.passwordHash); - } - - public record MemberCreateCommand(String nickName, String userId, String password, String email){ + public record MemberCreateCommand(String nickName, String userId, String password, String email) { } } diff --git a/domain/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java b/domain/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java index 80216ff..e557897 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java +++ b/domain/src/main/java/org/mandarin/booking/domain/member/SecurePasswordEncoder.java @@ -5,5 +5,6 @@ @NullUnmarked public interface SecurePasswordEncoder { String encode(String password); + boolean matches(String rawPassword, String encodedPassword); } diff --git a/domain/src/main/java/org/mandarin/booking/domain/show/Show.java b/domain/src/main/java/org/mandarin/booking/domain/show/Show.java index 4c2997d..d5970a0 100644 --- a/domain/src/main/java/org/mandarin/booking/domain/show/Show.java +++ b/domain/src/main/java/org/mandarin/booking/domain/show/Show.java @@ -22,25 +22,18 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Show extends AbstractEntity { + @OneToMany(mappedBy = "show", fetch = LAZY, cascade = MERGE) + private final List schedules = new ArrayList<>(); private String title; - @Enumerated(EnumType.STRING) private Type type; - @Enumerated(EnumType.STRING) private Rating rating; - private String synopsis; - private String posterUrl; - private LocalDate performanceStartDate; - private LocalDate performanceEndDate; - @OneToMany(mappedBy = "show", fetch = LAZY, cascade = MERGE) - private final List schedules = new ArrayList<>(); - private Show(String title, Type type, Rating rating, String synopsis, String posterUrl, LocalDate performanceStartDate, LocalDate performanceEndDate) { @@ -53,6 +46,14 @@ private Show(String title, Type type, Rating rating, String synopsis, String pos this.performanceEndDate = performanceEndDate; } + public void registerSchedule(Long hallId, ShowScheduleCreateCommand command) { + if (!isInSchedule(command.startAt(), command.endAt())) { + throw new ShowException("BAD_REQUEST", "공연 기간 범위를 벗어나는 일정입니다."); + } + + var schedule = ShowSchedule.create(this, hallId, command); + this.schedules.add(schedule); + } public static Show create(ShowCreateCommand command) { var startDate = command.getPerformanceStartDate(); @@ -73,15 +74,6 @@ public static Show create(ShowCreateCommand command) { ); } - public void registerSchedule(Long hallId, ShowScheduleCreateCommand command) { - if (!isInSchedule(command.startAt(), command.endAt())) { - throw new ShowException("BAD_REQUEST", "공연 기간 범위를 벗어나는 일정입니다."); - } - - var schedule = ShowSchedule.create(this, hallId, command); - this.schedules.add(schedule); - } - private boolean isInSchedule(LocalDateTime scheduleStartAt, LocalDateTime scheduleEndAt) { return scheduleStartAt.isAfter(performanceStartDate.atStartOfDay()) && scheduleEndAt.isBefore(performanceEndDate.atStartOfDay()); diff --git a/domain/src/test/java/org/mandarin/booking/domain/AbstractEntityTest.java b/domain/src/test/java/org/mandarin/booking/domain/AbstractEntityTest.java index a9e7e2f..bc0a707 100644 --- a/domain/src/test/java/org/mandarin/booking/domain/AbstractEntityTest.java +++ b/domain/src/test/java/org/mandarin/booking/domain/AbstractEntityTest.java @@ -12,8 +12,16 @@ class AbstractEntityTest { - static class Member extends AbstractEntity { } - static class Product extends AbstractEntity { } + private static T withId(T entity, Long id) { + try { + Field f = AbstractEntity.class.getDeclaredField("id"); + f.setAccessible(true); + f.set(entity, id); + return entity; + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } @Nested @DisplayName("proxy branches") @@ -44,52 +52,6 @@ void hashCode_proxy_branch() { } } - // ---- HibernateProxy test doubles to cover proxy branches ---- - static class ProxyMember extends Member implements org.hibernate.proxy.HibernateProxy { - @Override - public org.hibernate.proxy.LazyInitializer getHibernateLazyInitializer() { - Class persistentClass = Member.class; - return (org.hibernate.proxy.LazyInitializer) java.lang.reflect.Proxy.newProxyInstance( - getClass().getClassLoader(), - new Class[]{org.hibernate.proxy.LazyInitializer.class}, - (proxy, method, args) -> { - if (method.getName().equals("getPersistentClass")) { - return persistentClass; - } - if (method.getReturnType().isPrimitive()) { - if (method.getReturnType() == boolean.class) { - return false; - } - if (method.getReturnType() == int.class) { - return 0; - } - if (method.getReturnType() == long.class) { - return 0L; - } - } - return null; - } - ); - } - - @Serial - @Override - public Object writeReplace() { - return this; - } - } - - private static T withId(T entity, Long id) { - try { - Field f = AbstractEntity.class.getDeclaredField("id"); - f.setAccessible(true); - f.set(entity, id); - return entity; - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } - @Nested @DisplayName("equals") class EqualsSpec { @@ -191,6 +153,47 @@ void hashCode_Collection() { } } + static class Member extends AbstractEntity { + } + + static class Product extends AbstractEntity { + } + + // ---- HibernateProxy test doubles to cover proxy branches ---- + static class ProxyMember extends Member implements org.hibernate.proxy.HibernateProxy { + @Override + public org.hibernate.proxy.LazyInitializer getHibernateLazyInitializer() { + Class persistentClass = Member.class; + return (org.hibernate.proxy.LazyInitializer) java.lang.reflect.Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[]{org.hibernate.proxy.LazyInitializer.class}, + (proxy, method, args) -> { + if (method.getName().equals("getPersistentClass")) { + return persistentClass; + } + if (method.getReturnType().isPrimitive()) { + if (method.getReturnType() == boolean.class) { + return false; + } + if (method.getReturnType() == int.class) { + return 0; + } + if (method.getReturnType() == long.class) { + return 0L; + } + } + return null; + } + ); + } + + @Serial + @Override + public Object writeReplace() { + return this; + } + } + static class ProxyProduct extends Product implements org.hibernate.proxy.HibernateProxy { @Override public org.hibernate.proxy.LazyInitializer getHibernateLazyInitializer() { diff --git a/internal/src/main/java/org/mandarin/booking/adapter/JwtTokenUtils.java b/internal/src/main/java/org/mandarin/booking/adapter/JwtTokenUtils.java index 04245ab..7704cc0 100644 --- a/internal/src/main/java/org/mandarin/booking/adapter/JwtTokenUtils.java +++ b/internal/src/main/java/org/mandarin/booking/adapter/JwtTokenUtils.java @@ -56,7 +56,7 @@ public TokenHolder generateToken(String refreshToken) { String userId = claims.getPayload().get(USER_ID).toString(); String nickName = claims.getPayload().get(NICK_NAME).toString(); List authorities = Arrays.stream(claims.getPayload().get(ROLES, String.class).split(",")) - .map(s->s.substring(5)) + .map(s -> s.substring(5)) .map(MemberAuthority::valueOf) .toList(); return generateToken(userId, nickName, authorities); @@ -73,8 +73,9 @@ public String getClaim(String token, String claimName) { public Collection getClaims(String token, String claimName) { Jws claims = parseClaims(token); var rawPayload = claims.getPayload().get(claimName, String.class); - if(rawPayload.isBlank()) + if (rawPayload.isBlank()) { return new ArrayList<>(); + } return Arrays.stream(rawPayload.split(",")).toList(); } From ddd3bcf5df788b129c344c87d319981eb17e2fd7 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 11 Sep 2025 12:58:37 +0900 Subject: [PATCH 19/36] remove public modifier from controller and repository classes --- .../org/mandarin/booking/app/persist/HallRepository.java | 2 +- .../org/mandarin/booking/app/persist/MemberRepository.java | 2 +- .../org/mandarin/booking/app/persist/ShowRepository.java | 2 +- .../java/org/mandarin/booking/webapi/AuthController.java | 6 +++--- .../java/org/mandarin/booking/webapi/MemberController.java | 4 ++-- .../java/org/mandarin/booking/webapi/ShowController.java | 6 +++--- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/mandarin/booking/app/persist/HallRepository.java b/application/src/main/java/org/mandarin/booking/app/persist/HallRepository.java index c34fd54..5a93620 100644 --- a/application/src/main/java/org/mandarin/booking/app/persist/HallRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/persist/HallRepository.java @@ -3,7 +3,7 @@ import org.mandarin.booking.domain.venue.Hall; import org.springframework.data.repository.Repository; -public interface HallRepository extends Repository { +interface HallRepository extends Repository { Hall save(Hall hall); boolean existsById(Long id); diff --git a/application/src/main/java/org/mandarin/booking/app/persist/MemberRepository.java b/application/src/main/java/org/mandarin/booking/app/persist/MemberRepository.java index e7ddb6a..baede3c 100644 --- a/application/src/main/java/org/mandarin/booking/app/persist/MemberRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/persist/MemberRepository.java @@ -4,7 +4,7 @@ import org.mandarin.booking.domain.member.Member; import org.springframework.data.repository.Repository; -public interface MemberRepository extends Repository { +interface MemberRepository extends Repository { boolean existsByUserId(String userId); boolean existsByEmail(String email); diff --git a/application/src/main/java/org/mandarin/booking/app/persist/ShowRepository.java b/application/src/main/java/org/mandarin/booking/app/persist/ShowRepository.java index e152cfa..3c21a0e 100644 --- a/application/src/main/java/org/mandarin/booking/app/persist/ShowRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/persist/ShowRepository.java @@ -4,7 +4,7 @@ import org.mandarin.booking.domain.show.Show; import org.springframework.data.repository.Repository; -public interface ShowRepository extends Repository { +interface ShowRepository extends Repository { Show save(Show show); boolean existsByTitle(String title); diff --git a/application/src/main/java/org/mandarin/booking/webapi/AuthController.java b/application/src/main/java/org/mandarin/booking/webapi/AuthController.java index 2749a2d..23124d4 100644 --- a/application/src/main/java/org/mandarin/booking/webapi/AuthController.java +++ b/application/src/main/java/org/mandarin/booking/webapi/AuthController.java @@ -12,15 +12,15 @@ @RestController @RequestMapping("/api/auth") -public record AuthController(AuthUseCase authUsecase) { +record AuthController(AuthUseCase authUsecase) { @PostMapping("/login") - public TokenHolder login(@RequestBody @Valid AuthRequest request) { + TokenHolder login(@RequestBody @Valid AuthRequest request) { return authUsecase.login(request.userId(), request.password()); } @PostMapping("/reissue") - public TokenHolder reissue(@RequestBody @Valid ReissueRequest request) { + TokenHolder reissue(@RequestBody @Valid ReissueRequest request) { return authUsecase.reissue(request.refreshToken()); } } diff --git a/application/src/main/java/org/mandarin/booking/webapi/MemberController.java b/application/src/main/java/org/mandarin/booking/webapi/MemberController.java index 041e105..707c340 100644 --- a/application/src/main/java/org/mandarin/booking/webapi/MemberController.java +++ b/application/src/main/java/org/mandarin/booking/webapi/MemberController.java @@ -11,10 +11,10 @@ @RestController @RequestMapping("/api/member") -public record MemberController(MemberRegisterer memberRegisterer) { +record MemberController(MemberRegisterer memberRegisterer) { @PostMapping - public MemberRegisterResponse register(@RequestBody @Valid MemberRegisterRequest request) { + MemberRegisterResponse register(@RequestBody @Valid MemberRegisterRequest request) { return memberRegisterer.register(request); } } diff --git a/application/src/main/java/org/mandarin/booking/webapi/ShowController.java b/application/src/main/java/org/mandarin/booking/webapi/ShowController.java index c51e070..c0724eb 100644 --- a/application/src/main/java/org/mandarin/booking/webapi/ShowController.java +++ b/application/src/main/java/org/mandarin/booking/webapi/ShowController.java @@ -13,15 +13,15 @@ @RestController @RequestMapping("/api/show") -public record ShowController(ShowRegisterer showRegisterer) { +record ShowController(ShowRegisterer showRegisterer) { @PostMapping - public ShowRegisterResponse register(@RequestBody @Valid ShowRegisterRequest request) { + ShowRegisterResponse register(@RequestBody @Valid ShowRegisterRequest request) { return showRegisterer.register(request); } @PostMapping("/schedule") - public ShowScheduleRegisterResponse registerSchedule(@RequestBody @Valid ShowScheduleRegisterRequest request) { + ShowScheduleRegisterResponse registerSchedule(@RequestBody @Valid ShowScheduleRegisterRequest request) { return showRegisterer.registerSchedule(request); } } From 54c66dd50b5da0599654ac35dadb20cd438047a8 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Thu, 11 Sep 2025 13:05:10 +0900 Subject: [PATCH 20/36] rename webapi packages to adapter and update dependencies --- application/build.gradle | 1 - .../mandarin/booking/{ => adapter}/webapi/AuthController.java | 2 +- .../mandarin/booking/{ => adapter}/webapi/MemberController.java | 2 +- .../mandarin/booking/{ => adapter}/webapi/ShowController.java | 2 +- domain/build.gradle | 2 +- internal/build.gradle | 2 +- 6 files changed, 5 insertions(+), 6 deletions(-) rename application/src/main/java/org/mandarin/booking/{ => adapter}/webapi/AuthController.java (95%) rename application/src/main/java/org/mandarin/booking/{ => adapter}/webapi/MemberController.java (94%) rename application/src/main/java/org/mandarin/booking/{ => adapter}/webapi/ShowController.java (95%) diff --git a/application/build.gradle b/application/build.gradle index 8e486ac..bc1e625 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -17,7 +17,6 @@ dependencies { api(project(':internal')) api(project(':external')) - implementation project(':common') // ---- Spring Boot Core ---- implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-actuator' diff --git a/application/src/main/java/org/mandarin/booking/webapi/AuthController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java similarity index 95% rename from application/src/main/java/org/mandarin/booking/webapi/AuthController.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java index 23124d4..7d67b5c 100644 --- a/application/src/main/java/org/mandarin/booking/webapi/AuthController.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.webapi; +package org.mandarin.booking.adapter.webapi; import jakarta.validation.Valid; import org.mandarin.booking.TokenHolder; diff --git a/application/src/main/java/org/mandarin/booking/webapi/MemberController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java similarity index 94% rename from application/src/main/java/org/mandarin/booking/webapi/MemberController.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java index 707c340..fc3e413 100644 --- a/application/src/main/java/org/mandarin/booking/webapi/MemberController.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.webapi; +package org.mandarin.booking.adapter.webapi; import jakarta.validation.Valid; import org.mandarin.booking.app.port.MemberRegisterer; diff --git a/application/src/main/java/org/mandarin/booking/webapi/ShowController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java similarity index 95% rename from application/src/main/java/org/mandarin/booking/webapi/ShowController.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java index c0724eb..7028b3e 100644 --- a/application/src/main/java/org/mandarin/booking/webapi/ShowController.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.webapi; +package org.mandarin.booking.adapter.webapi; import jakarta.validation.Valid; import org.mandarin.booking.app.port.ShowRegisterer; diff --git a/domain/build.gradle b/domain/build.gradle index fda90f7..dba45aa 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -7,7 +7,7 @@ jar { } dependencies { - implementation project(':common') + api project(':common') api 'org.springframework.boot:spring-boot-starter-data-jpa' api 'org.springframework.boot:spring-boot-starter-validation' diff --git a/internal/build.gradle b/internal/build.gradle index e14cb97..fe36461 100644 --- a/internal/build.gradle +++ b/internal/build.gradle @@ -1,5 +1,5 @@ dependencies { - implementation project(':common') + api project(':common') // ---- Spring Boot Web ---- api 'org.springframework.boot:spring-boot-starter-web' From aaa1a4ff036685ec8ed7e56efb1b26e194d60908 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 12 Sep 2025 00:41:23 +0900 Subject: [PATCH 21/36] move packages to improve modularity and organization --- .../adapter/security/CustomAuthenticationProvider.java | 2 +- .../{adapter/webapi => app/member}/AuthController.java | 3 +-- .../org/mandarin/booking/app/{ => member}/AuthService.java | 4 +--- .../mandarin/booking/app/{port => member}/AuthUseCase.java | 2 +- .../app/{persist => member}/MemberCommandRepository.java | 2 +- .../{adapter/webapi => app/member}/MemberController.java | 3 +-- .../app/{persist => member}/MemberQueryRepository.java | 2 +- .../booking/app/{ => member}/MemberRegisterValidator.java | 3 +-- .../booking/app/{port => member}/MemberRegisterer.java | 2 +- .../booking/app/{persist => member}/MemberRepository.java | 2 +- .../mandarin/booking/app/{ => member}/MemberService.java | 4 +--- .../app/{persist => show}/ShowCommandRepository.java | 2 +- .../{adapter/webapi => app/show}/ShowController.java | 3 +-- .../booking/app/{persist => show}/ShowQueryRepository.java | 2 +- .../mandarin/booking/app/{port => show}/ShowRegisterer.java | 2 +- .../booking/app/{persist => show}/ShowRepository.java | 2 +- .../org/mandarin/booking/app/{ => show}/ShowService.java | 6 ++---- .../app/{persist => venue}/HallCommandRepository.java | 2 +- .../booking/app/{ => venue}/HallExistCheckEvent.java | 2 +- .../booking/app/{persist => venue}/HallQueryRepository.java | 2 +- .../booking/app/{persist => venue}/HallRepository.java | 2 +- .../org/mandarin/booking/app/{ => venue}/HallService.java | 3 +-- .../java/org/mandarin/booking/IntegrationTestUtils.java | 6 +++--- .../src/test/java/org/mandarin/booking/TestConfig.java | 6 +++--- .../org/mandarin/booking/webapi/auth/login/POST_specs.java | 2 +- .../java/org/mandarin/booking/webapi/member/POST_specs.java | 2 +- 26 files changed, 31 insertions(+), 42 deletions(-) rename application/src/main/java/org/mandarin/booking/{adapter/webapi => app/member}/AuthController.java (90%) rename application/src/main/java/org/mandarin/booking/app/{ => member}/AuthService.java (92%) rename application/src/main/java/org/mandarin/booking/app/{port => member}/AuthUseCase.java (81%) rename application/src/main/java/org/mandarin/booking/app/{persist => member}/MemberCommandRepository.java (91%) rename application/src/main/java/org/mandarin/booking/{adapter/webapi => app/member}/MemberController.java (87%) rename application/src/main/java/org/mandarin/booking/app/{persist => member}/MemberQueryRepository.java (94%) rename application/src/main/java/org/mandarin/booking/app/{ => member}/MemberRegisterValidator.java (87%) rename application/src/main/java/org/mandarin/booking/app/{port => member}/MemberRegisterer.java (85%) rename application/src/main/java/org/mandarin/booking/app/{persist => member}/MemberRepository.java (89%) rename application/src/main/java/org/mandarin/booking/app/{ => member}/MemberService.java (88%) rename application/src/main/java/org/mandarin/booking/app/{persist => show}/ShowCommandRepository.java (91%) rename application/src/main/java/org/mandarin/booking/{adapter/webapi => app/show}/ShowController.java (91%) rename application/src/main/java/org/mandarin/booking/app/{persist => show}/ShowQueryRepository.java (97%) rename application/src/main/java/org/mandarin/booking/app/{port => show}/ShowRegisterer.java (92%) rename application/src/main/java/org/mandarin/booking/app/{persist => show}/ShowRepository.java (87%) rename application/src/main/java/org/mandarin/booking/app/{ => show}/ShowService.java (93%) rename application/src/main/java/org/mandarin/booking/app/{persist => venue}/HallCommandRepository.java (90%) rename application/src/main/java/org/mandarin/booking/app/{ => venue}/HallExistCheckEvent.java (87%) rename application/src/main/java/org/mandarin/booking/app/{persist => venue}/HallQueryRepository.java (90%) rename application/src/main/java/org/mandarin/booking/app/{persist => venue}/HallRepository.java (84%) rename application/src/main/java/org/mandarin/booking/app/{ => venue}/HallService.java (83%) diff --git a/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java b/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java index 30b2c28..2ad51a0 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java +++ b/application/src/main/java/org/mandarin/booking/adapter/security/CustomAuthenticationProvider.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import org.mandarin.booking.AuthException; import org.mandarin.booking.adapter.CustomMemberAuthenticationToken; -import org.mandarin.booking.app.persist.MemberQueryRepository; +import org.mandarin.booking.app.member.MemberQueryRepository; import org.mandarin.booking.domain.member.Member; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java b/application/src/main/java/org/mandarin/booking/app/member/AuthController.java similarity index 90% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java rename to application/src/main/java/org/mandarin/booking/app/member/AuthController.java index 7d67b5c..665eb6d 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java +++ b/application/src/main/java/org/mandarin/booking/app/member/AuthController.java @@ -1,8 +1,7 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.app.member; import jakarta.validation.Valid; import org.mandarin.booking.TokenHolder; -import org.mandarin.booking.app.port.AuthUseCase; import org.mandarin.booking.domain.member.AuthRequest; import org.mandarin.booking.domain.member.ReissueRequest; import org.springframework.web.bind.annotation.PostMapping; diff --git a/application/src/main/java/org/mandarin/booking/app/AuthService.java b/application/src/main/java/org/mandarin/booking/app/member/AuthService.java similarity index 92% rename from application/src/main/java/org/mandarin/booking/app/AuthService.java rename to application/src/main/java/org/mandarin/booking/app/member/AuthService.java index 718175d..73b0fa9 100644 --- a/application/src/main/java/org/mandarin/booking/app/AuthService.java +++ b/application/src/main/java/org/mandarin/booking/app/member/AuthService.java @@ -1,11 +1,9 @@ -package org.mandarin.booking.app; +package org.mandarin.booking.app.member; import lombok.RequiredArgsConstructor; import org.mandarin.booking.AuthException; import org.mandarin.booking.TokenHolder; import org.mandarin.booking.adapter.TokenUtils; -import org.mandarin.booking.app.persist.MemberQueryRepository; -import org.mandarin.booking.app.port.AuthUseCase; import org.mandarin.booking.domain.member.Member; import org.mandarin.booking.domain.member.SecurePasswordEncoder; import org.springframework.stereotype.Service; diff --git a/application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java b/application/src/main/java/org/mandarin/booking/app/member/AuthUseCase.java similarity index 81% rename from application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java rename to application/src/main/java/org/mandarin/booking/app/member/AuthUseCase.java index 14afc17..a55b3da 100644 --- a/application/src/main/java/org/mandarin/booking/app/port/AuthUseCase.java +++ b/application/src/main/java/org/mandarin/booking/app/member/AuthUseCase.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app.port; +package org.mandarin.booking.app.member; import org.mandarin.booking.TokenHolder; diff --git a/application/src/main/java/org/mandarin/booking/app/persist/MemberCommandRepository.java b/application/src/main/java/org/mandarin/booking/app/member/MemberCommandRepository.java similarity index 91% rename from application/src/main/java/org/mandarin/booking/app/persist/MemberCommandRepository.java rename to application/src/main/java/org/mandarin/booking/app/member/MemberCommandRepository.java index 51f7b4d..f666975 100644 --- a/application/src/main/java/org/mandarin/booking/app/persist/MemberCommandRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/member/MemberCommandRepository.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app.persist; +package org.mandarin.booking.app.member; import lombok.RequiredArgsConstructor; import org.mandarin.booking.domain.member.Member; diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java b/application/src/main/java/org/mandarin/booking/app/member/MemberController.java similarity index 87% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java rename to application/src/main/java/org/mandarin/booking/app/member/MemberController.java index fc3e413..31d87ff 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java +++ b/application/src/main/java/org/mandarin/booking/app/member/MemberController.java @@ -1,7 +1,6 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.app.member; import jakarta.validation.Valid; -import org.mandarin.booking.app.port.MemberRegisterer; import org.mandarin.booking.domain.member.MemberRegisterRequest; import org.mandarin.booking.domain.member.MemberRegisterResponse; import org.springframework.web.bind.annotation.PostMapping; diff --git a/application/src/main/java/org/mandarin/booking/app/persist/MemberQueryRepository.java b/application/src/main/java/org/mandarin/booking/app/member/MemberQueryRepository.java similarity index 94% rename from application/src/main/java/org/mandarin/booking/app/persist/MemberQueryRepository.java rename to application/src/main/java/org/mandarin/booking/app/member/MemberQueryRepository.java index bb23695..d91c351 100644 --- a/application/src/main/java/org/mandarin/booking/app/persist/MemberQueryRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/member/MemberQueryRepository.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app.persist; +package org.mandarin.booking.app.member; import java.util.Optional; import lombok.RequiredArgsConstructor; diff --git a/application/src/main/java/org/mandarin/booking/app/MemberRegisterValidator.java b/application/src/main/java/org/mandarin/booking/app/member/MemberRegisterValidator.java similarity index 87% rename from application/src/main/java/org/mandarin/booking/app/MemberRegisterValidator.java rename to application/src/main/java/org/mandarin/booking/app/member/MemberRegisterValidator.java index 4fd8ee6..88897a6 100644 --- a/application/src/main/java/org/mandarin/booking/app/MemberRegisterValidator.java +++ b/application/src/main/java/org/mandarin/booking/app/member/MemberRegisterValidator.java @@ -1,7 +1,6 @@ -package org.mandarin.booking.app; +package org.mandarin.booking.app.member; import lombok.RequiredArgsConstructor; -import org.mandarin.booking.app.persist.MemberQueryRepository; import org.mandarin.booking.domain.member.MemberException; import org.springframework.stereotype.Component; diff --git a/application/src/main/java/org/mandarin/booking/app/port/MemberRegisterer.java b/application/src/main/java/org/mandarin/booking/app/member/MemberRegisterer.java similarity index 85% rename from application/src/main/java/org/mandarin/booking/app/port/MemberRegisterer.java rename to application/src/main/java/org/mandarin/booking/app/member/MemberRegisterer.java index 99867cf..9d4b0ea 100644 --- a/application/src/main/java/org/mandarin/booking/app/port/MemberRegisterer.java +++ b/application/src/main/java/org/mandarin/booking/app/member/MemberRegisterer.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app.port; +package org.mandarin.booking.app.member; import org.mandarin.booking.domain.member.MemberRegisterRequest; import org.mandarin.booking.domain.member.MemberRegisterResponse; diff --git a/application/src/main/java/org/mandarin/booking/app/persist/MemberRepository.java b/application/src/main/java/org/mandarin/booking/app/member/MemberRepository.java similarity index 89% rename from application/src/main/java/org/mandarin/booking/app/persist/MemberRepository.java rename to application/src/main/java/org/mandarin/booking/app/member/MemberRepository.java index baede3c..685972f 100644 --- a/application/src/main/java/org/mandarin/booking/app/persist/MemberRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/member/MemberRepository.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app.persist; +package org.mandarin.booking.app.member; import java.util.Optional; import org.mandarin.booking.domain.member.Member; diff --git a/application/src/main/java/org/mandarin/booking/app/MemberService.java b/application/src/main/java/org/mandarin/booking/app/member/MemberService.java similarity index 88% rename from application/src/main/java/org/mandarin/booking/app/MemberService.java rename to application/src/main/java/org/mandarin/booking/app/member/MemberService.java index ba5c9ce..02083fc 100644 --- a/application/src/main/java/org/mandarin/booking/app/MemberService.java +++ b/application/src/main/java/org/mandarin/booking/app/member/MemberService.java @@ -1,8 +1,6 @@ -package org.mandarin.booking.app; +package org.mandarin.booking.app.member; import lombok.RequiredArgsConstructor; -import org.mandarin.booking.app.persist.MemberCommandRepository; -import org.mandarin.booking.app.port.MemberRegisterer; import org.mandarin.booking.domain.member.Member; import org.mandarin.booking.domain.member.Member.MemberCreateCommand; import org.mandarin.booking.domain.member.MemberRegisterRequest; diff --git a/application/src/main/java/org/mandarin/booking/app/persist/ShowCommandRepository.java b/application/src/main/java/org/mandarin/booking/app/show/ShowCommandRepository.java similarity index 91% rename from application/src/main/java/org/mandarin/booking/app/persist/ShowCommandRepository.java rename to application/src/main/java/org/mandarin/booking/app/show/ShowCommandRepository.java index c5fb793..3344547 100644 --- a/application/src/main/java/org/mandarin/booking/app/persist/ShowCommandRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowCommandRepository.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app.persist; +package org.mandarin.booking.app.show; import lombok.RequiredArgsConstructor; import org.mandarin.booking.domain.show.Show; diff --git a/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java b/application/src/main/java/org/mandarin/booking/app/show/ShowController.java similarity index 91% rename from application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java rename to application/src/main/java/org/mandarin/booking/app/show/ShowController.java index 7028b3e..9713c43 100644 --- a/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowController.java @@ -1,7 +1,6 @@ -package org.mandarin.booking.adapter.webapi; +package org.mandarin.booking.app.show; import jakarta.validation.Valid; -import org.mandarin.booking.app.port.ShowRegisterer; import org.mandarin.booking.domain.show.ShowRegisterRequest; import org.mandarin.booking.domain.show.ShowRegisterResponse; import org.mandarin.booking.domain.show.ShowScheduleRegisterRequest; diff --git a/application/src/main/java/org/mandarin/booking/app/persist/ShowQueryRepository.java b/application/src/main/java/org/mandarin/booking/app/show/ShowQueryRepository.java similarity index 97% rename from application/src/main/java/org/mandarin/booking/app/persist/ShowQueryRepository.java rename to application/src/main/java/org/mandarin/booking/app/show/ShowQueryRepository.java index 6e90912..b397472 100644 --- a/application/src/main/java/org/mandarin/booking/app/persist/ShowQueryRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowQueryRepository.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app.persist; +package org.mandarin.booking.app.show; import static org.mandarin.booking.domain.show.QShowSchedule.showSchedule; diff --git a/application/src/main/java/org/mandarin/booking/app/port/ShowRegisterer.java b/application/src/main/java/org/mandarin/booking/app/show/ShowRegisterer.java similarity index 92% rename from application/src/main/java/org/mandarin/booking/app/port/ShowRegisterer.java rename to application/src/main/java/org/mandarin/booking/app/show/ShowRegisterer.java index eb4b66c..cc9a038 100644 --- a/application/src/main/java/org/mandarin/booking/app/port/ShowRegisterer.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowRegisterer.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app.port; +package org.mandarin.booking.app.show; import jakarta.validation.Valid; import org.mandarin.booking.domain.show.ShowRegisterRequest; diff --git a/application/src/main/java/org/mandarin/booking/app/persist/ShowRepository.java b/application/src/main/java/org/mandarin/booking/app/show/ShowRepository.java similarity index 87% rename from application/src/main/java/org/mandarin/booking/app/persist/ShowRepository.java rename to application/src/main/java/org/mandarin/booking/app/show/ShowRepository.java index 3c21a0e..9dfe79e 100644 --- a/application/src/main/java/org/mandarin/booking/app/persist/ShowRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowRepository.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app.persist; +package org.mandarin.booking.app.show; import java.util.Optional; import org.mandarin.booking.domain.show.Show; diff --git a/application/src/main/java/org/mandarin/booking/app/ShowService.java b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java similarity index 93% rename from application/src/main/java/org/mandarin/booking/app/ShowService.java rename to application/src/main/java/org/mandarin/booking/app/show/ShowService.java index 98bad9a..3dd828a 100644 --- a/application/src/main/java/org/mandarin/booking/app/ShowService.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java @@ -1,11 +1,9 @@ -package org.mandarin.booking.app; +package org.mandarin.booking.app.show; import static java.util.Objects.requireNonNull; import lombok.RequiredArgsConstructor; -import org.mandarin.booking.app.persist.ShowCommandRepository; -import org.mandarin.booking.app.persist.ShowQueryRepository; -import org.mandarin.booking.app.port.ShowRegisterer; +import org.mandarin.booking.app.venue.HallExistCheckEvent; import org.mandarin.booking.domain.show.Show; import org.mandarin.booking.domain.show.Show.ShowCreateCommand; import org.mandarin.booking.domain.show.ShowException; diff --git a/application/src/main/java/org/mandarin/booking/app/persist/HallCommandRepository.java b/application/src/main/java/org/mandarin/booking/app/venue/HallCommandRepository.java similarity index 90% rename from application/src/main/java/org/mandarin/booking/app/persist/HallCommandRepository.java rename to application/src/main/java/org/mandarin/booking/app/venue/HallCommandRepository.java index 2389f62..5f18b0f 100644 --- a/application/src/main/java/org/mandarin/booking/app/persist/HallCommandRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/venue/HallCommandRepository.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app.persist; +package org.mandarin.booking.app.venue; import lombok.RequiredArgsConstructor; import org.mandarin.booking.domain.venue.Hall; diff --git a/application/src/main/java/org/mandarin/booking/app/HallExistCheckEvent.java b/application/src/main/java/org/mandarin/booking/app/venue/HallExistCheckEvent.java similarity index 87% rename from application/src/main/java/org/mandarin/booking/app/HallExistCheckEvent.java rename to application/src/main/java/org/mandarin/booking/app/venue/HallExistCheckEvent.java index c23a11d..de0ab51 100644 --- a/application/src/main/java/org/mandarin/booking/app/HallExistCheckEvent.java +++ b/application/src/main/java/org/mandarin/booking/app/venue/HallExistCheckEvent.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app; +package org.mandarin.booking.app.venue; import lombok.Getter; diff --git a/application/src/main/java/org/mandarin/booking/app/persist/HallQueryRepository.java b/application/src/main/java/org/mandarin/booking/app/venue/HallQueryRepository.java similarity index 90% rename from application/src/main/java/org/mandarin/booking/app/persist/HallQueryRepository.java rename to application/src/main/java/org/mandarin/booking/app/venue/HallQueryRepository.java index 0bdb28a..34b4b40 100644 --- a/application/src/main/java/org/mandarin/booking/app/persist/HallQueryRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/venue/HallQueryRepository.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app.persist; +package org.mandarin.booking.app.venue; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; diff --git a/application/src/main/java/org/mandarin/booking/app/persist/HallRepository.java b/application/src/main/java/org/mandarin/booking/app/venue/HallRepository.java similarity index 84% rename from application/src/main/java/org/mandarin/booking/app/persist/HallRepository.java rename to application/src/main/java/org/mandarin/booking/app/venue/HallRepository.java index 5a93620..a1bd6bd 100644 --- a/application/src/main/java/org/mandarin/booking/app/persist/HallRepository.java +++ b/application/src/main/java/org/mandarin/booking/app/venue/HallRepository.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.app.persist; +package org.mandarin.booking.app.venue; import org.mandarin.booking.domain.venue.Hall; import org.springframework.data.repository.Repository; diff --git a/application/src/main/java/org/mandarin/booking/app/HallService.java b/application/src/main/java/org/mandarin/booking/app/venue/HallService.java similarity index 83% rename from application/src/main/java/org/mandarin/booking/app/HallService.java rename to application/src/main/java/org/mandarin/booking/app/venue/HallService.java index c04a0df..b6c1b81 100644 --- a/application/src/main/java/org/mandarin/booking/app/HallService.java +++ b/application/src/main/java/org/mandarin/booking/app/venue/HallService.java @@ -1,7 +1,6 @@ -package org.mandarin.booking.app; +package org.mandarin.booking.app.venue; import lombok.RequiredArgsConstructor; -import org.mandarin.booking.app.persist.HallQueryRepository; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; diff --git a/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java b/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java index 826e108..e7699d6 100644 --- a/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java +++ b/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java @@ -12,9 +12,9 @@ import java.util.List; import java.util.UUID; import org.mandarin.booking.adapter.TokenUtils; -import org.mandarin.booking.app.persist.HallCommandRepository; -import org.mandarin.booking.app.persist.MemberCommandRepository; -import org.mandarin.booking.app.persist.ShowCommandRepository; +import org.mandarin.booking.app.member.MemberCommandRepository; +import org.mandarin.booking.app.show.ShowCommandRepository; +import org.mandarin.booking.app.venue.HallCommandRepository; import org.mandarin.booking.domain.member.Member; import org.mandarin.booking.domain.member.Member.MemberCreateCommand; import org.mandarin.booking.domain.member.SecurePasswordEncoder; diff --git a/application/src/test/java/org/mandarin/booking/TestConfig.java b/application/src/test/java/org/mandarin/booking/TestConfig.java index 2e95a6c..312fe18 100644 --- a/application/src/test/java/org/mandarin/booking/TestConfig.java +++ b/application/src/test/java/org/mandarin/booking/TestConfig.java @@ -2,9 +2,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.mandarin.booking.adapter.TokenUtils; -import org.mandarin.booking.app.persist.HallCommandRepository; -import org.mandarin.booking.app.persist.MemberCommandRepository; -import org.mandarin.booking.app.persist.ShowCommandRepository; +import org.mandarin.booking.app.member.MemberCommandRepository; +import org.mandarin.booking.app.show.ShowCommandRepository; +import org.mandarin.booking.app.venue.HallCommandRepository; import org.mandarin.booking.domain.member.SecurePasswordEncoder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; diff --git a/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java index b9e907c..ad67635 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java @@ -20,7 +20,7 @@ import org.mandarin.booking.IntegrationTest; import org.mandarin.booking.IntegrationTestUtils; import org.mandarin.booking.TokenHolder; -import org.mandarin.booking.app.persist.MemberQueryRepository; +import org.mandarin.booking.app.member.MemberQueryRepository; import org.mandarin.booking.domain.member.AuthRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; diff --git a/application/src/test/java/org/mandarin/booking/webapi/member/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/member/POST_specs.java index 586e3ba..27307f8 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/member/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/member/POST_specs.java @@ -10,7 +10,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mandarin.booking.IntegrationTest; import org.mandarin.booking.IntegrationTestUtils; -import org.mandarin.booking.app.persist.MemberQueryRepository; +import org.mandarin.booking.app.member.MemberQueryRepository; import org.mandarin.booking.domain.member.MemberRegisterRequest; import org.mandarin.booking.domain.member.MemberRegisterResponse; import org.mandarin.booking.domain.member.SecurePasswordEncoder; From 1a0cad54f7eba82b534ef3c06f8a2517770fcda7 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 12 Sep 2025 01:00:04 +0900 Subject: [PATCH 22/36] refactor HallService and ShowService to implement HallValidator for hall existence checks --- .../mandarin/booking/app/show/ShowService.java | 16 +++------------- .../mandarin/booking/app/venue/HallService.java | 12 ++++++------ .../booking/app/venue/HallValidator.java | 5 +++++ 3 files changed, 14 insertions(+), 19 deletions(-) create mode 100644 application/src/main/java/org/mandarin/booking/app/venue/HallValidator.java diff --git a/application/src/main/java/org/mandarin/booking/app/show/ShowService.java b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java index 3dd828a..e75e755 100644 --- a/application/src/main/java/org/mandarin/booking/app/show/ShowService.java +++ b/application/src/main/java/org/mandarin/booking/app/show/ShowService.java @@ -3,7 +3,7 @@ import static java.util.Objects.requireNonNull; import lombok.RequiredArgsConstructor; -import org.mandarin.booking.app.venue.HallExistCheckEvent; +import org.mandarin.booking.app.venue.HallValidator; import org.mandarin.booking.domain.show.Show; import org.mandarin.booking.domain.show.Show.ShowCreateCommand; import org.mandarin.booking.domain.show.ShowException; @@ -12,8 +12,6 @@ import org.mandarin.booking.domain.show.ShowScheduleCreateCommand; import org.mandarin.booking.domain.show.ShowScheduleRegisterRequest; import org.mandarin.booking.domain.show.ShowScheduleRegisterResponse; -import org.mandarin.booking.domain.venue.HallException; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @Service @@ -21,7 +19,7 @@ public class ShowService implements ShowRegisterer { private final ShowCommandRepository commandRepository; private final ShowQueryRepository queryRepository; - private final ApplicationEventPublisher applicationEventPublisher; + private final HallValidator hallValidator; @Override public ShowRegisterResponse register(ShowRegisterRequest request) { @@ -39,7 +37,7 @@ public ShowScheduleRegisterResponse registerSchedule(ShowScheduleRegisterRequest var show = queryRepository.findById(request.showId()); var hallId = request.hallId(); - checkHallExist(hallId); + hallValidator.checkHallExist(hallId); checkConflictSchedule(hallId, request); var command = new ShowScheduleCreateCommand(request.showId(), request.startAt(), request.endAt()); @@ -60,13 +58,5 @@ private void checkConflictSchedule(Long hallId, ShowScheduleRegisterRequest requ throw new ShowException("해당 회차는 이미 공연 스케줄이 등록되어 있습니다."); } } - - private void checkHallExist(Long hallId) { - var event = new HallExistCheckEvent(hallId); - applicationEventPublisher.publishEvent(event); - if (!event.isExist()) { - throw new HallException("NOT_FOUND", "해당 공연장을 찾을 수 없습니다."); - } - } } diff --git a/application/src/main/java/org/mandarin/booking/app/venue/HallService.java b/application/src/main/java/org/mandarin/booking/app/venue/HallService.java index b6c1b81..113519d 100644 --- a/application/src/main/java/org/mandarin/booking/app/venue/HallService.java +++ b/application/src/main/java/org/mandarin/booking/app/venue/HallService.java @@ -1,18 +1,18 @@ package org.mandarin.booking.app.venue; import lombok.RequiredArgsConstructor; -import org.springframework.context.event.EventListener; +import org.mandarin.booking.domain.venue.HallException; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor -public class HallService { +class HallService implements HallValidator { private final HallQueryRepository queryRepository; - @EventListener(HallExistCheckEvent.class) - public void hallExistCheckHandler(HallExistCheckEvent event) { - if (queryRepository.existsById(event.getHallId())) { - event.exist(); + @Override + public void checkHallExist(Long hallId) { + if (!queryRepository.existsById(hallId)) { + throw new HallException("NOT_FOUND", "해당 공연장을 찾을 수 없습니다."); } } } diff --git a/application/src/main/java/org/mandarin/booking/app/venue/HallValidator.java b/application/src/main/java/org/mandarin/booking/app/venue/HallValidator.java new file mode 100644 index 0000000..001bba6 --- /dev/null +++ b/application/src/main/java/org/mandarin/booking/app/venue/HallValidator.java @@ -0,0 +1,5 @@ +package org.mandarin.booking.app.venue; + +public interface HallValidator { + void checkHallExist(Long hallId); +} From 31221d48a2b74660168927164288f64d6421cefa Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 12 Sep 2025 01:03:22 +0900 Subject: [PATCH 23/36] remove unused event dto --- .../booking/app/venue/HallExistCheckEvent.java | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 application/src/main/java/org/mandarin/booking/app/venue/HallExistCheckEvent.java diff --git a/application/src/main/java/org/mandarin/booking/app/venue/HallExistCheckEvent.java b/application/src/main/java/org/mandarin/booking/app/venue/HallExistCheckEvent.java deleted file mode 100644 index de0ab51..0000000 --- a/application/src/main/java/org/mandarin/booking/app/venue/HallExistCheckEvent.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.mandarin.booking.app.venue; - -import lombok.Getter; - -@Getter -public class HallExistCheckEvent { - private final Long hallId; - private boolean exist = false; - - public HallExistCheckEvent(Long hallId) { - this.hallId = hallId; - } - - public void exist() { - this.exist = true; - } -} From 5cbb2549e8509931f741e3e91a52887d876b8789 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 12 Sep 2025 01:04:38 +0900 Subject: [PATCH 24/36] add Spring Modulith dependencies and named interfaces for member, show, and venue packages --- application/build.gradle | 13 ++++++++++++- .../mandarin/booking/app/member/package-info.java | 4 ++++ .../org/mandarin/booking/app/show/package-info.java | 4 ++++ .../mandarin/booking/app/venue/package-info.java | 4 ++++ domain/build.gradle | 10 ++++++++-- 5 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 application/src/main/java/org/mandarin/booking/app/member/package-info.java create mode 100644 application/src/main/java/org/mandarin/booking/app/show/package-info.java create mode 100644 application/src/main/java/org/mandarin/booking/app/venue/package-info.java diff --git a/application/build.gradle b/application/build.gradle index bc1e625..f302c64 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -12,6 +12,12 @@ ext { set('snippetsDir', file('build/generated-snippets')) } +dependencyManagement { + imports { + mavenBom 'org.springframework.modulith:spring-modulith-bom:1.4.3' + } +} + dependencies { api(project(':domain')) api(project(':internal')) @@ -29,6 +35,12 @@ dependencies { testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' byteBuddyAgent 'net.bytebuddy:byte-buddy-agent:1.17.6' + // ---- Spring Modulith ---- + runtimeOnly 'org.springframework.modulith:spring-modulith-runtime' + implementation "org.springframework.modulith:spring-modulith-starter-core" + implementation 'org.springframework.modulith:spring-modulith-starter-jpa' + implementation 'org.springframework.modulith:spring-modulith-events-api:1.4.3' + testImplementation 'org.springframework.modulith:spring-modulith-starter-test' // ---- API Docs (REST Docs + Rest Assured) ---- testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' @@ -55,6 +67,5 @@ asciidoctor { tasks.named('test') { jvmArgs "-javaagent:${configurations.byteBuddyAgent.singleFile}" - jvmArgs "-javaagent:${configurations.testRuntimeClasspath.find { it.name.contains('mockito-core') }}", '-Xshare:off' outputs.dir snippetsDir } diff --git a/application/src/main/java/org/mandarin/booking/app/member/package-info.java b/application/src/main/java/org/mandarin/booking/app/member/package-info.java new file mode 100644 index 0000000..e8b3356 --- /dev/null +++ b/application/src/main/java/org/mandarin/booking/app/member/package-info.java @@ -0,0 +1,4 @@ +@NamedInterface("member") +package org.mandarin.booking.app.member; + +import org.springframework.modulith.NamedInterface; diff --git a/application/src/main/java/org/mandarin/booking/app/show/package-info.java b/application/src/main/java/org/mandarin/booking/app/show/package-info.java new file mode 100644 index 0000000..19ade8f --- /dev/null +++ b/application/src/main/java/org/mandarin/booking/app/show/package-info.java @@ -0,0 +1,4 @@ +@NamedInterface("show") +package org.mandarin.booking.app.show; + +import org.springframework.modulith.NamedInterface; diff --git a/application/src/main/java/org/mandarin/booking/app/venue/package-info.java b/application/src/main/java/org/mandarin/booking/app/venue/package-info.java new file mode 100644 index 0000000..43a5843 --- /dev/null +++ b/application/src/main/java/org/mandarin/booking/app/venue/package-info.java @@ -0,0 +1,4 @@ +@NamedInterface("venue") +package org.mandarin.booking.app.venue; + +import org.springframework.modulith.NamedInterface; diff --git a/domain/build.gradle b/domain/build.gradle index dba45aa..6ee338c 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -12,8 +12,14 @@ dependencies { api 'org.springframework.boot:spring-boot-starter-validation' // ---- Querydsl ---- - api 'com.querydsl:querydsl-jpa:5.1.0:jakarta' - annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' + api 'com.querydsl:querydsl-jpa:5.1.1:jakarta' + annotationProcessor 'com.querydsl:querydsl-apt:5.1.1:jakarta' annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0' annotationProcessor 'jakarta.annotation:jakarta.annotation-api:2.1.1' } + +dependencyManagement { + imports { + mavenBom 'org.springframework.modulith:spring-modulith-bom:1.4.3' + } +} From 104ac543a4e5c569ecd34908121a005596c07656 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 12 Sep 2025 01:23:24 +0900 Subject: [PATCH 25/36] move test utility classes to utils package for better organization --- .../org/mandarin/booking/LoggingAspectTest.java | 1 + .../adapter/security/AuthIntegrationTest.java | 6 +++--- .../security/CustomAuthenticationProviderTest.java | 2 +- .../mandarin/booking/{ => utils}/DocsUtils.java | 2 +- .../booking/{ => utils}/IntegrationTest.java | 3 ++- .../booking/{ => utils}/IntegrationTestUtils.java | 12 +++++++----- .../{ => utils}/IntegrationTestUtilsSpecs.java | 2 +- .../mandarin/booking/{ => utils}/JwtTestUtils.java | 2 +- .../booking/{fixture => utils}/MemberFixture.java | 2 +- .../mandarin/booking/{ => utils}/NoRestDocs.java | 2 +- .../mandarin/booking/{ => utils}/TestConfig.java | 2 +- .../mandarin/booking/{ => utils}/TestResult.java | 2 +- .../booking/webapi/auth/login/POST_specs.java | 14 +++++++------- .../booking/webapi/auth/reissue/POST_specs.java | 14 +++++++------- .../mandarin/booking/webapi/member/POST_specs.java | 12 ++++++------ .../booking/webapi/not_found/GET_specs.java | 6 +++--- .../mandarin/booking/webapi/show/POST_specs.java | 4 ++-- .../booking/webapi/show/schedule/POST_specs.java | 4 ++-- 18 files changed, 48 insertions(+), 44 deletions(-) rename application/src/test/java/org/mandarin/booking/{ => utils}/DocsUtils.java (99%) rename application/src/test/java/org/mandarin/booking/{ => utils}/IntegrationTest.java (87%) rename application/src/test/java/org/mandarin/booking/{ => utils}/IntegrationTestUtils.java (91%) rename application/src/test/java/org/mandarin/booking/{ => utils}/IntegrationTestUtilsSpecs.java (98%) rename application/src/test/java/org/mandarin/booking/{ => utils}/JwtTestUtils.java (97%) rename application/src/test/java/org/mandarin/booking/{fixture => utils}/MemberFixture.java (95%) rename application/src/test/java/org/mandarin/booking/{ => utils}/NoRestDocs.java (81%) rename application/src/test/java/org/mandarin/booking/{ => utils}/TestConfig.java (97%) rename application/src/test/java/org/mandarin/booking/{ => utils}/TestResult.java (99%) diff --git a/application/src/test/java/org/mandarin/booking/LoggingAspectTest.java b/application/src/test/java/org/mandarin/booking/LoggingAspectTest.java index dd44be8..470dfd4 100644 --- a/application/src/test/java/org/mandarin/booking/LoggingAspectTest.java +++ b/application/src/test/java/org/mandarin/booking/LoggingAspectTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mandarin.booking.utils.IntegrationTest; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; diff --git a/application/src/test/java/org/mandarin/booking/adapter/security/AuthIntegrationTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/AuthIntegrationTest.java index 60e431f..4503884 100644 --- a/application/src/test/java/org/mandarin/booking/adapter/security/AuthIntegrationTest.java +++ b/application/src/test/java/org/mandarin/booking/adapter/security/AuthIntegrationTest.java @@ -7,13 +7,13 @@ import java.util.List; import org.junit.jupiter.api.Test; -import org.mandarin.booking.IntegrationTest; -import org.mandarin.booking.IntegrationTestUtils; -import org.mandarin.booking.NoRestDocs; import org.mandarin.booking.adapter.JwtFilter; import org.mandarin.booking.adapter.TokenUtils; import org.mandarin.booking.adapter.security.AuthIntegrationTest.TestAuthController; import org.mandarin.booking.adapter.security.AuthIntegrationTest.TestAuthController.TestSecurityConfig; +import org.mandarin.booking.utils.IntegrationTest; +import org.mandarin.booking.utils.IntegrationTestUtils; +import org.mandarin.booking.utils.NoRestDocs; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; diff --git a/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java index 61dddc8..de39963 100644 --- a/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java +++ b/application/src/test/java/org/mandarin/booking/adapter/security/CustomAuthenticationProviderTest.java @@ -5,8 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -import org.mandarin.booking.IntegrationTest; import org.mandarin.booking.adapter.CustomMemberAuthenticationToken; +import org.mandarin.booking.utils.IntegrationTest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; diff --git a/application/src/test/java/org/mandarin/booking/DocsUtils.java b/application/src/test/java/org/mandarin/booking/utils/DocsUtils.java similarity index 99% rename from application/src/test/java/org/mandarin/booking/DocsUtils.java rename to application/src/test/java/org/mandarin/booking/utils/DocsUtils.java index c65ccbb..fcd348a 100644 --- a/application/src/test/java/org/mandarin/booking/DocsUtils.java +++ b/application/src/test/java/org/mandarin/booking/utils/DocsUtils.java @@ -1,4 +1,4 @@ -package org.mandarin.booking; +package org.mandarin.booking.utils; import static io.restassured.RestAssured.given; import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; diff --git a/application/src/test/java/org/mandarin/booking/IntegrationTest.java b/application/src/test/java/org/mandarin/booking/utils/IntegrationTest.java similarity index 87% rename from application/src/test/java/org/mandarin/booking/IntegrationTest.java rename to application/src/test/java/org/mandarin/booking/utils/IntegrationTest.java index 0343906..014ca21 100644 --- a/application/src/test/java/org/mandarin/booking/IntegrationTest.java +++ b/application/src/test/java/org/mandarin/booking/utils/IntegrationTest.java @@ -1,9 +1,10 @@ -package org.mandarin.booking; +package org.mandarin.booking.utils; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.mandarin.booking.BookingApplication; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; diff --git a/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java b/application/src/test/java/org/mandarin/booking/utils/IntegrationTestUtils.java similarity index 91% rename from application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java rename to application/src/test/java/org/mandarin/booking/utils/IntegrationTestUtils.java index e7699d6..859b7f6 100644 --- a/application/src/test/java/org/mandarin/booking/IntegrationTestUtils.java +++ b/application/src/test/java/org/mandarin/booking/utils/IntegrationTestUtils.java @@ -1,9 +1,9 @@ -package org.mandarin.booking; +package org.mandarin.booking.utils; -import static org.mandarin.booking.fixture.MemberFixture.EmailGenerator.generateEmail; -import static org.mandarin.booking.fixture.MemberFixture.NicknameGenerator.generateNickName; -import static org.mandarin.booking.fixture.MemberFixture.PasswordGenerator.generatePassword; -import static org.mandarin.booking.fixture.MemberFixture.UserIdGenerator.generateUserId; +import static org.mandarin.booking.utils.MemberFixture.EmailGenerator.generateEmail; +import static org.mandarin.booking.utils.MemberFixture.NicknameGenerator.generateNickName; +import static org.mandarin.booking.utils.MemberFixture.PasswordGenerator.generatePassword; +import static org.mandarin.booking.utils.MemberFixture.UserIdGenerator.generateUserId; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -11,6 +11,8 @@ import java.util.Collection; import java.util.List; import java.util.UUID; +import org.mandarin.booking.MemberAuthority; +import org.mandarin.booking.TokenHolder; import org.mandarin.booking.adapter.TokenUtils; import org.mandarin.booking.app.member.MemberCommandRepository; import org.mandarin.booking.app.show.ShowCommandRepository; diff --git a/application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java b/application/src/test/java/org/mandarin/booking/utils/IntegrationTestUtilsSpecs.java similarity index 98% rename from application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java rename to application/src/test/java/org/mandarin/booking/utils/IntegrationTestUtilsSpecs.java index 36da8be..5eff281 100644 --- a/application/src/test/java/org/mandarin/booking/IntegrationTestUtilsSpecs.java +++ b/application/src/test/java/org/mandarin/booking/utils/IntegrationTestUtilsSpecs.java @@ -1,4 +1,4 @@ -package org.mandarin.booking; +package org.mandarin.booking.utils; import static org.assertj.core.api.Assertions.assertThat; import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; diff --git a/application/src/test/java/org/mandarin/booking/JwtTestUtils.java b/application/src/test/java/org/mandarin/booking/utils/JwtTestUtils.java similarity index 97% rename from application/src/test/java/org/mandarin/booking/JwtTestUtils.java rename to application/src/test/java/org/mandarin/booking/utils/JwtTestUtils.java index 4d3308c..9373f5d 100644 --- a/application/src/test/java/org/mandarin/booking/JwtTestUtils.java +++ b/application/src/test/java/org/mandarin/booking/utils/JwtTestUtils.java @@ -1,4 +1,4 @@ -package org.mandarin.booking; +package org.mandarin.booking.utils; import static java.util.Base64.getUrlDecoder; import static org.assertj.core.api.Assertions.assertThat; diff --git a/application/src/test/java/org/mandarin/booking/fixture/MemberFixture.java b/application/src/test/java/org/mandarin/booking/utils/MemberFixture.java similarity index 95% rename from application/src/test/java/org/mandarin/booking/fixture/MemberFixture.java rename to application/src/test/java/org/mandarin/booking/utils/MemberFixture.java index deb6724..bf48ac2 100644 --- a/application/src/test/java/org/mandarin/booking/fixture/MemberFixture.java +++ b/application/src/test/java/org/mandarin/booking/utils/MemberFixture.java @@ -1,4 +1,4 @@ -package org.mandarin.booking.fixture; +package org.mandarin.booking.utils; import java.util.UUID; diff --git a/application/src/test/java/org/mandarin/booking/NoRestDocs.java b/application/src/test/java/org/mandarin/booking/utils/NoRestDocs.java similarity index 81% rename from application/src/test/java/org/mandarin/booking/NoRestDocs.java rename to application/src/test/java/org/mandarin/booking/utils/NoRestDocs.java index 17a56cb..e9a3f25 100644 --- a/application/src/test/java/org/mandarin/booking/NoRestDocs.java +++ b/application/src/test/java/org/mandarin/booking/utils/NoRestDocs.java @@ -1,4 +1,4 @@ -package org.mandarin.booking; +package org.mandarin.booking.utils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/application/src/test/java/org/mandarin/booking/TestConfig.java b/application/src/test/java/org/mandarin/booking/utils/TestConfig.java similarity index 97% rename from application/src/test/java/org/mandarin/booking/TestConfig.java rename to application/src/test/java/org/mandarin/booking/utils/TestConfig.java index 312fe18..0cbd7ad 100644 --- a/application/src/test/java/org/mandarin/booking/TestConfig.java +++ b/application/src/test/java/org/mandarin/booking/utils/TestConfig.java @@ -1,4 +1,4 @@ -package org.mandarin.booking; +package org.mandarin.booking.utils; import com.fasterxml.jackson.databind.ObjectMapper; import org.mandarin.booking.adapter.TokenUtils; diff --git a/application/src/test/java/org/mandarin/booking/TestResult.java b/application/src/test/java/org/mandarin/booking/utils/TestResult.java similarity index 99% rename from application/src/test/java/org/mandarin/booking/TestResult.java rename to application/src/test/java/org/mandarin/booking/utils/TestResult.java index cafe868..d4068f8 100644 --- a/application/src/test/java/org/mandarin/booking/TestResult.java +++ b/application/src/test/java/org/mandarin/booking/utils/TestResult.java @@ -1,4 +1,4 @@ -package org.mandarin.booking; +package org.mandarin.booking.utils; import static org.assertj.core.api.Assertions.fail; diff --git a/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java index ad67635..8fe2193 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/auth/login/POST_specs.java @@ -1,14 +1,14 @@ package org.mandarin.booking.webapi.auth.login; import static org.assertj.core.api.Assertions.assertThat; -import static org.mandarin.booking.JwtTestUtils.assertJwtFormat; -import static org.mandarin.booking.JwtTestUtils.getExpiration; -import static org.mandarin.booking.JwtTestUtils.getTokenClaims; import static org.mandarin.booking.adapter.ApiStatus.BAD_REQUEST; import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; import static org.mandarin.booking.adapter.ApiStatus.UNAUTHORIZED; -import static org.mandarin.booking.fixture.MemberFixture.PasswordGenerator.generatePassword; -import static org.mandarin.booking.fixture.MemberFixture.UserIdGenerator.generateUserId; +import static org.mandarin.booking.utils.JwtTestUtils.assertJwtFormat; +import static org.mandarin.booking.utils.JwtTestUtils.getExpiration; +import static org.mandarin.booking.utils.JwtTestUtils.getTokenClaims; +import static org.mandarin.booking.utils.MemberFixture.PasswordGenerator.generatePassword; +import static org.mandarin.booking.utils.MemberFixture.UserIdGenerator.generateUserId; import io.jsonwebtoken.security.Keys; import java.util.Date; @@ -17,11 +17,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.mandarin.booking.IntegrationTest; -import org.mandarin.booking.IntegrationTestUtils; import org.mandarin.booking.TokenHolder; import org.mandarin.booking.app.member.MemberQueryRepository; import org.mandarin.booking.domain.member.AuthRequest; +import org.mandarin.booking.utils.IntegrationTest; +import org.mandarin.booking.utils.IntegrationTestUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; diff --git a/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java index cab7cfa..b883a18 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/auth/reissue/POST_specs.java @@ -1,15 +1,15 @@ package org.mandarin.booking.webapi.auth.reissue; import static org.assertj.core.api.Assertions.assertThat; -import static org.mandarin.booking.JwtTestUtils.assertJwtFormat; -import static org.mandarin.booking.JwtTestUtils.getExpiration; import static org.mandarin.booking.MemberAuthority.USER; import static org.mandarin.booking.adapter.ApiStatus.BAD_REQUEST; import static org.mandarin.booking.adapter.ApiStatus.SUCCESS; import static org.mandarin.booking.adapter.ApiStatus.UNAUTHORIZED; -import static org.mandarin.booking.fixture.MemberFixture.NicknameGenerator.generateNickName; -import static org.mandarin.booking.fixture.MemberFixture.PasswordGenerator.generatePassword; -import static org.mandarin.booking.fixture.MemberFixture.UserIdGenerator.generateUserId; +import static org.mandarin.booking.utils.JwtTestUtils.assertJwtFormat; +import static org.mandarin.booking.utils.JwtTestUtils.getExpiration; +import static org.mandarin.booking.utils.MemberFixture.NicknameGenerator.generateNickName; +import static org.mandarin.booking.utils.MemberFixture.PasswordGenerator.generatePassword; +import static org.mandarin.booking.utils.MemberFixture.UserIdGenerator.generateUserId; import io.jsonwebtoken.security.Keys; import java.util.Date; @@ -18,11 +18,11 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.mandarin.booking.IntegrationTest; -import org.mandarin.booking.IntegrationTestUtils; import org.mandarin.booking.TokenHolder; import org.mandarin.booking.adapter.TokenUtils; import org.mandarin.booking.domain.member.ReissueRequest; +import org.mandarin.booking.utils.IntegrationTest; +import org.mandarin.booking.utils.IntegrationTestUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.test.context.TestPropertySource; diff --git a/application/src/test/java/org/mandarin/booking/webapi/member/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/member/POST_specs.java index 27307f8..3ba2b08 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/member/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/member/POST_specs.java @@ -1,21 +1,21 @@ package org.mandarin.booking.webapi.member; import static org.assertj.core.api.Assertions.assertThat; -import static org.mandarin.booking.fixture.MemberFixture.EmailGenerator.generateEmail; -import static org.mandarin.booking.fixture.MemberFixture.UserIdGenerator.generateUserId; +import static org.mandarin.booking.utils.MemberFixture.EmailGenerator.generateEmail; +import static org.mandarin.booking.utils.MemberFixture.UserIdGenerator.generateUserId; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.mandarin.booking.IntegrationTest; -import org.mandarin.booking.IntegrationTestUtils; import org.mandarin.booking.app.member.MemberQueryRepository; import org.mandarin.booking.domain.member.MemberRegisterRequest; import org.mandarin.booking.domain.member.MemberRegisterResponse; import org.mandarin.booking.domain.member.SecurePasswordEncoder; -import org.mandarin.booking.fixture.MemberFixture.NicknameGenerator; -import org.mandarin.booking.fixture.MemberFixture.PasswordGenerator; +import org.mandarin.booking.utils.IntegrationTest; +import org.mandarin.booking.utils.IntegrationTestUtils; +import org.mandarin.booking.utils.MemberFixture.NicknameGenerator; +import org.mandarin.booking.utils.MemberFixture.PasswordGenerator; import org.springframework.beans.factory.annotation.Autowired; @IntegrationTest diff --git a/application/src/test/java/org/mandarin/booking/webapi/not_found/GET_specs.java b/application/src/test/java/org/mandarin/booking/webapi/not_found/GET_specs.java index 822fb91..45f344d 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/not_found/GET_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/not_found/GET_specs.java @@ -4,9 +4,9 @@ import static org.mandarin.booking.adapter.ApiStatus.NOT_FOUND; import org.junit.jupiter.api.Test; -import org.mandarin.booking.IntegrationTest; -import org.mandarin.booking.IntegrationTestUtils; -import org.mandarin.booking.NoRestDocs; +import org.mandarin.booking.utils.IntegrationTest; +import org.mandarin.booking.utils.IntegrationTestUtils; +import org.mandarin.booking.utils.NoRestDocs; import org.springframework.beans.factory.annotation.Autowired; @IntegrationTest diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java index 3732ee5..5159b3b 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/POST_specs.java @@ -14,10 +14,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.mandarin.booking.IntegrationTest; -import org.mandarin.booking.IntegrationTestUtils; import org.mandarin.booking.domain.show.ShowRegisterRequest; import org.mandarin.booking.domain.show.ShowRegisterResponse; +import org.mandarin.booking.utils.IntegrationTest; +import org.mandarin.booking.utils.IntegrationTestUtils; import org.springframework.beans.factory.annotation.Autowired; @IntegrationTest diff --git a/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java b/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java index 8741754..580f485 100644 --- a/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java +++ b/application/src/test/java/org/mandarin/booking/webapi/show/schedule/POST_specs.java @@ -15,11 +15,11 @@ import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mandarin.booking.IntegrationTest; -import org.mandarin.booking.IntegrationTestUtils; import org.mandarin.booking.domain.show.Show; import org.mandarin.booking.domain.show.ShowScheduleRegisterRequest; import org.mandarin.booking.domain.show.ShowScheduleRegisterResponse; +import org.mandarin.booking.utils.IntegrationTest; +import org.mandarin.booking.utils.IntegrationTestUtils; import org.springframework.beans.factory.annotation.Autowired; @IntegrationTest From 20b9bf6a481b313f04d8539015b39ccc0f7fe47a Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 12 Sep 2025 01:27:52 +0900 Subject: [PATCH 26/36] downgrade Querydsl dependencies to version 5.1.0 for consistency --- domain/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domain/build.gradle b/domain/build.gradle index 6ee338c..086120f 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -12,8 +12,8 @@ dependencies { api 'org.springframework.boot:spring-boot-starter-validation' // ---- Querydsl ---- - api 'com.querydsl:querydsl-jpa:5.1.1:jakarta' - annotationProcessor 'com.querydsl:querydsl-apt:5.1.1:jakarta' + api 'com.querydsl:querydsl-jpa:5.1.0:jakarta' + annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0' annotationProcessor 'jakarta.annotation:jakarta.annotation-api:2.1.1' } From 7a3537aa9a758bce1da63a99e933599d347111bc Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Fri, 12 Sep 2025 01:47:36 +0900 Subject: [PATCH 27/36] disable bootJar task and enable jar task in build configuration --- internal/build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/build.gradle b/internal/build.gradle index fe36461..4f2d129 100644 --- a/internal/build.gradle +++ b/internal/build.gradle @@ -1,3 +1,11 @@ +bootJar { + enabled = false +} + +jar { + enabled = true +} + dependencies { api project(':common') From 3ee350f91d5bf6530e71e669b6fe16547c335209 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Sat, 13 Sep 2025 23:50:01 +0900 Subject: [PATCH 28/36] update build configuration and enhance documentation utilities --- application/.gitignore | 3 +- application/build.gradle | 123 +- application/docs/index.adoc | 28 - application/docs/index.html | 2567 ----------------- .../org/mandarin/booking/utils/DocsUtils.java | 102 +- 5 files changed, 185 insertions(+), 2638 deletions(-) delete mode 100644 application/docs/index.adoc delete mode 100644 application/docs/index.html diff --git a/application/.gitignore b/application/.gitignore index 2d1477b..152badd 100644 --- a/application/.gitignore +++ b/application/.gitignore @@ -1 +1,2 @@ -./docs +src/main/resources/static/docs/ +spy.log diff --git a/application/build.gradle b/application/build.gradle index f302c64..47a93ac 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -8,10 +8,6 @@ configurations { asciidoctorExt } -ext { - set('snippetsDir', file('build/generated-snippets')) -} - dependencyManagement { imports { mavenBom 'org.springframework.modulith:spring-modulith-bom:1.4.3' @@ -23,49 +19,120 @@ dependencies { api(project(':internal')) api(project(':external')) - // ---- Spring Boot Core ---- implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-actuator' - // ---- Dev Only ---- developmentOnly 'org.springframework.boot:spring-boot-docker-compose' developmentOnly 'org.springframework.boot:spring-boot-devtools' - // ---- Testing ---- testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' byteBuddyAgent 'net.bytebuddy:byte-buddy-agent:1.17.6' - // ---- Spring Modulith ---- runtimeOnly 'org.springframework.modulith:spring-modulith-runtime' - implementation "org.springframework.modulith:spring-modulith-starter-core" + implementation 'org.springframework.modulith:spring-modulith-starter-core' implementation 'org.springframework.modulith:spring-modulith-starter-jpa' implementation 'org.springframework.modulith:spring-modulith-events-api:1.4.3' testImplementation 'org.springframework.modulith:spring-modulith-starter-test' - // ---- API Docs (REST Docs + Rest Assured) ---- + testImplementation 'io.rest-assured:rest-assured' testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' +} + +/** + * Provider 기반 최신 문법: buildDir 대신 layout.buildDirectory 사용*/ +def snippetsDir = layout.buildDirectory.dir("generated-snippets") +def generatedIndexDir = layout.buildDirectory.dir("tmp/asciidoc") +def asciidocOutputDir = layout.buildDirectory.dir("asciidoc") - asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:3.1.0' +tasks.named('test') { + jvmArgs "-javaagent:${configurations.byteBuddyAgent.singleFile}" + outputs.dir(snippetsDir) } -asciidoctor { - group = 'documentation' - description = 'Converts AsciiDoc files to HTML, including REST Docs snippets.' - dependsOn test - baseDirFollowsSourceDir() - sourceDir = file('src/docs') - sources { - include '**/*.adoc' - } - inputs.dir snippetsDir - attributes 'snippets': snippetsDir - resources { - from(snippetsDir) { into 'snippets' } +tasks.register('generateIndexAdoc') { + dependsOn tasks.named('test') + outputs.file(generatedIndexDir.map { it.file("index.adoc") }) + doLast { + def genDir = generatedIndexDir.get().asFile + def snip = snippetsDir.get().asFile + + genDir.mkdirs() + + def ops = snip.listFiles()?.findAll { it.isDirectory() } ?: [] + + def groupKey = { String name -> name.contains(' - ') ? name.substring(0, name.indexOf(' - ')) : name } + + def grouped = ops.groupBy { dir -> groupKey(dir.name) }.sort { a, b -> a.key <=> b.key } + + def includes = ['curl-request.adoc', + 'http-request.adoc', + 'http-response.adoc', + 'path-parameters.adoc', + 'query-parameters.adoc', + 'request-parameters.adoc', + 'request-fields.adoc', + 'response-fields.adoc', + 'links.adoc'] + + def sb = new StringBuilder() + sb << "= API 문서\n:toc: left\n:sectnums:\n\n" + + grouped.each { grp, dirs -> + sb << "== ${grp}\n\n" + + dirs.findAll { it.name == grp }.each { dir -> + includes.each { inc -> + def f = new File(dir, inc) + if (f.exists()) { + def caption = inc.replace('.adoc', '').replace('-', ' ') + sb << ".${caption}\ninclude::{snippets}/" << dir.name << "/" << inc << "[]\n\n" + } + } + } + + dirs.findAll { it.name != grp } + .sort { it.name } + .each { dir -> + def methodTitle = dir.name.substring(grp.length() + " - ".length()) + sb << "=== ${methodTitle}\n\n" + includes.each { inc -> + def f = new File(dir, inc) + if (f.exists()) { + def caption = inc.replace('.adoc', '').replace('-', ' ') + sb << ".${caption}\ninclude::{snippets}/" << dir.name << "/" << inc << "[]\n\n" + } + } + } + } + + new File(genDir, "index.adoc").text = sb.toString() } - outputDir = layout.buildDirectory.dir("docs/asciidoc").get().asFile } -tasks.named('test') { - jvmArgs "-javaagent:${configurations.byteBuddyAgent.singleFile}" - outputs.dir snippetsDir +tasks.named('asciidoctor') { + dependsOn tasks.named('generateIndexAdoc') + inputs.dir(snippetsDir) + configurations = [project.configurations.asciidoctorExt] + sourceDir generatedIndexDir.get().asFile + sources { include 'index.adoc' } + baseDirFollowsSourceFile() + outputDir asciidocOutputDir.get().asFile + attributes 'snippets': snippetsDir.get().asFile.absolutePath +} + +tasks.named('build') { + dependsOn tasks.named('asciidoctor') +} + +tasks.named('bootJar') { + dependsOn tasks.named('asciidoctor') + from(asciidocOutputDir) { into 'static/docs' } +} + +tasks.named('clean') { + doFirst { + delete asciidocOutputDir.get().asFile + delete generatedIndexDir.get().asFile + } } diff --git a/application/docs/index.adoc b/application/docs/index.adoc deleted file mode 100644 index 4312d24..0000000 --- a/application/docs/index.adoc +++ /dev/null @@ -1,28 +0,0 @@ -= Booking API Documentation -:toc: left -:toclevels: 2 -:sectanchors: -:sectnums: -:source-highlighter: highlightjs - -This is the generated API documentation using Spring REST Docs and Asciidoctor. - -== Authentication - -include::{snippets}/post-api-auth-login/http-request.adoc[] -include::{snippets}/post-api-auth-login/http-response.adoc[] - -== Reissue Token - -include::{snippets}/post-api-auth-reissue/http-request.adoc[] -include::{snippets}/post-api-auth-reissue/http-response.adoc[] - -== Member Registration - -include::{snippets}/post-api-member/http-request.adoc[] -include::{snippets}/post-api-member/http-response.adoc[] - -== Show Registration - -include::{snippets}/post-api-show/http-request.adoc[] -include::{snippets}/post-api-show/http-response.adoc[] diff --git a/application/docs/index.html b/application/docs/index.html deleted file mode 100644 index 4823abf..0000000 --- a/application/docs/index.html +++ /dev/null @@ -1,2567 +0,0 @@ - - - - - - - - Booking API Documentation - - - - - - - -
-
-
-
-

This is the generated API documentation using Spring REST Docs and Asciidoctor.

-
-
-
-
-

1. Authentication

-
-
-
-
POST /api/auth/login HTTP/1.1
-Accept: application/json, application/javascript, text/javascript, text/json
-Content-Type: application/json
-Host: localhost:60312
-Content-Length: 110
-
-{
-  "userId" : "922a834b-52ca-4e10-9ca8-9b851d8f558a",
-  "password" : "d92f16da-ad24-4ca2-97bf-094c460a1794"
-}
-
-
-
-
-
HTTP/1.1 200 OK
-X-Content-Type-Options: nosniff
-X-XSS-Protection: 0
-Cache-Control: no-cache, no-store, max-age=0, must-revalidate
-Pragma: no-cache
-Expires: 0
-X-Frame-Options: DENY
-Content-Type: application/json
-Transfer-Encoding: chunked
-Date: Fri, 05 Sep 2025 07:14:22 GMT
-Keep-Alive: timeout=60
-Connection: keep-alive
-Content-Length: 131
-
-{
-  "status" : "UNAUTHORIZED",
-  "message" : "잘못된 userID 또는 비밀번호",
-  "timestamp" : "2025-09-05T16:14:22.994152"
-}
-
-
-
-
-
-

2. Reissue Token

-
-
-
-
POST /api/auth/reissue HTTP/1.1
-Accept: application/json, application/javascript, text/javascript, text/json
-Content-Type: application/json
-Host: localhost:60365
-Content-Length: 413
-
-{
-  "refreshToken" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJiNzJhNDU4Ny1kNTVmLTQ1ZTEtOThjOC1kOWRjNTlmZDI0NTciLCJyb2xlcyI6IlJPTEVfVVNFUiIsIm5pY2tOYW1lIjoiMjhmOGQ5MmQtMDdkZi00YWVkLWFkZTktMTc0ZWRhZjQ0ODJiIiwidXNlcklkIjoiYjcyYTQ1ODctZDU1Zi00NWUxLTk4YzgtZDlkYzU5ZmQyNDU3IiwiaWF0IjoxNzU3MDU2NDYzLCJleHAiOjE3NTcwNTY0NjN9.DLMSqhiLIIB_IJrcO-akHmy_siNHtPjWCvexuo_-oOVrpsNRNjBR3VRlFl0hcqpockupX8q1Tm34zpIyV-T32g"
-}
-
-
-
-
-
HTTP/1.1 200 OK
-X-Content-Type-Options: nosniff
-X-XSS-Protection: 0
-Cache-Control: no-cache, no-store, max-age=0, must-revalidate
-Pragma: no-cache
-Expires: 0
-X-Frame-Options: DENY
-Content-Type: application/json
-Transfer-Encoding: chunked
-Date: Fri, 05 Sep 2025 07:14:23 GMT
-Keep-Alive: timeout=60
-Connection: keep-alive
-Content-Length: 131
-
-{
-  "status" : "UNAUTHORIZED",
-  "message" : "토큰 검증에 실패했습니다.",
-  "timestamp" : "2025-09-05T16:14:23.944676"
-}
-
-
-
-
-
-

3. Member Registration

-
-
-
-
POST /api/member HTTP/1.1
-Accept: application/json, application/javascript, text/javascript, text/json
-Content-Type: application/json
-Host: localhost:60312
-Content-Length: 193
-
-{
-  "nickName" : "b214e075-e391-4f44-8f42-ea7e16dabcf4",
-  "userId" : "id",
-  "password" : "ac334c81-c0bc-4812-bc87-13660da6e095",
-  "email" : "9425394d-4517-48df-803e-283cf71afda9@gmail.com"
-}
-
-
-
-
-
HTTP/1.1 200 OK
-X-Content-Type-Options: nosniff
-X-XSS-Protection: 0
-Cache-Control: no-cache, no-store, max-age=0, must-revalidate
-Pragma: no-cache
-Expires: 0
-X-Frame-Options: DENY
-Content-Type: application/json
-Transfer-Encoding: chunked
-Date: Fri, 05 Sep 2025 07:14:24 GMT
-Keep-Alive: timeout=60
-Connection: keep-alive
-Content-Length: 143
-
-{
-  "status" : "INTERNAL_SERVER_ERROR",
-  "message" : "이미 존재하는 회원입니다: id",
-  "timestamp" : "2025-09-05T16:14:24.646164"
-}
-
-
-
-
-
-

4. Show Registration

-
-
-
-
POST /api/show HTTP/1.1
-Accept: application/json, application/javascript, text/javascript, text/json
-Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIwYjhlODRkNC0wNmRjLTRjNDYtYmU3Yy1jZGJmNzkxZGM3NmEiLCJyb2xlcyI6IlJPTEVfQURNSU4iLCJuaWNrTmFtZSI6IjhlYjkwNDhlLWNlNjAtNGJmMy1iNDlkLWJjZmFkZjUyZWI5NyIsInVzZXJJZCI6IjBiOGU4NGQ0LTA2ZGMtNGM0Ni1iZTdjLWNkYmY3OTFkYzc2YSIsImlhdCI6MTc1NzA1NjQ2NSwiZXhwIjoxNzU3MDU3MDY1fQ.qQGd6qo4xb3mHEl95T0j5KJ2LBC24kmZhIxz1ve70MwMQJCtqlYi50eoApQqxgMA_CfFj4NKvl3fl2M4vh8v0g
-Content-Type: application/json
-Host: localhost:60312
-Content-Length: 240
-
-{
-  "title" : "공연 제목",
-  "type" : "MUSICAL",
-  "rating" : "AGE12",
-  "synopsis" : "공연 줄거리",
-  "posterUrl" : "https://example.com/poster.jpg",
-  "performanceStartDate" : "2025-09-05",
-  "performanceEndDate" : "2025-09-04"
-}
-
-
-
-
-
HTTP/1.1 200 OK
-X-Content-Type-Options: nosniff
-X-XSS-Protection: 0
-Cache-Control: no-cache, no-store, max-age=0, must-revalidate
-Pragma: no-cache
-Expires: 0
-X-Frame-Options: DENY
-Content-Type: application/json
-Transfer-Encoding: chunked
-Date: Fri, 05 Sep 2025 07:14:25 GMT
-Keep-Alive: timeout=60
-Connection: keep-alive
-Content-Length: 176
-
-{
-  "status" : "INTERNAL_SERVER_ERROR",
-  "message" : "공연 시작 날짜는 종료 날짜 이후에 있을 수 없습니다.",
-  "timestamp" : "2025-09-05T16:14:25.757063"
-}
-
-
-
-
-
- - - - - - diff --git a/application/src/test/java/org/mandarin/booking/utils/DocsUtils.java b/application/src/test/java/org/mandarin/booking/utils/DocsUtils.java index fcd348a..03f274c 100644 --- a/application/src/test/java/org/mandarin/booking/utils/DocsUtils.java +++ b/application/src/test/java/org/mandarin/booking/utils/DocsUtils.java @@ -13,7 +13,11 @@ import io.restassured.http.ContentType; import io.restassured.specification.RequestSpecification; import java.lang.StackWalker.StackFrame; +import java.lang.reflect.Method; +import java.util.Arrays; import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; import org.springframework.core.env.Environment; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.restassured.RestAssuredRestDocumentation; @@ -22,40 +26,110 @@ @Component public record DocsUtils(Environment environment, ObjectMapper objectMapper) { - private static final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); + private static final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); + private static final String DISPLAY_SLASH = "/"; private static volatile boolean started = false; public String execute(String method, String path, Object requestBody, Map headers) throws Exception { - var snippet = sanitize(method, path); + var baseSnippet = sanitize(method, path, false); + var methodSpecificSnippet = sanitize(method, path, true); + boolean disableDocs = isRestDocsDisabledForCurrentCall(); if (!disableDocs) { ensureStarted(); } var spec = prepareSpec(headers, disableDocs); - if ("POST".equals(method)) { + + if ("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method)) { spec.contentType(ContentType.JSON); if (requestBody != null) { spec.body(objectMapper.writeValueAsString(requestBody)); } } - var resp = ("GET".equals(method)) - ? (disableDocs ? spec.when().get(path) - : spec.filter(docFilter(snippet)).when().get(path)) - : (disableDocs ? spec.when().post(path) - : spec.filter(docFilter(snippet)).when().post(path)); + + if (!disableDocs) { + spec = spec.filter(docFilter(baseSnippet)).filter(docFilter(methodSpecificSnippet)); + } + + var resp = switch (method.toUpperCase()) { + case "GET" -> spec.when().get(path); + case "POST" -> spec.when().post(path); + case "PUT" -> spec.when().put(path); + case "PATCH" -> spec.when().patch(path); + case "DELETE" -> spec.when().delete(path); + default -> throw new IllegalArgumentException("Unsupported method: " + method); + }; return resp.then().extract().asString(); } + private String sanitize(String method, String path, boolean withMethodSuffix) { + String groupTitle = getCurrentTestClass() + .flatMap(this::getDisplayNameOfClass) + .orElseGet(() -> (method.toUpperCase() + " " + path).replace("/", DISPLAY_SLASH).trim()); + + String name = groupTitle.replace("/", DISPLAY_SLASH).replaceAll("\\s+", " ").trim(); + + if (withMethodSuffix) { + String methodTitle = getCurrentTestMethodName() + .flatMap(mn -> getCurrentTestClass().flatMap(cls -> getDisplayNameOfMethod(cls, mn)) + .or(() -> Optional.of(mn))) + .orElse(""); + + if (!methodTitle.isEmpty()) { + name = name + " - " + methodTitle; + } + } + return name; + } + + private Optional> getCurrentTestClass() { + try { + return getInstance(RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .map(StackFrame::getDeclaringClass) + .filter(cls -> cls.getName().startsWith("org.mandarin")) + .filter(cls -> cls.isAnnotationPresent(IntegrationTest.class)) + .findFirst()); + } catch (Throwable t) { + return Optional.empty(); + } + } + + private Optional getDisplayNameOfClass(Class cls) { + DisplayName ann = cls.getAnnotation(DisplayName.class); + return Optional.ofNullable(ann).map(DisplayName::value); + } + + private Optional getDisplayNameOfMethod(Class cls, String methodName) { + try { + Method m = Arrays.stream(cls.getDeclaredMethods()) + .filter(mm -> mm.getName().equals(methodName)) + .findFirst() + .orElse(null); + if (m == null) { + return Optional.empty(); + } + DisplayName ann = m.getAnnotation(DisplayName.class); + return Optional.ofNullable(ann).map(DisplayName::value); + } catch (Throwable t) { + return Optional.empty(); + } + } - private String sanitize(String method, String path) { - var name = method + path; - name = name.replaceAll("^/+", ""); - name = name.replaceAll("[/{}]", "-"); - name = name.replaceAll("[^a-zA-Z0-9-_]", "-"); - return name.toLowerCase(); + private Optional getCurrentTestMethodName() { + try { + return getInstance(RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .filter(f -> f.getDeclaringClass().getName().startsWith("org.mandarin")) + .filter(f -> f.getDeclaringClass().isAnnotationPresent(IntegrationTest.class)) + .findFirst() + .map(StackFrame::getMethodName)); + } catch (Throwable t) { + return Optional.empty(); + } } private boolean isRestDocsDisabledForCurrentCall() { From a6920d6952dcf74b49ee3aae538a8448b375625e Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Sun, 14 Sep 2025 10:40:55 +0900 Subject: [PATCH 29/36] remove outdated comments and streamline build directory references --- application/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/application/build.gradle b/application/build.gradle index 47a93ac..3f52569 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -39,8 +39,6 @@ dependencies { asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' } -/** - * Provider 기반 최신 문법: buildDir 대신 layout.buildDirectory 사용*/ def snippetsDir = layout.buildDirectory.dir("generated-snippets") def generatedIndexDir = layout.buildDirectory.dir("tmp/asciidoc") def asciidocOutputDir = layout.buildDirectory.dir("asciidoc") From afb7cf2a644c61371da5e6b87cc7147671590d8e Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Mon, 15 Sep 2025 16:10:06 +0900 Subject: [PATCH 30/36] update dependency configuration and add Docker compose file --- application/build.gradle | 1 + application/src/main/resources/application-local.yml | 1 + compose.yaml => application/src/main/resources/compose.yaml | 0 domain/build.gradle | 2 +- internal/build.gradle | 2 +- 5 files changed, 4 insertions(+), 2 deletions(-) rename compose.yaml => application/src/main/resources/compose.yaml (100%) diff --git a/application/build.gradle b/application/build.gradle index 3f52569..3d288c1 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -18,6 +18,7 @@ dependencies { api(project(':domain')) api(project(':internal')) api(project(':external')) + implementation project(':common') implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-actuator' diff --git a/application/src/main/resources/application-local.yml b/application/src/main/resources/application-local.yml index 7586e06..5e4f9a9 100644 --- a/application/src/main/resources/application-local.yml +++ b/application/src/main/resources/application-local.yml @@ -14,6 +14,7 @@ spring: docker: compose: lifecycle-management: start_only + file: classpath:compose.yaml web.resources.add-mappings: false jwt: token: diff --git a/compose.yaml b/application/src/main/resources/compose.yaml similarity index 100% rename from compose.yaml rename to application/src/main/resources/compose.yaml diff --git a/domain/build.gradle b/domain/build.gradle index 086120f..83e1f51 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -7,7 +7,7 @@ jar { } dependencies { - api project(':common') + implementation project(':common') api 'org.springframework.boot:spring-boot-starter-data-jpa' api 'org.springframework.boot:spring-boot-starter-validation' diff --git a/internal/build.gradle b/internal/build.gradle index 4f2d129..2eff4bb 100644 --- a/internal/build.gradle +++ b/internal/build.gradle @@ -7,7 +7,7 @@ jar { } dependencies { - api project(':common') + implementation project(':common') // ---- Spring Boot Web ---- api 'org.springframework.boot:spring-boot-starter-web' From 5f05158a4df9ce36eb1cedcedc81f149b0c32094 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Mon, 15 Sep 2025 16:16:31 +0900 Subject: [PATCH 31/36] rename controllers and update package structure for modularization --- .../booking/{app/member => adapter/webapi}/AuthController.java | 3 ++- .../{app/member => adapter/webapi}/MemberController.java | 3 ++- .../booking/{app/show => adapter/webapi}/ShowController.java | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) rename application/src/main/java/org/mandarin/booking/{app/member => adapter/webapi}/AuthController.java (89%) rename application/src/main/java/org/mandarin/booking/{app/member => adapter/webapi}/MemberController.java (87%) rename application/src/main/java/org/mandarin/booking/{app/show => adapter/webapi}/ShowController.java (91%) diff --git a/application/src/main/java/org/mandarin/booking/app/member/AuthController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java similarity index 89% rename from application/src/main/java/org/mandarin/booking/app/member/AuthController.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java index 665eb6d..038afe1 100644 --- a/application/src/main/java/org/mandarin/booking/app/member/AuthController.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/AuthController.java @@ -1,7 +1,8 @@ -package org.mandarin.booking.app.member; +package org.mandarin.booking.adapter.webapi; import jakarta.validation.Valid; import org.mandarin.booking.TokenHolder; +import org.mandarin.booking.app.member.AuthUseCase; import org.mandarin.booking.domain.member.AuthRequest; import org.mandarin.booking.domain.member.ReissueRequest; import org.springframework.web.bind.annotation.PostMapping; diff --git a/application/src/main/java/org/mandarin/booking/app/member/MemberController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java similarity index 87% rename from application/src/main/java/org/mandarin/booking/app/member/MemberController.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java index 31d87ff..dae95d0 100644 --- a/application/src/main/java/org/mandarin/booking/app/member/MemberController.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/MemberController.java @@ -1,6 +1,7 @@ -package org.mandarin.booking.app.member; +package org.mandarin.booking.adapter.webapi; import jakarta.validation.Valid; +import org.mandarin.booking.app.member.MemberRegisterer; import org.mandarin.booking.domain.member.MemberRegisterRequest; import org.mandarin.booking.domain.member.MemberRegisterResponse; import org.springframework.web.bind.annotation.PostMapping; diff --git a/application/src/main/java/org/mandarin/booking/app/show/ShowController.java b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java similarity index 91% rename from application/src/main/java/org/mandarin/booking/app/show/ShowController.java rename to application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java index 9713c43..2a98764 100644 --- a/application/src/main/java/org/mandarin/booking/app/show/ShowController.java +++ b/application/src/main/java/org/mandarin/booking/adapter/webapi/ShowController.java @@ -1,6 +1,7 @@ -package org.mandarin.booking.app.show; +package org.mandarin.booking.adapter.webapi; import jakarta.validation.Valid; +import org.mandarin.booking.app.show.ShowRegisterer; import org.mandarin.booking.domain.show.ShowRegisterRequest; import org.mandarin.booking.domain.show.ShowRegisterResponse; import org.mandarin.booking.domain.show.ShowScheduleRegisterRequest; From c8bef459b6d10074d6487a6bdc33954bec72c4f1 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Mon, 15 Sep 2025 16:17:51 +0900 Subject: [PATCH 32/36] modify few documents --- README.md | 26 +++-- docs/devlog/250914.md | 80 +++++++++++++ docs/specs/domain.md | 2 +- docs/specs/policy/application.md | 189 ++++++++++++++++++++++--------- docs/todo.md | 6 +- 5 files changed, 236 insertions(+), 67 deletions(-) create mode 100644 docs/devlog/250914.md diff --git a/README.md b/README.md index f0c171a..b9e4c56 100644 --- a/README.md +++ b/README.md @@ -25,19 +25,23 @@ ## 3. 아키텍처 개요 (Hexagonal) -헥사고날 아키텍처를 적용하여 다음과 같은 레이어 규칙을 따릅니다. +헥사고날 아키텍처를 일부 차용한 모듈 구조를 채택했습니다. 각 모듈의 책임은 다음과 같습니다. -- domain: 순수한 도메인 모델과 비즈니스 규칙. 프레임워크 의존 금지. -- app: 유스케이스 서비스, 입력/출력 포트, 트랜잭션 경계, 검증, AOP. -- adapter: 웹 API, 보안, 영속성 등 외부 인터페이스. +- internal: 애플리케이션 내부의 생태계를 관리한다. 직접적인 비즈니스 관리영역이 아닌 '애플리케이션' 자체를 관리한다. 로그 설정, web 설정, 보안 설정등 비즈니스 요구사항을 직접적으로 나타내지 않는 + 구현들이 존재한다. +- external: 외부 세계와의 통신을 담당한다. 도메인 로직은 물론 애플리케이션과도 완전 독립적인 모듈이다. MQ, STMP등등에 대한 기능의 구현이 존재한다. +- domain: 비즈니스 영역의 핵심이 되는 영역이다. 비즈니스를 해결하기 위한 도메인 그 자체를 의미하며 도메인 개념을 로직으로 풀어나가는 영역이다. Entity와 통신 객체들이 여기에 해당한다. +- common: 공통코드들을 관리한다. 파급효과가 가장 큰 영역인 만큼 라이브러리 사용을 방지하고 POJO 스타일을 원칙으로 한다. 상수와 type object들이 존재한다. +- application: 모든 영역들을 통합해 애플리케이션을 만들어 관리한다. Spring boot의 main class가 존재하며, 각 모듈들을 통합해 비즈니스 요구사항을 해결한다. 비즈니스 로직을 해결하는 + 영역과 이를 전달하는 영역으로 대부분의 Service 영역과 Controller영역, 그리고 통합 테스트가 존재한다. -근거와 세부 규칙 +- 근거와 세부 규칙 - 정책 문서: [docs/specs/policy](docs/specs/policy) -- 레이어 테스트: `src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java` +- 레이어 테스트: `application/src/test/java/org/mandarin/booking/arch/ModuleDependencyRulesTest.java` - 패키지 구조 예 - 도메인: `src/main/java/org/mandarin/booking/domain/*` - - 앱/포트/영속 어댑터: `src/main/java/org/mandarin/booking/app/*` (`app/persist` 포함) + - 앱/포트/영속 어댑터: `src/main/java/org/mandarin/booking/app/*` - 웹/보안 어댑터: `src/main/java/org/mandarin/booking/adapter/{webapi,security}/*` 텍스트 다이어그램: [Controllers/Security/External] → adapter → app(ports, services) → domain @@ -48,6 +52,7 @@ - Show (Aggregate Root): 제목, 감독, 장르, 상영시간, 개봉일, 등급, 줄거리, 포스터URL, 출연진 등. 팩토리/커맨드 기반 생성. - Member (Aggregate Root): 닉네임, userId, email, passwordHash, 권한 목록. 비밀번호 해시 일치 검증. +- Hall (Entity): 상영관 이름, 좌석 배치(행/열), 총 좌석 수. 자세한 속성과 규칙: [docs/specs/domain.md](docs/specs/domain.md) @@ -62,6 +67,7 @@ - Hexagonal: 테스트 용이성과 변경 격리를 위해 계층 경계를 명확히. 또한, 추후 모듈화 or MSA 전환시 이점을 위해 애플리케이션 아키텍처를 영역에 따라 구분. - Spring Security + JWT: 무상태(stateless) API 인증과 확장성. - JPA + RDB(H2/MySQL): 표준 ORM과 빠른 테스트 사이클. +- Spring Modulith: 명확한 Bounded Context 경계 분리 및 추수 MSA 전환 대비 --- @@ -71,8 +77,8 @@ - 테스트 정책 문서: [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` +- 모듈 구조 테스트: 모듈간 의존관계 테스트 + - 예시: `application/src/test/java/org/mandarin/booking/arch/ModuleDependencyRulesTest.java` Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profiles, JUnit Platform, ByteBuddy javaagent). @@ -94,7 +100,7 @@ Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profile - 프로필: `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) + - 근거: `application-local.yml`, Docker Compose: [compose.yaml](application/src/main/resources/compose.yaml) - test: H2 메모리 + MySQL 호환 모드 + JPA `ddl-auto: create` - 근거: `application-test.yml` diff --git a/docs/devlog/250914.md b/docs/devlog/250914.md new file mode 100644 index 0000000..1df9373 --- /dev/null +++ b/docs/devlog/250914.md @@ -0,0 +1,80 @@ +## 예찬 + +프로젝트 전반의 구조에 대해 확장 가능성이 충분히 존재할것으로 예상되어 빌드 구조적, 도메인 개념적 구조화된 분리를 할 필요성이 있다고 느껴져 모듈화 작업을 진행했다. + +기존의 싱글모듈 프로젝트에서 멀티모듈로 전환하는 과정을 거쳤다. 변경된 모듈 구조는 아래와 같다. + +Root: 프로젝트 최상단. 모든 모듈에게 공통적으로 사용되는 의존성을 관리한. test, lombok, null-safety 관련 의존성이 존재한다. + +- Internal: 애플리케이션 내부의 생태계를 관리한다. 직접적인 비즈니스 관리영역이 아닌 '애플리케이션' 자체를 관리한다. 로그 설정, web 설정, 보안 설정등 비즈니스 요구사항을 직접적으로 나타내지 않는 + 구현들이 존재한다. +- External: 외부 세계와의 통신을 담당한다. 도메인 로직은 물론 애플리케이션과도 완전 독립적인 모듈이다. MQ, STMP등등에 대한 기능의 구현이 존재한다. +- Domain: 비즈니스 영역의 핵심이 되는 영역이다. 비즈니스를 해결하기 위한 도메인 그 자체를 의미하며 도메인 개념을 로직으로 풀어나가는 영역이다. Entity와 통신 객체들이 여기에 해당한다. +- Common: 공통코드들을 관리한다. 파급효과가 가장 큰 영역인 만큼 라이브러리 사용을 방지하고 POJO 스타일을 원칙으로 한다. 상수와 type object들이 존재한다. +- Application: 모든 영역들을 통합해 애플리케이션을 만들어 관리한다. Spring boot의 main class가 존재하며, 각 모듈들을 통합해 비즈니스 요구사항을 해결한다. 비즈니스 로직을 해결하는 + 영역과 이를 전달하는 영역으로 대부분의 Service 영역과 Controller영역, 그리고 통합 테스트가 존재한다. + +그래서 모듈 구조를 정리해보자면, + +```mermaid +flowchart + Application --> Domain + Application --> Common + Application --> Internal + Application --> External + Domain --> Common + Internal --> Common + +``` + +조금더 영역을 나눠본다면 다음과 같을것이다. + +```mermaid +flowchart TB + subgraph L4["Application"] + A2[Controller] + A3[Service] + A4[통합 테스트] + end + + subgraph L3["Infrastructure"] + subgraph Internal["Internal"] + I1[Logging] + I2[Web Configurations] + I3[Security Configurations] + end + subgraph External["External"] + E1[MQ] + E2[SMTP] + E3[External API] + end + end + + subgraph L2["Domain"] + D1[Entity] + D2[DTO/VO] + end + + subgraph L1["Common"] + C1[enum] + C2[Type Objects] + end + +%% 의존 관계 (상위 → 하위) + A2 --> A3 + A3 --> D1 + A3 --> Internal + A3 --> External + D1 --> L1 + A2 --> D2 + A3 --> D2 + Internal --> L1 + External --> L1 + +``` + +참고로 테스트는 따지자면 그 어떤 영역에도 의존하지 않아야한다. 테스트는 만들어진 기능의 가장 첫 사용자이기 때문에 원칙대로라면 그 어떤 모듈에도 의존하지 않아야한다. 다만 개발 편의를 위해 일부 의존된 부분이 +있으니 어느정도 수용하고 넘어갈 수 있는 부분인거 같다. + +그리고 이제 패키지와 모듈 관계를 정리하며 외부와 소통할 클래스만 public을 사용한다. java의 클래스 기본 접근 제한자가 package-private인 이유는 필요한 기능들에 대해서만 외부에 노출하고 그 +외에는 외부에서 신경쓰지 않도록 하기 위해서다. 그 의도에 맞게 적당히 필요한 경우에만 public을 사용하고 나머지는 package-private으로 유지한다. diff --git a/docs/specs/domain.md b/docs/specs/domain.md index e96528e..cab60ce 100644 --- a/docs/specs/domain.md +++ b/docs/specs/domain.md @@ -36,7 +36,7 @@ _Aggregate Root_ #### 행위 - `create(command: ShowCreateCommand)` -- `registerSchedule(hallId, startAt, endAt, runtimeOverride)` +- `registerSchedule(hallId, startAt, endAt)` #### 관련 타입 diff --git a/docs/specs/policy/application.md b/docs/specs/policy/application.md index 0de348a..5f15a03 100644 --- a/docs/specs/policy/application.md +++ b/docs/specs/policy/application.md @@ -7,43 +7,79 @@ 프로젝트는 크게 세 계층으로 구성됩니다. -- domain: 도메인 모델과 비즈니스 규칙의 순수 영역 - - 위치: `src/main/java/org/mandarin/booking/domain` - - 포함: 엔티티(를 표현하기 위한 매핑정보), 값 객체, 도메인 서비스(필요시), 도메인 예외, 도메인 전용 인터페이스(예: `SecurePasswordEncoder`), 유스케이스에 전달되는 순수 모델( - `*Request`, `*Response`, `*Command` 등) - - 금지: 프레임워크/외부 라이브러리 의존(JPA/Spring/Web 등), I/O 접근, 인프라 세부 사항 - -- app: 애플리케이션 서비스(유스케이스)와 포트 인터페이스 - - 위치: `src/main/java/org/mandarin/booking/app` - - 포함: 유스케이스 서비스(`*Service`), 입력/출력 포트(`app/port`), 트랜잭션 경계, 조합/오케스트레이션 로직, 검증기(애플리케이션 수준), 크로스커팅(AOP, 로깅 등) - - 의존: domain에는 의존 가능, adapter에는 의존 금지 - -- adapter: 외부 세계와의 연결(웹, 보안, 영속성 등) - - 위치: `src/main/java/org/mandarin/booking/adapter` - - 하위 영역: - - `webapi`: REST 컨트롤러, DTO 매핑, 예외/응답 공통 처리 - - `security`: 인증/인가 컴포넌트(JwtFilter, AuthenticationProvider 등) - - `persist`: 영속성 구현은 현재 `app/persist` 패키지에 배치되어 있으며, 어댑터 구현으로 취급합니다. JPA 리포지토리와 실제 데이터 접근 로직이 위치합니다. - - 의존: app의 포트에만 의존해야 하며 domain, app 구현 내부로 직접 의존하지 않습니다(서비스 구현 클래스 참조 금지). - -텍스트 다이어그램: - -[Controllers/Security/JPA] → adapter → app(ports, services) → domain\(pure model) +- domain 모듈 + +--- + +- common 모듈 +- **위치**: `./common` +- **책임**: 모든 모듈이 공통적으로 사용할 수 있는 순수 코드 + - Enum + - Type Object +- **특징**: + - 외부 라이브러리 의존 금지 + - POJO 스타일 유지 + +--- + +- domain 모듈 +- **위치**: `./domain` +- **책임**: 비즈니스 핵심 모델과 규칙 + - Entity + - DTO/VO +- **의존성**: Common 모듈에만 의존 가능 + +--- + +- Internal 모듈 + +- **위치**: `./internal` +- **책임**: 애플리케이션 내부 생태계 관리 + - Logging + - Web Configurations + - Security Configurations +- **의존성**: Common 모듈에 의존 가능 +- External 모듈 +- **위치**: `./external` +- **책임**: 외부 세계와의 연결 + - MQ + - SMTP + - External API +- **의존성**: Common 모듈에 의존 가능 + +--- + +## 4. Application 모듈 + +- **위치**: `./application` +- **책임**: 애플리케이션 통합 및 유스케이스 실행 + - Controller + - Service + - 통합 테스트 +- **의존성** + - Domain 모듈 + - Common 모듈 + - Internal 모듈 + - External 모듈 + +--- ## 2. 의존성 규칙 - domain -> another domain -- app -> domain (OK), adapter (금지) -- adapter -> app 포트(OK), app 서비스/구현(금지), domain(읽기 전용 OK. 단, 비즈니스 수행은 app 경유) +- application -> domain (OK), adapter (금지) +- adapter -> application 포트(OK), application 서비스/구현(금지), domain(읽기 전용 OK. 단, 비즈니스 수행은 application 경유) - DTO/엔티티 경계: - webapi의 요청/응답 DTO는 한시적으로 domain에 존재. 추후 변경 가능성 있음. - 영속성 엔티티는 domain에만 존재. domain 엔티티와 동일 클래스로 사용. ## 3. 포트와 어댑터 -- 입력 포트(inbound port): 유스케이스 인터페이스. 위치: `app/port` 컨트롤러는 입력 포트를 통해서만 유스케이스 호출. -- 출력 포트(outbound port): 외부 시스템/리포지토리에 대한 인터페이스. 위치: `app/persist` 또는 `app/port` 하위에 정의 가능. -- 어댑터(adapters): 포트 인터페이스의 구현체. 위치: adapter 하위. 현재 JPA 기반 구현은 `app/persist/*Repository`를 통해 동작하며, 해당 패키지는 어댑터 계층으로 +- 입력 포트(inbound port): 유스케이스 인터페이스. 위치: `application/aggregate-root` 컨트롤러는 입력 포트를 통해서만 유스케이스 호출. +- 출력 포트(outbound port): 외부 시스템/리포지토리에 대한 인터페이스. 위치: `application/aggregate-root` 또는 `application/aggregate-root` 하위에 정의 + 가능. +- 어댑터(adapters): 포트 인터페이스의 구현체. 위치: adapter 하위. 현재 JPA 기반 구현은 `application/aggregate-root/*Repository`를 통해 동작하며, 해당 패키지는 + 어댑터 계층으로 간주합니다. 권장 네이밍: @@ -54,55 +90,101 @@ ## 4. 트랜잭션/검증/예외/로깅 규칙 -- 트랜잭션 경계: app 계층의 유스케이스 서비스 메서드 수준에서 관리(@Transactional). 컨트롤러/어댑터에서는 트랜잭션을 시작하지 않습니다. +- 트랜잭션 경계: application 계층의 유스케이스 서비스 메서드 수준에서 관리(`@Transactional`). 컨트롤러/어댑터에서는 트랜잭션을 시작하지 않습니다. - 검증: - 형태/구문 검증: adapter(webapi)에서 기본적인 바인딩/형식 검증 허용. - - 비즈니스/정책 검증: app 또는 domain에서 수행. `Validator` 등의 컴포넌트는 app에 위치. + - 비즈니스/정책 검증: application 또는 domain에서 수행. `Validator` 등의 컴포넌트는 application에 위치. - 예외: - 도메인 오류는 domain 예외(`DomainException`의 자식 클래스)로 표현. - - 어댑터/기술 오류는 해당 계층에서 포착하고 app/domain 의미의 예외로 변환 또는 적절히 매핑. + - 어댑터/기술 오류는 해당 계층에서 포착하고 domain 의미의 예외로 변환 또는 적절히 매핑. - webapi는 예외를 `GlobalExceptionHandler`로 공통 변환하여 `ErrorResponse`로 응답. -- 로깅: 크로스커팅은 app 계층의 AOP(`LoggingAspect`)에서 처리. 민감 정보(비밀번호, 토큰 등)는 로그 금지. - -## 5. 패키지 구조 규칙 - -- domain: `org.mandarin.booking.domain.{boundedContext}` - - 예: `domain.member`, `domain.show` -- app: `org.mandarin.booking.app` - - 하위: `port`, `persist`(출력 포트/구현), 서비스 클래스 -- adapter: `org.mandarin.booking.adapter.{webapi|security|...}` -- 순환 의존 금지: 위 규칙 위반 시 컴파일/테스트 단계에서 아키텍처 테스트 실패로 간주. +- 로깅: 크로스커팅은 internal 계층의 AOP(`LoggingAspect`)에서 처리. 민감 정보(비밀번호, 토큰 등)는 로그 금지. + +## 5. 모듈 구조 규칙 + +- internal: 애플리케이션 내부의 생태계를 관리한다. 직접적인 비즈니스 관리영역이 아닌 '애플리케이션' 자체를 관리한다. 로그 설정, web 설정, 보안 설정등 비즈니스 요구사항을 직접적으로 나타내지 않는 + 구현들이 존재한다. +- external: 외부 세계와의 통신을 담당한다. 도메인 로직은 물론 애플리케이션과도 완전 독립적인 모듈이다. MQ, STMP등등에 대한 기능의 구현이 존재한다. +- domain: 비즈니스 영역의 핵심이 되는 영역이다. 비즈니스를 해결하기 위한 도메인 그 자체를 의미하며 도메인 개념을 로직으로 풀어나가는 영역이다. Entity와 통신 객체들이 여기에 해당한다. +- common: 공통코드들을 관리한다. 파급효과가 가장 큰 영역인 만큼 라이브러리 사용을 방지하고 POJO 스타일을 원칙으로 한다. 상수와 type object들이 존재한다. +- application: 모든 영역들을 통합해 애플리케이션을 만들어 관리한다. Spring boot의 main class가 존재하며, 각 모듈들을 통합해 비즈니스 요구사항을 해결한다. 비즈니스 로직을 해결하는 + 영역과 이를 전달하는 영역으로 대부분의 Service 영역과 Controller영역, 그리고 통합 테스트가 존재한다. + +### 5.1 모듈간 의존관계 + +```mermaid +flowchart TB + subgraph L4["Application"] + A2[Controller] + A3[Service] + A4[통합 테스트] + end + + subgraph L3["Infrastructure"] + subgraph Internal["Internal"] + I1[Logging] + I2[Web Configurations] + I3[Security Configurations] + end + subgraph External["External"] + E1[MQ] + E2[SMTP] + E3[External API] + end + end + + subgraph L2["Domain"] + D1[Entity] + D2[DTO/VO] + end + + subgraph L1["Common"] + C1[enum] + C2[Type Objects] + end + + A2 --> A3 + A3 --> D1 + A3 --> Internal + A3 --> External + D1 --> L1 + A2 --> D2 + A3 --> D2 + Internal --> L1 + External --> L1 + +``` ## 6. 컨트롤러와 DTO 변환 규칙 - 컨트롤러는 입력 포트만 의존한다. -- 요청 DTO -> domain/app 요청 모델로 변환 후 유스케이스 호출. +- 요청 DTO -> domain/application 요청 모델로 변환 후 유스케이스 호출. - 유스케이스 반환값 -> web DTO로 매핑하여 응답한다. - 컨트롤러에서 비즈니스 로직/트랜잭션 처리 금지. 예시(공연 등록): -- `adapter/webapi/ShowController` -> `app/port/ShowRegisterer` 호출 +- `adapter/webapi/ShowController` -> `application/port/ShowRegisterer` 호출 - `domain.show.ShowRegisterRequest`/`ShowCreateCommand` 사용하여 유스케이스 실행 - 결과를 `domain.show.ShowRegisterResponse` 받아 web 응답으로 래핑(`ApiResponse`) ## 7. 영속성 규칙(JPA) -- JPA 엔티티는 domain에, Repository는 app(persist)에만 존재. -- app 계층은 JPA 구체 타입에 의존하지 않고, 출력 포트 인터페이스를 통해서만 데이터 접근. +- JPA 엔티티는 domain에, Repository는 application(persist)에만 존재. +- application 계층은 JPA 구체 타입에 의존하지 않고, 출력 포트 인터페이스를 통해서만 데이터 접근. - 매핑 책임은 어댑터에 위치: JPA 엔티티 <-> 도메인 엔티티/모델 변환. ## 8. 보안 규칙 - 인증/인가 컴포넌트는 adapter/security에 위치: `JwtFilter`, `CustomAuthenticationProvider`, `SecurityConfig` 등. -- 보안 컨텍스트와 토큰 파싱은 어댑터에서 처리하고, app 유스케이스에는 인증된 식별자/역할만 전달. +- 보안 컨텍스트와 토큰 파싱은 어댑터에서 처리하고, application 유스케이스에는 인증된 식별자/역할만 전달. ## 9. 테스트 규칙 - `src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java` 는 아키텍처 규칙을 자동 검증합니다. - 규칙 위반 예: - - adapter가 app 서비스 구현 클래스에 직접 의존 - - app이 adapter 패키지에 의존 + - adapter가 application 서비스 구현 클래스에 직접 의존 + - application이 adapter 패키지에 의존 - domain이 프레임워크에 의존 - 새로운 모듈/클래스 추가 시 해당 테스트가 통과하는지 반드시 확인합니다. @@ -111,15 +193,15 @@ 새 유스케이스(예: 공연 수정) 추가 절차: 1) domain에 필요한 모델/명세 정의(예: `ShowUpdateCommand`). -2) app/port에 입력 포트 정의(예: `ShowUpdater`). -3) app에 서비스 구현(`ShowService` 내 메서드 또는 별도 서비스) 및 트랜잭션/검증 구현. +2) application/port에 입력 포트 정의(예: `ShowUpdater`). +3) application에 서비스 구현(`ShowService` 내 메서드 또는 별도 서비스) 및 트랜잭션/검증 구현. 4) 필요 시 출력 포트 정의 및 어댑터 구현(persist/JPA 등). 5) adapter/webapi에 컨트롤러 엔드포인트 추가 및 DTO 매핑. 6) 아키텍처/통합 테스트 통과 확인. 새 어댑터(예: 외부 결제 API) 추가 절차: -1) app에 출력 포트 인터페이스 추가(예: `PaymentGateway`). +1) application에 출력 포트 인터페이스 추가(예: `PaymentGateway`). 2) adapter 하위에 구현(예: `adapter/external/PaymentGatewayHttpClient`). 3) 구성(Security/Config)과 예외 매핑 추가. @@ -127,16 +209,17 @@ Do -- 유스케이스 입출력은 app 포트를 통해서만 노출/호출한다. +- 유스케이스 입출력은 application 포트를 통해서만 노출/호출한다. - 도메인 모델은 순수하게 유지한다(프레임워크 의존 금지). - 어댑터는 포트 인터페이스를 구현한다. -- 트랜잭션과 로깅은 app에서 관리한다. +- 트랜잭션과 로깅은 application에서 관리한다. Don’t - 컨트롤러에서 비즈니스 로직 수행 금지. -- app에서 adapter 패키지/구현에 의존 금지. +- application에서 adapter 패키지/구현에 의존 금지. - domain에서 JPA/Spring 등에 의존 금지. +- 모듈간 의존관계를 위배하는 구현 금지. ## 12. 용어 diff --git a/docs/todo.md b/docs/todo.md index f0b4d18..cd8330d 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -23,9 +23,9 @@ 2025.09.09 -- [ ] 모듈화 설계 - - [ ] public 떡칠하지 말고 기본 접근제어자 적극 활용 - - [ ] Spring Modulith 사용 가능한지 점검 +- [x] 모듈화 설계 + - [x] public 떡칠하지 말고 기본 접근제어자 적극 활용 + - [x] Spring Modulith 사용 가능한지 점검 --- From 63209b726aa52e265f6088d5c63079aa837f7788 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Mon, 15 Sep 2025 16:19:30 +0900 Subject: [PATCH 33/36] update README to simplify project overview --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index b9e4c56..9b91d1c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -# booking — 공연 예매 시스템 (개요 중심 README) - -- 저장소 루트: 단일 모듈(Spring Boot) 프로젝트 +# booking — 공연 예매 시스템 --- From 0a7537143d1375647d0737183147b831b8d225b0 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Mon, 15 Sep 2025 16:21:12 +0900 Subject: [PATCH 34/36] update README to reflect new package structure for modularization --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9b91d1c..c8be2a1 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,9 @@ - 정책 문서: [docs/specs/policy](docs/specs/policy) - 레이어 테스트: `application/src/test/java/org/mandarin/booking/arch/ModuleDependencyRulesTest.java` - 패키지 구조 예 - - 도메인: `src/main/java/org/mandarin/booking/domain/*` - - 앱/포트/영속 어댑터: `src/main/java/org/mandarin/booking/app/*` - - 웹/보안 어댑터: `src/main/java/org/mandarin/booking/adapter/{webapi,security}/*` + - 도메인: `domain/src/main/java/org/mandarin/booking/domain/*` + - 앱/포트/영속 어댑터: `application/src/main/java/org/mandarin/booking/app/*` + - 웹/보안 어댑터: `application/src/main/java/org/mandarin/booking/adapter/{webapi,security}/*` 텍스트 다이어그램: [Controllers/Security/External] → adapter → app(ports, services) → domain From d0cee67ce2e147895e1620ad442b54cc9a793823 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Mon, 15 Sep 2025 16:34:43 +0900 Subject: [PATCH 35/36] update build.gradle to improve task dependencies and fix output directory reference --- application/build.gradle | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/application/build.gradle b/application/build.gradle index 3d288c1..6dda820 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -37,20 +37,26 @@ dependencies { testImplementation 'io.rest-assured:rest-assured' testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' - asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:3.1.0' } def snippetsDir = layout.buildDirectory.dir("generated-snippets") def generatedIndexDir = layout.buildDirectory.dir("tmp/asciidoc") def asciidocOutputDir = layout.buildDirectory.dir("asciidoc") +tasks.register('prepareSnippetsDir') { + doLast { + snippetsDir.get().asFile.mkdirs() + } +} + tasks.named('test') { jvmArgs "-javaagent:${configurations.byteBuddyAgent.singleFile}" outputs.dir(snippetsDir) } tasks.register('generateIndexAdoc') { - dependsOn tasks.named('test') + dependsOn tasks.named('test'), tasks.named('prepareSnippetsDir') outputs.file(generatedIndexDir.map { it.file("index.adoc") }) doLast { def genDir = generatedIndexDir.get().asFile @@ -110,7 +116,7 @@ tasks.register('generateIndexAdoc') { } tasks.named('asciidoctor') { - dependsOn tasks.named('generateIndexAdoc') + dependsOn tasks.named('prepareSnippetsDir'), tasks.named('generateIndexAdoc') inputs.dir(snippetsDir) configurations = [project.configurations.asciidoctorExt] sourceDir generatedIndexDir.get().asFile @@ -126,7 +132,7 @@ tasks.named('build') { tasks.named('bootJar') { dependsOn tasks.named('asciidoctor') - from(asciidocOutputDir) { into 'static/docs' } + from(asciidocOutputDir.get()) { into 'static/docs' } } tasks.named('clean') { From 24c3b1c6b8c61d40f545ec0777bd4e1fca9aca66 Mon Sep 17 00:00:00 2001 From: YeaChan05 Date: Mon, 15 Sep 2025 16:36:11 +0900 Subject: [PATCH 36/36] update build.gradle to remove specific version for asciidoctor extension --- application/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/build.gradle b/application/build.gradle index 6dda820..2e221c4 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -37,7 +37,7 @@ dependencies { testImplementation 'io.rest-assured:rest-assured' testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' - asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:3.1.0' + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' } def snippetsDir = layout.buildDirectory.dir("generated-snippets")