Skip to content

[Feat/#24] 인증 사용자 주입을 위한 ApiUser와 ArgumentResolver 추가 - #25

Merged
sangrae2325 merged 3 commits into
mainfrom
feat/#24-api-user-resolver
Sep 11, 2026
Merged

[Feat/#24] 인증 사용자 주입을 위한 ApiUser와 ArgumentResolver 추가#25
sangrae2325 merged 3 commits into
mainfrom
feat/#24-api-user-resolver

Conversation

@sangrae2325

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

🎯 해결하려는 문제가 무엇인가요?

컨트롤러가 인증된 사용자를 파라미터로 주입받는 통로가 없습니다.

인증 체인 자체는 이미 동작합니다 — JwtAuthFilter가 토큰을 검증해 UserAuthenticationSecurityContextHolder에 넣고, SecurityConfig/v1/app/**STUDENT, /v1/admin/**ADMIN으로 1차 인가하며, PrincipalProvider/SecurityPrincipalProvideruserId를 꺼내줍니다. 비어 있는 것은 컨트롤러 메서드가 그 값을 받는 마지막 한 칸뿐입니다.

config-and-auth.md 5절이 규정한 ApiUser·구현체·ArgumentResolver·등록 설정이 저장소에 하나도 없었습니다.

❓ 왜 해결해야 하나요?

사용자 식별이 필요한 모든 API가 이것 없이는 시작할 수 없습니다. 당장 #23 행사 신청 API가 막혀 있고, app-api·admin-api를 쓰는 다른 작업들도 같은 지점에서 멈춥니다. 열려 있는 PR 어디에도 이 인프라가 없어 별도 이슈로 분리했습니다.

⭐ 어떻게 해결했나요?

WebMvcConfigcommon-api에 두되 common-apiapp-api/admin-api를 컴파일 타임에 참조하지 않습니다. 리졸버를 타입으로 지목하지 않고 주입받아 일괄 등록합니다.

api:common-api
  ApiUser                     인증 사용자 스냅샷 인터페이스 (Long userId())
  ApiUserArgumentResolver     HandlerMethodArgumentResolver 상속 마커 인터페이스
  WebMvcConfig                ObjectProvider<ApiUserArgumentResolver> → addArgumentResolvers에 전부 등록
        ↑ (구현)                      ↑ (런타임에 스프링이 수집)
api:app-api                          api:admin-api
  app/AppApiUser                       admin/AdminApiUser
  app/resolver/AppApiUserArgumentResolver   admin/resolver/AdminApiUserArgumentResolver

bootstrap이 세 모듈을 모두 조립하므로 런타임에 스프링이 리졸버 빈을 모아 넘깁니다. 클라이언트 모듈이 추가돼도 WebMvcConfig는 수정할 필요가 없습니다.

리졸버 동작은 config-and-auth.md 4-3절 그대로입니다 — 파라미터 타입이 맞으면 해당 role을 확인하고, 없으면 BusinessException(FORBIDDEN), 있으면 스냅샷을 주입합니다.

새로 추가한 Gradle 의존이 없습니다. 세 모듈 모두 이미 core:common·gateway:auth·spring-boot-starter-webmvc를 갖고 있습니다.

🧩 이 PR의 한계 & 트레이드오프

  • ApiUseruserId()만 노출합니다. 리졸버가 이미 role을 검증하고 통과시키므로 컨트롤러가 role을 다시 볼 일이 없다고 판단했습니다. 필요해지면 그때 넓히는 편이 안전합니다.
  • AdminApiUser에 부서(CouncilDepartment)를 담지 않았습니다. config-and-auth.md 4-4절이 부서 단위 인가를 DepartmentAccessChecker(별도 클래스) 소관으로 "이 프로젝트의 기본"이라 규정합니다. 그 클래스는 아직 없어 별도 이슈가 필요합니다.
  • 컨트롤러 적용은 이 PR에 없습니다. 실제 사용은 각 기능 이슈(행사 신청서 폼 조회/신청 API 추가 #23 등)에서 붙입니다.
  • 테스트 코드가 없습니다. 저장소 관례에 따라 별도 요청 시 작성합니다.

⛓️ 기존 기능에 미치는 영향

  • 기존 코드를 수정한 것이 없습니다. 신규 파일 7개만 추가했습니다.
  • WebMvcConfig가 처음 생기는 WebMvcConfigurer입니다. 등록하는 리졸버는 스프링 기본 리졸버보다 앞에 삽입되지만, ApiUser 타입 파라미터만 가져가므로 기존 파라미터 바인딩에 영향이 없습니다.
  • api/app-api/src, api/admin-api/src가 이 PR로 처음 생깁니다. [Feat/#19] 게시판(공지) 목록/상세 조회 API 추가 #21(공지 API)·행사 신청서 폼 조회/신청 API 추가 #23(행사 API)이 같은 디렉터리에 컨트롤러를 추가하지만 파일이 겹치지 않습니다.
  • gradle check(ModularityTests.verify() + DomainImplAccessTests) 통과를 clean 빌드에서 확인했습니다.

🔀 Edge Case & 실패 시나리오

상황 처리
해당 role이 없는 사용자가 호출 BusinessException(CommonErrorCode.FORBIDDEN) → 403. SecurityConfig의 URL 패턴 인가가 1차, 이것이 2차 방어선
인증 정보가 아예 없음 SecurityPrincipalProviderUNAUTHORIZED → 401
리졸버 빈이 하나도 없는 컨텍스트 ObjectProvider.orderedStream()이 빈 스트림이라 기동은 정상. 파라미터는 아래 항목대로 처리됨
리졸버 없이 ApiUser 파라미터가 선언됨 요청 파라미터로 바인딩되지 않고 500으로 실패합니다. 아래 리뷰 포인트 참조

📋 검토한 대안과 선택 이유

1. 컨트롤러에 PrincipalProvider를 직접 주입 — 이미 있는 것만으로 끝나지만, config-and-auth.md가 규정한 ApiUser 패턴과 다른 선례가 남고 컨트롤러 시그니처에 인증 요구가 드러나지 않습니다. 채택하지 않았습니다.

2. List<HandlerMethodArgumentResolver>를 그대로 주입 — 컨텍스트에 등록된 무관한 리졸버까지 함께 수집될 수 있어, 마커 인터페이스 ApiUserArgumentResolver로 좁혔습니다.

3. List<ApiUserArgumentResolver> 생성자 주입 — 스프링은 후보 빈이 하나도 없으면 주입에 실패하므로 모듈 단위 슬라이스 테스트에서 기동이 깨집니다. ObjectProvider로 받아 addArgumentResolvers() 시점에 펼칩니다.

4. AppApiUser/AdminApiUserrecord로 선언 — 처음에는 record였으나 아래 이유로 생성자를 감춘 클래스로 바꿨습니다.

💬 리뷰 포인트

[r] AppApiUser/AdminApiUser가 record가 아닌 이유

ApiUser 구현체를 어노테이션 없는 record 파라미터로 두면, 리졸버가 등록되지 않은 컨텍스트에서 인증이 열린 채로 실패합니다.

RequestMappingHandlerAdapter.getDefaultArgumentResolvers()는 커스텀 리졸버 뒤에 폴백으로 ServletModelAttributeMethodProcessor(annotationNotRequired = true)를 등록합니다. 이 폴백은 BeanUtils.isSimpleProperty()가 false인 모든 타입을 가져가고, Spring 6부터 record를 요청 파라미터로 생성자 바인딩합니다. 결과적으로 GET /v1/app/foo?userId=999new AppApiUser(999L)을 만들어 컨트롤러가 공격자가 넣은 userId로 동작하게 됩니다. Spring 7.0.8 바이트코드에서 확인했습니다.

현재 조립된 앱에서는 재현되지 않습니다(bootstrap이 리졸버를 항상 스캔). 다만 @WebMvcTest 슬라이스는 WebMvcConfigurer를 포함하면서 일반 @Component는 제외하므로 리졸버 리스트가 비고, 이때 폴백이 먹습니다. 앞으로 ApiUser 타입을 추가하면서 리졸버를 빠뜨려도 같습니다.

그래서 두 타입을 private 생성자 + 정적 팩토리 클래스로 두었습니다. 폴백의 BeanUtils.getResolvableConstructor()가 public 생성자도 no-arg 생성자도 찾지 못해 예외를 던지므로, 조용한 스푸핑 대신 500으로 닫힙니다. coding-style.md 2-10절의 기본 규칙(정적 팩토리 + 생성자 감추기)에도 부합하고 gateway:authUserAuthentication과 같은 형태입니다.

[c] 네이밍이 컨벤션 문서와 다릅니다

config-and-auth.md 4-5절은 {Role}ApiUser(즉 StudentApiUser)로 적혀 있으나, 컨트롤러가 App*Controller/Admin*Controller이고 모듈도 app-api/admin-api클라이언트 기준 네이밍으로 갔습니다. 패키지도 {client}/{Client}ApiUser + {client}/resolver/... 형태로 맞췄습니다.

작업 중 같은 문서의 다른 드리프트도 확인했습니다 — 문서의 Department·DEPT_ 접두사가 실제 코드에서는 CouncilDepartment·COUNCIL_입니다. 문서 정리는 별도 이슈로 다루는 게 좋겠습니다.

[a] ApiUser 인터페이스를 직접 파라미터로 선언하는 경우

두 리졸버가 구체 타입 equals로만 매칭하므로 ApiUser 자체를 선언하면 폴백이 인터페이스를 생성하려다 실패해 500이 납니다. 닫히는 방향이라 위험하진 않지만, 공개한 추상 타입이라 명시적으로 막을지 의견 주시면 반영하겠습니다.

@tnals0924 tnals0924 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빠른 작업 감사합니다! LGTM~

@sangrae2325
sangrae2325 merged commit 3e585cc into main Sep 11, 2026
1 check passed
@sangrae2325
sangrae2325 deleted the feat/#24-api-user-resolver branch September 11, 2026 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

인증 사용자 주입을 위한 ApiUser와 ArgumentResolver 추가

2 participants