미안하지만, AI가 코드를 짜도 생각은 직접 해야 합니다

김팔복 2026-10-12 23:25:27
조회 52 추천 0 댓글 0

출처: It's all about the bit (Piotr Sarnacki)
원문: https://itsallaboutthebit.com/i-am-sorry-but-you-still-have-to-think/
작성자: 팔복소프트-김팔복

한눈에 보기

  • DHH가 AI 에이전트로 Campfire Once를 Rust, Elixir, Go로 재작성했고, Piotr Sarnacki가 그 결과물을 뜯어본 글입니다.
  • 언어 간 성능 비교는 사실상 의미가 없습니다. 하위 호환성, Redis 사용 여부 같은 설계 결정이 버전마다 달랐기 때문입니다.
  • 프롬프트에 적지 않은 요구사항(지연 시간과 처리량 중 무엇이 중요한지, 메모리 한도 등)은 AI가 알아서 정해버립니다.
  • Rust 버전의 알림 전달률 1%는 "Rust가 동시성에 약하다"가 아니라 용량이 정해진 채널 설계에서 나온 결과였습니다.
  • 채널 용량만 키우면 전달률은 오르지만 최악 지연이 130초를 넘어, 오히려 사용자 경험이 나빠집니다.
  • 결론은 단순합니다. AI가 코드를 써도 실패 모드와 트레이드오프는 사람이 정해야 합니다.

배경

Campfire Once는 Basecamp가 공개한 Ruby on Rails 기반 채팅 앱입니다. 실시간 채팅이라 새 메시지가 올라오면 접속한 여러 클라이언트에 WebSocket으로 이벤트를 뿌려야 하고, 이 부분이 부하 테스트에서 가장 먼저 무너지는 지점입니다. DHH는 Rails를 만든 사람이지만 Rust 코드는 직접 읽고 쓰기 싫어해서, 재작성을 AI 에이전트에게 맡겼습니다. 이 글은 그 결과를 두고 벌어진 언어 우열 논쟁에 대한 반론입니다.

같은 앱이지만 같은 설계가 아니다

비교의 출발점부터 어긋나 있었습니다. Rust 버전은 캐싱을 쉽게 하려고 CSRF 토큰을 빼버렸고, 알림 같은 기능에 쓰던 Redis를 프로세스 내부 큐로 바꿨습니다. 반면 Elixir 버전은 Rails 원본에 훨씬 가깝게 남았습니다. 이런 차이는 언어와 무관하게 AI가 그때그때 내린 선택입니다. "Elixir로 바꿔줘"라고 했다고 해서 하위 호환을 지키기로 한 게 아니라는 뜻입니다.

코드 품질 문제도 양쪽에 다 있었습니다. Elixir 버전은 동시에 실행해도 되는 읽기 쿼리까지 하나의 프로세스가 순서대로 처리했습니다. Rust 버전은 일부 DB 작업과 락을 비동기 런타임 안에서 블로킹 방식으로 호출했습니다. Rust의 async 런타임은 선점형이 아니라 협력형 스케줄링이라, 태스크가 양보(yield)하지 않으면 같은 워커 스레드의 다른 태스크가 전부 멈춥니다. 그래서 저자는 tokio::sync::Mutex 같은 async 락을 기본으로 쓰고, 동기 락은 아주 짧게 잡을 때만 쓰라고 권합니다.

1% 전달률의 진짜 원인

Zach Daniels가 부하 상황에서 새 글 알림 전달률을 재보니 Rust 버전은 1%였습니다. 그런데 이 테스트는 클라이언트 N개가 응답을 받자마자 다음 요청을 보내는 closed-loop 방식이었습니다. 이 방식에서는 빠른 시스템일수록 요청을 더 많이 받습니다. 실제로 Rust는 6~7천 건 중 1%, Elixir는 약 1.7천 건 중 100%를 전달했으니 애초에 같은 양을 비교한 게 아닙니다.

저자는 요청 속도를 고정하는 constant arrival rate 방식으로 초당 100건 POST를 다시 돌렸습니다.

초당 100건 고정 부하에서 Rust 두 설정과 Elixir의 전달률, HTTP 오류, 최악 지연 비교

그림 1. 초당 100건 POST 고정 부하에서의 결과 비교 (원문 수치 기반, 팔복소프트 작성)

Rust 버전은 tokio::sync::broadcast 채널로 이벤트를 뿌리는데, 용량이 256으로 정해져 있어 따라오지 못하는 수신자는 RecvError::Lagged를 받고 연결이 끊깁니다. 브라우저는 재접속하면서 최신 메시지를 다시 가져오니, 놓친 이벤트는 큰 문제가 안 됩니다. Elixir는 GenServer 메일박스에 기본 한도가 없어 이벤트를 쌓아두다가, 메모리가 1.8GB까지 올라가고 HTTP POST의 약 23%가 타임아웃됐습니다. 용량을 16384로 늘린 Rust는 전달률이 약 90%까지 오르지만 최악 지연도 11초에서 130초 이상으로 뛰었습니다. 저자는 이쪽이 오히려 더 나쁘다고 봅니다. 3분 가까이 늦게 도착하는 알림보다, 끊고 다시 붙어서 새로 받아오는 편이 낫다는 판단입니다.

팔복소프트 관점

  • "AI로 다른 언어로 옮기기"는 국내에서도 흔한 요청이 될 겁니다. 오래된 Java/Spring이나 PHP 서비스를 Go나 Kotlin으로 옮기자는 이야기가 AI 덕분에 훨씬 가볍게 나옵니다. 이 사례가 보여주는 건, 번역 결과물이 원본과 "같은 앱"이라는 보장이 전혀 없다는 점입니다. 재작성을 맡기기 전에 하위 호환 범위, 외부 의존성(Redis, MQ)을 유지할지, 부하 시 무엇을 버릴지를 문서로 먼저 정해두는 게 맞습니다.
  • 부하 테스트 방식부터 의심하세요. 국내 팀이 많이 쓰는 JMeter, nGrinder의 기본 사용법도 대개 "스레드 N개가 쉬지 않고 요청"하는 closed-loop에 가깝습니다. 이벤트성 트래픽(선착순, 티켓 오픈, 출근 시간 알림)이 주요 시나리오라면 k6의 constant arrival rate처럼 도착률을 고정하는 테스트를 따로 돌려야 실제 장애 양상이 보입니다.
  • "무제한 큐"는 안전장치가 아닙니다. Elixir 버전이 전달률이 높아 보인 이유는 쌓아뒀기 때문이고, 그 대가는 메모리와 지연이었습니다. Kafka 컨슈머 랙, Spring의 비동기 executor 큐, Node.js 스트림 버퍼도 똑같은 고민입니다. 한도를 정하지 않으면 결국 OOM이나 타임아웃이 한도를 대신 정해줍니다.
  • 아쉬운 점도 있습니다. 저자의 재측정도 단일 시나리오이고, Go 버전은 다루지 않았습니다. 이 글을 "Rust가 Elixir보다 낫다"로 읽으면 저자가 비판한 실수를 반대 방향으로 반복하는 셈입니다.

정리

AI 에이전트는 코드를 빨리 써주지만, 명시하지 않은 요구사항은 대신 결정해버립니다. 그 결정이 지연, 메모리, 실패 처리 방식을 바꾸고, 벤치마크 숫자 하나로는 그 차이가 드러나지 않습니다. 재작성 결과를 받아들기 전에 "부하가 넘칠 때 이 시스템은 무엇을 포기하는가"부터 확인하세요.


  • AI 코딩
  • 부하 테스트
  • Rust
  • Elixir
  • 백프레셔

댓글 0

아직 댓글이 없습니다.