Cursor-based RepositoryItemReader 구현과 case 정리
Cursor-based RepositoryItemReader 구현과 case 정리
RepositorySeekMethodItemReader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
open class RepositorySeekMethodItemReader<T, E>(
chunkSize: Int,
initialCursor: E,
private val cursorGetter: (T) -> E,
private val readBlock: (Int, Int, E) -> List<T>,
) : AbstractPagingItemReader<T>() {
private var cursor = initialCursor
init {
super.setPageSize(chunkSize)
}
override fun doReadPage() {
results = readBlock(cursor, pageSize)
cursor = results.last().let(cursorGetter)
}
override fun doJumpToPage(itemIndex: Int) {}
}
1
2
3
4
5
6
7
8
// usage
fun reader() = RepositorySeekMethodItemReader(
chunkSize = CHUNK_SIZE,
initialCursor = 0,
cursorGetter = SmsMessage::sequence,
) { cursor, pageSize ->
smsMessageRepository.findAllByCondition1(condition1, cursor, pageSize).toList()
}
pitfall
initialCursor = 0이면 첫 쿼리가 매우 느릴 수 있다.
- 위 예시에서 PK는 sequence이고, 그래서 cursorGetter를 sequence로 설정했고, initialCursor은 0으로 설정했다.
- 검색 조건이 sequence, condition1 이기 때문에, 인덱스도 이렇게 걸려 있을 것이다. (또는 sequence에만)
- initialCursor = 0이기 때문에, 맨 앞에서 부터 읽기 시작한다. 즉, index full scan이 뜬다.
- 만약 쿼리 결과로 잡혀야 하는 row의 sequence가 맨 뒤쪽에 있고(보통 그렇다), table size가 매우 크다면, 첫 번째 쿼리가 매우 느릴 수 있다.
- 두 번째 쿼리 부터는, cursor를 저장해두고 읽기 때문에 문제가 없다.
- 첫 쿼리 속도 때문에 이 방법이 Offset-based pagination에 비해서 항상 우월한 방법은 아니다. (다만 대부분의 상황에서 우월한 것은 맞다)
- 상황에 따라 Offset-based가 더 나을 수도, Cursor-based가 더 나을 수도 있어 상황에 맞게 사용해야 한다.
- 별도 쿼리로 적절한 initialCursor를 빠르게 알아낼 수 있는지 여부 등
cursor 대상 컬럼이 unique 하지 않은 경우
[!warning] 기본적으로 Seek Method는 대상 컬럼이 unique 하다는 전제로 사용해야 한다.
- 불러온 item을 모두 delete, update하는 경우, 대상 컬럼이 unique 하지 않아도 문제가 발생하지 않는다.
- 하지만 모두 delete, update 할거라면, 굳이 Seek Method 쓸 필요 없이 하단의
RepositoryPagingItemReader에 Page 0으로 고정하고 쓰는게 더 나아보인다.
cursor 대상 컬럼이 unique 한 경우
복합키(Composite Key)에 대한 Seek Method
[!warning] 복합키에 대한 Seek Method는 불가능하다. (생각해보면 알 수 있다)
Paging이 틀어지면서 중복, 누락 발생 케이스를 생각해보면…
불러온 item을 delete, update 하지 않는 경우
- Seek Method 말고, 일반적인 Paging 사용하면 된다.
불러온 item을 일부만 delete, update 하는 경우
- 중복, 누락 없으려면 반드시 CursorItemReader를 써야 한다.
- 여기서 Cursor는 DB가 관리해주는 Cursor를 의미함
- DB에 세션 잡고 Cursor 열어둔 채로
ResultSet.next()해서 하나씩 읽어오는 방식으로 진행
불러온 item을 모두 delete, update 하는 경우
- Seek Method 사용하지 않고, 항상 Page를 0으로 고정한 일반적인 Paging을 사용하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
open class RepositoryPagingItemReader<T>(
chunkSize: Int,
private val readBlock: (Int, Int) -> List<T>,
) : AbstractPagingItemReader<T>() {
init {
super.setPageSize(chunkSize)
}
override fun doReadPage() {
results = readBlock(page, pageSize)
}
override fun doJumpToPage(itemIndex: Int) {}
}
1
2
3
4
5
6
// usage
fun reader() = RepositoryPagingItemReader(
chunkSize = CHUNK_SIZE
) { _, pageSize ->
smsMessageRepository.findAllByBlaBla(PageRequest.of(0, pageSize)).toList()
}
참고
- https://tech.kakaopay.com/post/ifkakao2022-batch-performance-read/
This post is licensed under CC BY 4.0 by the author.



