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
이 문제는 FieldLogTrace는 싱글톤으로 등록된 스프링 빈이다. 이 객체가 인스턴스가 애플리케이션에 딱 1 존재한다는 뜻이다. 이렇게 하나만 있는 인스턴스의 FieldLogTrace.traceIdHolder 필드를 여러 쓰레드가 동시에 접근하기 때문에 문제가 발생한다.
[Test worker] INFO hello.advanced.trace.threadlocal.FieldServiceTest -- main start
[thread-A] INFO hello.advanced.trace.threadlocal.code.FieldService -- 저장 name=userA -> nameStore=null
[thread-A] INFO hello.advanced.trace.threadlocal.code.FieldService -- 조회 nameStore=userA
[thread-B] INFO hello.advanced.trace.threadlocal.code.FieldService -- 저장 name=userB -> nameStore=userA
[thread-B] INFO hello.advanced.trace.threadlocal.code.FieldService -- 조회 nameStore=userB
[Test worker] INFO hello.advanced.trace.threadlocal.FieldServiceTest -- main exit
실행 결과를 보면 문제가 없다
Thread-A 는 userA 를 nameStore 에 저장했다.
Thread-A 는 userA 를 nameStore 에서 조회했다.
Thread-B 는 userB 를 nameStore 에 저장했다.
Thread-B 는 userB 를 nameStore 에서 조회했다.
✅ 동시성문제발생코드
이번에는 sleep(100) 을 설정해서 thread-A 의 작업이 끝나기 전에 thread-B 가 실행되도록 해보자.
참고로 FieldService.logic() 메서드는 내부에 sleep(1000)으로 1초의 지연이 있다. 따라서 1초 이후에 호출하면 순서대로 실행할 수 있다. 다음에 설정할 100(ms)는 0.1초이기 때문에 thread-A 의 작업이 끝나기 전에 thread-B 가 실행된다.
//sleep(2000); //동시성 문제 발생X
sleep(100); //동시성 문제 발생O
결과
[Test worker] INFO hello.advanced.trace.threadlocal.FieldServiceTest -- main start
[thread-A] INFO hello.advanced.trace.threadlocal.code.FieldService -- 저장 name=userA -> nameStore=null
[thread-A] INFO hello.advanced.trace.threadlocal.code.FieldService -- 조회 nameStore=userA
[thread-B] INFO hello.advanced.trace.threadlocal.code.FieldService -- 저장 name=userB -> nameStore=userA
[thread-B] INFO hello.advanced.trace.threadlocal.code.FieldService -- 조회 nameStore=userB
[Test worker] INFO hello.advanced.trace.threadlocal.FieldServiceTest -- main exit
먼저 thread-A 가 userA 값을 nameStore 에 보관한다.
0.1초 이후에 thread-B 가 userB 의 값을 nameStore 에 보관한다. 기존에 nameStore 에 보관되어 있던 userA 값은 제거되고 userB 값이 저장된다.
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 입장에서는 저장한 데이터와 조회한 데이터가 다른 문제가 발생한다. 이처럼 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라 한다. 이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타나지 않고, 트래픽이 점점 많아 질 수 록 자주 발생한다. 특히 스프링 빈 처럼 싱글톤 객체의 필드를 변경하며 사용할 때 이러한 동시성 문제를 조심해야 한다.
5️⃣ ThreadLocal - 소개
쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다. 쉽게 이야기해서 물건 보관 창구를 떠올리면 된다. 여러 사람이 같은 물건 보관 창구를 사용하더라도 창구 직원은 사용자를 인식해서 사용자별로 확실하게 물건을 구분해준다.
사용자A, 사용자B 모두 창구 직원을 통해서 물건을 보관하고, 꺼내지만 창구 지원이 사용자에 따라 보관한 물건을 구분해주는 것이다.
✅ 일반적인 변수 필드
여러 쓰레드가 같은 인스턴스의 필드에 접근하면 처음 쓰레드가 보관한 데이터가 사라질 수 있다.
thread-A 가 userA 라는 값을 저장하고
thread-B 가 userB 라는 값을 저장하면 직전에 thread-A 가 저장한 userA 값은 사라진다.
✅ 쓰레드로컬
쓰레드 로컬을 사용하면 각 쓰레드마다 별도의 내부 저장소를 제공한다. 따라서 같은 인스턴스의 쓰레드 로컬 필드에 접근해도 문제 없다.
thread-A가userA라는 값을 저장하면 쓰레드 로컬은thread-A 전용 보관소에 데이터를 안전하게 보관한다.
쓰레드 로컬을 통해서 데이터를 조회할 때도 thread-A 가 조회하면 쓰레드 로컬은 thread-A 전용 보관소에서userA 데이터를 반환해준다. 물론 thread-B 가 조회하면 thread-B 전용 보관소에서 userB 데이터를 반환해준다.
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.
섹션3. 쓰레드 로컬 - ThreadLocal
1️⃣ 필드 동기화 - 개발
섹션2. 에서 로그 추적기를 만들었다. 하지만 동기화 문제점이 있었다.
동기화 문제는 해결되었지만 로그를 출력하는 모든 메서드에서 TraceId 파라미터를 다 추가해야 하는 문제가 있다.
✅ 테스트
2️⃣ 필드 동기화
FieldLogTrace를 애플리케이션에 적용해보자.
첫 번째 작업은 LogTrace 스프링 빈 등록 → 향후 구현체를 편리하게 변경할 수 있다는 장점이 생김
강의에서는 1년에 한번씩 일어나는 문제가 있다고한다.
바로 동시성 문제3️⃣ 필드 동기화 - 동시성 문제
동시성 문제 확인 → 로직을 1초 안에 2번 실행 해보기
기대하는 결과값
🚨 동시성 문제
이 문제는 FieldLogTrace는 싱글톤으로 등록된 스프링 빈이다. 이 객체가 인스턴스가 애플리케이션에 딱 1 존재한다는 뜻이다. 이렇게 하나만 있는 인스턴스의 FieldLogTrace.traceIdHolder 필드를 여러 쓰레드가 동시에 접근하기 때문에 문제가 발생한다.
→ 실무에서 한번 나타나면 개발자들을 괴롭힌다.
4️⃣ 동시성 문제 - 예제 코드
Thread-A는userA를nameStore에 저장했다.Thread-A는userA를nameStore에서 조회했다.Thread-B는userB를nameStore에 저장했다.Thread-B는userB를nameStore에서 조회했다.✅ 동시성 문제 발생 코드
sleep(100)을 설정해서thread-A의 작업이 끝나기 전에thread-B가 실행되도록 해보자.참고로
FieldService.logic()메서드는 내부에sleep(1000)으로 1초의 지연이 있다. 따라서 1초 이후에 호출하면 순서대로 실행할 수 있다. 다음에 설정할 100(ms)는 0.1초이기 때문에thread-A의 작업이 끝나기 전에thread-B가 실행된다.thread-A가userA값을nameStore에 보관한다.thread-B가userB의 값을nameStore에 보관한다. 기존에nameStore에 보관되어 있던userA값은 제거되고userB값이 저장된다.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에서 조회했다.✅동시성 문제
5️⃣ ThreadLocal - 소개
✅ 일반적인 변수 필드
thread-A가userA라는 값을 저장하고thread-B가userB라는 값을 저장하면 직전에thread-A가 저장한userA값은 사라진다.✅ 쓰레드 로컬
가userA라는 값을 저장하면 쓰레드 로컬은thread-A 전용 보관소에 데이터를 안전하게 보관한다.thread-A가 조회하면 쓰레드 로컬은thread-A전용 보관소에서userA데이터를 반환해준다. 물론thread-B가 조회하면thread-B전용 보관소에서userB데이터를 반환해준다.6️⃣ ThreadLocal - 예제 코드
✅ ThreadLocal 사용법
7️⃣ 쓰레드 로컬 동기화 - 개발
FieldLogTrace에서 발생했던 동시성 문제를ThreadLocal로 해결해보자.TraceId traceIdHolder필드를 쓰레드 로컬을 사용하도록ThreadLocal<TraceId> traceIdHolder로변경하면 된다.ThreadLocalLogTrace를 새로 만들자.8️⃣ 쓰레드 로컬 동기화 - 적용
동시성 문제가 있는
FieldLogTrace대신에 문제를 해결한ThreadLocalLogTrace를 스프링 빈으로 등록정상로그
🚨 동시 요청
1초 안에 2번 실행해보자.
실행결과
nio-8080-exec-3,nio-8080-exec-4별로 로그가 정확하게 나누어 진 것을 확인할 수 있다.9️⃣ 쓰레드 로컬 - 주의 사항
thread-A가 할당되었다.thread-A는사용자A의 데이터를 쓰레드 로컬에 저장한다.thread-A전용 보관소에사용자A데이터를 보관한다.thread-A를 쓰레드 풀에 반환한다. 쓰레드를 생성하는 비용은 비싸기 때문에 쓰레드를 제거하지 않고, 보통 쓰레드 풀을 통해서 쓰레드를 재사용한다.
thread-A는 쓰레드풀에 아직 살아있다. 따라서 쓰레드 로컬의thread-A전용 보관소에사용자A데이터도 함께 살아있게 된다
ThreadLocal.remove()를 통해서 꼭 제거해야 한다.
All reactions