본문 바로가기

Spring

[Spring] ThreadLocal이란?

ThreadLocal이란?

ThreadLocal은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다. 쉽게 말해서 여러사람이 사용하는 물건 보관 창구를 의미하며 여러사람(여러 쓰레드)이 ThreadLocal이라는 공용 창구를 사용하고 ThreadLocal이 사용자(쓰레드)별로 자원을 구분해준다.

 

그렇다면 ThreadLocal은 왜 어떤경우에 사용할까? 예를 들어 다음과 같은 상황이 있다고 가정해보자.

thread-A가 먼저 userA라는 변수를 필드에 저장했지만 thread-B가 userB라는 변수를 같은 필드에 저장했더니 userA를 저장했던 변수에 덮어 씌워져 버렸다. 이렇게 되면 thread-A가 자신이 저장했던 userA라는 값을 사용하기 위해 필드를 호출했을 때 userA가 아닌 userB가 호출된다. 

위와 같은 문제 상황을 동시성 문제라고 한다. 동시성 문제란 여러 쓰레드가 동시에 같은 자원에 접근하여 값을 변경할 때 발생하는 문제로 트래픽이 적은 상황에서는 잘 발생하지 않고 트래픽이 많아질수록 발생할 확률도 높아진다.

 

동시성 문제는 지역 변수에서는 쓰레드마다 각각 다른 메모리 영역이 할당되기 때문에 발생하지 않고 static 같은 공용 필드나 하나의 인스턴스를 생성해서 사용하는 싱글톤(인스턴스 필드)에서 자주 발생한다. 또한 값을 변경하지 않고 조회만 하는 경우에는 동시성 문제가 발생하지 않는다.

 

하지만 ThreadLocal을 사용하면 ThreadLocal이 쓰레드별로 자원들 구분해서 관리해주기 때문에 동시성 문제를 해결해 줄 수 있다.

thread-A가 ThreadLocal의 set() 메서드를 이용해 userA를 저장하면 ThreadLocal이 thread-A 전용 보관소에 userA라는 값을 저장해놓는다.

그런다음 thread-B가 똑같이 set() 메서드를 사용해 userB라는 값을 ThreadLocal에 저장하면 thread-B 전용 보관소에 userB라는 값이 저장된다. 

그리고 나서 각 쓰레드가 ThreadLocal의 get() 메서드를 이용해 값을 조회하면 이렇게 쓰레드 별로 값을 따로 저장하기 때문에 자신이 저장했던 원하는 값을 조회해올 수 있게된다.

 

자바는 언어차원에서 쓰레드 로컬을 지원하기 위해 java.lang.ThreadLocal 클레스를 제공해준다.

ThreadLocal 사용 방법

private ThreadLocal<String> threadLocal = new ThreadLocal<>();

 

위와 같이 제네릭 타입을 지정해서 ThreadLocal 객체를 생성해 주고 아래 메서드를 사용해 값의 저장, 조회, 삭제를 수행할 수 있다.

 

  • 값 저장 : ThreadLocal.set()
  • 값 조회 : ThreadLocal.get()
  • 값 제거 : ThreadLocal.remove()

ThreadLocal 예제 코드

ThreadLocalService

nameStore라는 ThreadLocal에 name을 저장하고 nameStore에서 값을 조회하는 메서드

@Slf4j
public class ThreadLocalService {

    private ThreadLocal<String> nameStore = new ThreadLocal<>();

    public String logic(String name) {
        log.info("저장 name={} -> nameStore={}", name, nameStore.get());
        nameStore.set(name);
        sleep(1000);
        log.info("조회 nameStore={}", nameStore.get());
        return nameStore.get();
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ThreadLocalServiceTest

thread-A thread-B Thread를 각각 생성해주고 Runnable 인터페이스를 구현해 service 클래스의 logic() 메서드가 실행되도록 한다.

@Slf4j
public class ThreadLocalServiceTest {

    private ThreadLocalService service = new ThreadLocalService();

    @Test
    void field() {
        log.info("main start");

        Runnable userA = () -> {
            service.logic("userA");
        };

        Runnable userB = () -> {
            service.logic("userB");
        };

        Thread threadA = new Thread(userA);
        threadA.setName("thread-A");

        Thread threadB = new Thread(userB);
        threadB.setName("thread-B");

        threadA.start();
        // sleep(2000); // 동시성 문제 발생 X
        sleep(100);
        threadB.start();
        sleep(3000); // 메인 쓰레드 종료 대기
        log.info("main exit");
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

테스트 결과

테스트 결과를 보면 처음에 값을 조회했을 때 null값이 조회됐다가 각각 userA와 userB를 ThreadLocal에 저장하고 1초 뒤에 다시 조회했을 때 쓰레드 별로 userA와 userB가 각각 잘 출력된 것을 확인할 수 있다.

 

만약 ThreadLocal을 사용하지 않고 아래와 같이 변수를 선언하고 수행하면 thread-A와 thread-B의 값이 모두 userB로 호출되어 버린다.

private String nameStore;