Volatile의 위험성... 보다 잡다한 것이 더 많은

Jane·2023년 4월 6일
3

평화롭게 개발하던 중, 소스코드에서 volatile 키워드를 사용한 곳을 발견했다.

봘러틀, 읽기도 힘든 그녀... 자주 쓸 일이 없어서 그저 cpu cache가 아닌 메인 메모리로부터 변수를 읽고/쓴다고만 알고 있었다.

그녀는 언제부터 우리 코드에 녹아든 것일까(?)

문제의 서막은 배치였다. 기존 배치가 MyBatisPagingItemReader를 사용하고 있었는데, limit offset 방식으로 페이징해서 가져오다보니 슬로우쿼리가 발생했던 것.

다들 알다시피 limit offset을 사용할 경우 조회해 올 데이터가 많아질수록, 뒤로갈수록 쿼리의 성능이 떨어진다. 예를들어 100만건의 데이터를 조회했고 그 이후로 1천건을 조회하려면 100만 1천건을 조회해야 하기 때문이다. O(limit+offset) = O(N)

이를 개선하기 위해서는 역시 잘 알려져 있듯이 no offset 기반 페이징으로 바꾸면 된다.
커서 페이징을 이용할 경우 커서 위치부터 row를 읽기 때문에 O(limit) = O(1) 시간복잡도를 가진다.

이렇게 좋은데 왜 프레임워크 단에서 offset 기반으로 페이징을 구현한게 훨씬 많냐고 물어본다면

잘 모르겠다. offset 방식이 더 구현이 간단하기 때문일 것 같기도 하고.. 커서페이징은 아무래도 cursor로 이용될 column이 unique하고 sequential 해야돼서 정렬이 목적이면 비추하기도 한단다.


무튼 옆길로 많이 샜는데, 저런 이유로 커서페이징을 하든, between으로 range scan을 하게하든 skip rows를 전부 읽게 하는 건 개선해야 하는 상황이었고, (구현은 공개할 수 없으니 스킵하고 결론만 말하자면) 그 개선으로 만든 클래스의 일종의 cursor 처럼 쓰이는 변수가 volatile로 선언되어 있었다.

volatile로 선언된게 왜 문제일까?

이미지 출처: https://jenkov.com/tutorials/java-concurrency/volatile.html

동일한 배치를 연속으로 실행한다고 가정해보자.

각 배치 job은 신나게 메인 메모리에 cursor(쿼리 실행 시 where 조건에 넘길 값이 담겨있는 변수를 편하게 cursor라고 부르겠음) 값을 업데이트할 것이다. A job이 1000씩 성실하게 페이징을 수행하여 커서 값이 20000으로 올랐고, Main memory에 공유된 cursor 값도 20000으로 설정되었다고 하자.

이 때 뒤늦게 시작한 B job이 Main memory에서 cursor 값을 읽어오면 어떻게 될까?

B job에서 쿼리를 실행할 때 읽어온 cursor 값으로 요청하여 cursor 이전 데이터는 패싱될 것이다. 어떤 잡에서든 한 번만 수행되면 되고, 여러 번 수행되어도 상관없는 멱등성을 가지고 있는 로직이면 모르겠지만 그렇지 않다면 (...)


이를 해결하기 위해선 volatile 대신 ExecutionContext에 cursor값을 저장해두는 방식으로 개선하면 된다. 😁

ExecutionContext 내부를 보면 map이 있는데, 간단하게 java 코드에서 put() 또는 get()으로 값을 저장, 조회할 수 있다.

public class ExecutionContext implements Serializable {

	private volatile boolean dirty = false;

	private final Map<String, Object> map;
    
	public ExecutionContext() {
		this.map = new ConcurrentHashMap<>(); // 동기화 보장
	}
 ...
}

JobExecution의 ExecutionContext에 있는 값은BATCH_JOB_EXECUTION_CONTEXT 테이블에 저장되고, StepExecution의 ExecutionContext에 있는 값은 BATCH_STEP_EXECUTION_CONTEXT 테이블에 저장되는데 job과 step 마다 execution id가 다르기 때문에 해당 값이 공유될 걱정은 안 해도 된다.

BATCH_STEP_EXECUTION_CONTEXT

STEP_EXECUTION_IDSHORT_CONTEXTSERIALIZED_CONTEXT
1{"@class":"java.util.HashMap","cursor":["java.lang.Long",20000]}null

참고

0개의 댓글