MongoRepository를 통한 대량 데이터 삽입시 발생한 이슈 #1

LSM ·2022년 2월 2일
0

1. 발생 배경

프로젝트를 진행하면서 5만개 이상의 메타데이터를 삽입할 필요가 생겼다.
기존의 메타데이터 삽입 과정은 아래와 같았다.

  • 5개의 메타데이터를 저장하기 위해서는 클라이언트 단에서 5번의 삽입 api를 호출하여 진행하였다.
  • 서버는 개별적인 메타데이터를 MongoRepository를 통해 save하는 형식으로 진행하였다.

위 과정에 따라 5개의 메타데이터를 저장하기 위해서는 총 5번의 api 호출이 5만개이면, 5만번의 api 호출이 발생한다.

그러다보니, 클라이언트에서 5만개의 메타데이터를 삽입했을 때 Request에 대한 작업이 중단되는 상황이 발생했다.

즉 첫번째 문제는 너무 많은 요청이었다.

WAS와 서블릿 관련하여 정리했던 글에서 작성하였듯이 클라이언트의 요청은 스레드를 생성하여 처리한다. 그런데 서버에 5만번이나 접근해 호출하니 프로세스에서 최대로 처리할 수 있는 스레드의 크기를 넘어 발생한 문제 인 것 같다..

두번째 문제는 Service 단에서 처리하는 Transaction처리이다.

지금과 같은 구조로는 하나의 메타데이터를 처리하기 위해 하나의 트랜잭션으로 관리가 되는 구조이다. 그러다 보니 트랜잭션을 생성하고 제거하는 overhead가 메타데이터 갯수 만큼 증가하게 된다. 이 또한 성능이슈를 발생시킬 가능성을 내재하고 있다고 생각한다.

2. 해결방안 고안..

일단 위에서 정리 했듯이 가장 큰 문제는 클라이언트에서 서버에 Api를 요청하는 횟수라고 생각했다. 사실상 5만개의 데이터의 삽입이 과연 대량 데이터라고 부를 수 있을 까..? 라고도 생각을 했다. 그래서 기존에 클라이언트 단에서 메타데이터를 보낼 때 메타데이터들의 리스트를 보내는 방식으로 전환하면 어떨까 생각할 수 있었다. 그렇게 되면 5만개의 데이터를 보낸다고 할 지라도 단 한번의 요청으로 서버는 5만개의 데이터를 처리 할 수 있다.

그리고 서버는 이제 이 메타데이터의 리스트를 하나의 트랜잭션으로 묶어서 한번에 5만번의 메타데이터를 save 할 수 있다. 즉 개별적으로 처리하기 위해 트랜잭션을 생성하고 제거하는 overhead가 메타데이터 갯수 만큼 증가하지 않게 된다.

	@Transactional
    public void insertAll(MetaDataCreateAllRequestDto metaDataCreateAllRequestDto){

        if(metaDataCreateAllRequestDto.getBodyList() != null){
            List<Document>bodyList = metaDataCreateAllRequestDto.getBodyList();
            String projectId = metaDataCreateAllRequestDto.getProjectId();
            for(Document body : bodyList){
                MetaData metaData = new MetaData().builder()
                        .projectId(projectId)
                        .body(body).build();
                        
                metaDataRepository.save(metaData);
            }
        }
    }

위 코드는 앞서 설명한 메타데이터의 리스트를 하나의 트랜잭션으로 묶어 처리한 코드이다.

3. 그렇다면 모든 작업을 하나의 트랜잭션으로 묶는게 좋은 것인가..?

정답은 아닐 것이라고 생각한다. 그 이유는 트랜잭션이 유지되는 기간이 길어지기 때문이다. 그 말은 즉 다른 세션의 트랜잭션에서 해당 데이터의 접근이 불가해지는 시간이 길어진다는 말이다. trade off 인 것 같다. 트랜잭션을 하나로 묶어 처리할 수 록 하나의 작업을 끝내는 시간은 단축되지만 다른 session에서는 이 작업이 끝나기를 기다리기 위해 추가적인 시간을 소비한다.

그렇다면 이를 적절하게 유지할 수 있는 방법은 무엇인가?

1. 트랜잭션의 격리수준을 낮춘다. 즉 트랜잭션끼리의 고립도를 낮추는 것이다.

그렇게 되면 삽입과정시에도 격리수준 내에서 접근이 가능해 진다.

2. 서버의 Service단에서 하나의 Transcation 당 처리하는 데이터의 양을 나눈다.

	@Transactional
    public void insertAll(MetaDataCreateAllRequestDto metaDataCreateAllRequestDto){

        if(metaDataCreateAllRequestDto.getBodyList() != null){
            List<Document>bodyList = metaDataCreateAllRequestDto.getBodyList();
            String projectId = metaDataCreateAllRequestDto.getProjectId();
            for(int i= 0 ; i < CHUNK_SIZE; i++ ){
                MetaData metaData = new MetaData().builder()
                        .projectId(projectId)
                        .body(body[i]).build();
                metaDataRepository.save(metaData);
            }
        }
    }

위 코드는 이해를 돕기 위해 작성한 코드이며, 실제 작동은 하지 않는다.

설명하자면 CHUNK_SIZE 단위로 메타데이터 리스트를 나누어 하나의 트랜잭션이 처리하게 한다. 그렇게 되면 일정 기간 트랜잭션이 유지되다 종료되면 다른 세션에서 접근할 수 있게 된다.

물론 이렇게 하게 되면 트랜잭션 생성,종료 overhead는 증가하게 되어 삽입 자체만의 성능은 줄어들게 된다.

4. 결론

본인 같은 경우에는 해당 데이터를 사용자 별로 접근하기에 각 트랜잭션들이 같은 데이터의 접근 하는 일이 없을 것이라 판단하여 삽입 자체만의 성능을 높이기 위해 트랜잭션을 청크 단위로 나누지는 않았다. 서비스 특성을 잘 고려하여 데이터 조작을 유연하게 잘 할 수 있는 것도 중요하다는 것을 알게되었다.


5. 참고자료

https://www.baeldung.com/spring-data-save-saveall

profile
개발 및 취준 일지

0개의 댓글