Redis와 Kafka를 활용한 대규모 트래픽 처리 시스템 설계하기

미니 프로젝트 실습환경 구축하기

미니 프로젝트 실습환경 구축하기


레거시 모놀리식 서비스를 Redis와 Kafka 기반으로 전환하기

대규모 트래픽을 처리하는 서비스를 설계하려면 처음부터 모든 기능을 마이크로서비스로 개발하는 것보다, 기존 서비스를 분석하고 점진적으로 분리하는 과정이 중요하다.

가게, 상품, 주문, 재고 기능이 하나의 애플리케이션에 모두 들어 있는 레거시 모놀리식 서비스를 기준으로 Redis와 Kafka를 적용해 보자.

Redis는 캐싱, 세션, 재고, 분산 락을 담당하고 Kafka는 이벤트 전달, 비동기 처리, 이벤트 로그 저장을 담당하도록 구성한다. 이후 서비스를 가게, 상품, 주문, 재고 단위로 분리해 독립적으로 확장할 수 있는 구조로 개선한다.


개념

레거시 시스템

레거시 시스템은 오랫동안 사용되어 온 소프트웨어나 인프라를 의미한다.

레거시라는 말이 반드시 나쁜 시스템이라는 뜻은 아니다. 오랜 기간 운영되면서 비즈니스 요구사항을 충족하고 있을 수도 있다.

다만 다음과 같은 문제가 누적되기 쉽다.

  • 오래된 기술과 라이브러리 사용
  • 변경 영향 범위 파악 어려움
  • 기능 간 강한 결합
  • 테스트 코드 부족
  • 특정 개발자에게 지식 집중
  • 배포 시 전체 서비스 재배포 필요
  • 작은 변경에도 전체 장애 발생 가능

따라서 레거시 시스템을 개선할 때는 기존 기능을 모두 버리고 새로 개발하기보다, 현재 동작을 이해한 뒤 변경 범위를 단계적으로 나누는 방식이 적합하다.

모놀리식 아키텍처

모놀리식 아키텍처는 애플리케이션의 모든 기능이 하나의 코드베이스와 하나의 프로세스 안에서 동작하는 구조이다.

flowchart LR
    Client["Client"] --> Monolith["Monolithic Application"]
    Monolith --> Store["Store Module"]
    Monolith --> Product["Product Module"]
    Monolith --> Order["Order Module"]
    Monolith --> Stock["Stock Module"]
    Monolith --> Database["Single Database"]

모놀리식 구조는 초기 개발이 단순하다는 장점이 있다. 하나의 프로젝트 안에서 모듈을 호출하고, 하나의 데이터베이스를 사용하면 되기 때문이다.

반면 서비스 규모가 커지면 다음 문제가 발생한다.

  • 상품 기능을 수정해도 전체 애플리케이션을 빌드해야 한다.
  • 주문 트래픽이 증가해도 전체 애플리케이션을 함께 확장해야 한다.
  • 하나의 기능 장애가 전체 서비스에 영향을 준다.
  • 기능 간 데이터베이스 접근이 뒤섞인다.
  • 배포와 롤백 단위가 지나치게 커진다.

마이크로서비스 아키텍처

마이크로서비스 아키텍처는 하나의 애플리케이션을 여러 개의 작은 서비스로 분리하는 구조이다.

각 서비스는 특정 비즈니스 기능을 담당하며 독립적으로 개발, 배포, 확장할 수 있다.

flowchart LR
    Client["Client"] --> Gateway["Service Gateway"]
    Gateway --> Store["Store Service"]
    Gateway --> Product["Product Service"]
    Gateway --> Order["Order Service"]
    Gateway --> Stock["Stock Service"]

    Store --> StoreDB["Store Database"]
    Product --> ProductDB["Product Database"]
    Order --> OrderDB["Order Database"]
    Stock --> Redis["Redis"]

마이크로서비스로 분리한다고 해서 단순히 패키지나 클래스를 나누는 것만으로 충분하지는 않다.

다음 항목까지 함께 분리해야 한다.

  • 서비스별 책임
  • 서비스별 데이터 소유권
  • 서비스 간 통신 방식
  • 장애 전파 범위
  • 배포 단위
  • 로그와 모니터링 방식
  • 트랜잭션 처리 기준

왜 사용하는가?

서비스별 독립적인 확장

상품 조회는 주문보다 훨씬 자주 발생할 수 있다. 상품 서비스와 주문 서비스가 하나의 애플리케이션으로 묶여 있으면 상품 조회량이 증가했을 때 주문 기능까지 함께 확장해야 한다.

서비스를 분리하면 상품 서비스만 여러 인스턴스로 늘릴 수 있다.

flowchart LR
    Gateway["Gateway"] --> ProductA["Product Service A"]
    Gateway --> ProductB["Product Service B"]
    Gateway --> Order["Order Service"]
    ProductA --> Redis["Redis Cache"]
    ProductB --> Redis

장애 범위 축소

주문 서비스에 문제가 발생하더라도 상품 조회 서비스까지 반드시 중단될 필요는 없다.

물론 서비스 간 의존성이 있다면 일부 기능이 제한될 수 있지만, 모든 기능이 동시에 중단되는 상황은 줄일 수 있다.

Redis를 통한 응답 속도 개선

상품 목록이나 상품 상세 정보처럼 조회가 많은 데이터는 Redis에 캐시할 수 있다.

상품 조회 요청
→ Redis 캐시 확인
→ 캐시가 있으면 즉시 반환
→ 캐시가 없으면 데이터베이스 조회
→ 조회 결과를 Redis에 저장

데이터베이스에 반복적으로 접근하는 횟수를 줄이면 데이터베이스 부하와 응답 시간을 낮출 수 있다.

Kafka를 통한 비동기 처리

주문이 완료된 뒤 로그 저장, 알림 발송, 통계 계산을 모두 동기적으로 처리하면 사용자는 모든 작업이 끝날 때까지 기다려야 한다.

Kafka를 사용하면 주문 서비스는 주문 생성에 필요한 작업을 완료한 뒤 이벤트를 발행하고 빠르게 응답할 수 있다.

주문 생성
→ 주문 이벤트 발행
→ 사용자 응답

주문 이벤트 소비
→ 이벤트 로그 저장
→ 알림 발송
→ 통계 처리

각 소비자는 독립적으로 동작하기 때문에 새로운 후속 기능을 추가할 때 주문 서비스의 변경을 줄일 수 있다.


주요 특징

이커머스 서비스의 책임 분리

가게, 상품, 주문, 재고를 다음과 같이 분리할 수 있다.

서비스책임주요 데이터
Store Service가게 등록과 조회가게명, 판매자, 운영 상태
Product Service상품 등록과 조회상품명, 가격, 설명
Order Service주문 생성과 상태 관리주문자, 상품, 주문 상태
Stock Service재고 조회와 차감상품별 재고 수량
Auth Service인증과 세션 관리사용자 인증 정보
Event Service이벤트 소비와 로그 저장주문 이벤트, 재고 이벤트

서비스를 나눌 때는 데이터베이스 테이블 기준보다 비즈니스 책임을 기준으로 나누는 것이 좋다.

예를 들어 상품 테이블과 재고 테이블이 같은 데이터베이스에 있다고 해서 반드시 하나의 서비스로 묶어야 하는 것은 아니다. 상품 정보와 재고 정보는 변경 빈도와 동시성 요구사항이 다르기 때문이다.

Gateway를 통한 단일 진입점

클라이언트가 각 서비스에 직접 요청하면 서비스 주소와 포트가 클라이언트에 노출된다.

Gateway를 사용하면 외부에서는 하나의 주소만 사용하고, Gateway가 내부 서비스로 요청을 전달한다.

/api/stores/**   → Store Service
/api/products/** → Product Service
/api/orders/**   → Order Service
/api/auth/**     → Auth Service

Spring Cloud Gateway 설정 예시는 다음과 같다.

spring:
  cloud:
    gateway:
      routes:
        - id: store-service
          uri: http://store-service:8080
          predicates:
            - Path=/api/stores/**

        - id: product-service
          uri: http://product-service:8080
          predicates:
            - Path=/api/products/**

        - id: order-service
          uri: http://order-service:8080
          predicates:
            - Path=/api/orders/**

        - id: auth-service
          uri: http://auth-service:8080
          predicates:
            - Path=/api/auth/**
설정의미
id라우팅 규칙 이름
uri요청을 전달할 내부 서비스 주소
Path해당 라우팅을 적용할 URL 패턴

Gateway에는 인증 토큰 확인, 공통 로깅, CORS, 요청 제한 등을 배치할 수 있다.

다만 주문 생성이나 재고 차감 같은 비즈니스 로직을 Gateway에 넣으면 Gateway의 책임이 지나치게 커진다. Gateway는 요청을 전달하고 공통 정책을 적용하는 역할에 집중하는 것이 좋다.

OpenFeign을 이용한 서비스 간 통신

주문 서비스가 상품 정보를 확인하거나 재고 서비스에 재고 차감을 요청해야 한다면 서비스 간 통신이 필요하다.

OpenFeign을 사용하면 HTTP 통신을 인터페이스 형태로 작성할 수 있다.

@FeignClient(name = "stock-service", url = "${services.stock.url}")
public interface StockClient {

    @PostMapping("/stocks/{productId}/decrease")
    void decrease(
        @PathVariable("productId") Long productId,
        @RequestParam("quantity") int quantity
    );
}

설정 파일은 다음과 같이 구성할 수 있다.

services:
  stock:
    url: http://stock-service:8080

OpenFeign을 사용할 때는 다음 항목을 함께 설정해야 한다.

  • 연결 타임아웃
  • 읽기 타임아웃
  • 재시도 횟수
  • 장애 서비스 차단
  • 실패 시 대체 응답
  • 요청 추적용 헤더

서비스 간 통신에서 타임아웃을 설정하지 않으면 하나의 장애 서비스 때문에 다른 서비스의 스레드가 계속 대기할 수 있다.

Redis의 역할

Redis는 다음 기능에 활용할 수 있다.

기능Redis 자료구조 또는 기능
글로벌 세션String, Hash, Redis Session
상품 캐시String
재고 수량String
분산 락Redisson Lock
좋아요 사용자Set
일일 방문자HyperLogLog
최근 검색어List
캐시 동기화Pub/Sub

Redis를 사용할 때는 저장하는 데이터의 중요도를 구분해야 한다.

  • 다시 생성할 수 있는 데이터: 캐시
  • 짧은 시간만 필요한 데이터: 세션, 인증 코드
  • 동시성 제어가 필요한 데이터: 재고, 락
  • 유실되면 안 되는 데이터: 원본 데이터베이스나 Kafka와 함께 관리

Kafka의 역할

Kafka는 서비스에서 발생하는 이벤트를 전달한다.

예를 들어 주문이 생성되면 다음과 같은 이벤트를 발행할 수 있다.

{
  "eventId": "order-event-5001",
  "eventType": "ORDER_CREATED",
  "orderId": 5001,
  "userId": 1001,
  "productId": 2001,
  "quantity": 2,
  "createdAt": "2026-09-01T12:00:00"
}

이벤트를 소비하는 서비스는 서로 다른 Consumer Group을 사용할 수 있다.

Consumer Group역할
event-log-group이벤트 로그 저장
notification-group사용자 알림 발송
statistics-group판매 통계 계산

같은 이벤트를 여러 목적에 사용해야 한다면 소비자 그룹을 분리해야 한다. 하나의 소비자 그룹 안에서는 각 메시지가 하나의 소비자에게만 전달되지만, 서로 다른 소비자 그룹은 같은 이벤트를 각각 받을 수 있다.


예제

프로젝트 환경 구성

로컬에서 Redis와 Kafka를 실행하기 위한 docker-compose.yml 예시는 다음과 같다.

services:
  redis:
    image: redis:7.2
    container_name: ecommerce-redis
    ports:
      - "6379:6379"
    command:
      - redis-server
      - --appendonly
      - "yes"
      - --requirepass
      - redis_password
    healthcheck:
      test:
        [
          "CMD",
          "redis-cli",
          "-a",
          "redis_password",
          "ping"
        ]
      interval: 5s
      timeout: 3s
      retries: 10

  kafka:
    image: bitnami/kafka:3.7
    container_name: ecommerce-kafka
    ports:
      - "9092:9092"
    environment:
      KAFKA_CFG_NODE_ID: 1
      KAFKA_CFG_PROCESS_ROLES: controller,broker
      KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
      KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
      KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
      KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CFG_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_ENABLE_KRAFT: "yes"
      ALLOW_PLAINTEXT_LISTENER: "yes"

이 구성은 Spring Boot 애플리케이션을 호스트 운영체제에서 실행하고 Redis와 Kafka만 Docker에서 실행하는 형태이다.

컨테이너를 실행한다.

docker compose up -d

실행 결과 예시는 다음과 같다.

[+] Running 2/2
 ✔ Container ecommerce-redis  Started
 ✔ Container ecommerce-kafka  Started

-d 옵션은 백그라운드 모드로 컨테이너를 실행한다는 의미이다.

실행 중인 컨테이너는 다음 명령어로 확인한다.

docker compose ps

실행 결과 예시는 다음과 같다.

NAME               IMAGE                 SERVICE   STATUS
ecommerce-redis    redis:7.2             redis     running
ecommerce-kafka    bitnami/kafka:3.7     kafka     running

Redis 연결을 확인한다.

docker exec ecommerce-redis redis-cli -a redis_password ping

실행 결과는 다음과 같다.

PONG

Kafka 토픽을 생성한다.

docker exec ecommerce-kafka kafka-topics.sh \
  --create \
  --topic order-events \
  --bootstrap-server localhost:9092 \
  --partitions 3 \
  --replication-factor 1

실행 결과 예시는 다음과 같다.

Created topic order-events.
옵션의미
--topic생성할 토픽 이름
--partitions 3토픽을 3개 파티션으로 구성
--replication-factor 1메시지 복제본 수
--bootstrap-serverKafka 브로커 주소

개발 환경에서는 복제본 수를 1로 설정할 수 있지만, 운영 환경에서는 여러 브로커를 구성하고 적절한 복제본 수를 설정해야 한다.

Gradle 의존성

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.kafka:spring-kafka'
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
    implementation 'org.redisson:redisson-spring-boot-starter:3.27.2'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.kafka:spring-kafka-test'
}
의존성용도
starter-webREST API 개발
starter-data-redisRedis 연결
spring-kafkaKafka Producer와 Consumer
starter-gatewayAPI Gateway
starter-openfeign서비스 간 HTTP 통신
redisson-spring-boot-starter분산 락
spring-kafka-testKafka 테스트

Spring Cloud 의존성은 프로젝트의 Spring Boot 버전과 호환되는 Spring Cloud BOM을 함께 사용해야 한다. 실제 프로젝트에서는 버전 호환성을 확인한 뒤 의존성을 추가해야 한다.

애플리케이션 설정

spring:
  application:
    name: order-service

  data:
    redis:
      host: localhost
      port: 6379
      password: redis_password
      timeout: 2s

  kafka:
    bootstrap-servers: localhost:9092

    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      acks: all
      retries: 3

    consumer:
      group-id: event-log-group
      enable-auto-commit: false
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "com.example.event"

services:
  stock:
    url: http://localhost:8082

Redis와 Kafka를 Docker에서 실행하고 Spring Boot를 로컬에서 실행할 때는 localhost를 사용할 수 있다.

반대로 Spring Boot 애플리케이션도 Docker 컨테이너에서 실행한다면 localhost는 애플리케이션 컨테이너 자신을 의미한다. 이 경우 다음처럼 Docker Compose 서비스 이름을 사용해야 한다.

spring:
  data:
    redis:
      host: redis
      port: 6379

  kafka:
    bootstrap-servers: kafka:9092

이 차이를 이해하지 못하면 컨테이너는 실행 중인데 애플리케이션에서 Redis나 Kafka를 찾지 못하는 문제가 발생한다.

프로젝트 실행

Gradle 프로젝트를 빌드한다.

./gradlew clean build

Windows에서는 다음 명령어를 사용할 수 있다.

.\gradlew.bat clean build

실행 결과 예시는 다음과 같다.

BUILD SUCCESSFUL in 12s
8 actionable tasks: 8 executed

빌드가 성공하면 컴파일과 테스트 작업이 정상적으로 완료된 것이다.

애플리케이션을 실행한다.

./gradlew bootRun

Windows에서는 다음과 같이 실행한다.

.\gradlew.bat bootRun

실행 로그에서 다음 항목을 확인해야 한다.

Started OrderApplication in 4.321 seconds

이 메시지는 Spring Boot 애플리케이션이 정상적으로 시작되었다는 의미이다.

서비스 상태는 다음과 같이 확인할 수 있다.

curl http://localhost:8080/actuator/health

실행 결과는 다음과 같다.

{
  "status": "UP"
}

구조

레거시 모놀리식 서비스를 마이크로서비스로 전환하는 과정은 한 번에 모든 기능을 분리하는 방식보다 단계적으로 진행하는 것이 안전하다.

flowchart LR
    Legacy["Legacy Monolith"] --> Analyze["Responsibility Analysis"]
    Analyze --> Gateway["Service Gateway"]
    Analyze --> Auth["Auth Service"]
    Analyze --> Store["Store Service"]
    Analyze --> Product["Product Service"]
    Analyze --> Order["Order Service"]
    Analyze --> Stock["Stock Service"]

    Product --> RedisCache["Redis Cache"]
    Auth --> RedisSession["Redis Session"]
    Stock --> RedisLock["Redis Distributed Lock"]
    Order --> Kafka["Kafka Events"]

전체 개발 흐름은 다음과 같이 구성할 수 있다.

flowchart TD
    Env["Project Environment"] --> GatewayStep["Gateway and Routing"]
    GatewayStep --> AuthStep["Authentication and Session"]
    AuthStep --> Ecommerce["Store Product Order"]
    Ecommerce --> StockStep["Stock and Distributed Lock"]
    StockStep --> EventStep["Kafka Event Log"]
    EventStep --> UserFeature["Like Visit Search"]
    UserFeature --> LoadTest["Traffic Test"]
    LoadTest --> Improve["Bottleneck Improvement"]

Kafka를 활용한 주문 이벤트 흐름은 다음과 같다.

sequenceDiagram
    participant Client
    participant Gateway
    participant Order
    participant Stock
    participant Redis
    participant Database
    participant Kafka
    participant Logger

    Client->>Gateway: 주문 요청
    Gateway->>Order: 주문 요청 전달
    Order->>Stock: 재고 차감 요청
    Stock->>Redis: 분산 락 획득
    Stock->>Redis: 재고 확인 및 차감
    Redis-->>Stock: 차감 완료
    Stock-->>Order: 재고 처리 성공
    Order->>Database: 주문 저장
    Order->>Kafka: 주문 이벤트 발행
    Kafka->>Logger: 이벤트 전달
    Logger->>Database: 이벤트 로그 저장
    Order-->>Client: 주문 성공 응답

Event Sourcing

Event Sourcing은 현재 상태만 저장하는 대신 상태를 변경한 이벤트를 저장하고, 이벤트를 재생해 현재 상태를 복원하는 방식이다.

예를 들어 재고 수량을 직접 7로 저장하는 대신 다음과 같은 이벤트를 저장할 수 있다.

STOCK_CREATED quantity=10
STOCK_DECREASED quantity=2
STOCK_INCREASED quantity=1
STOCK_DECREASED quantity=2

이벤트를 순서대로 재생하면 현재 재고가 7이라는 사실을 계산할 수 있다.

다만 Kafka에 이벤트 로그를 저장한다고 해서 자동으로 완전한 Event Sourcing이 되는 것은 아니다. 완전한 Event Sourcing을 적용하려면 이벤트가 상태의 원본이 되어야 하며, 이벤트 스키마 변경과 재생 전략까지 설계해야 한다.

프로젝트에서는 먼저 주문 이벤트를 Kafka로 발행하고 로그를 저장하는 방식부터 적용한 뒤, 필요에 따라 Event Sourcing으로 확장할 수 있다.

CQRS

CQRS는 명령을 처리하는 모델과 조회를 처리하는 모델을 분리하는 방식이다.

flowchart LR
    Command["Command Request"] --> WriteModel["Write Model"]
    WriteModel --> WriteDB["Write Database"]
    WriteModel --> Event["Domain Event"]
    Event --> ReadModel["Read Model"]
    ReadModel --> ReadDB["Read Database"]
    Query["Query Request"] --> ReadModel

주문 생성은 쓰기 모델에서 처리하고, 상품 목록이나 주문 조회는 조회 모델에서 처리하도록 분리할 수 있다.

조회 데이터는 Redis에 캐시하거나 별도의 조회용 테이블로 구성할 수 있다. 이렇게 하면 읽기 트래픽이 증가해도 쓰기 작업에 미치는 영향을 줄일 수 있다.

Event-Driven Architecture

Event-Driven Architecture는 서비스가 직접 모든 후속 작업을 호출하는 대신 이벤트를 발행하고, 다른 서비스가 이벤트를 구독해 처리하는 구조이다.

Order Service
→ ORDER_CREATED 이벤트 발행

Event Log Consumer
→ 이벤트 로그 저장

Notification Consumer
→ 주문 알림 발송

Statistics Consumer
→ 판매 통계 반영

이 구조를 사용하면 주문 서비스와 통계 서비스가 직접 결합되지 않는다. 통계 기능을 제거하거나 새로운 소비자를 추가해도 주문 서비스의 핵심 로직은 크게 변경되지 않는다.


실무에서의 활용

마이그레이션 순서 정하기

레거시 시스템을 분리할 때는 다음 순서가 적합하다.

  1. 현재 기능과 데이터 흐름을 분석한다.
  2. 서비스별 책임과 데이터 소유권을 정의한다.
  3. 로컬 개발 환경을 구성한다.
  4. Gateway를 추가한다.
  5. 인증과 세션을 Redis 기반으로 변경한다.
  6. 가게와 상품 기능을 분리한다.
  7. 주문과 재고 기능을 분리한다.
  8. 재고에 분산 락을 적용한다.
  9. Kafka 이벤트를 발행한다.
  10. 부하 테스트를 진행한다.

처음부터 주문, 재고, Kafka, 세션, Gateway를 모두 동시에 변경하면 문제가 발생했을 때 원인을 추적하기 어렵다.

각 단계마다 다음 항목을 확인하는 것이 좋다.

  • 기존 기능과 동일하게 동작하는가
  • API 응답 형식이 바뀌지 않았는가
  • 데이터가 중복 저장되지 않는가
  • 장애 발생 시 복구 가능한가
  • 로그로 요청 흐름을 추적할 수 있는가

환경별 설정 분리

로컬, 테스트, 운영 환경은 Redis와 Kafka 주소가 다르다. 설정 파일을 분리하거나 환경 변수로 관리해야 한다.

spring:
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:local}

로컬 설정은 다음과 같이 작성할 수 있다.

spring:
  config:
    activate:
      on-profile: local

  data:
    redis:
      host: localhost
      port: 6379
      password: redis_password

  kafka:
    bootstrap-servers: localhost:9092

운영 환경에서는 비밀번호를 설정 파일에 직접 작성하지 않고 환경 변수나 비밀 관리 시스템을 사용하는 것이 좋다.

spring:
  data:
    redis:
      host: ${REDIS_HOST}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD}

  kafka:
    bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS}

서비스 간 데이터 소유권

마이크로서비스에서는 각 서비스가 자신의 데이터를 관리해야 한다.

데이터소유 서비스
가게 정보Store Service
상품 정보Product Service
주문 정보Order Service
재고 정보Stock Service
세션 정보Auth Service 또는 공통 세션 저장소
이벤트 로그Event Log Service

주문 서비스가 상품 데이터베이스에 직접 접근하거나 상품 서비스가 주문 테이블을 직접 수정하면 서비스 간 결합도가 높아진다.

필요한 데이터는 API나 이벤트를 통해 전달받는 구조가 적합하다.

Redis와 Kafka의 장애 대응

Redis나 Kafka가 항상 정상 동작한다고 가정하면 안 된다.

Redis 장애 시에는 Redis의 용도에 따라 대응 방식이 달라진다.

Redis 용도장애 대응
상품 캐시원본 데이터베이스로 우회
세션인증 실패 또는 재로그인 처리
재고주문 차단 또는 대체 저장소 검토
분산 락주문 요청 제한
좋아요임시 저장 후 재처리

Kafka 장애가 발생하면 이벤트 발행 실패를 처리해야 한다.

  • 발행 재시도
  • 실패 이벤트 별도 저장
  • Outbox Pattern
  • 이벤트 발행 상태 기록
  • Consumer 재처리
  • 중복 이벤트 방지

특히 주문 저장은 성공했지만 Kafka 이벤트 발행이 실패하는 상황을 반드시 고려해야 한다.

부하 테스트

분산 환경을 구성한 뒤에는 실제로 성능이 개선되었는지 확인해야 한다.

비교 대상은 다음과 같이 구성할 수 있다.

환경설명
기준 환경모놀리식 구조, Redis와 Kafka 미사용
캐시 환경Redis 상품 캐시 적용
이벤트 환경Kafka 주문 이벤트 적용
분산 환경서비스 분리, Redis와 Kafka 모두 적용

측정해야 할 지표는 다음과 같다.

  • 평균 응답 시간
  • 최대 응답 시간
  • 초당 처리량
  • 오류율
  • CPU 사용량
  • 메모리 사용량
  • 데이터베이스 커넥션 수
  • Redis 캐시 적중률
  • Kafka Consumer 지연 시간

상품 조회와 주문 요청을 동일한 비율로 반복하기보다 실제 사용 패턴을 반영해야 한다.

상품 목록 조회: 45%
상품 상세 조회: 30%
로그인과 세션 확인: 10%
좋아요 처리: 5%
주문 생성: 5%
재고 조회: 5%

테스트 결과를 해석할 때는 TPS만 보면 안 된다. 처리량이 높더라도 재고가 잘못 차감되거나 주문 이벤트가 누락되면 정상적인 시스템이라고 할 수 없다.


정리

레거시 모놀리식 서비스를 Redis와 Kafka 기반의 마이크로서비스로 전환하려면 먼저 기존 기능과 데이터 흐름을 분석해야 한다.

가게, 상품, 주문, 재고 기능을 비즈니스 책임에 따라 분리하고, Gateway를 통해 외부 요청을 전달한다. Redis는 세션, 캐시, 재고, 분산 락, 좋아요, 방문자 수, 최근 검색어처럼 빠른 처리와 동시성 제어가 필요한 기능에 활용한다.

Kafka는 주문 이벤트와 같은 서비스 이벤트를 전달하고, 이벤트 로그 저장, 알림, 통계와 같은 후속 작업을 비동기로 처리하는 데 사용한다.

Event Sourcing은 이벤트를 원본 데이터로 관리하는 방식이고, CQRS는 명령과 조회 모델을 분리하는 방식이며, Event-Driven Architecture는 서비스 간 직접 호출을 줄이고 이벤트 중심으로 연결하는 방식이다.

다만 Redis나 Kafka를 적용했다고 해서 자동으로 확장성과 고가용성이 확보되는 것은 아니다. 서비스별 데이터 소유권, 장애 대응, 재시도, 중복 이벤트, 타임아웃, 부하 테스트까지 함께 설계해야 한다.

프로젝트는 환경 구성부터 시작해 Gateway, 인증과 세션, 이커머스 기능, 재고 분산 락, Kafka 이벤트, 사용자 경험 기능, 부하 테스트 순서로 단계적으로 진행하는 것이 안정적이다.


한 줄 요약

레거시 모놀리식 이커머스 서비스를 서비스별로 분리하고 Redis는 빠른 데이터 처리와 동시성 제어에, Kafka는 이벤트 기반 비동기 처리에 활용하면 확장 가능한 분산 시스템으로 발전시킬 수 있다.


© 2020. All rights reserved.

SIKSIK