기록

[Kafka × Elasticsearch 기반 상품 검색 시스템] 1. 상품 검색 엔진 설계 본문

Web/Spring

[Kafka × Elasticsearch 기반 상품 검색 시스템] 1. 상품 검색 엔진 설계

zyin 2025. 11. 24. 10:00

대규모 상품 데이터를 효율적으로 검색하기 위해서는, 데이터의 생성·저장·조회가 동일한 시스템에서 동시에 처리되지 않도록 분리해야 한다. 이번 글에서는 이러한 원리를 바탕으로 CQRS(Command Query Responsibility Segregation) 구조를 적용한 상품 검색 엔진(Product Search Engine) 설계를 다룬다. 시스템은 크게 두 개의 서버로 구성된다.

1. 전체 구조 개요

상품 검색 엔진은 다음과 같은 구조를 가진다.

주요 구성 요소

구성 요소  역할
API 서버 (Command) 상품 등록 및 수정 요청을 처리하고 Kafka로 이벤트를 발행한다.
Kafka 브로커 등록된 상품 정보를 이벤트 스트림 형태로 전달한다.
Query 서버 (Search) Kafka 이벤트를 수신하여 Elasticsearch에 색인하고, 사용자 검색 요청을 처리한다.
Elasticsearch 색인된 상품 데이터를 기반으로 빠른 검색을 수행한다.
Kibana 색인 데이터 시각화 및 디버깅 도구로 사용된다.

2. 도커 기반 개발 환경

로컬 개발 환경은 docker-compose.yml을 사용해 구성한다. Zookeeper, Kafka, Elasticsearch, Redis, PostgreSQL, Kibana를 한 번에 실행할 수 있다.

version: '3.8'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.5.1
    ports: ["2181:2181"]

  kafka:
    image: confluentinc/cp-kafka:7.5.1
    ports: ["29092:29092"]
    environment:
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:29092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    depends_on: [zookeeper]

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:9.1.5
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
    command: >
      bash -c "
        if [ ! -d 'plugins/analysis-nori' ]; then
          elasticsearch-plugin install --batch analysis-nori;
        fi;
        exec bin/elasticsearch
      "
    ports: ["9200:9200"]

  kibana:
    image: docker.elastic.co/kibana/kibana:9.1.5
    ports: ["5601:5601"]
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    depends_on: [elasticsearch]

 

Kafka와 Elasticsearch는 로컬 브리지 네트워크에서 통신하고, 호스트 환경에서는 localhost:29092, localhost:9200으로 접근할 수 있다.


3. 아키텍처 설계 철학

3.1. CQRS 구조

상품 등록과 검색은 성격이 다르다. 등록은 정확성트랜잭션 일관성이 중요하고, 검색은 속도대량 조회 처리가 중요하다. 이를 분리하면 각 서버가 자신의 책임에 집중할 수 있다.

구분 Command 서버 Query 서버
프레임워크 Spring MVC (spring-boot-starter-web) Spring WebFlux (spring-boot-starter-webflux)
주요 기능 상품 등록, 수정, Kafka 이벤트 발행 상품 색인, 검색 API
데이터 저장소 PostgreSQL (선택) Elasticsearch
메시징 Kafka Producer Kafka Listener
처리 방식 동기 요청/응답 비동기 Reactive Stream

3.2. 이벤트 드리븐 설계

상품 등록이 발생하면, API 서버는 Kafka에 ProductRegisterEvent를 발행한다. Query 서버는 이를 비동기적으로 수신하여 Elasticsearch에 색인한다. 이 구조의 장점은 다음과 같다.

  1. 확장성: 검색 서버가 독립적으로 스케일링 가능하다.
  2. 비동기 처리: 등록 지연 없이 색인이 병렬로 수행된다.
  3. 장애 격리: 검색 서버 장애가 등록 트랜잭션에 영향을 주지 않는다.
  4. 리플레이 가능성: Kafka 로그를 재처리하여 색인을 복원할 수 있다.

4. Elasticsearch 설계

상품 데이터는 Elasticsearch의 products 인덱스에 저장된다. 한국어 검색 품질을 위해 Nori 분석기를 적용하였다.

products-settings.json:

{
  "analysis": {
    "tokenizer": {
      "nori_tokenizer_custom": {
        "type": "nori_tokenizer",
        "decompound_mode": "mixed"
      }
    },
    "analyzer": {
      "nori_korean": {
        "type": "custom",
        "tokenizer": "nori_tokenizer_custom",
        "filter": ["lowercase"]
      }
    }
  }
}

 

이 설정은 복합명사 분해(mixed)와 소문자 정규화를 수행한다. 예를 들어 “무선마우스”는 “무선”, “마우스”로 분석되어 검색 일치율이 높아진다.


5. 서비스 간 데이터 흐름

다음은 전체 데이터 흐름이다.

[1] 사용자 → API 서버
POST /products/register
{
  "productId": 1001,
  "name": "무선마우스",
  "description": "2.4GHz 블루투스 마우스",
  "category": "전자기기",
  "price": 24900
}

[2] API 서버
→ KafkaEventProducer.send("product-register", ProductRegisterRequest)

[3] Kafka Topic (product-register)

[4] Query 서버
@KafkaListener(topics="product-register")
→ ProductService.upsert(request)
→ Elasticsearch.save(ProductEntity)

[5] 사용자 검색
GET /products?q=마우스
→ Elasticsearch "products" 인덱스 검색

 

Comments