Batch를 이용한 데이터 처리 최적화: 30% 속도 개선

YuJun Oh·2024년 11월 15일
0

개발을 진행하다 보면 기존에 작성된 코드가 특정 작업에서 시간이 많이 걸리거나, 시스템 성능에 부담을 주는 경우를 자주 접합니다. 최근 프로젝트에서 실제로 겪었던 사례를 바탕으로, 성능 최적화를 어떻게 접근하고 구현했는지 간략히 공유하려 합니다. 해당 최적화를 통해 1,270ms에서 882ms로 실행 시간을 줄일 수 있었습니다.


1. 문제 상황

기존 코드에서 사용자의 설정을 업데이트하는 메서드 updateUserSettings가 매우 비효율적으로 동작하고 있었습니다. 문제를 살펴본 결과, 다음과 같은 비효율적인 부분들이 발견되었습니다:

  1. 데이터베이스 접근 과다
    사용자 설정 값을 업데이트할 때 설정 항목마다 별도로 DB에서 데이터를 조회하고 처리하고 있었습니다. 이는 대량의 데이터가 있는 경우, 각 항목마다 쿼리를 실행해 성능 병목이 발생할 수 있는 구조였습니다.

  2. 비효율적인 데이터 필터링
    기존의 설정 값과 업데이트될 설정 값을 비교하여 실제 변경 사항만 필터링하는 과정이 분산되어 처리 시간이 오래 걸렸습니다.

  3. 단일 처리 방식
    설정 값을 업데이트하고, 관련 패턴 규칙을 수정하는 작업이 개별적으로 수행되면서 작업이 비효율적으로 나눠져 있었습니다.


2. 최적화 목표

  • 데이터베이스 호출 최소화: 데이터베이스 접근 횟수를 줄이고, 한 번의 호출로 필요한 데이터를 가져오도록 최적화합니다.
  • 필터링 효율화: 변경된 설정 값만 처리하여 불필요한 작업을 제거합니다.
  • 배치 처리 도입: 대량의 업데이트 작업을 한 번에 처리하도록 배치 처리 방식을 도입합니다.

3. 최적화 과정

Step 1: 기존 구조 분석

먼저 기존 코드의 동작 방식을 철저히 분석하고, 각 단계에서 실행 시간이 얼마나 소요되는지 측정했습니다. 로그를 통해 특정 단계에서 지연이 발생하는 원인을 파악한 결과, 데이터베이스 호출과 필터링 과정에서 병목이 발생하고 있음을 확인했습니다.

Step 2: 데이터베이스 호출 최적화

기존에는 각 설정 항목마다 개별적으로 데이터를 조회하는 방식이 사용되었습니다. 이를 개선하기 위해 한 번의 쿼리로 모든 설정 데이터를 가져오고, 메모리에서 효율적으로 매핑 및 처리하도록 수정했습니다.

Map<String, List<TblSetCfgUser>> existingSettings = userSettingMapper
    .selectUserSettingsByUserIdAndHospitalCode(userId, hospitalCode)
    .stream()
    .collect(Collectors.groupingBy(TblSetCfgUser::getCfgCd));

이 방식으로 DB 호출을 단 한 번으로 줄이고, 메모리 내에서 데이터를 키로 매핑하여 빠르게 접근할 수 있게 만들었습니다.


Step 3: 변경 사항 필터링 개선

모든 설정 값을 업데이트하는 대신, 기존 값과 비교하여 변경된 값만 업데이트하도록 개선했습니다. 이 과정에서 streamfilter를 활용하여 변경된 설정 값만 효율적으로 추려냈습니다.

List<TblSetCfgUser> updatedSettings = userSettingList.stream()
    .filter(configData -> {
        List<TblSetCfgUser> existingSettingList = existingSettings.get(configData.getCfgCd());
        if (existingSettingList == null || existingSettingList.isEmpty()) return false;
        return existingSettingList.stream()
            .anyMatch(existingSetting -> !existingSetting.getSetVal().equals(configData.getSetVal()));
    })
    .flatMap(configData -> existingSettings.get(configData.getCfgCd()).stream()
        .map(originalSetting -> mapStruct.toUpdateSettingEntity(originalSetting, configData)))
    .toList();

이 과정은 불필요한 데이터베이스 호출 및 메모리 연산을 크게 줄여주는 효과가 있었습니다.


Step 4: 배치 처리 도입

개별적으로 업데이트를 처리하던 기존 방식을 배치 처리로 전환하여 성능을 대폭 개선했습니다. SqlSession을 활용하여 여러 업데이트 작업을 한 번에 처리함으로써 데이터베이스와의 통신 횟수를 줄였습니다.

try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
    TblSetCfgUserMapper mapper = sqlSession.getMapper(TblSetCfgUserMapper.class);
    for (TblSetCfgUser setting : updatedSettings) {
        mapper.updateValue(setting);
    }
    sqlSession.flushStatements();
}

Step 5: 동적 패턴 규칙 업데이트 최적화

동적 패턴 규칙 업데이트 역시 기존에는 설정 항목마다 개별적으로 처리되었으나, 이를 배치 방식으로 개선하여 처리 시간을 단축했습니다.


4. 성능 비교

최적화를 적용한 후, 다음과 같은 성능 개선이 있었습니다:

버전실행 시간
최적화 이전1,270ms
최적화 이후882ms

DB 호출 최소화, 변경 사항 필터링, 배치 처리 등을 통해 약 30%의 성능 개선을 달성할 수 있었습니다.


5. 최적화의 교훈

  1. 병목 구간을 찾는 것부터 시작하라
    모든 코드를 무작정 최적화하려고 하면 비효율적입니다. 성능 병목 구간을 정확히 파악하고, 그 부분을 집중적으로 개선해야 합니다.

  2. DB 호출은 최소화하라
    데이터베이스와의 통신은 대부분의 시스템에서 가장 비용이 큰 작업입니다. 가능하면 한 번의 호출로 필요한 데이터를 모두 가져오는 것이 중요합니다.

  3. 배치 처리의 중요성
    대량의 작업은 배치로 처리하는 것이 성능 개선에 큰 도움을 줍니다. 특히 데이터베이스 업데이트와 같은 작업에서는 배치 처리가 필수적입니다.


6. 마무리

이번 최적화는 상대적으로 간단한 방법으로도 실행 시간을 크게 줄일 수 있음을 보여준 사례입니다. 성능 최적화는 작은 개선들의 조합으로 이루어지며, 문제를 명확히 이해하고 적절히 대응하는 것이 핵심입니다.

0개의 댓글

관련 채용 정보