본문 바로가기
Java

[Java] 공유 자원의 동시 접근 문제를 해결하는 ThreadLocal 정리

by slf4j 2026. 1. 24.
반응형

ThreadLocal은 멀티 스레드 환경에서 공유 자원에 대한 동시 접근으로 발생하는 문제를 회피하기 위해, 스레드별로 독립적인 값을 관리하도록 돕는 기술입니다. 동시성 이슈란 여러 스레드가 동시에 하나의 자원에 접근하면서, 의도하지 않은 값 변경이나 예측 불가능한 동작이 발생하는 상황을 의미합니다.

  • 예시
@Slf4j
public class ExampleService {

    private String name; // 동시성 이슈 발생 코드

    public String getName() {
        log.info("getName: {}", this.name);
        return this.name;
    }

    public void setName(String name) {
        log.info("beforeName: {}, afterName: {}", this.name, name);
        this.name = name;
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {}
    }
}

getter, setter만 존재하는 단순한 구조의 Service입니다. 동시성 이슈를 재현하기 위해 setName 에 접근한 Thread는 3초 간 sleep하도록 설정했습니다.

@Slf4j
public class MyTest {

    public ExampleService service = new ExampleService();

    @Test
    void test() throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            service.setName("name1");
            service.getName();
        });
        Thread thread2 = new Thread(() -> {
            service.setName("name2");
            service.getName();
        });
        thread1.setName("thread1");
        thread2.setName("thread2");

        thread1.start();
        Thread.sleep(500);
        thread2.start();

        Thread.sleep(5000); // 메인스레드 종료 대기
				log.info("main exit");
    }
}

각 스레드는 각각 ExampleService.name에 세팅후 다시 name 값을 조회하는 로직을 실행합니다.

위 테스트를 실행하면 thread1이 실행된 후 0.5초 뒤 thread2가 실행됩니다.

  • 위 테스트 실행 결과

ExampleService 내부 전역 변수에 두 스레드가 동시에 접근했고, name 변수에 대한 값이 서로 덮어씌워지는 문제가 발생하여 두 스레드 모두 name2를 리턴받습니다. 각 스레드 별로 세팅한 name을 리턴받는 방법으로 ThreadLocal을 사용하면 해당 문제를 해결할 수 있습니다.

  • 동시성 이슈를 해결한 ExampleService
@Slf4j
public class ExampleService {

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

    public String getName() {
			  log.info("getName: {}", this.name.get());
        return this.name.get();
    }

    public void setName(String name) {
			  log.info("beforeName: {}, afterName: {}", this.name.get(), name);
        this.name.set(name);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {}
    }
}

전역변수 name을 ThreadLocal로 변경했습니다.

  • 테스트 실행 결과

각 스레드 별로 세팅한 name값을 리턴받습니다.

ThreadLocal

ThreadLocal은 기존 변수와 다르게 해당 변수를 참조하는 Thread별로 해당 변수의 복사본을 가집니다.

위 예시에서는 thread1은 name1으로 초기화된 name 변수, thread2는 name2로 초기화 된 name 변수 복사본을 가지고 있습니다.

해당 복사본을 참조하는 코드가 있거나, 스레드가 살아 있으면서 ThreadLocal 변수에 접근할 수 있는 동안에는 스레드는 계속 복사본을 가지고 있습니다.

해당 복사본을 참조하는 코드가 없고, 스레드가 사라진 후에는 해당 스레드의 모든 복사본이 가비지 컬렉션의 대상이 됩니다.

Constructor

ThreadLocal은 기본 생성자 1개만 존재합니다.

ThreadLocal<Member> memberThreadLocal = new ThreadLocal<>();
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
ThreadLocal<? extends Collection> collectionThreadLocal = new ThreadLocal<>();

Method

public

void set(T), T get(), void remove() 세 가지가 존재합니다.

  • set 예시
ThreadLocal<String> name = new ThreadLocal<>();
name.set("name1");

위 코드를 실행하는 스레드의 name 변수 복사본을 “name1”으로 설정합니다.

  • get 예시
ThreadLocal<String> name = new ThreadLocal<>();
name.set("name1");
name.get(); // name1

위 코드를 실행하는 스레드의 name 변수 복사본에 있는 값을 반환합니다.

  • remove 예시
ThreadLocal<String> name = new ThreadLocal<>();
name.set("name1");
name.remove();
name.get(); // null

위 코드를 실행하는 스레드의 name 변수 복사본에 있는 값을 제거합니다.

protected

T initialValue() 한 가지가 존재합니다. ThreadLocal을 재정의해서 사용할 수 있습니다.

ThreadLocal에 기본값을 지정할 수 있습니다. 스레드가 ThreadLocal변수에 대해 set 메서드를 호출하지 않았고, 스레드가 처음으로 get 메서드를 사용하여 변수에 접근할 때 호출됩니다.

  • initialValue 예시
ThreadLocal<String> name = new ThreadLocal<>() {
    @Override
    protected String initialValue() {
        return "name1";
    }
};
name.get(); // name1, initialValue 호출 됨.

name.remove();
name.get(); // name1, initialValue 호출 됨.

name.get(); // name1

name.set("name2");
name.get(); // name2;

위와 비슷한 방법으로 public ThreadLocal<S> withInitial(Supplier<? extends S> supplier) 을 호출하는 방법이 있습니다.

withInitial 을 호출할 경우 ThreadLocal 내부 클래스인 SuppliedThreadLocal 을 생성합니다.

Supplier 가 null일 경우 java.lang.NullPointerException 를 던집니다.

  • withInitial 예시
ThreadLocal<String> name = ThreadLocal.withInitial(() -> "name1");
name.get(); // name1, SuppliedThreadLocal에서 재정의한 initialValue 호출 됨.

ThreadLocal<String> threadLocal =
        ThreadLocal.withInitial(null); // NullPointerException

주의사항

ThreadPool을 사용하는 환경에서는 모든 처리가 끝난 ThreadLocal의 복사본을 remove로 지워줘야합니다.

복사본을 지우지 않은 Thread가 ThreadPool로 반환된 후에 어떤 새로운 요청으로 인해서 해당 Thread가 ThreadPool에서 꺼내졌다면, 복사본이 지워지지 않은 ThreadLocal의 복사본 값을 재사용 할 수가 있기 때문입니다.

참고 문서

스프링 핵심 원리 - 고급편 - 인프런 | 강의

ThreadLocal (Java Platform SE 8 )

반응형

'Java' 카테고리의 다른 글

[Java] @WebMvcTest 테스트 시간 20% 줄이기  (3) 2026.01.29