메서드의 로그를 출력하는 로그추적기 객체를 생성했다. 메서드 시작부분에 로그추적기의 begin 메서드를, 메서드 종료부분에 로그추적기의 end 메서드, 메서드에서 예외가 발생하면 로그추적기의 exception 메서드를 출력하면 된다. 이때 요청마다 Id 와 level 를 동기화 하기위해 파라미터로 TraceId 를 넘겨주는 방식을 채택했다.
파라미터로 넘겨주지 않고 메서드 간에 traceId를 공유할 수 있는 방법이 없을까?
각 클래스에서 싱글톤 빈인 로그추적기 객체를 주입받아 사용한다. 즉 같은 객체를 사용한다는 소리다. 그러면 그 객체의 필드에 TraceId를 저장하면, Controller Service Repository 에서 해당 TraceId 를 공유하면서 사용할 수 있을 것이다.
로그추적기의 TraceId 가 null 이라면. 즉 레벨이 0인 상태에서 로그추적기를 사용하면 TraceId 를 생성해서 넣어주고. 레벨이 0이 아니라면 이전 TraceId 에서 Level을 1 증가시켜서 사용하면 된다.
필드에 TraceId 를 보관하게 되면서 이를 주입받아 사용하는 Controller Service Repository 클래스가 TraceId 를 공유할 수 있게 되었다. 이제 파라미터로 넘겨줄 필요가 없어, 모든 메서드에서 TraceId 를 추가할 필요가 없어졌다.
매핑된 Url 로 컨트롤러에 요청을 두번해보자. 위와같이 이상한 로그가 찍힌다. 왜 그런걸까?
상기 언급했듯이 스프링은 싱글톤으로 객체를 관리한다. 모든 요청스레드에서 동일한 로그추적기 객체를 사용한다는 뜻이다. 우리는 목표는 각 스레드의 Controller Service Repository 가 TraceId 를 공유했으면 하는 마음에서 TraceId 를 필드로 추가한 것인데, 모든 스레드가 해당 TraceId 를 공유하게 된 것이다.
즉 멀티 스레드 환경에서 싱글톤 빈의 상태를 유지하는 필드가 동기화 문제를 발생시킨 것이다.
스레드 간에는 코드, 데이터, 힙 영역을 공유하게 되어 힙 영역의 객체도 공유하게 된다. 이 때 두 스레드가 하나의 객체에 동시에 접근할 경우 문제가 생길 수 있다. 두 스레드 모두 객체를 읽기만 하면 문제가 없는데 한 스레드가 쓰기를 수행할 경우 문제가 생길 수 있다. 그래서 쓰기를 수행할 수 있는 객체의 필드를 조심해야한다.
FieldService 클래스이다. nameStore 를 필드로 갖는다. logic 메서드는 nameStore 에 파라미터인 name 을 기록하고 1초 뒤에 읽는 메서드이다.
FieldService 를 이용하는 테스트 코드이다. fieldService.logic("user") 를 하나의 Runnable 로 설정한 뒤 threadA 와 threadB 를 생성하였다.
메인 스레드가 threadA 를 실행한다. threadA 는 필드에 "userA" 를 기록하고 1초뒤에 필드에 저장되어 있던 userA를 돌려받는다.
그리고 1초뒤 메인스레드가 threadB 를 실행한다. threadB 는 필드에 "userB" 를 기록하고 1초 뒤 필드의 "userB" 를 돌려받는다.
스레드는 병렬적으로 수행되지만, 메인스레드가 threadA 실행 후 2초를 대기하고 threadB를 실행하기에 필드의 일관성을 보장하게 된 것이다.
threadA 실행 후 바로 threadB를 실행하면 어떻게 될까?
threadA 가 객체의 필드에 userA 를 기록한다. 그리고 sleep 메서드로 threadA 는 1초간 대기한다.
스레드는 병렬적으로 수행되므로, 직후 실행된 threadB 가 객체의 필드에 userB 를 기록한다. 그리고 threadB 는 1초간 대기한다.
대기에서 깨어난 threadA 가 필드를 읽으면 자신이 기록했던 userA 가 아닌 threadB 가 기록한 userB 가 읽힌다.
실제로도 동시성 문제가 발생하여 A가 조회하였는데 userB가 읽히는 것을 알 수 있다.
즉 스레드는 병렬적으로 실행되고, 스레드 간에 객체를 공유하므로 스레드가 값을 읽고 쓰는 객체의 필드에서 동시성 문제가 발생할 수 있다.
스레드 로컬은 스레드 마다 별도의 내부 저장소를 제공하는 객체이다. 객체의 필드를 여러 스레드가 공유하는 것이 동시성 문제의 원인이므로, 필드를 스레드 로컬 객체로 두면 동시성 문제를 해결할 수 있다. 자바는 언어차원에서 java.lang.ThreadLocal 클래스를 제공한다.
상기 예제에 스레드 로컬을 적용해보자.
객체의 필드에 String 대신 ThreadLocal<타입> 을 사용하면 해결된다.
스레드를 병렬적으로 실행하였음에도 스레드 별로 별도의 저장소를 활용하여 각자의 값을 가져오는 것을 확인할 수 있다.
해당 스레드가 스레드 로컬을 모두 사용하고 나면 꼭 remove() 로 스레드 로컬의 값을 지워줘야한다. WAS 는 스레드 풀 방식으로 동작하는데, 스레드 반환시 remove 로 스레드 로컬을 초기화시킨 후 반환하지 않으면, 다음 요청에서 이전 요청의 데이터를 접근할 수 있다.
필드에 TraceId 대신 TraceId를 저장하는 ThreadLocal 로 변경하자. 이제 각 스레드마다 TraceId 를 저장할 수 있는 별도의 저장소를 활용할 것이다. 그래서 하나의 스레드의 Controller Service Repository 간에만 TraceId 를 공유하게 된다.
로그추적기의 메서드들에서 TraceId 를 가져오는 부분은 스레드 로컬의 get 메서드를. TraceId를 설정하는 부분은 set 메서드를 설정하고. 메서드 완료 후 level 이 0 이라면 remove 메서드를 실행하도록 하자.
동시성 문제를 해결하였음을 알 수 있다.
WAS 는 멀티스레드를 지원하며 스프링 컨테이너는 싱글톤으로 객체를 관리한다. 스레드 간에 객체는 공유되는데, 값이 유지되는 객체의 필드에서 동시성 문제가 발생 가능성이 있다.
객체의 필드를 ThreadLocal 객체로하면 스레드마다 별도의 저장소를 활용해서 동시성 문제를 방지할 수 있다. 스레드 풀에서 이전 요청의 정보를 활용하는 것을 방지하기 위해 스레드 로컬의 사용을 완료하면 remove 메서드로 초기화시키자.