SQL은 빠르지만 API는 느린 이유

데이터베이스 쿼리는 빠릅니다. 캐싱도 잘 작동하고, 백그라운드 작업도 문제없습니다.

하지만 API는 여전히 느립니다. CPU 사용량은 높습니다.

문제는 데이터베이스가 아닙니다. 문제는 Ruby 레이어에 있습니다.

병목 현상은 데이터가 데이터베이스를 떠나 애플리케이션으로 들어온 직후에 발생하는 경우가 많습니다. 이는 주로 다음 세 가지 문제로 인해 발생합니다:

  • 비대한 직렬화(Serialization)
  • 과도한 객체 할당
  • 반복되는 연산

해결 방법은 다음과 같습니다.

  1. 비대한 직렬화 방지

많은 개발자가 모델 전체를 JSON으로 변환합니다.

render json: @shipments

만약 shipment 데이터에 40개의 컬럼이 있는데 프론트엔드에서 5개만 필요하다면, 불필요한 CPU 사이클을 낭비하게 됩니다. 또한 API 키나 비용 같은 민감한 데이터가 유출될 위험도 있습니다.

해결책: 필요한 필드만 반환하세요.

render json: @shipments.as_json(only: [:id, :tracking_no, :status])

속도를 더 높이려면 pluck을 사용하여 데이터를 배열로 가져오세요. 이렇게 하면 무거운 ActiveRecord 객체를 생성하는 과정을 완전히 피할 수 있습니다.

  1. 객체 할당 줄이기

Ruby가 생성하는 모든 객체는 메모리를 소모합니다. 단일 요청 중에 수천 개의 객체를 생성하면 가비지 컬렉터(GC)가 더 많이 작동해야 하며, 이는 시스템 전체의 속도를 저하시킵니다.

루프 내부에서 새로운 해시나 문자열을 생성하는 것을 피하세요.

나쁜 예:

@shipments.map do |s|
  { label: "#{s.tracking_no} - #{s.status.upcase}" }
end

좋은 예: 정적 데이터를 루프 외부로 옮기세요. Ruby에서 처리하기보다 데이터베이스에서 더 많은 작업을 처리하도록 하세요.

  1. 반복되는 연산 피하기

하나의 요청 내에서 동일한 메서드를 여러 번 호출하면 시간을 낭비하게 됩니다.

예시:

def total_weight
  shipments.sum(&:weight)
end

만약 뷰(view), 헬퍼(helper), 시리얼라이저(serializer)에서 모두 이 메서드를 호출한다면, 합계를 세 번 계산하게 됩니다.

해결책: 메모이제이션(memoization)을 사용하세요.

def total_weight
  @total_weight ||= shipments.sum(&:weight)
end

이렇게 하면 요청당 계산이 단 한 번만 수행되도록 보장할 수 있습니다.

요약 표:

  • 비대한 직렬화: 필요한 필드만 반환하거나 pluck 사용.
  • 높은 할당량: 루프 내에서 객체 생성 최소화.
  • 반복되는 연산: 메모이제이션을 사용하여 결과 재사용.

데이터베이스 최적화는 데이터를 적게 요청하는 것을 의미합니다. 애플리케이션 최적화는 데이터를 가져온 후 불필요한 작업을 줄이는 것을 의미합니다.

Source: https://dev.to/danewu/your-sql-is-fast-but-the-api-is-slow-its-the-ruby-layer-2fno