dependency-management

2 개의 포스트

코드 품질 개선 기법 27편: 티끌이 모여 태산이 되듯 의존성도 쌓이면 (새 탭에서 열림)

의존성 주입(Dependency Injection)은 코드의 유연성을 높이는 강력한 도구이지만, 명확한 목적 없이 모든 요소를 주입 대상으로 삼는 것은 오히려 코드 복잡도를 높이고 유지보수를 어렵게 만듭니다. 참조 투명한 유틸리티나 단순한 모델 클래스까지 외부에서 주입받기보다는, 복잡도가 낮거나 변경 가능성이 희박한 객체는 내부에서 직접 생성하는 것이 더 효율적일 수 있습니다. 따라서 의존성을 주입할 때는 객체의 라이프사이클 관리, 구현체 전환, 테스트 용이성 등 구체적인 목적이 있는지 먼저 검토해야 합니다. **불필요한 의존성 주입의 사례와 개선** * **과도한 주입의 예시**: 뉴스 기사 스니펫을 생성하는 `LatestNewsSnippetUseCase` 클래스에서 데이터 모델인 `NewsSnippet`의 팩토리 함수나, 단순한 문자열 포매터인 `StringTruncator`까지 생성자로 주입받는 경우입니다. * **개선 방향**: 상태를 갖지 않는 단순한 유틸리티 구현체나 데이터 모델의 생성자는 클래스 내부에서 직접 호출하도록 변경합니다. * **단순화 결과**: 환경에 따라 달라지는 값(Locale)이나 네트워크 통신 등 복잡한 로직을 가진 리포지터리만 주입 대상으로 남겨 코드를 더 간결하게 유지할 수 있습니다. **의존성 주입이 필요한 명확한 목적** * **라이프사이클 및 범위 관리**: 객체의 상태를 공유해야 하거나, 사용하는 객체보다 더 긴 수명을 가진 객체를 활용해야 할 때 주입을 사용합니다. * **의존성 역전(DIP)**: 모듈 간의 순환 의존성을 해결하거나 아키텍처에서 정의한 계층 간 의존 방향을 준수하기 위해 필요합니다. * **구현체 전환 및 분리**: 테스트나 디버깅을 위해 Mock 객체로 교체해야 하는 경우, 혹은 빌드 속도 향상을 위해 독점 라이브러리를 분리해야 할 때 유용합니다. **무분별한 주입이 초래하는 문제점** * **추적의 어려움**: 인터페이스와 주입이 남발되면 특정 로직의 실제 동작을 확인하기 위해 생성자의 호출자를 거꾸로 추적해야 하는 수고가 발생합니다. * **호출자 책임 전가**: 하위 클래스의 모든 의존성을 상위 호출자가 해결해야 하므로, 의존성 해결이 연쇄적으로 전달되어 메인 클래스에 과도한 책임이 집중됩니다. * **연관 데이터의 일관성 파괴**: 예를 들어 동일한 `Locale` 값을 사용해야 하는 여러 객체를 각각 주입받을 경우, 실수로 서로 다른 로케일이 전달되어도 컴파일 타임에 이를 감지하기 어렵고 테스트 작성이 까다로워집니다. 의존성 주입은 '할 수 있기 때문'이 아니라 '필요하기 때문'에 수행해야 합니다. 복잡한 비즈니스 로직이나 외부 시스템 의존성은 주입을 통해 유연성을 확보하되, 단순한 값 객체(Value Object)나 유틸리티는 직접 인스턴스화하여 코드의 명확성을 높이는 것을 권장합니다.

코드 품질 개선 기법 14편: 책임을 부여하는 오직 하나의 책임 (새 탭에서 열림)

단일 책임 원칙(SRP)을 기계적으로 적용하여 클래스를 과도하게 분리하면, 오히려 시스템 전체의 복잡도가 증가하고 사양의 제약 조건을 파악하기 어려워질 수 있습니다. 코드 품질을 높이기 위해서는 개별 클래스의 응집도뿐만 아니라, 분리된 클래스들이 맺는 의존 관계와 호출자가 짊어져야 할 관리 부담을 종합적으로 고려해야 합니다. 결국 핵심적인 제약 조건을 한곳에서 관리할 수 있다면, 약간의 책임이 섞여 있더라도 초기 구현의 단순함을 유지하는 것이 더 나은 선택일 수 있습니다. **과도한 책임 분리가 초래하는 문제** * 동적으로 실행 로직이 변하는 '론치 버튼'을 구현할 때, 버튼 바인딩 책임과 로직 선택 책임을 별도 클래스로 분리하면 각 클래스는 단순해지지만 시스템 구조는 복잡해집니다. * 로직별로 별도의 바인더 인스턴스를 생성하고 `isEnabled` 상태를 통해 실행 여부를 제어하게 되면, 버튼 하나에 여러 개의 리스너가 등록되는 등 내부 동작을 추적하기 어려워집니다. * 결과적으로 "단 하나의 로직만 실행되어야 한다"는 비즈니스 제약 조건을 확인하기 위해 여러 클래스와 루프 문을 모두 훑어야 하는 비용이 발생합니다. **제약 조건의 분산과 상태 중복** * 책임을 분리하면 특정 사양이 코드 전체로 흩어지는 '책임 떠넘기기' 현상이 발생할 수 있습니다. * 예를 들어 어떤 로직이 활성화되었는지 나타내는 상태를 상위 클래스(Selector)에 추가하면, 하위 클래스(Binder)의 `isEnabled` 속성과 데이터가 중복되어 상태 불일치 문제가 생길 위험이 있습니다. * 이러한 중복은 코드의 신뢰성을 떨어뜨리며, 사양 변경 시 수정해야 할 포인트가 늘어나는 결과를 초래합니다. **의존성 비대화와 라비올리 코드(Ravioli Code)** * 세부 사항을 은닉하기 위해 의존 관계를 더 잘게 쪼개면, 이를 조합해야 하는 호출자(Caller)의 코드가 비대해지는 '갓 클래스(God Class)' 현상이 나타날 수 있습니다. * 너무 작은 단위로 쪼개진 클래스들이 서로 얽히면 전체 흐름을 파악하기 위해 수많은 파일을 오가야 하는 '라비올리 코드'가 되어 유지보수성이 저하됩니다. * 객체 지향의 핵심은 캡슐화인데, 제약 조건을 보장하는 로직을 분리해버리면 오히려 캡슐화가 깨지고 외부 의존성만 강해지는 부작용이 생깁니다. **실용적인 설계를 위한 제언** 클래스를 분할할 때는 응집도라는 단일 지표에만 매몰되지 말고, 분할 후의 의존성 그래프와 호출자의 편의성을 반드시 확인해야 합니다. 만약 특정 클래스가 내부에서 핵심 제약 조건을 깔끔하게 관리하고 있다면, 억지로 책임을 나누기보다 그 응집된 구조를 유지하는 것이 시스템 전체의 결합도를 낮추고 코드의 가독성을 높이는 길입니다.