LoadMap Part : 스프링 핵심원리 고급편
Section : 2.Thread Local
CreateDate : 2022.11.14
UpdateDate : 2022.11.17
파라미터로 넘기도록 구현해서 동기화는 성공했지만, 로그를 출력하는 모든 메서드에
TreadId
파라미터를 추가해야되는 문제가 발생한다.
파라미터로 넘기지 않고 이 문제를 해결할 수 있는 방법은 없을까
- 인터페이스부터 시작하여 제대로된 로그 추적기를 만들어 보자
- 로그추적기에 필요한, begin(), end(), exception()이 들어간다.
public interface LogTrace {
TraceStatus begin(String message);
void end(TraceStatus status);
void exception(TraceStatus status, Exception e);
}
- 파라미터를 넘기지 않아도 TraceId를 동기화 할 수 있는 구현체 이다.
- 기존에 만들었던 프로토타입 HelloTraceV2랑 기본 메서드는 비슷하다.
- 구현체 FieldLogTrace 코드
- 추가된 코드
- 변수로
TraceId
를 담는traceIdHolder
가 생겼다.- 이로인해, 파라미터로 넘겨주지않아도 된다.
syncTraceId()
: 동기화 이슈 처리해주는 매서드, 기존꺼가 있으면 굳이 새로만들지 않고 +1 해준다.releaseTraceId()
: 동기화 이슈 처리해주는 매서드, level이 1이상이면 삭제하지않고 -1 해준다.
- 변수로
public class FieldLogTrace implements LogTrace {
//..생략
private TraceId traceIdHolder; // traceId 동기화, 동시성 이슈 발생
@Override
public TraceStatus begin(String message) {
syncTraceId();
TraceId traceId = traceIdHolder;
//..생략
}
private void syncTraceId() {
if (traceIdHolder == null) {
traceIdHolder = new TraceId();
} else
traceIdHolder = traceIdHolder.createNextId();
}
private void complete(TraceStatus status, Exception e) {
//..생략
releaseTraceId(); //추가
}
private void releaseTraceId() {
if (traceIdHolder.isFirstLevel()) {
traceIdHolder = null; // destroy
} else
traceIdHolder = traceIdHolder.createPreviousId();
}
}
class FieldLogTraceTest {
FieldLogTrace trace = new FieldLogTrace();
@Test
void begin_end_level2() {
TraceStatus status1 = trace.begin("hello1");
TraceStatus status2 = trace.begin("hello2");
trace.end(status2);
trace.end(status1);
}
@Test
void begin_exception_level2() {
TraceStatus status1 = trace.begin("hello1");
TraceStatus status2 = trace.begin("hello2");
trace.exception(status2, new IllegalStateException());
trace.exception(status1, new IllegalStateException());
}
}
- 확인 결과 깔끔하게 잘 나온다.
하지만 이럴 경우, 동시성 문제가 발생한다.
- 심각한 동시성문제를 가지고있다. 호출을 여러번해보면 바로 확인할 수 있다.
- 애플리케이션 실행 후 빠르게 호출을 2번하니 아래와 같이 나왔다.
- 두개의 쓰레드를 사용해 로그 추적기를 실행했지만,
- 트랜잭션 ID는 두개의 호출다 동일했으며,
- level도 중첩되어서, 깊이가 이상하게 출력됨을 볼 수 있다.
FieldLogTrace
는 싱글톤으로 등록된 스프링 빈이다- 객체의 인스턴스가 애플리케이션에 딱 1 개만 존재한다는 뜻이다.
- 이렇게 하나만 있는 인스턴스의
FieldLogTrace.traceIdHolder
필드를 여러 쓰레드가 동시에 접근하기 때문에 문제가 발생한다 - 이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타나지 않고, 트래픽이 점점 많아질수록 자주 발생한다.
- 특히 스프링 빈 처럼 싱글톤 객체의 필드를 변경하며 사용할때에는 더욱 조심해야된다.
- 동시성 문제가 발생하는 곳은 같은 인스턴스의 필드(주로 싱글톤에서 자주 발생), 또는 static 같은 공용 필드에 접근할 때 발생한다
- 지역변수는 상관없음
동시성 문제를 해결하기 위해 사용하는 것이 바로 쓰레드 로컬이다.
- 쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다
- 물건 보관 창구를 떠올리면 된다.
- 자바는 언어차원에서 쓰레드 로컬을 지원하기 위한 java.lang.ThreadLocal 클래스를 제공한다.
- set("")으로 전용보관소에 저장하며, get()으로 확인할 수가 있다.
- 선언 : new ThreadLocal()
- 예시 )
private ThreadLocal<String> nameStore = new ThreadLocal<>();
- 예시 )
- 값 저장 : ThreadLocal.set(xxx)
- 값 조회 : ThreadLocal.get()
- 값 제거 : ThreadLocal.remove()
- 해당 쓰레드가 모두 사용하고 나면, 쓰레드 로컬에 저장된 값을 제거 해주어야한다.
- 참고코드
- ThreadLocal을 구현한 코드를 APP에 적용하는 것은 간단하다.
- 전에 로그추적기를 Bean으로 등록해놓은 LogTraceConfig에서, 구현체만 바꿔주면된다.
@Configuration
public class LogTraceConfig {
@Bean
public LogTrace logTrace(){
return new ThreadLocalLogTrace();
}
}
- 변경하고 애플리케이션을 두번 실행해본 결과 아래와 같이 나온다.
- 쓰레드로컬은 아래와 같이 WAS의 쓰레드 풀을 사용하는 경우 문제가 발생할 수 있다.
- 쓰레드 풀은 커넥션풀 처럼 미리 만들어 놓은 쓰레드를 사용하고 반납할 수 있게 보관 및 관리 처리하는 곳이다.
- 그렇기에, 사용이 끝난 쓰레드는 풀에 반환하게된다. 쓰레드를 생성하는 비용이 비싸기 때문에 보통 풀을 통해서 재사용하기 때문이다.
- 만약 사용이 끝난 쓰레드의 정보를 지우지않고, 반환하게 된다면 재사용됐을시 기존의 정보가 남아 유출되기 때문이다.
이런 문제를 예방하려면 요청이 끝날 때 쓰레드 로컬의 값을 ThreadLocal.remove() 를 통해서 꼭 제거해야 한다