[Spring] ThreadLocal 개념과 동작 방식

Loopy·2023년 1월 4일
0

스프링

목록 보기
4/16
post-thumbnail

일반적인 변수 필드

여러 스레드가 같은 인스턴스의 필드에 접근하면, 처음 스레드가 보관한 데이터가 사라질 수 있다. 또한 동시성 문제가 발생한다. 이때, 스레드 로컬을 사용하여 해결할 수 있다.

📚 동시성 문제
같은 인스턴스 필드에 두개 이상의 스레드가 접근해서 값을 변경할 때, 저장한 값과 조회 결과값이 달라지는 현상을 의미한다. 지역 변수가 아닌 객체가 하나밖에 없는 싱글톤이나 static 전역 변수에서 발생한다.

☁️ 스레드 로컬이란?

해당 스레드만 접근할 수 있는 특별한 저장소를 말한다.
스레드 로컬은 각 스레드마다 별도의 내부 저장소를 제공하고 관리하기 때문에, 동시에 같은 인스턴스의 스레드 로컬 필드에 접근해도 문제가 없다.

쉽게 말해서 창구를 예시로 들자면, 여러 사람이 한 공간에 물건을 보관해도 창구 직원이 사용자별로 물건을 구별해주는 것이다.

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

☁️ Java ThreadLocal 동작 방식

자바 스레드 로컬은 해시맵(HashMap) 방식으로 동작한다.

각 스레드는 ThreadLocal 의 내부 클래스인 ThreadLocalMap 객체를 참조 필드로 가지고 있기 때문에 해당 필드에 새로 생성한 맵을 저장해준다. 쉽게 말해서 스레드마다 자신만의 저장소가 생기는 것이다.

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}

ThreadLocalMap 생성

ThreadLocalMap 생성자를 보면 알 수 있듯이, 각 원소의 타입은Entry 이다. 즉 Entry 배열인 것이다.

🔖 Entry?
EntryThreadLocal의 내부 정적 클래스로 value 에는 해당 동시성 문제를 해결하고자 하는 필드의 값이 들어간다.

생성자에서는 해시코드 값을 계산하여 해당 자리에 초기화 할 첫번째 값을 넣어주고 있는데, 해시코드는 현재 ThreadLocal 객체의 해시코드 값을 활용해서 생성하고 있다.

만약 아래와 같이 여러개의 ThreadLocal 필드가 존재한다 가정해보자. 각 스레드 별로 스레드에 이미 생성되어 있는 ThreadLocalMap 을 가져와서 저장하기 때문에 각 ThreadLocal 의 해시코드 값으로 구분이 필요한 것이다.

 private final ThreadLocal<String> nameStore = new ThreadLocal<>();
 private final ThreadLocal<Integer> money = new ThreadLocal<>();

값 저장

값을 저장하고자 할 때, 스레드에 생성해놓은 ThreadLocalMap이 없다면 새로 생성해서 필드에 저장하고 아니라면 기존의 맵에 값만 저장한다.

값 조회

값을 가져올때도 마찬가지로 현재 ThreadLocal 객체의 해시코드를 활용하여 인덱싱한다.

활용

@Slf4j
public class ThreadLocalService {

    // private String nameStore;  동시성 문제가 발생하는 코드
    private final 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();
    }
   ...
}
  1. 값 저장: ThreadLocal.set(xxx)
  2. 값 조회: ThreadLocal.get()
  3. 값 제거: ThreadLocal.remove()

테스트

이전과 다르게 각자 별도의 저장소를 가지므로 userA 의 조회값이 잘 나오는 것을 볼 수 있다.

☁️ 주의사항

스레드 로컬의 값은 사용 후에 꼭 제거를 해주어야 한다.

그렇지 않으면 보통의 WAS(톰캣) 는 스레드풀로 미리 스레드들을 생성해놓아서 관리하는데, 이때 문제가 발생하게 된다. 아래의 예제를 보자.

  1. 사용자 A가 HTTP 저장 요청을 보내고, thread-A 스레드를 할당받아서 스레드 로컬에(전용 보관소)에 사용자 A의 데이터를 저장한다.
  2. WAS는 사용이 끝난 thread-A를 제거하지 않고 스레드 풀에 다시 반납한다. 따라서 thread-A와 함께 스레드 로컬의 데이터도 살아있게 된다.
  3. 이후 사용자 B가 HTTP 조회 요청을 보내고, thread-A 스레드를 할당받게 된다.
  4. thread-A 는 쓰레드 로컬에서 데이터를 조회하는데 사용자 A의 데이터가 저장되어 있으므로 사용자 B의 요청이지만 A 값을 반환하는 문제가 발생한다.

따라서 요청이 끝날 때 쓰레드 로컬의 값을 ThreadLocal.remove() 를 통해서 꼭 제거해 주어야 한다.

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글