조회 성능 개선 지표
우선 네트워크 속도 차이 및 제 개인 맥북으로 테스트를 하는 것이기 때문에 성능개선에 대해서 차이가 존재함을 말씀드립니다.
🚀 성능 개선 상황
데이터가 쌓여 수천, 수만건이 되는 경우 특정 필터들을 걸어 데이터를 조회하는 것은 굉장히 비효율적입니다. 결국은 Full-scan 을 통해서 데이터 목록을 조회하기 때문입니다.
이는 실제 사용자 경험(UX)에 직접적인 영향을 끼치는 부분입니다. 따라서 이를 개선하며 테스트하고 최종적으로는 어떤 방식을 사용했는지 작성해보겠습니다.
✅ 인덱스
모임 목록을 조회할 때는 몇가지 필터를 적용할 수 있습니다. 사용하는 필터는 category_id, is_deleted, status, meetingDate, title등을 필터로 걸 수 있습니다. 위 작성한 상황처럼 데이터의 양이 많아지는 경우 점점 조회의 효율성은 떨어질 것이기 때문에 인덱스를 활용하여 개선 및 테스트를 진행했습니다.
⚡️ Index 정의
인덱스는 데이터를 가지고 있는 것이 아닌 데이터의 위치를 가지고 있는다라고 생각하면 좋을 것 같습니다.
이는 인덱스 (Mysql기반)는 내부적으로 별도의 자료구조 형태(B+Tree)를 기반으로 만들어져있기 때문입니다.
따라서 원하는 데이터를 바로 찾을 수 있게 됩니다.
⚡️ Index 적용
우선 테이블에 인덱스를 적용시킵니다. 위 조건에서 title 은 Like 구문을 통해 조회하게 되는데 Like 구문은 인덱스를 이용할 수 없기 때문에 제외합니다.
- Meeting DDL Index 추가
INDEX idx_meeting_deleted_category_status_date (is_deleted, category_id, status, meeting_date)
- Index 적용 후 QueryDsl을 통한 조회 코드
- code
@Override
public Page<Meeting> getMeetings(String title, LocalDateTime meetingDate, MeetingStatus status, Integer categoryId,
Pageable pageable) {
BooleanBuilder builder = new BooleanBuilder();
if (!title.trim().isEmpty()) {
builder.and(meeting.title.like("%" + title.trim() + "%"));
}
if (meetingDate != null) {
LocalDate date = meetingDate.toLocalDate();
LocalDateTime start = date.atStartOfDay();
LocalDateTime end = start.plusDays(1);
builder.and(meeting.meetingDate.goe(start).and(meeting.meetingDate.lt(end)));
}
if (status != null) {
builder.and(meeting.status.eq(status));
}
if (categoryId != null) {
builder.and(meeting.categoryId.eq(categoryId));
}
builder.and(meeting.isDeleted.eq(false));
List<Meeting> meetingContent = queryFactory
.selectFrom(meeting)
.where(builder)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(meeting.meetingDate.desc())
.fetch();
Long total = Optional.ofNullable(queryFactory
.select(meeting.count())
.from(meeting)
.where(builder)
.fetchOne()).orElse(0L);
return new PageImpl<>(meetingContent, pageable, total);
}
⚡️ 성능 비교
데이터의 양이 N만건으로 늘어나고 최악의 상황, 특정 인기 모임들이 많이 개설되는 경우 등을 고려하여 TPS를 높게 잡았습니다.
초당 요청 1000, 총 데이터 50000건 1분 기준 인덱스 vs 기본 조회 테스트 - 피크 테스트
📄 테스트 결과 사진
- 인덱스 미적용
- 인덱스 적용
📄 결과 비교표
Average 1443 ms 732 ms 약 49% 감소
Median | 1246 ms | 665 ms | 약 47% 감소 |
90% Line | 1894 ms | 1029 ms | 약 46% 감소 |
95% Line | 2402 ms | 1250 ms | 약 48% 감소 |
99% Line | 5814 ms | 2404 ms | 약 59% 감소 |
Maximum | 6425 ms | 3153 ms | 약 50% 감소 |
Error % | 4.80% | 2.28% | 약 50% 감소 |
Throughput | 669.5/sec | 1341.1/sec | 2배 증가 |
⚡️ 결론
인덱스를 걸었을 때가 평균 속도부터 처리량 까지 모두 두배가까이 개선되었습니다. 그러나 인덱스를 사용한다는 것은 추가적인 저장 공간이 필요한 것이며, 변경 시 index도 갱신을 해야하기 때문에 단점 또한 존재. 또한 title 필터도 존재하기 때문에 우리의 프로젝트에서는 인덱싱만을 통한 성능개선이 항상 된다고 표현할 수 없음.
✅ Elasticsearch
Index 를 사용하여 성능 개선을 시켰지만 현재 이용하는 필터에는 title 이 실제로는 포함이 되어 있습니다. 물론 복합인덱스에 포함하지는 않았지만 이 또한 처리할 수 있다면 더욱 성능 개선을 시킬 수 있을 것이라 판단했습니다.
⚡️ Elasticsearch 정의
Elasticsearch란 오픈소스 분산 검색엔진이며, 대용량 데이터를 빠르게 검색하고 분석할 수 있습니다. Elasticsearch는 Lucene을 기반으로 만들어졌으며, Lucene은 역색인을 제공하기 때문입니다.
⚡️ 역색인 정의
기존에 주로 사용하던 관계형 데이터베이스는 단방향 색인 방식을 사용합니다. 특정 검색어가 있고 이를 찾기 위해서는 모든 문서를 확인하여 존재하는지 검색을 해야합니다. 한마디로 전체 검색을 해야 한다는 의미가 됩니다. 그러나 역색인은 value-key 방식으로 데이터 별로 어떤 곳에 존재하는지를 바로 파악할 수 있게 됩니다.
⚡️ Elasticsearch 쿼리 코드
@Repository
@RequiredArgsConstructor
public class MeetingElasticCustomRepositoryImpl implements MeetingElasticCustomRepository {
private final ElasticsearchOperations elasticsearchOperations;
@Override
public Page<MeetingDocument> getMeetings(String title, LocalDateTime meetingDate, MeetingStatus status,
Integer categoryId, Pageable pageable) {
Criteria criteria = new Criteria();
if (title != null && !title.isBlank()) {
criteria = criteria.and("title").contains(title);
}
if (meetingDate != null) {
LocalDateTime start = meetingDate.toLocalDate().atStartOfDay();
LocalDateTime end = start.plusDays(1);
criteria = criteria.and("meetingDate").between(start, end);
}
if (status != null) {
criteria = criteria.and("status").is(status.name());
}
if (categoryId != null) {
criteria = criteria.and("categoryId").is(categoryId);
}
CriteriaQuery query = new CriteriaQuery(criteria, pageable);
SearchHits<MeetingDocument> searchHits =
elasticsearchOperations.search(query, MeetingDocument.class);
List<MeetingDocument> content = searchHits.getSearchHits()
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
return new PageImpl<>(content, pageable, searchHits.getTotalHits());
}
}
코드 및 용어 설명
- Criteria : 검색 조건 빌더 객체
- title, meetingDate, status, categoryId가 존재하는 경우 빌더에 조건 추가
- CriteriaQuery query : 조건(criteria)과 페이징(pageable)을 묶어서 Elasticsearch에 전달할 쿼리 객체 생성
- SearchHits<MeetingDocument> searchHits : elasticsearchOperations.search() 실행
- List<MeetingDocument> content : searchHits의 결과를 List 형식으로 변환
⚡️ 성능 비교
데이터의 양이 N만건으로 늘어나고 최악의 상황, 특정 인기 모임들이 많이 개설되는 경우 등을 고려하여 TPS를 높게 잡았습니다.
초당 요청 50(TPS), 총 데이터 10000건 1분 기준 인덱스 vs Elasticsearch 테스트 (1)
📄 테스트 결과 사진
- Index
- Elasticsearch
📄 결과 비교표
구분 인덱스(DB) Elasticsearch 설명
# Samples | 4,741 | 11,964 | ES가 2.5배 더 많은 요청 처리 |
평균 응답시간 (Average) | 1,893 ms | 770 ms | ES가 59.3% 더 빠름 |
중앙값 (Median) | 1,443 ms | 279 ms | ES가 80.7% 더 빠름 |
90% Line | 4,012 ms | 1,772 ms | ES가 55.8% 더 빠름 |
95% Line | 4,958 ms | 3,612 ms | ES가 27.1% 더 빠름 |
99% Line | 7,138 ms | 7,039 ms | 거의 동일 |
처리량 (Throughput) | 26.3/sec | 64.6/sec | ES가 145.6% 더 높음 (약 2.5배) |
Received KB/sec | 116.27 | 280.09 | ES가 141% 더 많음 |
Sent KB/sec | 9.26 | 22.13 | ES가 139% 더 많음 |
초당 요청 1000(TPS), 총 데이터 50000건 1분 기준 인덱스 vs Elasticsearch 테스트 (2) - 피크 대비 테스트
📄 테스트 결과 사진
- Index
- Elasticsearch
📄 결과 비교표
Average (ms) 4,013 2,485 ES가 38.1% 더 빠름
Median (ms) | 3,706 | 2,352 | ES가 36.5% 더 빠름 |
90% Line (ms) | 6,683 | 3,661 | ES가 45.2% 더 빠름 |
95% Line (ms) | 7,490 | 4,154 | ES가 44.5% 더 빠름 |
99% Line (ms) | 8,393 | 6,507 | ES가 22.5% 더 빠름 |
Min (ms) | 15 | 5 | ES가 더 빠름 |
Max (ms) | 10,829 | 6,882 | ES가 더 빠름 (36% 낮음) |
Error % | 6.72% | 3.85% | ES가 더 안정적 (오류율 낮음) |
Throughput | 244.5/sec | 395.1/sec | ES가 61.6% 더 높은 처리량 |
Received KB/sec | 1,207.24 | 1,942.63 | ES가 더 많은 데이터 수신 |
Sent KB/sec | 103.55 | 174.36 | ES가 더 많은 데이터 송신 |
초당 요청 1000, 총 데이터 50000건 1분 기준 인덱스 vs Elasticsearch 테스트 (3) - 피크 대비 테스트
📄 테스트 결과 사진
- Index
- Elasticsearch
📄 결과 비교표
Average (ms) 2,476 1,983 ES가 20% 더 빠름
Median (ms) | 2,306 | 1,941 | ****ES가 **15.8%**더 빠름 |
90% Line (ms) | 3,195 | 2,528 | ****ES가 21% 더 빠름 |
95% Line (ms) | 3,661 | 2,753 | ****ES가 **24.8%**더 빠름 |
99% Line (ms) | 5,742 | 2,941 | ****ES가 **50%**더 빠름 |
Min (ms) | 17 | 1 | ES가 더 빠름 |
Max (ms) | 7,267 | 3,145 | ****ES가 더 빠름 (56% 낮음) |
Error % | 4.13% | 3.18% | ES (더 안정적) |
Throughput | 399.2/sec | 497.1/sec | ****ES가 24.5% 더 높은 처리량 |
Received KB/sec | 1,997.29 | 2,452.10 | ES 22.7% 더 많은 데이터 수신 |
Sent KB/sec | 173.05 | 219.99 | ES 27.1% 더 많은 데이터 송신 |
⚡️ 결론
인덱스만을 사용하여 성능 개선을 했을 때도 기본적인 조회보다 2배 가까이 성능개선을 할 수 있었는데, Elasticsearch 적용 이후 30%~50%가까이 대부분의 지표에서 개선이 되는 것을 확인 할 수 있었습니다. 물론 Elasticsearch를 사용한다는 것은 그만큼 비용을 투자한다는 것이고 데이터 정합성 오류가 발생하지 않도록 유의해야 합니다.
또한 인덱스를 사용하는 것 또한 리소스를 사용하는 것입니다. 따라서 정말 성능 저하가 발생하는 곳, 성능 개선이 필수적인 부분을 잘 정의하고 사용해야 함을 알게 되었습니다.