2026-07-26-TIL: 진짜 Spring 내부 읽기와 BeanFactoryPostProcessor 등록 순서
요즘은 Spring을 두 방향에서 보고 있다. spring-lite에서는 직접 만든 축소판을 원고와 시각화 페이지로 정리했고, 새로 시작한 spring-internals-lab에서는 실제 Spring Framework 6.2 소스를 읽고 실험으로 확인했다. 만든 것과 진짜를 나란히 두니, 내가 축소하면서 생략한 부분이 어디인지 더 분명하게 보였다.
최근에 한 작업
spring-internals-lab: 실제 소스를 실험으로 읽기
주제마다 “공식 문서 읽기, 최소 예제, 디버깅, 테스트, 축소 구현” 순서를 밟도록 학습 계획을 정리하고, Claude Code가 그 계획을 따라가도록 저장소 규칙 파일에 연결했다. 빈 조회 경로, refresh() 순서, 생명주기 콜백, 후처리기, 컴포넌트 스캔, @Configuration 프록시, JDK/CGLIB 프록시 선택까지 차례로 다뤘고, 최근에는 애노테이션 기반 자동 프록시 생성기까지 올렸다.
디버깅 단계에서 한 번 막혔다. 터미널 세션에서 jdb에 명령을 파이프로 넘기면 브레이크포인트 도달과 명령 실행이 맞물리지 않아 스크립트로 돌릴 수가 없었다. 그래서 IDE 디버거가 쓰는 것과 같은 com.sun.jdi API로 자식 JVM을 띄우고, 브레이크포인트에 걸릴 때마다 호출 스택과 지역 변수를 출력하는 작은 추적 도구를 만들었다.
spring-lite와 다른 프로젝트
spring-lite는 원고에 실제 Spring 소스 참조를 붙이고, 같은 시나리오를 Spring Boot 앱과 spring-lite 앱으로 각각 돌려 보는 비교 샘플을 추가했다. 순서와 분기를 브라우저에서 조작해 보는 시각화 페이지도 만들었다.
monticker에서는 일부 저장소를 MongoDB로 옮기고 검색에 Elasticsearch를 붙였다. DB가 기준이고 ES는 검색 레이어라서, 인덱싱이 실패해도 경고만 남기고 트랜잭션에는 영향을 주지 않는다. iterview는 따로 있던 백엔드와 프론트엔드를 모노레포로 합치고 CI를 하나로 모았다.
addBeanFactoryPostProcessor로 넘긴 처리기는 스캔보다 먼저 돈다
이번에 가장 기억에 남는 건 후처리기 실험에서 만난 버그다. @Excluded가 붙은 빈 정의를 레지스트리에서 지우는 BeanDefinitionRegistryPostProcessor를 만들었는데, 아무 예외 없이 빈이 그대로 남아 있었다.
1
2
3
4
context.register(RewriterConfig.class); // @ComponentScan 포함
context.addBeanFactoryPostProcessor(new ExclusionRegistryPostProcessor());
context.refresh();
// excludedService가 여전히 등록돼 있다
원인은 PostProcessorRegistrationDelegate의 실행 순서에 있었다.
addBeanFactoryPostProcessor()로 넘긴, 빈이 아닌 처리기를 가장 먼저 실행한다.- 빈으로 등록된 레지스트리 후처리기를
PriorityOrdered,Ordered, 나머지 순으로 실행한다.@ComponentScan을 처리하는ConfigurationClassPostProcessor가PriorityOrdered그룹에 있다. - 마지막으로
postProcessBeanFactory()와 일반BeanFactoryPostProcessor를 실행한다.
내 처리기는 1단계에서 돌았고, 지우려던 빈은 2단계의 스캔에서야 등록됐다. 지울 대상이 아직 없을 때 지우려 한 것이다. 처리기를 registerBean()으로 빈으로 등록하자 스캔 이후로 밀려나 정상적으로 동작했다. 반면 scope나 lazy 같은 메타데이터 수정은 처음부터 잘 됐다. 레지스트리가 확정된 뒤 단계에서 일어나는 작업이라 등록 방법의 영향을 받지 않았다.
addBeanFactoryPostProcessor()가 가장 먼저 도는 건 부트스트랩 이전에 반드시 실행돼야 하는 처리기를 위한 통로이기 때문이다. 다만 스캔 결과에 의존하는 로직에는 이 “가장 먼저”가 함정이 된다.
Claude Opus 5
얼마 전 Anthropic이 Claude Opus 5를 공개했다. 회사 설명으로는 상위 모델인 Fable 5에 가까운 성능을 절반 가격에 내고, 가격은 Opus 4.8과 같으면서 성능은 크게 올랐다고 한다. 코딩과 지식 작업 벤치마크에서 최고 성능이라는 주장도 함께 실었는데, 모두 회사가 고른 평가 기준이라는 점은 감안해야 한다.
개인적으로 눈에 띈 설명은 결과를 스스로 검증하고 성공할 때까지 신중하게 반복하는 능력이 좋아졌다는 부분이다. 이번 후처리기 버그처럼 예외 없이 조용히 틀리는 문제는, 실행 결과를 다시 확인하지 않으면 그냥 지나간다. 에이전트가 “돌았다”에서 멈추지 않고 “의도대로 됐는가”까지 확인해 준다면 실험 루프가 꽤 짧아질 것 같다.
API에서는 지능과 토큰 사용량 사이를 고르는 effort 설정, 더 빠르게 응답하는 대신 비싼 fast mode 같은 선택지도 같이 나왔다. 같은 모델이라도 어떤 작업에 얼마나 생각하게 할지를 사용자가 고르는 방향으로 가는 것 같다.
참고:
느낀 점
spring-lite에서 조건부 빈을 스캔 시점에 평가했다가 순서 문제를 겪었는데, 실제 Spring에서도 같은 종류의 문제를 다른 자리에서 만났다. 무엇을 하느냐만큼 레지스트리가 어느 상태일 때 하느냐가 중요하다. 확장 지점의 인터페이스와 등록 방법이 곧 “어느 단계에 끼어드는가”를 정한다.
예상을 먼저 적고 실행해서 틀린 지점을 기록하는 방식도 효과가 있었다. 등록 방법과 상관없이 실행 시점이 같을 거라는 예상이 정면으로 틀렸기 때문에 이 버그가 문서의 중심이 됐다.
다음에 확인할 것
- 수동 프록시와 자동 프록시 생성의 차이를 문서로 정리하기
- 트랜잭션 전파 실험으로 넘어가고, spring-lite의
REQUIRED단일 전파와 비교하기 - 직접 만든 축소 컨테이너에 후처리기 단계를 넣을 때, 이번 등록 순서 문제까지 재현할지 정하기
댓글
아직 댓글이 없습니다