[mybatis] fetchSize와 Cursor :: 잡다한 프로그래밍
반응형

mybatis Cursor란?

- 공식문서에 다음과 같이 설명하고 있다

A Cursor offers the same results as a List, except it fetches data lazily using an Iterator.

(Cursor는 Iterator를 사용하여 Lazy하게 데이터를 가져오고 이는 List와 동일한 결과를 제공한다)

 

간단 정리 : 대량 데이터(대량 ROW)를 가져올때 사용하는 방법

- 약 1천만건의 데이터를 List에 담아서 어떠한 처리를 한다고 가정했을때 기존 방법은 OOM 발생 > CURSOR사용하면 해결 가능

 

1. cursor를 사용할때 동작방식의 차이

 

- 기존 방식의 동작 방법

1. DAO Mapper 인터페이스 선언을 바탕으로, Mybatis가 동적으로 생성한 코드로 DB작업을 준비

2. DAO Mapper를 통해 DB작업이 진행되면 알맞은 드라이버 (ex: JDBC)나 풀을 통하여 작업을 수행

3. 2.의 작업이 완료될때까지 코드 블로킹 (통상 service 코드 블로킹)

4. DB작업이 끝나면 spring 으로 돌아옴 (서비스로)

- Cursor를 이용했을 때 동작 방법

1. DAO Mapper 인터페이스 선언을 바탕으로, Mybatis가 동적으로 생성한 코드로 DB작업을 준비

2. DAO Mapper를 통해 DB작업이 진행되면 알맞은 드라이버 (ex: JDBC)나 풀을 통하여 작업을 수행

3. 트랜잭션을 시작, cursor의 경우 2의 작업이 cursor로 iteration을 반복할 수 있는 상태가 되면 모든 데이터가 받아지지 않더라도 DAO interface를 통해 cursor를 반환한다. (반환한 cursor를 서비스에서 처리 후 > 다시 반복)
4. DB 커넥션이 유지되는 동안 필요한 작업을 수행. cursor가 데이터셋의 끝에 도달할때까지 반복 가능

5. 트랜잭션 종료 (커넥션 종료)

 

 

3번에 따라 JVM 메모리에 한번에 모든 결과를 올려둘 필요가 없으므로, 충분한 시간만 주어진다면 조회 데이터 수가 많더라도 OOM 없이 데이터를 모두 읽어서 처리할 수 있다.

 

2. 사용 방법

기존 코드를 다음과 같이 개선하여 사용할 수 있다.

 

- 기존 Mapper 코드

List<TestDto> selectTest();

- 개선 Mapper 코드

Cursor<TestDto> selectTest();

 

- 기존 서비스 코드

@Service
@RequiredArgsConstructor
public class testService {
    @Autowired
    private TestMapper testMapper;

    public void test() {
    	List<TestDto> list = testMapper.selectTest();
        ...
        ....
    }
}

 

- 개선 서비스 코드

@Service
@RequiredArgsConstructor
public class testService {
    @Autowired
    private TestMapper testMapper;

	@Transactional
    public void test() {
    	try( Cursor<TestDto> list = testMapper.selectTest()) {
        	for (TestDto dto : list) {
            //....
            //..
            
            }
        } catch (Exception e) {
        	// ..
        }
    }
}

왜 이렇게 개선되어야 할까? 이는 앞서 말한 동작방식을 보면 알 수 있다.

Cursor 는 한줄씩 데이터를 처리할 수 있게 해준다고 앞에서 언급했는데, 이는 즉 데이터 처리가 끝나면 다음 줄을 읽어와야하는것을 의미한다.

따라서 전체 데이터를 모두 순회 할때까지 DB 연결이 유지되어야 한다는 걸 의미한다.

 

정리해보면

1. 해당 서비스 메소드에 @Transactional 을 달아 트랜잭션 상태를 유지시킨다

2. Service 메소드를 벗어나기전 Cursor를 써야하는 작업을 모두 마쳐야한다.

 

3. cursor와 fetchsize의 관계

네트워크 통신보다 메모리에 있는 내용을 처리하는 속도가 훨씬 빠름 따라서 얼만큼 처리할 데이터를 메모리에 올려놓는지 적절히 조율이 필요함

  • 통신 빈도를 줄인다 - 통신 한번에 받아올 데이터의 양이 늘어난다(캐시를 많이 해야하므로 JVM 메모리를 많이 먹는다)
  • 통신 빈도를 늘린다 - 통신 한번에 받아올 데이터의 양이 줄어든다(캐시를 적게 해도 되므로 JVM 메모리를 적게 먹는다)

 

4. 내가 겪었던 상황

 

테이블 A, 테이블 B의 데이터를 JOIN해서 select 해오고 있었음

A에 약 100만건, B에 1000만건의 데이터가 있다고 가정

 

JOIN하는 순간부터 너무 많은 시간이 걸림

반응형

+ Recent posts