0. 들어가며
앞선 글에서 우리는 마이크로서비스 아키텍처의 개념과 서비스 분해 전략에 대해 살펴보았다. 비즈니스 도메인에 따라 서비스를 분해했다면, 이제 그 서비스들이 어떻게 통신할 것인가를 결정해야 한다. 분산 시스템에서 서비스 간 통신은 단순한 기술적 선택을 넘어 시스템의 성능, 안정성, 확장성에 직접적인 영향을 미치는 핵심 설계 결정이다.
마이크로서비스 간 통신은 크게 동기(Synchronous) 방식과 비동기(Asynchronous) 방식으로 나뉜다. 이번 글에서는 동기 통신 방식에 집중하여 REST, gRPC, GraphQL의 특징과 장단점을 비교하고, API Gateway 패턴의 필요성과 다양한 변형(BFF, Aggregator)에 대해 알아본다.
1. MSA에서 통신이 중요한 이유
1.1. 분산 시스템의 통신 과제
모놀리식 아키텍처에서는 모든 통신이 프로세스 내부에서 이루어지므로 간단한 메서드 호출로 충분했다. 하지만 MSA에서는 서로 다른 프로세스, 다른 서버, 다른 네트워크에 위치한 서비스들이 통신해야 한다.
모놀리스 vs MSA 통신 비교:
// 모놀리스: 인메모리 메서드 호출
public class OrderService {
private final UserRepository userRepository;
public OrderDto getOrderWithUser(String orderId) {
Order order = orderRepository.findById(orderId); // 같은 프로세스
User user = userRepository.findById(order.getUserId()); // 같은 프로세스
return combine(order, user);
}
}
// MSA: 네트워크를 통한 통신
public class OrderService {
private final UserServiceClient userServiceClient; // HTTP 클라이언트
public OrderDto getOrderWithUser(String orderId) {
Order order = orderRepository.findById(orderId); // 로컬 DB
UserDto user = userServiceClient.getUser(order.getUserId()); // 원격 호출
// ↑ 네트워크 지연, 타임아웃, 장애 가능성, 로드밸런싱, 서킷브레이커...
return combine(order, user);
}
}
분산 통신의 과제:
- 네트워크 지연: 항상 일정하지 않은 응답 시간
- 부분 실패: 일부 서비스만 실패하는 상황
- 메시지 손실: 네트워크 불안정으로 인한 패킷 손실
- 순서 보장: 메시지 도착 순서가 불확실
- 보안: 네트워크 구간 암호화 필요
- 버전 관리: API 진화에 따른 호환성 유지
1.2. 통신 방식 선택 기준
| 기준 | 동기식 | 비동기식 |
| 응답 시간 | 즉시 응답 필요 | 지연 응답 허용 |
| 결합도 | 상대적 높음 | 낮음 (이벤트 기반) |
| 장애 전파 | 직접적 영향 | 격리 가능 |
| 복잡도 | 낮음 | 높음 |
| 트랜잭션 | 강한 일관성 가능 | 최종 일관성 |
| 사용 사례 | 조회 API, 명령형 | 이벤트 처리, 워크플로우 |
2. 동기 통신 방식 비교
2.1. RESTful API
REST는 Representational State Transfer의 약자로, HTTP 프로토콜을 기반으로 하는 가장 보편적인 API 설계 방식이다.
특징:
- 리소스(Resource) 중심 설계
- HTTP 메서드(GET, POST, PUT, DELETE) 활용
- JSON/XML 형식의 메시지
- 무상태(Stateless) 통신
장점:
- 단순하고 직관적
- HTTP의 장점(캐싱, 프록시, 로드밸런싱) 활용
- 다양한 언어와 플랫폼에서 지원
- 개발자 도구 생태계 풍부 (Swagger, Postman)
단점:
- 오버헤드가 큰 텍스트 기반 프로토콜
- 약한 타입 안정성
- 실시간 스트리밍에 부적합
실습 예제:
// REST API 정의
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@GetMapping("/{orderId}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable String orderId) {
Order order = orderService.findById(orderId);
return ResponseEntity.ok(OrderResponse.from(order));
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@Valid @RequestBody CreateOrderRequest request) {
Order order = orderService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(OrderResponse.from(order));
}
}
// Feign Client를 통한 호출
@FeignClient(name = "order-service")
public interface OrderServiceClient {
@GetMapping("/api/v1/orders/{orderId}")
OrderResponse getOrder(@PathVariable("orderId") String orderId);
@PostMapping("/api/v1/orders")
OrderResponse createOrder(@RequestBody CreateOrderRequest request);
}
2.2. gRPC
gRPC는 Google에서 개발한 고성능 RPC(Remote Procedure Call) 프레임워크로, Protocol Buffers를 사용하여 직렬화한다.
특징:
- Protocol Buffers 기반 바이너리 직렬화
- HTTP/2 기반 스트리밍 지원
- 강력한 타입 계약 (.proto 파일)
- 다양한 언어 지원
장점:
- 높은 성능 (JSON 대비 약 5-10배 빠름)
- 엄격한 타입 안정성
- 양방향 스트리밍 지원
- 코드 자동 생성으로 생산성 향상
단점:
- 브라우저 지원 제한적 (gRPC-web 필요)
- 사람이 읽을 수 없는 바이너리 포맷
- 학습 곡선 존재
실습 예제(protobuf):
// order.proto
syntax = "proto3";
package ecommerce;
service OrderService {
rpc GetOrder (GetOrderRequest) returns (Order) {}
rpc CreateOrder (CreateOrderRequest) returns (Order) {}
rpc StreamOrders (StreamRequest) returns (stream Order) {}
}
message GetOrderRequest {
string order_id = 1;
}
message Order {
string order_id = 1;
string user_id = 2;
repeated OrderItem items = 3;
double total_amount = 4;
string status = 5;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
double price = 3;
}
// 서버 구현
@GrpcService
public class OrderGrpcService extends OrderServiceGrpc.OrderServiceImplBase {
@Override
public void getOrder(GetOrderRequest request, StreamObserver<Order> responseObserver) {
String orderId = request.getOrderId();
OrderEntity order = orderService.findById(orderId);
Order protoOrder = Order.newBuilder()
.setOrderId(order.getId())
.setUserId(order.getUserId())
.setTotalAmount(order.getTotalAmount())
.setStatus(order.getStatus())
.build();
responseObserver.onNext(protoOrder);
responseObserver.onCompleted();
}
}
// 클라이언트 호출
@GrpcClient("order-service")
private OrderServiceGrpc.OrderServiceBlockingStub orderServiceStub;
public Order getOrder(String orderId) {
GetOrderRequest request = GetOrderRequest.newBuilder()
.setOrderId(orderId)
.build();
Order order = orderServiceStub.getOrder(request);
return order;
}
2.3. GraphQL
GraphQL은 Facebook에서 개발한 쿼리 언어로, 클라이언트가 필요한 데이터를 정확히 지정할 수 있다.
특징:
- 클라이언트 주도 데이터 요청
- 단일 엔드포인트
- 강력한 타입 시스템 (스키마)
- 중첩된 리소스 조회 가능
장점:
- 오버페칭/언더페칭 문제 해결
- 여러 API 호출을 단일 요청으로 통합
- API 버전 관리 불필요
- 클라이언트 주도 개발
단점:
- 복잡한 쿼리에 따른 성능 이슈 가능
- 캐싱이 REST보다 복잡
- 파일 업로드 등 복잡한 기능 처리 어려움
실습 예제 (graphql):
# 스키마 정의
type Query {
order(id: ID!): Order
ordersByUser(userId: ID!): [Order]
}
type Mutation {
createOrder(input: CreateOrderInput!): Order!
}
type Order {
id: ID!
userId: ID!
items: [OrderItem!]!
totalAmount: Float!
status: OrderStatus!
user: User # 다른 서비스의 데이터
createdAt: String!
}
type OrderItem {
productId: ID!
productName: String!
quantity: Int!
price: Float!
product: Product # 다른 서비스의 데이터
}
// GraphQL 리졸버
@Component
@RequiredArgsConstructor
public class OrderResolver implements GraphQLQueryResolver {
private final OrderService orderService;
private final UserServiceClient userServiceClient;
private final ProductServiceClient productServiceClient;
public Order order(String id) {
return orderService.findById(id);
}
public List<Order> ordersByUser(String userId) {
return orderService.findByUserId(userId);
}
}
@Component
@RequiredArgsConstructor
public class OrderFieldResolver implements GraphQLResolver<Order> {
private final UserServiceClient userServiceClient;
private final ProductServiceClient productServiceClient;
public User user(Order order) {
// 다른 서비스의 데이터를 조회하여 반환
return userServiceClient.getUser(order.getUserId());
}
public List<OrderItem> items(Order order) {
return order.getItems().stream()
.map(item -> {
Product product = productServiceClient.getProduct(item.getProductId());
item.setProductName(product.getName());
return item;
})
.collect(Collectors.toList());
}
}
# 클라이언트 쿼리 예시- graphql
query GetOrderDetail($orderId: ID!) {
order(id: $orderId) {
id
totalAmount
status
user {
name
email
}
items {
productName
quantity
price
product {
description
imageUrl
}
}
}
}
2.4. REST vs GraphQL vs gRPC 비교
| 특성 | REST | gRPC | GraphQL |
| 프로토콜 | HTTP/1.1, HTTP/2 | HTTP/2 | HTTP/1.1, HTTP/2 |
| 데이터 포맷 | JSON, XML | Protocol Buffers | JSON |
| 타입 안정성 | 약함 (런타임) | 강함 (컴파일타임) | 강함 (스키마) |
| 성능 | 중간 | 매우 높음 | 중간 |
| 브라우저 지원 | 완벽 | 제한적 (gRPC-web) | 완벽 |
| 학습 곡선 | 낮음 | 중간 | 중간 |
| 캐싱 | 내장 지원 (HTTP 캐시) | 어려움 | 복잡 |
| 실시간 | WebSocket 별도 | 내장 스트리밍 | Subscription |
| 문서화 | Swagger | Proto 파일 | Introspection |
| 사용 사례 | 공개 API, 웹 서비스 | 내부 서비스 간 통신 | 다양한 클라이언트, 복잡한 쿼리 |
2.5. 선택 가이드
REST를 선택해야 하는 경우:
- 공개 API를 제공해야 할 때
- 단순하고 직관적인 API가 필요할 때
- 캐싱이 중요한 경우
- 개발자 친화적인 API가 필요할 때
gRPC를 선택해야 하는 경우:
- 높은 성능이 필요한 내부 통신
- 실시간 스트리밍이 필요한 경우
- 강력한 타입 계약이 중요한 경우
- 마이크로서비스 간 고성능 통신
GraphQL을 선택해야 하는 경우:
- 다양한 클라이언트(웹, 모바일)를 지원해야 할 때
- 복잡한 데이터 그래프를 탐색해야 할 때
- 네트워크 효율성이 중요한 모바일 앱
- API 버전 관리를 피하고 싶을 때
3. API Gateway 패턴
3.1. API Gateway의 필요성
마이크로서비스 아키텍처에서는 각 서비스가 각자의 API를 노출한다. 만약 클라이언트가 각 서비스에 직접 요청을 보내게 되면 어떤 문제가 발생할까?
클라이언트 직접 통신의 문제점:
[모바일 앱] → 직접 호출 → [주문 서비스]
→ 직접 호출 → [상품 서비스]
→ 직접 호출 → [사용자 서비스]
→ 직접 호출 → [결제 서비스]
→ 직접 호출 → [배송 서비스]
- 클라이언트 복잡성: 클라이언트가 여러 서비스의 엔드포인트를 모두 알아야 함
- 인증/인가 중복: 각 서비스마다 인증 로직을 구현해야 함
- 크로스커팅 관심사: 로깅, 모니터링, Rate Limiting 등을 모든 서비스에 구현
- 프로토콜 변환: 클라이언트 프로토콜과 서비스 프로토콜이 다를 경우 변환 필요
- 서비스 위치 변경: 서비스 주소가 변경되면 모든 클라이언트 업데이트 필요
3.2. API Gateway 패턴의 역할
API Gateway는 모든 클라이언트 요청의 단일 진입점 역할을 하며, 다음과 같은 기능을 제공한다.
[클라이언트] → [API Gateway] → [마이크로서비스들]
↓
[횡단 관심사 처리]
- 인증/인가
- 로깅
- Rate Limiting
- 라우팅
- 로드밸런싱
- 응답 변환
- 캐싱
주요 기능:
1. 라우팅(Routing)
# Spring Cloud Gateway 설정
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://ORDER-SERVICE
predicates:
- Path=/api/orders/**
- id: product-service
uri: lb://PRODUCT-SERVICE
predicates:
- Path=/api/products/**
2. 인증/인가 (Authentication/Authorization)
@Configuration
public class SecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/api/public/**").permitAll()
.pathMatchers("/api/orders/**").authenticated()
.pathMatchers("/api/admin/**").hasRole("ADMIN")
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
.build();
}
}
3. Rate Limiting (요청 제한)
spring:
cloud:
gateway:
routes:
- id: product-service
uri: lb://PRODUCT-SERVICE
predicates:
- Path=/api/products/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
4. 로깅과 모니터링
@Component
@Slf4j
public class LoggingFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
log.info("Request: {} {}", request.getMethod(), request.getURI());
long startTime = System.currentTimeMillis();
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long duration = System.currentTimeMillis() - startTime;
log.info("Response status: {}, duration: {}ms",
exchange.getResponse().getStatusCode(), duration);
}));
}
}
3.3. BFF (Backend for Frontend) 패턴
BFF는 클라이언트 유형별로 별도의 API Gateway를 두는 패턴이다. 웹, 모바일, 서드파티 등 각 클라이언트의 요구사항이 다를 때 유용하다.
BFF 패턴의 구조:
[웹 브라우저] → [BFF-Web] → [공통 서비스들]
[모바일 앱] → [BFF-Mobile] → [공통 서비스들]
[서드파티] → [BFF-Partner] → [공통 서비스들]
BFF가 필요한 이유:
// 웹 BFF - 페이지 렌더링에 필요한 데이터 조합
@RestController
@RequestMapping("/web/api/orders")
public class WebOrderController {
@GetMapping("/{orderId}")
public WebOrderResponse getOrderForWeb(@PathVariable String orderId) {
Order order = orderService.getOrder(orderId);
User user = userService.getUser(order.getUserId());
List<Product> products = productService.getProducts(order.getProductIds());
// 웹에 최적화된 응답 조합
return WebOrderResponse.builder()
.orderId(order.getId())
.userName(user.getName())
.products(products.stream()
.map(p -> new WebProduct(p.getName(), p.getImageUrl()))
.collect(Collectors.toList()))
.totalAmount(order.getTotalAmount())
.build();
}
}
// 모바일 BFF - 모바일에 최적화된 간결한 응답
@RestController
@RequestMapping("/mobile/api/orders")
public class MobileOrderController {
@GetMapping("/{orderId}")
public MobileOrderResponse getOrderForMobile(@PathVariable String orderId) {
Order order = orderService.getOrder(orderId);
// 모바일은 최소한의 데이터만 필요
return MobileOrderResponse.builder()
.orderId(order.getId())
.totalAmount(order.getTotalAmount())
.status(order.getStatus())
.build();
}
}
BFF의 장점:
- 클라이언트별 최적화된 API 제공
- 클라이언트 변경이 다른 클라이언트에 영향 없음
- 각 BFF를 독립적으로 개발/배포 가능
- 보안 정책을 클라이언트 유형별로 다르게 적용
3.4. Aggregator 패턴
Aggregator 패턴은 여러 서비스의 응답을 조합하여 하나의 응답으로 반환하는 패턴이다.
Aggregator 패턴의 동작:
클라이언트 요청
↓
[API Gateway / Aggregator Service]
├─→ [주문 서비스] → 주문 정보
├─→ [사용자 서비스] → 사용자 정보
├─→ [상품 서비스] → 상품 정보
└─→ [배송 서비스] → 배송 정보
↓
클라이언트 응답 (조합된 결과)
구현 예시:
@RestController
@RequestMapping("/api/orders/{orderId}/detail")
public class OrderAggregatorController {
private final OrderServiceClient orderClient;
private final UserServiceClient userClient;
private final ProductServiceClient productClient;
private final ShippingServiceClient shippingClient;
@GetMapping
public Mono<OrderDetailResponse> getOrderDetail(@PathVariable String orderId) {
// 여러 서비스에 비동기로 동시 요청
Mono<Order> orderMono = orderClient.getOrder(orderId);
Mono<User> userMono = orderMono.flatMap(order ->
userClient.getUser(order.getUserId()));
Mono<List<Product>> productsMono = orderMono.flatMap(order ->
Flux.fromIterable(order.getProductIds())
.flatMap(productClient::getProduct)
.collectList());
Mono<Shipping> shippingMono = shippingClient.getShipping(orderId);
// 모든 응답이 도착하면 조합
return Mono.zip(orderMono, userMono, productsMono, shippingMono)
.map(tuple -> {
Order order = tuple.getT1();
User user = tuple.getT2();
List<Product> products = tuple.getT3();
Shipping shipping = tuple.getT4();
return OrderDetailResponse.builder()
.order(order)
.userName(user.getName())
.userEmail(user.getEmail())
.products(products)
.shippingAddress(shipping.getAddress())
.shippingStatus(shipping.getStatus())
.build();
});
}
}
Aggregator 패턴의 장점:
- 클라이언트의 여러 API 호출을 단일 호출로 줄임
- 네트워크 라운드트립 감소
- 서비스 내부 구조를 클라이언트로부터 숨김
- 응답 데이터를 클라이언트에 최적화된 형태로 가공 가능
4. 실무 적용 시 고려사항
4.1. API 버전 관리
마이크로서비스는 독립적으로 진화하므로 API 버전 관리가 중요하다.
버전 관리 전략:
// 1. URI Path에 버전 포함
@RestController
@RequestMapping("/api/v1/orders")
public class OrderControllerV1 { ... }
@RestController
@RequestMapping("/api/v2/orders")
public class OrderControllerV2 { ... }
// 2. Accept 헤더 사용
@GetMapping(value = "/orders/{id}", produces = "application/vnd.ecommerce.v1+json")
public Order getOrderV1(...) { ... }
@GetMapping(value = "/orders/{id}", produces = "application/vnd.ecommerce.v2+json")
public Order getOrderV2(...) { ... }
4.2. 타임아웃과 서킷 브레이커
동기 통신에서 장애가 전체 시스템으로 전파되는 것을 방지하기 위해 타임아웃과 서킷 브레이커 설정이 필수적이다.
resilience4j:
circuitbreaker:
instances:
order-service:
sliding-window-size: 10
failure-rate-threshold: 50
wait-duration-in-open-state: 10s
permitted-number-of-calls-in-half-open-state: 3
timelimiter:
instances:
order-service:
timeout-duration: 3s
4.3. 서비스 디스커버리 연동
API Gateway는 서비스 디스커버리(Eureka, Consul)와 연동하여 동적으로 서비스 위치를 찾을 수 있어야 한다.
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
eureka:
client:
service-url:
defaultZone: <http://localhost:8761/eureka/>
5. 정리
5.1. 동기 통신 방식 선택 요약
| 상황 | 추천 방식 |
| 공개 API, 단순한 서비스 | REST |
| 고성능 내부 통신, 스트리밍 | gRPC |
| 다양한 클라이언트, 복잡한 데이터 | GraphQL |
5.2. API Gateway 패턴 선택 요약
| 상황 | 추천 패턴 |
| 단순한 라우팅만 필요 | Single API Gateway |
| 다양한 클라이언트 지원 | BFF 패턴 |
| 응답 조합이 많은 경우 | Aggregator 패턴 |
| 서비스 메시 도입 예정 | Sidecar 패턴 고려 |
5.3. 다음 글 예고
이번 글에서는 동기 통신 방식과 API Gateway 패턴을 살펴보았다. 다음 글에서는 비동기 통신과 Event-Driven Architecture에 대해 알아볼 예정이다. 메시지 브로커(Kafka, RabbitMQ)를 활용한 Pub/Sub 패턴과 이벤트 기반 아키텍처의 장점, 그리고 실무 적용 사례를 살펴보겠다.
'MSA > MSA 아키텍처' 카테고리의 다른 글
| [ADVANCED #1] MSA 데이터 관리 전략 (DB per Service, CQRS, Sharding) (0) | 2026.03.02 |
|---|---|
| [BASIC #4] 비동기 통신과 Event-Driven Architecture (0) | 2026.03.02 |
| [BASIC #2] Microservice Architecture 개요와 서비스 분해 전략 (0) | 2026.03.02 |
| [BASIC #1] 마이크로서비스 아키텍처의 이해: Monolithic에서 MSA까지 (0) | 2026.03.02 |
| [5] EDA (0) | 2025.10.20 |
