Skip to main content

Spring Boot & Kafka


Intro

1. 왜 그냥 API 호출로는 안 될까?

대규모 이메일 발송이나 주문 시스템을 개발하다 보면 필연적으로 대용량 트래픽 문제에 직면합니다.
가령, 10,000명의 고객에게 동시에 이메일을 보내야 한다고 가정해 보자. 가장 단순한 방법은 Spring Boot에서 for 문을 돌며 외부 이메일 서버에 HTTP 요청을 보내는 것입니다.

// 나쁜 예: 동기식 직접 호출
for (User user : users) {
emailClient.send(user.getEmail()); // 여기서 외부 서버가 느려지면 내 서버도 같이 멈춤!
}

하지만 이 방식은 치명적인 단점이 있습니다.

  • 결합도: 외부 이메일 서버가 죽으면, 우리 서버의 로직도 에러를 내뿜으며 실패합니다.
  • 속도 차이: 우리 서버는 1초에 1000개를 보낼 수 있어도 받는 쪽이 1초에 10개만 처리 가능하다면 받는 쪽 서버가 폭주하여 다운됩니다.

이 문제를 해결하기 위해 등장한 도구가 바로 Apache Kafka입니다.


2. Apache Kafka란?

아파치 카프카(Apache Kafka)는 분산형 스트리밍 플랫폼으로, 대량의 데이터를 안정적이고 실시간으로 처리할 수 있도록 설계되었습니다. 카프카는 주로 대량의 이벤트 스트림 데이터를 처리하고 여러 시스템 간에 데이터를 신속하게 전송하는 데 사용됩니다.

2.1. 핵심 용어 정리

Kafka를 다루기 위해서는 다음 4가지 용어를 알아야 합니다.

  • Producer (생산자): 데이터를 보내는 쪽입니다. 예제에서는 이메일 발송 요청을 하는 API 서버를 의미합니다.
  • Consumer (소비자): 데이터를 가져가서 처리하는 쪽입니다. 이메일을 실제로 발송하는 서비스를 의미합니다.
  • Topic (토픽): 데이터가 저장되는 주제 또는 폴더입니다. 예시는 email-send-job입니다.
  • Offset (오프셋): 책갈피입니다. Consumer가 어디까지 읽었는지 표시하는 숫자입니다.

핵심 원리는 다음과 같습니다. Producer는 Consumer가 바쁘든 말든 상관하지 않고 Topic에 데이터를 밀어 넣습니다. Consumer는 자기 속도에 맞춰서 Topic에서 데이터를 꺼내갑니다. 이를 통해 두 시스템의 속도 차이를 완충해줍니다.


3.1. 인프라 구축 (Docker Compose)

Docker를 통해 로컬환경에 Kafka를 설치해보았습니다.

# docker-compose.yml
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.4.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181

kafka:
image: confluentinc/cp-kafka:7.4.0
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://kafka:29092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT_INTERNAL
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

터미널에서 docker-compose up -d를 입력하면 Kafka가 실행됩니다.

3.2. 의존성 추가 (build.gradle)

spring-kafka 라이브러리를 추가합니다.

dependencies {
implementation 'org.springframework.kafka:spring-kafka'
}

3.3. 설정 (application.properties)

Kafka 접속 정보와 Consumer 그룹 정보를 설정합니다.

# Kafka 서버 주소
spring.kafka.bootstrap-servers=localhost:9092

# 데이터를 직렬화/역직렬화할 방식 (String 사용)
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer

spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer

# Consumer 그룹 ID
spring.kafka.consumer.group-id=email-consumer-group

# 처음 실행할 때 데이터를 어디서부터 읽을 것인가?
spring.kafka.consumer.auto-offset-reset=earliest

3.4. Producer 구현 (보내는 쪽)

KafkaTemplate을 주입받아 사용하면 매우 간단합니다.

@Service
@RequiredArgsConstructor
public class EmailProducer {

private final KafkaTemplate<String, String> kafkaTemplate;

public void sendEmail(String message) {
// "daily-email-job"이라는 토픽에 메시지 전송
kafkaTemplate.send("daily-email-job", message);
System.out.println("kafka로 메시지 전송 완료: " + message);
}
}

3.5. Consumer 구현 (받는 쪽)

@KafkaListener 어노테이션을 사용합니다.

@Service
public class EmailConsumer {

@KafkaListener(topics = "daily-email-job", groupId = "email-consumer-group")
public void consume(String message) {
System.out.println("Kafka에서 메시지 수신: " + message);

// 실제 이메일 발송 로직 수행 (여기서 외부 API 호출)
sendToExternalApi(message);
}
}


4. 트러블 슈팅 : 메시지를 안 읽어오는 현상

프로젝트를 진행하며 겪은 문제는 Consumer가 데이터를 읽어오지 않는 현상이었습니다.

  • 원인: auto.offset.reset 설정의 기본값이 latest이기 때문입니다.
  • 상황: Producer가 메시지를 1만 개 보내고 난 직후에 Consumer 서버를 켰다면, latest 설정 때문에 Consumer는 내가 오기 전 일은 모른다며 아무것도 읽지 않습니다.
  • 해결: 설정을 earliest로 변경하거나 Consumer를 먼저 켜두는 방식으로 해결했습니다.