속도 개선을 위한 DB 인덱싱 및 비동기 적용

상황

  • 일별/월별 통계를 보여주는 대시보드가 너무 느리다는 이슈가 발생했다.
  • 해당 조회 대상 테이블을 보니 다수의 로그성 테이블에서 정보를 요청 시 join 및 조회하는 방식의 뷰테이블이었다.
  • 해당 테이블들에는 인덱스가 없어 풀스캔이나 다름 없었다 => 인덱스 생성하기로 한다.
  • 대시보드에는 다수의 위젯이 있는데 위젯별로 그 쿼리를 다 따로 하고 있다. 그래서 뜨는 시간이 느리다 => 비동기 처리한다.

개요

  • DB 인덱싱
    • 인덱스 생성할 테이블을 listup -> 컬럼 선택 -> 인덱싱
  • 비동기 처리
    • 해당 Service의 getDTO 메서드에 비동기 처리

DB 인덱싱

테이블 및 컬럼 파악

  • 대시보드의 위젯별 테이블을 파악한다. image

  • 인덱싱 대상이 될 컬럼을 산정한다.
  • 인덱싱 컬럼 기준
    • 검색 빈도 : 가장 빈번하게 검색되는 컬럼
    • 검색 조건 : WHERE 절/ join에서 자주 사용되는 컬럼
    • 결합된 검색 조건 : 여러개의 컬럼을 결합하여 검색하는 경우 이 조합을 인덱스로 지정
    • 정렬 및 그룹화 : ORDER BY 및 GROUP BY 절에 사용되는 컬럼으로 그룹화 작업 성능 향상
    • 조인 : join에 사용되는 컬럼
    • 카디널리티 : 고유값이 많은 컬럼

인덱스 생성

SELECT * FROM pg_indexes WHERE tablename = '{테이블명}';
  • 테이블에 대한 모든 인덱스 반환
EXPLAIN SELECT * FROM {테이블명} WHERE {컬럼명}={조건}';
  • 테이블의 쿼리 실행 계획 확인 (인덱스 적용되었는지 확인)
CREATE INDEX CONCURRENTLY {인덱스명} ON {테이블명} ({컬럼명});
  • 테이블의 특정 컬럼에 대한 인덱스 생성
  • CONCURRENTLY : 인덱스 생성 작업을 다른 트랜잭션과 병행하여 수행 (중단 없이 인덱스 생성)

비동기 처리

  • Service의 getDTO 메서드에서 CompletableFuture를 사용하여 비동기 처리하도록 한다.
package hdclabs.cspwas.service;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import hdclabs.cspwas.model.dtoAnalysis.FmsTeamDTO;
import hdclabs.cspwas.model.vo.SearchAnalysisVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
@RequiredArgsConstructor
public class AnalysisService extends AbstractService {

    private final FmsTeamService fmsTeamService;

    @Transactional
    public FmsTeamDTO getFmsTeamDTO(SearchAnalysisVO searchAnalysisVO) {
        FmsTeamDTO fmsTeamDTO = new FmsTeamDTO();

        // 위젯1. 작업 지시 발생 건
        CompletableFuture<WorkOrderIssueDTO> workOrderIssueFuture = CompletableFuture.supplyAsync(
            () -> fmsTeamService.getWorkOrderIssueDTO(searchAnalysisVO));

        // 위젯2. 작업 지시 처리율
        CompletableFuture<WorkOrderReqCompleteDTO> workOrderReqCompleteFuture = CompletableFuture.supplyAsync(
            () -> fmsTeamService.getWorkOrderReqCompleteDTO(searchAnalysisVO));

        // 위젯3. 작업 지시 건 별 평균 완료 시간(분)
        CompletableFuture<WorkOrderReqCompleteAvgTimeDTO> workOrderReqCompleteAvgTimeFuture = CompletableFuture.supplyAsync(
            () -> fmsTeamService.getWorkOrderReqCompleteAvgTimeDTO(searchAnalysisVO));

        // 위젯4. 작업 지시 유형별 발생
        CompletableFuture<List<WorkOrderTypeDTO>> workOrderTypeFuture = CompletableFuture.supplyAsync(
            () -> fmsTeamService.getWorkOrderTypeDTOs(searchAnalysisVO));

        // 위젯5. 작업 지시 구분별 발생
        CompletableFuture<List<WorkOrderClassDTO>> workOrderClassFuture = CompletableFuture.supplyAsync(
            () -> fmsTeamService.getWorkOrderClassDTOs(searchAnalysisVO));

        // 위젯6. 유형별 완료 소요 시간
        CompletableFuture<List<WorkOrderTypeAvgTimeDTO>> workOrderTypeAvgTimeFuture = CompletableFuture.supplyAsync(
            () -> fmsTeamService.getWorkOrderTypeReqCompleteAvgTimeDTOs(searchAnalysisVO));

        // 위젯7. 구분별 완료 소요 시간
        CompletableFuture<List<WorkOrderClassAvgTimeDTO>> workOrderClassAvgTimeFuture = CompletableFuture.supplyAsync(
            () -> fmsTeamService.getWorkOrderClassReqCompleteAvgTimeDTOs(searchAnalysisVO));

        // 위젯8. 작업 처리 단계별 시간 추이
        CompletableFuture<List<WorkOrderStepAvgTimeDTO>> workOrderStepAvgTimeFuture = CompletableFuture.supplyAsync(
            () -> fmsTeamService.getWorkOrderStepAvgTimeDTOs(searchAnalysisVO));

        // 위젯9. 알람 종류별 작업 지시 응답
        CompletableFuture<List<AlarmAvgTimeDTO>> alarmTypeResAvgTimeFuture = CompletableFuture.supplyAsync(
            () -> fmsTeamService.getAlarmTypeResAvgTimeDTOs(searchAnalysisVO));

        // 위젯10. 알람 종류별 작업 지시 처리 시간
        CompletableFuture<List<AlarmAvgTimeDTO>> alarmTypeReqCompleteAvgTimeFuture = CompletableFuture.supplyAsync(
            () -> fmsTeamService.getAlarmTypeReqCompleteAvgTimeDTOs(searchAnalysisVO));

        // 모든 CompletableFuture가 완료될때까지 기다린 후 결과 반환 
        CompletableFuture.allOf(workOrderIssueFuture, workOrderReqCompleteFuture, workOrderReqCompleteAvgTimeFuture,
                workOrderTypeFuture, workOrderClassFuture, workOrderTypeAvgTimeFuture, workOrderClassAvgTimeFuture,
                workOrderStepAvgTimeFuture, alarmTypeResAvgTimeFuture, alarmTypeReqCompleteAvgTimeFuture)
            .join();

        fmsTeamDTO.setWorkOrderIssueDTO(workOrderIssueFuture.join());
        fmsTeamDTO.setWorkOrderReqCompleteDTO(workOrderReqCompleteFuture.join());
        fmsTeamDTO.setWorkOrderReqCompleteAvgTimeDTO(workOrderReqCompleteAvgTimeFuture.join());
        fmsTeamDTO.setWorkOrderTypeDTOs(workOrderTypeFuture.join());
        fmsTeamDTO.setWorkOrderClassDTOs(workOrderClassFuture.join());
        fmsTeamDTO.setWorkOrderTypeAvgTimeDTOs(workOrderTypeAvgTimeFuture.join());
        fmsTeamDTO.setWorkOrderClassAvgTimeDTOs(workOrderClassAvgTimeFuture.join());
        fmsTeamDTO.setWorkOrderStepAvgTimeDTOs(workOrderStepAvgTimeFuture.join());
        fmsTeamDTO.setAlarmTypeResAvgTimeDTOs(alarmTypeResAvgTimeFuture.join());
        fmsTeamDTO.setAlarmTypeReqCompleteAvgTimeDTOs(alarmTypeReqCompleteAvgTimeFuture.join());

        return fmsTeamDTO;
    }
...

}

결과 및 TODO

  • 서버 응답시간이 절반으로 감소하였다 (6.04초 -> 3.11초)
  • 다만, 이렇게 요청 즉시 집계하는 방식은 효율적이지 않다. 일별 및 월별 통계만 조회하므로 실시간으로 계속해서 통계낼 필요가 없으며 부하도 크다.
  • 향후 배치 프로그램을 두어 통계 테이블을 구성하고, 그 테이블을 조회할 수 있도록 바꿔야할 것이다.