brunch

You can make anything
by writing

C.S.Lewis

by 티맵모빌리티 May 09. 2024

TMAP 대중교통이 티맵 안으로 들어온 이유

26편 – Reactive Programming으로 재탄생한 대중교통

지난해 9월 출시된 티맵 10.0 버전부터 대중교통 기능이 새롭게 탑재됐습니다. 

티맵은 이미 'TMAP 대중교통'이라는 별도의 앱을 갖고 있었는데요. 왜 TMAP 대중교통이 티맵 안으로 들어가게 되었는지 백엔드 엔지니어 입장에서 설명드리고자 합니다.




티맵 대중교통 HISTORY


기존의 'TMAP 대중교통'은 수백 대의 On-premises 서버들로 구성된, 자원을 갉아먹는 초거대 아키텍처의 잔여물이었습니다.

출처: https://www.slideshare.net/balladofgale/spring-cloud-workshop


‘티맵 택시’가 존재하던 시절, 티맵 대중교통은 티맵 택시와 Netflix OSS 환경으로 구성된 Eco-System으로 아키텍처가 구성되어 있었는데요. 예전엔 함께였던 티맵 택시가 (우버로) 떠나고, Eco-System을 대중교통 혼자 사용하려다 보니 수많은 자원이 필요했습니다. 
 

그렇게 티맵 대중교통은 운영모드로 들어가게 되었고 기술부채가 쌓이는 시간이 점차 늘어났죠. 또, 기존 대중교통 시스템들은 다양한 언어를 채택했습니다. 개발자의 자율성을 뒀던 거 같은데요. java, kotlin, scala, python 등 기존 개발자가 조직을 이동하면서 운영자는 유지보수를 위주로 하게 되어 엄청난 기술 부채가 되었죠.


‘마틴 파울러’는 이런 말을 남겼습니다. 


“일을 성급하고 지저분하게 처리하면 기술 부채가 쌓이는데, 이는 금융 부채와 비슷하다. 금융 부채처럼 이자를 지불해야 하고, 결국은 나중에 개발해야만 하는 추가적인 업무의 형태로 남는다”


티맵 대중교통을 재구성하고, AWS 환경으로 재구축하고, Spring Cloud 진영으로 전환하는 임무를 맡은 저에게는 이 기술부채들이 거대한 벽이었습니다. 그렇지만! 그 어려운 걸 자꾸 해냅니다.” 

결국 더 나은 서비스로 나아가기 위해 대부분의 서비스가 구성 완료되었고, 잔여 작업을 진행 중입니다.


서비스 전환의 방법은?


AWS에 신규로 구축되는 대중교통 서비스는 최적의 비용으로 최고의 성능을 내는 것에 초점을 맞췄습니다. 또한 다른 개발자가 투입되더라도 러닝 커브가 적은 언어를 채택했습니다. (기존 대중교통은 Scala에 대한 경험이 없어도 했어야 하는 상황이었습니다. 기술 자율성이 주는 부채, 언어는 굉장히 좋음!)


전환 방식은 처음부터 모든 기능을 배포하지 않고(not BigBang~) 점진적으로 이동하는 방식으로 진행했습니다. 클라이언트에게 닿는 API 담당 서비스를 먼저 이전했고 consumer 서비스, DB, Kafka 그리고 원천 데이터를 받는 지자체 수신 BIS를 마지막으로 전환하게 됩니다.


대중교통 재구축



대중교통은 서울, 경기, 부산 등 각 지자체의 BIS(Bus Information System)와 SIS(Subway Information System)에서 실시간으로 가져오는 데이터를 내부적인 필터와 보정을 통해 더욱더 정확한 도착정보로 만들어 사용자에게 제공하고 있습니다.


지자체 BIS는 약 50개의 지자체로부터 5~10초마다 버스 위치정보, 버스 도착정보, 버스 소요시간 정보 등을 받아오는데요. 해당 데이터를 가공해서 Kafka Topic에 적재하고 있습니다. 각 지자체마다의 Topic이 분리되어 있으며, 약 50개의 지자체 데이터가 약 50개*N개의 토픽으로 구성되고, kafka record(message)는 수신 시점의 1차 가공된 데이터가 전부 적재되어 있습니다.


예를 들어, 서울 BIS의 오전 07시 00분의 서울 전체 지역의 도착정보가 Topic 내 하나의 record로 protobuf 타입을 가지고 serialization(직렬화)해서 적재됩니다. 즉, 대한민국 전역의 버스, 지하철 정보가 지역마다 5~10초마다 생성되고, 생성된 정보는 약 1~2초 내로 고객에게 전달되고 있습니다.


대중교통 Data's Life Cycle


대중교통 Data의 Life Cycle은 이렇습니다. 각 지자체에서 가져온 실시간 data를 Kafka Cluster에 적재하고, 적재된 record는 대중교통 Consumer 서비스에서 소비를 하게 됩니다. Consumer에서 모든 연산과 작업이 이루어지고 redis에 적재되어 re-write 되거나, 시간이 지나면 자연스럽게 expire 됩니다. 


 또한, 중점적으로 고객에게 사용되는 API에서는 최대한 시간 복잡도가 적고, 단순 조회만 할 수 있는 기능을 만들기 위해 노력했습니다. API를 경량화해서 더 많은 요청을 받기 위해선 연산작업 등을 진행하는 Consumer에서 하는 역할이 중요한데요. 


 대중교통 Consumer Service는 Asynchronous/Non-Blocking 한 reactive-programming을 기반으로 개발을 진행했고, I/O 작업이 많은 걸 고려해 최대한 blocking, idle이 없는 서비스를 만들기 위해 노력했습니다. 바로 지금부터 그 방법들을 소개하고자 합니다. 


대중교통  Consumer 서비스의 자원 효율과 방법

1. Asynchronous/Non-Blocking의 활용
2. reactive를 지원하는 redis를 채택
3. coroutine의 선택과 scope 내의 async/non-Blocking의 조화
4. kafka의 대용량 처리 


1. Asynchronous/Non-Blocking의 활용

Asynchronous(비동기) 프로그래밍은 I/O 작업이나 긴 처리 과정을 주 스레드가 아닌 별도의 스레드에서 실행하고, 결과가 준비되면 Callback을 받는 방식입니다.

Non-Blocking은 시스템 호출(I/O 작업 등)이 즉시 결과를 Callback 하고, 작업 완료를 기다리지 않는 방식입니다


2. Reactive를 지원하는 Redis의 활용

Reactive Redis는 데이터 스트림을 비동기적으로 처리하고, 이벤트 기반의 Non-Blocking I/O 모델을 활용합니다. 이는 Reactive Programming 패러다임을 따르며, 데이터베이스 연산을 논블로킹으로 수행합니다. Reactive Redis를 사용함으로써 데이터베이스 작업 시 응답성이 향상되고, 시스템의 전체 처리량이 증가합니다. 이는 특히 데이터베이스 요청이 빈번한 고성능 애플리케이션에 유리합니다. 
reactive-redis의 Reactive Streams을 활용해 subscribe() 인터페이스를 사용하였으며, onSubscribe, onNext 처리, onError or onComplete 통해 non-blocking 처리를 진행합니다.


3. coroutine의 선택과 scope내의 async/non-Blocking의 조화

Coroutine은 경량 스레드로, Kotlin에서 제공하는 비동기 프로그래밍 모델입니다. 
async 함수를 사용하여 비동기적으로 작업을 수행하고, 결과를 나중에 await을 통해 받을 수 있습니다. Coroutine을 활용하면 여러 작업을 동시에 비동기적으로 수행할 수 있어, I/O 또는 CPU 바운드 작업에서 애플리케이션의 전반적인 성능을 향상시킬 수 있습니다. 그리고 더욱 간결한 소스로 처리가 가능합니다.

BIS, SIS 데이터는 실시간 데이터이기 때문에, 과거 데이터보다 최근에 수신된 데이터가 가장 중요합니다. Kafka Lag이 발생하게 되면 결국 도착정보의 신뢰성이 떨어지게 됩니다. 대중교통 consumer에서는 CoroutineScope(Dispatchers.IO)를 활용하였습니다. 
비동기, 논블로킹 방식으로 record를 다 소비하지 않더라도 다음 record를 pulling 해오는 방식을 선택했습니다. 물론, trade-off는 발생합니다. 한 번에 많은 record를 수신하고 처리하게 되면 cpu throttling에 취약해집니다.(producer service에서 생성하는 message에 대한 약속이 있기 때문에, message comsuming cost가 커서 지연되더라도 lag을 발생시키지 않겠다는 의지


Dispatchers.IO를 다량 발생할 경우 코루틴내 background thread pool에서 사용될 스레드가 생성되게 됩니다. 스레드 생성 비용과 콘텍스트 스위칭 비용, 스레드 간 경합이 발생해 cpu throttling 이슈를 직면하게 됩니다. 적절하게 적용 구성하는 게 적절한 사용입니다. 

 CASE

cpu, 메모리는 충분하지만 스레드가 모자라 처리가 불가능

스레드가 많이 사용하면 context-swiching overhead(콘텍스트 스위칭)으로 인해 cpu 부하 증가
→ 항상 적절하게 효율적인 사용이 중요.

아래는 대중교통 api 내에서 coroutine을 이용한 nonBlocking, async 활용입니다.


4. Kafka의 대용량 처리

Kafka는 고성능, 분산형 스트리밍 플랫폼으로, 대규모 데이터 파이프라인을 효율적으로 처리할 수 있습니다. Kafka는 높은 처리량과 데이터 내구성, 확장성을 제공합니다.
Kafka를 사용하면 대용량 데이터를 신속하게 처리하고, 실시간 데이터 스트리밍이나 이벤트 기반 아키텍처에서 높은 처리량과 낮은 지연 시간을 달성할 수 있습니다. 대중교통 consumer group 내 발생한 Total Lag는 0~2 정도로 지극히 효율적인 consuming 처리를 보여줍니다. 즉, 버스 및 지하철의 도착, 위치 정보의 serving이 지연되지 않습니다.



정리

대중교통 consumer는 Asynchronous/Non-Blocking 한 reactive-programing을 기반으로 개발을 진행했습니다. 


백엔드 개발자들은 개발을 하다 보면 DB의 Block과 비즈니스라는 이유 때문에 보상 트랜잭션을 이용하곤 하는데요. 그중 consumer에서 reactive처리가 가능했던 이유는 Blocking에 문제가 되지 않는 Blocking DB와의 connection이 없었다는 게 큽니다. 


Kafka → consumer → redis의 프로세스로, reacitve-programing의 활용과 coroutine을 활용하면서 Scope내의 async를 활용해 논블로킹, 비동기처리를 활용할 수 있었습니다. 


만약 적용을 고민하고 계실 경우, 구성하려는 서비스 중 Blocking 되는 구간이 생기면 위에서 적용한 방식에 대한 도입은 미루는 게 좋습니다.


마치며

고가용 서버를 구성하기 위한 수단이 너무 다양합니다. 많은 데이터를 처리할 수 있어서 좋은 경험이었고 재미있었습니다.


티맵은 차량용 내비게이션뿐만 아니라 대중교통, 렌터카, 바이크, 공항버스 등 다양한 모빌리티 서비스를 제공하고 있습니다. 차량이 없더라도, 운전을 하지 않더라도 티맵을 이용해 갈 수 있는 곳이 아주 많습니다. 

티맵을 사용해 목적지까지 도달하는 경험을 꼭 하시길 바라겠습니다.


그리고 중요한 공지! 종료된 TMAP 대중교통 앱의 기능 중, ‘지하철 노선도’와 ‘하차 알림’ 기능은 빠른 시일 내에 반영될 예정입니다. 조금만 기다려주시면 더 좋은 기능들로 찾아올게요. 앞으로도 더욱더 완성도 높은 기능과 서비스로 티맵의 대중교통 길안내 및 도착정보를 신속 정확하게 전달하겠습니다!


현재 대중교통 이용자는 n00 만 명이지만, 티맵 네비처럼 2000만 명이 사용하는 사랑받는 서비스가 되길 바라며! 감사합니다.



브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari