디커플링(DECOUPLING)은 생산자와 소비자간 결합도를 줄여서 각각의 독립성을 보장하고 유지보수성과 확장성을 얻는 설계 원칙이며 시스템 아키텍처나 애플리케이션 아키텍처를 설계하고 개선함에 있어서 꼭 필요하다. 이 글에서는 디커플링이 왜 필요한지 시스템·애플리케이션·조직이라는 세 가지 관점에서 실제 사례를 통해 설명한다.

  • 결합도란 모듈간의 상호 의존하는 정도를 의미한다. 따라서 높은 결합도는 상호 의존성이 높기 때문에 한쪽의 변경이 다른 한쪽에 영향을 줄 가능성이 높아진다.
  • 독립성은 한쪽 코드를 고쳐도 다른 쪽에 영향을 주지 않는다.
  • 생산자와 소비자란 시스템 아키텍처 수준에서 서비스 단위일 수도 있고 혹은 애플리케이션 아키텍처 수준에서 모듈, 클래스, 함수 단위일 수 있다.

시스템 단위에서 높은 결합도로 인한 독립성이 보장되지 않는 케이스를 살펴보자.

첫 번째 예를 살펴보자. 주문 서비스(생산자)와 결제 서비스(소비자)가 있다. 만약 결제 서비스가 주문 서비스의 DB를 직접 조회하는 경우, 주문 서비스가 DB의 스키마, 제약조건, 컬럼명 등을 수정하게 되면 결제 서비스도 그에 맞게 시스템을 고쳐야 한다. 즉 서비스간 계약을 DB 스키마에 의존하면 강결합이 발생할 수 있다. 다른 단점으로는 주문 서비스에서 DB 변경이 있을 때마다 사전에 결제 팀과 협의해야 하며, 만약 주문 서비스 개발자의 실수로 DB 변경을 하게 되면 결제 팀은 이슈를 파악하기 위해서 분석하는 시간과 노력이 필요하다. 즉 디버깅 포인트가 증가하게 된다.

두 번째 예를 살펴보자. 주문 서비스와 쿠폰 서비스가 있다. 주문 서비스가 쿠폰 서비스에 너무 강결합이 되어있어서 쿠폰 서비스가 죽으면 주문도 할 수 없게 된다. 하지만 제품 관점에서 쿠폰이 없어도 주문은 가능해야 한다.

세 번째 예를 살펴보자. 주문 생성 > 결제 생성 > 재고 차감 > 배송 생성과 같은 일련의 흐름이 Two-Phase Commit과 같은 분산 트랜잭션(Distributed Transaction)으로 엮이게 되면 어느 하나만 실패하도 전체가 롤백 되어야 한다.

이외에도 다양한 케이스가 많다. 다음으로는 애플리케이션 단위에서 높은 결합도로 인한 독립성이 보장되지 않는 케이스를 살펴보자.

대표적인 예가 공통 DTO를 공유하는 경우다. 예를 들어 UserDto가 OrderService, PaymentService, CouponService, DeliveryService 등에서 사용되고 있다면 UserDto의 필드를 제거하거나 추가하기가 정말 어렵다. UserDto에 있던 address 필드를 삭제했는데 DeliveryService에서 사이드 이펙트가 발생할 수 있다.

마지막으로 조직 수준의 아키텍처 설계에서도 디커플링을 필수로 고려해야 한다.

예를 들어 초기에는 주문, 결제, 배송 모두 하나의 팀에서 개발하는 상황이 많다. 하지만 제품의 트래픽이 많아지고 제품이 확장 될 수록 각 컴포넌트의 확장성, 유지 보수성, 트러블 슈팅, 관측성 등 많은 요소들을 고려해야 한다. 따라서 기존의 적은 인원으로는 각 컴포넌트를 모두 담당하기 힘들다. 그렇기 때문에 채용을 하게 되며 각 컴포넌트를 담당할 전문 인력들을 배치하게된다. 이 경우 주문팀, 결제팀 같이 팀이 분리될 가능성이 있다. 팀이 분리된 이후에 강결합을 갖는 컴포넌트들에 대해 기능을 하나 수정하더라도 여러 팀과 일정 조율과 협의가 필요할 수 있다. 즉 조직의 의사소통 구조가 점점 복잡해지고 기능 추가에 많은 커뮤니케이션 비용이 들게 되며 실제 배포까지 하루 걸리던 게 1주일이 될 수도 있다.

따라서 각 팀이 독립적으로 확장 가능 하도록 만들기 위해서는 컴포넌트들이 “구현"이 아니라 “계약(Contract)” 중심으로 바뀌어야 유연성이 극대화 된다. SOLID 원칙 중 하나인 DIP(Dependency Inversion Principle)의 개념을 빌려 설명하면 “의존성이 추상(abstraction)에 의존하며 구체(concretion)에는 의존하지 않는 시스템” 을 갖춰야 한다.

디커플링은 단순히 컴포넌트를 분리하는 기술이 아니라, 변경의 영향을 격리하는 설계 원칙이다.