Spring/Spring Core Advanced

ThreadLocal

민철킹 2021. 10. 29. 19:03

2021.10.28 - [Spring/Spring Core Advanced] - 로그 추적기

 

로그 추적기

애플리케이션이 커질수록 모니터링과 운영이 중요해진다. 어떤 부분에서 병목현상이 발생하는지 어떤 부분에서 예외가 발생하는지를 로그로 확인하는 것은 매우 중요하다. 아래와 같은 요구사

minchul-son.tistory.com

 

위 글에서 만든 로그 추적기는 파라미터로 트랜잭션 상태를 넘기며 동기화를 하였다.

 

하지만 이 방식은 모든 메서드에 파라미터를 추가해야하는 등 몇가지 문제가 존재했는데 파라미터를 넘기지 않도록 만들어 이 문제를 해결할 수 있는 방식에 대해 알아보자.

 


필드 동기화

파라미터를 넘기지 않고 필드를 동기화할 수 있는 방법에 대해 알아보자.

 

먼저 LogTrace의 인터페이스를 구현한다.

  • begin : HTTP 트랜잭션 시작
  • end : 정상 종료
  • exception : 예외 발생

직전 버전과 동일한 기능을 하지만 바뀐 부분은 traceIdHolder라는 필드를 가지고 이 필드를 사용해 동기화를 한다.

 

모두 동일한 begin()을 호출하지만 traceIdHolder에 상태에 따라서 동작한다.

 

traceIdHolder가 null이라면 처음 호출되었다는 의미이므로 기본생성자를 통해 객체를 생성하고, 그 외에는 필드에 저장된 객체를 통해 createNextId()를 호출하여 level을 1씩 증가시켜준다.

 

또한 complete 메서드에서는 releaseTraceId()를 호출하는데 이때도 마찬가지로 traceIdHolder의 상태에 따라 동작한다.

 

level이 0이라면 요청이 모두 빠져나왔다는 의미이므로 traceIdHolder에 null로 다시 초기화해주고 그렇지 않다면 level을 -1해준다.

 

테스트 코드

정상흐름
예외흐름

이제 새롭게 만든 로그 추적기를 스프링 빈으로 등록하고 애플리케이션에 적용시켜보자

 

구현체에 따라 갈아끼울 수 있도록 config 클래스를 만들어 스프링 빈을 수동 등록하였다.

 

컨트롤러
서비스
레포지토리

이제 파라미터를 넘겨 메소드를 변경할 일도 없고 모든 레이어에서 동일한 begin 메소드를 사용하게 되었다.

 

이 애플리케이션을 실제 서비스에 대입해보자.

 

실제 서비스하는 애플리케이션에서 요청은 간격을 두고 한 사람씩 차례차례 들어오지 않는다.

 

동시에 또는 매우 짧은 간격으로 수십 수백만건의 요청이 들어온다.

 

즉, 여러 쓰레드가 동시에 로직을 호출하기 때문에 필드를 공유하는 것은 매우 위험하다.

 

위와 같이 하나의 필드를 공유하여 사용한다면 동시성 문제가 발생할 수 밖에 없다.

 

실제로 새로고침을 통해 요청을 거의 동시에 전달하면 다른 쓰레드이지만 같은 트랜잭션 ID를 공유하고 있고 레벨또한 의도하지 않은대로 출력되고 있다.

 

이는 스프링 컨테이너가 스프링 빈을 싱글톤으로 등록하고 사용하고 있기 때문이다.

 

싱글톤이기 때문에 하나의 인스턴스를 공유하여 사용하게 되는데 동시에 요청이 들어온다면 문제가 발생한다.

 

동시성 문제는 트래픽이 점점 많아질수록 자주 발생한다.

또한, 지역변수는 쓰레드마다 각자의 stack 영역에 저장되기 때문에 동시성 문제가 발생하지 않는다.

 

싱글톤 객체의 필드를 사용하면서 동시성 문제를 해결하기 위해서 사용하는 것이 바로 ThreadLocal이다.

ThreadLocal

ThreadLocal은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 의미한다.

 

즉, 쓰레드 별로 저장한 데이터를 꺼내올 수 있다.

 

`java.lang.ThreadLocal`을 통해 클래스를 제공하고 있다.

https://docs.oracle.com/javase/7/docs/api/java/lang/ThreadLocal.html

 

ThreadLocal (Java Platform SE 7 )

Returns the current thread's "initial value" for this thread-local variable. This method will be invoked the first time a thread accesses the variable with the get() method, unless the thread previously invoked the set(T) method, in which case the initialV

docs.oracle.com

말 그대로 Map과 Set같은 저장소라고 생각하면 이해가 빠를 것이다.

 

ThreadLocal과 내부에 들어갈 타입을 지정하여 인스턴스를 생성한다.

 

그 후, 값을 저장할 때는 set을 통해 저장하고 값을 꺼낼 때는 get를 통해 꺼낸다.

 

삭제할 때는 remove를 사용해 삭제하면 된다.

 

여기서 주의할 점은, 해당 Thread가 ThreadLocal의 사용을 완료하고 나면 `remove()`를 호출하여 저장된 값을 완전히 제거해주어야 한다!! ==> 메모리 낭비

 

ThreadLocal을 사용하여 5개의 Thread를 동시에 실행시켜보았다. ThreadLocal을 사용하지 않았을 때는 동시성 문제가 발생했는데 어떻게 수행될지 살펴보자.

 

로그를 살펴보면 Thread마다 별도의 저장소를 사용하고 있다는 사실을 확인할 수 있다.

 

만약 별도의 저장소를 사용하지 않는다면 nameStore에는 바로 직전에 실행된 Thread가 저장한 값이 들어있어야하지만 모두 null이 들어가있다.

 


ThreadLocal을 사용하여 로그 추적기를 만들고 적용시켜보자.

 

나머지 부분은 이전과 동일하고 밑줄친 부분만 수정된 부분이다.

 

ThreadLocal에 값을 저장하고 꺼내오는 식으로 로그 추적기를 구현하였다.

 

애플리케이션 적용을 위해 Spring Bean의 구현체를 갈아끼운후, 요청을 동시에 두번 전송해보자.

 

동시에 요청이 전송되어 처리되지만 동시성 문제가 발생하지 않고 깔끔하게 HTTP 트랜잭션을 추적하는 것을 확인할 수 있다.

 


주의사항

ThreadLocal의 값을 사용후 왜 remove를 해주어야 할까?

 

Thread를 생성하는 비용은 매우 비싸다. 그렇기에 WAS는 Thread Pool에 Thread를 미리 만들어주고 할당과 해제를 통해 Thread를 재사용하는 형식으로 사용한다.

 

그렇기에 각 사용자마다 특정 Thread가 할당되는 것이 아닌 사용가능한 Thread가 할당되게된다.

 

다시말해, ThreadLocal에 저장된 값을 remove해주지 않으면 그 곳에 담겨있는 데이터를 같은 Thread를 할당받은 다른 사용자가 볼 수 있다는 의미이다.(만약 이 데이터가 민감 정보라면?😱)

 

 

 

 

반응형

'Spring > Spring Core Advanced' 카테고리의 다른 글

Spring의 프록시 기술  (0) 2021.12.16
동적 프록시 기술  (0) 2021.11.18
프록시 패턴과 데코레이터 패턴  (0) 2021.11.08
템플릿 메서드 패턴과 콜백 패턴  (0) 2021.11.01
로그 추적기  (0) 2021.10.28