
DI 컨테이너는 겉으로 보면 꽤 조용한 도구다. 클래스를 하나 달라고 하면 인스턴스가 나오고, 모듈을 부트스트랩하면 provider들이 알아서 이어진다. 문제는 “알아서”라는 단어가 커질수록 코드가 점점 마법처럼 보인다는 데 있다. 마법은 데모에서는 멋지지만, 장애가 나면 갑자기 로그를 남기지 않는 동료가 된다.
fluo DI 시리즈는 그 마법을 일부러 풀어보는 글 묶음이다. decorator가 어떤 정보를 남기고, token과 provider가 어떤 주소 체계를 만들고, module graph가 왜 컴파일되어야 하며, resolve() 한 번이 실제 new 호출까지 가는 동안 어떤 결정을 지나치는지 차례대로 따라간다. DI를 쓰기만 할 때는 몰라도 되는 내부일 수 있지만, DI를 만들거나 오래 운영하려면 결국 한 번은 마주치는 질문들이다.
이 시리즈는 DI를 쓰는 법보다, DI가 동작하기 위해 무엇을 숨기고 무엇을 명시해야 하는지에 대한 기록에 가깝다.
시리즈 구성
- Decorator와 Metadata — fluo가 reflect-metadata를 버린 이유 레거시 decorator metadata에 기대지 않고 표준 decorator와 명시적 토큰 선언을 선택한 이유를 다룬다.
- Token과 Provider — DI의 주소 체계와 등록 단위 token이 컨테이너의 주소가 되는 방식과 class/value/factory/existing provider의 의미를 정리한다.
- Module Graph 설계 — 타입 체계와 가시성 모델 module을 단순한 묶음이 아니라 provider visibility boundary로 보고 imports/exports의 경계를 살핀다.
- Module Graph 컴파일 — DFS, 가시성 검증, 에러 설계 module graph를 컴파일하면서 순환, 가시성, 잘못된 export를 어떻게 검증하는지 확인한다.
- Bootstrap & Lifecycle — 앱이 살아나는 6단계 애플리케이션이 부트스트랩되며 provider와 lifecycle hook을 어떤 순서로 다루는지 따라간다.
- Resolution Planning — 조회 캐시와 스코프 라우팅
resolve()요청이 들어왔을 때 컨테이너가 어떤 경로로 provider를 찾고 scope를 결정하는지 본다. - Instance Creation — resolve에서 new까지 dependency를 모으고 constructor를 호출해 실제 인스턴스를 만드는 마지막 단계를 살펴본다.
- Scope & Disposal — 인스턴스의 생애주기와 정리 singleton, request, transient 같은 scope와 disposal이 왜 함께 설계되어야 하는지 다룬다.
- Testing — DI 구조가 테스트를 쉽게 만드는 방식 provider override와 module boundary가 테스트 가능성을 어떻게 만들어내는지 확인한다.
- Ecosystem — DI 위에서 동작하는 패키지들 DI 컨테이너 위에 config, web, scheduler 같은 패키지가 얹힐 때 어떤 구조가 필요한지 정리한다.
추천 읽기 순서
처음 읽는다면 1편부터 4편까지는 순서대로 읽는 것이 좋다. decorator, token, provider, module graph는 서로 따로 떨어진 개념처럼 보이지만, 실제로는 DI 컨테이너의 주소와 지도에 해당한다. 주소가 흔들리면 길찾기는 대체로 감으로 바뀐다. 감으로 돌아가는 DI는 처음에는 빠르지만, 나중에는 미신에 가까워진다.
컨테이너가 실제로 인스턴스를 만드는 흐름이 궁금하다면 5편부터 8편까지 이어서 읽으면 된다. 이 구간에서는 bootstrap, resolution planning, instance creation, disposal이 하나의 생애주기로 연결된다. 객체는 태어나는 순간보다 언제 사라지는지가 더 중요할 때도 있다. 사람도 그렇지만, DI 컨테이너에서는 특히 그렇다.
테스트와 확장 구조가 목적이라면 9편과 10편을 마지막에 읽으면 된다. 앞선 설계가 테스트에서 어떤 식으로 보상받는지, 그리고 ecosystem 패키지가 DI 위에 올라갈 때 어떤 경계가 필요한지 확인할 수 있다.
결국 마법을 줄이는 설계
DI는 편리함을 주지만, 그 편리함이 어디에서 오는지 설명할 수 없으면 금방 불안해진다. @Inject() 하나로 모든 것이 연결되는 것처럼 보이지만, 실제로는 token 선택, provider 등록, module visibility, scope routing, lifecycle ordering 같은 결정이 쌓여 있다. 이 결정들이 명시적일수록 컨테이너는 덜 마법적이고 더 디버깅 가능한 도구가 된다.
이 시리즈의 목표는 DI를 신비롭게 만드는 것이 아니다. 오히려 반대다. 겉으로는 조용히 동작하더라도, 안쪽에서는 어떤 선택이 있었는지 추적할 수 있어야 한다. 좋은 DI 컨테이너는 개발자가 컨테이너를 믿게 만드는 도구가 아니라, 컨테이너를 의심할 때 확인할 수 있는 단서를 남기는 도구다.
댓글
댓글을 불러오는 중...