Skip to content

Latest commit

 

History

History
200 lines (150 loc) · 7.81 KB

2. 쓰레드 로컬.md

File metadata and controls

200 lines (150 loc) · 7.81 KB

스프링의 강의 리뷰📽

LoadMap Part : 스프링 핵심원리 고급편
Section : 2.Thread Local
CreateDate : 2022.11.14
UpdateDate : 2022.11.17

목차



IntelliJ 단축키





필드 동기화 - 개발

파라미터로 넘기도록 구현해서 동기화는 성공했지만, 로그를 출력하는 모든 메서드에 TreadId파라미터를 추가해야되는 문제가 발생한다.
파라미터로 넘기지 않고 이 문제를 해결할 수 있는 방법은 없을까

LogTrace 인터페이스

  • 인터페이스부터 시작하여 제대로된 로그 추적기를 만들어 보자
    • 로그추적기에 필요한, begin(), end(), exception()이 들어간다.
public interface LogTrace {
    TraceStatus begin(String message);

    void end(TraceStatus status);

    void exception(TraceStatus status, Exception e);
}

FieldLogTrace 구현체

  • 파라미터를 넘기지 않아도 TraceId를 동기화 할 수 있는 구현체 이다.
  • 추가된 코드
    • 변수로 TraceId를 담는 traceIdHolder가 생겼다.
      • 이로인해, 파라미터로 넘겨주지않아도 된다.
    • syncTraceId() : 동기화 이슈 처리해주는 매서드, 기존꺼가 있으면 굳이 새로만들지 않고 +1 해준다.
    • releaseTraceId() : 동기화 이슈 처리해주는 매서드, level이 1이상이면 삭제하지않고 -1 해준다.
public class FieldLogTrace implements LogTrace {
  //..생략
    private TraceId traceIdHolder; // traceId 동기화,  동시성 이슈 발생

    @Override
    public TraceStatus begin(String message) {
        syncTraceId();
        TraceId traceId = traceIdHolder;
      //..생략
    }

    private void syncTraceId() {
        if (traceIdHolder == null) {
            traceIdHolder = new TraceId();
        } else
            traceIdHolder = traceIdHolder.createNextId();
    }

    private void complete(TraceStatus status, Exception e) {
        //..생략
        releaseTraceId(); //추가
    }


    private void releaseTraceId() {
        if (traceIdHolder.isFirstLevel()) {
            traceIdHolder = null; // destroy
        } else
            traceIdHolder = traceIdHolder.createPreviousId();
    }


}

테스트

class FieldLogTraceTest {

  FieldLogTrace trace = new FieldLogTrace();

  @Test
  void begin_end_level2() {
    TraceStatus status1 = trace.begin("hello1");
    TraceStatus status2 = trace.begin("hello2");
    trace.end(status2);
    trace.end(status1);
  }

  @Test
  void begin_exception_level2() {
    TraceStatus status1 = trace.begin("hello1");
    TraceStatus status2 = trace.begin("hello2");
    trace.exception(status2, new IllegalStateException());
    trace.exception(status1, new IllegalStateException());
  }
}
  • 확인 결과 깔끔하게 잘 나온다.



app적용

하지만 이럴 경우, 동시성 문제가 발생한다.



동시성 문제

  • 심각한 동시성문제를 가지고있다. 호출을 여러번해보면 바로 확인할 수 있다.
  • 애플리케이션 실행 후 빠르게 호출을 2번하니 아래와 같이 나왔다.

  • 두개의 쓰레드를 사용해 로그 추적기를 실행했지만,
    • 트랜잭션 ID는 두개의 호출다 동일했으며,
    • level도 중첩되어서, 깊이가 이상하게 출력됨을 볼 수 있다.

동시성 문제 원인

  • FieldLogTrace 는 싱글톤으로 등록된 스프링 빈이다
    • 객체의 인스턴스가 애플리케이션에 딱 1 개만 존재한다는 뜻이다.
  • 이렇게 하나만 있는 인스턴스의 FieldLogTrace.traceIdHolder 필드를 여러 쓰레드가 동시에 접근하기 때문에 문제가 발생한다
  • 이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타나지 않고, 트래픽이 점점 많아질수록 자주 발생한다.
    • 특히 스프링 빈 처럼 싱글톤 객체의 필드를 변경하며 사용할때에는 더욱 조심해야된다.
  • 동시성 문제가 발생하는 곳은 같은 인스턴스의 필드(주로 싱글톤에서 자주 발생), 또는 static 같은 공용 필드에 접근할 때 발생한다
    • 지역변수는 상관없음

동시성 문제를 해결하기 위해 사용하는 것이 바로 쓰레드 로컬이다.


쓰레드로컬(ThreadLocal)

  • 쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다
    • 물건 보관 창구를 떠올리면 된다.

  • 자바는 언어차원에서 쓰레드 로컬을 지원하기 위한 java.lang.ThreadLocal 클래스를 제공한다.
  • set("")으로 전용보관소에 저장하며, get()으로 확인할 수가 있다.

ThreadLocal 사용법

  • 선언 : new ThreadLocal()
    • 예시 ) private ThreadLocal<String> nameStore = new ThreadLocal<>();
  • 값 저장 : ThreadLocal.set(xxx)
  • 값 조회 : ThreadLocal.get()
  • 값 제거 : ThreadLocal.remove()
    • 해당 쓰레드가 모두 사용하고 나면, 쓰레드 로컬에 저장된 값을 제거 해주어야한다.
  • 참고코드

적용 결과물 비교



ThreadLocal을 APP 적용

  • ThreadLocal을 구현한 코드를 APP에 적용하는 것은 간단하다.

LogTraceConfig

  • 전에 로그추적기를 Bean으로 등록해놓은 LogTraceConfig에서, 구현체만 바꿔주면된다.
@Configuration
public class LogTraceConfig {
    @Bean
    public LogTrace logTrace(){
        return new ThreadLocalLogTrace();
    }
}
  • 변경하고 애플리케이션을 두번 실행해본 결과 아래와 같이 나온다.

ThreadLocal 주의사항

  • 쓰레드로컬은 아래와 같이 WAS의 쓰레드 풀을 사용하는 경우 문제가 발생할 수 있다.

  • 쓰레드 풀은 커넥션풀 처럼 미리 만들어 놓은 쓰레드를 사용하고 반납할 수 있게 보관 및 관리 처리하는 곳이다.
    • 그렇기에, 사용이 끝난 쓰레드는 풀에 반환하게된다. 쓰레드를 생성하는 비용이 비싸기 때문에 보통 풀을 통해서 재사용하기 때문이다.
    • 만약 사용이 끝난 쓰레드의 정보를 지우지않고, 반환하게 된다면 재사용됐을시 기존의 정보가 남아 유출되기 때문이다.

이런 문제를 예방하려면 요청이 끝날 때 쓰레드 로컬의 값을 ThreadLocal.remove() 를 통해서 꼭 제거해야 한다