ThreadLocal

이원석·2024년 2월 16일

Spring

목록 보기
12/20
post-thumbnail
*인프런 김영한 강사님의 강좌를 참고하여 정리한 내용입니다.*

스프링은 컨테이너를 통해 빈(Bean) 객체를 생성하고 의존성을 주입한다! 그리고 이 빈 객체들은 기본적으로 모두 싱글톤으로 생성된다. 그런데, 스프링은 멀티 스레드 방식을 사용하는 환경으로 사용자의 요청에 따라 여러개의 스레드가 생성된다.

그렇다면 하나의 인스턴스(싱글톤)에 대해 여러 스레드가 접근을 하고 데이터를 변경 시킨다면 어떻게 될까??




동시성 이슈

동시성 이슈는 멀티 스레드, 프로세스 환경에서 발생할 수 있는 문제로, 주로 공유된 자원에 여러 스레드나 프로세스가 동시에 접근할 때 발생한다.

사용자 A와 사용자 B가 각각 시간차를 두고 nameStore의 데이터를 저장하고 조회하는 상황을 가정해보자.

  1. Thread-AuserAnameStore 에 저장했다.
  2. Thread-AuserAnameStore 에서 조회했다.
  3. Thread-BuserBnameStore 에 저장했다.
  4. Thread-BuserBnameStore 에서 조회했다.

문제없이 정상 흐름대로 동작한다.



그렇다면, 사용자 A와 사용자 B가 동시에 nameStore의 데이터를 저장하고 조회하는 상황을 가정해보자.

실행결과

[Test worker] main start
[Thread-A]	  저장 name=userA -> nameStore=null 
[Thread-B]	  저장 name=userB -> nameStore=userA
[Thread-A]	  조회 nameStore=userB
[Thread-B]	  조회 nameStore=userB
[Test worker] main exit
  1. Thread-AuserAnameStore 에 저장했다.
  2. Thread-BuserBnameStore 에 저장했다.
  3. Thread-AuserBnameStore 에서 조회했다.
  4. Thread-BuserBnameStore 에서 조회했다.

기대하던 것과 다른 결과가 나왔다!! 거의 동시에 요청을 처리하며 순서가 섞여버린것이다. 따라서 스레드A 입장에서는 저장한 데이터와 조회한 데이터가 다른 문제가 발생했다.

이러한 동시성 문제는 여러 스레드가 하나의 같은 인스턴스 필드에 접근해야 하기 때문에 트래픽이 점점 많아질수록 자주 발생한다. (특히 스프링 빈 처럼 싱글톤 객체)



ThreadLocal

이러한 동시성 문제를 해결하기 위한 방법으로 ThreadLocal이 있다. ThreadLocal은 지정한 특정 스레드만 접근할 수 있는 특별 저장소를 말한다.

일반적인 변수 필드

각각의 스레드가 싱글톤 객체의 변수나 전역 변수에 데이터를 저장하는 것과 다르게


스레드 로컬


스레드 로컬은 각 스레드마다 별도의 내부 저장소를 제공한다. 따라서 같은 인스턴스의 쓰레드 로컬 필드에 접근해도 문제가 발생하지 않는다.

스레드 로컬에서 사용하기 위한 스레드 생성에 대해 간단하게 알아보자.



Runnable

Runnable은 인터페이스이며 자바에서 멀티스레드 프로그래밍을 지원하기 위해 사용되는 함수형 인터페이스(Functional Interface) 중 하나이다.

함수형 인터페이스 - @FunctionalInterface
함수형 인터페이스란 java8 부터 도입된 개념으로, 단 하나의 추상 메서드를 가지고 있는 인터페이스를 말한다. 보통 람다식이나 익명클래스를 사용하여 인스턴스화 과정을 거친다.


Runnable 인터페이스는 스레드에서 실행할 작업을 정의하기 위해 사용되는데, run 메서드를 오버라이딩 해서 구체화하는 과정이 필요하다.

@FunctionalInterface
public interface Runnable {
    /**
     * 스레드에서 실행할 작업을 정의하는 메서드.
     */
    public abstract void run();
}



Thread

Thread 클래스의 생성자에 Runnable 객체를 전달하여 스레드를 생성한다. 그리고 생성된 스레드는 start() 메서드를 호출함으로써 실행된다.

public void start()
public final void setName(String name)





ThreadLocal 예제 코드

// ThreadLocalService.java
public class ThreadLocalService {

	// private String nameStore;
	private ThreadLocal<String> nameStore = new ThreadLocal<>();
    
    public String logic(String name) {
    	log.info("저장 name = {} -> nameStore = {}", anem, 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();
        }
    }
}

ThreadLocal 사용법

  • 값 저장: ThreadLocal.set(xxx)
  • 값 조회: ThreadLocal.get()
  • 값 제거: ThreadLocal.remove() - 쓰레드 로컬을 사용하고 나면 반드시 저장된 값을 제거해야 한다!!

기존의 nameStore 변수를 사용하는 경우 동시성 문제가 발생한다. ThreadLocal을 사용함으로써 동시성 문제를 해결할 수 있다.


public class ThreadLocalServiceTest {

	private ThreadLocalSerivce service = new ThreadLocalService();
    
    @Test
    void threadLocal() {
    	log.info("main start");
        
        // 람다
        Runnable userA = () -> {
        	service.logic("userA");
        };
        
        // 익명 클래스
        Runnable userB = new Runnable() {
            @Override
            public void run() {
                service.login("userB");
            }
        };
        
        Thread threadA = new Thread(userA);
        threadA.setName("thread-A");
        
        Thread threadB = new Thread(userB);
        threadB.setName("thread-B");
        
        // 0.1초의 간격을 두고 거의 동시에 스레드 실행!
        threadA.start();
        sleep(100);
        threadB.start();
        sleep(2000);
        
        log.info("main exit");
	}
}

흐름도

  1. ThreadLocalSerivce 를 통해 ThreadLocal에 각각의 nameStore에 userA, userB를 저장한다.
  2. Runnable 인터페이스를 인스턴스화 한다.
  3. Thread 인스턴스를 생성하며 생성자에 Runnable 타입 인스턴스를 전달한다.
  4. 스레드 실행

실행 결과

[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을 활용해 스레드 마다 별도의 데이터 저장소를 가지게 되었다. 따라서 동시성 문제가 해결되었다.




ThreadLocal - 주의사항

스레드 로컬의 값을 사용 후 제거하지 않는다면, WAS(톰캣)처럼 멀티 스레드 환경에서 스레드 풀을 사용하는 경우 심각한 문제가 발생할 수 있다.

사용자A의 저장 요청

  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는 스레드 풀에 아직 살아있음!!
  4. 따라서 사용자A의 데이터도 함께 살아있다.

만약 사용자B가 조회를 위해 새로운 HTTP 요청을 하게 되어 thread-A가 할당이 되는 경우, 조회 요청일 발생한다면 thread-A는 스레드 로컬의 thread-A 전용 보관소에서 데이터를 조회한다. 이렇게 데이터가 불일치 하는 문제가 발생할 수 있다.

따라서 요청이 끝날 때 사용한 스레드 로컬은 반드시 ThreadLocal.remove()를 통해서 파괴시켜주자!





참고문헌

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8

0개의 댓글