서비스 간의 의존성을 줄여야 하는 이유는 무엇인가요
GPT 설전#
소프트웨어를 설계할때 모듈이나 서비스의 의존성을 줄여야 하는 이유가 뭐야? 어떤 문어발 서비스의 한 메서드 하나에만 의존하는 것이 직관적으로는 좋은 설계가 아닌 것 같지만 팀원을 설득하지 못하겠어. 단순한 메서드 하나를 사용하기 위해 사용하지도 않을 수많은 서비스에 의존한다고 사실 크게 바뀌는 것은 없어보이거든.
모듈이나 서비스 간의 의존성을 줄여야 하는 이유는 크게 다음과 같은 측면에서 살펴볼 수 있다. 단순히 "메서드 하나만 쓰니까 상관없다"라는 생각은, 당장의 구현에서는 편해 보일 수 있지만, 장기적으로 시스템의 유지보수성과 확장성에 문제를 일으킬 수 있다.
- 결합도(Coupling) 감소를 통한 유지보수성 향상:
- 특정 서비스가 다른 서비스에 깊이 의존할수록, 그 서비스가 변경될 때 의존하고 있는 모든 서비스나 모듈에 영향을 줄 수 있다.
- 예를 들어, "문어발 서비스"라고 할 정도로 많은 기능을 담고 있는 거대 서비스 A가 있다고 해보자. 이 서비스 A의 한 메서드에 의존하던 다른 모듈 B가 있다면, 서비스 A가 리팩토링되거나 분할될 때 모듈 B도 영향을 받아 변경을 강요당할 수 있다.
-
이러한 높은 결합도는 변경에 민감하게 반응하는 시스템을 만들어, 점진적 개선이나 리팩토링을 어렵게 만든다. 결과적으로 유지보수 비용 증가로 이어지며, 팀 전체가 해당 변경에 눈덩이처럼 휩쓸릴 수 있다.
-
재사용성과 유연성 향상:
- 의존성이 적은 모듈은 독립적으로 재사용하기가 훨씬 쉽다.
- 특정 기능을 필요로 하는 다른 서비스나 모듈에서 쉽게 가져다 쓸 수 있고, 새로운 환경(다른 프로젝트나 다른 마이크로서비스 컨텍스트)에서도 유연하게 적용할 수 있다.
-
의존성이 많은 서비스는 다른 곳에서 재사용하기 위해 불필요한 의존성들을 그대로 가져가야 하고, 이는 재사용을 사실상 제한한다.
-
테스트 용이성 개선:
- 테스트를 할 때, 의존성이 많은 모듈은 그 의존 대상들을 전부 모의(Mock)하거나 설정해야 하므로 테스트 환경 구성이 복잡해진다.
- 반면 의존성이 낮은 모듈은 해당 모듈 자체를 상대적으로 독립적으로 테스트할 수 있고, 이는 빠른 단위 테스트, 검증 작업, CI/CD 파이프라인에서의 피드백 사이클 단축에 큰 이점을 제공한다.
-
결국 테스트 자동화에도 긍정적인 영향을 주어 개발 생산성을 향상시키고, 품질을 높인다.
-
변경 파급 영향 최소화 (Isolation):
- 단순히 "메서드 하나만 사용한다" 하더라도, 이 메서드가 속한 서비스 전체 구조에 종속된 형태로 코드를 구성하면, 해당 서비스 내부 변경 시 영향 범위가 넓어진다.
-
필요하지 않은 의존성을 제거하면, 특정 기능이 변경될 때 연쇄적인 영향이나 예측 불가능한 버그 발생 가능성을 줄이고, 문제 해결의 범위도 보다 명확해진다.
-
성능 및 자원 관리 측면:
- 물론 처음에는 의존성이 많다고 해서 성능 자체에 직접적인 문제가 생기지 않을 수도 있다.
- 하지만 의존성이 많으면 종종 배포나 로딩 시점에 추가적인 컴포넌트 초기화, 네트워크 콜, 보안 토큰 관리 등의 부수 작업이 불가피해질 수 있다.
- 이런 점들이 쌓이면 결국 성능 저하나 자원 낭비로 이어질 수 있으며, 특히 클라우드 환경 또는 마이크로서비스 아키텍처에서 이러한 불필요한 의존성은 전체 시스템의 운영 비용을 증가시킨다.
정리하자면, 단순히 한 메서드만 쓰니까 지금 당장엔 문제 없어 보이지만, 장기적으로 봤을 때 유지보수, 확장성, 테스트 용이성, 안정성, 재사용성 등 다양한 관점에서 의존성이 적은 구조가 훨씬 유리하다. 이러한 점들을 팀원에게 강조하면, 현재의 단기 편의보다 장기적인 시스템 건강성을 위한 구조적 선택이 왜 중요한지 설득할 수 있을 것이다.
그렇다면 문어발 서비스 A의 한 메서드를 직접 사용하기보단 그 메서드가 필요로 하는 의존성만을 정의한 새 서비스B를 만들어 A가 B에 의존하게 만들고 원래 A에 의존하려던 서비스또한 B를 의존하게 만드는 것이 최선의 선택이야?
반드시 새 서비스 B를 만들어서 의존성을 갈라내는 것이 항상 “최선의 선택”인 것은 아니다. 그보다는 의존성을 잘게 쪼개고 모듈화하는 과정에서의 설계적 판단이 필요하다. 다음과 같은 관점을 고려해 볼 수 있다.
- 책임(Responsibility)의 명확화
- A 서비스 내에 필요한 단 하나의 메서드가 단독으로 의미 있는 기능 단위를 이루고 있고, 이를 다른 모듈들이 독립적으로 재사용할 필요가 있다면, 해당 기능을 별도의 서비스(B)로 추출하는 것은 타당한 전략이다.
-
이렇게 하면 B는 해당 기능에 대한 전담 서비스를 제공하고, A 및 C 등 다른 서비스들은 B에 의존하면서 각자의 주 책임을 유지한다.
-
도메인 경계(Domain Boundary) 재검토
- A 서비스가 “문어발”로 불릴 만큼 많은 기능을 담고 있다면, 그 기능들의 도메인적 연관성이 있는지 다시 살펴보자.
-
만약 A의 기능 중 하나가 사실상 독립된 도메인 로직이라면 B로 떼어내어 별도의 도메인 경계를 설정할 수 있다. 이렇게 하면 도메인 상의 결합도를 줄여 도메인 주도 설계(DDD) 관점에서도 이점을 얻을 수 있다.
-
단일 추상화계층(Interface) 혹은 Adapter 패턴의 적용
- 새로운 서비스를 만드는 대신, A 서비스 내 특정 기능에 대해 간단한 인터페이스나 Adapter를 제공하는 방법을 고려해볼 수 있다. 예를 들어, A가 제공하는 특정 기능에 대해 인터페이스(계약)를 정의하고, 이 인터페이스를 구현하는 어댑터를 사용해 필요한 부분만 노출한다.
-
이를 통해 서비스 C는 A 자체에 깊이 들어가지 않고, 필요한 기능에 대한 추상 인터페이스에 의존하게 된다. 추후 이 기능을 다른 서비스로 분리하거나 구현체를 바꾸기 용이하다.
-
과도한 분리로 인한 오버헤드 고려
- 새로운 서비스를 만든다는 것은 서비스 관리, 배포 파이프라인, 모니터링, 로깅, 보안, 인프라 구성 등을 추가로 고려해야 한다는 의미다. 팀 조직, 인프라 성숙도, 운영 부담 등을 감안해 과도한 서비스를 늘리는 것이 과연 가치 있는지 판단해야 한다.
-
즉, 분리가 가져다주는 설계적 이점과, 서비스를 분리함으로써 생기는 운영 비용을 균형감 있게 비교해야 한다.
-
점진적 개선(Incremental Refactoring)
- 반드시 한 번에 큰 변화를 줄 필요는 없다. 특정 기능이 자주 재사용되거나 다른 서비스들이 참조하는 빈도가 늘어날 때, 그 시점에서 해당 기능을 별도의 서비스로 추출하는 식으로 점진적으로 구조를 개선할 수 있다.
- 처음에는 단순히 A 서비스 내부에서 계층을 분리하고(예: 패키지, 모듈 단위) 인터페이스를 도입한 뒤, 필요시 해당 계층만 독립 서비스화 할 수 있다.
정리하자면, A 서비스의 한 메서드를 바로 B라는 별도 서비스로 추출하는 것은 가능한 전략 중 하나일 뿐, 언제나 최선은 아니다. 기능의 독립성, 도메인 경계 재설정 필요성, 팀 인프라 환경, 오버헤드 등을 종합적으로 검토한 뒤, 단순히 한 메서드 때문에 새로운 서비스를 만드는 것이 진정 설계적으로 의미 있는지 판단하는 것이 좋다.
A가 많은 책임을 가지고 있음과 동시에 많은 서비스에 의존하는 상황일 경우, 도메인 주도 설계의 관점에서 점진적으로 리팩토링 하는 방법에 대해서 설명해줘.
도메인 주도 설계(DDD) 관점에서 A 서비스가 과도한 책임을 지고 있고, 동시에 다양한 서비스에 의존하는 상황은 “경계가 명확하지 않은 도메인” 또는 “지나치게 복합적인 Bounded Context” 문제로 해석할 수 있다. 이를 점진적으로 개선하기 위한 접근 방법을 정리해보면 다음과 같다.
1. 현재 도메인 지형 파악하기 (Domain Landscape Understanding)#
- Ubiquitous Language(보편 언어) 재점검:
A 서비스가 맡고 있는 기능들과 개념을 정리해보며 비즈니스 영역에서 사용하는 용어들을 체계화한다. - 컨텍스트 매핑(Context Mapping):
A 서비스가 어떤 도메인 지식을 담고 있고, 다른 서비스나 모듈은 어떤 역할을 하며, 각 컨텍스트 간의 관계(파트너십, 고객-공급자 관계, 반부패 계층 등)를 그림으로 표현한다. - 핵심 도메인(Core Domain) 식별:
A 서비스 안에 있는 로직 중 비즈니스적으로 가장 중요한 부분(핵심 도메인), 일반적인 지원 로직(지원 서브도메인), 범용 서비스(범용 서브도메인)를 구분한다.
2. 의존성 구조 분석 및 경계 재설정#
- 모듈(패키지) 단위로 책임 분리:
초기 단계에선 바로 마이크로서비스로 분리하기보다는 기존 모놀리틱 구조 내에서 모듈과 패키지를 통해 논리적 경계를 만든다. - 예: A 서비스 내부를 도메인 개념별로 하위 모듈(A1, A2, A3...)로 나누어 책임을 명확히 한다.
- 도메인 모델 정제(Domain Model Refinement):
각 모듈마다 Aggregate, Entity, Value Object, Domain Service를 식별하고, Bounded Context를 명확히 정의한다. - 예: "주문(Order)" 도메인 모델과 "결제(Payment)" 도메인 모델을 명확히 나누고, 양자가 어떻게 협력하는지 정의한다.
3. 의존성 최소화를 위한 도메인 규칙 재배치#
- 도메인 규칙의 자체 완결성(Self-Containment):
가능한 한 도메인 로직은 해당 도메인의 Aggregate 내부에서 처리하도록 하여 외부 서비스에 대한 의존을 축소한다. - 예: A 서비스 안에서 "주문 검증" 로직이 다른 서비스에 분산되어 있다면, 이를 주문 도메인의 Aggregate 내부나 도메인 서비스로 모아둔다.
- Anti-Corruption Layer(ACL) 또는 Adapter 적용:
다른 컨텍스트(또는 외부 시스템)에 의존할 때는 도메인 모델에 직접 의존하기보다는 ACL이나 Adapter 패턴을 사용해 외부 모델로부터 도메인 모델을 보호한다. - 예: A 서비스(주문 도메인)가 B 서비스(고객 관리)의 기능을 사용할 때, 직접 B 서비스를 호출하기보다 CustomerContextAdapter를 두어 도메인 언어로 변환하는 레이어를 둔다.
4. 점진적 분리(Incremental Extraction)#
- 핵심 도메인 우선 분리:
가장 중요한 핵심 도메인 부분을 먼저 분리하여 독립된 Bounded Context로 만들고, 나중에 마이크로서비스로 독립시킨다. - 예: A 서비스 내 핵심 로직인 "주문 처리" 부분을 OrderContext로 독립 패키지화하고, A 서비스 내부에서 서비스-모듈 의존성을 OrderContext에 맞추어 정리한다.
- 단계별로 독립 서비스화:
초기에는 단순히 내부 모듈로 나누고, 테스트를 통해 안정성을 확보한 뒤, 그 모듈이 충분히 자립할 수 있게 되면 별도 서비스로 분리한다. - 예: OrderContext를 충분히 안정화한 뒤, 해당 컨텍스트를 별도 마이크로서비스로 독립 배포한다.
5. 리팩토링 사이클에서의 피드백 루프 마련#
- 테스트 기반 리팩토링:
모듈 분리를 시작하기 전, 충분한 단위/통합 테스트를 구축한다. 이렇게 하면 모듈이나 서비스 분리 과정에서 회귀 버그를 최소화할 수 있다. - 작은 단위의 변경 적용 후 모니터링:
한 번에 대규모 개편을 하지 말고, 작은 변경(예: 패키지 단위 리팩토링 -> 일부 기능 분리 -> ACL 도입)을 적용한 뒤 테스트 및 모니터링으로 안정성을 확인한다.
6. 조직 문화 및 협업 고려#
- 팀 내 도메인 전문가(도메인 지식 보유자)와 긴밀한 협업:
도메인 모델 개선은 비즈니스 전문가의 인풋이 중요하다. 도메인 언어 정립, 경계 재설정 등에 비즈니스 팀 의견을 적극 반영한다. - 지속적인 커뮤니케이션 & 문서화:
점진적 리팩토링 진행 과정에서 변경된 컨텍스트 경계, ACL 역할, 도메인 모델 등을 문서화하고 팀과 공유한다.
정리하자면, DDD 관점의 점진적 리팩토링은 도메인 언어 정립 → Bounded Context 식별 → 내부 모듈화 → 도메인 로직 자립화 → 필요 시 독립 서비스화라는 단계를 거친다. 이 과정에서 항상 테스트를 통한 안전망 확보, 작은 단위의 변경, 비즈니스 및 팀 커뮤니케이션을 통한 합의된 경계 정의가 핵심이다. 이를 통해 A 서비스가 지닌 과도한 책임과 의존성을 점진적으로 줄이고, 명확한 도메인 경계를 가진, 유지보수성과 확장성이 높은 구조로 바꿔갈 수 있다.