Language/Java

[Java] 스레드 로컬 (Thread Local )

Junhong Kim 2023. 8. 27. 18:21
728x90
반응형

스레드 로컬이란?

여러 스레드가 같은 인스턴스의 필드에 접근하면 최초 스레드가 보관한 데이터는 다른 스레드에 의해서 덮어 씌워질 수 있습니다. 예를 들어, ThreadA가 특정 인스턴스의 필드에 `userA`라는 값을 저장하고 이후 ThreadB가 동일한 인스턴스의 필드에 `userB`라는 값을 저장하면 ThreadA가 인스턴스 필드에 저장한 `userA` 값이 `userB` 값으로 덮어 씌워집니다. 이후 ThreadA가 인스턴스의 필드를 조회할 때 `userB`가 조회되면서 의도하지 않은 동작을 하게 됩니다.

 

이러한 문제점을 해결하기 위해서 스레드 로컬(Java에서는 java.lang.ThreadLocal 클래스를 제공)을 사용하는 것을 고려해볼 수 있습니다. 스레드 로컬(Thread Local)이란 각 스레드마다 별도의 내부 저장소를 제공하여 해당 스레드에서만 접근할 수 있는 특별한 저장소를 의미 합니다. 예를 들어, ThreadA가 특정 인스턴스의 필드에 저장한 `userA` 값은 ThreadA만 확인할 수 있고, ThreadB가 동일한 인스턴스의 필드에 저장한 `userB` 값은 ThreadB만 확인 할 수 있게 됩니다. 따라서 같은 인스턴스의 필드에 접근해도 스레드 로컬을 사용하면 이전과 같은 문제가 발생하지 않습니다.

스레드 로컬 사용법

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("userA") // 저장
threadLocal.get(); // 조회
threadLocal.remove(); // 제거

스레드 로컬 내부 로직

public class ThreadLocal<T> {
    // ...

    public void set(T value) {
        Thread t = Thread.currentThread(); // 현재 스레드를 가져와서
        ThreadLocalMap map = getMap(t); // 현재 스레드에 ThreadLocalMap 가져오기
        if (map != null) {
            map.set(this, value); // 있으면 기존 스레드 로컬에 저장 
        } else {
            createMap(t, value); // 없으면 스레드 로컬 생성
        }
    }


    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this); // 현재 스레드에 ThreadLocalMap.Entry 가져오기
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result; // ThreadLocal<T>의 값 반환
            }
        }
        return setInitialValue(); // 없으면 스레드 로컬 초기화
    }


    public void remove() {
       ThreadLocalMap m = getMap(Thread.currentThread());
       if (m != null) {
           m.remove(this); // ThreadLocalMap에서 현제 스레드 로컬 제거
       }
    }

    // ...
}

스레드 로컬 적용 전

@Slf4j
public class FieldService {

    private String nameStore;

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

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


@Slf4j
public class FieldServiceTest {

    private FieldService fieldService = new FieldService();

    @Test
    void field() {
        log.info("main start");
        Runnable userA = () -> {
            fieldService.logic("userA"); // 1초 걸리는 로직
        };
        Runnable userB = () -> {
            fieldService.logic("userB");
        };

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

        threadA.start();
//        sleep(2_000); // 동시성 문제 발생X
        sleep(100); // 동시성 문제 발생O
        threadB.start();

        sleep(3_000); // 메인 쓰레드 종료 대기
        log.info("main exit");
    }

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

---

15:27:11.390 [thread-A] INFO hello.advanced.trace.threadlocal.code.FieldService - 저장 name=userA -> nameStore=null
15:27:11.493 [thread-B] INFO hello.advanced.trace.threadlocal.code.FieldService - 저장 name=userB -> nameStore=userA
15:27:12.397 [thread-A] INFO hello.advanced.trace.threadlocal.code.FieldService - 조회 nameStore=userB
15:27:12.498 [thread-B] INFO hello.advanced.trace.threadlocal.code.FieldService - 조회 nameStore=userB

ThreadA를 실행 후 0.1초 대기 후 ThreadB를 실행하게 되면 같은 인스턴스 필드(nameStore)인 `String`을 공유하고 있기 때문에 모든 스레드가 `userB` 를 반환됩니다. ThreadA는 `userA`가 반환되야 하고, ThreadB는 `userB`가 반환되어야 하는데 의도하지 않는 결과가 반환됩니다.

스레드 로컬 적용 후

@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(1_000);
        log.info("조회 nameStore={}", nameStore.get());
        return nameStore.get();
    }

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

---

15:27:36.753 [thread-A] INFO hello.advanced.trace.threadlocal.code.ThreadLocalService - 저장 name=userA -> nameStore=null
15:27:36.858 [thread-B] INFO hello.advanced.trace.threadlocal.code.ThreadLocalService - 저장 name=userB -> nameStore=null
15:27:37.761 [thread-A] INFO hello.advanced.trace.threadlocal.code.ThreadLocalService - 조회 nameStore=userA
15:27:37.863 [thread-B] INFO hello.advanced.trace.threadlocal.code.ThreadLocalService - 조회 nameStore=userB

동일하게 ThreadA를 실행 후 0.1초 대기 후 ThreadB를 실행하고 같은 인스턴스 필드(nameStore)인 `ThreadLocal<String>`을 공유하고 있으면 스레드마다 다른 결과가 반환됩니다. ThreadA는 `userA`를 반환하고, ThreadB`는 `userB`를 반환합니다.

스레드 로컬 주의사항

스레드 로컬을 모두 사용하고 나면 `remove()`를 호출해서 스레드 로컬에 저장된 데이터를 반드시 제거해야 합니다. 스레드 로컬 저장소에 있는 데이터를 삭제하지 않으면 해당 스레드 로컬의 데이터는 계속 남아있습니다. WAS처럼 스레드 풀을 사용하는 경우, 스레드 로컬에 데이터가 저장된 스레드를 스레드 풀에 반환하고, 이후 스레드 풀에서 동일한 스레드를 꺼내서 사용하면 이전에 저장한 스레드 로컬의 데이터가 반환되어 치명적인 버그가 발생할 수 있습니다.

 

[예시]

1. `사용자A`가 스레드 로컬 `thread-A 전용 보관소`에 데이터를 저장합니다.

2. `사용자A`가 스레드 로컬을 사용한 후 `thread-A 전용 보관소`에 저장된 데이터를 제거하지 않고 스레드 풀에 반환합니다.

3. 이후 `사용자B`가 스레드 풀에서 `사용자A`가 사용했던 스레드(thread-A)를 할당 받아 해당 스레드 로컬의 데이터를 조회하면 `사용자A`가 `thread-A 전용 보관소`에 저장해둔 데이터(데이터: 사용자A)를 반환 받습니다. 이처럼 `사용자B`가 `사용자A`의 데이터를 사용할 수 있게되면서 시스템에 치명적인 문제가 발생할 수 있습니다.

1. 사용자A가 스레드 로컬에 데이터 저장
2. 사용자A가 스레드 로컬의 데이터를 제거하지 않고 스레드 풀에 반환
3. 사용자B가 사용자 A가 사용했던 thread-A를 할당 받아 조회하여 사용자A의 데이터를 조회할 수 있게 된다.

요약

1. 스레드 로컬 데이터 저장 (set)

- ThreadA가 `userA`라는 데이터를 스레드 로컬에 저장하면 스레드 로컬은 `thread-A 전용 보관소`에 데이터를 보관한다.

- ThreadB는 `userB`라는 데이터를 스레드 로컬에 저장하면 스레드 로컬은 `thread-B 전용 보관소`에 데이터를 보관한다.

2 스레드 로컬 데이터 조회 (get)

- ThreadA가 스레드 로컬 데이터를 조회하면 `thread-A 전용 보관소`에서 `userA` 데이터를 반환한다.

- ThreadB가 스레드 로컬 데이터를 조회하면 `thread-B 전용 보관소`에서 `userB` 데이터를 반환한다.

3. 스레드 로컬 데이터 제거 (remove)

- 스레드 로컬 데이터는 사용 후 반드시 제거해야 한다.

- 스레드 로컬 데이터를 제거하지 않으면 해당 스레드가 스레드 풀에 반환되어도 스레드 로컬에 있는 데이터는 남아있다.

- 스레드 풀에 있는 스레드를 재사용하면 이전에 저장된 스레드 로컬 데이터를 조회할 수 있어서 치명적인 버그를 초래한다.

참고

스프링 핵심 원리 고급편 - ThreadLocal (김영한님)

스프링 핵심 원리 - 고급편

728x90
반응형