From 91e4b28ac63ec158739abc8cf81d2899ae471c55 Mon Sep 17 00:00:00 2001 From: RosieOh Date: Thu, 22 Oct 2026 17:41:42 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EA=B8=B0=EB=B0=98=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EA=B0=80=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=9D=84=20=EC=95=8C=EB=8D=98=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EB=81=8A=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "core 기반 패키지는 domain 을 의존하지 않는다" 위반 50건은 전부 인증·인가 기반 코드였다. 다섯 덩어리였고 각각 원인이 달랐다. - CurrentUserFacade (15): 이 클래스가 곧 사용자 도메인의 입구다 → domain/user/app - KakaoUtil (11): 카카오 응답 형식(KakaoProfile·KakaoAccount·KakaoProperties· KakaoProfileInfo·KakaoOAuthToken)이 우리 API 응답 패키지에 있었다 → external/kakao - 전역 예외 핸들러 (10): ConsentRequiredException 이 ConsentGuard 안의 중첩 클래스여서 예외를 가리키려면 서비스를 import 해야 했다 → core/exception 으로 꺼내고 동의 종류를 문자열로 담는다(열거형이면 핸들러가 다시 도메인을 알아야 한다) - CustomUserDetailsService (8): → domain/user/security, SecurityConfig 는 스프링의 UserDetailsService 인터페이스로 받는다 - JwtAuthenticationFilter (6): 필터는 토큰 검증만 필요한데 JwtService 는 로그인 응답 DTO 까지 다룬다 → AccessTokenVerifier 포트로 필요한 만큼만 끊었다 옮기는 방향이 중요했다. 셋은 core 에서 도메인으로 옮겼고(자격 증명으로 사용자를 찾는 일과 현재 사용자를 꺼내는 일은 사용자 도메인의 규칙이다), 둘은 반대로 핸들러·필터가 도메인을 보지 않도록 경계를 세웠다(토큰 서명 검증과 예외→HTTP 변환은 기반 코드의 일이고 도메인 타입을 알 필요가 없다). 카카오 DTO 가 잘못된 자리에 있던 것이 실제 문제였다. 필드명이 connected_at, thumbnail_image_url 처럼 snake_case 인 것이 카카오의 와이어 포맷이라는 증거인데, 그것이 우리가 프런트에 주는 응답을 모아 둔 domain/user/dto/response 에 섞여 있었다. 규칙도 한 줄 고쳤다. CurrentUserFacade 가 도메인으로 들어오자 "도메인은 SecurityContextHolder 를 직접 쓰지 않는다" 에 걸렸다 — 그 클래스의 존재 이유가 바로 그 접근을 혼자 하는 것인데도. 규칙의 because 가 이미 "현재 사용자는 CurrentUserFacade 로만 얻는다" 라고 적고 있으므로 파사드 자신만 예외로 뒀다. 남은 1건(CommunityService)은 계속 추적한다. 규칙 문구를 고치면 ArchUnit 이 새 키로 baseline 을 만들고 옛 파일이 고아로 남는데 자동으로 지워지지 않아 손으로 정리했다. - core 기반 → 도메인 참조 50 → 0 - ArchUnit baseline 291 → 241 (고아 baseline 1개 정리 포함) - 남은 241건 중 237건이 도메인 경계 한 규칙이다 검증: compileJava·compileTestJava 통과, ArchitectureRulesTest 통과. 전체 스위트는 다른 세션이 같은 저장소에서 테스트를 돌리는 중이라(build 디렉터리 잠금) 로컬에서 끝까지 돌리지 못했다. CI 가 깨끗한 체크아웃에서 돌린다. Refs #132 --- .../4d025d0f-f5d7-4e6b-b9f8-e044d0950c24 | 50 ---------------- .../a6b21c55-f3df-4514-8faa-c32628f81448 | 20 +++---- ...b => ef0b66c5-984e-4d22-8249-4d73a7a1a013} | 0 archunit-baseline/stored.rules | 4 +- docs/quality/architecture-rules.md | 58 +++++++++++++++++-- .../exception/ConsentRequiredException.java | 42 ++++++++++++++ ...tomizedResponseEntityExceptionHandler.java | 12 ++-- .../core/security/AccessTokenClaims.java | 11 ++++ .../core/security/AccessTokenVerifier.java | 25 ++++++++ .../security/JwtAuthenticationFilter.java | 12 ++-- .../core/security/SecurityConfig.java | 14 +++-- .../service/PolicyVerificationService.java | 2 +- .../chatbot/controller/ChatbotController.java | 2 +- .../community/service/CommunityService.java | 2 +- .../community/service/ModerationService.java | 2 +- .../controller/CareFacilityController.java | 2 +- .../service/FacilityWaitlistService.java | 2 +- .../health/controller/HealthController.java | 2 +- .../domain/health/service/ChildService.java | 2 +- .../health/service/GrowthChartService.java | 2 +- .../HealthRecordAttachmentService.java | 2 +- .../service/SiblingOverviewService.java | 2 +- .../controller/NotificationController.java | 2 +- .../NotificationLinkController.java | 2 +- .../controller/BenefitLinkController.java | 2 +- .../policy/controller/PolicyController.java | 2 +- .../service/BenefitAmountReportService.java | 2 +- .../policy/service/MissedBenefitService.java | 2 +- .../service/PolicyRecommendationService.java | 2 +- .../RegionalBenefitComparisonService.java | 2 +- .../user/app}/CurrentUserFacade.java | 2 +- .../user/controller/AuthController.java | 2 +- .../user/controller/KakaoAuthController.java | 4 +- .../user/controller/UserController.java | 2 +- .../security/CustomUserDetailsService.java | 2 +- .../domain/user/service/ConsentGuard.java | 19 +----- .../domain/user/service/JwtService.java | 19 +++++- .../domain/user/service/PrivacyService.java | 2 +- .../user/service/impl/AuthServiceImpl.java | 10 ++-- .../kakao}/KakaoAccount.java | 2 +- .../kakao}/KakaoOAuthToken.java | 2 +- .../kakao}/KakaoProfile.java | 2 +- .../kakao}/KakaoProfileInfo.java | 2 +- .../kakao}/KakaoProperties.java | 2 +- .../util => external/kakao}/KakaoUtil.java | 4 +- .../architecture/ArchitectureRulesTest.java | 4 ++ .../ChatbotControllerOwnershipTest.java | 2 +- .../CommunityServiceHiddenPostTest.java | 2 +- .../CommunityServiceOwnershipTest.java | 2 +- .../HealthRecordAttachmentServiceTest.java | 2 +- .../BenefitAmountReportServiceTest.java | 2 +- .../service/MissedBenefitServiceTest.java | 2 +- .../RegionalBenefitComparisonServiceTest.java | 2 +- .../user/app}/CurrentUserFacadeTest.java | 2 +- .../domain/user/service/ConsentGuardTest.java | 9 +-- .../impl/AuthServiceImplKakaoTest.java | 10 ++-- .../integration/SampleDataScenarioTest.java | 2 +- 57 files changed, 245 insertions(+), 158 deletions(-) rename archunit-baseline/{74b2056c-4f0e-4cb3-af35-0cfdcc359eeb => ef0b66c5-984e-4d22-8249-4d73a7a1a013} (100%) create mode 100644 src/main/java/com/carecode/core/exception/ConsentRequiredException.java create mode 100644 src/main/java/com/carecode/core/security/AccessTokenClaims.java create mode 100644 src/main/java/com/carecode/core/security/AccessTokenVerifier.java rename src/main/java/com/carecode/{core/security => domain/user/app}/CurrentUserFacade.java (99%) rename src/main/java/com/carecode/{core => domain/user}/security/CustomUserDetailsService.java (97%) rename src/main/java/com/carecode/{domain/user/dto/response => external/kakao}/KakaoAccount.java (88%) rename src/main/java/com/carecode/{domain/user/dto/response => external/kakao}/KakaoOAuthToken.java (90%) rename src/main/java/com/carecode/{domain/user/dto/response => external/kakao}/KakaoProfile.java (87%) rename src/main/java/com/carecode/{domain/user/dto/response => external/kakao}/KakaoProfileInfo.java (88%) rename src/main/java/com/carecode/{domain/user/dto/response => external/kakao}/KakaoProperties.java (86%) rename src/main/java/com/carecode/{core/util => external/kakao}/KakaoUtil.java (98%) rename src/test/java/com/carecode/{core/security => domain/user/app}/CurrentUserFacadeTest.java (99%) diff --git a/archunit-baseline/4d025d0f-f5d7-4e6b-b9f8-e044d0950c24 b/archunit-baseline/4d025d0f-f5d7-4e6b-b9f8-e044d0950c24 index 9be28508..e69de29b 100644 --- a/archunit-baseline/4d025d0f-f5d7-4e6b-b9f8-e044d0950c24 +++ b/archunit-baseline/4d025d0f-f5d7-4e6b-b9f8-e044d0950c24 @@ -1,50 +0,0 @@ -Constructor (com.carecode.domain.user.repository.UserRepository)> has parameter of type in (CurrentUserFacade.java:0) -Constructor (com.carecode.domain.user.repository.UserRepository)> has parameter of type in (CustomUserDetailsService.java:0) -Constructor (com.carecode.domain.user.service.JwtService)> has parameter of type in (JwtAuthenticationFilter.java:0) -Field has type in (CurrentUserFacade.java:0) -Field has type in (CustomUserDetailsService.java:0) -Field has type in (JwtAuthenticationFilter.java:0) -Method calls method in (CustomizedResponseEntityExceptionHandler.java:114) -Method calls method in (CustomizedResponseEntityExceptionHandler.java:115) -Method calls method in (CustomizedResponseEntityExceptionHandler.java:113) -Method calls method in (CustomizedResponseEntityExceptionHandler.java:109) -Method calls method in (CustomizedResponseEntityExceptionHandler.java:113) -Method calls method in (CustomizedResponseEntityExceptionHandler.java:114) -Method calls method in (CustomizedResponseEntityExceptionHandler.java:115) -Method calls method in (CustomizedResponseEntityExceptionHandler.java:116) -Method has annotation member of type in (CustomizedResponseEntityExceptionHandler.java:0) -Method has parameter of type in (CustomizedResponseEntityExceptionHandler.java:0) -Method calls method in (CurrentUserFacade.java:58) -Method has generic return type > with type argument depending on in (CurrentUserFacade.java:0) -Method calls method in (CurrentUserFacade.java:115) -Method calls method in (CurrentUserFacade.java:115) -Method calls method in (CurrentUserFacade.java:112) -Method has parameter of type in (CurrentUserFacade.java:0) -Method calls method in (CurrentUserFacade.java:63) -Method has return type in (CurrentUserFacade.java:0) -Method calls method in (CurrentUserFacade.java:72) -Method calls method in (CurrentUserFacade.java:68) -Method calls method in (CurrentUserFacade.java:96) -Method has return type in (CurrentUserFacade.java:0) -Method has return type in (CurrentUserFacade.java:0) -Method calls method in (CustomUserDetailsService.java:27) -Method calls method in (CustomUserDetailsService.java:30) -Method calls method in (CustomUserDetailsService.java:22) -Method calls method in (CustomUserDetailsService.java:29) -Method calls method in (CustomUserDetailsService.java:29) -Method calls method in (CustomUserDetailsService.java:20) -Method calls method in (JwtAuthenticationFilter.java:71) -Method calls method in (JwtAuthenticationFilter.java:72) -Method calls method in (JwtAuthenticationFilter.java:70) -Method calls method in (JwtAuthenticationFilter.java:64) -Method calls method in (KakaoUtil.java:119) -Method calls method in (KakaoUtil.java:127) -Method calls method in (KakaoUtil.java:147) -Method calls method in (KakaoUtil.java:151) -Method has parameter of type in (KakaoUtil.java:0) -Method has return type in (KakaoUtil.java:0) -Method references class object in (KakaoUtil.java:145) -Method calls method in (KakaoUtil.java:99) -Method calls method in (KakaoUtil.java:99) -Method has return type in (KakaoUtil.java:0) -Method references class object in (KakaoUtil.java:97) diff --git a/archunit-baseline/a6b21c55-f3df-4514-8faa-c32628f81448 b/archunit-baseline/a6b21c55-f3df-4514-8faa-c32628f81448 index 419c90dc..37d68a02 100644 --- a/archunit-baseline/a6b21c55-f3df-4514-8faa-c32628f81448 +++ b/archunit-baseline/a6b21c55-f3df-4514-8faa-c32628f81448 @@ -54,7 +54,7 @@ admin → notification 리포지토리: Method (com.carecode.domain.user.repository.UserRepository, com.carecode.domain.health.repository.HospitalRepository, com.carecode.domain.policy.repository.PolicyRepository)> has parameter of type in (AdminDashboardService.java:0) admin → policy 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.policy.repository.PolicyCategoryRepository, com.fasterxml.jackson.databind.ObjectMapper)> has parameter of type in (PolicyAdminService.java:0) admin → policy 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.policy.repository.PolicyCategoryRepository, com.fasterxml.jackson.databind.ObjectMapper)> has parameter of type in (PolicyAdminService.java:0) -admin → policy 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (PolicyVerificationService.java:0) +admin → policy 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.user.app.CurrentUserFacade)> has parameter of type in (PolicyVerificationService.java:0) admin → policy 리포지토리: Field has type in (AdminDashboardService.java:0) admin → policy 리포지토리: Field has type in (PolicyAdminService.java:0) admin → policy 리포지토리: Field has type in (PolicyAdminService.java:0) @@ -111,8 +111,8 @@ chatbot → user 리포지토리: Field calls method in (ChatbotService.java:382) chatbot → user 리포지토리: Method calls method in (ChatbotService.java:381) community → user 리포지토리: Constructor (com.carecode.domain.community.repository.PostRepository, com.carecode.domain.community.repository.TagRepository, com.carecode.domain.user.repository.UserRepository, org.springframework.security.crypto.password.PasswordEncoder)> has parameter of type in (CommunityInitializationService.java:0) -community → user 리포지토리: Constructor (com.carecode.domain.community.repository.PostRepository, com.carecode.domain.community.repository.CommentRepository, com.carecode.domain.community.repository.TagRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.community.repository.PostLikeRepository, com.carecode.domain.community.repository.BookmarkRepository, com.carecode.domain.community.mapper.CommunityMapper, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (CommunityService.java:0) -community → user 리포지토리: Constructor (com.carecode.domain.community.repository.ReportRepository, com.carecode.domain.community.repository.UserBlockRepository, com.carecode.domain.community.repository.PostRepository, com.carecode.domain.community.repository.CommentRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (ModerationService.java:0) +community → user 리포지토리: Constructor (com.carecode.domain.community.repository.PostRepository, com.carecode.domain.community.repository.CommentRepository, com.carecode.domain.community.repository.TagRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.community.repository.PostLikeRepository, com.carecode.domain.community.repository.BookmarkRepository, com.carecode.domain.community.mapper.CommunityMapper, com.carecode.domain.user.app.CurrentUserFacade)> has parameter of type in (CommunityService.java:0) +community → user 리포지토리: Constructor (com.carecode.domain.community.repository.ReportRepository, com.carecode.domain.community.repository.UserBlockRepository, com.carecode.domain.community.repository.PostRepository, com.carecode.domain.community.repository.CommentRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.app.CurrentUserFacade)> has parameter of type in (ModerationService.java:0) community → user 리포지토리: Field has type in (CommunityInitializationService.java:0) community → user 리포지토리: Field has type in (CommunityService.java:0) community → user 리포지토리: Field has type in (ModerationService.java:0) @@ -137,13 +137,13 @@ facility → user 리포지토리: Method calls method in (CareFacilityService.java:390) facility → user 리포지토리: Method calls method in (CareFacilityService.java:380) health → facility 리포지토리: Constructor (com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.health.service.HealthActorResolver, com.carecode.domain.user.app.ChildDirectory, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.facility.repository.CareFacilityRepository)> has parameter of type in (HealthInsightService.java:0) -health → facility 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.facility.repository.FacilityWaitlistRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (SiblingOverviewService.java:0) +health → facility 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.facility.repository.FacilityWaitlistRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.user.app.CurrentUserFacade)> has parameter of type in (SiblingOverviewService.java:0) health → facility 리포지토리: Field has type in (HealthInsightService.java:0) health → facility 리포지토리: Field has type in (SiblingOverviewService.java:0) health → facility 리포지토리: Method calls method in (HealthInsightService.java:113) health → facility 리포지토리: Method calls method in (SiblingOverviewService.java:91) health → policy 리포지토리: Constructor (com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.health.service.HealthActorResolver, com.carecode.domain.user.app.ChildDirectory, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.facility.repository.CareFacilityRepository)> has parameter of type in (HealthInsightService.java:0) -health → policy 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.facility.repository.FacilityWaitlistRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (SiblingOverviewService.java:0) +health → policy 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.facility.repository.FacilityWaitlistRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.user.app.CurrentUserFacade)> has parameter of type in (SiblingOverviewService.java:0) health → policy 리포지토리: Constructor (com.carecode.domain.health.service.ChildService, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.policy.repository.PolicyRepository)> has parameter of type in (ChildTimelineService.java:0) health → policy 리포지토리: Field has type in (HealthInsightService.java:0) health → policy 리포지토리: Field has type in (SiblingOverviewService.java:0) @@ -152,11 +152,11 @@ health → policy 리포지토리: Method calls method in (SiblingOverviewService.java:101) health → policy 리포지토리: Method calls method in (ChildTimelineService.java:180) health → user 리포지토리: Constructor (com.carecode.domain.health.service.HealthService, com.carecode.domain.health.service.HealthInsightService, com.carecode.domain.health.repository.HospitalRepository, com.carecode.core.ops.sync.SyncFreshnessService, com.carecode.domain.health.repository.HospitalLikeRepository, com.carecode.domain.health.repository.HospitalReviewRepository, com.carecode.domain.health.mapper.HospitalMapper, com.carecode.domain.health.mapper.HospitalReviewMapper, com.carecode.domain.user.repository.UserRepository)> has parameter of type in (HealthFacade.java:0) -health → user 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.user.app.ChildDirectory, com.carecode.domain.health.mapper.ChildMapper, com.carecode.core.security.CurrentUserFacade, com.carecode.domain.health.service.VaccinationScheduleService, com.carecode.core.analytics.EventLogger)> has parameter of type in (ChildService.java:0) -health → user 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.user.app.ChildDirectory, com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (GrowthChartService.java:0) +health → user 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.user.app.ChildDirectory, com.carecode.domain.health.mapper.ChildMapper, com.carecode.domain.user.app.CurrentUserFacade, com.carecode.domain.health.service.VaccinationScheduleService, com.carecode.core.analytics.EventLogger)> has parameter of type in (ChildService.java:0) +health → user 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.user.app.ChildDirectory, com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.user.app.CurrentUserFacade)> has parameter of type in (GrowthChartService.java:0) health → user 리포지토리: Constructor (com.carecode.domain.user.repository.UserRepository)> has parameter of type in (HealthActorResolver.java:0) health → user 리포지토리: Constructor (com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.user.service.ConsentGuard, com.carecode.domain.health.repository.HealthRecordAttachmentRepository, com.carecode.domain.health.service.HealthActorResolver, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.user.app.ChildDirectory, com.carecode.domain.health.mapper.HealthRecordMapper)> has parameter of type in (HealthService.java:0) -health → user 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.facility.repository.FacilityWaitlistRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (SiblingOverviewService.java:0) +health → user 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.facility.repository.FacilityWaitlistRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.user.app.CurrentUserFacade)> has parameter of type in (SiblingOverviewService.java:0) health → user 리포지토리: Constructor (com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.user.repository.ChildRepository)> has parameter of type in (VaccinationScheduleService.java:0) health → user 리포지토리: Field has type in (HealthFacade.java:0) health → user 리포지토리: Field has type in (ChildService.java:0) @@ -229,9 +229,9 @@ policy → user 리포지토리: Method calls method in (PolicyDeadlineNotifier.java:101) policy → user 리포지토리: Method calls method in (PolicyService.java:264) policy → user 리포지토리: Method calls method in (PolicyService.java:263) -user → community 리포지토리: Constructor (com.carecode.domain.user.repository.UserConsentRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.community.repository.PostRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (PrivacyService.java:0) +user → community 리포지토리: Constructor (com.carecode.domain.user.repository.UserConsentRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.community.repository.PostRepository, com.carecode.domain.user.app.CurrentUserFacade)> has parameter of type in (PrivacyService.java:0) user → community 리포지토리: Field has type in (PrivacyService.java:0) user → community 리포지토리: Method calls method in (PrivacyService.java:109) -user → health 리포지토리: Constructor (com.carecode.domain.user.repository.UserConsentRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.community.repository.PostRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (PrivacyService.java:0) +user → health 리포지토리: Constructor (com.carecode.domain.user.repository.UserConsentRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.community.repository.PostRepository, com.carecode.domain.user.app.CurrentUserFacade)> has parameter of type in (PrivacyService.java:0) user → health 리포지토리: Field has type in (PrivacyService.java:0) user → health 리포지토리: Method calls method in (PrivacyService.java:108) diff --git a/archunit-baseline/74b2056c-4f0e-4cb3-af35-0cfdcc359eeb b/archunit-baseline/ef0b66c5-984e-4d22-8249-4d73a7a1a013 similarity index 100% rename from archunit-baseline/74b2056c-4f0e-4cb3-af35-0cfdcc359eeb rename to archunit-baseline/ef0b66c5-984e-4d22-8249-4d73a7a1a013 diff --git a/archunit-baseline/stored.rules b/archunit-baseline/stored.rules index 4e9db0e5..b9c699ab 100644 --- a/archunit-baseline/stored.rules +++ b/archunit-baseline/stored.rules @@ -1,11 +1,11 @@ # -#Mon Oct 05 01:50:57 KST 2026 +#Thu Oct 22 17:32:46 KST 2026 no\ classes\ that\ reside\ in\ a\ package\ '..entity..'\ should\ depend\ on\ classes\ that\ reside\ in\ any\ package\ ['..dto..',\ '..controller..'],\ because\ API\ \uBAA8\uC591\uC774\ \uBC14\uB014\ \uB54C\ \uD14C\uC774\uBE14\ \uB9E4\uD551\uC774\ \uD568\uAED8\ \uD754\uB4E4\uB9AC\uBA74\ \uC548\ \uB41C\uB2E4=87360be1-7d6d-4423-b366-16fa4fe6f6b6 +no\ classes\ that\ reside\ in\ a\ package\ 'com.carecode.domain..'\ and\ do\ not\ have\ fully\ qualified\ name\ 'com.carecode.domain.user.app.CurrentUserFacade'\ should\ depend\ on\ classes\ that\ have\ fully\ qualified\ name\ 'org.springframework.security.core.context.SecurityContextHolder',\ because\ \uD604\uC7AC\ \uC0AC\uC6A9\uC790\uB294\ CurrentUserFacade\ \uB85C\uB9CC\ \uC5BB\uB294\uB2E4=ef0b66c5-984e-4d22-8249-4d73a7a1a013 no\ classes\ that\ reside\ in\ a\ package\ 'com.carecode.core..'\ should\ be\ annotated\ with\ @RestController\ or\ should\ be\ annotated\ with\ @Controller,\ because\ API\ \uB294\ \uB3C4\uBA54\uC778\ \uD328\uD0A4\uC9C0\uC5D0\ \uB454\uB2E4.\ core\ \uC5D0\ \uC788\uC73C\uBA74\ \uC18C\uC18D\uC744\ \uC54C\ \uC218\ \uC5C6\uB2E4=ab7a1eba-4dd5-499e-95ed-0c71f0b6f61f classes\ that\ reside\ in\ a\ package\ 'com.carecode.domain..'\ should\ \uC18C\uBB38\uC790\ \uB3C4\uBA54\uC778\ \uD328\uD0A4\uC9C0\uC5D0\ \uC788\uB2E4,\ because\ \uD55C\ \uAD70\uB370\uB9CC\ \uB2E4\uB978\ \uADDC\uCE59\uC774\uBA74\ \uC0C8\uB85C\ \uB4E4\uC5B4\uC628\ \uC0AC\uB78C\uC774\ \uB458\ \uB2E4\ \uD749\uB0B4\ \uB0B8\uB2E4=05a7a88e-9a95-451a-b96f-4a8eb9be6c1e classes\ that\ reside\ in\ a\ package\ 'com.carecode.domain..'\ should\ \uB2E4\uB978\ \uB3C4\uBA54\uC778\uC758\ \uB9AC\uD3EC\uC9C0\uD1A0\uB9AC\uB97C\ \uC9C1\uC811\ \uCC38\uC870\uD558\uC9C0\ \uC54A\uB294\uB2E4,\ because\ \uB0A8\uC758\ \uB3C4\uBA54\uC778\ \uD14C\uC774\uBE14\uC744\ \uC9C1\uC811\ \uC77D\uC73C\uBA74\ \uADF8\ \uB3C4\uBA54\uC778\uC774\ \uC9C0\uD0A4\uB294\ \uADDC\uCE59(\uC18C\uC720\uAD8C\ \uAC80\uC99D\ \uB4F1)\uC744\ \uAC74\uB108\uB6F4\uB2E4=a6b21c55-f3df-4514-8faa-c32628f81448 no\ classes\ that\ reside\ in\ any\ package\ ['com.carecode.core.security..',\ 'com.carecode.core.web..',\ 'com.carecode.core.handler..',\ 'com.carecode.core.config..',\ 'com.carecode.core.storage..',\ 'com.carecode.core.util..',\ 'com.carecode.core.monitoring..',\ 'com.carecode.core.aspect..',\ 'com.carecode.core.annotation..',\ 'com.carecode.core.constants..',\ 'com.carecode.core.components..',\ 'com.carecode.core.ops..']\ should\ depend\ on\ classes\ that\ reside\ in\ a\ package\ 'com.carecode.domain..',\ because\ \uAE30\uBC18\ \uCF54\uB4DC\uAC00\ \uD2B9\uC815\ \uB3C4\uBA54\uC778\uC744\ \uC54C\uBA74\ \uADF8\ \uB3C4\uBA54\uC778\uC758\ \uC77C\uBD80\uC9C0,\ \uACF5\uD1B5\uC774\ \uC544\uB2C8\uB2E4=4d025d0f-f5d7-4e6b-b9f8-e044d0950c24 no\ classes\ that\ have\ simple\ name\ ending\ with\ 'Mapper'\ should\ depend\ on\ classes\ that\ have\ simple\ name\ ending\ with\ 'Repository',\ because\ \uBCC0\uD658\ \uD55C\ \uBC88\uC5D0\ \uCFFC\uB9AC\uAC00\ \uC228\uC5B4\ \uB4E4\uBA74\ N+1\ \uC744\ \uCD94\uC801\uD560\ \uC218\ \uC5C6\uB2E4=fb482b80-693f-4971-aa10-71da548d267d -no\ classes\ that\ reside\ in\ a\ package\ 'com.carecode.domain..'\ should\ depend\ on\ classes\ that\ have\ fully\ qualified\ name\ 'org.springframework.security.core.context.SecurityContextHolder',\ because\ \uD604\uC7AC\ \uC0AC\uC6A9\uC790\uB294\ CurrentUserFacade\ \uB85C\uB9CC\ \uC5BB\uB294\uB2E4=74b2056c-4f0e-4cb3-af35-0cfdcc359eeb no\ classes\ that\ have\ simple\ name\ ending\ with\ 'Controller'\ should\ depend\ on\ classes\ that\ have\ simple\ name\ ending\ with\ 'Repository',\ because\ \uAC80\uC99D\uACFC\ \uD2B8\uB79C\uC7AD\uC158\ \uACBD\uACC4\uB294\ \uC11C\uBE44\uC2A4\uC5D0\ \uB454\uB2E4=05a5820d-39ea-4db6-b8e7-f737ac6a90df no\ classes\ that\ reside\ in\ any\ package\ ['..service..',\ '..repository..']\ should\ depend\ on\ classes\ that\ reside\ in\ a\ package\ '..controller..',\ because\ \uD638\uCD9C\ \uBC29\uD5A5\uC740\ \uCEE8\uD2B8\uB864\uB7EC\ \u2192\ \uC11C\uBE44\uC2A4\ \u2192\ \uB9AC\uD3EC\uC9C0\uD1A0\uB9AC\ \uD55C\ \uCABD\uC774\uB2E4=d1434e17-b3a6-4ae9-a8f3-86c5e4bd1c05 diff --git a/docs/quality/architecture-rules.md b/docs/quality/architecture-rules.md index 6dda8640..c6c4d758 100644 --- a/docs/quality/architecture-rules.md +++ b/docs/quality/architecture-rules.md @@ -31,8 +31,8 @@ ArchUnit 의 `FreezingArchRule` 을 씁니다. ## 규칙과 현재 위반 -진척: **489건(0단계) → 391건(1b) → 364건(4a) → 357건(5b) → 331건(6) → 291건(7단계)**. 남은 것은 -도메인 경계(237)와 core 기반의 도메인 참조(50)입니다. **컨트롤러의 리포지토리 직접 사용은 0 이 됐습니다.** +진척: **489건(0단계) → 364건(4a) → 331건(6) → 291건(7) → 241건(8단계)**. 남은 것은 거의 전부 +도메인 경계(237)입니다. **컨트롤러의 리포지토리 직접 사용과 core 기반의 도메인 참조는 0 이 됐습니다.** > 세는 법: `archunit-baseline/` 에는 규칙별 위반 파일 외에 색인 파일 `stored.rules` 도 있습니다. > `cat archunit-baseline/* | wc -l` 은 색인 11줄을 함께 세므로 실제 위반보다 11 크게 나옵니다. @@ -42,10 +42,10 @@ ArchUnit 의 `FreezingArchRule` 을 씁니다. | 다른 도메인의 리포지토리를 직접 쓰지 않는다 | 237 | 남의 테이블을 직접 읽으면 그 도메인의 규칙(소유권 검증 등)을 건너뛴다 | | 도메인 패키지 이름은 소문자다 | **0** | `careFacility`→`facility` 로 해소 (1b 단계) | | 컨트롤러는 리포지토리를 직접 쓰지 않는다 | **0** | 7단계에서 해소 | 검증·트랜잭션 경계가 컨트롤러로 샌다 | -| core 기반 패키지는 domain 을 의존하지 않는다 | 50 | 기반 코드가 도메인을 알면 공통이 아니다 | +| core 기반 패키지는 domain 을 의존하지 않는다 | **0** | 8단계에서 해소 | | 매퍼는 리포지토리를 쓰지 않는다 | 3 | 변환 한 번에 숨은 쿼리가 생기면 N+1 을 추적할 수 없다 | | core 에는 컨트롤러가 없다 | **0** | 1a 에서 도메인으로 옮기고, 3a 에서 중복이라 삭제 | -| 도메인은 SecurityContextHolder 를 직접 쓰지 않는다 | 1 | principal 모양이 바뀌면 고칠 곳이 흩어진다(시설 API 500 의 원인) | +| 도메인은 SecurityContextHolder 를 직접 쓰지 않는다 | 1 (파사드 제외) | principal 모양이 바뀌면 고칠 곳이 흩어진다(시설 API 500 의 원인) | | 서비스·리포지토리는 컨트롤러를 의존하지 않는다 | **0** | 층 방향은 이미 지켜지고 있다 | | 엔티티는 DTO·컨트롤러를 의존하지 않는다 | **0** | 매핑 경계는 지켜지고 있다 | @@ -526,6 +526,53 @@ not to be equal to: "d14eb045@example.com" - 전체 테스트 **647개, 실패 0** - OpenAPI 경로·스키마 수 변화 없음(226 / 158) +## 기반 코드가 사용자 도메인을 알던 자리 (8단계) + +"core 기반 패키지는 domain 을 의존하지 않는다" 위반 50건은 **전부 인증·인가 기반 코드**였습니다. +다섯 덩어리였고, 각각 원인이 달랐습니다. + +| 어디 | 무엇을 알고 있었나 | 어떻게 끊었나 | +|------|---------------------|---------------| +| `CurrentUserFacade` (15) | `User`, `UserRepository` | **이 클래스가 곧 사용자 도메인의 입구**다. `domain/user/app` 으로 옮겼다 | +| `KakaoUtil` (11) | `KakaoOAuthToken`, `KakaoProfile` … | 카카오 **응답 형식**이 우리 API 응답 패키지(`user/dto/response`)에 있었다. 클라이언트와 DTO 6개를 `external/kakao` 로 옮겼다 | +| 전역 예외 핸들러 (10) | `ConsentGuard` | `ConsentRequiredException` 이 **서비스 안의 중첩 클래스**여서, 예외를 가리키려면 서비스를 import 해야 했다 | +| `CustomUserDetailsService` (8) | `User`, `UserRepository` | `domain/user/security` 로 옮기고, `SecurityConfig` 는 스프링의 `UserDetailsService` **인터페이스**로 받는다 | +| `JwtAuthenticationFilter` (6) | `JwtService` | 필터는 토큰 검증만 필요한데 그 서비스는 로그인 응답 DTO 까지 다룬다. `AccessTokenVerifier` 포트로 필요한 만큼만 끊었다 | + +### 옮기는 방향이 중요했다 + +세 건은 **core 에서 도메인으로** 옮겼습니다(`CurrentUserFacade`, `CustomUserDetailsService`, +카카오 DTO). 나머지 둘은 **반대로** 핸들러·필터가 도메인을 보지 않도록 경계를 세웠습니다 +(`core/exception` 의 예외, `core/security` 의 포트). + +기준은 "누가 그 규칙의 주인인가" 였습니다. 자격 증명으로 사용자를 찾는 일과 현재 사용자를 +꺼내는 일은 사용자 도메인의 규칙입니다. 반대로 토큰 서명 검증과 예외 → HTTP 상태 변환은 +기반 코드의 일이고, 그쪽이 도메인 타입을 알 필요가 없습니다. + +### 규칙도 한 줄 고쳤다 + +`CurrentUserFacade` 가 도메인으로 들어오자 "도메인은 `SecurityContextHolder` 를 직접 쓰지 +않는다" 에 걸렸습니다 — 그 클래스의 존재 이유가 바로 그 접근을 혼자 하는 것인데도요. +규칙의 `because` 가 이미 "현재 사용자는 CurrentUserFacade 로만 얻는다" 라고 적고 있으므로 +**파사드 자신만 예외**로 두었습니다. 남은 1건(`CommunityService` 가 직접 꺼내는 곳)은 그대로 +추적합니다. + +> 규칙 문구를 고치면 ArchUnit 이 새 키로 baseline 을 만들고 **옛 파일은 고아로 남습니다.** +> 자동으로 지워지지 않아 손으로 정리했습니다(`stored.rules` 의 줄 + 해당 파일). + +### 카카오 DTO 가 잘못된 자리에 있었던 것이 실제 문제였다 + +`KakaoProfile`·`KakaoAccount`·`KakaoProperties`·`KakaoProfileInfo`·`KakaoOAuthToken` 은 +**카카오가 주는 JSON 모양**입니다(필드명이 `connected_at`, `thumbnail_image_url` 처럼 snake_case +인 것이 그 증거). 그것이 `domain/user/dto/response` — 우리가 **프런트에 주는** 응답을 모아 둔 +패키지 — 에 섞여 있었습니다. 카카오가 응답을 바꾸면 우리 API 응답 패키지가 바뀌는 셈입니다. + +### 숫자 + +- **core 기반 → 도메인 참조 50 → 0** +- ArchUnit baseline 291 → **241** (고아 baseline 1개 정리 포함) +- 남은 241건 중 **237건이 도메인 경계** 한 규칙입니다 + ## 다음 단계 | 단계 | 내용 | 효과 | @@ -543,4 +590,5 @@ not to be equal to: "d14eb045@example.com" | ~~6. 어드민 컨트롤러의 리포지토리 사용~~ | ~~61건 전부가 어드민 컨트롤러에 있었다~~ | **완료** — 61 → 37, 버그 둘 | | 7. 식별 로직을 user 로 | `HealthActorResolver` 가 health 의 마지막 `UserRepository` 사용처다 | 결합 추가 해소 | | ~~7. 남은 컨트롤러 6개~~ | ~~대시보드·사용자·정책·검증·링크 2개~~ | **완료** — 37 → **0**, 버그 둘 | -| 8. 어드민 조회용 포트 | 도메인 경계 237건 중 어드민 몫. 각 도메인이 어드민 조회 입구를 내주면 사라진다 | 경계 완성 | +| ~~8. core 기반의 도메인 참조~~ | ~~인증·인가 기반 코드가 user 도메인을 알고 있었다~~ | **완료** — 50 → **0** | +| 9. 어드민 조회용 포트 | 도메인 경계 237건 중 어드민 몫. 각 도메인이 어드민 조회 입구를 내주면 사라진다 | 경계 완성 | diff --git a/src/main/java/com/carecode/core/exception/ConsentRequiredException.java b/src/main/java/com/carecode/core/exception/ConsentRequiredException.java new file mode 100644 index 00000000..cfce9ee5 --- /dev/null +++ b/src/main/java/com/carecode/core/exception/ConsentRequiredException.java @@ -0,0 +1,42 @@ +package com.carecode.core.exception; + +/** + * 동의가 없어 막혔음을 클라이언트가 구분할 수 있게 하는 예외. + * + *

전역 예외 핸들러가 이것을 잡아 "어떤 동의가 필요한가" 를 응답에 담는다. 그래서 동의 종류를 + * 문자열로 들고 있다 — 열거형을 들고 있으면 {@code core/exception} 과 핸들러가 모두 + * 사용자 도메인의 {@code ConsentType} 을 알아야 한다. 응답은 JSON 이라 어차피 문자열이고, + * 종류를 추가하는 일은 도메인 쪽에서만 일어난다. + * + *

한때 이 예외는 {@code ConsentGuard} 안의 중첩 클래스였고, 그래서 웹 계층이 사용자 + * 도메인의 서비스를 import 해야 예외를 가리킬 수 있었다. + * + * @see com.carecode.domain.user.service.ConsentGuard + */ +public class ConsentRequiredException extends CareServiceException { + + private final String consentType; + private final String displayName; + private final boolean sensitive; + + public ConsentRequiredException(String consentType, String displayName, boolean sensitive) { + super(displayName + " 동의가 필요합니다."); + this.consentType = consentType; + this.displayName = displayName; + this.sensitive = sensitive; + } + + /** 동의 종류 코드. 클라이언트가 어떤 동의 화면을 띄울지 고르는 값이다. */ + public String getConsentType() { + return consentType; + } + + public String getDisplayName() { + return displayName; + } + + /** 민감정보 동의인지. 화면이 안내 문구를 다르게 띄운다. */ + public boolean isSensitive() { + return sensitive; + } +} diff --git a/src/main/java/com/carecode/core/handler/CustomizedResponseEntityExceptionHandler.java b/src/main/java/com/carecode/core/handler/CustomizedResponseEntityExceptionHandler.java index 106a386d..4ee28919 100644 --- a/src/main/java/com/carecode/core/handler/CustomizedResponseEntityExceptionHandler.java +++ b/src/main/java/com/carecode/core/handler/CustomizedResponseEntityExceptionHandler.java @@ -2,7 +2,7 @@ import com.carecode.core.exception.*; import com.carecode.core.ops.OperationalAlerter; -import com.carecode.domain.user.service.ConsentGuard; +import com.carecode.core.exception.ConsentRequiredException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -103,16 +103,16 @@ public ResponseEntity handleBusinessException(BusinessException e } /** 동의가 없어 막힌 경우. 클라이언트가 어떤 동의를 받아야 하는지 알아야 화면을 띄울 수 있다. */ - @ExceptionHandler(ConsentGuard.ConsentRequiredException.class) + @ExceptionHandler(ConsentRequiredException.class) public ResponseEntity> handleConsentRequired( - ConsentGuard.ConsentRequiredException ex, WebRequest request) { + ConsentRequiredException ex, WebRequest request) { log.info("동의 미완료로 접근 차단: {}", ex.getConsentType()); Map body = new LinkedHashMap<>(); body.put("error", "CONSENT_REQUIRED"); - body.put("consentType", ex.getConsentType().name()); - body.put("displayName", ex.getConsentType().getDisplayName()); - body.put("sensitive", ex.getConsentType().isSensitive()); + body.put("consentType", ex.getConsentType()); + body.put("displayName", ex.getDisplayName()); + body.put("sensitive", ex.isSensitive()); body.put("message", ex.getMessage()); body.put("path", request.getDescription(false)); diff --git a/src/main/java/com/carecode/core/security/AccessTokenClaims.java b/src/main/java/com/carecode/core/security/AccessTokenClaims.java new file mode 100644 index 00000000..a8649c17 --- /dev/null +++ b/src/main/java/com/carecode/core/security/AccessTokenClaims.java @@ -0,0 +1,11 @@ +package com.carecode.core.security; + +/** + * 액세스 토큰에서 꺼낸, 인증 필터가 쓰는 값들. + * + * @param userId 가입 시 발급한 사용자 코드 + * @param email principal 로 쓰는 값 + * @param role 권한 문자열 (ROLE_ 접두사 없음) + */ +public record AccessTokenClaims(String userId, String email, String role) { +} diff --git a/src/main/java/com/carecode/core/security/AccessTokenVerifier.java b/src/main/java/com/carecode/core/security/AccessTokenVerifier.java new file mode 100644 index 00000000..c9e6bc38 --- /dev/null +++ b/src/main/java/com/carecode/core/security/AccessTokenVerifier.java @@ -0,0 +1,25 @@ +package com.carecode.core.security; + +import java.util.Optional; + +/** + * 인증 필터가 토큰을 확인하는 통로. + * + *

필터는 토큰이 유효한지와 거기서 꺼낸 몇 개의 값만 필요하다. 그런데 구현 + * ({@code JwtService})은 사용자 도메인에 있고 로그인 응답 DTO({@code TokenDto} 등)까지 + * 다루므로, 필터가 그 클래스를 직접 쓰면 보안 기반 코드가 사용자 도메인의 API 응답 모양을 + * 알게 된다. 필요한 만큼만 이 인터페이스로 끊는다. + * + *

토큰 발급은 여기 없다. 발급은 로그인 흐름의 일부이고 응답 DTO 를 만들어야 하므로 + * 도메인에 남는다. + */ +public interface AccessTokenVerifier { + + /** + * 유효한 액세스 토큰이면 담긴 값을 돌려준다. + * + *

만료·서명 불일치·토큰 종류 불일치는 모두 {@link Optional#empty()} 다. 필터는 그 셋을 + * 구분할 필요가 없고, 구분해서 응답에 담으면 공격자에게 단서를 준다. + */ + Optional verifyAccessToken(String token); +} diff --git a/src/main/java/com/carecode/core/security/JwtAuthenticationFilter.java b/src/main/java/com/carecode/core/security/JwtAuthenticationFilter.java index 544c7b67..a60d4a10 100644 --- a/src/main/java/com/carecode/core/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/carecode/core/security/JwtAuthenticationFilter.java @@ -1,6 +1,5 @@ package com.carecode.core.security; -import com.carecode.domain.user.service.JwtService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -23,7 +22,7 @@ @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final JwtService jwtService; + private final AccessTokenVerifier tokenVerifier; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -61,15 +60,16 @@ private void authenticateIfTokenPresent(HttpServletRequest request) { } // Access Token 만 허용한다. Refresh Token 으로는 API 인증이 되지 않아야 한다. - if (!jwtService.validateAccessToken(token)) { + AccessTokenClaims claims = tokenVerifier.verifyAccessToken(token).orElse(null); + if (claims == null) { log.debug("JWT 토큰이 유효하지 않음"); SecurityContextHolder.clearContext(); return; } - String userId = jwtService.getUserIdFromToken(token); - String email = jwtService.getEmailFromToken(token); - String role = jwtService.getRoleFromToken(token); + String userId = claims.userId(); + String email = claims.email(); + String role = claims.role(); if (!StringUtils.hasText(email) || !StringUtils.hasText(role)) { // role 이 없으면 "ROLE_null" 권한으로 인증되던 문제를 차단한다. diff --git a/src/main/java/com/carecode/core/security/SecurityConfig.java b/src/main/java/com/carecode/core/security/SecurityConfig.java index a4726b2e..cc317cd8 100644 --- a/src/main/java/com/carecode/core/security/SecurityConfig.java +++ b/src/main/java/com/carecode/core/security/SecurityConfig.java @@ -1,6 +1,7 @@ package com.carecode.core.security; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; @@ -30,16 +31,21 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; - private final CustomUserDetailsService customUserDetailsService; + /** + * 스프링 시큐리티 인터페이스로 받는다. 구현({@code CustomUserDetailsService})은 사용자 + * 도메인에 있다 — 자격 증명으로 사용자를 찾는 일은 그 도메인의 규칙이다. 여기서 구현 + * 클래스를 직접 가리키면 보안 설정이 특정 도메인을 알게 된다. + */ + private final UserDetailsService userDetailsService; private final List allowedOrigins; private final Environment environment; public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, - CustomUserDetailsService customUserDetailsService, + UserDetailsService userDetailsService, Environment environment, @Value("${app.security.cors.allowed-origins:http://localhost:3000,http://127.0.0.1:3000}") String allowedOrigins) { this.jwtAuthenticationFilter = jwtAuthenticationFilter; - this.customUserDetailsService = customUserDetailsService; + this.userDetailsService = userDetailsService; this.environment = environment; this.allowedOrigins = Arrays.stream(allowedOrigins.split(",")) .map(String::trim) @@ -64,7 +70,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) - .userDetailsService(customUserDetailsService) + .userDetailsService(userDetailsService) .exceptionHandling(exceptions -> exceptions .authenticationEntryPoint((request, response, authException) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); diff --git a/src/main/java/com/carecode/domain/admin/service/PolicyVerificationService.java b/src/main/java/com/carecode/domain/admin/service/PolicyVerificationService.java index 18ba2fb4..ce15acd5 100644 --- a/src/main/java/com/carecode/domain/admin/service/PolicyVerificationService.java +++ b/src/main/java/com/carecode/domain/admin/service/PolicyVerificationService.java @@ -1,7 +1,7 @@ package com.carecode.domain.admin.service; import com.carecode.core.exception.ResourceNotFoundException; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.admin.dto.response.PolicyVerificationResponse; import com.carecode.domain.admin.dto.response.RegionVerificationStatusResponse; import com.carecode.domain.policy.entity.Policy; diff --git a/src/main/java/com/carecode/domain/chatbot/controller/ChatbotController.java b/src/main/java/com/carecode/domain/chatbot/controller/ChatbotController.java index 97399deb..eccf0254 100644 --- a/src/main/java/com/carecode/domain/chatbot/controller/ChatbotController.java +++ b/src/main/java/com/carecode/domain/chatbot/controller/ChatbotController.java @@ -3,7 +3,7 @@ import com.carecode.core.annotation.LogExecutionTime; import com.carecode.core.annotation.RateLimit; import com.carecode.core.web.BaseController; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.core.exception.CareServiceException; import com.carecode.domain.chatbot.dto.request.ChatbotMessageRequest; import com.carecode.domain.chatbot.dto.response.ChatbotMessageResponse; diff --git a/src/main/java/com/carecode/domain/community/service/CommunityService.java b/src/main/java/com/carecode/domain/community/service/CommunityService.java index bc55df08..1bf4e099 100644 --- a/src/main/java/com/carecode/domain/community/service/CommunityService.java +++ b/src/main/java/com/carecode/domain/community/service/CommunityService.java @@ -4,7 +4,7 @@ import com.carecode.core.exception.CommentAccessDeniedException; import com.carecode.core.exception.PostAccessDeniedException; import com.carecode.core.exception.ResourceNotFoundException; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.community.dto.request.CommunityCreatePostRequest; import com.carecode.domain.community.dto.request.CommunityUpdatePostRequest; import com.carecode.domain.community.dto.request.CommunityCreateCommentRequest; diff --git a/src/main/java/com/carecode/domain/community/service/ModerationService.java b/src/main/java/com/carecode/domain/community/service/ModerationService.java index da1a40c1..626bd6e6 100644 --- a/src/main/java/com/carecode/domain/community/service/ModerationService.java +++ b/src/main/java/com/carecode/domain/community/service/ModerationService.java @@ -3,7 +3,7 @@ import com.carecode.core.exception.BusinessException; import com.carecode.core.exception.ErrorCode; import com.carecode.core.exception.ResourceNotFoundException; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.community.dto.request.ReportCreateRequest; import com.carecode.domain.community.dto.response.ReportResponse; import com.carecode.domain.community.entity.Comment; diff --git a/src/main/java/com/carecode/domain/facility/controller/CareFacilityController.java b/src/main/java/com/carecode/domain/facility/controller/CareFacilityController.java index 9f7dd6b2..6b19dfd7 100644 --- a/src/main/java/com/carecode/domain/facility/controller/CareFacilityController.java +++ b/src/main/java/com/carecode/domain/facility/controller/CareFacilityController.java @@ -4,7 +4,7 @@ import com.carecode.core.annotation.ValidateLocation; import com.carecode.core.annotation.ValidateChildAge; import com.carecode.core.web.BaseController; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.core.util.PageRequestUtil; import com.carecode.domain.facility.dto.request.CareFacilitySearchRequest; import com.carecode.domain.facility.dto.request.CareFacilityAdvancedSearchRequest; diff --git a/src/main/java/com/carecode/domain/facility/service/FacilityWaitlistService.java b/src/main/java/com/carecode/domain/facility/service/FacilityWaitlistService.java index f8ca2d25..75ff9b20 100644 --- a/src/main/java/com/carecode/domain/facility/service/FacilityWaitlistService.java +++ b/src/main/java/com/carecode/domain/facility/service/FacilityWaitlistService.java @@ -5,7 +5,7 @@ import com.carecode.core.exception.BusinessException; import com.carecode.core.exception.ErrorCode; import com.carecode.core.exception.ResourceNotFoundException; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.facility.dto.request.WaitlistRequest; import com.carecode.domain.facility.dto.response.WaitlistStatsResponse; import com.carecode.domain.facility.entity.CareFacility; diff --git a/src/main/java/com/carecode/domain/health/controller/HealthController.java b/src/main/java/com/carecode/domain/health/controller/HealthController.java index d7c80b26..38eca306 100644 --- a/src/main/java/com/carecode/domain/health/controller/HealthController.java +++ b/src/main/java/com/carecode/domain/health/controller/HealthController.java @@ -3,7 +3,7 @@ import com.carecode.core.annotation.LogExecutionTime; import com.carecode.core.web.BaseController; import com.carecode.core.util.PageRequestUtil; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.health.dto.request.HealthCreateHealthRecordRequest; import com.carecode.domain.health.dto.request.HealthRecordAttachmentRequest; import com.carecode.domain.health.dto.request.HealthUpdateHealthRecordRequest; diff --git a/src/main/java/com/carecode/domain/health/service/ChildService.java b/src/main/java/com/carecode/domain/health/service/ChildService.java index 69a6ff2e..05a4dade 100644 --- a/src/main/java/com/carecode/domain/health/service/ChildService.java +++ b/src/main/java/com/carecode/domain/health/service/ChildService.java @@ -3,7 +3,7 @@ import com.carecode.core.analytics.EventLogger; import com.carecode.core.analytics.EventType; import com.carecode.core.exception.ChildNotFoundException; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.health.dto.request.ChildCreateRequest; import com.carecode.domain.health.dto.response.ChildInfoResponse; import com.carecode.domain.health.mapper.ChildMapper; diff --git a/src/main/java/com/carecode/domain/health/service/GrowthChartService.java b/src/main/java/com/carecode/domain/health/service/GrowthChartService.java index 4d5c2599..886406b5 100644 --- a/src/main/java/com/carecode/domain/health/service/GrowthChartService.java +++ b/src/main/java/com/carecode/domain/health/service/GrowthChartService.java @@ -1,7 +1,7 @@ package com.carecode.domain.health.service; import com.carecode.core.exception.ChildNotFoundException; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.health.dto.response.GrowthPointResponse; import com.carecode.domain.health.entity.HealthRecord; import com.carecode.domain.health.growth.GrowthMetric; diff --git a/src/main/java/com/carecode/domain/health/service/HealthRecordAttachmentService.java b/src/main/java/com/carecode/domain/health/service/HealthRecordAttachmentService.java index e3d9b57d..37f34ab3 100644 --- a/src/main/java/com/carecode/domain/health/service/HealthRecordAttachmentService.java +++ b/src/main/java/com/carecode/domain/health/service/HealthRecordAttachmentService.java @@ -2,7 +2,7 @@ import com.carecode.core.exception.HealthRecordNotFoundException; import com.carecode.core.exception.ResourceNotFoundException; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.core.storage.FileStorageService; import com.carecode.domain.health.dto.response.AttachmentDownload; import com.carecode.core.storage.StoredFile; diff --git a/src/main/java/com/carecode/domain/health/service/SiblingOverviewService.java b/src/main/java/com/carecode/domain/health/service/SiblingOverviewService.java index 0b60c7e8..83677157 100644 --- a/src/main/java/com/carecode/domain/health/service/SiblingOverviewService.java +++ b/src/main/java/com/carecode/domain/health/service/SiblingOverviewService.java @@ -1,6 +1,6 @@ package com.carecode.domain.health.service; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.facility.repository.FacilityWaitlistRepository; import com.carecode.domain.health.dto.response.SiblingOverviewResponse; import com.carecode.domain.health.entity.VaccinationSchedule; diff --git a/src/main/java/com/carecode/domain/notification/controller/NotificationController.java b/src/main/java/com/carecode/domain/notification/controller/NotificationController.java index 1cb2bbec..df201c07 100644 --- a/src/main/java/com/carecode/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/carecode/domain/notification/controller/NotificationController.java @@ -3,7 +3,7 @@ import com.carecode.core.annotation.LogExecutionTime; import com.carecode.core.web.BaseController; import com.carecode.core.exception.CareServiceException; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.notification.dto.request.NotificationCreateRequest; import com.carecode.domain.notification.dto.request.NotificationMarkAsReadRequest; import com.carecode.domain.notification.dto.request.NotificationRegisterPushTokenRequest; diff --git a/src/main/java/com/carecode/domain/notification/controller/NotificationLinkController.java b/src/main/java/com/carecode/domain/notification/controller/NotificationLinkController.java index 47a9fa6e..83945d6d 100644 --- a/src/main/java/com/carecode/domain/notification/controller/NotificationLinkController.java +++ b/src/main/java/com/carecode/domain/notification/controller/NotificationLinkController.java @@ -1,6 +1,6 @@ package com.carecode.domain.notification.controller; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.notification.service.NotificationLinkService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; diff --git a/src/main/java/com/carecode/domain/policy/controller/BenefitLinkController.java b/src/main/java/com/carecode/domain/policy/controller/BenefitLinkController.java index d8838488..274cb5a9 100644 --- a/src/main/java/com/carecode/domain/policy/controller/BenefitLinkController.java +++ b/src/main/java/com/carecode/domain/policy/controller/BenefitLinkController.java @@ -1,6 +1,6 @@ package com.carecode.domain.policy.controller; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.policy.service.BenefitLinkService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; diff --git a/src/main/java/com/carecode/domain/policy/controller/PolicyController.java b/src/main/java/com/carecode/domain/policy/controller/PolicyController.java index 68284a19..408018c4 100644 --- a/src/main/java/com/carecode/domain/policy/controller/PolicyController.java +++ b/src/main/java/com/carecode/domain/policy/controller/PolicyController.java @@ -4,7 +4,7 @@ import com.carecode.core.annotation.ValidateLocation; import com.carecode.core.web.BaseController; import com.carecode.core.util.PageRequestUtil; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.core.exception.CareServiceException; import com.carecode.core.exception.PolicyNotFoundException; import com.carecode.domain.policy.dto.request.BenefitAmountReportRequest; diff --git a/src/main/java/com/carecode/domain/policy/service/BenefitAmountReportService.java b/src/main/java/com/carecode/domain/policy/service/BenefitAmountReportService.java index 6d56d1ae..ad3ba281 100644 --- a/src/main/java/com/carecode/domain/policy/service/BenefitAmountReportService.java +++ b/src/main/java/com/carecode/domain/policy/service/BenefitAmountReportService.java @@ -3,7 +3,7 @@ import com.carecode.core.analytics.EventLogger; import com.carecode.core.analytics.EventType; import com.carecode.core.exception.ResourceNotFoundException; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.policy.dto.request.BenefitAmountReportRequest; import com.carecode.domain.policy.dto.response.BenefitAmountConsensusResponse; import com.carecode.domain.policy.entity.BenefitAmountReport; diff --git a/src/main/java/com/carecode/domain/policy/service/MissedBenefitService.java b/src/main/java/com/carecode/domain/policy/service/MissedBenefitService.java index 22e2a107..ab7d2204 100644 --- a/src/main/java/com/carecode/domain/policy/service/MissedBenefitService.java +++ b/src/main/java/com/carecode/domain/policy/service/MissedBenefitService.java @@ -2,7 +2,7 @@ import com.carecode.core.analytics.EventLogger; import com.carecode.core.analytics.EventType; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.policy.dto.response.MissedBenefitResponse; import com.carecode.domain.policy.dto.response.MissedBenefitSummaryResponse; import com.carecode.domain.policy.entity.Policy; diff --git a/src/main/java/com/carecode/domain/policy/service/PolicyRecommendationService.java b/src/main/java/com/carecode/domain/policy/service/PolicyRecommendationService.java index 172d6759..a828c1dc 100644 --- a/src/main/java/com/carecode/domain/policy/service/PolicyRecommendationService.java +++ b/src/main/java/com/carecode/domain/policy/service/PolicyRecommendationService.java @@ -2,7 +2,7 @@ import com.carecode.core.analytics.EventLogger; import com.carecode.core.analytics.EventType; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.policy.dto.response.PersonalizedPolicyResponse; import com.carecode.domain.policy.entity.Policy; import com.carecode.domain.policy.mapper.PolicyMapper; diff --git a/src/main/java/com/carecode/domain/policy/service/RegionalBenefitComparisonService.java b/src/main/java/com/carecode/domain/policy/service/RegionalBenefitComparisonService.java index f5fd9fc2..0fc957b6 100644 --- a/src/main/java/com/carecode/domain/policy/service/RegionalBenefitComparisonService.java +++ b/src/main/java/com/carecode/domain/policy/service/RegionalBenefitComparisonService.java @@ -6,7 +6,7 @@ import com.carecode.core.exception.ResourceNotFoundException; import com.carecode.core.analytics.EventLogger; import com.carecode.core.analytics.EventType; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.policy.dto.response.RegionalBenefitComparisonResponse; import com.carecode.domain.policy.dto.response.RegionalBenefitResponse; import com.carecode.domain.policy.entity.Policy; diff --git a/src/main/java/com/carecode/core/security/CurrentUserFacade.java b/src/main/java/com/carecode/domain/user/app/CurrentUserFacade.java similarity index 99% rename from src/main/java/com/carecode/core/security/CurrentUserFacade.java rename to src/main/java/com/carecode/domain/user/app/CurrentUserFacade.java index e567afa8..0e2690e9 100644 --- a/src/main/java/com/carecode/core/security/CurrentUserFacade.java +++ b/src/main/java/com/carecode/domain/user/app/CurrentUserFacade.java @@ -1,4 +1,4 @@ -package com.carecode.core.security; +package com.carecode.domain.user.app; import com.carecode.core.exception.CareServiceException; import com.carecode.domain.user.entity.User; diff --git a/src/main/java/com/carecode/domain/user/controller/AuthController.java b/src/main/java/com/carecode/domain/user/controller/AuthController.java index aed08448..7b519b04 100644 --- a/src/main/java/com/carecode/domain/user/controller/AuthController.java +++ b/src/main/java/com/carecode/domain/user/controller/AuthController.java @@ -3,7 +3,7 @@ import com.carecode.core.annotation.LogExecutionTime; import com.carecode.core.annotation.RateLimit; import com.carecode.core.web.BaseController; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.core.security.RefreshTokenCookieFactory; import com.carecode.core.exception.UserNotFoundException; import com.carecode.domain.user.dto.request.LoginRequestDto; diff --git a/src/main/java/com/carecode/domain/user/controller/KakaoAuthController.java b/src/main/java/com/carecode/domain/user/controller/KakaoAuthController.java index b5f73eb7..ceecfc51 100644 --- a/src/main/java/com/carecode/domain/user/controller/KakaoAuthController.java +++ b/src/main/java/com/carecode/domain/user/controller/KakaoAuthController.java @@ -3,9 +3,9 @@ import com.carecode.core.annotation.LogExecutionTime; import com.carecode.core.web.BaseController; import com.carecode.core.exception.UserNotFoundException; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.core.security.RefreshTokenCookieFactory; -import com.carecode.core.util.KakaoUtil; +import com.carecode.external.kakao.KakaoUtil; import com.carecode.domain.user.dto.request.KakaoRegistrationRequest; import com.carecode.domain.user.dto.response.KakaoLoginUrlResponse; import com.carecode.domain.user.dto.response.TokenDto; diff --git a/src/main/java/com/carecode/domain/user/controller/UserController.java b/src/main/java/com/carecode/domain/user/controller/UserController.java index 6e5def84..08f4d6e6 100644 --- a/src/main/java/com/carecode/domain/user/controller/UserController.java +++ b/src/main/java/com/carecode/domain/user/controller/UserController.java @@ -3,7 +3,7 @@ import com.carecode.core.annotation.LogExecutionTime; import com.carecode.core.web.BaseController; import com.carecode.core.handler.ApiSuccess; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.user.app.UserFacade; import com.carecode.domain.user.dto.request.UserUpdateRequestDto; import com.carecode.domain.user.dto.response.ProfileImageResponse; diff --git a/src/main/java/com/carecode/core/security/CustomUserDetailsService.java b/src/main/java/com/carecode/domain/user/security/CustomUserDetailsService.java similarity index 97% rename from src/main/java/com/carecode/core/security/CustomUserDetailsService.java rename to src/main/java/com/carecode/domain/user/security/CustomUserDetailsService.java index 7e35d185..611e8b30 100644 --- a/src/main/java/com/carecode/core/security/CustomUserDetailsService.java +++ b/src/main/java/com/carecode/domain/user/security/CustomUserDetailsService.java @@ -1,4 +1,4 @@ -package com.carecode.core.security; +package com.carecode.domain.user.security; import com.carecode.domain.user.entity.User; import com.carecode.domain.user.repository.UserRepository; diff --git a/src/main/java/com/carecode/domain/user/service/ConsentGuard.java b/src/main/java/com/carecode/domain/user/service/ConsentGuard.java index 1055f6cf..14f0d53f 100644 --- a/src/main/java/com/carecode/domain/user/service/ConsentGuard.java +++ b/src/main/java/com/carecode/domain/user/service/ConsentGuard.java @@ -1,6 +1,6 @@ package com.carecode.domain.user.service; -import com.carecode.core.exception.CareServiceException; +import com.carecode.core.exception.ConsentRequiredException; import com.carecode.domain.user.entity.ConsentType; import com.carecode.domain.user.entity.UserConsent; import com.carecode.domain.user.repository.UserConsentRepository; @@ -21,7 +21,7 @@ public class ConsentGuard { /** 동의가 없거나 철회됐으면 접근을 막는다. */ public void require(Long userId, ConsentType type) { if (!hasConsent(userId, type)) { - throw new ConsentRequiredException(type); + throw new ConsentRequiredException(type.name(), type.getDisplayName(), type.isSensitive()); } } @@ -30,19 +30,4 @@ public boolean hasConsent(Long userId, ConsentType type) { .map(UserConsent::isGranted) .orElse(false); } - - /** 동의가 필요해서 막힌 것인지 클라이언트가 구분할 수 있게 별도 예외로 던진다. */ - public static class ConsentRequiredException extends CareServiceException { - - private final transient ConsentType consentType; - - public ConsentRequiredException(ConsentType consentType) { - super(consentType.getDisplayName() + " 동의가 필요합니다."); - this.consentType = consentType; - } - - public ConsentType getConsentType() { - return consentType; - } - } } diff --git a/src/main/java/com/carecode/domain/user/service/JwtService.java b/src/main/java/com/carecode/domain/user/service/JwtService.java index 172aaf42..80b44033 100644 --- a/src/main/java/com/carecode/domain/user/service/JwtService.java +++ b/src/main/java/com/carecode/domain/user/service/JwtService.java @@ -1,6 +1,8 @@ package com.carecode.domain.user.service; import com.carecode.core.exception.BusinessException; +import com.carecode.core.security.AccessTokenClaims; +import com.carecode.core.security.AccessTokenVerifier; import com.carecode.core.exception.ErrorCode; import com.carecode.domain.user.dto.response.TokenDto; import com.carecode.domain.user.dto.response.TokenValidationResponse; @@ -21,7 +23,7 @@ /** JWT 토큰 서비스 */ @Slf4j @Service -public class JwtService { +public class JwtService implements AccessTokenVerifier { /** 토큰 종류를 구분하는 클레임. Access Token 을 Refresh 로, 혹은 그 반대로 쓰는 것을 막습니다. */ public static final String CLAIM_TOKEN_TYPE = "typ"; @@ -180,6 +182,21 @@ public boolean validateToken(String token) { } // Access Token 전용 검증 - typ=access 인 토큰만 통과시킵니다. + /** + * {@link AccessTokenVerifier} 구현. 인증 필터는 이 메서드만 쓴다. + * + *

유효성 확인과 값 꺼내기를 한 번에 하는 이유: 필터가 따로 부르면 그 사이에 네 번 + * 파싱하게 되고, 유효성 확인 뒤에 값을 꺼내는 순서를 호출자가 지켜야 한다. + */ + @Override + public java.util.Optional verifyAccessToken(String token) { + if (!validateAccessToken(token)) { + return java.util.Optional.empty(); + } + return java.util.Optional.of(new AccessTokenClaims( + getUserIdFromToken(token), getEmailFromToken(token), getRoleFromToken(token))); + } + public boolean validateAccessToken(String token) { return validateTokenOfType(token, TOKEN_TYPE_ACCESS); } diff --git a/src/main/java/com/carecode/domain/user/service/PrivacyService.java b/src/main/java/com/carecode/domain/user/service/PrivacyService.java index cc7fcc22..407fac71 100644 --- a/src/main/java/com/carecode/domain/user/service/PrivacyService.java +++ b/src/main/java/com/carecode/domain/user/service/PrivacyService.java @@ -1,6 +1,6 @@ package com.carecode.domain.user.service; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.community.repository.PostRepository; import com.carecode.domain.health.repository.HealthRecordRepository; import com.carecode.domain.user.dto.request.ConsentUpdateRequest; diff --git a/src/main/java/com/carecode/domain/user/service/impl/AuthServiceImpl.java b/src/main/java/com/carecode/domain/user/service/impl/AuthServiceImpl.java index ab15dad5..fff2afae 100644 --- a/src/main/java/com/carecode/domain/user/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/carecode/domain/user/service/impl/AuthServiceImpl.java @@ -4,11 +4,11 @@ import com.carecode.core.exception.CareCodeException; import com.carecode.core.exception.CareServiceException; import com.carecode.core.exception.ErrorCode; -import com.carecode.core.util.KakaoUtil; -import com.carecode.domain.user.dto.response.KakaoOAuthToken; -import com.carecode.domain.user.dto.response.KakaoProfile; -import com.carecode.domain.user.dto.response.KakaoAccount; -import com.carecode.domain.user.dto.response.KakaoProperties; +import com.carecode.external.kakao.KakaoUtil; +import com.carecode.external.kakao.KakaoOAuthToken; +import com.carecode.external.kakao.KakaoProfile; +import com.carecode.external.kakao.KakaoAccount; +import com.carecode.external.kakao.KakaoProperties; import com.carecode.domain.user.dto.response.TokenDto; import com.carecode.domain.user.entity.User; import com.carecode.domain.user.entity.UserRole; diff --git a/src/main/java/com/carecode/domain/user/dto/response/KakaoAccount.java b/src/main/java/com/carecode/external/kakao/KakaoAccount.java similarity index 88% rename from src/main/java/com/carecode/domain/user/dto/response/KakaoAccount.java rename to src/main/java/com/carecode/external/kakao/KakaoAccount.java index a388dc0f..bd94ab5f 100644 --- a/src/main/java/com/carecode/domain/user/dto/response/KakaoAccount.java +++ b/src/main/java/com/carecode/external/kakao/KakaoAccount.java @@ -1,4 +1,4 @@ -package com.carecode.domain.user.dto.response; +package com.carecode.external.kakao; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; diff --git a/src/main/java/com/carecode/domain/user/dto/response/KakaoOAuthToken.java b/src/main/java/com/carecode/external/kakao/KakaoOAuthToken.java similarity index 90% rename from src/main/java/com/carecode/domain/user/dto/response/KakaoOAuthToken.java rename to src/main/java/com/carecode/external/kakao/KakaoOAuthToken.java index fd4ddcae..bfbe8194 100644 --- a/src/main/java/com/carecode/domain/user/dto/response/KakaoOAuthToken.java +++ b/src/main/java/com/carecode/external/kakao/KakaoOAuthToken.java @@ -1,4 +1,4 @@ -package com.carecode.domain.user.dto.response; +package com.carecode.external.kakao; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; diff --git a/src/main/java/com/carecode/domain/user/dto/response/KakaoProfile.java b/src/main/java/com/carecode/external/kakao/KakaoProfile.java similarity index 87% rename from src/main/java/com/carecode/domain/user/dto/response/KakaoProfile.java rename to src/main/java/com/carecode/external/kakao/KakaoProfile.java index b45191f2..af7a0699 100644 --- a/src/main/java/com/carecode/domain/user/dto/response/KakaoProfile.java +++ b/src/main/java/com/carecode/external/kakao/KakaoProfile.java @@ -1,4 +1,4 @@ -package com.carecode.domain.user.dto.response; +package com.carecode.external.kakao; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; diff --git a/src/main/java/com/carecode/domain/user/dto/response/KakaoProfileInfo.java b/src/main/java/com/carecode/external/kakao/KakaoProfileInfo.java similarity index 88% rename from src/main/java/com/carecode/domain/user/dto/response/KakaoProfileInfo.java rename to src/main/java/com/carecode/external/kakao/KakaoProfileInfo.java index b6f4d778..37f42c36 100644 --- a/src/main/java/com/carecode/domain/user/dto/response/KakaoProfileInfo.java +++ b/src/main/java/com/carecode/external/kakao/KakaoProfileInfo.java @@ -1,4 +1,4 @@ -package com.carecode.domain.user.dto.response; +package com.carecode.external.kakao; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; diff --git a/src/main/java/com/carecode/domain/user/dto/response/KakaoProperties.java b/src/main/java/com/carecode/external/kakao/KakaoProperties.java similarity index 86% rename from src/main/java/com/carecode/domain/user/dto/response/KakaoProperties.java rename to src/main/java/com/carecode/external/kakao/KakaoProperties.java index 88cafacc..967f04ee 100644 --- a/src/main/java/com/carecode/domain/user/dto/response/KakaoProperties.java +++ b/src/main/java/com/carecode/external/kakao/KakaoProperties.java @@ -1,4 +1,4 @@ -package com.carecode.domain.user.dto.response; +package com.carecode.external.kakao; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; diff --git a/src/main/java/com/carecode/core/util/KakaoUtil.java b/src/main/java/com/carecode/external/kakao/KakaoUtil.java similarity index 98% rename from src/main/java/com/carecode/core/util/KakaoUtil.java rename to src/main/java/com/carecode/external/kakao/KakaoUtil.java index 5f155024..ef8e3adb 100644 --- a/src/main/java/com/carecode/core/util/KakaoUtil.java +++ b/src/main/java/com/carecode/external/kakao/KakaoUtil.java @@ -1,7 +1,5 @@ -package com.carecode.core.util; +package com.carecode.external.kakao; -import com.carecode.domain.user.dto.response.KakaoOAuthToken; -import com.carecode.domain.user.dto.response.KakaoProfile; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/test/java/com/carecode/architecture/ArchitectureRulesTest.java b/src/test/java/com/carecode/architecture/ArchitectureRulesTest.java index b9995b97..f00ab4e8 100644 --- a/src/test/java/com/carecode/architecture/ArchitectureRulesTest.java +++ b/src/test/java/com/carecode/architecture/ArchitectureRulesTest.java @@ -134,6 +134,10 @@ void innerLayersDoNotDependOnControllers() { void domainsUseCurrentUserFacade() { check(noClasses() .that().resideInAPackage("com.carecode.domain..") + // 파사드 자신은 예외다. 이 규칙의 목적이 "모든 접근을 한 클래스로 모으는 것" 이고, + // 그 한 클래스가 바로 여기다. (한때 core/security 에 있어 이 규칙에 걸리지 않았는데, + // 그 자리에 있으면 기반 코드가 사용자 도메인을 알게 돼서 도메인으로 옮겼다.) + .and().doNotHaveFullyQualifiedName("com.carecode.domain.user.app.CurrentUserFacade") .should().dependOnClassesThat() .haveFullyQualifiedName("org.springframework.security.core.context.SecurityContextHolder") .because("현재 사용자는 CurrentUserFacade 로만 얻는다")); diff --git a/src/test/java/com/carecode/domain/chatbot/controller/ChatbotControllerOwnershipTest.java b/src/test/java/com/carecode/domain/chatbot/controller/ChatbotControllerOwnershipTest.java index df1629cf..64a181f0 100644 --- a/src/test/java/com/carecode/domain/chatbot/controller/ChatbotControllerOwnershipTest.java +++ b/src/test/java/com/carecode/domain/chatbot/controller/ChatbotControllerOwnershipTest.java @@ -1,6 +1,6 @@ package com.carecode.domain.chatbot.controller; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.chatbot.app.ChatbotFacade; import com.carecode.domain.chatbot.entity.ChatMessage; import com.carecode.domain.chatbot.entity.ChatSession; diff --git a/src/test/java/com/carecode/domain/community/service/CommunityServiceHiddenPostTest.java b/src/test/java/com/carecode/domain/community/service/CommunityServiceHiddenPostTest.java index 5198a3f2..259deb88 100644 --- a/src/test/java/com/carecode/domain/community/service/CommunityServiceHiddenPostTest.java +++ b/src/test/java/com/carecode/domain/community/service/CommunityServiceHiddenPostTest.java @@ -53,7 +53,7 @@ class CommunityServiceHiddenPostTest { @Mock private PostLikeRepository postLikeRepository; @Mock private BookmarkRepository bookmarkRepository; @Mock private CommunityMapper communityMapper; - @Mock private com.carecode.core.security.CurrentUserFacade currentUserFacade; + @Mock private com.carecode.domain.user.app.CurrentUserFacade currentUserFacade; @InjectMocks private CommunityService communityService; diff --git a/src/test/java/com/carecode/domain/community/service/CommunityServiceOwnershipTest.java b/src/test/java/com/carecode/domain/community/service/CommunityServiceOwnershipTest.java index 00c7a18c..6b4df199 100644 --- a/src/test/java/com/carecode/domain/community/service/CommunityServiceOwnershipTest.java +++ b/src/test/java/com/carecode/domain/community/service/CommunityServiceOwnershipTest.java @@ -56,7 +56,7 @@ class CommunityServiceOwnershipTest { @Mock private PostLikeRepository postLikeRepository; @Mock private BookmarkRepository bookmarkRepository; @Mock private CommunityMapper communityMapper; - @Mock private com.carecode.core.security.CurrentUserFacade currentUserFacade; + @Mock private com.carecode.domain.user.app.CurrentUserFacade currentUserFacade; @InjectMocks private CommunityService communityService; diff --git a/src/test/java/com/carecode/domain/health/service/HealthRecordAttachmentServiceTest.java b/src/test/java/com/carecode/domain/health/service/HealthRecordAttachmentServiceTest.java index b80a73be..14581d9c 100644 --- a/src/test/java/com/carecode/domain/health/service/HealthRecordAttachmentServiceTest.java +++ b/src/test/java/com/carecode/domain/health/service/HealthRecordAttachmentServiceTest.java @@ -2,7 +2,7 @@ import com.carecode.core.exception.HealthRecordNotFoundException; import com.carecode.core.exception.ResourceNotFoundException; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.core.storage.FileStorageService; import com.carecode.domain.health.dto.response.AttachmentDownload; import com.carecode.domain.health.entity.HealthRecord; diff --git a/src/test/java/com/carecode/domain/policy/service/BenefitAmountReportServiceTest.java b/src/test/java/com/carecode/domain/policy/service/BenefitAmountReportServiceTest.java index 9e4c8b3e..62c20071 100644 --- a/src/test/java/com/carecode/domain/policy/service/BenefitAmountReportServiceTest.java +++ b/src/test/java/com/carecode/domain/policy/service/BenefitAmountReportServiceTest.java @@ -1,6 +1,6 @@ package com.carecode.domain.policy.service; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.policy.dto.request.BenefitAmountReportRequest; import com.carecode.domain.policy.dto.response.BenefitAmountConsensusResponse; import com.carecode.domain.policy.entity.BenefitAmountReport; diff --git a/src/test/java/com/carecode/domain/policy/service/MissedBenefitServiceTest.java b/src/test/java/com/carecode/domain/policy/service/MissedBenefitServiceTest.java index 87f0fa28..79bf62e2 100644 --- a/src/test/java/com/carecode/domain/policy/service/MissedBenefitServiceTest.java +++ b/src/test/java/com/carecode/domain/policy/service/MissedBenefitServiceTest.java @@ -1,6 +1,6 @@ package com.carecode.domain.policy.service; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.policy.dto.response.MissedBenefitSummaryResponse; import com.carecode.domain.policy.entity.Policy; import com.carecode.domain.policy.repository.PolicyRepository; diff --git a/src/test/java/com/carecode/domain/policy/service/RegionalBenefitComparisonServiceTest.java b/src/test/java/com/carecode/domain/policy/service/RegionalBenefitComparisonServiceTest.java index c122fa19..ae97ae48 100644 --- a/src/test/java/com/carecode/domain/policy/service/RegionalBenefitComparisonServiceTest.java +++ b/src/test/java/com/carecode/domain/policy/service/RegionalBenefitComparisonServiceTest.java @@ -2,7 +2,7 @@ import com.carecode.domain.policy.benefit.BenefitProjectionCalculator; import com.carecode.core.exception.BusinessException; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.policy.dto.response.RegionalBenefitComparisonResponse; import com.carecode.domain.policy.dto.response.RegionalBenefitResponse; import com.carecode.domain.policy.entity.Policy; diff --git a/src/test/java/com/carecode/core/security/CurrentUserFacadeTest.java b/src/test/java/com/carecode/domain/user/app/CurrentUserFacadeTest.java similarity index 99% rename from src/test/java/com/carecode/core/security/CurrentUserFacadeTest.java rename to src/test/java/com/carecode/domain/user/app/CurrentUserFacadeTest.java index b7a111cc..b1bf7858 100644 --- a/src/test/java/com/carecode/core/security/CurrentUserFacadeTest.java +++ b/src/test/java/com/carecode/domain/user/app/CurrentUserFacadeTest.java @@ -1,4 +1,4 @@ -package com.carecode.core.security; +package com.carecode.domain.user.app; import com.carecode.core.exception.CareServiceException; import com.carecode.domain.user.entity.User; diff --git a/src/test/java/com/carecode/domain/user/service/ConsentGuardTest.java b/src/test/java/com/carecode/domain/user/service/ConsentGuardTest.java index 0841b224..65819c3e 100644 --- a/src/test/java/com/carecode/domain/user/service/ConsentGuardTest.java +++ b/src/test/java/com/carecode/domain/user/service/ConsentGuardTest.java @@ -1,5 +1,6 @@ package com.carecode.domain.user.service; +import com.carecode.core.exception.ConsentRequiredException; import com.carecode.domain.user.entity.ConsentType; import com.carecode.domain.user.entity.UserConsent; import com.carecode.domain.user.repository.UserConsentRepository; @@ -34,7 +35,7 @@ void blocksWithoutConsent() { when(repository.findLatest(anyLong(), eq(ConsentType.HEALTH_DATA))).thenReturn(Optional.empty()); assertThatThrownBy(() -> guard.require(1L, ConsentType.HEALTH_DATA)) - .isInstanceOf(ConsentGuard.ConsentRequiredException.class) + .isInstanceOf(ConsentRequiredException.class) .hasMessageContaining("건강정보"); } @@ -48,7 +49,7 @@ void blocksWhenRevoked() { assertThat(guard.hasConsent(1L, ConsentType.HEALTH_DATA)).isFalse(); assertThatThrownBy(() -> guard.require(1L, ConsentType.HEALTH_DATA)) - .isInstanceOf(ConsentGuard.ConsentRequiredException.class); + .isInstanceOf(ConsentRequiredException.class); } @Test @@ -68,8 +69,8 @@ void exposesRequiredConsentType() { when(repository.findLatest(anyLong(), eq(ConsentType.HEALTH_DATA))).thenReturn(Optional.empty()); assertThatThrownBy(() -> guard.require(1L, ConsentType.HEALTH_DATA)) - .isInstanceOfSatisfying(ConsentGuard.ConsentRequiredException.class, - e -> assertThat(e.getConsentType()).isEqualTo(ConsentType.HEALTH_DATA)); + .isInstanceOfSatisfying(ConsentRequiredException.class, + e -> assertThat(e.getConsentType()).isEqualTo(ConsentType.HEALTH_DATA.name())); } @Test diff --git a/src/test/java/com/carecode/domain/user/service/impl/AuthServiceImplKakaoTest.java b/src/test/java/com/carecode/domain/user/service/impl/AuthServiceImplKakaoTest.java index 6be9aabd..6b24679a 100644 --- a/src/test/java/com/carecode/domain/user/service/impl/AuthServiceImplKakaoTest.java +++ b/src/test/java/com/carecode/domain/user/service/impl/AuthServiceImplKakaoTest.java @@ -2,11 +2,11 @@ import com.carecode.core.exception.CareCodeException; import com.carecode.core.exception.ErrorCode; -import com.carecode.core.util.KakaoUtil; -import com.carecode.domain.user.dto.response.KakaoAccount; -import com.carecode.domain.user.dto.response.KakaoOAuthToken; -import com.carecode.domain.user.dto.response.KakaoProfile; -import com.carecode.domain.user.dto.response.KakaoProfileInfo; +import com.carecode.external.kakao.KakaoUtil; +import com.carecode.external.kakao.KakaoAccount; +import com.carecode.external.kakao.KakaoOAuthToken; +import com.carecode.external.kakao.KakaoProfile; +import com.carecode.external.kakao.KakaoProfileInfo; import com.carecode.domain.user.dto.response.TokenDto; import com.carecode.domain.user.entity.User; import com.carecode.domain.user.entity.UserRole; diff --git a/src/test/java/com/carecode/integration/SampleDataScenarioTest.java b/src/test/java/com/carecode/integration/SampleDataScenarioTest.java index ee7e926c..55a2f1ed 100644 --- a/src/test/java/com/carecode/integration/SampleDataScenarioTest.java +++ b/src/test/java/com/carecode/integration/SampleDataScenarioTest.java @@ -3,7 +3,7 @@ import com.carecode.CareCodeApplication; import com.carecode.devtools.SampleFacilitySeeder; import com.carecode.devtools.SamplePolicySeeder; -import com.carecode.core.security.CurrentUserFacade; +import com.carecode.domain.user.app.CurrentUserFacade; import com.carecode.domain.facility.dto.response.AdmissionForecastResponse; import com.carecode.domain.facility.dto.response.FacilityPopularityResponse; import com.carecode.domain.facility.entity.CareFacility;