들어가면서
경매 시스템의 입찰 로직에 Kafka 기반 비동기 이벤트 전송 기능을 도입하게 되었다.
입찰 등록 시 Kafka로 이벤트를 발행하고, 이를 Consumer가 받아서 후속 처리하는 구조로 변경하면서,
이벤트 기반 아키텍처의 이점을 실제로 적용하고 검증해보고자 했다.
이후 통합 테스트를 작성하면서 Kafka까지 포함된 end-to-end 테스트를 수행하려고 했고,
이를 위해 spring-kafka-test 라이브러리의 @EmbeddedKafka를 사용하여 내장 브로커 기반 테스트 환경을 구성했다.
Embedded Kafka vs Testcontainers
`Embedded Kafka`는 JVM 안에 Kafka 브로커를 띄우는 방식이라 속도가 빠르고 설정이 간단하지만, 실제 Kafka와 완벽히 동일하지 않다는 단점이 있다. 반면, `Testcontainers`는 Docker 기반의 실제 Kafka 인스턴스를 띄우기 때문에 프로덕션 환경과 거의 동일한 테스트가 가능하지만, 속도가 느리고 Docker 의존성이 있다는 trade-off가 있다.
입찰 도메인의 통합 테스트는 Kafka를 설정하여 통과했는데, 문제가 발생했다.
동일한 API 모듈의 다른 도메인 테스트 코드들이 전부 터지기 시작한 것이다. 원인은 간단했다.
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'kafkaProducerConfig'
...
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'spring.embedded.kafka.brokers' in value "${spring.embedded.kafka.brokers}"
...
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'biddingEventProducer'
...
KafkaProducerConfig, KafkaConsumerConfig가 언제나 빈으로 등록되던 상태라,
Kafka에 대한 의존이 없는 테스트들도 Kafka 연결을 시도하면서 오류가 났던 것.
이에 대한 해결책으로 `@ConditionalOnProperty`를 도입하여,
`event.kafka.enabled: true`일 때만 Kafka 관련 빈이 등록되도록 수정했다.
@ConditionalOnProperty란?
@Configuration
@ConditionalOnProperty(name = "event.kafka.enabled", havingValue = "true", matchIfMissing = false)
public class KafkaProducerConfig {
...
스프링부트에서 제공하는 이 어노테이션은 설정 파일에 명시된 프로퍼티 값에 따라 특정 Bean을 등록할지 말지를 결정할 수 있게 해준다.
여기서 `event.kafka.enabled`라는 설정이 "true"일 때만 해당 설정이 적용되고,
`matchIfMissing = false` 덕분에 이 프로퍼티가 아예 없으면 등록되지 않도록 만들 수 있다.
즉, Kafka가 필요한 상황에서만 관련 설정이 활성화되도록 제어한 것이다.
이후 EmbeddedKafka 통합 테스트 환경에서는 `@ConditionalOnProperty` 조건이 무시되는 예상치 못한 이슈까지 겪게 되었다.
Spring Boot의 테스트 컨텍스트와 조건부 빈 등록 사이의 관계를 이해를 할 수 있었고, 실제 운영환경과 테스트 환경을 분리하고 안전하게 구성하는 설계에 대해 고민하게 되는 계기가 되었던 개발일기이다.
EmbeddedKafka 통합 테스트 환경에서, 왜 @ConditionalOnProperty가 무시되었을까?
@SpringJUnitConfig // Embedded Broker가 Test Application Context에 추가됨. -> Broker를 Autowire 해줄 수 있음.
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public abstract class ApiWithKafkaTestSupport extends ApiTestSupport {
private static String BROKER_ADDRESS;
@Autowired
private EmbeddedKafkaBroker broker;
@DynamicPropertySource
static void setKafkaProps(DynamicPropertyRegistry registry) {
registry.add("spring.kafka.producer.bootstrap-servers", () -> BROKER_ADDRESS);
registry.add("spring.kafka.consumer.bootstrap-servers", () -> BROKER_ADDRESS);
}
@BeforeAll
void setupBroker(@Autowired EmbeddedKafkaBroker broker) {
BROKER_ADDRESS = broker.getBrokersAsString();
}
}
@DisplayName("[BiddingApiController 테스트]")
@EmbeddedKafka(partitions = 1, topics = {"bidding-events"})
class BiddingApiControllerTest extends ApiWithKafkaTestSupport {
...
# test의 application.yml
spring:
kafka:
producer:
bootstrap-servers: ${spring.embedded.kafka.brokers}
consumer:
bootstrap-servers: ${spring.embedded.kafka.brokers}
event:
kafka:
enabled: false
...
@Configuration
@ConditionalOnProperty(name = "event.kafka.enabled", havingValue = "true", matchIfMissing = false)
public class KafkaProducerConfig {
...
분명 false로 설정했으니 빈이 등록되지 않아야 한다.
그런데 입찰 통합 테스트에서는 카프카가 잘 동작하고 있다!
원인 분석: 테스트에서 조건이 무시되는 이유
1. @EmbeddedKafka가 테스트 환경에서 Kafka Broker를 띄움
@EmbeddedKafka(partitions = 1, topics = {"bidding-events"})
- spring-kafka-test에서 제공하는 애노테이션
- 테스트 컨텍스트에 `Kafka 브로커를 자동 등록`해줌
- 이와 함께 EmbeddedKafkaBroker 빈이 생성됨
2. @DynamicPropertySource로 Kafka 서버 주소를 덮어씀
@DynamicPropertySource
static void setKafkaProps(DynamicPropertyRegistry registry) {
registry.add("spring.kafka.producer.bootstrap-servers", () -> BROKER_ADDRESS);
registry.add("spring.kafka.consumer.bootstrap-servers", () -> BROKER_ADDRESS);
}
- application.yml보다 `우선순위 높은 테스트 프로퍼티를 설정`
- 그래서 실제 프로퍼티 설정값은 덮어써짐
3. @SpyBean, @Import, @SpringJUnitConfig의 조합
// BiddingApiControllerTest
@SpyBean
private BiddingEventListener biddingEventListener;
- @SpyBean은 테스트 시 자동으로 Bean을 등록하거나 감싸줌
- 이로 인해, `해당 Bean이 의존하는 Kafka 관련 빈들도 자동 주입됨`
- 이 과정에서 @ConditionalOnProperty의 조건이 무시될 수 있음
마무리
1. 실제 운영 환경에서는 `event.kafka.enabled: true`가 반드시 필요하다. 테스트는 편의상 조건을 무시할 수 있지만, 운영에서는 조건부 등록에 의존하는 Kafka 설정값이 적용된다.
2. 테스트 환경에서는 실제 Kafka를 구동하지 말고 EmbeddedKafka를 사용해서 테스트 시간을 아껴주자. 이번 포스팅에서 다뤘던 포인트를 주의하면서.
'Etc' 카테고리의 다른 글
[Git] 원격 저장소와 로컬 저장소 병합 문제 해결 : non-fast-forward (0) | 2025.01.09 |
---|---|
ChatGPT로 간단한 웹사이트 만들기 - 3 (0) | 2023.04.25 |
ChatGPT로 간단한 웹사이트 만들기 - 2 (0) | 2023.04.25 |
ChatGPT로 간단한 웹사이트 만들기 - 1 (0) | 2023.04.22 |
IT 개인 블로그 추천 (백엔드 / 알고리즘) (1) | 2022.07.26 |