유성

DB Index 로 불가능해 역색으로 해결한 성능 개선 본문

기타

DB Index 로 불가능해 역색으로 해결한 성능 개선

백엔드 유성 2026. 1. 7. 17:58

신입으로 입사한지 얼마 되지 않았을 때 과거 배치 문제점을 파악하고,

성능을 개선한 개발에 대해서 작성하는 글이다.

 

1. 주변 서비스가 다운된다.

아침에 출근하여 모니터링 툴을 확인해보면 CPU가 3시간동안 90%를 찍고 다른 배치 서비스가 종종 다운되는 현상이 발생했다.

물론 이중화 시스템이여서 데이터의 이상은 없었고, 인지 후 재기동을 꼭 해주어야만 했다.

 

로직을 확인해보니 아래와 같이 작성이 되어있었고, 수만건을 아래와 같이 실행하고 있었다.

 

// 간단하게 흐름만 작성한 코드

List<MyEntity> entities = repository.findAll();

while (true) {
    String matchingNode = "targetValue";
    List<MyEntity> nextNode = entities.parallelStream()
            .filter(e -> e.nextNode().equals(matchingNode))
            .toList();

    nextProcess(nextNode);
}

 

위 코드에서 확인할 수 있는건 모든 데이터를 가져와서 다음 노드에 대한 값을 이용해 조회를 하는 작업이였다.

성능을 높이고자 parallelStream을 사용한 것으로 보여지는데, 테이블은 총 6개 테이블이며 데이터는 총합 1,000만건 중 일부 테이블의 검색 작업을 이렇게 개발이 되어있었다.

 

2. CPU 사용량을 줄이기 위한 개선

메모리에 대해서는 배치서버 특성상 많이 할당되어있기에 CPU 사용량을 떨어뜨리는게 우선 과제였다.

 

CPU를 떨어뜨리기 위해서도 그렇고 인터페이스로 분리하면서 명확히 보이는 것은 Database 내에서 Select 쿼리를 작성하는 것이 가장 합리적이고 바람직하다고 생각해, 해당 검색 로직을 DB 검색 쿼리로 작성하였다.

SELECT id, next_node, ... FROM node_table_name WHERE next_node = ?;

 

이전 작업의 결과물이 파라미터로 들어가는데 영역을 특정할 수 없는 String 값이므로, 1건 단위 개별 검색을 진행했다.

 

그렇게 6개의 테이블에 대한 쿼리를 모두 작성하고, 꽤나 복잡한 GIS 계산식을 수행하니 CPU 사용률이 3% 이내로 수렴하게 되었다.

그러나 Index를 태웠음에도 하나의 작업에 1초 가량 소요되는게 큰 문제였다.

 

처리량 대비 처리시간을 확인해보니 배치 전체를 완료하는데 25시간이 넘게 걸린다는 결과가 나왔다.

 

왜 로직이 parallelStream으로 작성되었는지 알게되었다.

아마 처음 이러한 개발을 진행했던 분(뵌적은 없지만)은 나와 반대로 DB를 사용했다가 일일 배치에 적합한 속도가 안나와 비동기 처리를 임시 방편으로 했을거라 추측된다.

 

3. DB를 사용하지 않고 성능을 개선

일단 성능 개선에 있어서 DB를 사용하지 않는 방법을 가닥을 잡고 방법을 고안했다.

 

이미 DB에 있는 데이터들이 메모리에 올라와있으므로 이를 활용해보고자 했다.

굳이 List를 순회하면서 찾기보다는 Map을 만들어서 찾는 방법은 어떨까를 생각했고 바로 개발과 테스트를 진행했다.

 

단순히 Map<Id, Instance> 뿐 아니라 Instance 의 특정 값을 사용하는 Map<Instance.nextNode, List<Instance>>

도 만들어야 했으므로 역색인 기법을 사용했고, 이렇게 성능을 개선하고 보니

 

처리시간은 4시간에서 13분으로, CPU 사용률은 90%에서 10%대로 개선이 가능했다.

 

그 당시에는 알지 못했지만, Instance는 그 자체로 값을 갖는게 아닌 객체 참조만 갖고 해당 참조에 데이터가 존재하기 때문에

역색인을 활용하여도 메모리가 크게 늘지 않는 장점이 있었다.

 

4. 만약 다시 돌아가 성능을 개선하게 된다면

이 배치 작업이 단지 값을 얻으면 바로 처리가 완료되는 것은 아니다.

A값을 토대로 B라는 값을 찾고, 이 값을 계산한 후 특정 길이에 해당하지 않으면 B라는 값을 토대로 C라는 값을 찾고 조건에 만족하면 이를 암호화 하는 일련의 재귀형태를 가지고있다.

 

그럼 초기값(seed)를 토대로 계산 과정을 파이프라인화 해놓고 비동기 작업을 수행하도록 했을 것이다.

 

예를 들어 1000개의 값에 대해서 작업을 한다고 가정했을 때,

100개씩 쪼개서 청크로 만들고

청크 작업 한번 진행한 후, 조건에 만족하는 값은 return하고 조건에 만족하지 않는 청크는 작업을 반복하는 식으로 한다.

 

알고리즘으로 보면 너비 우선 탐색 방식에 해당한다.

 

이렇게 만들면 100개에 대한 조회가 한번에 수행되어 select 시간을 획기적으로 줄일 수 있고,

이러한 파이프라인을 2~3개 정도로 비동기로 처리하면 속도를 충분히 끌어올리면서도 CPU 사용량을 줄일 수 있는것이다.

 

마치며

신입 개발자로서 마주했던 이 문제는 '인덱스를 설정했으니 빠를 것이다'라는 막연한 생각이 잘못된 생각인지 알려주는 계기가 되었다.

 

시스템의 병목은 단순히 쿼리 속도뿐만 아니라 네트워크 왕복 비용, CPU의 연산 방식, 그리고 데이터 구조의 설계 등 복합적인 요인에서 발생하는 것이다.