[Spring] 벌크 계정 생성 시, 병목 지점을 개선해보자

Hocaron·2023년 7월 29일
0

Spring

목록 보기
29/44

10000개의 데이터를 처리하는데 1시간 20분 정도가 걸리고 있다. 클라이언트와 read timeout 설정이 5분이라서 해당 메서드는 @Async 처리를 하였지만, 여전히 백그라운드에서 로직처리에 1시간 20분이 소요되고 있고, 이 시간동안 작업 요청자는 기다려야 한다.

병목지점을 찾아서 개선해보자.

데이터 처리 로직은 다음과 같아요.

  1. CSV 파일 읽기 및 데이터 프로세싱
  2. 아래 로직부터는 청크 단위로 수행(300개)
  3. 데이터 유효성 검증
  4. A 데이터 저장
  5. A 데이터 연관된 B 데이터 저장
  6. A 데이터 암호화
  7. A FeingClient 호출
  8. B FeingClient 호출
  9. C FeingClient 호출
  10. A Kafka 프로듀싱

각 로직에 수행되는 시간

로직수행시간
CSV 파일 읽기 및 데이터 프로세싱(1000개 기준)8,001 ms
데이터 유효성 검증(300개 기준)60,817 ms
A 데이터 저장(300개 기준)6,834 ms
A 데이터 암호화(300개 기준)6,032 ms
A 데이터 연관된 B 데이터 저장(300개 기준)6,160 ms
A FeingClient 호출(300개 기준)12,372 ms
B FeingClient 호출(600개 기준)12,464 ms
C FeingClient 호출12,468 ms
A Kafka 프로듀싱(300개 기준)18,073 ms

속도를 줄여볼 수 있는 지점은 다음과 같아요.

데이터 유효성 검증시, findByXXX 로 단건 검증 수행

현재 데이터 한건당 4건의 select 문이 존재한다. 즉 청크 단위당 1200(4 * 300) 건의 select 문이 존재한다.

❎ 첫번째 시도, 단건 검증 자체를 병렬 처리해보자!

60,817 ms → 8,213 ms

✅ 두번째 시도, findByXXXIsIn를 사용하여 다건 검증을 수행해보자

60,668 ms → 204ms

@DataJpaTest
@ActiveProfiles("local")
public class JpaRepositoryTest {

    @Autowired
    private ActiveMemberRepository activeMemberRepository;

    private static List<String> ids;

    @BeforeAll
    static void setUp() {
        ids = new ArrayList<>();
        IntStream.range(0, 100)
                .forEach(i -> ids.add(RandomStringUtils.randomAlphanumeric(10)));
    }

    @Test
    void findByXXXTest() { // 1sec 688ms
        ids.forEach(id -> {
            activeMemberRepository.findByMemberId(id);
        });
    }

    @Test
    void findByXXXIsInTest() { // 204ms
        activeMemberRepository.findByMemberIdIsIn(ids);
    }
}

속도변화가 있었으나 기존에 단건으로 구현된 검증 로직에서 변경사항이 크다는 단점이 있다...😢

수행된 후 그 다음로직에 영향이 안 가는 부분은 비동기 처리

응답시간이 외부 서비스에 영향을 받는 네트워크 통신 로직들은 다른 로직과 동시에 진행되도록 처리되도록한다.

데이터 적재 및 프로듀싱(약 36초)이 수행되면서 동시에 A FeingClient 호출, B FeingClient 호출, C FeingClient 호출을 처리하여 외부 서비스 호출에 소요되는 시간을 삭제하는 효과를 얻었다.

        List<CompletableFuture<Boolean>> allFutureList = new ArrayList<>();
        aDatas.forEach(registrationNumber -> allFutureList.add(executeForSaveLogs(aDatas)));

-- 엄청 오래걸리는 로직
        CompletableFuture.allOf(allFutureList.toArray(new CompletableFuture[0])).join();
    }

     private CompletableFuture<Boolean> executeForSaveLogs(Data data) {
        return CompletableFuture.supplyAsync(() -> {
                loadCertificateAndScraping(registrationNumber, OrganizationType.STOCK);
            }
            return true;
        }, customExecutor)
        .exceptionally(e -> {
            // 취소 API 가 생기면 해당 로직이 추가될 수 있다.
            return false;
        });
    }

청크 단위로 비동기 처리(1000개 계정 생성)

동기 / 비동기청크 단위소요 시간
동기300개9 m 12.54 s
비동기200개4 m 18.54 s
비동기100개1 m 11.34 s

비약적인 속도 변환가 있었다.

비동기 로직에 300개로 청크되는 단위에도 비동기로 돌릴 수 있었지만, 문제는 요구사항에 하나가 실패하면 전체 롤백이 수행되어져야 하는데, 비동기 처리시에 다른 스레드에서 수행되는 적재 로직들은 transactional 에 의한 롤백이 불가능하다🤔
적재하는 테이블이 많아서 중간에 예외가 발생한 경우, 삭제하는 로직을 추가하는 것이 힘들 것으로 예상한다.

채택한 방법 및 결과

✅ 데이터 유효성 검증시, findByXXX 로 다건 검증 수행

✅ 수행된 후 그 다음로직에 영향이 안 가는 부분은 비동기 처리

✅ 데이터 일괄 적재

청크 단위로 비동기 처리(1000개 계정 생성)
총 소요시간(1000개 기준)

🥳 7 m 25.50 s -> 2m 18.07 s

진행하면서 궁금한 부분

  • 동일 Transactional 내에서 메서드가 끝나고 commit 과 동시에 flush 하면서 쿼리가 나가는데, chunk 하면 쿼리가 동시에 나가지 않는걸까?
    쓰기지연 기능을 통해 flush 시점에 한번에 쿼리가 나가게 된다.

  • 비동기 처리시에 쓰레드 풀(N개)을 지정해놓았을 때, 해당 메서드를 여러번 호출하면 N개 만큼의 쓰레드만 사용될까? 아니면 N * 요청 개수 만큼 사용될까?

  • 적정 쓰레드 풀은 어떻게 책정할 수 있을까?

profile
기록을 통한 성장을

2개의 댓글

comment-user-thumbnail
2023년 7월 29일

좋은 정보 감사합니다

1개의 답글