본문 바로가기
프로그래밍/Java

[java] ThreadLocal에 관하여

by 사바라다 2021. 5. 15.

 

안녕하세요. 오늘은 ThreadLocal에 대해서 알아보도록 하겠습니다.

변수를 공유하는 방법

객체는 Heap 또는 Stack 메모리 영역에 배치시킬 수 있습니다. Heap 영역은 일반적으로 모든 thread에서 접근 할 수 있으며 stack은 thread 하나당 만들어 지는 메모리 영역으로 thread간 접근이 불가능한 것으로 알려져 있습니다.

아래 코드의 UserRepository 변수는 Heap 영역에 만들어진 객체를 가리키고 있으며 다른 곳에서도 해당 객체를 바로 접근할 수 있습니다. 함께 공유해서 사용하기 때문에 여러 thread에서 사용할 때 공유된 정보로써 제공할 수 있습니다. 따라서 만약 UserRepository가 설정 정보를 가지고 있고 이를 변경한다면 사용하고 있는 모든 곳에서 영향을 받게 됩니다.

public class UserService {

  private final UserRepository userRepository;

  public UserServiceV1(UserRepository userRepositoryV1) {
      this.userRepository = userRepository;
  }

  @Transactional
  public void signUp(SignUpRequest request) {
    userRepository.save(request.toUser());
}

반면 아래처럼 한 메서드 안에서 변수를 사용한다면 Stack 메모리에 올라갈 것이며 이 변수의 값은 thread에 안전한 변수로 사용될 수 있을 것입니다. 왜냐하면 하나의 thread에 한정적으로 사용할 수 있기 때문이죠. 아래예제를 보도록 하겠습니다. SignInRequest 파라미터를 받아서 String 변수인 email, password에 할당한 후 SignInResponse 객체를 생성하여 리턴합니다. 이경우 email, password 객체에 대해서는 외부에서 접근을 할 수 있는 방법이 없기 때문에 외부 thread에 안전한 변수로 사용할 수 있는 것입니다. 그렇기 때문에 변수 공유를 위해서는 파라미터로 받아서 사용해야하며 자신의 변수를 다른 곳에서 사용하게 하기 위해서는 리턴값으로 제공해야하는 단점이 있습니다.

public SignInResponse signIn(SignInRequest request) {

    String email = request.getEmail();
    String password = request.getPassword();

    return new SignInResponse(email, pasword);
}

ThreadLocal 이란

ThreadLocal을 정의하기전에 변수의 선언에 대해서 메모리 관점으로 알아보았습니다. 그 이유는 바로 ThreadLocal은 한 thread 안에서 파라미터 또는 리턴 값으로 정보를 제공하는 것이 아닌 다른 방법으로 thread 안에서의 값을 공유하는 방법을 제공해주기 때문입니다. 이렇게 Stack 영역에 변수를 선언하는 것에 대해서 단점을 해소해줄 수 있습니다.

ThreadLocal의 내부는 thread 정보를 key로 하여 값을 저장해두는 Map 구조를 가지고 있습니다. 기본적인 사용에는 get, set 메서드를 이용합니다. 아래의 코드를 통해 한번 사용해보도록 하겠습니다.

기본 사용 ( 선언, get, set )

public class UserTermService {

  public static ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>(); // ThreadLocal 사용을 위한 선언

  public UserTermService(Integer value) {
    threadLocalValue.set(value); // ThreadLocal에 set 값을 세팅
  }

  public void threadLocal_1() {
    System.out.println("threadLocalValue : " + threadLocalValue.get()); // ThreadLocal에 있는 값을 가져오기
  }
}

테스트 코드를 아래와 같이 작성하였고 예상하는 값은 threadLocalValue : 5 이며 정상적으로 호출 된 것을 확인할 수 있었습니다.

public class ThreadLocalTest {

    @Test
    public void threadLocalTest() {
        UserTermService userTermService = new UserTermService(5);
        userTermService.threadLocal_1(); // threadLocalValue : 5 출력 확인
    }
}

추가적인 메서드

추가적으로 ThreadLocal은 아래처럼 withInitial 메서드를 이용하면 lambda funciton을 파라미터로하여 기본값을 설정해 줄 수 있습니다.

public static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 5);

또한 remove 메서드를 이용하여 설정된 값을 삭제할 수 있습니다.

threadLocalValue.remove();

주의사항

ThreadLocal을 사용할 때 반드시 명심해야 할 부분이 있습니다. ThreadLocal은 Thead의 정보를 key로 하여 Map의 형식으로 데이터를 저장한 후 사용할 수 있는 자료구조를 가지고 있습니다. 따라서 만약 ThreadPool을 사용하여 thread를 재활용한다면 동일한 이전에 세팅했던 ThreadLocal의 정보가 남아있어 원치않는 동작을 할 수 있습니다. 따라서 ThreadPool을 사용하는 경우에는 반드시 모두 사용 후 THreadLocal의 값을 remove 메서드를 사용하여 값을 제거해주는것이 필요합니다.

활용

그렇다면 이러한 ThreadLocal은 어디서 사용하는 걸까요 ? 이는 Spring Security에서 사용자 인증 정보를 사용할 때 확인할 수 있었습니다. Spring Security를 사용하여 사용자 인증정보를 가져올 때 아래 코드를 통해서 가져올 수 있습니다. 해당 코드를 이용하면 방금 요청된 정보의 인증정보를 내가 만들고 있는 비즈니스 코드에서 이용할 수 있는 것입니다.

SecurityContextHolder.getContext().getAuthentication();

이 코드를 내부로 들어가면 아래와 같은 코드를 발견할 수 있었습니다. getContext() 메서드 내부가 ThreadLocal을 통해 구현되어 있었으며 Thread 별로 인증정보를 다르게 가지고 있는 것을 알 수 있었습니다.

final class ThreadLocalSecurityContextHolderStrategy implements
        SecurityContextHolderStrategy {
    // ~ Static fields/initializers
    // =====================================================================================

    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

    // ~ Methods
    // ========================================================================================================

    public void clearContext() {
        contextHolder.remove();
    }

    public SecurityContext getContext() {
        SecurityContext ctx = contextHolder.get();

        if (ctx == null) {
            ctx = createEmptyContext();
            contextHolder.set(ctx);
        }

        return ctx;
    }

    public void setContext(SecurityContext context) {
        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
        contextHolder.set(context);
    }

    public SecurityContext createEmptyContext() {
        return new SecurityContextImpl();
    }
}

마무리

오늘은 이렇게 ThreadLocal을 사용하기 위해 관련된 내용과 코드, 그리고 활용되고 있는 코드들에 대해서 알아보는 시간을 가져보았습니다.

어떠한 덧글이든 언제나 환영합니다.

감사합니다.

참고

멀티 코어를 100% 활용하는 자바 병렬 프로그래밍

baeldung_java-threadlocal

댓글3

  • djpibo@naver.com 2022.01.17 20:20

    좋은글 잘보았습니다.

    Static 변수랑 기능이 동일한 것으로 보이는데 ThreadLocal 과의 차이가 있나요?
    답글

    • 사바라다 2022.01.20 00:19 신고

      ThreadLocal은 힙 메모리 영역에 저장되고 실행되는 thread 내부에서 공유되는 값입니다. 그리고 해당 thread가 종료되면 그 값은 사라집니다. 반면 static은 메모리로 메서드 영역에 저장됩니다. 그렇기 때문에 처음 시스템과 함께 메모리에 올라오고 어떤 thread 에서 접근하든 동일한 메모리를 가리킵니다.

      이러한 특성 때문에 threadLocal은 security에 인증 정보를 담는다던지, 아니면 traceId를 담는다던지 하는 용도로 사용합니다. :)

  • @xftg77g 2022.08.12 15:54 신고

    정독 중입니다 좋은 글이 너무 많네용
    답글