지난 포스팅에 이어, 이번 포스팅에서는 5) ~ 10)
까지의 내용을 정리한다.
👉 목차는 다음과 같다.
1) 필드 동기화 - 개발
2) 필드 동기화 - 적용
3) 필드 동기화 - 동시성 문제
4) 동시성 문제 - 예제 코드
5) ThreadLocal - 소개
6) ThreadLocal - 예제 코드
7) 쓰레드 로컬 동기화 - 개발
8) 쓰레드 로컬 동기화 - 적용
9) 쓰레드 로컬 - 주의사항
10) 정리
바로 하나씩 확인해보자.
쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다.
쉽게 이야기해서 물건 보관 창구를 떠올리면 된다. 여러 사람이 같은 물건 보관 창구를 사용하더라도, 창구 직원은 사용자를 인식해서 사용자별로 확실하게 물건을 구분해준다. 사용자A, 사용자B 모두 창구 직원을 통해서 물건을 보관하고, 꺼내지만 창구 직원이 사용자에 따라 보관한 물건을 구분해주는 것이다.
일반적인 변수 필드
thread-A
가 userA
라는 값을 저장하고 thread-B
가 userB
라는 값을 저장하면, 직전에 thread-A
가 저장한 userA
값은 사라진다. ( 이렇게 되면 동시성 문제가 발생할 수 있다. 따라서 이러한 문제를 해결해주는 것이 쓰레드 로컬이라는 기능이다. )
쓰레드 로컬
thread-A
가 userA
라는 값을 저장하면 쓰레드 로컬은 thread-A
전용 보관소에 데이터를 안전하게 보관한다.thread-B
가 userB
라는 값을 저장하면 쓰레드 로컬은 thread-B
전용 보관소에 데이터를 안전하게 보관한다.thread-A
가 조회하면 쓰레드 로컬은 thread-A
전용 보관소에서 userA
데이터를 반환해준다. 물론 thread-B
가 조회하면 thread-B
전용 보관소에서 userB
데이터를 반환해준다.java.lang.ThreadLocal
이라는 클래스를 제공한다.👉 예제 코드를 통해서 ThreadLocal
을 학습해보자.
FieldService
와 거의 같은 코드인데, nameStore
필드가 일반 String
타입에서 ThreadLocal
을 사용하도록 변경되었다.ThreadLocal.set(xxx)
ThreadLocal.get()
ThreadLocal.remove()
ThreadLocal.remove()
를 호출해서 쓰레드 로컬에 저장된 값을 제거해주어야 한다. 제거하는 구체적인 예제는 조금 뒤에 설명하겠다.FieldLogTrace
에서 발생했던 동시성 문제를 ThreadLocal
로 해결해보자.
TraceId traceIdHolder
필드를 쓰레드 로컬을 사용하도록 ThreadLocal traceIdHolder
로 변경하면 된다.
👉 필드 대신에 쓰레드 로컬을 사용해서 데이터를 동기화하는 ThreadLocalLogTrace
를 새로 만들자.
traceIdHolder
가 필드에서 ThreadLocal
로 변경되었다. 따라서 값을 저장할 때는 set(..)
을 사용하고, 값을 조회할 때는 get()
을 사용한다.
✔️ ThreadLocal.remove()
ThreadLocal.remove()
를 호출해서 쓰레드 로컬에 저장된 값을 제거해주어야 한다. 쉽게 이야기해서 다음의 마지막 로그를 출력하고 나면 쓰레드 로컬의 값을 제거해야 한다.releaseTraceId()
를 통해 level
이 점점 낮아져서 2 -> 1 -> 0이 되면 로그를 처음 호출한 부분으로 돌아온 것이다.TraceId
값을 추적하지 않아도 된다. 그래서 traceId.isFirstLevel()
( level==0
)인 경우, ThreadLocal.remove()
를 호출해서 쓰레드 로컬에 저장된 값을 제거해준다.
👉 코드에 문제가 없는지 간단한 테스트를 만들어서 확인해보자.
멀티쓰레드 상황에서 문제가 없는지는 애플리케이션에 ThreadLocalLogTrace
를 적용해서 확인해보자.
FieldLogTrace
대신에, 문제를 해결한 ThreadLocalLogTrace
를 스프링 빈으로 등록하자.http://localhost:8080/v3/request?itemId=hello
)http://localhost:8080/v3/request?itemId=ex
)http://localhost:8080/v3/request?itemId=hello
을 1초 안에 2번 실행해보자. )nio-8080-exec-3
, nio-8080-exec-4
) 별로 로그가 정확하게 나누어 진 것을 확인할 수 있다. 쓰레드 로컬의 값을 사용 후 제거하지 않고 그냥 두면 WAS(톰캣)처럼 쓰레드 풀을 사용하는 경우에 심각한 문제가 발생할 수 있다.
👉 다음 예시를 통해서 알아보자.
thread-A
가 할당되었다.thread-A
는 사용자A
의 데이터를 쓰레드 로컬에 저장한다.thread-A
전용 보관소에 사용자A
데이터를 보관한다.thread-A
를 쓰레드 풀에 반환한다. ( 쓰레드를 생성하는 비용은 비싸기 때문에 쓰레드를 제거하지 않고, 보통 쓰레드 풀을 통해서 쓰레드를 재사용한다. )thread-A
는 쓰레드 풀에 아직 살아있다. 따라서 쓰레드 로컬의 thread-A
전용 보관소에 사용자A
데이터도 함께 살아있게 된다.thread-A
가 할당되었다. (물론 다른 쓰레드가 할당될 수 도 있다.)thread-A
는 쓰레드 로컬에서 데이터를 조회한다.thread-A
전용 보관소에 있는 사용자A
값을 반환한다.사용자A
값이 반환된다.결과적으로 사용자B는 사용자A의 데이터를 확인하게 되는 심각한 문제가 발생하게 된다.
이런 문제를 예방하려면 사용자A의 요청이 끝날 때 쓰레드 로컬의 값을 ThreadLocal.remove()
를 통해서 꼭 제거해야 한다.
쓰레드 로컬을 사용할 때는 이 부분을 꼭! 기억하자.
강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 김영한 강사님께 있습니다.
안녕하세요~! 고급편- 패턴 부분 정리를 너무너무 잘해주셔서 도움 받으며 공부하고 있었는데
글이 갑자기 삭제되어서..... 다시 오픈해주실 수 있으실지... 조심스럽게 여쭈어봐도 될까요....?!