마이크로서비스 통신 전략 완벽 가이드

2025년 12월 29일 2026년 3월 24일 11분

들어가며

마이크로서비스 아키텍처에서 서비스 간 통신 전략
시스템의 성능, 확장성, 유지보수성을 결정짓는 핵심 요소입니다.

API Gateway가 요청을 라우팅하고, Worker가 작업을 처리하며,
결과를 저장소에 기록하는 모든 과정이 서비스 간 통신으로 이루어집니다.

이 글에서는 직접 통신(REST, gRPC, WebSocket)과 간접 통신(Message Queue),
네 가지 주요 통신 전략을 비교하고 각각 어떤 상황에서 적합한지 알아봅니다.

api-communication-examples
https://github.com/tklee-yonsei/api-communication-examples

통신 전략 한눈에 보기

구분방식패턴지연시간사용 사례
직접REST API요청-응답중간CRUD 작업, 외부 API
직접gRPC요청-응답/스트리밍낮음내부 서비스 간 고속 통신
직접WebSocket양방향 실시간매우 낮음실시간 모니터링, 알림
간접Message Queue비동기가변작업 큐, 이벤트 처리

직접 통신 (Point-to-Point)

서비스 간 중간 브로커 없이 직접 연결하여 통신합니다.
요청-응답(REST, gRPC)부터 양방향 실시간(WebSocket)까지 다양한 패턴을 포함하며,
통신 당사자가 서로를 알고 있어야 합니다.

REST API: HTTP 기반의 표준 통신

REST API 개념

REST(Representational State Transfer)는 HTTP 프로토콜을 활용한 통신 방식입니다.
웹의 기본 원리를 그대로 활용하기 때문에 가장 널리 사용됩니다.

핵심 원칙

리소스 중심 설계: 모든 것을 “리소스"로 표현하고 URL로 식별합니다.

1
2
3
4
5
6
GET    /jobs          → 작업 목록 조회
POST   /jobs          → 새 작업 생성
GET    /jobs/{id}     → 특정 작업 조회
PUT    /jobs/{id}     → 작업 전체 수정
PATCH  /jobs/{id}     → 작업 부분 수정
DELETE /jobs/{id}     → 작업 삭제

실제 구현 예시

Flask를 사용한 REST API 서버:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from flask import Flask, request, jsonify
import uuid

app = Flask(__name__)
jobs = {}

@app.route('/jobs', methods=['POST'])
def create_job():
    """새로운 작업 생성"""
    data = request.get_json()
    job_id = str(uuid.uuid4())
    
    jobs[job_id] = {
        'id': job_id,
        'type': data['type'],
        'params': data['params'],
        'status': 'pending'
    }
    
    return jsonify({
        'job_id': job_id,
        'status': 'pending'
    }), 201  # 201 Created

@app.route('/jobs/<job_id>', methods=['GET'])
def get_job(job_id):
    """작업 상태 조회"""
    if job_id not in jobs:
        return jsonify({'error': 'Job not found'}), 404
    
    return jsonify(jobs[job_id]), 200

클라이언트 요청:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import requests

# 작업 생성
response = requests.post(
    'http://api-server:8080/jobs',
    json={
        'type': 'data_processing',
        'params': {'input_file': 'data.csv', 'batch_size': 100}
    }
)
job = response.json()
print(f"생성된 작업 ID: {job['job_id']}")

# 결과 조회
result = requests.get(f"http://api-server:8080/jobs/{job['job_id']}")
print(result.json())

REST API 장단점

  • 장점
    • 표준 HTTP를 사용하여 방화벽/프록시 친화적
    • 브라우저에서 직접 테스트 가능
    • 캐싱, 로드밸런싱 등 기존 인프라 활용
    • 학습 곡선이 낮음
  • 단점
    • 텍스트 기반(JSON)으로 바이너리 대비 오버헤드 존재
    • 요청마다 연결을 새로 맺는 비용
    • 실시간 양방향 통신에 부적합

REST API 적합한 사용 시나리오

  • 외부 클라이언트(웹 브라우저, 모바일 앱) 연동
  • CRUD 중심의 데이터 조작
  • 다양한 언어/플랫폼 간 통신

gRPC: 고성능 바이너리 통신

gRPC 개념

gRPC는 Google이 개발한 고성능 RPC(Remote Procedure Call) 프레임워크입니다.
HTTP/2 기반에 Protocol Buffers라는 바이너리 직렬화 포맷을 사용하여 REST 대비 월등한 성능을 제공합니다.

Protocol Buffers 정의

서비스와 메시지를 .proto 파일로 정의합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// processing.proto
syntax = "proto3";

package processing;

// 서비스 정의
service ProcessingService {
    // 단일 요청-응답
    rpc SubmitJob(JobRequest) returns (JobResponse);
    
    // 서버 스트리밍: 클라이언트 1회 요청, 서버 여러 번 응답
    rpc StreamResults(JobId) returns (stream ProcessingResult);
    
    // 양방향 스트리밍
    rpc InteractiveProcess(stream InputData) 
        returns (stream OutputData);
}

// 메시지 정의
message JobRequest {
    string job_type = 1;
    int32 batch_size = 2;
    repeated double input_data = 3;
}

message JobResponse {
    string job_id = 1;
    Status status = 2;
    
    enum Status {
        PENDING = 0;
        RUNNING = 1;
        COMPLETED = 2;
        FAILED = 3;
    }
}

message ProcessingResult {
    string chunk_id = 1;
    bytes result_data = 2;  // 바이너리 데이터
    double timestamp = 3;
}

서버 구현

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import grpc
from concurrent import futures
import processing_pb2
import processing_pb2_grpc

class ProcessingServicer(processing_pb2_grpc.ProcessingServiceServicer):
    
    def SubmitJob(self, request, context):
        """단일 작업 제출"""
        job_id = create_job(request.job_type, request.batch_size)
        return processing_pb2.JobResponse(
            job_id=job_id,
            status=processing_pb2.JobResponse.PENDING
        )
    
    def StreamResults(self, request, context):
        """결과 스트리밍 - 처리가 완료될 때마다 전송"""
        for result in process_job(request.job_id):
            yield processing_pb2.ProcessingResult(
                chunk_id=result['chunk_id'],
                result_data=result['data'],
                timestamp=result['time']
            )

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    processing_pb2_grpc.add_ProcessingServiceServicer_to_server(
        ProcessingServicer(), server
    )
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

클라이언트 구현

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import grpc
import processing_pb2
import processing_pb2_grpc

def run_job():
    with grpc.insecure_channel('worker-service:50051') as channel:
        stub = processing_pb2_grpc.ProcessingServiceStub(channel)
        
        # 작업 제출
        response = stub.SubmitJob(processing_pb2.JobRequest(
            job_type='batch_processing',
            batch_size=100
        ))
        print(f"작업 ID: {response.job_id}")
        
        # 결과 스트리밍 수신
        for result in stub.StreamResults(
            processing_pb2.JobId(id=response.job_id)
        ):
            print(f"Chunk {result.chunk_id}: {len(result.result_data)} bytes")

REST vs gRPC 성능 비교

측정 항목REST (JSON)gRPC (Proto)
메시지 크기100%~30%
직렬화 속도1x~6x
지연시간1x~0.5x
CPU 사용량1x~0.7x

gRPC 장단점

  • 장점
    • 바이너리 포맷으로 높은 성능
    • 타입 안전성 (컴파일 타임 검증)
    • HTTP/2의 멀티플렉싱, 헤더 압축
    • 스트리밍 지원
  • 단점
    • 브라우저 직접 호출 불가 (gRPC-Web 필요)
    • 디버깅이 REST 대비 어려움
    • 추가 도구 필요 (protoc 컴파일러)

gRPC 적합한 사용 시나리오

  • 마이크로서비스 간 내부 통신
  • 대용량 바이너리 데이터 전송
  • 저지연이 중요한 실시간 처리

WebSocket: 실시간 양방향 통신

WebSocket 개념

WebSocket은 단일 TCP 연결을 통해 클라이언트와 서버 간 전이중(full-duplex) 양방향 통신을 제공합니다.
HTTP의 요청-응답 패턴과 달리, 연결이 유지되는 동안 양쪽에서 언제든 메시지를 보낼 수 있습니다.

연결 흐름

sequenceDiagram participant 클라이언트 participant 서버 클라이언트->>서버: HTTP Upgrade 요청 서버-->>클라이언트: 101 Switching Protocols Note over 클라이언트,서버: WebSocket 연결 수립 서버->>클라이언트: 서버 푸시: 상태 업데이트 클라이언트->>서버: 클라이언트 메시지 서버->>클라이언트: 서버 푸시: 실시간 데이터

서버 구현 (Flask-SocketIO)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from flask import Flask
from flask_socketio import SocketIO, emit, join_room

app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")

@socketio.on('connect')
def handle_connect():
    print(f"클라이언트 연결: {request.sid}")
    emit('status', {'message': '연결 성공'})

@socketio.on('subscribe')
def handle_subscribe(data):
    """특정 채널의 실시간 업데이트 구독"""
    channel = data['channel']
    join_room(channel)
    emit('subscribed', {'channel': channel})

def broadcast_update(channel, data):
    """특정 채널에 업데이트 브로드캐스트"""
    socketio.emit('update', {
        'channel': channel,
        'data': data,
        'timestamp': time.time()
    }, room=channel)

@socketio.on('user_action')
def handle_action(data):
    """사용자 액션 수신 → 처리 결과 반환"""
    result = process_action(data['action'], data['params'])
    emit('action_result', {
        'action_id': data['action_id'],
        'result': result
    })

if __name__ == '__main__':
    socketio.run(app, host='0.0.0.0', port=8080)

클라이언트 구현 (JavaScript)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 웹 브라우저 클라이언트
const socket = io('http://realtime-server:8080');

socket.on('connect', () => {
    console.log('서버 연결됨');
    
    // 채널 구독
    socket.emit('subscribe', { channel: 'job-updates' });
});

socket.on('update', (data) => {
    console.log(`업데이트: ${data.channel}`);
    updateUI(data.data);
});

socket.on('action_result', (data) => {
    handleResult(data.result);
});

// 사용자 액션 전송
function sendAction(action, params) {
    socket.emit('user_action', {
        action_id: generateId(),
        action: action,
        params: params
    });
}

Python 클라이언트

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import socketio

sio = socketio.Client()

@sio.event
def connect():
    print('서버 연결됨')
    sio.emit('subscribe', {'channel': 'job-updates'})

@sio.on('update')
def on_update(data):
    print(f"업데이트 수신: {data['channel']}")
    process_update(data['data'])

sio.connect('http://realtime-server:8080')
sio.wait()

WebSocket 장단점

  • 장점
    • 실시간 양방향 통신
    • 연결 유지로 오버헤드 최소화
    • 서버 푸시 가능
  • 단점
    • 상태 유지 필요 (stateful)
    • 로드밸런싱 복잡 (sticky session 필요)
    • 연결 관리 오버헤드

WebSocket 적합한 사용 시나리오

  • 실시간 모니터링 대시보드
  • 채팅, 알림 시스템
  • 라이브 데이터 스트리밍
  • 협업 도구 (동시 편집)

간접 통신 (Broker-Mediated)

중간 브로커를 통해 간접적으로 통신합니다. 생산자와 소비자가 서로를 알 필요가 없어 결합도가 낮고, 부하 분산과 장애 격리에 효과적입니다.

Message Queue: 비동기 작업 처리

Message Queue 개념

메시지 큐는 서비스 간 비동기 통신을 위한 중간 계층입니다.
생산자(Producer)가 메시지를 큐에 넣으면,
소비자(Consumer)가 자신의 속도로 처리합니다.
시스템 컴포넌트 간 결합도를 낮추고 부하를 분산합니다.

아키텍처 패턴

flowchart LR A[API Server
Producer] --> B[Message Queue
Redis/RabbitMQ] B --> C[Worker
Consumer] C --> D[(Database
결과 저장)]

Redis 기반 구현

생산자 (API Server)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import redis
import json
import uuid

redis_client = redis.Redis(host='redis-server', port=6379)

def submit_job(job_type, params):
    """작업을 큐에 제출"""
    job_id = str(uuid.uuid4())
    
    job_data = {
        'job_id': job_id,
        'type': job_type,
        'params': params,
        'submitted_at': time.time()
    }
    
    # 작업을 큐에 추가 (LPUSH: 왼쪽에서 삽입)
    redis_client.lpush('job_queue', json.dumps(job_data))
    
    # 작업 상태 초기화
    redis_client.hset(f'job:{job_id}', mapping={
        'status': 'queued',
        'progress': 0
    })
    
    return job_id
소비자 (Worker)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import redis
import json

redis_client = redis.Redis(host='redis-server', port=6379)

def worker_loop():
    """작업 큐에서 작업을 가져와 처리"""
    print("Worker 시작, 작업 대기 중...")
    
    while True:
        # BRPOP: 오른쪽에서 추출, 블로킹 (타임아웃 0 = 무한 대기)
        _, job_json = redis_client.brpop('job_queue', timeout=0)
        job = json.loads(job_json)
        
        job_id = job['job_id']
        print(f"작업 처리 시작: {job_id}")
        
        try:
            # 상태 업데이트
            redis_client.hset(f'job:{job_id}', 'status', 'running')
            
            # 실제 작업 수행
            result = process_job(job['type'], job['params'])
            
            # 결과 저장
            save_result(job_id, result)
            
            # 완료 상태
            redis_client.hset(f'job:{job_id}', mapping={
                'status': 'completed',
                'progress': 100
            })
            
        except Exception as e:
            redis_client.hset(f'job:{job_id}', mapping={
                'status': 'failed',
                'error': str(e)
            })

고급 패턴: 우선순위 큐

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 우선순위별 큐 사용
QUEUES = {
    'high': 'job_queue:high',
    'normal': 'job_queue:normal', 
    'low': 'job_queue:low'
}

def submit_job(job_data, priority='normal'):
    redis_client.lpush(QUEUES[priority], json.dumps(job_data))

def worker_with_priority():
    """우선순위 순서로 작업 처리"""
    while True:
        # 높은 우선순위부터 확인
        result = redis_client.brpop(
            [QUEUES['high'], QUEUES['normal'], QUEUES['low']],
            timeout=5
        )
        if result:
            queue_name, job_json = result
            process_job(json.loads(job_json))

RabbitMQ 대안

더 복잡한 라우팅이 필요하면 RabbitMQ를 고려합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pika

# 연결 설정
connection = pika.BlockingConnection(
    pika.ConnectionParameters('rabbitmq-server')
)
channel = connection.channel()

# 교환기와 큐 선언
channel.exchange_declare(exchange='jobs', exchange_type='topic')
channel.queue_declare(queue='image_processing', durable=True)
channel.queue_bind(
    exchange='jobs',
    queue='image_processing',
    routing_key='job.image.*'
)

# 메시지 발행
channel.basic_publish(
    exchange='jobs',
    routing_key='job.image.resize',
    body=json.dumps(job_data),
    properties=pika.BasicProperties(delivery_mode=2)  # 메시지 영속성
)

Message Queue 장단점

  • 장점
    • 서비스 간 느슨한 결합
    • 부하 분산 및 버퍼링
    • 장애 격리 (한 서비스 실패가 전체에 영향 X)
    • 재시도 및 데드레터 큐 지원
  • 단점
    • 즉시 응답 불가 (비동기)
    • 메시지 순서 보장이 복잡할 수 있음
    • 추가 인프라 필요

Message Queue 적합한 사용 시나리오

  • 시간이 오래 걸리는 작업 (이미지 처리, 보고서 생성)
  • 작업 부하 분산 (여러 Worker로 병렬 처리)
  • 서비스 간 이벤트 전달
  • 재시도가 필요한 작업

통신 전략 선택 가이드

결정 플로우차트

flowchart TD A[작업 특성 분석] --> B{실시간 양방향 필요?} B -->|Yes| C[WebSocket] B -->|No| D{즉시 응답 필요?} D -->|Yes| E{외부 클라이언트?} E -->|Yes| F[REST API] E -->|No| G[gRPC] D -->|No| H[Message Queue]

일반적인 적용 예시

통신 구간권장 방식이유
웹/모바일 → API 서버REST브라우저 호환성, 범용성
API 서버 → 작업 큐Message Queue비동기 작업 분배
마이크로서비스 간gRPC고성능, 타입 안전성
서버 → 클라이언트 푸시WebSocket실시간 업데이트
데이터 CRUDREST단순함, 표준화

하이브리드 아키텍처

구간별 기술 배치

실제 시스템에서는 각 구간의 특성에 맞게 여러 방식을 조합합니다:

flowchart TB subgraph Client[웹 브라우저] end subgraph Backend[백엔드 서비스] API[API Gateway
Flask/FastAPI] RT[Realtime Server
SocketIO] MQ[(Message Queue
Redis)] W1[Worker A] W2[Worker B] DB[(Database)] end Client -->|REST| API Client <-->|WebSocket| RT API -->|LPUSH| MQ RT -->|Pub/Sub| MQ MQ -->|BRPOP| W1 MQ -->|BRPOP| W2 W1 <-->|gRPC| W2 W1 -->|REST| DB W2 -->|REST| DB

기술 간 결합 패턴: gRPC + WebSocket 브릿지

같은 구간에서 두 기술을 결합하는 패턴도 실무에서 자주 사용됩니다.
대표적인 예가 gRPC 스트리밍과 WebSocket을 연결하는 브릿지 패턴입니다.

gRPC는 브라우저에서 직접 호출할 수 없기 때문에,
Realtime Server가 내부적으로 gRPC 스트리밍을 수신하고
이를 WebSocket으로 브라우저에 중계합니다.

flowchart LR subgraph Browser[웹 브라우저] UI[Dashboard UI] end subgraph Bridge[Realtime Server] WS[WebSocket 엔드포인트] GC[gRPC Client] end subgraph Internal[내부 서비스] PS[Processing Service
gRPC Server] end UI <-->|WebSocket| WS WS --- GC GC <-->|gRPC Stream| PS
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Realtime Server: gRPC 스트리밍 → WebSocket 브릿지
import grpc
import processing_pb2
import processing_pb2_grpc
from flask_socketio import SocketIO, emit

@socketio.on('subscribe_job')
def handle_subscribe(data):
    """gRPC 스트리밍을 받아 WebSocket으로 중계"""
    job_id = data['job_id']

    # 내부 gRPC 서비스에 스트리밍 요청
    channel = grpc.insecure_channel('processing-service:50051')
    stub = processing_pb2_grpc.ProcessingServiceStub(channel)

    for result in stub.StreamResults(
        processing_pb2.JobId(id=job_id)
    ):
        # gRPC 결과를 WebSocket으로 실시간 전달
        emit('job_progress', {
            'job_id': job_id,
            'chunk_id': result.chunk_id,
            'data_size': len(result.result_data),
            'timestamp': result.timestamp
        })

이 패턴의 장점은 내부 서비스 간에는 gRPC의 고성능과 타입 안전성을 유지하면서,
브라우저에는 WebSocket의 실시간 양방향 통신을 제공할 수 있다는 것입니다.

결론

통신 전략 선택은 단순한 기술 결정이 아니라 시스템 아키텍처를 형성하는 핵심 요소입니다.

통신 전략 비교 요약

항목REST APIgRPCWebSocketMessage Queue
연결직접 (요청-응답)직접 (요청-응답/스트리밍)직접 (양방향 실시간)간접 (브로커 경유)
프로토콜HTTP/1.1HTTP/2TCP (WS)AMQP / Redis 등
데이터JSON (텍스트)Protobuf (바이너리)자유 형식자유 형식
성능중간높음높음가변
결합도중간높음 (.proto 공유)중간낮음
브라우저지원미지원 (gRPC-Web)지원미지원
적합 구간외부 API내부 서비스 간실시간 푸시비동기 작업 분배

전략 선택 한눈에 보기

flowchart LR subgraph 직접통신[직접 통신] REST[REST API
외부 노출, CRUD] GRPC[gRPC
내부 고성능] WS[WebSocket
실시간 양방향] end subgraph 간접통신[간접 통신] MQ[Message Queue
비동기 작업 분배] end REST ---|결합| WS GRPC ---|브릿지| WS REST ---|작업 위임| MQ MQ ---|결과 알림| WS
  • 핵심 원칙:
    • 외부 노출 API는 REST로 시작하여 필요시 확장
    • 내부 고성능 통신은 gRPC 고려
    • 실시간 업데이트는 WebSocket
    • 시간이 오래 걸리는 작업은 Message Queue로 비동기화
    • 필요에 따라 gRPC + WebSocket 브릿지처럼 기술을 결합

대부분의 실제 시스템에서는 이 네 가지를 적절히 조합하여,
API Gateway는 REST로 외부 요청을 받고,
Message Queue로 Worker에 작업을 분배하며,
실시간 서버는 WebSocket으로 라이브 업데이트를 제공하는 하이브리드 구조가 효과적입니다.

참고 자료