모놀리식을 잘게 쪼개 MSA로 전환했더니 호출이 오히려 더 복잡해진 경험, 있으십니까. 메서드 하나로 끝나던 호출이 네트워크를 건너야 하는 순간, 지연·장애·서비스 위치 변동이라는 새로운 문제가 한꺼번에 따라옵니다.
이 글은 MSA에서 서비스 간 호출을 해결하는 대표적인 방식들을 비교하고, 어떤 상황에 무엇을 골라야 하는지 정리합니다. REST와 OpenFeign, gRPC, 비동기 메시징을 나란히 놓고 장단점을 따져본 뒤, 장애가 번지지 않게 막는 방법과 상황별 선택 기준까지 다룹니다. 통신 방식을 처음 설계하거나 기존 구조를 손봐야 하는 분께 판단 근거가 되도록 구성했습니다.
서비스 간 호출이 어려운 진짜 이유
같은 프로세스 안의 메서드 호출은 실패할 일이 거의 없습니다. 반면 서비스 간 호출은 네트워크를 타는 순간 전혀 다른 세계가 됩니다. 패킷이 유실되고, 응답이 느려지고, 상대 서비스가 잠시 죽어 있을 수 있습니다.
특히 위험한 것은 장애 전파입니다. A 서비스가 B를 호출하고 B가 다시 C를 호출하는 구조에서 C가 느려지면, B의 스레드가 응답을 기다리며 묶이고, 그 여파가 A까지 번져 전체 시스템이 함께 멈출 수 있습니다. 호출 방식을 고를 때 성능만큼이나 장애 격리를 따져야 하는 이유입니다.
여기에 서비스 인스턴스가 수시로 늘고 줄어드는 환경이 더해집니다. 호출 대상의 IP를 코드에 박아 둘 수 없으니, 위치를 동적으로 찾는 장치도 함께 필요합니다. 결국 서비스 간 호출 설계는 “어떻게 부를 것인가”와 “어떻게 안전하게 부를 것인가”를 동시에 푸는 문제입니다.
먼저 정할 축: 동기 호출 vs 비동기 메시징
세부 기술을 고르기 전에 큰 갈림길부터 정해야 합니다. 호출자가 응답을 기다리는 동기 방식인지, 메시지를 던지고 바로 다음 일을 하는 비동기 방식인지입니다. 이 선택이 결합도와 일관성 모델을 좌우합니다.
| 구분 | 동기 호출 (REST·gRPC) | 비동기 메시징 (Kafka·RabbitMQ) |
|---|---|---|
| 응답 처리 | 즉시 응답 받음 | 응답 없음 또는 별도 이벤트 |
| 서비스 결합도 | 높음 (상대가 살아 있어야 함) | 낮음 (브로커가 중개) |
| 일관성 | 강한 일관성 | 최종 일관성 |
| 장애 전파 | 번지기 쉬움 | 격리되기 쉬움 |
| 적합한 작업 | 즉시 결과가 필요한 조회·검증 | 알림, 집계, 후처리 등 |
핵심은 “조회처럼 즉시 답이 필요한가, 아니면 일을 맡기고 흘려보내도 되는가”입니다. 결제 검증처럼 결과를 바로 받아야 하면 동기가 맞고, 주문 후 알림 발송처럼 늦어도 되는 작업은 비동기가 결합도를 크게 낮춰 줍니다.
동기 호출 ①: REST와 OpenFeign, 그리고 서비스 디스커버리
가장 익숙한 방식은 HTTP 기반 REST 호출입니다. Spring 환경에서는 OpenFeign으로 인터페이스만 선언하면 구현체가 자동 생성되어, 호출 코드가 로컬 메서드처럼 깔끔해집니다.
@FeignClient(name = "order-service")
public interface OrderClient {
@GetMapping("/orders/{id}")
OrderResponse getOrder(@PathVariable("id") Long id);
}
여기서 name = "order-service"에 주목할 필요가 있습니다. IP가 아니라 서비스 이름으로 부른다는 뜻입니다. 실제 인스턴스 위치는 Eureka 같은 서비스 디스커버리가 알려주고, Spring Cloud LoadBalancer가 여러 인스턴스로 요청을 분산합니다. 인스턴스가 늘거나 줄어도 호출 코드는 그대로 둘 수 있습니다.
REST는 표준이 명확하고 디버깅이 쉬우며 생태계가 넓다는 강점이 있습니다. 다만 텍스트 기반이라 직렬화 비용이 있고, 내부 통신이 폭증하면 오버헤드가 누적됩니다. OpenFeign의 구체적인 설정과 활용은 별도 글에서 더 깊게 다루고 있습니다.
동기 호출 ②: gRPC, 내부 통신 성능이 필요할 때
서비스 간 호출이 매우 잦거나 지연에 민감하다면 gRPC가 강력한 대안입니다. gRPC는 HTTP/2 위에서 Protobuf라는 이진 포맷으로 통신해, JSON 기반 REST보다 직렬화가 가볍고 빠릅니다.
스키마(.proto)를 먼저 정의하고 양쪽 코드를 생성하는 계약 우선(contract-first) 방식이라, 서비스 간 인터페이스가 명확하게 고정됩니다. 단방향뿐 아니라 양방향 스트리밍도 지원해 실시간 데이터 교환에 유리합니다.
대신 비용도 있습니다. 이진 포맷이라 사람이 바로 읽기 어렵고, 브라우저에서 직접 호출하기 까다로우며, 팀 전체가 Protobuf 워크플로에 익숙해져야 합니다. 그래서 gRPC는 보통 외부 공개 API가 아닌 내부 서비스 간 고성능 통신에 선택적으로 도입합니다. REST를 전면 대체하기보다, 병목이 분명한 구간에 부분 적용하는 편이 현실적입니다.
비동기 메시징: 결합도를 끊고 장애를 가두기
서비스를 시간적으로 분리하고 싶다면 메시지 브로커를 통한 비동기 방식이 답입니다. 호출자는 메시지를 브로커에 넣고 바로 자기 일을 끝내며, 수신자는 준비되는 대로 메시지를 가져가 처리합니다. 상대 서비스가 잠시 죽어 있어도 메시지가 브로커에 쌓여 있다가 복구 후 처리되므로, 장애가 호출자로 번지지 않습니다.
이 방식은 이벤트 기반 아키텍처의 토대가 됩니다. “주문이 생성됨” 같은 이벤트를 발행하면, 재고·알림·정산 서비스가 각자 구독해 독립적으로 반응합니다. 새 서비스를 추가해도 발행자 코드를 건드릴 필요가 없어 확장에 강합니다.
대가는 복잡성입니다. 즉시 정합성 대신 최종 일관성을 받아들여야 하고, 여러 서비스에 걸친 작업의 실패 보상은 사가(Saga) 패턴 같은 별도 설계가 필요합니다. 브로커 선택 기준(Kafka와 RabbitMQ의 차이)은 별도 글에서 비교해 두었습니다.
호출만큼 중요한 것: 장애가 번지지 않게 막기
어떤 통신 방식을 쓰든, 동기 호출이 섞여 있다면 장애 격리 장치는 필수입니다. 핵심은 세 가지입니다. 타임아웃으로 무한정 기다리지 않게 하고, 재시도로 일시적 실패를 흡수하며, 서킷브레이커로 반복 실패하는 대상으로의 호출을 잠시 끊어 줍니다.
Spring 환경에서는 Resilience4j로 이 장치들을 어노테이션 수준에서 적용할 수 있습니다.
@CircuitBreaker(name = "orderService", fallbackMethod = "fallbackOrder")
public OrderResponse getOrder(Long id) {
return orderClient.getOrder(id);
}
public OrderResponse fallbackOrder(Long id, Throwable t) {
return OrderResponse.empty(); // 실패 시 대체 응답
}
서킷브레이커가 열리면 해당 서비스로의 호출을 차단하고 즉시 대체 응답을 돌려주므로, 느린 서비스 하나가 전체를 끌어내리는 연쇄 장애를 막습니다. 이 패턴은 API Gateway 계층의 외부 진입 통제와 함께 쓰면 효과가 더 큽니다.
상황별 선택 가이드
정리하면, 서비스 간 호출 방식은 작업의 성격에 따라 다음 기준으로 고르는 것이 합리적입니다.
- 즉시 결과가 필요하고 호출량이 보통 수준이면 REST + OpenFeign이 무난합니다. 표준적이고 운영·디버깅이 쉽습니다.
- 내부 통신이 매우 잦고 지연·대역폭이 병목이면 해당 구간에 gRPC를 부분 도입합니다.
- 상대 응답을 기다릴 필요가 없거나 결합도를 낮추고 싶으면 Kafka·RabbitMQ 기반 비동기 메시징을 씁니다.
- 동기 호출이 있는 모든 구간에는 타임아웃·재시도·서킷브레이커를 기본으로 깔아 장애 전파를 차단합니다.
대부분의 실무 시스템은 한 가지로 통일되지 않습니다. 조회는 REST, 고빈도 내부 통신은 gRPC, 후처리는 메시징처럼 혼합해서 쓰는 것이 정상입니다. 중요한 것은 각 호출이 “즉시성이 필요한가”와 “실패하면 어떻게 되는가”를 기준으로 의식적으로 선택되었는지입니다.
한눈 정리
- 큰 갈림길은 동기(REST·gRPC) vs 비동기(메시징) — 즉시성과 결합도로 결정
- gRPC는 내부 고성능 구간에 선택적 도입, REST 전면 대체는 비권장
- 동기 호출에는 타임아웃·재시도·서킷브레이커로 장애 전파를 반드시 차단
“MSA 서비스 간 호출 해결 방법 비교 가이드”에 대한 1개의 생각