스프링 | 쓰레드 로컬(Thread Local)

JINHO LEE·2023년 1월 25일
0

도입

여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하게 될때, 저장한 데이터와 조회한 데이터가 다른 문제가 발생하게 된다.
이처럼 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라고 한다.

예를 들어보자.

예시)

thread-A가 userA값을 nameStore에 보관한다.
thread-A는 1초 뒤에 nameStore에서 가져올 예정이다.

이와중에 thread-B가 userB값을 nameStore에 보관한다.
이렇게 되면 userA값은 nameStore에서 제거되고 userB값이 저장된다.

thread-A와 thread-B 모두 userB를 반환 받는다.

이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 잘 나타나지 않고, 트래픽이 많아 질수록 자주 발생한다.

동시성 문제는 지역 변수에서 발생하지 않는다.
지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당된다.
그러나 동시성 문제가 발생하는 곳은 같은 인스턴스의 필드 또는 static 같은 공용 필드에 접근할 때 발생한다.

*) 특히, 스프링 빈 처럼 싱글톤 객체의 필드를 변경하며 사용할 때 동시성 문제를 조심해야한다.

쓰레드 로컬

위와 같은 동시성 문제를 해결하기 위해 사용하는 것이 쓰레드 로컬이다.

쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 의미한다.

물건 보관 창구를 예를 들어 보자.

예시)
여러 사람이 같은 물건 보관 창구를 사용하더라도 창구 직원은 사용자를 인식해서 사용자별로 물건을 구별해준다.
사용자A, 사용자B는 직원을 통해서 물건을 보관하고, 꺼내지만 창구 직원이 사용자에 따라 보관한 물건을 구별 해주는 것이다.

쓰레드 로컬을 통해서 데이터를 조회할 때에도 마찬가지이다.
thread-A가 조회를 하게 되면 쓰레드 로컬은 thread-A 전용 보관소에서 userA 데이터를 반환해주고, thread-B가 조회를 하면 thread-B 전용 보관소에서 userB 데이터를 반환해준다.

자바는 이와같은 java.lang.ThreadLocal 클래스를 제공한다.

쓰레드 로컬 사용법

  • 쓰레드 로컬에 값 저장: ThreadLocal.set(Target)
  • 쓰레드 로컬에 값 조회: TheadLocal.get()
  • 쓰레드 로컬에 값 제거: ThreadLocal.remove()
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()); // 쓰레드 풀 값 조회
        String result = nameStore.get();
        nameStore.remove(name); // 쓰레드 풀 값 제거
        
        return result;
    }
}

public class ThreadLocalServiceTest {
	private ThreadLocalService service = new ThreadLocalService();
    
    @Test
    void threadLocal() {
    	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(100);
        threadB.start();
        
        sleep(2000);
		log.info("main exit");
        
    }
}

실행 결과

[Test worker] main start
[Thread-A] 저장 name=userA -> nameStore=null
[Thread-B] 저장 name=userB -> nameStore=null
[Thread-A] 조회 nameStore=userA
[Thread-B] 조회 nameStore=userB
[Test worker] main exit

추가로 쓰레드 로컬을 모두 사용하고 나면 꼭 ThreadLocal.remove()를 호출해서 쓰레드 로컬에 저장된 값을 제거해줘야한다.

쓰레드 로컬의 값을 사용 후 제거하지 않고 그냥 두면 WAS(톰캣)처럼 쓰레드 풀을 사용하는 경우에 심각한 문제가 발생할 수 있다.

예시)


1. 사용자A가 저장 HTTP를 요청
2. WAS는 쓰레드 풀에서 쓰레드 한개 조회
3. 쓰레드 (thread-A)가 할당
4. thread-A는 사용자A의 데이터를 쓰레드 로컬에 저장
5. 쓰레드 로컬의 thread-A 전용 보관소에 사용자 A 데이터 보관


1. 사용자A의 HTTP 응답이 끝남
2. WAS는 사용이 끝난 thread-A를 쓰레드 풀에 반환. 쓰레드 생성 비용이 높기 때문에 쓰레드 제거하지 않고 쓰레드 풀에서 쓰레드 재사용
3. thread-A 보관소에 사용자A 데이터 존재


1. 사용자B가 조회를 위한 새로운 HTTP 요청
2. WAS는 쓰레드 풀에서 쓰레드 조회
3. 쓰레드(thread-A)가 할당
4. 조회 요청이기에 thread-A 저용 보관소에 있는 사용자A값 반환
5. 사용자 B는 사용자A의 정보 조회

이와 같은 문제가 발생하기에 쓰레드 로컬을 사용할 경우 remove는 반드시 해주자.

참고) 김영한의 스프링 핵심 원리 - 고급편 (쓰레드 로컬 섹션)

0개의 댓글