You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
먼저 어떤 상황에 어떤 문제가 있고 이를 해결하기 위해서 쓰레드로컬을 이럴 때 쓴다!
를 알려줌.
앞서 로그 추적기를 만들면서 다음 로그를 출력할 때 트랜잭션ID 와 level 을 동기화 하는 문제가 있었다.
이 문제를 해결하기 위해 TraceId 를 파라미터로 넘기도록 구현했다.
이렇게 해서 동기화는 성공했지만, 로그를 출력하는 모든 메서드에 TraceId 파라미터를 추가해야 하는 문제가 발생했다. TraceId 를 파라미터로 넘기지 않고 이 문제를 해결할 수 있는 방법은 없을까?
이런 문제를 해결할 목적으로 새로운 로그 추적기를 만들어보자.
이제 프로토타입 버전이 아닌 정식 버전으로 제대로 개발해보자.
향후 다양한 구현제로 변경할 수 있도록 LogTrace 인터페이스를 먼저 만들고, 구현해보자.
기존에 만들었던 HelloTraceV2 와 동일한 기능. TraceId 를 동기화 하는 부분만 파라미터를 사용하는 것에서 TraceId traceIdHolder 필드를 사용하도록 변경되었다.
이젠 직전로그의 TraceId를 파라미터로 전달받지 않고 FieldLogTrace 필드의 traceIdHolder에 저장한다.
여기서 중요한 부분은 로그를 시작할 때 호출하는 syncTraceId() 와 로그를 종료할 때 호출하는 releaseTraceId() 이다.
syncTraceId()
TraceId 를 새로 만들거나 앞선 로그의 TraceId 를 참고해서 동기화하고, level도 증가한다.
최초 호출이면 TraceId 를 새로 만든다.
직전 로그가 있으면 해당 로그의 TraceId 를 참고해서 동기화하고, level도 하나 증가한다.
결과를 traceIdHolder 에 보관한다.
releaseTraceId()
메서드를 추가로 호출할 때는 level 이 하나 증가해야 하지만, 메서드 호출이 끝나면 level 이 하나 감소 해야 한다.
releaseTraceId() 는 level 을 하나 감소한다.
만약 최초 호출(level==0 )이면 내부에서 관리하는 traceId 를 제거한다.
[c80f5dbb] OrderController.request() //syncTraceId(): 최초 호출 level=0
[c80f5dbb] |-->OrderService.orderItem() //syncTraceId(): 직전 로그 있음 level=1증가
[c80f5dbb] | |-->OrderRepository.save() //syncTraceId(): 직전 로그 있음 level=2 증가
[c80f5dbb] | |<--OrderRepository.save() time=1005ms //releaseTraceId():level=2->1 감소
[c80f5dbb] |<--OrderService.orderItem() time=1014ms //releaseTraceId():level=1->0 감소
[c80f5dbb] OrderController.request() time=1017ms //releaseTraceId():level==0, traceId 제거
그런데 이걸 실제 서비스에 배포한다고 가정하면, 어떤 문제가 있을까?
바로 동시성 문제가 있음.
필드 동기화 - 동시성 문제
테스트 할 때는 문제가 없는 것 처럼 보인다. 사실 직전에 만든 FieldLogTrace 는 심각한 동시성 문제를 가지고 있다.
동시성 문제를 확인하려면 다음과 같이 동시에 여러번 호출해보면 된다.
그러니 트랜잭션이 구분이 안된다.
앞에 nio- ~~ 이게 쓰레드임.
보면 쓰레드는 다른데 트랜잭션 아이디가 구분이 안되는것 확인 가능.
원래는 쓰레드별로 트랜잭션 아이디가 달라야함!
쓰레드로 구분해보면
레벨이 뭔가 이상한것도 확인 가능함.
이것이 바로 동시성 문제.
FieldLogTrace 는 싱글톤으로 등록된 스프링 빈이다. 이 객체의 인스턴스가 애플리케이션에 딱 1 존재한다는 뜻이다. 이렇게 하나만 있는 인스턴스의 FieldLogTrace.traceIdHolder 필드를 여러 쓰레드가 동시에 접근하기 때문에 문제가 발생한다.
실무에서 한번 나타나면 개발자를 가장 괴롭히는 문제도 바로 이러한 동시성 문제이다.
threadA.start();
//sleep(2000);sleep(100); // 동시성 문제 발생threadB.start();
sleep(2000); // 메인쓰레드 종료 대기log.info("main exit");
thread-A 의 호출이 끝나면서 nameStore 의 결과를 반환받는데, 이때 nameStore 는 앞의 2번에서
userB 의 값으로 대체되었다. 따라서 기대했던 userA 의 값이 아니라 userB 의 값이 반환된다.
thread-B 의 호출이 끝나면서 nameStore 의 결과인 userB 를 반환받는다.
정리하면 다음과 같다.
Thread-A 는 userA 를 nameStore 에 저장했다.
Thread-B 는 userB 를 nameStore 에 저장했다.
Thread-A 는 userB 를 nameStore 에서 조회했다.
Thread-B 는 userB 를 nameStore 에서 조회했다.
동시성 문제
결과적으로 Thread-A 입장에서는 저장한 데이터와 조회한 데이터가 다른 문제가 발생한다. 이처럼 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라 한다. 이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타나지 않고, 트래픽이 점점 많아 질 수 록 자주 발생한다. 특히 스프링 빈 처럼 싱글톤 객체의 필드를 변경하며 사용할 때 이러한 동시성 문제를 조심해야 한다.
참고
이런 동시성 문제는 지역 변수에서는 발생하지 않는다. 지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당된다.
동시성 문제가 발생하는 곳은 같은 인스턴스의 필드(주로 싱글톤에서 자주 발생), 또는 static 같은 공용 필드에 접근할 때 발생한다.
동시성 문제는 값을 읽기만 하면 발생하지 않는다. 어디선가 값을 변경하기 때문에 발생한다.
그렇다면 지금처럼 싱글톤 객체의 필드를 사용하면서 동시성 문제를 해결하려면 어떻게 해야할까? 다시 파라미터를 전달하는 방식으로 돌아가야 할까? 이럴 때 사용하는 것이 바로 쓰레드 로컬이다.
ThreadLocal - 소개
쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소.
쉽게 말해 물건 보관 창구.
여러사람이 같은 물건 보관 창구를 사용해도 창구 직원은 사용자를 인식해 사용자별로 확실하게 물건을 구분해준다.
사용자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.remove()
추가로 쓰레드 로컬을 모두 사용하고 나면 꼭 ThreadLocal.remove() 를 호출해서 쓰레드 로컬에 저장된 값을 제
거해주어야 한다.
쉽게 이야기해서 다음의 마지막 로그를 출력하고 나면 쓰레드 로컬의 값을 제거해야 한다.
여기서는 releaseTraceId() 를 통해 level 이 점점 낮아져서 2 1 0이 되면 로그를 처음 호출한 부분으로 돌아온 것이다. 따라서 이 경우 연관된 로그 출력이 끝난 것이다. 이제 더 이상 TraceId 값을 추적하지 않아도 된다. 그래서 traceId.isFirstLevel() (level==0 )인 경우 ThreadLocal.remove() 를 호출해서 쓰레드 로컬에 저장된 값을 제거해준다.
쓰레드 로컬의 값을 사용 후 제거하지 않고 그냥 두면 WAS(톰캣)처럼 쓰레드 풀을 사용하는 경우에 심각한 문제가 발
생할 수 있다.
다음 예시를 통해서 알아보자.
한줄요약 : 제거하지 않으면 쓰레드 로컬에 이전 사용자 데이터가 남아 개인정보가 유출될 수 있다.
사용자A가 저장 HTTP를 요청했다.
WAS는 쓰레드 풀에서 쓰레드를 하나 조회한다.
쓰레드 thread-A 가 할당되었다.
thread-A 는 사용자A 의 데이터를 쓰레드 로컬에 저장한다.
쓰레드 로컬의 thread-A 전용 보관소에 사용자A 데이터를 보관한다.
사용자A의 HTTP 응답이 끝난다.
WAS는 사용이 끝난 thread-A 를 쓰레드 풀에 반환한다. 쓰레드를 생성하는 비용은 비싸기 때문에 쓰레드를 제거하지 않고, 보통 쓰레드 풀을 통해서 쓰레드를 재사용한다.
thread-A 는 쓰레드풀에 아직 살아있다. 따라서 쓰레드 로컬의 thread-A 전용 보관소에 사용자A 데이터도 함께 살아있게 된다.
사용자B가 조회를 위한 새로운 HTTP 요청을 한다.
WAS는 쓰레드 풀에서 쓰레드를 하나 조회한다.
쓰레드 thread-A 가 할당되었다. (물론 다른 쓰레드가 할당될 수 도 있다.)
이번에는 조회하는 요청이다. thread-A 는 쓰레드 로컬에서 데이터를 조회한다.
쓰레드 로컬은 thread-A 전용 보관소에 있는 사용자A 값을 반환한다.
결과적으로 사용자A 값이 반환된다.
사용자B는 사용자A의 정보를 조회하게 된다.
결과적으로 사용자B는 사용자A의 데이터를 확인하게 되는 심각한 문제가 발생하게 된다.
이런 문제를 예방하려면 사용자A의 요청이 끝날 때 쓰레드 로컬의 값을 ThreadLocal.remove() 를 통해서 꼭 제거해야 한다.
쓰레드 로컬을 사용할 때는 이 부분을 꼭! 기억하자.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
필드 동기화 - 개발
먼저 어떤 상황에 어떤 문제가 있고 이를 해결하기 위해서 쓰레드로컬을 이럴 때 쓴다!
를 알려줌.
앞서 로그 추적기를 만들면서 다음 로그를 출력할 때
트랜잭션ID와level을 동기화 하는 문제가 있었다.이 문제를 해결하기 위해
TraceId를 파라미터로 넘기도록 구현했다.이렇게 해서 동기화는 성공했지만, 로그를 출력하는 모든 메서드에
TraceId파라미터를 추가해야 하는 문제가 발생했다.TraceId를 파라미터로 넘기지 않고 이 문제를 해결할 수 있는 방법은 없을까?이런 문제를 해결할 목적으로 새로운 로그 추적기를 만들어보자.
이제 프로토타입 버전이 아닌 정식 버전으로 제대로 개발해보자.
향후 다양한 구현제로 변경할 수 있도록
LogTrace인터페이스를 먼저 만들고, 구현해보자.LogTrace인터페이스에는 로그 추적기를 위한 최소한의 기능인begin(),end(),exception()를 정의했다.이제 파라미터를 넘기지 않고
TraceId를 동기화 할 수 있는FieldLogTrace구현체를 만들어보자.기존에 만들었던 HelloTraceV2 와 동일한 기능.
TraceId를 동기화 하는 부분만 파라미터를 사용하는 것에서TraceId traceIdHolder필드를 사용하도록 변경되었다.이젠 직전로그의 TraceId를 파라미터로 전달받지 않고 FieldLogTrace 필드의 traceIdHolder에 저장한다.
여기서 중요한 부분은 로그를 시작할 때 호출하는
syncTraceId()와 로그를 종료할 때 호출하는releaseTraceId()이다.syncTraceId()TraceId를 새로 만들거나 앞선 로그의TraceId를 참고해서 동기화하고,level도 증가한다.TraceId를 새로 만든다.TraceId를 참고해서 동기화하고,level도 하나 증가한다.traceIdHolder에 보관한다.releaseTraceId()level이 하나 증가해야 하지만, 메서드 호출이 끝나면level이 하나 감소 해야 한다.releaseTraceId()는level을 하나 감소한다.level==0)이면 내부에서 관리하는traceId를 제거한다.테스트 코드
로그

필드 동기화 - 적용
적용하려면 FieldLogTrace 를 스프링빈에 등록해야함.
싱글톤으로 하나만 등록된다!
이후 v3 를 v2 복사해서 만든 다음
traceId 를 전부 삭제.
잘 실행되는것 볼 수 있음.
그런데 이걸 실제 서비스에 배포한다고 가정하면, 어떤 문제가 있을까?
바로 동시성 문제가 있음.
필드 동기화 - 동시성 문제
테스트 할 때는 문제가 없는 것 처럼 보인다. 사실 직전에 만든
FieldLogTrace는 심각한 동시성 문제를 가지고 있다.동시성 문제를 확인하려면 다음과 같이 동시에 여러번 호출해보면 된다.
그러니 트랜잭션이 구분이 안된다.
앞에 nio- ~~ 이게 쓰레드임.
보면 쓰레드는 다른데 트랜잭션 아이디가 구분이 안되는것 확인 가능.
원래는 쓰레드별로 트랜잭션 아이디가 달라야함!
쓰레드로 구분해보면
레벨이 뭔가 이상한것도 확인 가능함.
이것이 바로 동시성 문제.
FieldLogTrace는 싱글톤으로 등록된 스프링 빈이다. 이 객체의 인스턴스가 애플리케이션에 딱 1 존재한다는 뜻이다. 이렇게 하나만 있는 인스턴스의FieldLogTrace.traceIdHolder필드를 여러 쓰레드가 동시에 접근하기 때문에 문제가 발생한다.실무에서 한번 나타나면 개발자를 가장 괴롭히는 문제도 바로 이러한 동시성 문제이다.
동시성 문제 - 예제 코드
동시성 문제가 어떻게 발생하는지 단순화해서 알아보자.
테스트에도 롬복을 사용할 수 있게 build.gradle 에 아래 코드 추가
테스트에서 사용할 FieldService
테스트 코드 및 실행 결과
실행 결과를 보면 문제가 없다.
Thread-A는userA를nameStore에 저장했다.Thread-A는userA를nameStore에서 조회했다.Thread-B는userB를nameStore에 저장했다.Thread-B는userB를nameStore에서 조회했다.동시성 문제를 발생시키려면 아래처럼 중간에 sleep 을 아주 짧게 주면 된다
thread-A의 호출이 끝나면서nameStore의 결과를 반환받는데, 이때nameStore는 앞의 2번에서userB의 값으로 대체되었다. 따라서 기대했던userA의 값이 아니라userB의 값이 반환된다.thread-B의 호출이 끝나면서nameStore의 결과인userB를 반환받는다.정리하면 다음과 같다.
Thread-A는userA를nameStore에 저장했다.Thread-B는userB를nameStore에 저장했다.Thread-A는userB를nameStore에서 조회했다.Thread-B는userB를nameStore에서 조회했다.동시성 문제
결과적으로
Thread-A입장에서는 저장한 데이터와 조회한 데이터가 다른 문제가 발생한다. 이처럼 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라 한다. 이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타나지 않고, 트래픽이 점점 많아 질 수 록 자주 발생한다. 특히 스프링 빈 처럼 싱글톤 객체의 필드를 변경하며 사용할 때 이러한 동시성 문제를 조심해야 한다.참고
이런 동시성 문제는 지역 변수에서는 발생하지 않는다. 지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당된다.
동시성 문제가 발생하는 곳은 같은 인스턴스의 필드(주로 싱글톤에서 자주 발생), 또는 static 같은 공용 필드에 접근할 때 발생한다.
동시성 문제는 값을 읽기만 하면 발생하지 않는다. 어디선가 값을 변경하기 때문에 발생한다.
그렇다면 지금처럼 싱글톤 객체의 필드를 사용하면서 동시성 문제를 해결하려면 어떻게 해야할까? 다시 파라미터를 전달하는 방식으로 돌아가야 할까? 이럴 때 사용하는 것이 바로 쓰레드 로컬이다.
ThreadLocal - 소개
쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소.
쉽게 말해 물건 보관 창구.
여러사람이 같은 물건 보관 창구를 사용해도 창구 직원은 사용자를 인식해 사용자별로 확실하게 물건을 구분해준다.
사용자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 사용법
값 저장:
ThreadLocal.set(xxx)값 조회:
ThreadLocal.get()값 제거:
ThreadLocal.remove()주의
해당 쓰레드가 쓰레드 로컬을 모두 사용하고 나면
ThreadLocal.remove()를 호출해서 쓰레드 로컬에 저장 된 값을 제거해주어야 한다. 제거하는 구체적인 예제는 조금 뒤에 설명하겠다.이를 사용한 테스트
쓰레드 로컬 덕분에 쓰레드 마다 각각 별도의 데이터 저장소를 가지게 되었다. 결과적으로 동시성 문제도 해결되었다.
쓰레드 로컬 동기화 - 개발
기존 FieldLogTrace 에서 발생했던 동시성 문제를 ThreadLocal 로 해결해보자.
TraceId traceIdHolder필드를 쓰레드 로컬을 사용하도록ThreadLocal<TraceId> traceIdHolder로 변경하면 된다.
변경한 코드
ThreadLocal.remove()
추가로 쓰레드 로컬을 모두 사용하고 나면 꼭
ThreadLocal.remove()를 호출해서 쓰레드 로컬에 저장된 값을 제거해주어야 한다.
쉽게 이야기해서 다음의 마지막 로그를 출력하고 나면 쓰레드 로컬의 값을 제거해야 한다.
여기서는
releaseTraceId()를 통해level이 점점 낮아져서 2 1 0이 되면 로그를 처음 호출한 부분으로 돌아온 것이다. 따라서 이 경우 연관된 로그 출력이 끝난 것이다. 이제 더 이상TraceId값을 추적하지 않아도 된다. 그래서traceId.isFirstLevel()(level==0)인 경우ThreadLocal.remove()를 호출해서 쓰레드 로컬에 저장된 값을 제거해준다.테스트 코드
쓰레드 로컬 동기화 - 적용
우리 서비스에 ThreadLocalLogTrace 를 적용시키려면 아래 config 만 바꾸면 된다.
분리 잘 되되는것 확인 가능.
쓰레드 로컬 - 주의사항
쓰레드 로컬의 값을 사용 후 제거하지 않고 그냥 두면 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()를 통해서 꼭 제거해야 한다.쓰레드 로컬을 사용할 때는 이 부분을 꼭! 기억하자.
All reactions