25.09 - 쿼리가 1초 넘게 걸리는데 퇴근을 해? (N+1 Query, O(n*m) Algorithm
베이비매직 개발을 마치고 벌써 반년이 지났다
PACS 조회 기능을 가볍게 추가하기 위해
병원용 관리자 앱에 가볍게 기능을 추가 후 테스트를 하는데
하는데...
조회가 안 된다?
정확히는, 하루 종일 걸린다
언제부터 이렇게 느렸지?
요즘 세상에 쿼리가 1초 이상 걸리는 앱이 있다?
그런 앱을 만들고 퇴근을 하는 개발자가 있다?
근데 그동안 퇴근하고 있었다는 게 함정
이 사달이 난 원인
구구절절한 히스토리를 풀어보자면
우리 회사 제품의 초기 기획(8년 전)엔 태아 초음파 캡처 저장이 계획에 없었다
서비스의 목적은 태아 초음파 영상 저장 및 공유였으니까
근데 몇몇 병원의 요청으로 초음파 캡처 저장과 영상과의 페어링이 필요해졌고
하나의 진료에서 생성되는 영상과 캡처를 페어링 하여 보여주는 기능이 추가됐다(3년 전)
물론 여기까진 아무 문제가 없었다
괜찮았던 시절
최신 장비로 진행하는 초음파 세션은 fileId 가 있어서 영상과 캡처를 정확히 연결할 수 있었다
구형 장비가 저장하는 초음파 캡처에는 fileId 가 없었지만
초음파 영상 촬영 시간으로 구분하면 되고, 캡처의 양 자체가 많지 않아서 괜찮았다
일부 대형 병원에서도 캡처 기능을 요청했지만 그때까지도 괜찮았다
절대적인 데이터 양 자체가 많지 않았으니(백만 개 남짓)
문제 폭발
문제는 시간이 지나 글로벌 서비스로 성공적 확장을 이룬 현재다
미국, 멕시코, 베트남, 인도네시아 등으로 진출하며 이미 수백 대의 구형 장비가 현장에 설치되었는데
전체 새 장비 교체 지원은 금액적으로도, 병원의 운영에도 큰 부담이었다
결국 초기 제품 스펙의 성능 + 누적된 수천만 개의 데이터 결합으로
병원용 관리자 앱의 조회 속도는 수 초에서 수십 초까지 늘어나버린 것이다
진짜 문제: 쿼리와 알고리즘
조사해 보니 원인은 단순했다
- N+1 쿼리: 영상과 캡처 필터링 시 각각의 테이블에 대해 루프를 돌며 N번씩 DB 호출
- O(n*m): 중첩 반복문으로 모든 영상과 캡처 매칭
- 배열 탐색: 매번 O(n) 조회 반복
겉보기엔 단순 조회였지만, 실체는 데이터 폭탄 위에서 돌아가는 구조였다
해결 과정
쿼리 패턴 변경
- 100번 호출하던 쿼리를 배치 쿼리 1회로 통합
중복 계산 제거
- 동일 연산 캐싱 후 재사용
자료구조 변경
- 배열 대신 Map 사용 -> 조회를 O(1)로 단축
- 문자열 바코드 -> 숫자 비교로 연산 축소
알고리즘 개선
- O(n*m) -> O(n+m)으로 단순화
결과
| 전체 응답시간 | 5.15초 | ~1초 | 80% |
| DB 쿼리 | N+1 (100번) | 1번 | 99% |
| 알고리즘 복잡도 | O(n×m) | O(n+m) | 98% |
| 중복 계산 | 1000번 | 100번 | 90% |
| 배열 조회 | O(n)×100 | O(1)×100 | 98% |
가장 큰 효과는 역시 N+1 쿼리 제거
DB 접근 횟수를 줄여 5.15초 중 4.6초를 줄였고
나머지 600ms를 알고리즘 개선으로 해결!
결론
성능은 한순간의 기능 추가와 타협에서 무너진다
처음엔 "이 정도면 괜찮겠지"로 시작하지만
몇 년 뒤 글로벌 스케일로 누적되면 끔찍한 병목이 된다
DB 구조나 인덱스에 손대지 않고도
쿼리 패턴, 자료구조, 알고리즘 개선만으로도 90% 이상의 성능 향상을 만들 수 있음에도...!
결국 문제는 코드가 아니라 시간의 누적이 아닐까?
한 줄 한 줄 괜찮다고 넘겼던 과거의 선택들이
몇 년 뒤엔 수천만 건의 데이터 위에 업보로 쌓여 있다
단순한 리팩토링과 성능 개선의 즐거움을 넘어
시간이 만든 기술부채를 일시불 상환하는 짜릿함이었다

댓글