한 Magento 개발자가 300개의 개별 ProductRepositoryInterface::getById() 호출을 단일 컬렉션 쿼리로 교체했습니다. 데이터베이스 히트 수가 300회에서 1회로 줄어들었고, 카테고리 페이지 로딩 속도는 눈에 띄게 빨라졌습니다. 그 대가는 무엇이었을까요? 일부 제품이 스토어프론트에서 사라져 쇼핑객과 운영자들을 당혹스럽게 만들었습니다.

Magento 카테고리 페이지의 N+1 문제

해당 페이지에는 300개의 제품이 나열되어 있었습니다. 코드에서는 각 제품마다 리포지토리의 getById() 메서드를 통해 두 개의 커스텀 속성을 가져왔으며, 이는 제품당 하나의 쿼리를 생성하는 전형적인 N+1 패턴이었습니다. 이러한 개별 제품 로드를 단일 쿼리로 필요한 모든 행을 가져오는 컬렉션으로 교체하면서 쿼리 수가 급감했습니다. 프론트엔드 지연 시간은 개선되었지만, 제품 목록에서 이전에 나타났던 항목들이 누락되었습니다.

컬렉션이 품절된 항목을 숨기는 이유

Magento의 CatalogInventory 모듈은 프론트엔드용으로 구축된 모든 제품 컬렉션에 "재고 있음(in-stock)" 필터를 자동으로 추가합니다. 이 필터는 스토어 설정에서 Display Out of Stock Products가 꺼져 있을 때 활성화되며, 이는 품절된 항목을 보여주고 싶지 않은 판매자들이 흔히 선택하는 설정입니다.

반면, 리포지토리는 재고 필터를 적용하지 않습니다. 리포지토리는 재고 수준과 관계없이 단순히 제품이 존재하는지 확인하고 이를 반환합니다. 코드가 리포지토리 호출에서 컬렉션으로 전환되었을 때, 재고 필터는 수량이 0인 모든 제품을 조용히 제거했습니다. 로그에 오류가 남지도 않았습니다. 컬렉션이 개발자가 예상했던 것보다 적은 행을 반환했을 뿐입니다.

판매자에게 미치는 영향

품절된 SKU를 조용히 누락시키는 스토어프론트는 여러 문제를 야기합니다:

  • 프론트엔드 컴포넌트가 누락된 항목을 수신하여 빈 레이블이나 잘못된 가격을 표시하게 됩니다.
  • 증상(제품 누락)이 경고를 생성하지 않기 때문에 디버깅이 더 어려워집니다.

대부분의 자동화 테스트는 재고가 있는 피스처(fixtures)를 사용하기 때문에, 이 버그는 사이트가 실제 재고로 운영될 때까지 숨겨져 있는 경우가 많습니다.

재고를 숨기지 않으면서 속도를 유지하는 방법

  1. 기본 재고 필터 건너뛰기 – 가용성과 관계없이 모든 제품이 필요한 경우 컬렉션에서 필터를 명시적으로 제거하십시오. Magento는 쿼리별로 재고 플러그인을 비활성화하거나 교체할 수 있는 메서드를 제공합니다.
  2. 정확성이 중요할 때는 리포지토리 사용 – 리포지토리는 품절된 제품이라 하더라도 요청된 모든 제품 ID가 반환됨을 보장합니다. 추가 쿼리를 제어하기 위해 결과를 캐싱하거나 필요한 ID만 로드하십시오.
  3. 리소스 모델 또는 Raw SQL 사용 – 필요한 속성을 위해 제품 테이블을 직접 쿼리하십시오. 이렇게 하면 모든 컬렉션 플러그인을 우회하고 어떤 행을 포함할지에 대해 완전한 제어권을 가질 수 있습니다.

속도 대 정확성

시끄러운 N+1 루프를 단일 컬렉션 쿼리로 교체하는 것은 자연스럽게 느껴지며, 지연 시간 감소는 체감할 수 있을 정도입니다. 하지만 잘못된 제품 세트를 반환하는 빠른 쿼리는 최적화가 아니라 결함입니다. 개발자는 순수 속도와 불완전한 카탈로그를 보여줄 위험 사이에서 균형을 맞춰야 합니다.

향후 주의 사항

  • 설정 드리프트(Configuration drift)Display Out of Stock Products 플래그가 커스텀 컬렉션 코드의 의도된 동작과 일치하는지 확인하십시오.
  • 플러그인 부작용 – 다른 모듈이 컬렉션에 자체 필터를 추가할 수 있습니다. 쿼리 전략을 변경할 때마다 플러그인 스택을 검토하십시오.

교훈은 명확합니다. 컬렉션으로 N+1 문제를 해결하는 것은 컬렉션의 기본 필터까지 함께 검토할 때만 의미가 있습니다. Magento의 내장 재고 필터를 무시하면 카탈로그가 조용히 잘려 나갈 수 있으며, 이는 성능 향상을 수익 손실로 바꿀 수 있습니다.