Rails 애플리케이션 개발 시 흔히 발생하는 위험 요소와 해결 방안

全問正解率約3%: RubyKaigiで出題したやりがちな危険コード5選 / Yuta Nakashima - Kaigi on Rails 2025

작성자
Kaigi on Rails
발행일
2025년 11월 25일

핵심 요약

  • 1 레일즈 애플리케이션에서 SQL 쿼리 최적화, 트랜잭션 관리, 비동기 처리, 오류 핸들링 등 실제 프로덕션 코드에서 발생할 수 있는 5가지 주요 문제점과 해결책을 제시합니다.
  • 2 N+1 문제, 트랜잭션 내 외부 API 호출 및 비동기 작업 실행, 광범위한 예외 처리 등 성능, 안정성, 보안에 영향을 미치는 일반적인 개발 실수를 다룹니다.
  • 3 효율적인 데이터베이스 접근, 트랜잭션 외부 로직 분리, 구체적인 오류 분기 처리를 통해 애플리케이션의 견고성과 유지보수성을 향상시키는 방안을 강조합니다.

도입

본 발표는 RubyKaigi 2024에서 주식회사 Huble의 백엔드 테크 리드인 나카시마 유타(中島優太)가 진행한 세션으로, '이레카치 위험도 5000'이라는 제목으로 레일즈(Rails) 애플리케이션 개발 시 흔히 발생하는 문제점과 해결책을 다룹니다. Huble은 이번 RubyKaigi에 처음 참가하며, 실제 제품에서 발생했던 버그와 성능 저하를 유발했던 코드 기반의 퀴즈 이벤트를 부스에서 진행했습니다. 퀴즈의 낮은 정답률을 바탕으로, 해당 문제점과 해결 방안을 공유함으로써 다른 개발자들에게 유용한 지식을 제공하고자 합니다. 이 발표는 트랜잭션 범위, 비동기 처리 시점, 오류 핸들링, SQL과 Ruby 로직 분리, 테이블 설계 등 교과서적인 내용을 포함하고 있습니다.

발표는 실제 프로덕션 코드에서 간과하기 쉬운 5가지 주요 문제점을 로직 흐름에 따라 상세히 설명하고, 각 문제에 대한 해결 방안을 제시합니다. 문제 코드는 Huble의 문서 업로드 API를 모방한 형태로, 사용자 인증, 파라미터 유효성 검사, 문서 생성, 외부 API 호출, 비동기 알림 발행 등의 일반적인 처리 과정을 포함합니다.

1. SQL에서의 전체 데이터 조회 (N+1 문제의 서막)

  • 문제점: user.folders.any?와 같이 특정 조건 확인을 위해 테이블의 모든 레코드를 가져오는 방식은 N개의 ActiveRecord 객체를 메모리에 생성하여 가비지 컬렉션(GC) 부담을 증가시키고, 커넥션 풀을 장시간 점유하며, 데이터 양 증가 시 O(N)의 성능 저하를 야기합니다. 이는 특히 DDoS 공격에 취약할 수 있습니다.

  • 해결 방안: SQL 단에서 존재 여부를 확인하는 exists? 쿼리를 사용합니다. 이를 통해 불필요한 객체 생성과 컬럼 데이터 조회를 방지하고, 쿼리 횟수를 1회로 줄여 성능을 최적화합니다.

2. Ruby 측에서의 계산 (N+1 문제의 심화)

  • 문제점: 조직 내 총 업로드 사이즈를 계산하기 위해 모든 문서를 가져와 Ruby 코드 내에서 파일 사이즈를 합산하는 방식은 N+1 쿼리 문제를 발생시킵니다. 이는 다량의 쿼리 발행으로 커넥션 풀을 장시간 점유하고, 동일 쿼리라도 파라미터가 다르면 DB 캐시를 활용하지 못해 CPU 부하를 증가시킵니다.

  • 해결 방안: SQL의 집계 함수(예: SUM)를 사용하여 데이터베이스 단에서 계산을 수행합니다. 이를 통해 쿼리 횟수를 1회로 줄이고, SQL 엔진에 최적화된 집계 기능을 활용하여 성능을 향상시킵니다. 대안으로 organization 테이블에 총 파일 사이즈 집계 컬럼을 두거나 KVS 캐싱을 고려할 수 있습니다.

3. 트랜잭션 내에서의 외부 API 실행

  • 문제점: 문서 생성 트랜잭션 내에서 OCR API와 같은 외부 API를 호출하는 경우, 참조 테이블(예: organization)의 행이 외부 API 응답이 완료될 때까지 공유 락(Shared Lock) 상태로 유지됩니다. 이는 DB 락 시간을 불필요하게 늘리고, DB 로직 문제와 외부 API 장애를 구분하기 어렵게 만듭니다.

  • 해결 방안: 외부 API 호출을 트랜잭션 외부로 분리합니다. 이를 통해 참조 테이블의 락 시간을 줄이고 문제 원인 분석을 용이하게 합니다. OCR과 같이 즉각적인 정합성이 크게 요구되지 않는 경우 비동기 처리를 고려할 수 있으며, 복잡한 분산 트랜잭션의 경우 Saga 패턴과 같은 기법을 참고할 수 있습니다.

4. 트랜잭션 내에서의 비동기 처리 발화

  • 문제점: 트랜잭션 내에서 비동기 알림 처리와 같은 비동기 작업을 시작하는 경우, 작업 실행 시점에 따라 커밋되지 않은 데이터에 접근하여 예외를 발생시키는 레이스 컨디션(Race Condition)이 발생할 수 있습니다. 또한 트랜잭션이 롤백되더라도 비동기 작업은 이미 큐에 남아 불필요하게 실행되어 데이터 불일치를 초래할 수 있습니다.

  • 해결 방안: 비동기 처리 시작 시점을 트랜잭션 외부로 이동합니다. 이를 통해 미커밋 데이터 접근 문제를 해결하고 레이스 컨디션 및 데드락을 방지할 수 있습니다. Rails 7.2부터는 config.active_job.after_transaction_commit 옵션을 통해 트랜잭션 커밋 후에만 Active Job이 발행되도록 설정할 수 있습니다.

5. StandardError를 통한 모든 예외 처리

  • 문제점: rescue StandardError => e와 같이 모든 예외를 포괄적으로 처리하고 e.message를 반환하는 방식은 내부 오류 발생 시 데이터베이스 구조, 테이블명, 컬럼명, 파일 경로, 환경 변수 등 민감한 정보가 노출될 위험이 있습니다. 이는 SQL 인젝션 공격 등에 악용될 수 있으며, 모든 오류를 500 에러로 처리하여 유효성 검사 오류조차도 서버 오류로 보이게 하여 사용자 경험을 저해하고 문제 분석 및 디버깅을 어렵게 만듭니다.

  • 해결 방안: 오류 유형별로 분기하여 적절한 HTTP 상태 코드와 사용자 친화적인 메시지를 반환합니다. 이를 통해 불필요한 내부 정보 노출을 방지하고, 오류 조건별 테스트 작성을 용이하게 하여 시스템의 견고성을 높입니다.

결론

레일즈는 유연한 개발을 가능하게 하지만, 이러한 유연성은 동시에 개발자가 의식하지 않으면 쉽게 오류를 범할 수 있는 지점이 되기도 합니다. 지식적으로 알고 있더라도 촉박한 개발 일정이나 팀 내 코드 리뷰의 한계 등으로 인해 이러한 문제점들이 실제 코드에 반영될 수 있습니다. 특히 최근 커서(Cursor)나 데빈(Devin)과 같은 AI 기반의 코드 생성 도구를 활용하는 환경에서는 이러한 잠재적 문제점을 더욱 의식하고, 코드 규칙 및 프로세스 정비의 중요성이 커집니다. Huble은 현재 다양한 기술적 과제, 특히 성능 튜닝에 집중하고 있습니다. 복잡한 권한 관리 로직을 SQL 비트 연산으로 대체하거나, AI 에이전트 기능을 다른 기능으로 확장하는 등 높은 난이도의 개발을 수행하고 있습니다. 이러한 기술적 도전 과제에 관심 있는 개발자들에게 Huble은 매력적인 기회를 제공할 것입니다.

댓글 0

로그인이 필요합니다

댓글을 작성하거나 대화에 참여하려면 로그인이 필요합니다.

로그인 하러 가기

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!