pooney
article thumbnail
Published 2024. 12. 29. 00:15
ThreadLocal란? Spring boot

 

 

ThreadLocal

안녕하세요 오늘은 ThreadLocal에 대해서 알아 보려고 합니다. ThreadLocal은 Java에서 지원하는 Thread Safe한 기술로 멀티쓰레드 환경에서 동시성 이슈를 해결 할 수 있는 쓰레드별 저장공간이라 고 할 수 있습니다. 

 

 

 

 

동시성 이슈 상황 

 

만약 pooneyValue라는 변수에 두개의 쓰레드가 접근 하여  값을 변경 하는 상황이라고 가정 하겠습니다. 

 

조건 
  • pooneyValue라는 공유 변수 존재
  • 두개의 쓰레드 동시 접근 
  • 두개의 쓰레드 값은 변경 하는경우 

 

그림으로 표현하면 아래와 같은 상황 일 것입니다. 

 

 

 

 

 

 

코드로 작성하면 아래와 같이 작성을 할 수 있습니다. 여기서 동시성 이슈를 발생 시키기 위하여 sleep을 통해 시간을 조절 했습니다. 

 

 

@Slf4j
@Configuration
public class ThreadTest {
    public String pooneyValue;


    private void updatePooneyValue(String value) {
        pooneyValue = value;
    }

    private String getPooneyValue() {
        return pooneyValue;
    }

    public String callPooney(String value) {
        log.info("저장할 value : {}", value);
        log.info("현재 쓰레드 명 : {}", Thread.currentThread().getName());
        updatePooneyValue(value);
        sleep(2000);
        String pooneyValue = getPooneyValue();
        log.info("조회한 value value : {}", pooneyValue);
        return pooneyValue;
    }
}

 

 

@Slf4j
public class ThreadLocalTest {
    private final ThreadTest threadTest = new ThreadTest();
    @Test
    void concurrency() {
        Runnable runnable1 = () -> threadTest.callPooney("pooneyA");
        Runnable runnable2 = () -> threadTest.callPooney("pooneyB");

        Thread thread1 = new Thread(runnable1);
        thread1.setName("pooney-A");
        Thread thread2 = new Thread(runnable2);
        thread2.setName("pooney-B");
        thread1.start();
        thread2.start();
        CommonUtils.sleep(5000);
    }
}

 

 

 

동시성 없이 원하는 결과는 각 쓰레드가 저장한 값이 그대로 로그에 찍히는 형태로 아래와 같을 것입니다. 

 

 

 

 

하지만 결과는 동시성이 발생 했을때는 아래 제일 마지막에 pooney-A 쓰레드가 조회한 value 값이 저장한 pooneyA 값이 아닌 pooneyB 인 것을 로그를 통해 확인 해 볼 수가 있습니다. 

 

 

 

 

 

 

 

즉 하나의 자원을 멀티쓰레드가 접근하여 읽기만 하면 전혀 문제가 없겠지만 서로 수정 하다 보니 발생한 문제인데요. 물론 지역변수는 쓰레드마다 저장공간을 할당 받아 사용하니 문제가 없지만 static, 참조, 전역,공유 변수등은 동시성 이슈가 발생 할 수 있습니다. 이러한 문제가 실제 서비스에서 발생하다면 잘못된 값이 저장되고 조회가 되니 문제가 클 것입니다. 그러면 이러한 문제를 해결 할 수 있는 방법 중에 ThreadLocal을 사용하는 것인데요 

 

 

 

 

ThreadLocal을 통한 동시성 해결

 

 

ThreadLocal은 쉽게 말해 Thread 별로 가지는 내부 저장공간이라고 생각하면 이해 하기 쉽습니다.  개별로 저장공간을 사용하니 멀티쓰레드로 동작하더라도 다른 쓰레드 저장공간에 영향을 줄 수 가 없어 동시성 이슈에 대한 문제를 해결 할 수가 있습니다.  

 

 

 

 

 

 

 

 

 

그러면 확인을 해보기 위해 위와 동일한 조건으로 해서 한번 테스트를 해보겠습니다. 

 

 

 

@Slf4j
@Configuration
public class ThreadTest {
    public ThreadLocal<String> pooneyValue = new ThreadLocal<>(); // (1)

    private void updatePooneyValue(String value) {
        pooneyValue.set(value); // (2)
    }

    private String getPooneyValue() {
        return pooneyValue.get(); // (3)
    }

    public String callPooney(String value) {
        log.info("저장할 value : {}", value);
        log.info("현재 쓰레드 명 : {}", Thread.currentThread().getName());
        updatePooneyValue(value);
        sleep(2000);
        String pooneyValue = getPooneyValue();
        log.info("조회한 value value : {}", pooneyValue);
        return pooneyValue;
    }
}

 

 

 

(1) new ThreadLocal<>()

  • 내부 저장공간을 할당 해줍니다. 

(2) set()

  • 쓰레드 내부 저장 공간에 값을 저장합니다. 

(2) set()

  • 쓰레드 내부 저장 공간에 저장된 값을 조회합니다. 

 

 

 

 

@Slf4j
public class ThreadLocalTest {
    private final ThreadTest threadTest = new ThreadTest();
    @Test
    void concurrency() {
        Runnable runnable1 = () -> threadTest.callPooney("pooneyA");
        Runnable runnable2 = () -> threadTest.callPooney("pooneyB");

        Thread thread1 = new Thread(runnable1);
        thread1.setName("pooney-A");
        Thread thread2 = new Thread(runnable2);
        thread2.setName("pooney-B");
        thread1.start();
        thread2.start();
        CommonUtils.sleep(5000);
    }
}

 

 

 

 

 

 

테스트 코드를 확인한 결과 이전과는 다르게 쓰레드 별로 저장한 값이 올바르게 조회가 되는 것을 확인 할 수 있습니다. 

 

 

 

 

 

 

 

 

 

 

주의 사항

 

 

쓰레드는 고자원이기 때문에 요청이 올때 마다 생성 해서 쓰면 많은 많은 부화를 주기 때문에 Thread Pool을 이용하게 되는데요.  만약 이미 앞에 ThreadLocal의 값을 그대로 두면 어떻게 될까요?  그림으로 표현하면 아뢔 같은 상황 일 것입니다. 

 

 

 

 

 

FLOW

 

  1. pooney-A 쓰레드를 할당 받고 "pooneyA" 라는 값을 저장 요청
  2. pooney-A ThreadLocal 내부 저장 공간에 "pooneyA" 라는 값을 저장 
  3. pooney-A 쓰레드를 Thread Pool에 반환 
  4. 다른 유저가 pooney-A 쓰레드를 할당 받고 ThreadLocal의 값을 조회 
  5. 이전에 저장한 "pooneyA" 라는 값을 조회 

 

 

ThreadPool을 통해 Thread를 재사용 함으로 이전에 ThreadLocal에 저장한 값을 그대로 조회하여 사용을 하게 될 수 있고 값을 계속 누적해서 사용하다 보니 누수가 발생 할 수 있습니다. 이럴 경우 실제 서비스에서는 큰 문제로 이어질 수 있는데요. 그렇기 때문에 ThreadLocal은 사용을 다했으면 값을 초기화 해주는 작업을 해주는 것이 좋습니다.  초기화 방법은 아래 ThreadLocal.remove()를 통해 할 수 있습니다. 만약 사용을 하신다면 Filter와 같이 제일 상위단에서 초기화를 해주는 것을 추천합니다. 

 

 

@Slf4j
@Configuration
public class ThreadTest {
    public ThreadLocal<String> pooneyValue = new ThreadLocal<>(); 

    private void updatePooneyValue(String value) {
        pooneyValue.set(value);
    }

    private String getPooneyValue() {
        return pooneyValue.get();
    }

    public String callPooney(String value) {
        log.info("저장할 value : {}", value);
        log.info("현재 쓰레드 명 : {}", Thread.currentThread().getName());
        updatePooneyValue(value);
        sleep(2000);
        String pooneyValue = getPooneyValue();
        log.info("조회한 value value : {}", pooneyValue);
        clear()
        return pooneyValue;
    }
    
    
    // ThreadLocal 초기화 진행
    public void clear(){
        pooneyValue.remove();
    }
}

 

 

 

 

 

 

이렇게 동시성 이슈를 해결 할 수 있는 ThreadLocal에 대해서 알아보았는데 많은 분에게 도움이 되었으면 합니다.

profile

pooney

@pooney

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!