티스토리 뷰

스프링 로고

 

 

 

목차

1. ThreadLocal 특징
2. ThreadLocal 주요 메서드
3. ThreadLocal 예제 코드

4. ThreadLocal 실무 주의 사항
5. 주의 사항 문제 해결

 

 

 

ThreadLocal은 무엇인가?

 

우선 Thread(스레드)는 프로세스 내에서 실행되는 흐름의 단위를 뜻합니다.

즉, 하나의 프로세스 내에서 동작하는 여러 실행 흐름 중 하나인 것입니다.

 

ThreadLocal의 뜻은 "레드 별로 독립적인 데이터를 관리하기 위한 클래스"라고 말할 수 있습니다.

이것을 풀어 해석하면, 각 Thread마다 별도의 데이터 저장 공간을 가지며, 한 Thread에서 저장한 데이터는 다른 Thread에서 접근할 수 없는 것입니다.

 

 

 

 

ThreadLocal의 특징

 

  1. Thread에 대한 로컬 변수를 제공합니다.
  2. 각각의 Thread가 변수에 대해서 독립적으로 접근할 수 있습니다.

 

이미지로 연상하기

여러 사람이 같은 물건 보관소를 사용하더라도 사용자별로 확실하게 물건을 구분하는 물품 보관소라고 생각하면 될 것입니다.

물품보관소
서울 5호선 물품보관소

 

 

 

 

 

 

ThreadLocal 주요 메서드

 

아래에 보이는 클래스 안에 있는 구현된 주요 메서드를 알아보려고 합니다.

package java.lang;

public class ThreadLocal<T> {}

 

 

set(), get()

  • set() : ThreadLocal 객체를 Key로 사용하여 ThreadLocalMap에 값을 넣습니다.
  • get() : map이 null 이 아니라면 ThreadLocal의 내부에 Entry를 불러오고 ThreadLocal에 저장한 값을 불러옵니다.
// set
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}


// get
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

 

 

remove()

  • 현재 수행 중인 Thread를 기준으로 getMap()으로 ThreadLocalMap을 얻어 그 안에 있는 값을 제거합니다.
// remove
public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null) {
         m.remove(this);
     }
 }

 

 

createMap(), getMap()

  • createMap() : Thread 내부에 ThreadLocalMap을 새로 생성합니다.
  • getMap() : Thread를 받아와 해당 Thread에서 사용될 참조할 ThreadLocalMap을 반환합니다.
// createMap
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

// getMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

t. threadLocals를 타고 들어가면 아래와 같은 클래스와 코드를 만날 수 있습니다.

public class Thread implements Runnable {
	
    // 생략
    
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    // 생략
}

 

 

 

 

 

 

ThreadLocal 예제 코드

 

1. ThreadLocalService 세팅 및 구현

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ThreadLocalService {
    private ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "");  // 초기화

    public void logic(String userName) {
        log.info("[{}] Thread - Initial Value: {}", Thread.currentThread().getName(), threadLocal.get());

        // 스레드 로컬 변수에 값 설정
        threadLocal.set(userName);

        log.info("[{}] Thread - Updated Value: {}", Thread.currentThread().getName(), threadLocal.get());
    }
}

 

  • 비즈니스 로직을 수행할라는 Service 계층을 나타내고자 하였습니다.
  • 처음 수행하면 빈문자열("")으로 초기화가 됩니다.
  • 스레드 로컬에 값 설정(set())하는 로직 포함했습니다.

 

2. Test 코드 실행

@Slf4j
public class ThreadLocalServiceTest {

    private ThreadLocalService service = new ThreadLocalService();
    private List<String> USERNAME = List.of("Alpha", "Bravo", "Charlie", "Delta", "Echo");

    @Test
    void 쓰레드로컬_테스트() {
        log.info("main start");

        for(int i = 0; i < 5; i++){
            final int index = i; 		// 람다 표현식 내에서 사용할 인덱스를 별도로 저장
            Runnable name = () -> {
                service.logic(USERNAME.get(index));
            };
            Thread thread = new Thread(name);
            thread.setName("customThread-"+index);	// 스레드 이름 지정
            thread.start();				// 시작
        }

        log.info("main exit");
    }
}

 

 

3. 결과

[customThread-1] - Initial Value: 
[customThread-1] - Updated Value: Bravo
[customThread-2] - Initial Value: 
[customThread-4] - Initial Value: 
[customThread-3] - Initial Value: 
[customThread-3] - Updated Value: Delta
[customThread-0] - Initial Value: 
[customThread-2] - Updated Value: Charlie
[customThread-4] - Updated Value: Echo
[customThread-0] - Updated Value: Alpha

각각 쓰레드 번호에 빈 문자열로 생성되었다가 알맞은 이름으로 변경된 것을 볼 수 있습니다.

쓰레드로컬 이미지

 

 

 

 

 

 

 

 

ThreadLocal 실무 주의사항

 

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

 

순서1. Client 알파의 정보 저장

쓰레드 로컬 주의사항

  1. Client 알파가 저장 요청을 함.
  2. WAS는 쓰레드 풀에서 쓰레드 하나를 조회함.
  3. 쓰레드1이 할당됨.
  4. 쓰레드1은 데이터를 쓰레드 로컬에 저장하며 쓰레드1 전용 보관소에 데이터를 보관함.

쓰레드 로컬 주의사항

  1. Client 알파의 HTTP 응답이 끝.
  2. WAS는 사용이 끝난 Thread를 쓰레드 풀에 반납함.
  3. 쓰레드 로컬에 Thread1 전용 보관소에 Client 알파 데이터가 남음.
    왜냐하면 쓰레드1은 사라지지 않고 쓰레드 풀에 반납하였기 때문.

 

순서2. Client 브라보의 정보 조회

쓰레드 로컬 주의사항

  1. Client 브라보가 조회 요청.
  2. WAS는 쓰레드 풀에서 쓰레드 하나를 조회함.
  3. 쓰레드1이 할당됨. (물론 다른 쓰레드가 할당될 수 도 있다.)
  4. 쓰레드로컬에서 쓰레드1 데이터를 조회함.
    이때 남아있던 Client 알파의 데이터가 조회가 된다. 결국 Client 브라보가 알파 데이터를 본 것.

 

이렇게 심각한 문제가 발생할 수 있으니 ThreadLocal의 값을 remove() 메서드를 통해서 꼭 값을 없애줘야 합니다.

 

 

 

 

 

 

실무 주의사항 예시 코드와 설명

 

1. ThreadLocalService 세팅 및 구현

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ThreadLocalService {
    private ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "");  // 초기화

    public void logic(int index, String userName) {	// 추가) index : 실행번호
        log.info("[{}] Thread - {} -Initial Value: {}", index, Thread.currentThread().getName(), threadLocal.get());

        // 스레드 로컬 변수에 값 설정
        threadLocal.set(userName);

        log.info("[{}] Thread - {} -Updated Value: {}", index, Thread.currentThread().getName(), threadLocal.get());
    }
}

 

  • 비즈니스 로직을 수행할라는 Service 계층을 나타내고자 하였습니다.
  • 처음 수행하면 빈문자열("")으로 초기화가 됩니다.
  • 반복문 final index를 인수로 받아 몇 번 쓰레드가 실행되는지 확인하려고 하였습니다.
  • 스레드 로컬에 값 설정(set())하는 로직 포함했습니다.

 

2. Test 코드 실행

@Slf4j
public class ThreadLocalServiceTest {

    private ThreadLocalService service = new ThreadLocalService();
    private List<String> USERNAME = List.of("Alpha", "Bravo", "Charlie", "Delta", "Echo");

    @Test
    void 쓰레드로컬_스레드풀사용_테스트() {
        log.info("main start");

        ExecutorService executor = Executors.newFixedThreadPool(3); // 쓰레드 풀 3개만 사용

        for(int i = 0; i < 5; i++){
            final int index = i; // 람다 표현식 내에서 사용할 인덱스를 별도로 저장
            Runnable name = () -> {
                service.logic(index, USERNAME.get(index));	// 해당 실행번호 Index도 넘기기
            };
            executor.submit(name);
        }

        executor.shutdown(); // 스레드 풀 종료 명령, 실행중인 작업은 완료되고 종료됨

        log.info("main exit");
    }
}
  • Thread Pool 개수를 3개로 제한했습니다.
  • 반복문 내에 final index를 인수로 넘겨주었습니다.

 

3. 결과

[pool-1-thread-1] - 0 -Initial Value: 
[pool-1-thread-2] - 1 -Initial Value: 
[pool-1-thread-2] - 1 -Updated Value: Bravo
[pool-1-thread-3] - 2 -Initial Value: 
[pool-1-thread-3] - 2 -Updated Value: Charlie
[pool-1-thread-1] - 0 -Updated Value: Alpha
[pool-1-thread-2] - 3 -Initial Value: Bravo		<-- ??
[pool-1-thread-2] - 3 -Updated Value: Delta
[pool-1-thread-3] - 4 -Initial Value: Charlie		<-- ??
[pool-1-thread-3] - 4 -Updated Value: Echo

쓰레드로컬 스레드풀 문제점

 

ThreadLocal에 데이터가 없었기 때문에 지금까지 문제가 없었습니다.

이번에는 쓰레드 풀을 3개로 제한하고 돌리고 해당 ThreadLocal에 있는 데이터를 remove()를 시키지 않아서 해당 쓰레드의 ThreadLocal에 있는 데이터를 가져온 것을 볼 수 있습니다.

 

쓰레드로컬 스레드풀 문제점 이미지

 

 

 

 

 

 

 

문제 해결하기 위해 remove() 넣기

 

1. ThreadLocalService 세팅 및 구현 할 때 remove() 추가

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ThreadLocalService {
    private ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "");  // 초기화

    public void logic(int index, String userName) {	// 추가) index : 실행번호
        log.info("[{}] Thread - {} -Initial Value: {}", index, Thread.currentThread().getName(), threadLocal.get());

        // 스레드 로컬 변수에 값 설정
        threadLocal.set(userName);

        log.info("[{}] Thread - {} -Updated Value: {}", index, Thread.currentThread().getName(), threadLocal.get());
    
        threadLocal.remove();	// 추가) remove()
    }
}

 

2. 결과 - 정상적인

remove() 처리한 결과

 

 

 

 

 

감사합니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함