Search
Duplicate
📒

[Java Study] 13-x. 동시성 문제 - 쓰레드 로컬

상태
미진행
수업
Java Study
주제
4 more properties
참고

이전 내용

NOTE
메서드의 로그를 출력하는 로그 추적기 객체를 생성했다!
메서드 시작부분에 로그 추적기의 begin 메서드, 종료부분에 end 메서드, 예외 상황에 exception 메서드를 출력한다
이때 요청마다 Id와 level을 동기화 하기 위해 파라미터로 TraceID를 넘겨주는 방식을 채택했다.
파라미터로 넘겨주지 않고 메서드 간에 traceId를 공유할 수 있는 방법이 없을까?

로그 추적기 구현 - 필드 사용

NOTE
각 클래스에서 싱글톤 빈인 로그추적기 객체를 주입받아 사용한다 → 즉 같은 객체를 사용! 객체의 필드에 TraceId를 저장하면, Controller, Service, Repository에서 해당 TraceID 공유가 가능해진다!
전체코드
@Slf4j public class FieldLogTrace implements LogTrace { private static final String START_PREFIX = "-->"; private static final String COMPLETE_PREFIX = "<--"; private static final String EX_PREFIX = "<X-"; private TraceId traceIdHolder; // 파라미터로 넘기는게 아닌 필드로 보관을 한다! }
Java
복사
필드 사용

syncTraceId 메서드

NOTE
private void syncTraceId(){ if (traceIdHolder == null) { traceIdHolder = new TraceId(); } else{ traceIdHolder = traceIdHolder.createNextId(); } }
Java
복사
로그 추적기의 TraceId가 null이라면 새로운 값을, 있다면 1을 증가시켜서 사용한다.

필드 사용의 장점/단점

NOTE

장점

필드에 TraceId를 보관하게 되면서 이를 주입받아 사용하는 Controller, Service, Repository 클래스가 TraceId를 공유할 수 있게 되었다.
이제 파라미터로 넘겨줄 필요가 없어, 모든 메서드에서 TraceId를 추가할 필요가 없어졌다.

단점 - 동시성문제

로그가 뭔가 이상한데..?
스프링은 싱글톤으로 객체를 관리하므로, 모든 요청 스레드에서 동일한 로그 추적기를 사용한다
우리의 목표는 각 스레드의 Controller, Service, Repository가 TraceId를 공유하는 것인데, 모든 스레드가 해당 TraceId를 공유하게 된것!
멀티 스레드 환경에서 싱글톤 빈의 상태를 유지하는 필드동기화 문제를 발생시킨 것이다.

동시성 문제

NOTE
스레드 간에는 코드, 데이터, 힙 영역을 공유하게 되어 힙 영역의 객체도 공유하게 된다!
이 때 두 스레드하나의 객체에 동시에 접근할 경우 문제가 발생한다.
두 스레드 모두 객체를 읽기만 하면 상관없지만, 한 스레드가 쓰기를 수행하는 경우 문제가 생긴다.

FieldService 클래스

NOTE
@Slf4j public class FieldService { private String nameStore; public String logic(String name) { log.info("저장 name={} -> nameStore={}", name, nameStore); nameStore = name; sleep(1000); // 1초뒤에 저장값을 반환해준다. log.info("조회 nameStore={}", nameStore); return nameStore; } private void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } }
Java
복사
이름을 1초가 지난이후 저장해주는 로직

동시성 문제 X (작업시간이 겹치지 않음)

Thread threadA = new Thread(userA); threadA.setName("thread-A"); Thread threadB = new Thread(userB); threadB.setName("thread-B"); threadA.start(); sleep(2000); // A가 완전히 실행될떄 까지 대기 threadB.start(); sleep(3000); // 메인 쓰레드 종료대기
Java
복사

동시성 문제 O (작업시간이 겹친다)

Thread threadA = new Thread(userA); threadA.setName("thread-A"); Thread threadB = new Thread(userB); threadB.setName("thread-B"); threadA.start(); sleep(100); // 동시성 문제 발생 O threadB.start(); sleep(3000); // 메인 쓰레드 종료대기
Java
복사
스레드A가 저장값을 받아내기전에, 스레드B의 값으로 덮어 씌워짐
이제 스레드A도 스레드B가 기록한 내용을 읽어버린다!

스레드 로컬

NOTE
스레드 로컬은 스레드 마다 별도의 내부 저장소를 제공하는 객체이다!
@Slf4j public class ThreadLocalService { private ThreadLocal<String> nameStore = new ThreadLocal<>(); // ... }
Java
복사
필드를 일반 String 대신 ThreadLocal<String> 사용
객체의 필드를 여러 스레드가 공유하는 것이 동시성 문제의 원인이므로, 필드를 스레드 로컬 객체로 두면 동시성 문제를 해결할 수 있다!

스레드 로컬 적용

NOTE
전체코드
get() : 스레드 로컬 저장소의 값을 가져온다.
set() : 스레드 로컬 저장소의 값을 변경한다.
remove() : 스레드 로컬 저장소의 값을 제거한다.

주의할점

해당 스레드가 스레드 로컬을 모두 사용하고 나면 remove()로 로컬의 값을 지워야한다!
WAS는 스레드 풀 방식으로 동작하는데, 스레드 반환시 remove로 스레드 로컬을 초기화시킨 후 반환하지 않으면, 다음 요청에서 이전 요청의 데이터를 접근할 수 있다.

결론

NOTE
WAS는 멀티 스레드를 지원하며 스프링 컨테이너는 싱글톤으로 객체를 관리한다.
스레드 간에 객체는 공유되는데, 값이 유지되는 객체의 필드에서 동시성 문제가 발생 가능성이 있다.
객체의 필드를 ThreadLOcal 객체로 하면, 스레드마다 별도의 저장소를 활용해서 동시성 문제를 방지할 수 있다.
스레드 풀에서 이전 요청의 정보를 활용하는 것을 방지하기 위해 스레드 로컬의 사용을 완료하면 remove 메서드로 초기화 시키자!