8.0 KiB
8.0 KiB
title, source_type, status, related_branches, related_projects, tags, created, status_label
| title | source_type | status | related_branches | related_projects | tags | created | status_label | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| error / method-security-class-pointcut-final-usecase-bean-2026-06-12 | error-note | raw |
|
|
|
2026-06-12 | resolved |
error: method-security-class-pointcut-final-usecase-bean-2026-06-12
Layer:
raw/errors/— 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다.
Parent / 부모
- raw/branch-notes/feature-domain-event-outbox-contract — Task E
OutboxConfig의PublishPendingOutboxEventsUseCase수동@Bean등록이 adapter-web 의 method security 와 충돌해 bootRun 기동 실패.
증상 / Symptom
- 에러 메시지 1 (기동 실패, 원문 그대로):
Error creating bean with name 'publishPendingOutboxEventsUseCase' defined in class path resource [dev/caskeleton/bootstrap/outbox/OutboxConfig.class]: Could not generate CGLIB subclass of class dev.caskeleton.application.outbox.PublishPendingOutboxEventsUseCase ... Caused by: java.lang.IllegalArgumentException: Cannot subclass final class dev.caskeleton.application.outbox.PublishPendingOutboxEventsUseCase - 에러 메시지 2 (
final제거 후 매 틱 5초마다, 원문 그대로):outbox relay scheduler: unexpected error in relay cycle — relay will retry on the next tick org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext at ...AuthorizationManagerBeforeMethodInterceptor.getAuthentication(...) - 발생 컨텍스트:
./gradlew bootRun풀 컨텍스트 기동. Testcontainers 계약 테스트는 use case 를new로 직접 조립(minimal context, method security 부재)하므로 미검출 — 풀 컨텍스트에서만 재현. - 발생 환경: local, Spring Boot 3.5.15, Spring Security
@EnableMethodSecurity(prePostEnabled = false)+ 커스텀 Advisor. - 재현 가능 여부:
always.
재현 절차 / Reproduction
- adapter-web
MethodSecurityConfig가AnnotationMatchingPointcut.forClassAnnotation(RequiresPermission.class)를 포함한 union pointcut 의AuthorizationManagerBeforeMethodInterceptorAdvisor 를 등록한 상태. @RequiresPermission이 클래스 레벨에 붙은final클래스를@Bean으로 등록 (OutboxConfig.publishPendingOutboxEventsUseCase).- 기동 → auto-proxy 가 Advisor 매칭 빈을 CGLIB 서브클래싱 시도 →
Cannot subclass final class로 컨텍스트 refresh 실패. (Spring Boot 기본spring.aop.proxy-target-class=true— 인터페이스가 있어도 CGLIB.) final만 제거하면 기동은 성공하지만,@Scheduled스케줄러 스레드에는Authentication이 없으므로 use case 호출 시마다AuthenticationCredentialsNotFoundException— relay 가 한 건도 처리 못 함.
조사 단계 / Investigation log
- 2026-06-12 — bootRun 로그 첫 실패는 Flyway
Connection to localhost:5432 refused—ca-pgPostgreSQL 컨테이너가 18시간 전 Exited (restart policyno, 재부팅 후 자동 시작 안 됨).docker start ca-pg로 해소 (환경 문제, 코드 무관). - 2026-06-12 — 두 번째 실패가 CGLIB
Cannot subclass final class. 동작하는 4개 sample use case (CreateWorkLogUseCase등) 와 대조 → 전부@RequiresPermission+ non-finalpublic class. outbox use case 만public final class. - 2026-06-12 —
final제거로 기동 성공했으나 relay 틱마다AuthenticationCredentialsNotFoundException.AuthorizationManagerBeforeMethodInterceptor.getAuthentication은 SecurityContext 가 비어 있으면 커스텀AuthorizationManager.check도달 전에 throw — fail-closed 라 매니저 측 우회 불가. - 2026-06-12 — use case Javadoc 의 설계 의도 확인: "enforcement in the scheduler context is by convention (the scheduler is app-bootstrap-internal)" — 즉 annotation 은 ArchUnit D4 충족용 선언이고 스케줄러 경로 런타임 집행은 의도가 아님. 계약 테스트(
OutboxContainerTestSupport.relayUseCase)도 bean 이 아닌new조립.
근본 원인 / Root cause
- 직접 원인:
final클래스가 CGLIB auto-proxy 대상이 됨 (#1) / 인증 없는 스케줄러 스레드에서 method security 가 fail-closed 거부 (#2). - 근본 원인: 클래스 레벨
@RequiresPermissionpointcut 이 있는 컨텍스트에서, 그 annotation 이 붙은 클래스를 Spring bean 으로 등록하는 행위 자체가 두 증상의 공통 원인. bean 등록 = advisor 매칭 = 프록시 + 런타임 집행. 스케줄러 전용 시스템 use case 는 둘 다 비의도. - 트리거 조건:
@RequiresPermission클래스-레벨 annotation + 해당 클래스의 bean 등록 + (a)final또는 (b) 비인증 스레드(scheduler/batch)에서의 호출.
Sources / 근거
- 로컬 검증: bootRun 로그 3회 (
/tmp/bootrun{2,3,4}.log) — 수정 전 기동 실패/틱 ERROR, 수정 후Started CaSkeletonApplication in 3.394 seconds+ 3틱 이상 ERROR 0건 +/api/healthcheckHTTP 200 (locally-verified). - 수정 후 회귀:
./gradlew :application-core:test(outbox 3개 클래스 41건 포함 green),:app-bootstrap:test224/224 PASS (ArchUnit 48 rules + Testcontainers 계약 5종 실행),verifyCleanArchitectureDependenciesPASS. - Spring 공식 문서 인용은 미보강 (
needs-confirmation— proxy-target-class 기본값 및 method security 의 fail-closed 동작에 대한 reference 절 인용 권고).
해결 / Resolution
- 적용한 조치:
PublishPendingOutboxEventsUseCase를 context bean 에서 제외 —OutboxConfig의 단독@Bean제거,outboxRelayScheduler@Bean메서드 내부에서 수동 조립(계약 테스트와 동일 방식).OutboxRelayScheduler는@Component스캔 제거 후OutboxConfig@Bean등록으로 이전 (@ConditionalOnProperty게이트는@Bean메서드로 이동, 동일 property). use case 는 canonical 형태인public final class복원. 두 클래스 Javadoc 에 "bean 으로 등록하면 안 되는 이유" 제약 명시. - 검증 방법: bootRun 기동 + healthcheck 200 + relay 3틱 ERROR 0건; 위 Gradle 회귀 전부 green.
- 잔여 위험:
outbox:relay권한은 런타임 미집행(선언적 convention). 실제 집행이 필요해지면 스케줄러에 시스템 principal(SecurityContext) 을 세우고 role registry 에 권한을 매핑하는 별도 설계 결정 필요 — 보안 설계 확장이므로 리뷰 체인 몫.
회고 / Lessons
- 빨리 감지하는 신호: "Could not generate CGLIB subclass … final class" 가
@Bean등록 빈에서 나오면, 어떤 Advisor 가 그 빈을 매칭하는지부터 추적 (@RequiresPermission/@Transactional/@Observed류 클래스-레벨 pointcut).final제거는 증상 치료 — 프록시가 "왜" 생기는지가 근본 질문. - 예방 체크리스트: 클래스-레벨 annotation pointcut 이 있는 프로젝트에서 그 annotation 이 붙은 타입을 bean 으로 등록할 때는 (1) final 여부, (2) 호출 스레드의 SecurityContext 유무를 함께 점검. 스케줄러/배치 전용 use case 는 bean 등록 대신 수동 조립을 기본으로.
- 검출 공백: minimal-context 계약 테스트는 풀 컨텍스트 배선 결함을 못 잡는다 — 풀 컨텍스트 smoke 테스트(
@SpringBootTest+ Testcontainers context-load)가 없으면 이 부류는 bootRun 에서만 터진다 (개선 후보). - wiki 일반화 후보: "클래스-레벨 AOP pointcut 환경에서 bean 등록은 곧 '프록시 + 런타임 집행' 옵트인이다 — 선언만 원하면 bean 으로 만들지 마라" (wiki/concepts 추출 후보).
Related / 관련
- 관련 에러: raw/errors/spring-configuration-bean-factory-method-not-processed-2026-06-11 — 같은 "Spring 등록 방식이 처리 여부를 결정한다" 계열, raw/errors/testcontainers-two-context-shared-datasource-close-2026-06-11 — 같은 branch 의 계약 테스트 배선 문제.