왜 우리는 파이썬에서 Node.js로 마이그레이션했나

ko생성일: 2025. 11. 6.갱신일: 2025. 11. 6.

출시 일주일 만에 백엔드를 파이썬에서 Node.js로 전환한 이유, 그 과정에서 얻고 잃은 것들, 실제 마이그레이션 방법과 배운 점을 공유합니다.

2025년 11월 3일

이미지 1: Python에서 Node로의 마이그레이션

우리는 방금 미친 일을 하나 해냈습니다. 런칭 일주일 만에 백엔드를 파이썬에서 Node로 완전히 갈아엎었거든요.

우리가 이렇게 한 이유는 스케일링을 위해서입니다. 맞아요, 스케일링. 런칭 일주일 만에요.

어떤 면에서는 좋은 타이밍이긴 하죠? 코드베이스는 아직 작고 사용자도 많지 않으니까요.

하지만 한편으로는 초기 스타트업에게 주어지는 조언—“일단 만들고 팔아라, 스케일은 PMF를 찾은 다음에 고민해라.”—에 완전히 거스르는 일이기도 합니다. PG가 말했듯이, “스케일되지 않는 일을 하라(Do things that don't scale)”.

사실 우리는 마법 같은 런칭 주간을 보내서 유저가 폭발하고 어쩔 수 없이 스케일해야 했던 건 아닙니다. 그리고 일반적으로는 어떤 스택을 고르더라도 꽤 오랫동안 합리적인 수준까지는 스케일할 수 있고, 진짜로 프레임워크를 바꾸거나(혹은 백엔드를 다른 언어—예: Rust—로 다시 쓰는) 걸 고민해야 할 시점이 오기 전까지는 버틸 수 있다고 기대하죠.

그런데 왜 굳이 했느냐고요?

파이썬 async는 정말 빡셉니다

저는 Django의 큰 팬입니다. PostHog에서 처음 접했고 그 뒤로 대부분의 프로젝트에서 기본으로 쓰는 백엔드가 되었죠. 빠르게 시작할 수 있고, 도구와 추상화가 훌륭하며, 필요에 맞게 유연하게 손볼 수 있습니다.

그래서 자연스럽게, Skald의 백엔드를 만들기 시작할 때도 Django로 출발했습니다.

Skald에서는 LLM 및 임베딩 API 호출을 굉장히 많이 합니다. 즉, 비동기로 처리하고 싶은 네트워크 I/O가 아주 많습니다. 게다가 문서를 여러 청크로 나눠 각각에 대한 벡터 임베딩을 생성할 때처럼, 동시에 여러 요청을 쏴야 하는 경우도 잦습니다.

그리고 Django에서 상황은 금세 지저분해졌습니다.

미리 말씀드리면, 우리 둘 다 파이썬 async 코드를 많이 써본 경험은 없습니다(저는 주로 Node에서 비동기-heavy 서비스를 해왔죠). 하지만 그게 오히려 포인트라고 생각했습니다. 즉, 견고하고 성능 좋은 파이썬 async 코드를 쓰는 일은 정말 어렵고 직관적이지 않다는 겁니다. 그걸 하려면 모든 것의 바닥까지 파고들어야 하죠.

솔직히 저도 파이썬 async를 제대로 공부하는 데 시간을 들이고 싶습니다. 하지만 우리 컨텍스트에서는 a) 초기 스타트업으로서 출시를 위해 써야 할 소중한 시간을 잃게 되고, b) 그 과정에서 쉽게 스스로를 총으로 쏘게 됩니다.

그럼에도 저는 제 잘못이라고 생각했습니다. “나쁜 프로그래머! 나쁜 프로그래머!”라는 소리가 머릿속에서 울렸죠. 물론 더 잘 아는 사람이면 더 수월했을 테지만, 파이썬 async의 토대 자체도 사실 좀 흔들린다는 걸 알게 됐습니다.

자바스크립트는 시작부터 이벤트 루프(event loop)를 갖고 있었고, Go는 고루틴(goroutines)이라는 개념을 만들었습니다(둘 다 제가 꽤 좋아하고 실서비스에서 써본 동시성 모델입니다). 반면 파이썬의 async 지원은 나중에 덧붙여졌고, 거기에 어려움의 핵심이 있습니다.

이걸 정말 잘 다루는 두 블로그 글이 있습니다. 바로 “Python has had async for 10 years -- why isn't it more popular?”“Python concurrency: gevent had it right”인데, 제가 이걸 파기 시작하기 얼마 전에 편리하게도 올라왔더군요.

우리 케이스에서 배운 점을 요약하면 이렇습니다.

  • 파이썬에는 네이티브 async 파일 I/O가 없습니다.
  • Django는 아직 완전한 async 지원이 아닙니다. ORM의 async가 아직 완성되지 않았고, 이 지점에서 ‘컬러 함수(colored functions) 문제’가 제대로 드러납니다. 기술적으로 Django를 async와 함께 쓸 수는 있지만, 그들의 문서에는 경고가 너무 많아서 누구든 겁먹게 만들 겁니다.
  • sync_to_asyncasync_to_sync를 정말 모든 곳에 써야 합니다.
  • 파이썬 생태계의 서로 다른 부분에서 더 나은 async 지원을 제공하려는 온갖 모델이 나왔지만, 네이티브가 아니다 보니 각자 제약이 있습니다. 예를 들어 aiofiles는 파일 연산을 async API로 제공하지만 내부적으로 스레드 풀을 씁니다. Gevent의 그린릿(greenlets)도 꽤 멋지지만 동작을 위해 표준 라이브러리(stdlib)를 문자 그대로 패치합니다.
  • 많은 파이썬 async 지원이 언어의 네이티브가 아니라 위에 얹힌 레이어에 의존하기 때문에, 여러분이 쓰는 async 코드가 예컨대 어떤 Gunicorn 워커 타입을 쓰느냐에 따라 다른 의미를 갖게 됩니다(그나저나 Gunicorn 문서에서 이걸 깊게 배우기는 정말 힘들 겁니다).

전체적으로, Promise.all에 해당하는 걸 제대로 굴리면서 그 모든 함정을 이해하는 일조차 결코 쉽지 않았습니다.

이 상황에서 저는 PostHog 코드베이스를 들여다봤습니다.

저는 PostHog에서 3년을 일했는데, 그때 우리의 Django 코드베이스에는 async가 전혀 없었습니다. 하지만 이제는 거대한 회사가 되었고 AI 기능도 있으니, 분명 이걸 잘 풀었겠지요!

그리고 깨달은 건, 그들이 여전히 WSGI(ASGI가 아님) 위에 Gunicorn Gthread 워커를 돌리고 있다는 겁니다(일반적으로 처리 가능한 최대 동시 요청 수는 CPU 코어 수의 최대 약 4배). 그 결과 async를 도입한다고 해서 큰 이득을 보는 구조가 아닙니다. 코드베이스에는 async_to_sync 자체 구현 같은, async가 제대로 동작하도록 돕는 유틸도 잔뜩 있었습니다. 아마 이들이 큰 부하를 처리하는 주된 방식은 수평 스케일링일 겁니다.

결론적으로, Django에서 async를 제대로 돌리는 ‘아주 좋은’ 방법은 사실상 없습니다.

자, 이제 무엇?

우리는 Django가 곧장 우리를 아프게 할 거라고 결론 내렸습니다. 많은 트래픽을 받기 시작했을 때만의 문제가 아니라는 뜻입니다.

사용자가 많지 않은 지금도, 형편없는 지연 시간을 피하려면 이미 여러 대의 머신을 띄워야 하고, 유지보수가 어려운 투박한 코드를 쓰게 될 상황이었습니다.

물론 지금은 “스케일되지 않는 일”을 하며 돈(혹은 AWS 크레딧)으로 해결할 수도 있습니다. 하지만 느낌이 좋지 않았습니다. 그리고 이렇게 초기에 옮기면 다른 프레임워크로의 마이그레이션이 훨씬 쉬워지기도 합니다.

아마 여기까지 읽은 분들 중에는 화면에 대고 “그냥 FastAPI 쓰면 되잖아!”라고 외치는 분들이 있을 겁니다. 실제로 우리도 고려했습니다.

FastAPI는 제대로 된 async 지원을 갖춘, 성능이 좋다고 평가받는 사랑받는 프레임워크입니다. ORM이 필요하다면 async를 지원하는 SQLAlchemy도 쓸 수 있죠.

FastAPI로 옮겼다면 번역 없이 재사용할 수 있는 코드가 많아서 하루이틀(우리의 실제 마이그레이션은 3일 걸렸습니다) 정도는 아꼈을 겁니다. 하지만 이 시점에는 파이썬 async 생태계 전반에 대한 감이 썩 좋지 않았고, 이미 백그라운드 워커 서비스는 Node로 써둔 상태였기 때문에, 한 생태계에 올인할 좋은 기회라고 판단했습니다.

그래서 우리는 Node로 옮겼습니다. 프레임워크와 ORM 조합을 고르는 데 약간 시간을 썼고, 최종적으로 Express + MikroORM으로 정했습니다.

맞아요, Express가 오래되긴 했죠. 하지만 실전에서 수없이 검증됐고 친숙합니다. 어차피 이 모든 것의 핵심은 JS의 이벤트 루프로 넘어오는 데 있었으니까요.

얻은 것과 잃은 것

얻은 것: 효율

초기 벤치마크로는, 박스에서 꺼내 바로 쓴 상태로도 처리량이 약 3배 늘었습니다. 그것도 대부분의 코드를 비동기 컨텍스트에서 거의 순차적으로만 돌렸을 때 얘기입니다. 이제 Node로 넘어온 만큼, 청킹, 임베딩, 리랭킹 등에서 동시 처리를 대폭 늘릴 계획입니다. 시간이 지날수록 이 변화의 보상은 더 커질 겁니다.

잃은 것: Django

Django를 잃은 건 아픕니다. 이미 Express 쪽에서 우리가 직접 미들웨어와 유틸리티를 훨씬 더 많이 만들고 있다는 걸 느끼고 있어요. 더 완성도 높은 Node 프레임워크인 Adonis도 있지만, 우리에게는 생태계를 통째로 바꾸는 것보다 미니멀한 것을 쓰는 편이 일이 덜해 보였습니다.

가장 그리운 건 ORM입니다. 제 생각에 Django ORM은 정말 인체공학적입니다. 최고 성능을 뽑아내려면 어떤 ORM이든 조심해야 하지만, Django ORM은 파이썬으로 쿼리를 작성하더라도 성능이 충분히 나오도록 내부에서 꽤 괜찮은 일을 해줍니다. Django 모델을 MikroORM 엔티티로 옮기면서 이 부분에 대해 더 배우기도 했습니다.

얻은 것: MikroORM

MikroORM은 이번 마이그레이션에서 위안상 같은 존재였습니다. 여전히 Django ORM이 훨씬 마음에 들지만, 생태계가 다르면 도구도 달라야 하죠.

저는 이전에 써본 적이 없었는데, Django와 비슷한 지연 로딩(lazy loading), Prisma보다 훨씬 낫게 느껴지는 마이그레이션 구성, 그리고(기초를 수동으로 잘 깔아두면) 꽤 인체공학적인 API를 발견하고 긍정적으로 놀랐습니다.

전체적으로는 아직 초기 단계이긴 하지만, 현 시점에서는 기존 강자인 Prisma 대신 MikroORM을 선택한 게 만족스럽습니다.

잃은 것: 파이썬 생태계

이건 설명이 필요 없겠죠. RAG나 에이전트를 만들기 위한 대부분의 도구가 파이썬과 타입스크립트 SDK를 제공하지만, 여전히 파이썬이 우선순위를 가집니다. 그리고 여기서 말하는 건 API 래퍼 수준 이야기일 뿐이죠.

직접 ML을 만져보려 하면 비교가 안 됩니다. 우리도 더 고도화되면 결국 파이썬 서비스가 하나 생길 거라고 봅니다. 하지만 지금은 괜찮습니다.

얻은 것: 통합 코드베이스

Node로 옮기면 파이썬 서비스 하나와 Node 서비스 하나를 갖는 대신 Node 서비스 두 개를 돌리게 된다는 건 늘 알고 있었습니다. 그런데 옮긴 지 하루쯤 지나서야 두 코드베이스를 합칠 수 있다는 걸 깨달았고, 그게 엄청 도움이 됐습니다.

이전에는 Node 워커와 Django 서버 사이에 중복 로직이 많았습니다. 지금은 Express 서버와 백그라운드 워커를 하나의 코드베이스로 통합했는데, 훨씬 좋아졌습니다. 둘 다 이제 ORM을 쓸 수 있고(이전엔 워커가 생 SQL을 돌렸습니다) 여러 유틸을 공유합니다.

얻은 것: 훨씬 나은 테스트

이건 pytest vs jest 이야기가 아닙니다. 마이그레이션 후에도 모든 게 기대대로 동작하는지 확인하려고 테스트를 엄청 많이 썼다는 얘기죠. 이 과정과 약간의 리팩토링이 반가운 부수 효과였습니다.

어떻게 했나

이제 슬슬 글을 마무리해야 할 것 같은데, 실제 마이그레이션 과정에 대한 짧은 메모를 남깁니다.

  • 총 3일이 걸렸습니다.
  • 마지막 자잘한 부분까지 오기 전에는 AI 코드 생성을 거의 쓰지 않았습니다. 새로운 세팅의 토대, 특히 새로운 ORM의 내부 동작을 아주 잘 이해하는 게 중요하다고 느꼈거든요. 모든 기반을 다진 뒤에는 Claude Code가 중요도가 낮은 엔드포인트 코드 생성에 꽤 도움이 됐고, 코드베이스 이슈 스캔에도 도움을 줬습니다.
  • 중간에 몇 번이나 포기할 뻔했습니다. 고객이 새 기능을 요청하고 Django 코드에 버그도 있었기 때문에, 마이그레이션을 하느라 고객을 못 챙기고 시간을 낭비하는 기분이 들었거든요.

다시 하겠느냐고요?

솔직히, 우리는 이번 결정에 꽤 만족했고 100% 다시 할 겁니다. 장기적으로 보상이 올 뿐 아니라, 이미 오늘 당장 보상이 오고 있으니까요.

그 과정에서 배운 것도 많습니다. 그리고 이 글의 요점이 누군가 찾아와 “너네 멍청하고, 그냥 X나 Y를 했어야지”라고 말하든, 파이썬 async가 어떻게 돌아가는지 가르쳐주든, 그게 뭐든 정말 환영입니다. 저로서는 파이썬 async에 경험이 부족함을 기꺼이 인정하고, 더 배울 수 있다면 그게 바로 이득이니까요.

코드를 실제로 보고 싶다면 아래 PR을 확인해 보세요.

Skald는 MIT 라이선스의 RAG API 플랫폼입니다. 의견이나 걱정거리가 있다면 GitHub에서 마음껏 소리를 질러주세요. 아니면 아예 당신이 선호하는 프레임워크로 백엔드를 다시 쓰는 PR을 올려도 좋습니다 :D