개요
최근 회사 프로젝트 PR을 보던 중, Mapper 파일에서 그동안 보지 못했던 리턴 타입을 보았다. 주소록 그룹에서 FAX 번호를 대량으로 조회하는 쿼리의 리턴 타입이 Cursor<String>으로 되어 있었는데, 해당 리턴값을 사용하는 곳을 보니 일반 List처럼 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와의 비교 이점이 대용량 데이터 처리에 있다는 것을 알 수 있었다.
Cursor는 Mybatis 3.5.0 버전 이상에서 사용가능한 제공하는 인터페이스 중 하나이며, 대용량 데이터를 처리할 때 메모리를 효율적으로 사용할 수 있다. Iterable<T> 인터페이스를 확장하므로 위의 예에서 볼 수 있듯이 순회가 가능하다.
추가로 Cursor는 Closable 인터페이스도 확장하기 때문에 try문에 사용하거나 그렇지 않으면 close()메소드로 닫아야 하지만, 프레임워크의 도움을 받는다면 이러한 부분을 명시하지 않고 사용할 수 있다.
대용량 처리에 대한 이점
List를 이용한 대용량 처리
DB에서 대용량 데이터를 불러와 사용한다면 위 그림처럼 모든 데이터가 메모리에 적재된 뒤에야 해당 데이터를 소비할 수 있다. 이 경우, 데이터가 너무 많다면 outOfMemoryError가 발생할 수도 있고, 그렇지 않다고 하더라도 메모리를 너무 많이 점유해서 문제가 될 수 있다.
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을 사용할 수 없다…
Cursor를 적용할 때 생각해보아야 할 것
Cursor를 사용하면 메모리에 데이터를 전부 적재하지 않고도 처리할 수 있기 때문에 한 작업에 메모리가 과하게 쓰이지 않는다는 장점이 있다. 하지만 List 타입과 다르게 계속해서 DB로부터 데이터를 fetch해 오기 때문에 DB와의 통신 빈도가 늘어나며, 이에 대한 네트워크 리소스 비용이 늘어날 수 있다.
네트워크 통신을 하는데 드는 시간 보다 메모리에 적재되어 있는 데이터를 처리하는 것이 몇 배는 빠르기 때문에 무조건 대용량 처리에는 Cursor를 사용하는 것이 독이 될 수도 있다.
- fetchSize up ⇒ 메모리 점유 up, 네트워크 리소스 down
- fetchSize down ⇒ 메모리 점유 down, 네트워크 리소스 up
Cursor를 이용해 데이터를 가져올 때 fetchSize를 설정할 수 있는데, 이 크기를 적절히 조절하여 메모리 점유율과 네트워크 리소스 비용 사이의 트레이드 오프를 조정하며 최적수치를 찾아 적용해보는 과정이 필요할 것 같다.(회사 플젝에서 테스트해보려고 했는데 시간 부족으로 못해봤음..)
참고
'Java' 카테고리의 다른 글
[Java] UUID를 이용한 고유 식별자 생성 (1) | 2024.09.22 |
---|---|
자바 StringBuilder와 StringBuffer는 뭐가 다를까? (0) | 2024.09.20 |
[Java] Spring Boot Actuator (0) | 2023.02.14 |
[Spring Cloud] HA of Service Discovery (0) | 2023.02.07 |
[Spring Cloud] Service Discovery (0) | 2023.01.22 |
댓글