인덱스 적용을 통한 성능 개선 경험기

최인준·2024년 2월 9일
0
post-thumbnail

서론

DB 인덱스를 이론으로만 접한 상황이었다. 인덱스를 공부하면서 잘 활용하면 성능을 상당 부분 개선 시킬 수 있겠다는 것을 알았다. 그래서 실습을 해보기 위해 이전에 했던 프로젝트 쿼리에 인덱스를 적용시켜보기로 하였다.

프로젝트에 where 조건이 복잡한 쿼리가 없는게 아쉬웠지만 인덱스를 처음 적용해보는 것이기 때문에 오히려 기초적인 부분부터 시작하는 느낌이라 생각하고 실습을 진행해보았다.

이번 포스팅에서는 특정 쿼리에 인덱스를 적용시켜 해당 쿼리를 사용하는 api 테스트를 통해 성능 변화를 보도록 할 것이다.

테스트 환경 세팅

적용시킬 쿼리

인덱스를 적용시켜 볼 쿼리는 다음과 같다. fetch join을 통해 연관관계 매핑이 되어있는 테이블의 컬럼들까지 한번에 가져오는 쿼리다. where조건에는 member_id(PK), status필드가 있고 PK에는 기본 인덱스가 걸려있기 때문에 status 필드에 인덱스를 적용시킬 것이다!

@Query("select w from Waiting w join fetch w.shop "
       + "where w.member = :member and w.status = :status")

테스트 데이터

테스트 할 데이터를 EasyRandom 라이브러리를 통해 생성했다.

각 필드값이 랜덤으로 생성될 때 값을 고정시키거나 범위를 지정해주었다.

객체들 생성 후에는 JdbcTemplate을 통해 batch insert를 해주었다.

테스트 데이터 레코드 수는 300만행이다.

	@Test
    @Rollback(value = false)
    void insertData(){
        int threadCount = 1000000;
        List<Waiting> waitings = new ArrayList<>();
        Member member = memberRepository.findMemberByEmail(EMAIL).orElseThrow();

        Shop savedShop = shopRepository.findAll().get(0);

        Predicate<Field> peopleCountRange = named("peopleCount")
            .and(ofType(int.class))
            .and(inClass(Waiting.class));

        Predicate<Field> waitingNumberRange = named("waitingNumber")
            .and(ofType(int.class))
            .and(inClass(Waiting.class));

        Predicate<Field> remainRange = named("remainingPostponeCount")
            .and(ofType(int.class))
            .and(inClass(Waiting.class));

        EasyRandomParameters parameters = new EasyRandomParameters()
            .excludeField(
                named("id")
                    .and(ofType(Long.class))
                    .and(inClass(Reservation.class))
            )
            .randomize(peopleCountRange, () -> new Random().nextInt(1, 30))
            .randomize(waitingNumberRange, () -> new Random().nextInt(1, threadCount + 1))
            .randomize(remainRange, () -> new Random().nextInt(1, 3))
            .randomize(Member.class, () -> member)
            .randomize(Shop.class, () -> savedShop)

        EasyRandom easyRandom = new EasyRandom(parameters);

        IntStream.range(0, threadCount)
            .parallel()
            .forEach(i -> {
                Waiting waiting = easyRandom.nextObject(Waiting.class);
                waitings.add(waiting);
            });
        
		waitingJdbcRepository.batchInsert(waitings);
    }

실행 계획

테스트를 해보기 전에 explain 을 통해 인덱스 적용 전과 후의 실행계획을 살펴볼 것이다.

인덱스 적용 전 실행 계획

실행계획을 보면 웨이팅 테이블의 데이터를 Full Scan 하고 있다. 인덱스는 걸기 전이기 때문에 당연히 사용 중인 인덱스는 없다고 나온다. (shop 테이블은 join시에 PK에 걸려있는 인덱스를 사용한다.)

이제 다음 쿼리를 통해 인덱스를 생성하여 실행 계획을 다시 살펴볼 것이다.

인덱스는 웨이팅 테이블의 “status” 필드에 건다.

ALTER TABLE waiting ADD INDEX idx_status(status);

인덱스 적용 후 실행 계획

테이블에 인덱스를 적용 후의 실행 계획을 보면 웨이팅 테이블의 1 row만 스캔하는 것을 볼 수 있다.

위에서 인덱스를 걸 때 status에 인덱스를 걸었었는데 실행 계획을 살펴본 쿼리는 status가 “PROGRESS”인 결과를 찾는 쿼리이고 테이블에 해당 값을 만족시키는 레코드는 한개이다. 그래서 인덱스를 건 후에 status 기준으로 정렬이 되고 status가 “PROGRESS”인 레코드 하나를 스캔한다.

실행 계획을 본 결과로는 인덱스를 건 후의 성능이 더 개선될 것이라는 것을 알 수 있다.

이제 성능 테스트를 실제로 진행해보면서 성능이 어느 정도 개선되는지 볼 예정이다🤩

성능 비교

성능 비교는 해당 쿼리를 사용하는 API를 통해 간단히 테스트 해 볼 예정이다.

활용한 성능 툴은 JMeter이다!

스레드 그룹은 다음과 같이 설정하였다.

총 50개의 스레드가 10초동안 요청을 보낼 것이다. 즉, 총 500개의 요청이 간다.

먼저 인덱스 적용 전의 성능이다.

데이터가 300만건으로 많긴 하지만 TPS가 상당히 낮다..

이 정도의 데이터가 들어올 일은 없지만 대용량의 트래픽이 발생한다면 사용자 측의 답답함은 늘 것이다 😩

(참고로 평균 컬럼의 값은 응답시간으로 단위는 ms이다.)

이제 제거했던 인덱스를 다시 걸어주고 성능 테스트를 진행해보자.

인덱스 적용 후의 성능이다.

TPS가 50배 넘게 증가했다😃

물론 극단적인 상황 덕분에 인덱스가 더 빛을 발한 부분도 있다.

애초에 쿼리가 결과를 만족하는 값 단 1건을 찾는 쿼리고 테스트 데이터를 300만건으로 설정하였기 때문에 기존에 풀스캔을 하던 상황에서 1개의 레코드만 스캔하므로 극단적인 성능 차이를 보이기는 하였다.

응답시간도 압도적으로 줄었다.

중요한 것은 이러한 상황이 충분히 있을 수 있다. 당장 이 프로젝트만 하더라도 있는 것이기에 트래픽이 많은 서비스에서는 발생할 수 있는 상황이고 이러한 성능 개선도 가능하다고 생각한다.

결론

이번 포스팅에서는 기초적인 인덱스 개념만 있다면 적용해볼 수 있는 실습을 경험했다.
쿼리를 보고 적당한 인덱스를 걸어주어 성능이 개선되었는데 이러한 이유 하나만으로 인덱스를 막 걸면 안된다.

인덱스에 대한 설명은 하지 않았지만 인덱스를 걸어주게 되면 테이블에 정렬 작업이 수행된다.
근데 만약 쓰기 작업이 많은 테이블이라면 데이터가 들어올 때마다 인덱스에 대한 작업을 해주기 때문에 오히려 성능에 부하가 걸릴 수 있다. 이 외에도 많은 고려사항이 있으며 각자의 상황에 맞게 인덱스를 조심히 활용해야 한다.

어쨋든 이번 실습에서의 성능 개선은 성공적이었다. 정말 단순한 작업이긴 했지만 최소한의 자원으로 최대한의 결과를 이끌어내는 것도 소프트웨어 공학에서 강조하는 중요한 사고이기 때문에 의미 있는 실습이었다😃

성능 정리

쿼리 실행 성능은 각각 execution time, fetch time이다.

인덱스 적용 전인덱스 적용 후
쿼리 실행667ms, 57ms17ms, 28ms
TPS8.4/s490.2/s

0개의 댓글