[Kafka] 테스트 작성법: EmbeddedKafka vs Testcontainers, Awaitility 비동기 검증
![[Kafka] 테스트 작성법: EmbeddedKafka vs Testcontainers, Awaitility 비동기 검증](https://blog.kakaocdn.net/dna/pMHND/dJMcacpRsB3/AAAAAAAAAAAAAAAAAAAAADrtGrhdk1Rq_d79VIhsdxqbgvYdn8VB49x1X8oC0ayr/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1785509999&allow_ip=&allow_referer=&signature=fmysGyrnul3dAxzvkIYbXoxnpeY%3D)
[Kafka] 테스트 작성법: EmbeddedKafka vs Testcontainers, Awaitility 비동기 검증
카프카를 쓰는 코드를 테스트하려면 카프카가 필요해요. 그런데 단위 테스트마다 실제 카프카 클러스터를 띄울 순 없죠. 그래서 테스트용 카프카를 어떻게 마련하느냐가 문제가 돼요. 크게 두 가지 방법이 있어요. 인메모리 브로커를 띄우는 EmbeddedKafka와, 진짜 카프카를 도커로 띄우는 Testcontainers예요.
카프카 테스트가 왜 까다로운지에서 출발해, EmbeddedKafka 사용법, Testcontainers 사용법, 프로듀서·컨슈머 테스트 작성, 비동기 대기 처리, 둘의 비교, 자주 만나는 문제까지 짚어요. 컨슈머 동작은 @KafkaListener 컨슈머 설정 글을 먼저 보면 좋아요.
01. 카프카 테스트가 까다로운 이유
카프카 코드 테스트가 어려운 건 두 가지 때문이에요. 하나는 외부 의존이에요. 카프카 브로커가 있어야 send·consume이 돌아가요. 다른 하나는 비동기예요. 메시지를 보내도 컨슈머가 그걸 처리하기까지 시간이 걸려서 테스트가 "언제 검증해야 하는지"를 알기 어려워요.

그래서 카프카 테스트는 층을 나눠 접근해요. 처리 로직만 보는 단위 테스트, 연동까지 보는 통합 테스트(EmbeddedKafka), 실제 환경까지 보는 테스트(Testcontainers)예요. 무엇을 검증하느냐에 맞춰 층을 골라요.
02. EmbeddedKafka, 인메모리 브로커
EmbeddedKafka는 테스트 JVM 안에 카프카 브로커를 띄워요. spring-kafka-test 의존성을 추가하고 @EmbeddedKafka를 붙이면 돼요.
@SpringBootTest
@EmbeddedKafka(partitions = 1, topics = {"orders"})
class OrderConsumerTest {
@Autowired
EmbeddedKafkaBroker broker;
// ...
}
그러면 테스트가 시작될 때 인메모리 브로커가 뜨고 끝나면 사라져요. 도커도 외부 프로세스도 필요 없어서 가볍고 빨라요. CI에서 도커를 못 쓰는 환경에서도 돌아가요.
브로커 주소는 자동으로 주입돼요. spring-kafka-test 3.0.10부터 @EmbeddedKafka가 브로커 주소를 spring.kafka.bootstrap-servers에 바로 넣어주거든요. 예전 글에 자주 나오는 System.getProperty("spring.embedded.kafka.brokers") 방식은 이 변경 뒤로는 null이 와요. 주소가 직접 필요하면 EmbeddedKafkaBroker를 주입받아 getBrokersAsString()으로 꺼내요.
03. Testcontainers, 진짜 카프카 도커
Testcontainers는 테스트 동안 실제 카프카를 도커 컨테이너로 띄워요. 진짜 카프카 이미지를 쓰니 운영 환경과 가장 비슷해요. 참고로 최신 Testcontainers에서는 기존 KafkaContainer가 deprecated로 바뀌어서 권장 API인 ConfluentKafkaContainer로 예를 들게요.
@SpringBootTest
@Testcontainers
class OrderConsumerIT {
@Container
static ConfluentKafkaContainer kafka =
new ConfluentKafkaContainer("confluentinc/cp-kafka:7.8.0");
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
}
}
컨테이너가 뜨고 내려가는 시간이 있어서 EmbeddedKafka보다 느려요. 대신 실제 카프카의 동작(버전별 특성, 설정)을 그대로 검증할 수 있어요. 도커가 돌아가는 환경이 필요하다는 전제가 붙어요.
04. 프로듀서 테스트
프로듀서 테스트는 "보낸 메시지가 토픽에 잘 들어갔는가"를 봐요. KafkaTestUtils로 테스트용 컨슈머를 만들어 직접 받아 확인해요.
var consumer = new DefaultKafkaConsumerFactory<String, String>(
KafkaTestUtils.consumerProps("test-group", "true", broker))
.createConsumer();
consumer.subscribe(List.of("orders"));
orderProducer.send(new Order("o-1")); // 테스트 대상 실행
var records = KafkaTestUtils.getRecords(consumer, Duration.ofSeconds(5));
assertThat(records.count()).isEqualTo(1);
핵심은 받을 때 타임아웃을 주는 거예요. 메시지가 바로 안 올 수 있으니, 일정 시간 기다렸다가 검증해요.
05. 컨슈머 테스트와 비동기 대기
컨슈머 테스트는 반대예요. "메시지를 보냈을 때 컨슈머가 처리했는가"를 봐요. 그런데 처리가 비동기라, 보내자마자 검증하면 아직 처리 전이라 실패해요. 여기서 비동기 대기가 필요해요.

Thread.sleep으로 무작정 기다리면 느리고 불안정해요. 대신 Awaitility로 "조건이 만족될 때까지" 기다려요.
orderProducer.send(new Order("o-1"));
await().atMost(Duration.ofSeconds(5))
.untilAsserted(() -> assertThat(orderRepository.findById("o-1")).isPresent());
이러면 컨슈머가 처리해서 DB에 저장될 때까지 기다렸다 검증해요. 처리가 빨리 끝나면 빨리 통과하고 늦으면 최대 5초까지 기다려요. sleep보다 빠르고 안정적이에요.
06. 브로커 없이 단위 테스트하기
통합 테스트만 있는 건 아니에요. 비즈니스 로직만 빠르게 검증하고 싶으면 브로커 없이 단위 테스트를 짤 수 있어요.
프로듀서 쪽은 카프카가 주는 MockProducer를 써요. 실제 전송 없이 "무엇을 보내려 했는지"를 메모리에 기록하니, 보낸 메시지를 바로 검증할 수 있어요.
var mockProducer = new MockProducer<>(true, new StringSerializer(), new StringSerializer());
var service = new OrderService(mockProducer);
service.placeOrder("o-1");
assertThat(mockProducer.history()).hasSize(1);
assertThat(mockProducer.history().get(0).key()).isEqualTo("o-1");
컨슈머 쪽 로직은 보통 리스너 메서드를 그냥 직접 호출해서 테스트해요. @KafkaListener 메서드도 결국 평범한 메서드라, 메시지 객체를 만들어 직접 넣고 결과를 검증하면 돼요. 카프카 연동 자체가 아니라 "받은 뒤의 처리 로직"을 볼 때 이게 가장 빨라요.
07. EmbeddedKafka vs Testcontainers
둘 중 무엇을 쓸지는 "속도냐 실제성이냐"로 갈려요.

- EmbeddedKafka — 빠르고 가벼워요. 도커가 필요 없어요. 대신 실제 카프카와 미묘한 차이가 있을 수 있어요. 빠른 피드백이 필요한 다수의 통합 테스트에 좋아요.
- Testcontainers — 실제 카프카라 가장 정확해요. 대신 느리고 도커가 필요해요. 운영과 똑같은 동작을 검증해야 하는 핵심 테스트에 좋아요.
실무에서는 섞어 써요. 대부분의 통합 테스트는 EmbeddedKafka로 빠르게 돌리고, 버전 의존적이거나 정말 중요한 흐름만 Testcontainers로 검증하는 식이에요. EmbeddedKafka는 주소가 자동 주입되고 Testcontainers는 @DynamicPropertySource 한 덩이만 더하면 테스트 코드가 거의 같아서 나중에 바꾸기도 어렵지 않아요.
08. 자주 만나는 문제
테스트가 가끔 실패해요 (flaky)
대개 비동기 타이밍이에요. 보내자마자 검증하면 처리 전이라 실패해요. sleep 대신 Awaitility로 조건 기반 대기를 써요.
컨슈머가 메시지를 못 받아요
컨슈머가 구독을 시작하기 전에 메시지가 발행돼 놓친 경우가 많아요.
이게 카프카 테스트의 대표 함정이에요. 테스트는 "발행 → 소비"를 순서대로 써요. 하지만 컨슈머가 파티션을 할당받는 데 시간이 걸려서 실제로는 "발행 → (컨슈머 구독 완료) → 소비"가 안 될 수 있어요.auto.offset.reset이 기본값latest면 구독 전에 발행된 메시지는 안 보여요. 테스트 설정에서earliest로 두거나,ContainerTestUtils.waitForAssignment로 파티션 할당을 기다린 뒤 발행해요. (참고로 04절처럼 KafkaTestUtils.consumerProps로 만든 컨슈머는 spring-kafka 2.5부터 earliest가 기본이라 이 함정은 비켜가요.)
테스트끼리 메시지가 섞여요
같은 토픽·그룹을 여러 테스트가 공유한 거예요. 테스트마다 토픽 이름이나 그룹 ID를 다르게 주면 격리돼요.
정리
카프카 테스트는 테스트용 브로커를 띄우고 비동기 처리를 기다렸다 검증하는 통합 테스트가 기본이에요. 빠른 피드백은 EmbeddedKafka, 실제 환경 검증은 Testcontainers로 나눠 쓰고, 비동기는 sleep 대신 Awaitility로 조건 기반 대기를 걸어요. 이러면 카프카 코드도 안정적으로 테스트할 수 있어요.
테스트를 짜기 전에 한 번 물어요. 지금 검증하려는 게 받은 뒤의 처리 로직인가, 카프카 연동까지인가, 아니면 운영과 똑같은 동작인가? 그 답이 단위·EmbeddedKafka·Testcontainers 중 어느 층을 고를지 정해줘요. @KafkaListener 설정, 프로듀서 acks 같은 앞 글의 코드를 이 방법으로 검증하면 돼요.
출처: Spring for Apache Kafka — Testing Applications · Testcontainers — Kafka Module · Awaitility
'백엔드 > 메시징' 카테고리의 다른 글
| [Kafka] 성능 트러블슈팅 가이드: 컨슈머 lag, 리밸런싱, 프로듀서 타임아웃 증상별 원인과 대응 (0) | 2026.07.02 |
|---|---|
| [Kafka] 직렬화와 스키마 관리: JSON vs Avro, Schema Registry 호환성 검사 (0) | 2026.06.17 |
| [Kafka] 컨슈머 lag 모니터링: 측정 방법, 늘어나는 원인 진단, 알림 설계 (0) | 2026.06.15 |
| [Kafka] 파티셔닝과 메시지 키: 순서 보장, hot partition, 파티션 수 정하는 기준 (0) | 2026.06.12 |
| [Kafka] 프로듀서 acks=all, min.insync.replicas, 멱등 프로듀서: 메시지 유실 막고 순서 보장하기 (0) | 2026.06.11 |
📚 같이 보면 좋은
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."