일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- 주식
- AWS
- 레디스 동시성
- 깊게 생각해보기
- 아키텍쳐 개선
- 카카오
- 트랜잭샨
- 추천 검색 기능
- gRPC
- 완전탐색
- 쿠키
- 셀러리
- spring event
- 디버깅
- next-stock
- jwt 표준
- BFS
- JPA
- 크롤링
- 누적합
- 검색어 추천
- 객체지향패러다임
- 몽고 인덱스
- piplining
- 구현
- 결제서비스
- docker
- 프로그래머스
- 이분탐색
- 백준
- Today
- Total
코딩관계론
대규모 배치 이후, 느려진 검색 성능 본문
데이터를 적재하기 위해 대규모 배치 작업을 실행한 후, 검색 쿼리의 성능이 현저히 떨어지는 문제가 발생했습니다. 또한 다양한 데이터 오류도 함께 드러났습니다.
이 중 가장 시급한 이슈는 쿼리 성능이었는데, 아래와 같이 특정 문서를 찾는 쿼리가 4초 가까이 걸리는 상황을 확인했습니다.
1. 쿼리 실행 계획(Explain) 확인
문제가 되는 쿼리의 실행 계획을 확인하기 위해, 다음과 같은 명령어(explain("executionStats"))를 사용했습니다:
이를 해결하기 위해서 먼저 어떻게 쿼리가 실행되고 있는지 궁금하여 다음과 같이 질의하하여 쿼리 실행 계획을 알아봤다. 아래의 명령어를 사용하면 쿼리의 실행계획을 분석할 수 있다.
db.news.find({'stockCode': '437730', 'isRelated': true}).explain("executionStats")
//결과
{
"executionSuccess": true,
"nReturned": 8, //총 8개가 반환
"executionTimeMillis": 3374, //실행시간 약3초
"totalKeysExamined": 0,
"totalDocsExamined": 102457,
"executionStages": {
"stage": "COLLSCAN", //풀스캔을 의미함
"filter": { "$and": [ {"isRelated": true}, {"stockCode": "437730"} ] },
"docsExamined": 102457,
...
}
}
위의 중요한 필드 결과를 해석을 해보면
- stage가 COLLSCAN → 전체 컬렉션을 스캔(Full Collection Scan)했다는 의미
- totalDocsExamined = 102,457 → 전체 10만여 개 문서를 훑어봤다
- nReturned = 8 → 최종적으로 반환된 문서는 8개
- executionTimeMillis = 3,374ms(약 3.3초) → 쿼리 한 번에 3초 이상이 걸린 셈
결국, 인덱스 없이 모든 문서를 훑었기 때문에 성능이 크게 떨어진 것이었습니다.
2. 인덱스 설계로 풀 스캔 방지
MongoDB 또한 RDB와 마찬가지로 B-Tree 기반 인덱스를 사용합니다. 단일 필드 인덱스, 복합(Compound) 인덱스, 멀티키(Multi-key) 인덱스 등을 지원합니다.
- 단일 인덱스: { stockCode: 1 } 처럼 하나의 필드에 대한 인덱스
- 복합 인덱스: { stockCode: 1, isRelated: 1, isThema: 1 } 처럼 여러 필드를 조합
- 멀티키 인덱스: 배열 필드에 인덱스를 걸면 자동으로 생성되는 형태
2.1. 복합 인덱스 설계의 중요성
특히 복합 인덱스 사용 시, 선두(leading) 필드를 어떻게 배치하느냐가 매우 중요합니다.
예를 들어 (stockCode, isRelated, isThema) 순으로 인덱스를 구성할지, 아니면 (isRelated, stockCode, isThema) 순으로 구성할지는 쿼리 성능에 큰 영향을 미칩니다.
예시: (stockCode, isRelated, isThema)
- 쿼리에서 stockCode를 가장 많이 사용할 것으로 가정하고, **선두 필드를 stockCode**로 설정
- db.news.find({ stockCode: "437730" }) 처럼 stockCode만 사용하는 쿼리는 인덱스를 잘 활용하여 0초(또는 매우 짧은 시간) 만에 결과가 나옵니다.
{
"executionTimeMillis": 0, // 1ms 미만
"totalKeysExamined": 8,
"totalDocsExamined": 8,
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": {
"stockCode": 1,
"isRelated": 1,
"isThema": 1
},
...
}
}
예시: (isRelated, stockCode, isThema)
- 동일하게 db.news.find({ stockCode: "437730" })를 날려도, 선두 필드가 isRelated이기 때문에 인덱스 효율이 떨어져 여전히 3초 정도가 걸릴 수 있습니다.
{
"executionSuccess": true,
"nReturned": 8,
"executionTimeMillis": 3374,
"totalKeysExamined": 0,
"totalDocsExamined": 102457,
"executionStages": {
"stage": "COLLSCAN",
"filter": {"stockCode": "437730"},
"docsExamined": 102457,
...
}
}
이유:
MongoDB의 복합 인덱스는 내부적으로 (a, b, c) 순으로 B-Tree를 구성합니다. 선두 필드 a 값이 주어져야 인덱스가 빠르게 범위를 찾을 수 있습니다. 만약 b만 사용한다면, 인덱스가 “어디서부터 스캔해야 할지” 알기 어려워 풀 스캔에 가깝게 동작하게 됩니다.
따라서 쿼리에 가장 자주 등장하는 필드나 검색 범위를 가장 효과적으로 좁힐 수 있는 필드를 복합 인덱스의 선두로 배치하는 것이 일반적입니다.
3. Partial Index & Sparse Index
MongoDB에서는 인덱스 생성 시, 특정 조건을 만족하는 문서만 인덱싱하도록 할 수 있습니다.
- Partial Index: partialFilterExpression을 통해, 예를 들어 { isRelated: true }인 문서에만 인덱스를 생성
- Sparse Index: 필드가 존재하지 않는 문서는 인덱스에서 제외
예시로, isRelated: true인 문서만 인덱스를 생성하려면:
db.news.createIndex(
{stockCode: 1, isRelated: 1, isThema: 1},
{partialFilterExpression: {isRelated: true}}
);
이렇게 하면, 실제 필요한 문서만 인덱싱해서 인덱스 크기를 줄이고, 쓰기(Insert/Update) 시 불필요한 인덱스 업데이트를 줄일 수 있습니다.
단, 주의할 점:
- partialFilterExpression 조건에 해당하지 않는 문서를 조회하는 쿼리에서는 이 인덱스를 사용할 수 없어, 풀 스캔이 발생할 수 있습니다.
- 즉, Partial Index는 해당 조건의 쿼리를 최적화하는 데만 쓰이는 인덱스입니다.
4. 최종 결과: 밀리초 단위로 빨라진 쿼리
인덱스 설계(복합 인덱스 + Partial Index)를 통해, 아래처럼 쿼리 실행 시간이 수 초에서 1ms 미만으로 개선되었습니다.
{
"executionSuccess": true,
"nReturned": 8,
"executionTimeMillis": 0,
"totalKeysExamined": 8,
"totalDocsExamined": 8,
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"indexName": "stockCode_1_isRelated_1_isThema_1",
"isPartial": true
}
}
결론
- 배치 이후 쿼리 성능이 급격히 떨어졌다면, 인덱스 부재 혹은 잘못된 인덱스 설계가 원인일 가능성이 큽니다.
- MongoDB의 복합 인덱스에서는 **선두 필드(leading field)**가 쿼리 성능을 결정하는 데 매우 중요한 역할을 합니다.
- explain("executionStats")로 COLLSCAN 여부, docsExamined(스캔 문서 수), executionTimeMillis(실행 시간) 등을 확인해 인덱스가 잘 활용되는지 분석해야 합니다.
- 상황에 따라 Partial Index나 Sparse Index를 사용해 인덱스 크기와 쓰기 부담을 낮출 수도 있으나, 해당 조건 이외의 문서를 조회 시에는 인덱스를 사용할 수 없으니 설계 시 주의가 필요합니다.
이러한 과정을 통해, 쿼리 시간이 수 초에서 수 ms 단위로 크게 단축되는 성능 개선 효과를 기대할 수 있습니다.
따라서 3초에서 1ms로 약 99%에 달하는 성능 개선을 성공했습니다.
'개발 > Hot-Stock' 카테고리의 다른 글
수익화를 위한 쇼 - (에드센스) (0) | 2025.02.24 |
---|---|
사담 (0) | 2025.02.22 |
컨슈머 랙을 줄이기 위해서 파티션을 늘렸더니 동시성 이슈가 발생해버렸다 (1) | 2025.02.06 |
주식 테마 뉴스 정제: 벡터 DB와 RAG로 노이즈 아웃 (0) | 2025.01.29 |
크롤링 IP 제한, 이렇게 뚫었다! 나만의 해결 여정 (0) | 2025.01.21 |