-
쓰레드로컬(ThreadLocal) - 동시성 문제의 해결스프링/스프링 AOP 2023. 9. 3. 20:44
쓰레드 로컬을 사용하면 각 쓰레드마다 별도의 내부 저장소를 제공한다. 따라서 같은 인스턴스의 쓰레드 로컬 필드에 접근해도 문제가 없다.
thread-A가 userA라는 값을 저장하면 쓰레드 로컬은 thread-A 전용 보관소에 데이터를 넣고, thread-B가 userB라는 값을 저장하면 thread-B의 전용 보관소에 데이터를 넣는 것이다.
자바에서 진행하는 'java.lang.ThreadLocal' 클래스를 잘 사용하면 동시성 문제를 해결할 수 있다.
@Slf4j public class ThreadLocalService { private ThreadLocal<String> nameStore = new ThreadLocal<>(); public String logic(String name) { log.info("저장 name={} -> nameStore={}", name, nameStore.get()); nameStore.set(name); // 넣을땐 set sleep(1000); log.info("조회 nameStore={}", nameStore.get()); return nameStore.get(); // 반환할땐 get } private void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } }
기존에는 private String nameStore;라고 String으로 선언했지만 ThreadLocal<String>을 통해 ThreadLocal로 만들어준다.
여러명의 사용자가 0.1초도 안되는 순간에 같은 인스턴스를 사용하는 로직을 호출하더라도 ThreadLocal이 각각 다른 쓰레드를 사용자에게 넘겨주어서 절대 겹치지 않는 상황이 되었다.
nameStore는 이전에 저장된 값인데, 각각 다른 쓰레드를 사용하기 때문에 항상 null이다.
userA가 저장하고 조회하기 전에 userB가 저장을 했음에도 쓰레드가 꼬이지 않고 각각의 고유한 쓰레드를 사용하는 것을 확인할 수 있다.
private TraceId traceIdHolder; // 아래와 같이 변경 private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();
테스트코드가 아니라 실제 Service 계층에 선언된 TraceId도 ThreadLocal로 변경해주면 된다.
쓰레드 로컬 사용시 주의사항
해당 쓰레드가 쓰레드 로컬을 모두 사용하고 나면 ThreadLocal.remove()를 호출해서 쓰레드 로컬에 저장된 값을 반드시 제거해야만 한다. 제거하지 않으면 쓰레드가 남아있을 수 있기 때문이다.
만약 삭제하지 않고 반납만 한다면 나중에 다른 사용자가 같은 쓰레드를 사용하게 된다면 이전 사용자의 데이터가 나올수도 있기 때문에 심각한 문제가 발생하게 된다.
요청 처리가 끝나고 필터나 인터셉터에서 clear를 하거나 아래와 같이 log에서 제거를 해주는 작업이 필요하다.
private void releaseTraceId() { TraceId traceId = traceIdHolder.get(); if (traceId.isFirstLevel()) { traceIdHolder.remove(); // 해당 쓰레드만 remove } else { traceIdHolder.set(traceId.createPreviousId()); // 1씩 감소 } }
isFirstLevel은 level==0일 때를 의미한다. 쓰레드를 다 사용하면 위와같이 무조건 remove()로 삭제해줘야 한다.
'스프링 > 스프링 AOP' 카테고리의 다른 글
템플릿 콜백 패턴 - 템플릿 패턴, 전략 패턴의 최종 (0) 2023.09.05 템플릿 패턴의 한계와 전략 패턴 (0) 2023.09.05 템플릿 메서드 패턴 (0) 2023.09.03 로그 추적기 필드 동기화 - 동시성 문제 발생 (0) 2023.09.03 로그 추적기 (0) 2023.09.03