dev notes

프로세스 간 통신 — OS 이론부터 Redis, Kafka까지

2024-01-1510 min read
공유

개요#

OS 수업에서 IPC(Inter-Process Communication)를 배울 때는 공유 메모리, 파이프, 메시지 큐 같은 저수준 메커니즘만 보였습니다. 그때는 이게 실무에서 어디에 닿는지 잘 감이 안 왔습니다.

그런데 서버 개발을 하면서 Redis, Kafka, RabbitMQ 같은 도구를 쓰다 보니 결국 같은 문제를 다른 층위에서 풀고 있다는 걸 알게 됐습니다. 프로세스든 서비스든, 핵심은 결국 "서로 다른 주체가 어떻게 안전하게 데이터를 주고받을 것인가"였습니다.

IPC의 두 가지 모델#

OS에서 IPC는 크게 두 가지로 나뉩니다.

Loading diagram...

1. 공유 메모리 (Shared Memory)#

두 프로세스가 같은 메모리 영역에 접근해서 데이터를 주고받는 방식입니다.

POSIX 환경에서는 이런 흐름으로 구현합니다:

  1. shm_open() — 공유 메모리 객체 생성
  2. ftruncate() — 크기 설정
  3. mmap() — 프로세스 주소 공간에 매핑
  4. 데이터 읽기/쓰기
  5. shm_unlink() — 사용 끝나면 제거

장점: 데이터 복사가 없어서 빠릅니다. 대용량 데이터를 주고받을 때 유리합니다.

단점: 동기화를 직접 해야 합니다. 두 프로세스가 동시에 같은 메모리에 쓰면 데이터가 깨집니다. 세마포어나 뮤텍스로 직접 관리해야 하는데, 이게 까다롭습니다.

2. 메시지 전달 (Message Passing)#

OS 커널을 통해 메시지를 보내고 받는 방식입니다. send()receive() 두 연산으로 동작합니다.

공유 메모리와 달리 프로세스끼리 메모리를 공유하지 않기 때문에 동기화 문제가 없습니다. 커널이 메시지 전달을 관리해주니까요.

장점: 동기화를 신경 쓸 필요 없습니다. 구현이 깔끔합니다.

단점: 커널을 거치면서 데이터 복사가 발생합니다. 대용량 데이터에는 공유 메모리보다 느립니다.

직접 통신과 간접 통신#

메시지 전달 방식은 중간 매개체가 있느냐 없느냐로 나뉩니다. 소켓 통신은 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 명령으로 분산 락을 구현할 수 있습니다.

java
// 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작업 큐, 이벤트 알림

동기와 비동기, 어디서 자르나#

IPC를 설계할 때 실제로 가장 많이 고민하게 되는 게 이 지점이다. HTTP 요청처럼 응답을 기다리는 동기 방식은 구현이 단순하고 흐름을 추적하기 쉽지만, 호출 대상이 느려지면 스레드를 물고 있는 채로 같이 느려진다. 서비스가 연쇄적으로 느려지는 상황이 바로 여기서 나온다.

반면 메시지 큐에 넣고 끝내는 비동기 방식은 호출하는 쪽이 영향을 덜 받는다. 대신 결과를 즉시 받을 수 없고, 실패했을 때 재시도나 보상 트랜잭션 같은 처리가 붙어야 한다. 단순히 "비동기가 낫다"는 게 아니라, 빠른 응답이 필요한지 vs 처리 완료 여부가 중요한지에 따라 선택이 달라진다.

비동기 스레드 풀 분리 패턴에서 다룬 것처럼, 외부 API처럼 느린 작업은 비동기로 분리하고 핵심 로직은 동기로 유지하는 것이 실전에서 자주 쓰이는 패턴입니다.

참고 자료#

새로운 도구를 배울 때 "이게 어떤 IPC 모델인가"를 먼저 파악하는 게 생각보다 많이 도움이 됐다. Redis는 네트워크 너머의 공유 메모리처럼 쓰이고, Kafka는 내구성이 붙은 메시지 큐처럼 쓰인다. 이름이 바뀌고 구현체가 달라도, 결국 풀고 있는 문제는 OS 이론 시간에 이미 한 번 봤던 것들이다.

Connected Notes