컬리 검색/추천팀이 서로 다른 토픽의 메시지를 같은 키로 조합해 하나의 색인 메시지로 만들기 위해 카프카 스트림즈를 도입한 경험을 다룬다. 레디스를 중간 저장소로 쓰던 초기 방식을 스트림즈의 KTable/KStream join으로 대체하고, 조용히 죽는 스트림즈 스레드를 스프링 카프카로 관리·모니터링한 과정을 정리한다.
핵심 포인트- 두 토픽 메시지를 레디스에 적재 후 병합하던 방식을 카프카 스트림즈 KTable-KStream join으로 단순화
- 순수 자바로 StreamsBuilder를 직접 만들면 스트림즈 스레드가 죽어도 스프링 앱은 정상으로 표시되는 사각지대 발생
- 스프링 카프카의 @EnableKafkaStreams로 StreamsBuilder를 @Autowired 주입해 생명주기를 스프링에 위임
- KafkaStreams.state()를 읽는 HealthIndicator로 스트림즈 상태를 실시간 헬스체크
- Error 상태는 재시작 외 복구가 불가라 Down으로 판정해 조기 감지
상세 정리- 요구사항: 1편의 레디스 기반 조합(각 토픽 메시지를 정제해 레디스에 적재하고 한 상품의 데이터가 다 모이면 읽어 병합·색인)을 더 단순한 방식으로 바꾸고 싶었다.
- 스트림즈 선택: 카프카에 저장된 데이터를 처리하는 클라이언트 라이브러리로 진입장벽이 낮은 카프카 스트림즈를 발견했다.
- 기본 파이프라인: sourceTopicA를 KTable로, sourceTopicB를 KStream으로 읽어 같은 키로 join한 뒤 null을 필터링해 MSG-MERGED 토픽으로 내보내는 토폴로지를 순수 자바로 구현했다.
- 1차 스프링 연동: ApplicationStartedEvent 리스너에서 buildPipeline을 호출해 KafkaStreams를 직접 생성하고 start 하는 방식으로 붙였다.
- 장애: 테스트 환경의 규격 어긋난 메시지가 처리 오류를 냈고, All stream threads have died 로그를 남기며 스트림즈 스레드가 죽었다.
- 사각지대: 스레드가 죽어도 스프링 어플리케이션 상태는 정상으로 표시돼, 별도 헬스체크 없이는 장애를 인지할 수 없었다.
- 가시성 확보: 스프링 카프카가 카프카 스트림즈를 지원한다는 점을 활용해 @EnableKafkaStreams 설정과 KafkaStreamsConfiguration 빈을 구성했다.
- 구현 전환: KafkaStreams를 직접 만들지 않고 StreamsBuilder를 @Autowired로 주입해 스프링이 스트림즈 클라이언트 생성과 생명주기를 관리하게 바꿨고, num.stream.threads는 3으로 설정했다.
- 헬스체크: Created·Running·Re-Balancing은 up, Pending Shutdown·Not Running·Error는 down으로 보고, StreamsBuilderFactoryBean에서 KafkaStreams를 꺼내 state()를 확인하는 HealthIndicator를 구현했다.
- 결론: 스트림즈로 메시지 병합을 쉽게 처리하고, 관리와 상태 모니터링은 스프링에 일임할 수 있게 됐다.
왜 읽나스프링 환경에서 카프카 스트림즈를 운영하며 스레드 사망 가시성 문제로 고생한 백엔드 개발자에게 @EnableKafkaStreams와 HealthIndicator 패턴 레퍼런스.