테스트를 한 배경
저는 모임을 가입할 수 있는 팀프로젝트를 만들고 있으며 그 중 모임을 구현하고 있습니다.
소모임 같은 어플을 생각해봤을 때 가장 많이 사용되는 기능은 모임을 조회하는 로직이겠죠.
저 또한 그 어플을 예전에 몇번 사용해보며 농구 모임을 찾았던 기억이 있습니다.
그렇다면 그 부분에 대해서 어떤 식으로 조회를 구현했는지와 성능테스트 결과를 비교해보며 왜 그런 선택을 했는지 알아보도록 하겠습니다.
우선 기존 조회로직입니다. 모임 조회를 한다고 할 때 간단히만 생각해봐도 조건들이 좀 있을 겁니다. 모임의 제목 조회, 상태, 카테고리 등등 이 존재할 겁니다. 그렇기 때문에 컴파일러에서 에러를 잡을 수 있고 가독성을 잡기 위해서 queryDsl을 사용했습니다.
@Override
public Page<Meeting> findMeetings(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);
}
로직 구현을 위해 사용된 queryDsl 코드입니다. 반환값은 페이지네이션을 위해서 PageImpl 객체를 통해 반환하고 있습니다.
만약 실서비스라고 가정을 하고, 사용자가 수천 수만명이라고 생각을 해봅시다.
그런 경우에 현재 mysql을 사용하기 때문에 full scan을 할 것이고 그에 따른 성능 저하가 따라올 것입니다. 또한 기존은 인덱스가 없기 때문에 정렬을 할 때에도 정렬용 임시 테이블을 사용할 것이기에 더더욱 느려질 겁니다.
초당 500명이 요청한다고 가정하여 jmeter로 테스트해보겠습니다.
인덱스 X 기본 조회 query 테스트
위 로직을 통해 조회를 한다고 했을 때 현재 es등 외부툴은 사용하지 않고 있기에 어떤 방식으로 조회 성능을 개선할 수 있을까요? 바로 인덱스를 사용하는 것입니다.
물론 인덱스를 마구잡이로 사용하는 것은 오히려 성능면에서 마이너스가 될 수 있습니다. 변경이 자주되는 것을 써서도 안되구요. 또한 인덱스를 사용하더라도 like와 같은 문법은 걸리지 않기 때문에 잘 확인해서 걸어야합니다.
INDEX idx_meeting_deleted_category_status_date (is_deleted, category_id, status, meeting_date)
그래서 생각한 인덱스는 위와 같은 복합인덱스입니다. is_deleted는 항상 false를 통해서 조회하기 때문에 필요하며, category_id는 실사용자가 조회한다고 생각할 때, 관심사 위주로 검색을 할 것이며 id를 통해 like가 아닌 equal한 값을 찾기에 필요합니다.
status는 모집중인 값을 찾는 사람들이 많겠죠? 물론 모집완료된 로직 또한 존재하기 때문에 필터가 필요합니다. meeting_date 또한 equal 동일한 날짜를 찾는 것이기에 필요합니다.
그렇다면 위와 같은 복합인덱스를 걸고 성능 테스트를 진행해보겠습니다. 기존 테스트와 조건은 동일합니다.
인덱스 O 기본 조회 query 테스트
테스트 결과를 표로 한번 비교해보겠습니다.
항목 인덱스 없음 인덱스 있음 향상
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배 증가 |
인덱스를 걸었을 때가 평균 속도부터 처리량 까지 모두 두배가까이 개선되었습니다.
물론 추후 es를 도입해볼 예정이지만 그 전에 인덱스를 통해 성능 개선을 이뤄냈고 추후 es를 도입한다면 그 또한 성능 차이를 비교하여 대규모 트래픽 발생 시, 특히나 요청이 가장 빈번히 일어나는 조회 로직에서의 성능개선하는 방법과 타당성을 확인해보겠습니다.