Files
llm-wiki/raw/errors/method-security-class-pointcut-final-usecase-bean-2026-06-12.md
T

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
feature-domain-event-outbox-contract
ca-skeleton
error
ca-skeleton
spring
spring-security
method-security
cglib
aop
outbox
scheduler
2026-06-12 resolved

error: method-security-class-pointcut-final-usecase-bean-2026-06-12

Layer: raw/errors/ — 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다.

Parent / 부모

증상 / 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

  1. adapter-web MethodSecurityConfigAnnotationMatchingPointcut.forClassAnnotation(RequiresPermission.class) 를 포함한 union pointcut 의 AuthorizationManagerBeforeMethodInterceptor Advisor 를 등록한 상태.
  2. @RequiresPermission 이 클래스 레벨에 붙은 final 클래스를 @Bean 으로 등록 (OutboxConfig.publishPendingOutboxEventsUseCase).
  3. 기동 → auto-proxy 가 Advisor 매칭 빈을 CGLIB 서브클래싱 시도 → Cannot subclass final class 로 컨텍스트 refresh 실패. (Spring Boot 기본 spring.aop.proxy-target-class=true — 인터페이스가 있어도 CGLIB.)
  4. final 만 제거하면 기동은 성공하지만, @Scheduled 스케줄러 스레드에는 Authentication 이 없으므로 use case 호출 시마다 AuthenticationCredentialsNotFoundException — relay 가 한 건도 처리 못 함.

조사 단계 / Investigation log

  • 2026-06-12 — bootRun 로그 첫 실패는 Flyway Connection to localhost:5432 refusedca-pg PostgreSQL 컨테이너가 18시간 전 Exited (restart policy no, 재부팅 후 자동 시작 안 됨). docker start ca-pg 로 해소 (환경 문제, 코드 무관).
  • 2026-06-12 — 두 번째 실패가 CGLIB Cannot subclass final class. 동작하는 4개 sample use case (CreateWorkLogUseCase 등) 와 대조 → 전부 @RequiresPermission + non-final public 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).
  • 근본 원인: 클래스 레벨 @RequiresPermission pointcut 이 있는 컨텍스트에서, 그 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/healthcheck HTTP 200 (locally-verified).
  • 수정 후 회귀: ./gradlew :application-core:test (outbox 3개 클래스 41건 포함 green), :app-bootstrap:test 224/224 PASS (ArchUnit 48 rules + Testcontainers 계약 5종 실행), verifyCleanArchitectureDependencies PASS.
  • Spring 공식 문서 인용은 미보강 (needs-confirmation — proxy-target-class 기본값 및 method security 의 fail-closed 동작에 대한 reference 절 인용 권고).

해결 / Resolution

  • 적용한 조치: PublishPendingOutboxEventsUseCasecontext 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 추출 후보).