웹개발 서비스가 느려졌다면 데이터베이스 인덱스를 점검할 때
페이지가 느릴 때 왜 데이터베이스부터 살펴봐야 할까요?
Q. 프론트엔드 문제처럼 보여도 쿼리가 원인일 수 있나요?
한서린: 버튼을 누른 뒤 화면이 늦게 바뀌면 자바스크립트나 네트워크부터 의심하기 쉽습니다. 하지만 서버가 데이터베이스 응답을 기다리는 시간이 길다면 프론트엔드를 아무리 최적화해도 체감 속도는 크게 달라지지 않습니다. 특히 데이터가 수천 건에서 수십만 건으로 증가한 뒤 갑자기 느려졌다면 검색 조건과 인덱스의 불일치를 먼저 확인할 만합니다.
인덱스는 책의 색인과 비슷합니다. 모든 페이지를 처음부터 훑지 않고 원하는 위치로 이동하게 돕지만, 어떤 열을 어떤 순서로 찾는지에 따라 효과가 달라집니다. 코드를 명령과 절차로 표현한다는 기본 개념은 코딩 용어 설명에서도 확인할 수 있으며, 실제 웹개발에서는 그 코드가 데이터 접근 방식까지 결정한다는 점이 중요합니다.
- 목록 API의 응답 시간이 데이터 증가와 함께 꾸준히 늘어납니다.
- 특정 검색어나 날짜 범위에서만 요청이 유난히 오래 걸립니다.
- 서버 CPU보다 데이터베이스 CPU 또는 디스크 읽기 수치가 먼저 상승합니다.
- 같은 화면도 첫 호출은 느리고 연속 호출은 상대적으로 빠릅니다.
“느린 화면”은 증상이고 “많이 읽는 쿼리”가 원인일 수 있습니다. 브라우저 시간과 서버 처리 시간을 분리해서 측정하세요.
느린 쿼리는 어떤 순서로 찾아야 하나요?
Q. 로그를 열었는데 무엇부터 봐야 할지 모르겠습니다
한서린: 평균 응답 시간만 보면 문제를 놓칠 수 있습니다. 평소 30ms인 쿼리가 하루 한 번 8초 걸리는 경우와, 모든 요청이 400ms씩 걸리는 경우는 대응 방식이 다릅니다. 우선 API 단위로 전체 소요 시간을 확인하고, 그 안에서 데이터베이스 쿼리가 차지하는 시간을 나눠 보세요. 그다음 호출 횟수와 반환 행 수를 함께 기록해야 합니다.
예를 들어 상품 목록 한 번을 여는 동안 목록 쿼리 1회와 판매자 조회 100회가 실행된다면 단일 쿼리는 빨라도 전체 페이지는 느립니다. 이는 흔히 N+1 문제로 불리며 인덱스 추가만으로 완전히 해결되지 않습니다. 반대로 쿼리 한 번이 테이블 대부분을 읽는다면 실행 계획과 인덱스 설계가 핵심입니다. 느린 한 건과 너무 많은 호출을 반드시 구분해야 합니다.
- 브라우저 개발자 도구에서 지연되는 API를 특정합니다.
- 서버 로그나 추적 도구로 요청 내부의 쿼리 시간과 횟수를 확인합니다.
- 실제 운영과 비슷한 데이터 양에서 동일 조건을 재현합니다.
- 실행 계획으로 전체 스캔, 예상 행 수, 실제 행 수를 비교합니다.
- 수정 전 수치를 저장한 뒤 같은 조건으로 다시 측정합니다.
개인정보가 포함된 SQL과 파라미터를 로그에 그대로 남기는 것은 피해야 합니다. 사용자 이메일이나 토큰은 마스킹하고, 쿼리를 식별할 수 있는 이름과 실행 시간만 보존해도 상당수의 병목을 찾을 수 있습니다.
실행 계획에서는 어떤 신호를 읽어야 할까요?
Q. EXPLAIN 결과가 복잡한데 핵심 항목은 무엇인가요?
한서린: 처음에는 실행 계획의 모든 용어를 외우려 하지 않아도 됩니다. 먼저 데이터베이스가 테이블 전체를 읽는지, 인덱스를 이용하는지, 몇 행을 읽어 몇 행을 버리는지를 봅니다. 예상 행 수와 실제 행 수가 크게 다르면 통계 정보가 오래됐거나 데이터 분포가 한쪽으로 치우쳤을 가능성도 있습니다.
전체 스캔 자체가 항상 나쁜 것은 아닙니다. 100행짜리 설정 테이블은 인덱스를 오가는 것보다 한 번에 읽는 편이 빠를 수 있습니다. 문제는 결과 20행을 얻기 위해 수백만 행을 반복해서 검사하는 상황입니다. 정렬이나 조인을 위해 큰 임시 작업이 발생하는지도 살펴보세요. 쿼리는 올바른 답만 내면 끝나는 것이 아니라 제한된 자원으로 예측 가능한 시간 안에 답해야 합니다.
- 읽은 행 대비 반환 행: 차이가 클수록 불필요한 탐색 가능성이 큽니다.
- 조인 순서: 큰 집합이 먼저 만들어져 후속 연산 비용이 커지는지 봅니다.
- 정렬과 임시 공간: ORDER BY와 GROUP BY가 메모리 범위를 넘는지 확인합니다.
- 반복 횟수: 빠른 연산도 중첩 루프에서 수만 번 실행되면 병목이 됩니다.
- 예상치 오차: 옵티마이저가 데이터 분포를 잘못 추정하는지 확인합니다.
실제 실행을 포함하는 분석 명령은 쓰기 쿼리에서 데이터를 변경할 수 있으므로 복제 환경이나 트랜잭션 정책을 확인한 뒤 사용해야 합니다. 운영 서버에서는 부하가 큰 진단 자체가 장애를 키울 수 있다는 점도 잊지 마세요.
복합 인덱스의 열 순서는 어떻게 정하나요?
Q. WHERE에 등장한 열을 전부 묶으면 되나요?
한서린: 복합 인덱스는 열을 담은 목록이 아니라 순서가 있는 탐색 경로입니다. 예를 들어 쇼핑몰에서 status = 'SALE', category_id = 12, created_at > 특정 시점 조건으로 최신 상품을 찾는다면 동등 비교 열과 범위 조건, 정렬 순서를 함께 고려해야 합니다. WHERE 절에 등장했다는 이유만으로 무조건 모두 넣으면 쓰기 비용과 저장 공간만 커질 수 있습니다.
선택도가 높은 열을 앞에 둔다는 규칙도 절대적이지 않습니다. 실제 서비스가 어떤 조건 조합으로 조회하는지, 앞쪽 열 없이 뒤쪽 열만 검색하는 요청이 많은지, 정렬을 인덱스 순서로 처리할 수 있는지를 봐야 합니다. 프로그래밍이 문제 해결 절차를 설계하는 활동이라는 맥락은 프로그래밍 개념 자료에서 살펴볼 수 있습니다. 인덱스 역시 문법보다 접근 절차를 설계하는 작업에 가깝습니다.
- 실제 요청에서 자주 함께 사용되는 필터 조합을 수집합니다.
- 동등 조건, 범위 조건, 정렬 조건을 구분해 후보 순서를 만듭니다.
- 후보별 실행 계획과 읽은 행 수를 동일 데이터로 비교합니다.
- 기존 단일 인덱스와 기능이 중복되는지 확인합니다.
- 특정 API뿐 아니라 쓰기 작업과 다른 조회에도 미치는 영향을 측정합니다.
복합 인덱스는 “열이 많을수록 강한 인덱스”가 아닙니다. 자주 쓰는 질문에 가장 짧게 답하는 순서를 설계한 결과물입니다.
인덱스를 추가했는데도 왜 속도가 그대로일까요?
Q. 데이터베이스가 새 인덱스를 사용하지 않는 이유가 궁금합니다
한서린: 첫 번째 이유는 쿼리 조건이 인덱스 열을 가공하기 때문입니다. 날짜 열에 함수를 적용하거나 문자열 앞에 와일드카드가 붙은 검색을 수행하면 일반적인 B-트리 인덱스가 기대대로 작동하지 않을 수 있습니다. 숫자 열을 문자열과 비교하는 묵시적 형 변환, 서로 다른 문자 정렬 규칙도 탐색 경로 선택에 영향을 줍니다.
두 번째 이유는 옵티마이저가 전체 스캔이 더 싸다고 판단했기 때문입니다. 조건에 맞는 행이 테이블의 상당 부분이라면 인덱스로 위치를 찾고 본문을 반복 조회하는 것보다 한 번에 읽는 편이 합리적일 수 있습니다. 따라서 “인덱스를 사용하지 않았다”는 사실만으로 실패라고 단정하면 안 됩니다. 총 실행 시간, 실제 읽기량, 캐시 상태를 함께 비교해야 합니다.
- 인덱스 열에 함수나 계산식이 적용됐는지 확인합니다.
LIKE '%검색어%'처럼 앞부분이 열려 있는 검색인지 봅니다.- 비교하는 값과 열의 자료형이 정확히 일치하는지 점검합니다.
- 통계 정보가 최신이며 실제 데이터 분포를 반영하는지 확인합니다.
- 테스트 데이터가 운영보다 지나치게 적어 잘못된 결론을 내리지 않았는지 봅니다.
개발용 데이터 500건에서 2ms가 나온 결과는 운영 데이터 500만 건의 성능을 보장하지 않습니다. 개인정보를 제거한 분포 유사 데이터나 스테이징 환경을 준비하고, 첫 실행과 반복 실행을 분리해 기록해야 캐시 효과에 속지 않습니다.
읽기 성능을 얻는 대신 무엇을 지불하나요?
Q. 자주 조회하는 열마다 인덱스를 만들면 안전하지 않나요?
한서린: 인덱스는 공짜가 아닙니다. 행이 추가되거나 수정되거나 삭제될 때 관련 인덱스도 함께 갱신되므로 쓰기 시간이 늘고 저장 공간을 사용합니다. 주문이나 로그처럼 삽입량이 많은 테이블에 비슷한 인덱스를 여러 개 만들면 평소에는 문제가 없어 보여도 트래픽이 몰리는 순간 잠금, 디스크 쓰기, 복제 지연이 함께 커질 수 있습니다.
또한 인덱스가 많으면 어떤 것을 유지해야 하는지 판단하기 어려워집니다. 앞부분 열 구성이 같은 복합 인덱스와 단일 인덱스가 중복될 수 있고, 오래전에 삭제된 기능을 위한 인덱스가 남아 있을 수도 있습니다. 다만 사용 횟수가 낮다고 바로 삭제해서는 안 됩니다. 월말 정산이나 장애 복구처럼 드물지만 중요한 작업이 의존할 수 있기 때문입니다.
- 쓰기 지연: INSERT, UPDATE, DELETE 전후의 처리량을 측정합니다.
- 저장 공간: 테이블뿐 아니라 인덱스 크기와 증가율을 관찰합니다.
- 중복 여부: 열 순서와 포함 범위가 겹치는 인덱스를 찾습니다.
- 운영 의존성: 배치, 관리자 검색, 정산 쿼리까지 사용처를 조사합니다.
- 변경 안전성: 삭제 전 비활성화나 스테이징 검증이 가능한지 확인합니다.
인덱스 변경은 트래픽이 낮은 시간에만 한다는 단순 규칙보다 작업 방식과 잠금 특성을 이해하는 편이 중요합니다. 데이터베이스 제품과 버전에 따라 온라인 생성 지원 범위가 다르므로 공식 문서를 확인하고, 소요 시간과 취소 계획까지 준비하세요.
인덱스보다 쿼리 구조를 먼저 바꿔야 한다는 의견도 있습니다
Q. 튜닝의 출발점을 인덱스로 잡는 것이 오히려 위험하지 않나요?
한서린: 충분히 타당한 반대 의견입니다. 사용하지 않는 열을 모두 가져오는 조회, 수천 건을 불러온 뒤 애플리케이션에서 20건만 남기는 코드, 페이지가 내려갈수록 느려지는 큰 OFFSET 방식은 인덱스 하나로 감추기 어렵습니다. 필요한 열과 행을 줄이고, 커서 기반 페이지네이션이나 집계 구조를 검토하는 편이 더 큰 개선을 만들 수 있습니다.
반대로 쿼리 재작성만 강조하면 데이터 접근 경로가 없는 상태를 방치할 수 있습니다. 조건과 정렬이 명확하고 반환량도 작지만 적절한 인덱스가 없다면 애플리케이션 코드를 복잡하게 바꾸기보다 인덱스가 정답일 가능성이 큽니다. 핵심은 특정 기법을 믿는 것이 아니라 측정값으로 병목의 층위를 구분하는 것입니다. 코딩의 여러 의미와 활용 맥락은 관련 지식백과 항목도 참고할 수 있습니다.
- 반환하지 않아도 될 열과 행을 먼저 제거합니다.
- N+1 호출이나 반복 조회가 있다면 요청 구조를 고칩니다.
- 필터와 정렬이 안정된 뒤 그 패턴에 맞는 인덱스를 시험합니다.
- 변경 전후의 중앙값뿐 아니라 상위 지연 구간도 비교합니다.
- 읽기 개선이 쓰기 처리량과 운영 복잡성을 해치지 않는지 관찰합니다.
어떤 팀은 캐시가 먼저라고 말하고, 다른 팀은 데이터 모델을 다시 설계해야 한다고 주장합니다. 둘 다 맞을 수 있지만 캐시는 잘못된 쿼리를 숨길 수 있고 모델 변경은 비용이 큽니다. 그래서 좋은 웹개발 튜닝은 “인덱스를 넣을까?”가 아니라 어디에서 몇 행을 왜 읽고 있는가라는 질문으로 끝까지 검증하는 작업입니다.

- 이전글웹개발 API, REST와 GraphQL을 둘 다 써본 차이 26.09.05
- 다음글웹개발 입문, 자바스크립트와 타입스크립트 중 뭘 먼저 배울까? 26.09.03
등록된 댓글이 없습니다.
