프로세스 간 통신 — OS 이론부터 Redis, Kafka까지
개요#
OS 수업에서 IPC(Inter-Process Communication)를 배울 때는 공유 메모리, 파이프, 메시지 큐 같은 저수준 메커니즘만 보였습니다. 그때는 이게 실무에서 어디에 닿는지 잘 감이 안 왔습니다.
그런데 서버 개발을 하면서 Redis, Kafka, RabbitMQ 같은 도구를 쓰다 보니 결국 같은 문제를 다른 층위에서 풀고 있다는 걸 알게 됐습니다. 프로세스든 서비스든, 핵심은 결국 "서로 다른 주체가 어떻게 안전하게 데이터를 주고받을 것인가"였습니다.
IPC의 두 가지 모델#
OS에서 IPC는 크게 두 가지로 나뉩니다.
1. 공유 메모리 (Shared Memory)#
두 프로세스가 같은 메모리 영역에 접근해서 데이터를 주고받는 방식입니다.
POSIX 환경에서는 이런 흐름으로 구현합니다:
shm_open()— 공유 메모리 객체 생성ftruncate()— 크기 설정mmap()— 프로세스 주소 공간에 매핑- 데이터 읽기/쓰기
shm_unlink()— 사용 끝나면 제거
장점: 데이터 복사가 없어서 빠릅니다. 대용량 데이터를 주고받을 때 유리합니다.
단점: 동기화를 직접 해야 합니다. 두 프로세스가 동시에 같은 메모리에 쓰면 데이터가 깨집니다. 세마포어나 뮤텍스로 직접 관리해야 하는데, 이게 까다롭습니다.
2. 메시지 전달 (Message Passing)#
OS 커널을 통해 메시지를 보내고 받는 방식입니다. send()와 receive() 두 연산으로 동작합니다.
공유 메모리와 달리 프로세스끼리 메모리를 공유하지 않기 때문에 동기화 문제가 없습니다. 커널이 메시지 전달을 관리해주니까요.
장점: 동기화를 신경 쓸 필요 없습니다. 구현이 깔끔합니다.
단점: 커널을 거치면서 데이터 복사가 발생합니다. 대용량 데이터에는 공유 메모리보다 느립니다.
직접 vs 간접 통신#
메시지 전달에도 두 가지 방식이 있습니다:
- 직접 통신: 프로세스 A가 프로세스 B에게 직접 메시지를 보냅니다. 소켓 통신이 여기에 해당합니다.
- 간접 통신: 중간에 메일박스(큐)를 두고, A는 큐에 넣고 B는 큐에서 꺼냅니다. 메시지 큐가 여기에 해당합니다.
실무에서의 IPC#
서버 개발에서 쓰는 도구들이 결국 IPC의 확장입니다.
소켓 통신 → HTTP / gRPC#
가장 기본적인 프로세스 간 통신입니다. 서비스 A가 서비스 B에 HTTP 요청을 보내는 것 자체가 소켓 기반 IPC입니다. gRPC도 마찬가지로 소켓 위에서 동작합니다.
Vanilla Java로 HTTP Server를 직접 구현할 때 ServerSocket으로 클라이언트 연결을 받는 것이 바로 소켓 IPC의 기본형입니다.
공유 메모리 → Redis#
Redis를 캐시로 쓰면 여러 서비스가 같은 데이터에 접근하는 것이니 공유 메모리와 같은 개념입니다. 다만 같은 머신의 메모리가 아니라 네트워크를 거쳐서 접근한다는 차이가 있습니다.
Redis 분산 락
공유 메모리에서 동기화가 필요했던 것처럼, Redis를 공유 저장소로 쓸 때도 동시 접근 제어가 필요합니다. Redis의 SETNX 명령으로 분산 락을 구현할 수 있습니다.
// Redisson 분산 락 예시
RLock lock = redissonClient.getLock("coupon-stock:" + couponId);
try {
if (lock.tryLock(5, 3, TimeUnit.SECONDS)) {
// 재고 차감 로직
couponStock.decrease();
}
} finally {
lock.unlock();
}MySQL S/X 락에서 다룬 선착순 쿠폰 경합 문제를 DB 락 대신 Redis 분산 락으로도 해결할 수 있습니다. DB에 부하를 주지 않으면서 동시성을 제어할 수 있다는 장점이 있습니다.
메시지 큐 → Kafka / RabbitMQ#
OS의 메시지 전달 모델을 분산 환경으로 확장한 것이 메시지 큐입니다. Producer가 메시지를 큐에 넣고, Consumer가 꺼내서 처리합니다. 간접 통신 모델 그대로입니다.
Kafka
Producer → Topic(파티션) → Consumer Group
Kafka는 메시지를 디스크에 저장하고 offset으로 관리합니다. (Apache Kafka Documentation) Consumer가 죽어도 메시지가 사라지지 않고, 재처리가 가능합니다. 대용량 이벤트 스트리밍에 적합합니다.
RabbitMQ
Producer → Exchange → Queue → Consumer
RabbitMQ는 라우팅이 유연합니다. (RabbitMQ Tutorials) Exchange 타입(direct, topic, fanout)에 따라 메시지를 원하는 큐로 보낼 수 있습니다. 작업 분배나 이벤트 알림에 적합합니다.
언제 뭘 쓸까#
| 상황 | 도구 | 이유 |
|---|---|---|
| 서비스 간 실시간 요청/응답 | HTTP / gRPC | 동기 통신, 결과를 바로 받아야 할 때 |
| 공유 데이터 캐시 + 동시성 제어 | Redis | 빠른 읽기/쓰기 + 분산 락 |
| 비동기 이벤트 처리 | Kafka | 대용량, 순서 보장, 재처리 필요 시 |
| 작업 분배 + 유연한 라우팅 | RabbitMQ | 작업 큐, 이벤트 알림 |
동기 vs 비동기#
IPC에서 가장 중요한 결정 중 하나가 동기/비동기 선택입니다.
동기 통신 (HTTP 요청 → 응답 대기)
- 구현이 단순합니다
- 호출하는 쪽이 응답을 기다리는 동안 스레드를 점유합니다
- 호출 대상 서비스가 느려지면 호출하는 쪽도 같이 느려집니다
비동기 통신 (메시지 큐에 넣고 끝)
- 호출하는 쪽은 메시지를 넣고 바로 다음 작업으로 넘어갑니다
- 대상 서비스가 느려져도 호출하는 쪽에 영향이 없습니다
- 하지만 결과를 바로 받을 수 없고, 실패 처리가 복잡해집니다
비동기 스레드 풀 분리 패턴에서 다룬 것처럼, 외부 API처럼 느린 작업은 비동기로 분리하고 핵심 로직은 동기로 유지하는 것이 실전에서 자주 쓰이는 패턴입니다.
참고 자료#
돌아보면 이 글에서 말하고 싶었던 건 새로운 도구를 많이 아는 것보다, 그 도구가 결국 어떤 IPC 모델에 가까운지 보는 눈이 더 중요하다는 점이었습니다. Redis는 네트워크 너머의 공유 메모리처럼 쓰이고, Kafka는 내구성이 붙은 메시지 큐처럼 쓰입니다.
이 기본 감각이 있으면 처음 보는 도구를 만나도 덜 막막합니다. 이름이 바뀌고 구현체가 달라도, 결국 풀고 있는 문제는 생각보다 오래된 문제인 경우가 많았습니다.