본문 바로가기
개발 일기장/개발 일지

서버 복잡도를 높이지 않으면서 messaging client 바꾸기

by 룰루루 2026. 3. 4.

지난 약 2주동안 한 일을 요약하자면 위의 문장일 것 같다. 물론 그 외에도 자잘자잘한 일들이 있었던 것으로 기억하지만, 일단은 그렇다. 
 
messaging client를 바꿔야 하는 서버는 크게 2개였다.
 
두 서버의 메시징 클라이언트를 바꾸면서 가장 헷갈리고 또 많이 보았던 부분은 "해당 서버에서 몇 개의 스레드가 각각 어떤 로직을 수행하는지"였다. 특히 한 서버에서는 max_worker 환경변수(워커 스레드의 개수)의 값을 N이라고 하면, N+3개의 스레드가 실행되고 있어서 이런 부분을 따져볼 수밖에 없었다. 
 
그리고 "메시징 클라이언트를 바꾼다는 것"이 꼭 "기존에 잘 동작하던 paho-mqtt 기반의 코드를 다 지우고 nats로 교체하는 것"을 의미하는 것은 아니었다. 물론 nats로 메시지 브로커를 바꾸기로 결정하였지만, 이러한 작업은 reversible 해야 했고, 또 확장성을 고려해야 한다고 생각했다.
 
그래서 기존 코드를 일부 제거하는 방식이 아니라, 기존 코드는 그대로 두고, 새 프로토콜과 코드를 작성했다. 목적은 코드 한 줄로 paho-mqtt를 nats로 바꿀 수 있도록 하는 거였다. 쉽게 갈아끼울 수 있도록 말이다.
 

✅ 동기 방식의 동작

그러기 위해서는 paho-mqtt가 동기 방식인 것과 달리 nats가 비동기 방식인 것도 고려해야 했다. 공통의 protocol을 쓰려면 메소드의 동기/비동기 방식은 결국 둘 중 하나로 통일할 수밖에 없었고, nats에 맞춰서 모든 protocol의 메소드를 비동기로 바꾸게 된다면 해당 메소드를 호출하는 다른 콜백 로직들도 전부 다 비동기로 바꿔줄 수밖에 없었다. 번거로운 것도 있지만, 그보다는 코드 전체가 비동기로 바뀌면서 어떻게 동작할지 예상하기 어렵다는 문제가 있었다.
 
그래서 nats의 비동기 기반 로직을 사용하는 대신, nats Client를 동기로 구현했다.
 
asyncio의 run_coroutine_threadsafe() 등의 메소드를 접하면서 '그럼 coroutine이 thread safe하지 않게 실행될 수도 있나?' 등의 의문도 갖게 되었다. 결국 이런 것들은 멀티 스레드 환경일 가능성을 염두에 둔 asyncio의 기능이었다. 하나의 스레드가 이미 해당 코루틴에 접근한 상태에서는, thread safe한 환경을 보장해야 다른 스레드가 갑자기 끼어들어서 해당 스레드가 실행 중인 영역을 interrupt 또는 오염시키는 걸 막을 수 있다. 
 
Future 객체에 대해서도 정확히는 모르지만 이런 녀석이 있구나... 라는 걸 알았다. 사실 존재를 처음 알았다. 비동기 함수를 실행할 때, 결과가 완전히 반환되지 않았어도 Future 객체를 리턴한다고 이해했다. 그리고 .result() 메소드로 결과값을 반환 받거나, 아니면 .add_done_callback() 메소드로 해당 비동기 로직이 완료되고 나서 실행될 콜백을 정의하는 것도 가능했다.
 
공식문서를 읽으면서 이 녀석이 좀 복잡하다 싶었는데, Future 객체를 일반 어플리케이션 로직에서는 보통 잘 컨트롤할 일이 없다고 한다. 라이브러리/프레임워크 등의 개발자는 필요 시 Future로 더 세밀하게 비동기 로직을 제어할 수 있다고 한다. 
 

✅ Protocol - 최소한의 공통 규약을 유연하게 사용하기

그리고 Protocol에 대해서도, (사실 지금도 완전히 이해하지 못했지만) 처음에는 특히 더 이 녀석을 상위 클래스 상속과 헷갈렸던 것 같다. 멘토 팀원분이 protocol 공식문서도 알려주셨고 분명 나도 다 읽어보긴 했지만... 나중에 개발과 코드 리뷰에서 우여곡절을 겪으면서, Protocol은 상속과 달리 유연한 typing(이걸 duck typing이라고 하나보다)을 지원하기 위한 도구임을 조금 실감했다. 
 
즉 paho-mqtt handler, nats handler가 있을 때, protocol은 이 두 handler가 공통적으로 사용하는 최소한의 로직을 묶어서 선언하는 역할을 할 수는 있다. 사실 엄연히 따지면 순서가 반대긴 하다. Protocol을 정의하고 개별 handler를 정의하는 게 맞긴 하다...!
 
그러나 protocol에 맞추기 위해서 각 handler에서 억지로 필요 없는 메소드를 구현하게 되는 순간(내가 그랬다), protocol을 잘 활용하고 있지 못한 것이다.
 
처음엔 MqttServerProtocol 이라는 프로토콜 클래스에 connect, disconnect, subscribe, publish 등 최대한 많은 메소드를 정의해서 두 handler의 공통점을 넓히려고 애썼다. 그러나 두 라이브러리(paho-mqtt, nats)는 동작하는 방식이 달랐고, 기본 메소드도 달라서 묶기가 어려워졌다. 오히려 세부적인 protocol이 발목을 잡았다. 그래서 필요 없는 메소드는 일부 쳐내고, subscribe, publish 등의 기본 메소드만 유지했다. 
 

✅ 디버깅과 테스트 시도

디버깅과 테스트의 중요성도 많이 느낀다. 특히 docker container를 띄우면서 계속 failure가 발생할 때, 언제까지고 하나씩 고쳐서 docker container를 계속 띄워볼 수는 없는 노릇이었다. 이를 위해서 vscode에서 DevContainer라는 꽤 유용해 보이는 확장 프로그램을 찾았다. 디버깅 모드로 컨테이너를 실행해서, 컨테이너가 죽을 때 어떤 로직에서 어떤 값 때문에 종료되었는지를 추측하는 데 꽤 유용했다. 
 
그 외에도 pytest로 이 데이터 서버에서 어떤 로직이 다른 로직보다 먼저 실행되는지, 다른 함수가 더 먼저 초기화되는지 등을 테스트 해보려는 시도도 있었다. '시도'라고 말한 이유는 테스트를 작성하면서 배보다 배꼽이 더 커져서, 기능 구현하는 것보다 테스트를 만드는 데 시간을 더 쓴 감이 있기 때문이다. 그래도 시도는 좋았다. 
 
pytest 및 테스트 라이브러리에 대한 이해가 더 필요할 것 같다. 이런 로직을 점검하는 것은 늘 좋은 일이니, 백로그로 두더라도 틈틈이 잡고 여러 서버에 테스트들을 붙여 두면 분명 유용하게 쓰일 일이 있을 것이라고 생각한다. 
 
아 그리고, 이런 리팩토링을 하면서 결국 "복잡도를 높이지 않고" 또는 "유지보수를 용이하게 하면서" 메시징 클라이언트를 바꾸는 게 핵심이었는데, 정작 이를 객관적인 수치로는 측정하지 못한 것 같아서 아쉬움이 남는다. 정적 분석을 통해서는 대략적인 수치는 얻을 수 있겠으나, 결국 정성적으로 재려면 로직의 효율성, 디자인 패턴, 설계 원칙 등 여러 지식과 방법론들이 필요해 보인다. 어려운 일이고, 당장 다 마무리하지는 못했으나 이걸 알고 있자. 그리고 기회가 된다면 이번 프로젝트이건 다른 프로젝트이건, 더 수치화할 수 있는 지표를 얻어보자. 

'개발 일기장 > 개발 일지' 카테고리의 다른 글

20260320 with claude  (0) 2026.03.21
20260319 with claude  (0) 2026.03.19
python messaging client 바꾸기  (0) 2026.02.18
쿠버네티스에서 nats cluster 띄우기  (0) 2026.02.13
메시지 브로커 latency 측정  (0) 2026.01.31