본문 바로가기
Java

Cursor<T>

by 긍고 2023. 3. 9.
반응형

개요


그림 1. 리턴 타입이 Cursor<T>인 쿼리 예시

최근 회사 프로젝트 PR을 보던 중, Mapper 파일에서 그동안 보지 못했던 리턴 타입을 보았다. 주소록 그룹에서 FAX 번호를 대량으로 조회하는 쿼리의 리턴 타입이 Cursor<String>으로 되어 있었는데, 해당 리턴값을 사용하는 곳을 보니 일반 List처럼 for문을 사용해서 순회하는것을 확인할 수 있었다.

그림 2. Cursor<String> 타입을 for문을 이용해 순회하는 예

이를 통해 Cursor는 리스트와 비슷하게 순회가 가능하지만 다른 이점이 있을것이라고 생각했고, 그 내용에 대해 정리해 보았다.

 

Cursor?


Cursor contract to handle fetching items lazily using an Iterator. Cursors are a perfect fit to
handle millions of items queries that would not normally fits in memory.

위 문구는 마이바티스 공식 문서에서 확인한 Cursor의 정의이다. 번역하면 Iterator를 사용하여 item fetch를 느리게 처리할 수 있으며, 일반적으로 메모리에 맞지않는 대용량 조회 쿼리에 적합하다.이다. 이를 통해 개요에서 추측했던 List와의 비교 이점이 대용량 데이터 처리에 있다는 것을 알 수 있었다.

그림 3. Cursor 인터페이스

Cursor는 Mybatis 3.5.0 버전 이상에서 사용가능한 제공하는 인터페이스 중 하나이며, 대용량 데이터를 처리할 때 메모리를 효율적으로 사용할 수 있다. Iterable<T> 인터페이스를 확장하므로 위의 예에서 볼 수 있듯이 순회가 가능하다.

 

추가로 Cursor는 Closable 인터페이스도 확장하기 때문에 try문에 사용하거나 그렇지 않으면 close()메소드로 닫아야 하지만, 프레임워크의 도움을 받는다면 이러한 부분을 명시하지 않고 사용할 수 있다.

 

대용량 처리에 대한 이점


List를 이용한 대용량 처리

그림 4. List 이용시 대용량 처리

DB에서 대용량 데이터를 불러와 사용한다면 위 그림처럼 모든 데이터가 메모리에 적재된 뒤에야 해당 데이터를 소비할 수 있다. 이 경우, 데이터가 너무 많다면 outOfMemoryError가 발생할 수도 있고, 그렇지 않다고 하더라도 메모리를 너무 많이 점유해서 문제가 될 수 있다.


Cursor를 이용한 대용량 처리

그림 5. Cursor 이용시 대용량 처리

반면 Cursor를 이용하면 DB에서 모든 데이터가 메모리에 적재된 상태가 아니라도 데이터를 소비할 수 있다. Cursor 로 반환된 데이터는 iteration을 반복할 수 있는 상태가 되면 Cursor를 반환하여 이를 소비할 수 있게된다.

 

이는 DB 커넥션이 유지되는 동안 계속해서 이루어지며(하나의 트랜잭션이 유지되는 동안) 이 트랜잭션이 종료되었다고 판단되면 프레임워크에 의해 커넥션이 종료된다.

 

Cursor의 적용


Cursor를 사용하는 방법은 매우 간단하다. 기존에 Dao에서 리턴타입을 List 로 사용하던것을 Cursor로 변경하기만 하면 된다.

// DAO
public class CursorMapper {

	@Select(...)
	List<String> getFaxNumbers(); // List 사용

	@Select(...)
	Cursor<String> getFaxNumbers(); // Cursor 사용
}

// Service
@RequiredArgsConstructor
public class CursorService {
	private final CursorMapper cursorMapper;

	public void test() {
		Cursor<String> faxNumbers = cursorMapper.getFaxNumbers(); // 어떤 값이 들어있을까?
	}
}

위처럼 CursorMapper에서 Cursor를 이용한 메서드를 만들고 CursorService에서 해당 매퍼를 이용해 데이터를 받아오면 항상 빈 값만 조회되게 된다.


@Transactional 사용하기

그 이유는 Cursor는 데이터를 일정 단위로 처리할 수 있게 해준다고 했는데, 데이터 처리가 끝나면 다음 단위를 읽어와야 하기 때문에 전체 데이터를 모두 순회할 때까지 DB 커넥션이 유지되어야 하기 때문이다.

 

따라서 Cursor를 사용하려면 해당 서비스 메서드에 @Transactional 어노테이션을 추가하여 트랜잭션 상태를 유지시켜야 하고, 추가로 해당 메서드를 벗어나기 전에 Cursor를 사용하는 작업을 모두 끝마쳐야 한다. 위의 코드에서 보면 test()메서드 내부에서 Cursor를 사용하는 작업을 모두 끝마쳐야 한다.

// Service
@RequiredArgsConstructor
public class CursorService {
	private final CursorMapper cursorMapper;

	@Transactional // 중요!
	public void test() {
		Cursor<String> faxNumbers = cursorMapper.getFaxNumbers();
	}
}

Stream도 사용할 수 있을까?

반복문과 관련된 코드 중에서 for, foreach문 대신 stream을 이용하여 개발된 코드가 꽤 있다. 보통의 경우 List에 대해 반복문을 사용하기 때문에 그와 비슷한 Cursor 타입에도 stream을 쓸 수 있는지 궁금했는데, 아쉽게도 Cursor는 Collection을 구현하지 않기 때문에 stream을 사용할 수 없다…

그림 6. Stream 사용이 불가능한 Cursor<T> 타입

 

Cursor를 적용할 때 생각해보아야 할 것


Cursor를 사용하면 메모리에 데이터를 전부 적재하지 않고도 처리할 수 있기 때문에 한 작업에 메모리가 과하게 쓰이지 않는다는 장점이 있다. 하지만 List 타입과 다르게 계속해서 DB로부터 데이터를 fetch해 오기 때문에 DB와의 통신 빈도가 늘어나, 이에 대한 네트워크 리소스 비용이 늘어날 수 있다.

 

네트워크 통신을 하는데 드는 시간 보다 메모리에 적재되어 있는 데이터를 처리하는 것이 몇 배는 빠르기 때문에 무조건 대용량 처리에는 Cursor를 사용하는 것이 독이 될 수도 있다.

 

  • fetchSize up ⇒ 메모리 점유 up, 네트워크 리소스 down
  • fetchSize down ⇒ 메모리 점유 down, 네트워크 리소스 up

 

Cursor를 이용해 데이터를 가져올 때 fetchSize를 설정할 수 있는데, 이 크기를 적절히 조절하여 메모리 점유율과 네트워크 리소스 비용 사이의 트레이드 오프를 조정하며 최적수치를 찾아 적용해보는 과정이 필요할 것 같다.(회사 플젝에서 테스트해보려고 했는데 시간 부족으로 못해봤음..)

 

참고


 

 

Mybatis Cursor<T>

만약 Mybatis와 Cursor 를 한글로 조회하면 100이면 100 "오라클 커서"가 조회될 것이다. 압도적으로 이런 용도로 많이 쓰기 때문에 아마 대부분 Mybatis 에 Cu...

dev.to

 

 

MyBatis의 fetchSize와 Cursor 이야기

컴퓨터는 정해진 규칙에 의해 돌아가고 있기 때문에, 어떤 처리를 하려면 입력으로부터 필요한 수를 계산하거나 미리 알고 있는 수를 바탕으로 판단해야한다. 네트워크 통신에서 기본 몇 byte를

git.lily.rs

 

댓글