참고
이전 내용
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 메서드로 초기화 시키자!