본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
강의 요약
오늘 강의에서는 Spring for Kafka의 기본 개념과 구성 요소를 다루었다. Spring Kafka는 Apache Kafka와 Spring Framework를 통합하여 메시지 기반 애플리케이션을 구축할 수 있도록 지원한다. KafkaTemplate을 통한 메시지 발행, @KafkaListener를 사용한 메시지 소비, 그리고 이를 Spring Boot 환경에서 설정하는 방법을 학습했다. 그러면 테스트를 위해서는 어떻게해야할까? 알아보도록 하자
Spring Kafka 통합 테스트 전략
Kafka를 사용하는 애플리케이션을 개발할 때 가장 큰 과제 중 하나는 신뢰할 수 있는 테스트 환경을 구축하는 것이다. 외부 Kafka 브로커에 의존하면 테스트의 독립성과 재현성이 떨어질 수 있다. Spring Kafka Test는 이러한 문제를 해결하기 위해 두 가지 주요 접근 방식을 제공한다.
EmbeddedKafka를 활용한 테스트
의존성 설정
Spring Kafka Test 모듈은 테스트 환경에서 인메모리 Kafka 브로커를 실행할 수 있는 기능을 제공한다.
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<version>3.1.1</version>
<scope>test</scope>
</dependency>
테스트 구성
@EmbeddedKafka 어노테이션을 사용하면 테스트 실행 시 자동으로 Kafka 브로커가 시작된다.
@SpringBootTest
@DirtiesContext
@EmbeddedKafka(partitions = 1, brokerProperties = {
"listeners=PLAINTEXT://localhost:9092",
"port=9092"
})
class EmbeddedKafkaIntegrationTest {
@Autowired
private KafkaConsumer consumer;
@Autowired
private KafkaProducer producer;
@Value("${test.topic}")
private String topic;
@Test
public void givenEmbeddedKafkaBroker_whenSendingMessage_thenMessageReceived()
throws Exception {
String data = "Test message";
producer.send(topic, data);
boolean messageConsumed = consumer.getLatch().await(10, TimeUnit.SECONDS);
assertTrue(messageConsumed);
assertThat(consumer.getPayload(), containsString(data));
}
}
- @DirtiesContext 어노테이션은 각 테스트 간 컨텍스트를 격리하여 테스트 독립성을 보장한다.
- partitions 속성은 토픽당 파티션 수를 지정하며, 테스트 환경에서는 단순성을 위해 1로 설정하는 것이 일반적이다.
컨슈머 설정의 핵심
테스트에서 중요한 설정은 auto-offset-reset: earliest이다. 이 설정은 컨슈머가 메시지를 읽기 시작하는 오프셋 위치를 지정한다. 테스트 환경에서는 컨테이너가 메시지 발송 이후에 시작될 수 있으므로, earliest로 설정하여 토픽의 처음부터 메시지를 읽도록 보장해야 한다.
Testcontainers를 활용한 테스트
Testcontainers의 필요성
EmbeddedKafka는 빠르고 가벼운 장점이 있지만, 실제 Kafka 브로커와 미묘한 차이가 있을 수 있다. 또한 포트 충돌 가능성도 존재한다. Testcontainers는 Docker 컨테이너로 실제 Kafka 브로커를 실행하여 이러한 한계를 극복한다.
의존성 추가
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
테스트 구성
@RunWith(SpringRunner.class)
@SpringBootTest
@DirtiesContext
public class KafkaTestContainersTest {
@ClassRule
public static KafkaContainer kafka =
new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:5.4.3"));
@Test
public void givenKafkaDockerContainer_whenSendingMessage_thenMessageReceived()
throws Exception {
String data = "Test message";
producer.send(topic, data);
boolean messageConsumed = consumer.getLatch().await(10, TimeUnit.SECONDS);
assertTrue(messageConsumed);
}
}
동적 포트 바인딩 처리
Testcontainers는 포트 충돌을 방지하기 위해 동적으로 포트를 할당한다. 따라서 브로커 주소를 설정 파일에 하드코딩할 수 없다.
@Bean
public Map<String, Object> consumerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "baeldung");
return props;
}
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());
return new DefaultKafkaProducerFactory<>(configProps);
}
- kafka.getBootstrapServers() 메서드는 컨테이너가 시작된 후 동적으로 할당된 포트 정보를 포함한 브로커 주소를 반환한다.
테스트 전략 선택 시 고려사항
EmbeddedKafka 선택이 적합한 경우
- 빠른 테스트 실행 속도가 중요한 경우
- Docker 환경이 제약되거나 사용할 수 없는 경우
- 간단한 메시지 발행/소비 로직을 검증하는 경우
- CI/CD 파이프라인에서 실행 시간을 최소화해야 하는 경우
Testcontainers 선택이 적합한 경우
- 실제 프로덕션 환경과 최대한 유사한 테스트가 필요한 경우
- Kafka의 특정 버전이나 설정을 정확히 재현해야 하는 경우
- 복잡한 Kafka 설정이나 커스텀 플러그인을 테스트하는 경우
- 다른 시스템과의 통합 테스트가 필요한 경우
두 방식 모두 외부 Kafka 브로커 의존성을 제거하여 테스트의 독립성과 재현성을 확보한다는 공통 목표를 가진다. 프로젝트의 요구사항, 테스트 복잡도, 실행 환경의 제약사항을 종합적으로 고려하여 적절한 방식을 선택하는 것이 중요하다.
참고 출처



