본문 바로가기
Language/Java

[Java] ThreadLocal이란? - (ThreadLocal, InheritableThreadLocal) 설명 및 예제(테스트)

by lucas_owner 2023. 3. 7.

Spring Boot로 Log 추적기를 구현하던 도중, '동시성 문제'에 대해서 접하게 되었고, 

여러가지 많은 해결 방법중 ThreadLocal 이라는 Class를 알게 되었다. 

ThreadLocal 이 무엇인지, 어떤 경우에 사용하는지에 대해서 알아보자.

 

 

1. ThreadLocal 이란?

- JDK 1.2부터 등장한 클래스이며, Thread 단위로 로컬 변수를 사용할 수 있다.(각 스레드에 할당된 변수를 각각 전역변수 처럼 활용가능)

하지만 사용시 잘못 사용하게 되면, 부작용이 발생한다 (아래에서 다룰예정.) 

종류에는 ThreadLocal, InheritableThreadLocal 2가지가 존재한다. 

 

- 공식문서

ThreadLocal.class Doc

- 번역기를 통한 해석

이 클래스는 스레드 로컬 변수를 제공합니다. 이러한 변수는 (get 또는 set 메서드를 통해) 하나에 액세스하는 각 스레드가 고유하고 독립적으로 초기화된 변수 복사본을 갖는다는 점에서 일반 대응 변수와 다릅니다. ThreadLocal 인스턴스는 일반적으로 상태를 스레드(예: 사용자 ID 또는 트랜잭션 ID)와 연결하려는 클래스의 개인 정적 필드입니다.

 

- 간단 정리

ThreadLocal 에 할당된 변수는, 각 Thread마다 고유하게 할당되어, Multi Thread 환경에서
Thread Safe 하게 프로그램을 구성할 수 있는것.

 

 

 

2. ThreadLocal 사용법

public class AboutThreadLocal {

    private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    public static void main(String[] args) {
        THREAD_LOCAL.set("local Thread-1");
        
        // main Thread
        System.out.println("Thread name = " + Thread.currentThread());
        System.out.println("Thread Local variable1 = " + THREAD_LOCAL.get());

        // 별도의 Thread
        Thread thread = new Thread(() -> {
            System.out.println("Thread name2 = " + Thread.currentThread());
            System.out.println("Thread Local variable2 = " + THREAD_LOCAL.get()); // Main Thread 이외에는 접근 불가능, 결과 = null
        });
        thread.start();
    }
}

- 위 코드는, 각 스레드명, ThreadLocal에 변수를 저장하고 그것을 출력하는 코드예제이다. 

 

위 코드는 결과가 어떻게 될것인가? , THREAD_LOCAL 이라는 변수에 set("local Thread-1")을 해주었다. 

ThreadLocal은 각 스레드마다 고유하게 할당되어 접근이 가능하다고 했었다. 

즉, main thread의 것은 main만, 별도의 thread는 main thread에 접근이 불가하다. 

-> 더 쉽게 말하면, 별도의 thread는 main 스레드에 접근이 불가능하다 

---> main 스레드에서 저장한 THREAD_LOCAL 변수에 접근이 불가능한것이다. 

 


 

- 결과 - Console

예제 결과 console

- 결과를 보자, 별도의 스레드에서는 THREAD_LOCAL 변수에 따로 set 해준것이 없기 때문에, 결과값이 null이 나온것을 볼 수 있다. 

(스레드명(name)을 보게된다면, 각각의 스레드가 다른것도 확인이 가능)

 

정리: ThreadLocal은 각 thread마다 별도의 저장공간을 갖는다고 생각하면 쉽다. 
결과: main 스레드에서 set한 변수는 -> main 스레드에서만 접근이 가능하다.

 

 

 

3. Inheritable Thread Local

- Thread Local은 각 스레드마다 할당이 되기 때문에, 별도 스레드에서 Main 스레드로의 접근은 불가 했다.

 

- Inheritable Thread Local 이란? 

  1. ThreadLocal을 extends(확장)한 클래스이다. 
  2. 상위 스레드에 생성된 변수(값)을 하위 스레드에서 접근(사용) 가능 하다.
    1. 상위 스레드의 값을, 하위 스레드에 공유가능하다는 이야기.
    2. 하위 스레드에서, 상위 스레드의 값을 가져올때 상위 스레드의 값이 아닌 값을 반환받을 수 있다.
      1. 다른 값을 반환시에는, childValue() 메서드를 Override 해야한다.

 

- 예제 코드 (Inheritable Thread Local) - 기본

public class AboutInheritableThreadLocal {

    private static final ThreadLocal<String> INHERITABLE_THREAD_LOCAL = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        INHERITABLE_THREAD_LOCAL.set("IThreadLocal-1");
        // main Thread
        System.out.println("Thread name1 = " + Thread.currentThread());
        System.out.println("Thread Local variable1 = " + INHERITABLE_THREAD_LOCAL.get());

        Thread thread = new Thread(() -> {
            System.out.println("Thread name2 = " + Thread.currentThread());
            System.out.println("Thread Local variable2 = " + INHERITABLE_THREAD_LOCAL.get()); // 상위 thread(main)의 값을 상속받기 때문에 결과 존재 = IThreadLocal-1
        });
        thread.start();
    }
}

- ThreadLocal 예제에서 바뀐 부분은, new InheritableThreadLocal<>() 부분이다.(변수명 포함)

이전의 결과에서는 Thread Local Variable2의 결과가 null 이었다, 하지만 상위 스레드의 값을 받을 수 있는 inheritableThreadLocal의 결과를 보자. 

Inheritable Thread Local

- 이전의 코드와 비교해서, Variable2의 값이, Main 스레드와 같은값이 출력되는것을 확인 할 수 있다. 

 


 

3-2. inheritable Thread Local - 임의의값 return 

- 위의 예제와 달리 하위 스레드에서, 상위 스레드의 값을 가져올때, 다른값을 반환시켜 보자. 

 

public class InheritableThreadLocal_2 {
    public static void main(String[] args) {
        SuperThread su = new SuperThread();
        su.start();
    }
}// main Class


class SuperThread extends Thread {
    public static final ThreadLocal<String> INHERITABLE_THREAD_LOCAL = new InheritableThreadLocal() {
        // 임의의 값 set/get 
        @Override
        public String childValue(Object parentValue) {
            return "this is for Sub"; // 자식 스레드에 이값을 return
        }
    };

    @Override
    public void run() {
        INHERITABLE_THREAD_LOCAL.set("Super Thread-1");

        System.out.println("Super Thread : " + Thread.currentThread());
        System.out.println("Super Thread var : " + INHERITABLE_THREAD_LOCAL.get()); // super Thread-1

        SubThread st = new SubThread();
        st.start();
    }
}


/**
 * This is SubThread Class, This class belong to SuperThread.class
 * - 상위 클래스인 SuperThread에서 실행.
 */
class SubThread extends Thread {
    @Override
    public void run() {
        System.out.println("Sub Thread : " + Thread.currentThread());
        System.out.println("Sub Thread : " + SuperThread.INHERITABLE_THREAD_LOCAL.get()); // 예상되는결과 -> this is~
    }
}

- 간단 코드 설명 

  • SuperThread (상위), SubThread(하위) 클래스가 존재하며, SubThread에서 상위 thread인 SuperThread의 Inheritable Thread Local 변수를 get() 으로 요청한다. 
  • 상위 스레드의 값인 : "Super Thread-1" 이 출력 되어야 하지만, return 값을 상위 스레드와 다르게 주고싶다.
  • childValue() @Override 를 통해 return 값을 지정할 수 있다.
  • 하위 스레드에서 값을 요청하면, 변경한 값인 "this is for Sub" 라는 값이 반환된다.

Inheritable Thread Local : Change Return Value

 

 

4. Thread Local - 간단 

- Thread Local의 개념을 이제 어느정도 이해했다, 하지만 어플리케이션을 구성할때 어떻게 사용할 수 있을까? 혹은 어떤 예시가 있을까?

- 우선 이글의 서론에서 거론하였듯, 동시성문제에 대해서 해결 할 수 있다. 예시를 들어보겠다. 

 

-> 싱글톤 으로 생성된 객체가 있다고 가정했을때, 동시에 여러 Thread에서 접근하여 값을 바꾼다거나 하면 동시성 문제가 발생된다.

이러한 것은 Thread safe하지 않으며, 각 요청에 따른 정확한 결과값을 도출할 수가 없는것이다. 

(이런 Thread Safe 하지 않는것을 해결하는 방법이 여러가지가 존재한다)

 

하지만, 각 thread마다의 개별 저장공간을 준다면, Thread Safe하게 결과값을 도출할 수 있다. 

이때 사용하는 것이 ThreadLocal 이다. 

 

 

4-1. 예제 - 동시성문제 예시

동시성문제 - 리팩토링전
결과

- 모두가 예측 할 수 있듯, static 변수인 name 의 값이 'from main' 이라는 값으로 재정의 되어 출력된다.(Main thread, 별도 thread)

 

- 만약 해당 Class가 싱글톤 Bean 객체 라고 생각 해보자

   -> 동시 접속 사용자가 많은 어플리케이션이라면, 굉장히 크리티컬한 에러가 발생할것이다. 

 

4-2. ThreadLocal 사용 - Refactoring(동시성 문제 해결)

Refactoring
Refactoring - 결과

- 제일 간단하게 할 수 있는방법을 생각해 보았다. 

*** 사실 복잡한 어플리케이션의 경우, 위의 코드로 해결 되지 않을 수도 있다. (정답이 아니다.)

주로 싱글톤으로 생성된 객체에 대해서, 동시에 값의 변경이 있을 경우, 동시성문제가 발생하므로, 더 정확한 Test를 진행하려면 싱글톤 객체에 대해서 Thread Local 을 적용시켜 보는것이 훨씬 좋은 방법이다.

 

* 코드 

- 각 스레드마다, threadLocal 변수 사용전, setThreadLocal() 변수를 통해, null 이면 기본 값을 세팅해준다, 

위와 같은 코드를 실행하게 된다면, main 스레드에서 필드 변수의 값을 바꾸던 말던, 또 다른 Thread에서는 기본값을 가져올수 있다. 

또한 어플리케이션의 흐름대로 실행되며, 동시성문제를 어느정도 해결할 수 있었다. 

 

 

** 주의할점!

- ThreadLocal 사용할 경우 사용이 끝났다면, Thread가 종료되는 시점에 remove() 메서드를 사용해서 필수로 삭제해줘야한다. 

 

이전 스레드에서 저장한 값이 그대로 남아있어, 다음 요청이 올때 이전의 값이 남아있는 문제가 발생하기 때문이다. 


Thread Local 이 무엇인지 어떻게 사용하는지에 대해서 알아보았다, 멀티스레딩 환경을 구축해야 하고, Thread Safe 한 환경을 구축하기 위해서는 알아두면 좋을것 같다. (하지만, 그냥 막쓰면 위험할것 같다,,,, {모든곳에서 같지만 에러가 발생했을때 왜 발생한지 정도는 알아야 한다고 생각하기 때문이다..}) 

 

추후 멀티스레딩 환경 구축이나, Thread Local 을 사용하여 어플리케이션을 구축하는 예가 있다면 추가하도록 하겠다! 

 

 

만약 틀린점이나, 수정할 부분, 좋은 리팩토링 방법이 존재한다면 언제든 피드백 주세요! 

 

반응형

댓글