목차
Kafka와 Redis 사이를 논블로킹으로 잇는 대중교통 백엔드 (2024-05)
원문 정보
- 원문: TMAP 대중교통이 티맵 안으로 들어온 이유
- 부제: 26편 – Reactive Programming으로 재탄생한 대중교통
- 매체 · 게시일: 티맵모빌리티 테크노트(브런치) · 2024-05-09
- 저자: 티맵모빌리티 (본문에 저자 미표기, 재구축을 맡은 백엔드 엔지니어가 1인칭으로 서술)
- 원문 내용을 바탕으로 재구성한 요약입니다.
별도 앱에서 티맵 안으로
티맵 10.0은 지난해 9월 출시되며 대중교통 기능을 앱 안에 넣었습니다. 그 전에는 TMAP 대중교통이라는 별도 앱이 있었습니다.
옛 구조는 수백 대의 온프레미스 서버로 이뤄져 있었습니다. 티맵 택시가 있던 시절에는 택시와 함께 Netflix OSS 기반 생태계를 나눠 썼는데, 택시가 떠난 뒤에는 대중교통 혼자 그 자원을 감당했습니다. 서비스는 운영 모드에 들어가 기술 부채가 쌓였고, 언어도 Java, Kotlin, Scala, Python으로 제각각이었습니다. 원문은 개발자가 조직을 옮긴 뒤 운영자가 유지보수만 하게 됐다고 설명합니다.
작성자는 이를 AWS 환경과 Spring Cloud 진영으로 다시 짓는 일을 맡았습니다. 새 서비스는 최적의 비용으로 최고의 성능을 내는 데 초점을 맞췄고, 다른 개발자가 들어와도 배우기 쉬운 언어를 골랐습니다. 대부분의 서비스는 구성을 마쳤고 잔여 작업이 남아 있다고 합니다.
전환은 한꺼번에 하지 않고 점진적으로 했습니다. 클라이언트와 맞닿은 API 서비스를 먼저 옮기고, Consumer 서비스, DB, Kafka를 거쳐 지자체 데이터를 받는 BIS 수신 부분을 마지막에 옮깁니다.
지자체 데이터의 수집 주기와 Kafka 적재
서울, 경기, 부산 같은 지자체의 버스정보시스템(BIS)과 지하철정보시스템(SIS)에서 실시간 데이터를 받아 필터와 보정을 거쳐 도착정보로 만듭니다. 약 50개 지자체에서 5~10초마다 버스 위치, 도착, 소요시간 정보를 가져와 가공한 뒤 Kafka 토픽에 쌓습니다. 토픽은 지자체마다 나뉘어 있고, 레코드에는 수신 시점에 1차 가공한 데이터가 전부 들어갑니다.
예를 들어 서울 BIS의 오전 7시 00분 전 지역 도착정보가 레코드 하나가 되고, protobuf로 직렬화해 적재합니다. 원문은 이 흐름을 이렇게 적었습니다.
대한민국 전역의 버스, 지하철 정보가 지역마다 5~10초마다 생성되고, 생성된 정보는 약 1~2초 내로 고객에게 전달되고 있습니다.
Consumer가 연산하고 API는 조회만
적재된 레코드는 대중교통 Consumer 서비스가 소비합니다. 모든 연산은 Consumer에서 하고 결과를 Redis에 쓰며, 값은 다시 쓰이거나 시간이 지나 만료됩니다. 고객이 쓰는 API는 시간 복잡도가 낮은 단순 조회만 하도록 가볍게 만들어 더 많은 요청을 받습니다. 그래서 Consumer의 역할이 중요하다고 합니다.
비동기 논블로킹 Consumer
I/O가 많다는 점을 고려해 blocking과 idle을 최대한 없앴고, 방법은 네 가지입니다.
- 비동기·논블로킹: 긴 I/O를 주 스레드 밖에서 실행하고 결과는 콜백으로 받으며, 시스템 호출은 완료를 기다리지 않습니다.
- Reactive Redis: Reactive Streams의 subscribe로 onSubscribe, onNext, onError, onComplete 단계를 거쳐 논블로킹으로 처리합니다.
- Kotlin 코루틴: CoroutineScope(Dispatchers.IO) 안에서 async와 await를 씁니다.
- Kafka의 대용량 처리: Consumer 그룹의 전체 Lag가 0~2 수준이어서 버스·지하철 도착과 위치 정보 제공이 지연되지 않는다고 밝혔습니다.
실시간 데이터는 최근에 받은 것이 가장 중요해서 Lag이 생기면 도착정보 신뢰도가 떨어집니다. 그래서 레코드를 다 처리하지 않아도 다음 레코드를 가져오도록 했습니다. 대신 한꺼번에 많이 받으면 CPU throttling에 약해지는 트레이드오프가 있습니다. Dispatchers.IO를 과하게 쓰면 스레드 생성, 컨텍스트 스위칭, 스레드 간 경합 비용이 늘어납니다. CPU와 메모리는 충분한데 스레드가 모자라 처리를 못 하거나, 스레드가 많아 CPU 부하가 오르는 경우를 사례로 들었습니다.
적용 전의 조건
원문은 Consumer에서 reactive 처리가 가능했던 이유로, 블로킹이 문제가 되는 DB 연결이 없었다는 점을 꼽습니다. 흐름은 Kafka에서 Consumer, Redis로 이어집니다. 구성하려는 서비스에 블로킹 구간이 생긴다면 이 방식의 도입은 미루는 게 좋다고 권합니다.
끝에서는 종료된 앱의 지하철 노선도와 하차 알림 기능을 빠른 시일 내에 반영하겠다고 예고했습니다. 하차 알림은 승하차 알림 글에서 이어집니다.
원문의 주요 수치
| 항목 | 값 |
|---|---|
| 지자체 BIS 연동 | 약 50개 지자체 |
| 수신 주기 | 5~10초 |
| 고객 전달까지 | 약 1~2초 |
| 레코드 직렬화 | protobuf |
| Consumer 그룹 Total Lag | 0~2 |
| 언어와 런타임 | Kotlin 코루틴, Reactive Redis |