포스트

2026-07-09-TIL: spring-lite 시작과 조건부 빈 평가 시점

이번 주에는 Spring의 핵심 기능을 직접 구현해 보는 학습용 프레임워크 spring-lite를 새로 시작했다. 목표는 Spring과 호환되는 API가 아니라, IoC, DI, MVC, AOP, 트랜잭션, 부트스트랩을 작은 단위로 쪼개 직접 만들면서 각 계층이 어떤 책임을 지는지 코드로 설명할 수 있게 되는 것이다. 같은 기간에 monticker에서는 모놀리스 일부를 별도 서비스로 떼어내는 작업을 이어갔다.

이번 주에 한 작업

spring-lite: 뼈대에서 MVP까지

core, context, aop, web, jdbc, tx, boot 같은 모듈로 나누고, 모듈 사이 순환 의존을 금지한다는 원칙을 결정 기록으로 남기는 것부터 시작했다. 그 위에 애노테이션 기반 컨텍스트와 생성자 주입, 프록시 기반 @Transactional과 JdbcTemplate, MVC 디스패처, @ConditionalOn... 계열을 쓰는 자동 설정까지 MVP로 올렸다. 트랜잭션 전파는 REQUIRED 하나만 두고 롤백 규칙을 지원하는 정도로 범위를 좁혔다.

구현한 내용을 바탕으로 책 형태의 원고 초안도 썼다. 직접 설명하다 보니 빠진 기능이 보여서, 전역 예외 처리나 인터셉터 체인 같은 것을 다시 추가하고 원고를 구현 범위에 맞게 고쳤다.

monticker: 서비스 분리와 기동 문제

DDD 방향으로 도메인 메서드를 엔티티로 옮기고, 금액과 가격을 값 객체로 바꾸고, 조회와 명령 서비스를 나눴다. 워커는 환경 변수로 역할을 나눠 같은 이미지로 띄울 수 있게 했고, 퀀트 엔진과 트레이딩 서비스를 별도 서비스로 분리했다. 레이트 리밋, 서킷 브레이커, Outbox, Saga 같은 패턴도 붙이고 그 이유를 결정 기록으로 남겼다. 이 작업은 Claude Code와 함께 진행했다.

분리한 서비스를 실제로 띄우는 과정에서 문제가 이어서 나왔다. Docker 이미지는 JDK 21인데 Gradle 툴체인은 17을 요구해서 빌드가 실패했고, 툴체인을 21로 맞췄다. 더 오래 걸린 건 엔티티 스캔이었다. 새 서비스의 메인 클래스는 새 패키지에 있는데 엔티티와 리포지토리는 기존 패키지에 남아 있어서 JPA가 엔티티를 찾지 못했다. Spring Boot의 스캔은 메인 클래스 패키지를 기준으로 하므로, 바깥 패키지는 @EntityScan과 @EnableJpaRepositories로 직접 지정해서 해결했다.

조건부 빈은 언제 평가해야 하는가

spring-lite에서 가장 오래 생각한 버그는 조건부 등록 문제였다. 처음 구현은 클래스를 스캔하는 순간 바로 @ConditionalOnBean, @ConditionalOnMissingBean을 평가했다. 그러면 결과가 설정 클래스의 등록 순서에 따라 달라진다.

  • @ConditionalOnBean(Dependency.class)가 먼저 평가되면, 아직 다른 설정의 Dependency가 등록되지 않아 빈이 빠진다.
  • 기본 빈에 @ConditionalOnMissingBean을 붙여 두면, 사용자 빈이 나중에 등록될 때 기본 빈이 물러나지 않고 둘 다 남는다.

고친 방식은 평가 시점을 미루는 것이다. 스캔 단계에서는 정의를 모두 등록하고 조건 정보만 따로 보관한 뒤, getBean이나 싱글톤 사전 생성처럼 정의를 실제로 쓰는 시점에 조건을 판정한다. 조건끼리 서로를 참조해 무한 루프에 빠지지 않도록 방문 중인 빈 이름도 같이 넘긴다.

실제 Spring Boot는 다른 길을 택했다. @ConditionalOnBean의 Javadoc은 이 조건이 지금까지 처리된 빈 정의만 볼 수 있으니 자동 설정 클래스에만 쓰라고 권한다. 자동 설정은 사용자 설정이 모두 처리된 뒤에 적용되므로, 사용자 빈을 보고 물러나는 동작이 성립한다. spring-lite는 평가를 지연해서, Boot는 처리 순서를 보장해서 같은 문제를 푼 셈이다.

Claude Fable 5 접근 재개

지난달 공개 며칠 만에 내려갔던 Claude Fable 5와 Mythos 5가 이번 달 초에 다시 열렸다. Anthropic은 발표 글 위에 접근을 중단한다는 공지를 붙였다가, 같은 자리에 다시 쓸 수 있게 됐다는 업데이트를 덧붙였다. 중단 사유는 미국 상무부가 수출 통제 규정을 근거로 내린 조치였다고 알려져 있다.

결과만 보면 3주 가까이 최상위 모델을 아무도 못 쓴 셈이다. 그 사이 Opus 4.8 같은 다른 모델은 그대로 돌아갔으니, 모델을 바꿔 끼울 수 있게 만들어 둔 쪽은 큰 문제가 없었을 것이다.

이번 일로 모델도 외부 의존성이라는 걸 다시 느꼈다. 라이브러리 버전을 고정하고 대체 경로를 두듯이, 모델 이름을 설정으로 빼고 한 단계 아래 모델로도 기능이 돌아가는지 확인해 두는 게 맞다.

참고:

느낀 점

조건부 등록 버그는 기능이 없어서가 아니라 판단을 너무 일찍 내려서 생긴 문제였다. 빈 정의가 아직 다 모이지 않은 시점에 “이 빈이 있는가”를 묻는 건 답할 수 없는 질문이다. Spring이 정의 등록, 후처리, 인스턴스 생성을 단계로 나눠 두는 이유를 직접 만들어 보고서야 실감했다.

monticker의 엔티티 스캔 문제도 결국 같은 종류였다. 어떤 기본값이 어디를 기준으로 정해지는지 모르고 패키지를 나누면, 컴파일은 되는데 기동 시점에야 깨진다.

다음에 확인할 것

  • spring-lite 원고를 실제 구현 코드와 다시 대조하면서 장별 설명을 보강하기
  • 실제 Spring Boot에서 사용자 설정에 @ConditionalOnBean을 붙였을 때 순서에 따라 결과가 어떻게 달라지는지 재현해 보기
  • monticker에서 분리한 서비스들이 기존 패키지의 엔티티를 공유하는 구조를 어디까지 유지할지
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.
고쳐 쓴 기록 1번 수정 ·
  1. docs(til): drop dates and internal codes and add ai news to the 2026-07-09 til

댓글

아직 댓글이 없습니다