POOOLING FOREST
7년 묵은 레거시 Rails 모놀리스에 안전하게 AI 에이전트 이식하기 - 7년 된 Rails 모놀리스 시스템에 보안과 권한 로직을 유지하면서 RubyLLM과 Pundit을 활용해 안
Engineering & Tech

7년 묵은 레거시 Rails 모놀리스에 안전하게 AI 에이전트 이식하기

7년 된 Rails 모놀리스 시스템에 보안과 권한 로직을 유지하면서 RubyLLM과 Pundit을 활용해 안전하게 AI 에이전트를 이식한 기술적 여정과 경험을 공유합니다.

김영태

테크리드

안녕하세요, 풀링포레스트의 8년차 개발자 김테크입니다.

오래된 레거시 시스템, 특히 거대한 모놀리스(Monolith) 아키텍처를 운영하다 보면 최신 기술 트렌드를 따라가기가 참 버겁게 느껴질 때가 있습니다. 요즘 다들 AI 기능을 도입한다고 난리인데, 정작 우리 시스템은 7년 묵은 Rails 코드에, 복잡한 멀티 테넌트 구조, 거기다 민감한 개인정보까지 다루고 있다면 어떨까요? "우리 구조에서는 불가능해"라고 단정 짓기 쉽습니다.

저 또한 그랬습니다. 제가 다루는 시스템은 노인 및 장애인 사례 관리를 위한 SaaS인데, 데이터 접근 권한이 정말 층층이 겹쳐 있습니다. 단순히 "DB 다 열어줄게, 알아서 찾아봐"라고 LLM에게 맡길 수 있는 환경이 아니죠.

하지만 최근 샌프란시스코에서 열린 SF Ruby 컨퍼런스 자료들을 보며 생각이 바뀌었습니다. 핵심은 LLM에게 무제한 접근 권한을 주는 것이 아니라, 엄격하게 통제된 도구(Tool)만 쥐여주는 것이었습니다. 오늘은 기존의 복잡한 비즈니스 로직이나 보안 제약을 해치지 않고, RubyLLM과 Pundit 정책을 활용해 안전하게 AI 에이전트를 구축한 경험을 공유하려 합니다.

왜 RubyLLM인가?

RubyLLM은 다양한 LLM 제공자(OpenAI, Anthropic 등)와의 상호작용을 깔끔하게 추상화해주는 젬(gem)입니다. 설정도 매우 직관적입니다.

설정 파일에서 API 키를 세팅하고 타임아웃 등을 지정하면 준비는 끝납니다. 이 라이브러리의 핵심은 Conversation 모델을 통해 대화 맥락을 관리하고, Tools를 통해 LLM이 사용할 수 있는 함수를 정의할 수 있다는 점입니다.

핵심은 Function Call과 보안 로직의 결합

가장 큰 고민은 보안이었습니다. "존 스노우의 전화번호가 뭐야?"라고 물었을 때, AI가 아무 데이터나 뒤지면 안 되니까요. 여기서 우리는 '검색 도구(SearchTool)'를 만들고 그 안에 기존의 보안 로직을 태웠습니다.

Algolia 검색 엔진을 사용하고 있었는데, 도구의 실행 메서드(execute) 안에 Pundit 정책 스코프(Policy Scope)를 적용했습니다. 즉, AI가 검색을 요청하면 1차로 Algolia에서 데이터를 찾고, 2차로 Rails 애플리케이션의 권한 로직이 "이 사용자가 정말 이 데이터를 볼 수 있는가?"를 검증한 뒤에야 결과를 반환합니다.

코드로 보면 대략 이런 흐름입니다.

def execute(query:)
  # 1. Algolia 검색 수행
  response = Algolia::SearchClient.create(...).search(...)
  ids = response.hits.map { |hit| hit[:id] }

  # 2. Pundit 정책을 통해 권한이 있는 데이터만 필터링 (핵심!)
  base_scope = Client.where(id: ids)
  client = Admin::Org::ClientPolicy::Scope.new(base_scope).resolve.first

  return {} unless client

  # 3. 안전한 데이터만 반환
  {
    id: client.id,
    name: client.full_name,
    email: client.email
  }
end

이렇게 하면 LLM은 직접 DB에 접근하지 못합니다. 단지 "검색해줘"라는 요청을 보내고, 우리 시스템이 안전하게 필터링한 결과만 받아볼 뿐입니다. 존 스노우의 전화번호는 권한이 있는 사용자에게만 노출됩니다.

UI와 사용자 경험

사용자 인터페이스는 Rails의 Hotwire와 Turbo Stream을 활용했습니다. 복잡한 자바스크립트 프레임워크를 도입하지 않고도 실시간 채팅 경험을 구현하기에 충분했습니다.

사용자가 메시지를 입력하면 백그라운드 Job이 돌면서 LLM에게 질의를 던지고, LLM이 도구 사용이 필요하다고 판단하면 위에서 만든 SearchTool을 실행합니다. 그 결과가 다시 대화 맥락에 포함되어 최종 답변이 생성되면, Turbo Stream이 화면에 메시지를 뿌려주는 방식입니다.

모델 선택에 대한 시행착오

모델 선택 과정에서도 배운 점이 많았습니다. 처음엔 최신 모델이 무조건 좋을 거라 생각해서 GPT-5 계열이나 컨텍스트가 큰 모델들을 테스트해봤습니다. 하지만 GPT-5는 생각보다 응답 속도가 느렸습니다. 도구를 호출하고 결과를 받아 다시 응답하는 왕복 과정(Roundtrip)이 길어지니 사용자가 답답해하더군요.

반면 구버전인 GPT-4는 속도는 괜찮았지만, 도구를 호출해야 할 상황에서 호출하지 않고 그럴듯한 거짓말(Hallucination)을 지어내는 경우가 잦았습니다.

결과적으로 우리는 GPT-4o에 정착했습니다. 속도와 정확도 사이에서 가장 균형 잡힌 퍼포먼스를 보여주었기 때문입니다.

마치며

오래된 모놀리스 시스템이라고 해서 AI 도입을 겁낼 필요는 없습니다. 시스템을 완전히 갈아엎거나 보안 정책을 느슨하게 풀지 않아도 됩니다. 오히려 기존에 잘 짜여진 권한 로직(Policy)이 있다면, 이를 AI가 사용할 '도구' 안에 녹여냄으로써 훨씬 안전하고 강력한 에이전트를 만들 수 있습니다.

여러분의 레거시 시스템에도 작지만 똑똑한 AI 동료를 하나 붙여보는 건 어떨까요? 생각보다 어렵지 않습니다.

지금 읽으신 내용, 귀사에 적용해보고 싶으신가요?

상황과 목표를 알려주시면 가능한 옵션과 현실적인 도입 경로를 제안해드립니다.