--- title: error / method-security CGLIB vs JDK proxy — use case injection + unauth exception type source_type: error-note status: raw related_branches: [feature-authentication-authorization-contract] related_projects: [ca-skeleton] tags: [error, ca-skeleton, spring-security, method-security, aop, proxy, cglib, authorization] created: 2026-06-08 status_label: resolved --- # error: method-security AOP proxy — use case 주입 실패 + unauthenticated 예외 타입 > Layer: `raw/errors/` — `@EnableMethodSecurity` 로 use case bean 을 proxy 할 때 만난 두 가지 함정. ## Parent / 부모 - [[raw/branch-notes/feature-authentication-authorization-contract]] — `@RequiresPermission` enforcement(`RequiresPermissionAuthorizationManager` + `MethodSecurityConfig`) 구현 중 발생. ## 증상 1 / Symptom — `BeanNotOfRequiredTypeException` `WorkLogAuthorizationContractTest`(@SpringBootTest, classes=nested @Configuration) 가 4 케이스 전부 ``` org.springframework.beans.factory.UnsatisfiedDependencyException Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException ``` 로 실패. `@Autowired CreateWorkLogUseCase` 가 만족 안 됨. ### 근본 원인 method-security 의 custom Advisor 가 `@RequiresPermission` use case 를 AOP proxy 로 감쌌는데, **JDK dynamic proxy** 가 생성됨. JDK proxy 는 use case 가 구현한 인터페이스(`CommandUseCase`)만 구현하고 concrete `CreateWorkLogUseCase` 의 subtype 이 아니다. controller(`WorkLogController`)와 test 는 concrete `*UseCase` 타입을 주입받으므로 assign 불가. prod 앱(`CaSkeletonApplication`, `@SpringBootApplication`)은 Spring Boot 의 `AopAutoConfiguration` 이 `spring.aop.proxy-target-class=true`(CGLIB class proxy) 를 기본 적용 → concrete subtype proxy → 주입 정상. 그러나 **auto-config 가 없는 isolated test slice** 에는 그 기본이 안 들어와 JDK proxy 로 fallback. ### 해소 contract test 의 nested config 에 CGLIB 강제: ```java @Configuration @EnableAspectJAutoProxy(proxyTargetClass = true) @Import(MethodSecurityConfig.class) static class AuthzTestConfig { ... } ``` 이는 prod 의 AOP 기본을 mirror 하는 것이라 prod 동작 변경 없음. 교훈: **method security 를 거는 bean 을 concrete 타입으로 주입한다면 반드시 CGLIB proxy 여야 한다.** Boot 앱은 자동이지만, slice/standalone context 는 명시 필요. ## 증상 2 / Symptom — unauthenticated 가 403 아님 `unauthenticated_caller_is_denied_fail_closed` 테스트가 `AccessDeniedException` 을 기대했으나 실제론 `AuthenticationCredentialsNotFoundException` 발생 → 단언 실패. ### 근본 원인 `AuthorizationManagerBeforeMethodInterceptor` 는 `Supplier` 을 deferred 로 넘기는데, SecurityContext 가 비어 있으면(`getAuthentication()==null`) `.get()` 호출 시 `AuthenticationCredentialsNotFoundException`(= `AuthenticationException`, 401-family) 을 던진다. 즉 **권한 부족(403)** 과 **인증 자체 없음(401)** 은 다른 경로다. 내 `RequiresPermissionAuthorizationManager.check` 의 `auth==null` 분기는 supplier 가 먼저 throw 하므로 unauthenticated 케이스에선 도달하지 않는다(authenticated-but-not-authorized 토큰 케이스에서만 도달). ### 해소 테스트 단언을 `isInstanceOf(AuthenticationException.class)` 로 정정. prod 에서는 security filter chain(`.anyRequest().authenticated()`)이 method-security 도달 전에 401(EnvelopeAuthenticationEntryPoint)로 차단하므로, method-security 의 unauth 경로는 defense-in-depth backstop 으로만 의미. ## 교훈 / Lesson 1. method-secured bean 을 concrete 타입으로 DI 하면 CGLIB(`proxyTargetClass=true`) 필수. Boot 앱은 자동, slice 는 수동. 2. method-security 단의 거부는 **두 종류**: 인증 없음 → `AuthenticationException`(401), 권한 부족 → `AccessDeniedException`(403). 테스트·핸들러 매핑을 분리해 생각해야 함. 3. AOP self-invocation/non-bean 호출은 proxy 우회 → mutating 진입점이 전부 Spring bean 경유인지 정적 검증 필요(ArchUnit, host=architecture-enforcement-rules).