본 강연은 Zendesk의 David Hner가 Ruby on Rails 기반 애플리케이션의 성능 최적화 여정을 공유하는 자리입니다. Zendesk는 약 15년의 역사를 가진 대규모 Rails 애플리케이션을 운영하고 있으며, 초기 12년간 전담 성능 팀이 없었음에도 불구하고, 최근 3인으로 구성된 성능 팀이 P99(상위 99% 최악의 응답 시간) 기준으로 놀라운 성능 향상을 달성했습니다. 이들의 노력은 인프라 비용을 수천만 달러 규모로 절감하는 데 기여했으며, 본 강연에서는 데이터 주도적인 접근 방식을 통해 이룬 다양한 최적화 기법과 실질적인 팁들을 다룹니다.
Zendesk의 성능 최적화 전략은 여러 핵심 영역에 걸쳐 있습니다. 첫째, 관측 가능성(Observability) 확보가 가장 중요합니다. Zendesk는 DataDog을 활용하여 애플리케이션의 상세 트레이스를 수집하고, 특히 커스텀 트레이스 추가를 통해 병목 현상을 정확히 파악했습니다. rapy_dogging_trace
와 같은 메서드를 활용하여 코드 블록이나 메서드 호출을 감싸는 방식으로 트레이스를 추가했으며, 이는 문제의 근본 원인을 찾아내는 데 결정적인 역할을 했습니다. 비록 트레이스 비용이 발생하지만, 이를 통해 얻는 통찰력은 훨씬 가치 있습니다.
둘째, 데이터 관리 전략입니다. Zendesk는 오래된 데이터를 주 데이터 저장소(MySQL)에서 DynamoDB와 같은 아카이빙 스토어로 옮기는 전략을 사용합니다. 이는 MySQL의 부하를 줄여 쿼리 성능을 향상시키고, 애플리케이션의 전반적인 반응 속도를 개선합니다. 이러한 아카이빙 전략은 애플리케이션 설계 초기부터 고려하는 것이 중요합니다.
셋째, 쿼리 및 메모이제이션 테스트입니다. 애플리케이션에서 발생하는 SQL 쿼리 수를 테스트하는 것은 N+1 문제를 방지하고 성능 회귀를 막는 데 필수적입니다. Zendesk는 특정 구문을 사용하여 예상 쿼리 수를 검증하며, 메모이제이션이 올바르게 작동하는지 확인합니다. 이는 시간이 지나 코드 변경으로 인해 성능 문제가 발생할 경우 즉시 감지할 수 있게 합니다.
넷째, 작은 최적화의 큰 영향입니다. 초당 수십억 건의 요청이 발생하는 Zendesk와 같은 대규모 시스템에서는 단 1밀리초의 응답 시간 단축도 연간 수백 일에 달하는 서버 시간 절감으로 이어질 수 있습니다. 개발자 입장에서는 작은 개선으로 보일 수 있지만, 인프라 팀에게는 엄청난 이점으로 작용합니다.
다섯째, ‘God Object’와 메모이제이션 활용입니다. Zendesk의 Account
객체는 애플리케이션 전반에서 동일한 객체 ID를 가지는 ‘God Object’로 작동합니다. 이를 통해 Account
객체에 메서드를 추가하고, 해당 메서드의 결과를 메모이제이션함으로써 요청 주기 동안 불필요한 데이터베이스 쿼리를 줄일 수 있습니다. 예를 들어, user_emails
와 같은 반복적인 호출에서 첫 호출 이후에는 메모이제이션된 결과를 사용하여 성능을 극대화합니다.
여섯째, 효율적인 데이터 구조 사용입니다. Ruby에서 배열 대신 Set
을 사용하여 고유성 보장을 통한 unique
메서드 호출 비용을 절감하거나, Hash
를 활용하여 O(1) 시간 복잡도로 데이터를 빠르게 찾는 등 데이터 구조 선택의 중요성을 강조합니다. 또한, map.flatten
대신 flat_map
을 사용하여 불필요한 중간 배열 생성을 피하는 것도 성능 개선에 기여합니다.
일곱째, 데이터 정규화(Sanitization)의 중요성입니다. Zendesk는 이메일과 같이 외부에서 유입되는 데이터에 포함된 불필요한 공백이나 특수 문자를 수신 시점에 미리 정규화함으로써, 이후 복잡한 정규식(RegEx) 연산이 필요한 시점에서 발생하는 성능 저하를 방지합니다.
여덟째, pluck
메서드 활용입니다. 드롭다운 목록과 같이 대량의 데이터를 표시해야 할 때, ActiveRecord 객체를 2만 개씩 인스턴스화하는 대신 pluck
을 사용하여 필요한 컬럼만 배열이나 해시 형태로 가져옴으로써 객체 생성 비용을 크게 줄일 수 있습니다. 이는 특히 사용자 정의 필드가 많은 Zendesk와 같은 서비스에서 매우 효과적입니다.
아홉째, HTTP 캐싱 전략입니다. max-age
를 사용하여 브라우저가 일정 시간 동안 동일한 리소스에 대한 요청을 서버에 보내지 않도록 하여 트래픽을 줄입니다. 또한, Rails의 stale?
(ETag) 기능을 활용하여 서버에서 304 Not Modified 응답을 보내도록 하여 콘텐츠 렌더링을 생략하는 것이 이상적입니다. 그러나 Zendesk의 엔드포인트는 비RESTful하여 ETag 계산이 응답의 마지막 단계에서 이루어지는 특성이 있습니다. 이로 인해 Rails의 표준 stale?
미들웨어 캐싱을 직접 활용하기 어렵지만, Zendesk는 커스텀 캐싱 전략을 통해 ETag를 미리 캐시하고 요청 초기에 비교하여 304 응답을 보내는 방식으로 유사한 효과를 얻고 있습니다.
마지막으로, 캐싱 계층 최적화(read_multi
) 및 인덱스 관리입니다. 캐싱 계층에서도 N+1 문제가 발생할 수 있으므로, read_multi
와 같은 기능을 사용하여 여러 캐시 항목을 한 번에 가져오는 것이 중요합니다. 또한, 과거에 성능 향상을 위해 강제했던 인덱스(FORCE INDEX
)가 시간이 지남에 따라 오히려 병목 현상을 일으킬 수 있으므로, 정기적으로 검토하고 불필요한 강제 인덱스를 제거하는 것이 필요합니다.
Zendesk의 사례는 Ruby on Rails 애플리케이션의 성능 최적화가 단일 솔루션이 아닌, 다각적인 접근 방식과 지속적인 모니터링이 필요한 복합적인 작업임을 보여줍니다. 관측 가능성 확보를 통한 문제 식별, 데이터 관리 전략, 효율적인 코드 및 데이터 구조 활용, 그리고 레거시 시스템의 한계를 극복하기 위한 맞춤형 캐싱 전략 등은 모든 규모의 Rails 애플리케이션에 적용 가능한 귀중한 교훈을 제공합니다. 특히, 사소해 보이는 성능 개선이 대규모 서비스에서는 엄청난 비용 절감과 사용자 경험 향상으로 이어진다는 점을 명심해야 합니다. 성능 최적화는 결코 '완료'되는 작업이 아니며, 끊임없이 변화하는 환경에 맞춰 진화해야 합니다.