의존성 역전이 필요했던 이유와 Inversify 도입기
코드 구조를 명확히 분리하는 일은 더 이상 선택이 아닌 필수가 되었습니다. 필수가 된 이유는 이전 프로젝트부터 시작된 얽히고 섥힌 코드들 때문이었습니다. 팀은 Express 기반의 자유로운 개발 환경에서 출발했지만 물론 너무 자유로웠고 그로 인해 발생한 구조적 문제들을 해결하기 위해 점진적으로 구조 개선을 해나갈 수 있었습니다. 다를 수 있지만 저에겐 아주 중요한 경험이었습니다.
Controller - Service - Repository 구조로 설계를 개선한 이후, 의존성 관리를 효율화하고 테스트 가능한 구조를 만들기 위해 Inversify 기반의 DI(Dependency Injection)를 도입하게 되었습니다. 이 과정을 통해 결과적으로 DIP(Dependency Inversion Principle)의 기반도 마련할 수 있었습니다.
Controller → Service → Repository 구조로의 전환
팀 내 코드 문제는 명확했습니다. 각자 다른 방식으로 작성된 코드가 섞여 있었고, 중복 로직, 함수 간 강한 결합, 유지보수의 어려움이 일상적으로 발생하고 있었습니다. 이를 해결하기 위해 우리는 명확한 3 Layered Architecture를 도입했고,
각 계층의 책임은 다음과 같이 정의했습니다.
- Controller: 요청과 응답의 흐름을 제어하고, 외부 요청을 서비스로 위임 / 데이터 변환은 해당 Controller에서
- Service: 비즈니스 로직을 담당하며, 복잡한 로직을 Controller로부터 분리 / 비즈니스 로직의 판단을 명확히
- Repository: DB 접근 로직을 담당하고, 데이터베이스 처리에 집중
이러한 구조적 개선은 매우 효과적이었습니다. 책임과 관심사를 분리(SoC: Separation of Concern)함으로써 코드의 복잡성을 줄이고, 서로의 코드를 이해하고 규칙에 맞게 코드를 작성할 수 있었습니다. 또한, 새로운 팀원이 프로젝트를 빠르게 이해하는 데에도 큰 도움이 될것입니다.(실제로 들어오진 않았지만, 제가 신입이었을때 마주했던 당혹감은 없앴음을 확신합니다.)
특히 우리 회사처럼 지속적으로 유지보수인 척 하면서 새로운 기능이 요구되는 환경에서는, 기능별로 코드가 잘 분리되어 있다는 것이 업무 효율을 크게 끌어올려 주었습니다. 책임과 역할을 갖고 있는 코드가 어떤 파일에 어떻게 존재해야하는지 명확하게 이해했기 때문에 코드 작성시에 고민을 없앨 수 있었고, 전 코드보단 깔끔한 코드를 작성할 수 있었습니다.
Express의 한계, 그리고 Inversify 도입
하지만 여기서 또 하나의 문제가 발생했습니다. Express는 Nest처럼 의존성을 자동으로 주입해주는 DI Container 기능을 기본으로 제공하지 않기 때문에, 앞서 말한 구조 — Controller → Service → Repository로 계층을 나눈 구조를 실제로 구현하고 유지하려면, 의존성을 효율적으로 관리해줄 외부 도구의 도움이 필요했습니다.
그래서 선택한 것이 InversifyJS였습니다. Inversify는 IoC(제어의 역전, Inversion of Control) 을 지원하는 라이브러리로, 클래스 간의 의존성을 외부 컨테이너가 관리하게 도와줍니다.
왜 DI(Dependency Injection)가 필요했는가
DI를 도입한 가장 큰 이유는 매번 new 키워드를 통해 인스턴스를 직접 생성하는 것은 클래스 간의 결합도가 높아집니다. 이런 구조는 결국 테스트할 때 Mock이나 Stub으로 대체할 수 없는 상황으로 이어졌기 때문입니다. (실제로 모든 코드에 테스트 코드를 작성하지는 못했지만, 프로젝트가 종료되면 하나씩 테스트 코드를 작성해 추후 유지보수에도 힘을 쏟으려고 노력합니다.)
예를 들어, Service가 특정 Repository 구현체를 직접 생성하고 있다면, 테스트 환경에서는 이를 다른 구현으로 바꾸기 어렵습니다. 결국 테스트를 위해 실제 DB와 연결하거나, 설정을 억지로 바꾸는 수밖에 없게 되죠. 이 구조는 유지보수 측면에서도 큰 리스크를 안고 있습니다. 그래서 Inversify를 도입해 다음과 같은 구조를 만들었습니다.
- 필요한 인터페이스나 클래스는 모두 Inversify Container에 등록
- 클래스 간의 의존성은 @inject() 데코레이터를 통해 외부에서 주입받도록 구성했습니다.
결과적으로 컨테이너 덕분에 직접 new 키워드를 사용하는 대신, 필요한 객체를 주입(inject) 받을 수 있어, 객체 생성과 의존성 관리 책임을 코드에서 분리할 수 있었습니다.
팀원 설득, 그리고 구조적 전환
새로운 개념(Inversify, DI Container, Interface 기반 설계 등)을 팀원들에게 설명하고 이해시키는 과정은 쉽지 않았습니다. 하지만 우리가 겪은 문제들(테스트의 어려움, 유지보수 시 확장성 부족 등)을 구체적으로 예시로 들어 설명했고, 그 결과 모든 팀원이 의존성 역전의 필요성에 공감하게 되었습니다. 추가적으로 싱글톤 패턴으로 Container에서 관리하게 만들었습니다.
현실적인 DI 구조: 전부 인터페이스일 필요는 없다
사실 모든 곳이 확장 가능성이 충분히 존재합니다. 그래서 "각 계층은 구현이 아닌 인터페이스에만 의존해야 한다"고 이야기하지만, 실제 현업에서는 모든 컴포넌트에 인터페이스를 정의하는 것은 불필요한 오버엔지니어링이 될 수 있습니다. 어쨌든 Interface에 관련된 코드를 계속해서 작성해야 하기 때문입니다. 그래서 테스트 가능성과 확장 가능성이 중요한 부분에 인터페이스를 정의하고(DB, Redis, 파일 저장소), 그 외에는 단일 구현 클래스만을 사용함으로써 개발 효율과 유연성 사이의 균형을 맞췄습니다.
일부 컴포넌트는 테스트나 확장 가능성이 높은 곳에만 명시적으로 인터페이스를 정의했습니다. 그 외의 많은 컴포넌트는 단일 책임을 가진 클래스 형태로, SRP(Single Responsibility Principle)를 지키며 Container에 직접 주입했습니다. 즉, DI를 위한 구조를 취하되 꼭 불필요한 추상화를 강제하진 않았습니다. 늘 하던 스타일이 아닌 다른 스타일로 코드를 작성하는 상황에서 팀원의 불안감을 높일 이유는 없다고 판단했습니다. 결과적으로 적절한 유연성과 명확한 책임 분리를 동시에 달성했다고 생각하는 부분입니다.
Inversify를 도입하고 난 후
Inversify 기반 DI 구조를 적용한 이후, 테스트 용이성과 코드 확장성이 눈에 띄게 향상되었습니다. 특히 기능 추가 시 영향 범위를 명확히 파악할 수 있어 유지보수에 드는 비용이 줄어들었고, 리뷰 효율 또한 향상되었습니다.
결과적으로 Inversify를 통한 의존성 역전은 단순한 코드 리팩토링 수준을 넘어서, 팀 전체의 개발 문화를 "결합도를 낮추고 역할을 분리하는 방향"으로 정착시키는 계기가 되었습니다. 지금도 새로운 기능이나 모듈을 설계할 때 가장 먼저 고려하는 것은 "이 로직이 어디에 위치해야 하며, 어떤 책임을 가져야 하는가?"입니다.
InversifyJS
https://inversify.io/docs/introduction/getting-started/
Getting started | InversifyJS
Start by installing inversify and reflect-metadata:
inversify.io
'Project > 기록' 카테고리의 다른 글
객체지향의 사실과 오해 - 2 (0) | 2025.05.10 |
---|---|
객체지향의 사실과 오해 - 1 (0) | 2025.05.10 |
회사 코드 문화를 바꾸었다 - 완 (1) | 2025.05.07 |
회사 코드를 바꿔보자 - 2 (0) | 2025.05.07 |
회사 코드를 바꿔보자 - 1 (0) | 2025.05.07 |