FormFlex N+1 쿼리 최적화

설문 조회 API 6개에서 발생하던 N+1 쿼리를 제거해 최대 97.4% 쿼리 감소, 최대 33배 응답 속도 개선을 달성한 과정 정리.

Node.js Sequelize PostgreSQL 성능 최적화


📊 한눈에 보는 결과

엔드포인트 쿼리 수 (전 → 후) 응답 시간 (전 → 후) 쿼리 감소율 속도 개선
공개 설문 — 제목 검색 151 → 4 22,196ms → 658ms −97.4% 33.7×
공개 설문 — 목록 153 → 3 12,709ms → 419ms −98.0% 30.3×
내 응답 — 제목 검색 104 → 7 12,550ms → 980ms −93.3% 12.8×
내 응답 — 목록 43 → 6 3,973ms → 561ms −86.0% 7.1×
내 폼 — 제목 검색 22 → 4 1,815ms → 336ms −81.8% 5.4×
내 폼 — 목록 9 → 4 746ms → 1,311ms −55.6% 0.6× ⚠️

⚠️ 마지막 케이스만 시간이 늘어난 이유

작성한 설문이 8개로 적은 상황이었음. Answer COUNT를 8번 도는 것보다, 한 번의 JOIN + GROUP BY가 옵티마이저 입장에서 더 무거운 작업이 되는 케이스. 데이터가 늘어날수록(N이 커질수록) 후자가 압도적으로 유리해짐. 다른 5개 엔드포인트 결과가 이를 뒷받침. 쿼리 수 자체는 9 → 4로 줄었기 때문에 DB 부하 관점에서는 일관되게 개선된 결과.


🤔 무엇이 문제였나 — N+1 쿼리

FormFlex의 설문 목록 API는 응답마다 각 설문의 참여자 수내 참여 여부를 함께 내려줘야 했음. 처음 코드는 이런 패턴이었음:

// 1) 설문 목록을 한 번에 조회
const surveys = await Survey.findAll({ where: { userId } });

// 2) 설문마다 루프를 돌면서 추가 쿼리 실행 ← 여기가 N+1
for (const survey of surveys) {
  const attendCount = await Answer.count({ /* surveyId 기준 */ });
  const myAnswer    = await Answer.findOne({ /* userId, surveyId */ });
}

설문이 50개면 1 + 50 × 2 = 101번의 쿼리가 발생. 실측에서 공개 설문 검색 API는 한 번 호출에 151번의 쿼리, 22초의 응답 시간이 걸리는 상황이었음.

왜 이런 코드가 나왔는가


🛠️ 어떻게 해결했나

핵심 전략 3가지

  1. WHERE IN으로 한 번에 조회 — N개의 findOne을 1개의 findAll({ where: { id: [...] } })로 대체