MMeshOps
마이크로서비스의 함정: 쪼개기 전에 알아야 할 아키텍처 설계
8예상 10중급

언젠가 쪼개질 날을 대비하는 API 통신 설계

모듈 간 직접 참조 대신, 마치 외부 API를 호출하듯 명확한 인터페이스를 통해 통신하여 훗날 서비스 분리를 쉽게 만드는 법을 배웁니다.

지금까지 우리는 코드의 폴더를 나누고 데이터베이스의 소유권을 명확히 분리하여 모듈 간의 결합도를 낮추는 방법을 배웠습니다. 하지만 현실 세계의 비즈니스는 언제나 모듈 간의 협력을 요구합니다. 결제를 완료하려면 재고를 확인해야 하고, 주문이 생성되면 알림을 보내야 합니다. 이번 시간에는 모듈러 모놀리스 환경에서 다른 모듈과 안전하게 소통하는 '내부 API' 설계 기법을 다룹니다.

직접 호출의 위험성

가장 직관적이고 쉬운 방법은 다른 모듈의 서비스 클래스를 가져와서 직접 호출하는 것입니다. 예를 들어 주문 모듈 안에서 다음과 같이 코드를 작성할 수 있습니다.

import { PaymentService } from '../payments/services/payment.service';

function processOrder() {
  const paymentService = new PaymentService();
  paymentService.pay(1000);
}

이 코드는 당장은 잘 작동합니다. 하지만 결제 모듈이 내부적으로 라이브러리를 교체하거나 `pay` 함수의 동작 방식을 바꾸면, 그것을 호출하고 있던 주문 모듈의 코드도 영향을 받게 됩니다. 더 큰 문제는, 나중에 결제 시스템을 별도의 마이크로서비스 서버로 분리할 때 발생합니다. `import` 문으로 강하게 결합된 저 코드들을 전부 찾아내어 HTTP 호출로 바꿔야 하는 악몽이 시작되는 것입니다.

모듈의 출입국 관리소: 인터페이스 (Interface)

직접 호출을 피하기 위해, 각 모듈은 외부에서 접근할 수 있는 공식적인 출입구인 퍼블릭 인터페이스(Public Interface)를 열어두어야 합니다. 이를 흔히 인바운드 포트(Inbound Port)라고 부르기도 합니다.

모듈은 자신의 내부 폴더 구조나 클래스 이름들을 밖으로 노출(export)하지 않습니다. 오로지 외부 모듈이 사용할 수 있도록 약속된 `API` 파일 하나만 노출합니다.

내부 API 계층 만들기

마치 외부의 카카오페이나 토스 결제 API를 연동하듯, 우리 애플리케이션 내부의 다른 모듈을 연동할 때도 명확한 '계약'을 맺어야 합니다.

내부 API 설계 체크리스트

  • 모듈 외부에 노출할 클래스나 함수는 최소화되었는가?
  • 다른 모듈의 데이터를 가져올 때, DB 직접 조회가 아닌 내부 API를 호출하고 있는가?
  • 내부 API의 파라미터와 반환값이 도메인 특화 객체가 아닌 범용적인 형태(예: DTO)로 전달되는가?
  • 내부 API를 수정할 때, 이를 사용하는 다른 모듈이 깨지지 않도록 하위 호환성을 유지하고 있는가?

Facade 패턴 활용

결제 모듈 내부에 여러 개의 복잡한 클래스(카드 결제, 포인트 결제, 영수증 발행)가 있더라도, 외부(주문 모듈)에는 `PaymentFacade`라는 단 하나의 인터페이스만 노출합니다. 주문 모듈은 결제가 내부적으로 어떻게 처리되는지 알 필요 없이, 그저 `PaymentFacade.processPayment()` 하나만 호출하면 됩니다. 내부 복잡성을 완벽하게 숨기는 방법입니다.

이벤트 기반 통신 (Event-Driven)

인터페이스를 통한 호출(동기식 호출)은 여전히 두 모듈의 실행 시점을 강하게 묶어버립니다. 주문 모듈이 결제 모듈의 함수를 호출했는데 에러가 발생하면, 주문 처리 전체가 실패하게 됩니다.

이 결합도마저 끊어내기 위해 이벤트 기반 통신(Event-Driven Communication)을 도입할 수 있습니다.

  1. 이벤트 발행: 주문 모듈은 결제가 필요할 때 결제 모듈을 직접 호출하지 않습니다. 그저 "주문이 생성되었습니다(OrderCreated)"라는 이벤트를 시스템 내부에 던집니다.
  2. 이벤트 구독: 결제 모듈은 평소에 "주문 생성" 이벤트를 기다리다가, 이벤트가 감지되면 알아서 결제 프로세스를 시작합니다.

이 방식을 사용하면 주문 모듈은 결제 모듈의 존재조차 알 필요가 없습니다. 소스 코드에 `import Payment`가 전혀 등장하지 않는 완벽한 격리가 이루어집니다. 훗날 마이크로서비스로 분리할 때도, 이 사내(In-Memory) 이벤트를 카프카(Kafka) 같은 외부 메시지 큐로 살짝 바꿔주기만 하면 완벽히 분리됩니다.

모듈 간 통신 방식 선택

상황선택이유
동기식 함수 호출 (인터페이스)즉각적인 결과(예: 잔액 확인)가 필요하고 트랜잭션이 하나로 묶여야 할 때구현이 단순하지만 실행 결합도가 높음
비동기 이벤트 발행 (Event-Driven)결과를 바로 기다릴 필요가 없고(예: 이메일 알림, 로그 저장) 각 모듈이 완전히 독립적으로 동작해야 할 때결합도가 매우 낮아지지만 흐름 추적이 어려워짐

모놀리스에서 마이크로서비스로의 매끄러운 전환

우리가 지금까지 모듈러 모놀리스를 설계하며 지켜온 세 가지 원칙을 다시 떠올려 봅시다.

  1. 코드를 도메인별로 명확히 분리했다.
  2. 데이터베이스 테이블 접근 권한을 철저히 나누고 격리했다.
  3. 모듈 간 통신은 캡슐화된 인터페이스나 이벤트를 통해서만 수행했다.

이 원칙이 잘 지켜진 시스템은 사실상 물리적인 네트워크 선망 없는 마이크로서비스와 다름없습니다. 트래픽 폭주로 정말로 분리가 필요한 순간, 특정 모듈을 그대로 복사하여 새로운 서버에 올리고, 내부 함수 호출을 HTTP 요청이나 메시지 큐 통신으로 약간만 변경해주면 분리는 끝이 납니다.

이것이 모듈러 모놀리스가 마이크로서비스로 가는 가장 이상적이고 안전한 징검다리인 이유입니다.

개념 퀴즈

퀴즈 답을 맞춰야 학습 완료가 됩니다.

모듈러 모놀리스 환경에서 모듈 간의 강한 결합을 피하고, 향후 마이크로서비스 전환을 용이하게 만드는 통신 설계 방식으로 가장 적절한 것은 무엇입니까?

3줄 요약

  1. 1다른 모듈의 내부 코드를 직접 참조하지 말고, 외부 API를 연동하듯 명확한 퍼블릭 인터페이스(Inbound Port)를 통해 소통해야 합니다.
  2. 2실행 시점의 결합도를 낮추기 위해 이벤트 기반(Event-Driven) 통신을 활용하면 완벽한 모듈 격리를 이룰 수 있습니다.
  3. 3코드, 데이터베이스, 통신 방식이 모두 격리된 모듈러 모놀리스는 훗날 안전한 마이크로서비스 전환을 위한 완벽한 리허설이 됩니다.