Spring Boot로 Log 추적기를 구현하던 도중, '동시성 문제'에 대해서 접하게 되었고,
여러가지 많은 해결 방법중 ThreadLocal 이라는 Class를 알게 되었다.
ThreadLocal 이 무엇인지, 어떤 경우에 사용하는지에 대해서 알아보자.
1. ThreadLocal 이란?
- JDK 1.2부터 등장한 클래스이며, Thread 단위로 로컬 변수를 사용할 수 있다.(각 스레드에 할당된 변수를 각각 전역변수 처럼 활용가능)
하지만 사용시 잘못 사용하게 되면, 부작용이 발생한다 (아래에서 다룰예정.)
종류에는 ThreadLocal, InheritableThreadLocal 2가지가 존재한다.
- 공식문서
- 번역기를 통한 해석
이 클래스는 스레드 로컬 변수를 제공합니다. 이러한 변수는 (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
- 결과를 보자, 별도의 스레드에서는 THREAD_LOCAL 변수에 따로 set 해준것이 없기 때문에, 결과값이 null이 나온것을 볼 수 있다.
(스레드명(name)을 보게된다면, 각각의 스레드가 다른것도 확인이 가능)
정리: ThreadLocal은 각 thread마다 별도의 저장공간을 갖는다고 생각하면 쉽다.
결과: main 스레드에서 set한 변수는 -> main 스레드에서만 접근이 가능하다.
3. Inheritable Thread Local
- Thread Local은 각 스레드마다 할당이 되기 때문에, 별도 스레드에서 Main 스레드로의 접근은 불가 했다.
- Inheritable Thread Local 이란?
- ThreadLocal을 extends(확장)한 클래스이다.
- 상위 스레드에 생성된 변수(값)을 하위 스레드에서 접근(사용) 가능 하다.
- 상위 스레드의 값을, 하위 스레드에 공유가능하다는 이야기.
- 하위 스레드에서, 상위 스레드의 값을 가져올때 상위 스레드의 값이 아닌 값을 반환받을 수 있다.
- 다른 값을 반환시에는, 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의 결과를 보자.
- 이전의 코드와 비교해서, 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" 라는 값이 반환된다.
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(동시성 문제 해결)
- 제일 간단하게 할 수 있는방법을 생각해 보았다.
*** 사실 복잡한 어플리케이션의 경우, 위의 코드로 해결 되지 않을 수도 있다. (정답이 아니다.)
주로 싱글톤으로 생성된 객체에 대해서, 동시에 값의 변경이 있을 경우, 동시성문제가 발생하므로, 더 정확한 Test를 진행하려면 싱글톤 객체에 대해서 Thread Local 을 적용시켜 보는것이 훨씬 좋은 방법이다.
* 코드
- 각 스레드마다, threadLocal 변수 사용전, setThreadLocal() 변수를 통해, null 이면 기본 값을 세팅해준다,
위와 같은 코드를 실행하게 된다면, main 스레드에서 필드 변수의 값을 바꾸던 말던, 또 다른 Thread에서는 기본값을 가져올수 있다.
또한 어플리케이션의 흐름대로 실행되며, 동시성문제를 어느정도 해결할 수 있었다.
** 주의할점!
- ThreadLocal 사용할 경우 사용이 끝났다면, Thread가 종료되는 시점에 remove() 메서드를 사용해서 필수로 삭제해줘야한다.
이전 스레드에서 저장한 값이 그대로 남아있어, 다음 요청이 올때 이전의 값이 남아있는 문제가 발생하기 때문이다.
Thread Local 이 무엇인지 어떻게 사용하는지에 대해서 알아보았다, 멀티스레딩 환경을 구축해야 하고, Thread Safe 한 환경을 구축하기 위해서는 알아두면 좋을것 같다. (하지만, 그냥 막쓰면 위험할것 같다,,,, {모든곳에서 같지만 에러가 발생했을때 왜 발생한지 정도는 알아야 한다고 생각하기 때문이다..})
추후 멀티스레딩 환경 구축이나, Thread Local 을 사용하여 어플리케이션을 구축하는 예가 있다면 추가하도록 하겠다!
만약 틀린점이나, 수정할 부분, 좋은 리팩토링 방법이 존재한다면 언제든 피드백 주세요!
'Language > Java' 카테고리의 다른 글
[Java & Spring] Version 비교 방법 - version4j (2) | 2024.06.25 |
---|---|
[Java] var 키워드란? (간단예제포함) (0) | 2023.09.26 |
[Java] Java 메모리 영역(stack, heap, static), JVM, JAVA 변수 종류 (2) | 2023.02.07 |
[Java] Java 컬렉션(Collection) (0) | 2023.02.01 |
[Java] Multi Thread를 이용한 간단 게임 구현(구구단) (0) | 2023.01.26 |
댓글