← 목록으로

미안하지만, 여전히 직접 생각해야 합니다

요약
  • AI 에이전트를 활용한 코드 재작성 시에도 호환성, 지연 시간, 처리량 등 핵심적인 설계 결정은 여전히 인간의 비판적 판단이 필요합니다.
  • AI가 생성한 코드는 구체적인 요구사항이 없으면 암묵적인 선택을 포함할 수 있으며, 언어별 특성과 벤치마크 결과를 종합적으로 검토해야 신뢰할 수 있는 시스템을 구축할 수 있습니다.
  • AI 에이전트에 코드 재작성을 맡겨도 호환성, 지연 시간, 처리량, 메모리 한도 같은 설계 판단은 사라지지 않으며, 요구사항이 구체적이지 않으면 모델이 암묵적으로 결정함
  • Campfire Once의 Rust와 Elixir 재작성은 호환성과 구현 제약부터 서로 달라, 결과를 그대로 언어의 성능 차이로 해석하기 어려움
  • 처리량이나 알림 전달률 하나만으로는 시스템 품질을 판단할 수 없으며, 부하 생성 방식, HTTP 오류, 전달 지연, 재접속 동작까지 함께 살펴야 함
  • Rust의 이벤트 버퍼를 늘리자 전달률은 약 14%에서 약 90%로 높아졌지만, 최대 전달 지연도 11초에서 130초 초과로 증가해 성공률과 응답성의 상충관계가 드러남
  • 신뢰할 수 있는 시스템을 만들려면 언어와 무관하게 실패 방식과 자원 한도를 설계해야 하며, AI가 생성한 코드와 벤치마크 결과도 비판적으로 검토해야 함
AI가 대신 작성해도 설계 결정은 남음
  • Ruby on Rails를 만든 DHH는 Campfire Once를 Rails에서 Rust로 재작성하는 일을 AI 에이전트에 맡겼으며, 직접 Rust 코드를 읽거나 쓰는 것은 꺼림
    • 이후 Elixir와 Go 등 다른 언어의 재작성도 추가함
  • 프로그래밍 경험이 풍부한 사람이 AI를 사용하더라도, 코드를 읽지 않는 방식의 결과에는 여러 문제가 남음
  • 프롬프트가 충분히 구체적이지 않으면 정답이 하나가 아닌 설계 선택은 우연에 맡겨질 수 있음
    • 하위 호환성을 유지할지, 지연 시간과 처리량 중 무엇을 우선할지 결정해야 함
    • 부하 상황에서 메모리를 얼마나 허용할지, 장애 이후 새 콘텐츠 알림이 유실되어도 되는지 정해야 함
    • 사용자가 신경 쓰지 않은 항목도 LLM이 구현하는 순간 암묵적으로 결정됨
서로 다른 제약으로 만든 재작성은 공정한 언어 비교가 아님
  • Rust 재작성은 완전한 하위 호환성을 유지하지 않음
    • 캐싱을 쉽게 하려고 CSRF 토큰을 제거함
    • 알림 등에 사용하던 Redis를 프로세스 내부 큐로 대체함
  • Elixir 재작성은 Rails 원본에 훨씬 가까움
  • 이런 차이는 언어 자체와 독립적인 구현 선택이므로, 재작성 결과를 그대로 비교해 언어의 우열을 판단하기 어려움
    • Elixir로 재작성하라는 요청 자체가 완전한 하위 호환성을 선택하게 만드는 것은 아님
생성된 코드에도 동시성 검토가 필요함
  • Elixir 버전에는 단일 프로세스가 모든 SQL 쿼리를 순차 처리하는 문제가 있음
    • 동시에 실행할 수 있는 읽기 쿼리까지 직렬화함
    • DHH는 AI가 고성능 코드를 작성하지 못한 점을 Elixir에 불리한 결과로 보는 듯함
  • Rust 버전에도 일부 데이터베이스 작업이 비동기가 아닌 문제가 있으며, 상황에 따라서는 더 나쁠 수 있음
    • Rust 비동기 런타임의 스케줄링은 협력형이므로, 작업이 실행권을 양보하지 않으면 같은 워커 스레드의 다른 작업이 실행되지 못함
    • 100ms가 걸리는 SQL 쿼리의 응답을 기다릴 때는 비동기 입출력으로 런타임에 실행권을 넘기는 것이 바람직함
    • 재작성 코드의 일부 쿼리는 워커 스레드에서 실행되지만, 일부는 비동기 작업 안에서 블로킹 방식으로 실행됨
  • 잠금도 같은 검토가 필요하며, 비동기 런타임에서는 tokio::sync::Mutex 같은 비동기 잠금이 가장 안전한 선택임
    • 잠금 유지 시간이 매우 짧다는 확신이 있으면 동기 잠금도 사용할 수 있음
    • 동기 잠금으로 스레드가 10ms간 막히면 같은 스레드의 다른 작업도 기다리게 됨
  • 코딩이 이미 해결된 문제라고 보기 어려우며, 생성된 코드가 어떻게 동작하는지 이해할 필요가 있음
벤치마크는 무엇을 측정하는지부터 정해야 함
  • 처리량만 측정하면 시스템의 다른 속성을 놓침
    • Zach Daniels의 부하 상황 알림 전달률 측정에서는 Rust 버전의 전달 성공률이 1% 였음
  • DHH와 Zach의 테스트는 모두 폐쇄형 루프 벤치마크였음
    • N개의 클라이언트가 이전 요청의 응답을 받자마자 다음 메시지를 보내는 방식임
    • 일정 시간 동안 시스템이 얼마나 많은 요청을 처리하는지 측정하지만, 요청 유입 속도 자체가 응답 속도에 좌우됨
  • 많은 사용자가 다른 사용자의 작업 완료를 기다리지 않고 동시에 행동하는 상황에는 일정 도착률 방식이 더 적합할 수 있음
    • 일정 도착률(constant arrival rate)은 응답 속도와 무관하게 일정한 속도로 요청을 보냄
    • 이벤트 전달 신뢰성을 비교하려면 같은 수의 이벤트를 대상으로 시험하는 것이 바람직함
  • 기존 결과에서 Rust는 6,000~7,000개 알림 중 1%, Elixir는 약 1,700개 중 100%를 전달함
    • Elixir가 역압력(backpressure)을 더 잘 처리했지만, 요청량이 시스템을 과부하 상태로 만들지 않는 범위에서의 결과임
    • Rust는 약 4배 많은 요청을 처리했으므로 WebSocket으로 전달해야 할 이벤트도 더 많았음
Elixir의 동시성에도 상충관계가 있음
  • Rust의 1% 전달률만 보고 근본 원인을 확인하지 않은 채 Elixir의 동시성 우위로 결론 내리는 것은 오해를 낳을 수 있음
  • Elixir를 비롯한 BEAM 기반 언어는 동시성에 강하며, 일반적으로 Rust보다 동시성 코드를 작성하기 쉬움
    • 그 대신 저수준 제어가 제한되고, 메모리 사용량이 더 많으며, 속도에서도 대가를 치르는 경우가 많음
    • rustler가 존재하는 데도 이유가 있음
  • 복잡하고 신뢰성이 필요한 시스템에서 단점 없이 명백하게 우월한 선택은 드물며, 단일 지표만으로 언어의 우열을 단정해서는 안 됨
같은 요청률에서도 전달률만으로는 부족함
  • DHH의 Rust 버전과 여러 수정이 반영된 Zach의 Elixir 버전을 초당 100건의 POST 요청으로 다시 시험함
    • Rust 클라이언트는 이벤트의 약 14%를 받았으며, HTTP 오류는 없었음
    • Elixir 클라이언트는 약 60%를 받았지만, HTTP POST 요청의 약 23%가 시간 초과됨
    • Elixir의 최악 이벤트 전달 지연은 180초에 가까웠음
  • Rust는 전달이 뒤처지는 클라이언트의 연결을 끊으며, 브라우저 클라이언트는 재접속 후 최신 메시지를 가져옴
    • 최신 메시지를 다시 가져오면 놓친 이벤트 대부분은 더 이상 필요하지 않음
    • 백그라운드 재접속으로 새 내용을 가져오는 방식과 새 메시지 알림을 3분 기다리는 방식은 사용자 경험이 다름
  • 전달률, HTTP 성공 여부, 지연 시간, 복구 동작을 함께 보지 않으면 시스템의 실제 동작을 판단하기 어려움
버퍼 한도는 전달률, 지연 시간, 메모리를 함께 바꿈
  • Rust 버전은 여러 연결에 이벤트를 전달하기 위해 tokio::sync::broadcast 채널을 사용함
    • 채널에는 용량 한도가 있으며, 수신자가 충분히 빨리 처리하지 못하면 RecvError::Lagged 오류가 발생함
    • 재작성 버전의 브로드캐스트 용량은 256으로 설정되어 있었음
  • Elixir에서는 GenServer 프로세스가 이벤트 전달을 처리하며, 기본적으로 메일박스에 용량 제한이 없음
  • Rust의 stream_capacity를 256에서 16384로 변경하자 초당 100건 요청에서 전달률이 약 14%에서 약 90%로 높아짐
    • 동시에 최대 이벤트 전달 지연인 pMAX가 11초에서 130초 초과로 늘어남
    • Elixir와 비슷하게 전달이 매우 느려졌으므로, 전달률 상승만으로 더 나은 구현이라고 할 수 없음
  • 이 채팅 상황에서는 이벤트를 극도로 느리게 처리하기보다 빨리 실패하고 재접속을 유도하는 방식이 더 나을 수 있음
    • 11초도 지나치게 길며, 그보다 빨리 전달하지 못하는 이벤트는 버리는 편이 나음
  • 직접 한도를 정하지 않으면 외부 한도에 부딪힐 수 있음
    • 초당 100건 요청 테스트에서 Elixir 버전의 메모리 사용량은 1.8GB에 도달함
    • 일부 메시지를 버릴지, 메모리 부족으로 프로세스가 종료될 위험을 감수할지도 선택해야 함
    • Elixir도 동시성 문제를 자동으로 모두 해결하지 않으며, 어떤 언어든 실패 방식과 상충관계를 고려해야 함
신뢰성에는 실패 시점을 정하는 판단이 필요함
  • AI를 쓰더라도 비판적 사고와 구현에 대한 이해는 계속 필요함
  • 단일 지표로 성급하게 결론 내리지 말아야 하며, 신뢰할 수 있는 시스템을 만들려면 언제 실패해야 하는지 알아야 함
  • 벤치마크 설계와 해석은 여전히 어려움
그냥 목록으로
원문 보기 ↗