Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby on Rails 데이터베이스 성능 최적화 완벽 가이드: 테스트 방법과 핵심 전략

이 글에서는 Rails 애플리케이션에서 데이터베이스 성능을 테스트하는 방법과 가장 흔하게 발생하는 데이터베이스 성능 문제를 해결하는 전략을 다룹니다.

Rails 애플리케이션을 개발할 때 ActiveRecord는 기본적으로 데이터베이스를 관리하는 도구입니다. ActiveRecord는 .where, .save, .create, .update 같은 직관적인 명령어로 데이터를 조회하고 삽입할 수 있는 쉽고 빠른 인터페이스를 제공합니다. Rails가 이러한 명령어를 SQL 쿼리로 변환해 주기 때문에 개발 생산성은 높아지지만, 때로는 성능 문제의 원인이 되기도 합니다. 따라서 개발자라면 흔히 발생하는 성능 이슈와 최적화 방법을 반드시 이해해야 합니다.

Ruby on Rails의 ActiveRecord 간단 정리

Rails의 ActiveRecord는 MVC(Model-View-Controller) 아키텍처 중 모델 계층에 해당하며, 데이터베이스를 비즈니스 객체 형태로 표현하여 관리합니다. ActiveRecord 패턴은 ORM(Object-Relational Mapping) 기법을 활용해 애플리케이션의 객체를 관계형 데이터베이스 테이블에 연결합니다.

그럼 본격적으로 시작해 보겠습니다.

Rails에서 데이터베이스 성능 문제를 진단하고 테스트하는 3가지 방법

1. ActiveRecord 쿼리에 Explain 실행하기

EXPLAIN 문은 SQL 쿼리의 실행 계획(execution plan)을 보여줍니다. 즉, 쿼리가 어떻게 실행될지, 몇 개의 행을 스캔하는지, 어떤 인덱스가 사용되는지, 테이블이 어떤 순서로 조인되는지 등의 정보를 확인할 수 있습니다.

실행 계획을 통해 다음 사항들을 파악하여 쿼리 실행 속도를 늦추는 원인을 찾을 수 있습니다.

  • 쿼리 성능 향상을 위해 어떤 인덱스를 추가해야 하는지
  • 테이블이 최적의 순서로 조인되고 있는지 여부 — 필요하다면 STRAIGHT_JOIN을 사용해 조인 순서를 강제할 수 있습니다.

EXPLAIN은 SELECT뿐만 아니라 DELETE, INSERT, REPLACE, UPDATE 문에도 사용할 수 있습니다.

ActiveRecord에서 explain을 사용하는 방법은 매우 간단합니다. 아래와 같이 입력하면 됩니다.


명령어 끝에 .explain만 붙이면 해당 ActiveRecord 명령의 쿼리 플랜을 확인할 수 있습니다. 위 예제는 id(기본 키)로 테이블을 조회하는 매우 단순한 쿼리입니다. explain 결과를 보면 pkey 인덱스가 사용되었음을 알 수 있으며, 이를 통해 해당 쿼리가 최적화되어 빠르게 실행된다는 것을 확신할 수 있습니다.

느린 쿼리에 .explain을 붙여 실행 순서와 사용된 인덱스를 확인해 보세요. 만약 쿼리 플랜에 Seq Scan(순차 스캔)이 표시된다면 인덱스가 사용되지 않고 있다는 뜻이므로, 쿼리를 수정하거나 새로운 인덱스를 추가해야 합니다.

조인이 포함된 또 다른 예제를 살펴보겠습니다.


이 예제에서는 users 테이블이 collaborations 테이블과 조인됩니다. 쿼리 플랜을 살펴보면 collaborations 테이블은 seq scan으로 먼저 실행되고, users 테이블은 pkey 인덱스를 사용해 나중에 실행됩니다. 이 경우 collaborations 테이블의 user_id 컬럼에 인덱스를 추가하면 쿼리를 최적화할 수 있습니다. Explain은 쿼리를 세부적으로 분석해 주기 때문에 어느 부분을 최적화해야 하는지 파악하는 데 큰 도움이 됩니다.

2. 핵심 데이터베이스 지표 측정하기

쿼리 실행 시간만으로 성능을 판단해서는 안 됩니다. 쿼리가 실제로 잘 동작하는지 확인하려면 다음과 같은 다양한 데이터베이스 지표를 함께 살펴봐야 합니다.

  1. CPU 사용량
  2. 메모리 사용량
  3. 디스크 I/O 대기 큐
  4. 네트워크 대역폭(인바운드/아웃바운드 트래픽)
  5. 사용 가능한 디스크 공간
  6. 처리량(Throughput)

이러한 지표들이 특정 임계값을 초과하면 쿼리 속도가 느려질 수 있습니다. 성능 문제를 정확히 이해하려면 시간 범위별 데이터 포인트를 추적하고 분석해야 합니다.

측정해야 할 지표는 다음과 같은 요소에 따라 달라집니다.

  • 데이터베이스 유형:

    • 관계형(Relational)
    • 인메모리(In-memory)
    • NoSQL
    • 데이터 웨어하우스
  • 서버 호스팅 방식:

    • 온프레미스(On-premises)
    • 클라우드

즉, 데이터베이스 지표 모니터링에는 정답이 하나로 정해져 있지 않으며, 환경과 조건에 맞는 접근이 필요합니다.

3. AppSignal로 Rails 앱 성능 측정하기

모든 쿼리를 한눈에 볼 수 있는 중앙 집중식 도구 없이 성능 지표를 관리하는 것은 매우 어렵습니다. 모든 코드 블록에 성능 측정 코드를 일일이 추가하는 방식은 번거롭고 유지보수도 힘들어집니다.

AppSignal 같은 도구를 활용하면 애플리케이션에 손쉽게 성능 측정 기능을 통합할 수 있습니다. AppSignal은 Rails를 기본적으로 지원하며, 설치 과정은 'AppSignal for Ruby' 공식 문서에서 자세히 확인할 수 있습니다.

주목해야 할 핵심 지표는 다음과 같습니다.

  • 느린 쿼리(Slow queries)
  • 처리량 기반 데이터베이스 성능
  • N+1 쿼리
  • 데이터베이스 지연 시간(Latency)
  • 활성 커넥션 수

다음은 AppSignal 대시보드의 예시 화면입니다.

Ruby on Rails 데이터베이스 성능 최적화 완벽 가이드: 테스트 방법과 핵심 전략

Rails 데이터베이스 성능을 최적화하는 7가지 방법

1. Eager Loading으로 N+1 쿼리 해결하기

N+1 쿼리는 가장 흔한 데이터베이스 성능 문제입니다. user와 project 두 모델이 있을 때 발생하는 N+1 쿼리 예제를 살펴보겠습니다.


이제 사용자 이름과 프로젝트 이름을 함께 조회하려면 다음 코드를 실행합니다.


위 코드는 루프가 돌 때마다 데이터베이스에 쿼리를 날리기 때문에 심각한 성능 문제를 일으킵니다. 실행되는 쿼리의 총 개수는 '사용자 수 + 1'이 됩니다.

이 문제를 해결하는 방법은 아주 간단합니다. 바로 연관 관계를 즉시 로딩(Eager Loading)하는 것입니다. 쿼리 끝에 .includes(:projects)만 추가하면 됩니다.


이렇게 하면 프로젝트가 미리 로딩되기 때문에 루프를 실행할 때마다 데이터베이스에 쿼리를 날리지 않습니다.


Rails 6.1부터는 strict loading 기능을 제공하여, 연관 데이터에 접근하기 전에 반드시 eager loading이 적용되도록 강제할 수 있습니다. 모델에 다음 한 줄을 추가하면 활성화됩니다.


이후 eager loading 없이 projects에 접근하려고 하면 Rails는 ActiveRecord::StrictLoadingViolationError 예외를 발생시킵니다.

Rails 6.1 미만 버전을 사용한다면 Bullet 같은 젬(gem)을 활용할 수 있습니다.

2. 데이터베이스 인덱스 활용하기

데이터베이스는 데이터를 더 빠르게 조회할 수 있도록 인덱스를 제공합니다. 앞서 소개한 explain 명령어를 사용하면 쿼리가 적절한 인덱스를 사용하고 있는지 확인할 수 있습니다.

느린 쿼리가 기존 인덱스나 새로 추가한 인덱스를 활용하도록 수정하면 성능을 크게 개선할 수 있습니다.

MySQL에는 네 가지 종류의 인덱스가 있습니다.

  1. Primary key - 기본 키에 자동으로 생성되며 값의 고유성을 보장합니다.
  2. Unique - 해당 컬럼에 저장되는 값이 항상 고유하도록 보장합니다.
  3. Index - 기본 키 외의 컬럼에 추가하는 일반 인덱스입니다.
  4. Full text - 문자 기반 데이터 검색에 유용합니다.

인덱스는 B-Tree 또는 Hash 구조로 저장됩니다.

인덱스는 단일 컬럼에 추가할 수도 있고, 여러 컬럼을 조합한 복합 인덱스(composite index)로 생성할 수도 있습니다. 복합 인덱스는 여러 컬럼이 포함된 쿼리를 최적화할 때 특히 유용합니다. 단일 인덱스만 사용하면 대량의 데이터셋을 스캔해야 할 수 있습니다.

예를 들어 다음 쿼리에는 두 개의 컬럼이 사용됩니다.


프로젝트 "abc"에는 많은 사용자가 속해 있을 수 있고, country 필드까지 스캔해야 한다면 결과 데이터셋이 커져 처리 속도가 느려질 수 있습니다. 이런 경우 project와 country 두 컬럼에 복합 인덱스를 추가하면 성능을 개선할 수 있습니다.

Rails에서는 다음과 같은 ActiveRecord 마이그레이션 명령으로 인덱스를 추가할 수 있습니다.

단일 인덱스:


복합 인덱스:


3. Limit 사용하기

반환되는 레코드가 많을수록 성능은 저하됩니다. 대량의 데이터를 한 번에 가져오는 단일 쿼리보다는, 여러 번 나누어 조회하는 것이 더 좋습니다.


다음 100개 배치를 가져오려면 offset을 활용합니다.


이렇게 하면 성능이 눈에 띄게 개선됩니다. 다만 주의할 점이 있습니다. offset 값이 커질수록 쿼리는 오히려 느려지므로, offset에는 반드시 limit을 함께 지정해야 합니다.

4. find_each로 대량 데이터 조회하기

레코드를 반복 처리할 때는 Rails의 배치 처리 기능을 활용하는 것이 좋습니다.


위 코드는 데이터베이스의 모든 레코드를 한 번에 조회하기 때문에 메모리와 데이터베이스 성능에 악영향을 줄 수 있습니다.

find_eachfind_in_batches를 사용하면 동일한 작업을 배치 단위로 나눠 처리해 성능을 개선할 수 있습니다.


기본적으로 find_each는 1,000건씩 배치로 조회합니다. 인자로 값을 지정하면 배치 크기를 변경할 수 있습니다.


작업 목적에 따라 find_in_batches를 사용할 수도 있습니다. 두 메서드의 차이점은 find_in_batches는 개별 레코드가 아닌 모델 객체의 배열(array of models)로 결과를 반환한다는 점입니다.

5. Pluck으로 필요한 필드만 선택하기

Pluck 명령은 쿼리 결과를 ActiveRecord 객체 대신 배열로 직접 변환해 줍니다.

쿼리 결과가 클 경우 Pluck을 사용하면 코드 성능이 향상됩니다. Pluck은 데이터베이스에서 필요한 필드만 선택해서 가져옵니다.


결과는 메인 테이블이 아닌 인덱스에서 가져오기 때문에, 정렬(sort)이 포함된 쿼리에서 특히 효과적입니다.

6. 벌크 연산(Bulk Operations) 사용하기

벌크 삭제(Bulk Delete)
ActiveRecord 객체를 순회하면서 삭제하면 레코드가 하나씩 삭제됩니다.


레코드마다 개별적으로 쿼리를 실행해야 하므로 데이터베이스에 많은 부하가 걸립니다. 대신 단일 벌크 delete_all 쿼리를 사용하는 것이 훨씬 효율적입니다.


벌크 생성(Bulk Create)
벌크 삭제처럼 벌크 삽입도 ActiveRecord로 가능하다는 사실을 모르는 분들이 많습니다. n번의 쿼리를 단 한 번으로 줄일 수 있습니다. ActiveRecord::Basecreate 메서드는 해시 배열을 입력값으로 받습니다.


7. 필요하다면 인메모리 계산 활용하기

상황에 따라서는 쿼리보다 인메모리 계산이 더 나은 선택이 될 수 있습니다. 예를 들어 사용자 기록이 없는 국가를 찾는다고 가정해 보겠습니다.


위 쿼리는 결과를 얻기 위해 N번의 쿼리가 필요합니다. 대신 주어진 국가들의 사용자를 한 번의 쿼리로 조회한 뒤, 나머지 계산은 메모리에서 처리할 수 있습니다.


또한 경우에 따라 캐싱을 활용하면 요청-응답 사이클을 재사용해 데이터베이스 부하를 줄일 수 있습니다. Rails는 페이지 캐싱(page caching), 액션 캐싱(action caching), 프래그먼트 캐싱(fragment caching) 세 가지 캐싱 기법을 제공하며, 그중 프래그먼트 캐싱이 기본으로 지원됩니다.

마무리: ActiveRecord와 AppSignal로 Rails 성능 최적화하기

핵심 내용을 정리해 보겠습니다. 이번 글에서는 Rails에서 데이터베이스 성능 문제를 진단하고 테스트하는 세 가지 방법을 살펴보았습니다.

  • ActiveRecord 쿼리에 Explain 실행하기
  • 핵심 데이터베이스 지표 측정하기
  • AppSignal로 Rails 앱 성능 측정하기

그리고 데이터베이스 성능을 최적화하는 일곱 가지 방법도 소개했습니다.

  • N+1 쿼리 해결을 위한 Eager Loading
  • 데이터베이스 인덱스 활용
  • Limit 사용
  • 대량 데이터 조회 시 find_each 활용
  • Pluck으로 필요한 필드만 선택
  • 벌크 연산(Bulk Operations)
  • 인메모리 계산 활용

Rails는 애플리케이션을 빠르고 쉽게 개발할 수 있게 해줍니다. ActiveRecord는 데이터베이스 코드의 생산성, 재사용성, 유지보수성을 높여 주지만, ActiveRecord 쿼리가 실제 어떤 SQL로 변환되고 실행되는지 이해하는 것이 중요합니다.

하지만 Rails 데이터베이스 성능을 최적화하는 데 가장 중요한 것은 성능 데이터에 대한 가시성(visibility) 확보입니다. 성능 문제는 언제든 발생할 수 있지만, 데이터를 명확히 볼 수 있다면 충분히 해결할 수 있습니다. 데이터베이스 지표를 제공하는 적절한 모니터링 도구가 꼭 필요합니다. 저희는 AppSignal을 추천합니다 ;)

P.S. Ruby Magic의 새 글을 가장 먼저 읽고 싶다면 Ruby Magic 뉴스레터를 구독하세요. 어떤 글도 놓치지 않을 수 있습니다!