코드를 작성하는 비용은 저렴해졌지만, 그에 대한 책임은 그렇지 않습니다 (AI가 생성한 Gem을 출시하며 배운 점)
요약
LLM을 사용하여 Ruby gem을 개발하며 겪은 과잉 엔지니어링과 환각 문제에 대한 회고록입니다. AI가 생성한 코드의 논리적 오류를 검증 없이 배포했을 때 발생하는 위험성과 개발자의 책임감을 다룹니다.
핵심 포인트
- AI 생성 코드는 논리적 환각과 과잉 엔지니어링을 포함할 수 있음
- 이해하지 못한 코드는 제대로 방어하거나 유지보수할 수 없음
- LLM 활용 시 코드 리뷰와 검증 단계가 필수적임
- 특정 환경(Rails-native)에 최적화된 가벼운 도구의 필요성
읽기 전에 미리 경고합니다: 이것은 출시 발표가 아닙니다. 이것은 공개적으로 무언가를 잘못 처리한 경험과, 그로 인해 제 작업 방식이 어떻게 변했는지에 대한 이야기입니다. 만약 이런 종류의 이야기가 취향이 아니시라면, 서운해하지 마세요.
고백
한 달 조금 전, 저는 llm_cost_tracker라는 Ruby gem을 게시했습니다. 솔직히 말씀드리면: 거의 모든 코드가 LLM에 의해 작성되었습니다. 제가 프롬프트(Prompt)를 입력하면, 모델이 생성하고, 저는 배포했습니다. 약 일주일 동안은 제 자신이 꽤 괜찮게 느껴졌습니다. 그러다 피드백을 구하기 위해 r/ruby에 게시했는데, 사람들이 정확히 피드백을 주었습니다. 그리고 그들의 말이 맞았습니다. 코드의 상당 부분이 그저 환각 (Hallucination) 상태였습니다.
제가 계속해서 떠올리게 되는, 너무나 전형적인 사례가 하나 있습니다: 제 gemspec에는 add_dependency를 통해 activesupport와 activerecord를 필수 의존성 (Hard dependencies)으로 나열했습니다. 이 gem은 그것들 없이는 로드조차 할 수 없습니다. 그런데도 gem 내부에는 런타임 (Runtime)에 ActiveSupport와 ActiveRecord가 존재하는지 세심하게 확인하는 코드가 들어 있었습니다. 말 그대로 그것들 없이는 실행될 수 없는 두 가지 요소의 부재에 대해 방어하고 있었던 것입니다. 아무도 의도적으로 그런 코드를 작성하지 않습니다. 모델은 세심해 보이는 결과물을 만들어냈고, 저는 제대로 읽어보지 않았기에 알아채지 못한 채 배포했습니다.
그런 사례를 하나 발견하고 나면, 모든 곳에서 그런 것들이 보이기 시작합니다: 불가능한 상태에 대한 가드 절 (Guard clauses), 확인을 확인하는 체크 로직, 추상화 (Abstraction) 위에 겹겹이 쌓인 추상화들. 편집증 모드입니다. 발생할 수 없는 상황에 대한 과잉 엔지니어링 (Over-engineering). 어떤 피드백도 악의적이지 않았습니다. 그저 정확했을 뿐이며, 제가 그 어떤 것에도 반박할 수 없었기에 쓰라렸습니다. 애초에 이해하지 못한 코드는 방어할 수 없습니다. 아이디어는 괜찮았습니다. 제가 문제였습니다.
덧붙이자면: gem 자체는 실제 문제를 해결합니다. 만약 당신의 Rails 앱이 LLM을 호출한다면, 월간 청구서는 당신이 얼마를 썼는지는 알려주지만, 누가 썼는지에 대해서는 기본적으로 아무것도 알려주지 않습니다. 모델별 총액이나 API 키별 총액은 알 수 있습니다. 하지만 당신의 실제 환경은 알 수 없습니다: 어떤 기능이 호출을 발생시켰는지, 어떤 테넌트 (Tenant)에 속해 있는지, 혹은 지난 화요일에 배포한 프롬프트 수정 사항이 조용히 토큰 (Token) 사용량을 두 배로 늘렸는지와 같은 것들 말입니다.
그 정보는 오직 호출 시점(call time)에 당신의 앱 내부에서 잠시 존재할 뿐입니다. 제공자(Provider)는 "chat"이라는 기능이 당신에게 무엇을 의미하는지 전혀 알지 못합니다. 그 시점에서 정보를 놓치면 그것은 영원히 사라집니다. 이미 존재하는 도구들은 제가 필요로 했던 것보다 훨씬 더 높은 수준을 지향합니다. Langfuse는 완전한 관측성 (Observability) 도구이며, 이를 셀프 호스팅 (Self-hosting) 하려면 Postgres, ClickHouse, Redis, S3를 모두 실행해야 합니다. Helicone은 모든 호출 경로의 프록시 (Proxy)로 자리 잡고 있습니다. LiteLLM은 Python 환경에서 동작합니다. 저는 그저 어떤 기능이 비용을 소모했는지라는 단 하나의 질문에 답하고, 제 방해를 하지 않는 작은 Rails 네이티브 (Rails-native) 도구를 원했을 뿐입니다. 좋은 아이디어였지만, 실행은 좋지 않았습니다. 이 두 가지를 구분하지 못한 것이 프로젝트 전체를 삭제할 뻔하게 만든 원인이었습니다.
무엇을 바꿨고, 무엇을 바꾸지 않았나
여기서 주의해서 말씀드리고 싶은 부분이 있습니다. 이 이야기의 쉬운 버전은 거짓말이기 때문입니다. 저는 모든 것을 내다 버리고 애정 어린 마음으로 직접 다시 작성한 것이 아닙니다. 그랬다면 더 멋진 서사가 되었겠지만 사실이 아닙니다. 진실은 더 지저분합니다. 이 Gem에는 여전히 생성된 코드 더미가 쌓여 있으며, 저는 여전히 파일 하나하나를 살펴보며 정리하고 있습니다. 실제로 바뀐 것은 제가 일하는 방식입니다. 그중 일부는 이제 제가 직접 다시 작성합니다. 여전히 많은 부분을 에이전트 (Agent)에게 맡기기도 합니다. 도구 사용을 아예 끊어버리는 것은 어리석은 일이며, 저 또한 그렇게 믿지 않기 때문입니다. 하지만 저는 예전과는 완전히 다르게 위임합니다. 모든 디프 (Diff)를 읽습니다. 동료의 PR (Pull Request)을 검토하듯 기본적으로 의구심을 품고 검토합니다. 그리고 입 밖으로 설명할 수 없는 것은 무엇도 커밋 (Commit)하지 않습니다. 비난을 받았던 그 Gem과 오늘날의 Gem은 "AI가 작성한 것" 대 "사람이 작성한 것"의 차이가 아닙니다. 그것은 "눈을 감고 배포한 것" 대 "진정으로 나의 것인 것"의 차이입니다. 저는 바로 오늘, 그 정리 작업의 한복판에 있습니다. 편집증적인 가드 (Guard), 무의미한 셀프 체크 (Self-check), 그리고 무언가를 하기 위해서가 아니라 철저해 보이기 위해 존재했던 엔지니어링 요소들을 뜯어내고 있습니다. 이 과정은 느리고 지루하며 아무도 박수를 쳐주지 않습니다. 그럼에도 저는 이 일을 하고 있습니다. 품질은 제가 실제로 통제할 수 있는 부분이며, 아직 넘지 않은 선을 넘었다고 거짓말하기보다는 정리가 여전히 진행 중이라고 말씀드리는 편이 낫기 때문입니다.
교훈: 코드를 작성하는 비용은 저렴해졌고, 따라서 업무의 성격이 바뀌었습니다. 이 부분은 과거의 저에게 꼭 해주고 싶은 말입니다. 기계에게 키보드를 맡겨두고 자리를 비워서는 안 됩니다. 기계가 돌려주는 것은 초안 (draft)이지, 완성된 결과물이 아닙니다. 제가 초안을 완성된 것으로 취급하는 순간, 저는 저 자신의 이해도를 조용히 포기해 버리는 셈이었고, 누군가 그 빈틈을 알아차리는 데는 불과 30초도 걸리지 않았습니다. 누군가 질문을 하기 시작하면, 자신이 작성한 코드를 이해하고 있다는 사실을 속이는 것은 불가능합니다. 하지만 저에게 실제로 와닿은 재정의 (reframe)는 이것입니다. 이제 코드를 작성하는 것은 저렴합니다. 예전처럼 많은 시간이나 노력이 들지 않습니다. 그리고 무언가를 만드는 비용이 낮아지면, 가치는 다른 곳으로 이동합니다. 여기서는 그 가치가 품질 (quality)과 소유권 (ownership)으로 이동했습니다. 예전에 타이핑하는 데 쓰던 시간들을, 이제는 읽고, 형태를 잡고, 제 이름으로 나가는 모든 것에 책임을 지는 데 사용합니다. 이것은 더 작은 업무가 아닙니다. 1인 유지보수자 (solo maintainer)에게는 솔직히 이제 이것이 업무의 전부일 수도 있습니다. 비판 (roast)을 받았다고 해서 AI 사용을 그만두게 된 것은 아닙니다. 오히려 코드가 나타나는 속도에 맞춰 리뷰의 기준 (review bar)을 높여야겠다는 확신을 갖게 되었습니다. 생성하는 것은 저렴하지만, 책임지는 것은 비쌉니다. 그리고 그 책임은 언제나 저의 몫이 될 것이었습니다.
현재의 솔직한 상태
기능: 모든 LLM 호출을 사용자의 Postgres 또는 MySQL에 기록합니다. 제공자 (provider), 모델 (model), 토큰 (tokens), 비용 (cost), 지연 시간 (latency), 컴포넌트별 상세 내역, 그리고 제공자가 가격을 업데이트할 때 오래된 수치가 조용히 변하지 않도록 하는 가격 스냅샷 (pricing snapshot)을 포함합니다. 프록시 (proxy)를 사용하지 않으며, 호출은 제공자에게 직접 전달됩니다. 별도의 데이터 저장소를 실행할 필요도 없습니다. 속성 부여 (attribution)는 호출을 감싸는 태그일 뿐입니다:
LlmCostTracker.with_tags(user_id: Current.user.id, feature: "chat") do
client = OpenAI::Client.new(api_key: ENV["OPENAI_API_KEY"])
client.responses.create(model: "gpt-4o", input: "Hello")
end
의도적으로 포함하지 않은 기능: 프롬프트 캡처 (prompt capture), 트레이스 (traces), 리플레이 (replay), 인보이스 등급 (invoice-grade) 기능은 포함하지 않습니다. 만약 관측 가능성 (observability)이 필요하다면 Langfuse를 사용하세요. 면전에서 그렇게 말씀드리겠습니다.
그리고 제가 미화하지 않고 말씀드릴 두 가지가 있습니다. 이 프로젝트에는 여전히 제가 검토하고 재작업해야 하는 생성된 코드 (generated code)가 많이 포함되어 있습니다. 방향은 맞습니다. 하지만 완성된 것은 아니며, 그렇지 않은 척하지 않겠습니다. 저를 포함해 그 누구의 프로덕션 (production) 환경에서도 아직 실행되고 있지 않습니다. 저는 실제 OpenAI 및 Anthropic 키를 사용하여 별도의 테스트 앱에서 스트리밍을 포함한 모든 호출 비용을 제 사비로 지불하며 테스트했습니다. 캡처는 작동하고, 태그는 정확하게 속성을 부여하며, 대시보드는 렌더링됩니다. 하지만 "내 테스트 앱에서는 작동한다"는 말은 "실전 검증되었다 (battle-tested)"는 말과는 거리가 멀며, 저도 그 차이를 알고 있습니다.
제가 실제로 원하는 것
저는 소수의 초기 수용자 (early adopters)를 원합니다. 실제 Rails 앱에서 실제 LLM 호출을 사용하고 있는 분들 중, 이 도구를 적용해 보고 무엇이 무너지는지 저에게 솔직하게 말해줄 분들을 찾습니다. 솔직한 피드백은 분명 저에게 효과가 있습니다. 리포지토리 (repo)와 문서는 GitHub에 있으며, MIT 라이선스입니다. 무엇이 고장 났는지 알려주는 이슈 (issue) 하나가 저에게는 스타 (star) 하나보다 더 가치 있습니다.
그리고 만약 여러분도 이제는 대부분 그렇듯 AI를 사용하여 개발한다면, 아마 이것이 전체적인 교훈일지도 모릅니다. 도구들은 코드를 작성하는 비용을 저렴하게 만들었지만, 그 코드에 대한 책임을 지는 비용을 저렴하게 만들지는 않았습니다. 그 부분은 여전히 우리의 몫입니다. 저는 창피한 방식으로 이를 배웠습니다. 여러분은 그러지 않아도 됩니다.
그래서 진심으로 궁금합니다. 현재 여러분의 팀은 LLM 지출을 어떻게 속성 (attribute)하고 있나요? 제가 받는 모든 답변은 "우리가 직접 무언가를 짜 맞췄다"는 식의 변형된 형태였습니다. 그것이 바로 제가 여전히 해결하려고 애쓰고 있는 갈증입니다. 그리고 네, 투명하게 밝히자면, 이 포스트를 작성하는 데도 AI의 도움을 받았습니다. 다만 이번의 차이점은, 제가 실제로 그 과정에 직접 참여했다는 점입니다. ;)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기