본문 바로가기

Java and Spring

[Java] ThreadLocal 이해 및 활용

이번 글에서는 ThreadLocal이 왜 필요하고 어떤경우에 사용이 되는지 알아보고 정리를 해보려고 합니다.

그럼 먼저, ThreadLocal이 왜 필요할까요?? 결론부터 이야기하자면 동시성 문제가 발생할 수 있기 때문입니다.

 

동시성 문제 예제

예제는 간단한 테스트코드를 활용하여 진행해보도록 하겠습니다.

public class FieldServiceTest {

    private FieldService fieldService = new FieldService();

    @Test
    void field() {
        log.info("main start");
        Runnable userA = () -> {
            fieldService.logic("userA");
        };

        Runnable userB = () -> {
            fieldService.logic("userB");
        };

        Thread threadA = new Thread(userA);
        threadA.setName("thread-A");
        Thread threadB = new Thread(userB);
        threadB.setName("thread-B");

        threadA.start();
        sleep(2000); //동시성 문제 발생X
        //sleep(100); //동시성 문제 발생O
        threadB.start();

        sleep(3000); //메인 쓰레드 종료 대기
        log.info("main exit");
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
@Slf4j
public class FieldService {

    private String nameStore;

    public String logic(String name) {
        log.info("저장 name={} -> nameStore={}", name, nameStore);
        nameStore = name;

        sleep(1000);
        log.info("조회 nameStore={}", nameStore);
        return nameStore;
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

위와 같은 코드를 보면 sleep(2000)과 sleep(100)을 기점으로 동시성 문제가 나타날 수도 있고, 나타날 수 없을 수도 있습니다. 즉, threadA가 시작되고 sleep(2000)를 걸면 2초안에 threadA가 완료되어 threadB가 실행됨에 있어 문제가 없지만, sleep(100)을 걸면 threadA가 종료되지 않음에도 불구하고 threadB가 실행되어 FieldService에서 공용으로 사용되는 nameStore의 값을 변경 할 수 있기 때문입니다.

 

이해하기 쉽도록 그림으로 설명을 이어가도록 하겠습니다.

먼저, sleep(2000)을 주었을 때 동시성 문제가 발생하지 않는 경우입니다.

sleep(100)을 주었을 때 동시성 문제가 발생한 경우입니다.

위 그림과 같이 threadA가 완료되지않은 시점에서 다른 요청으로인해 nameStore가 변경되면 동시성 문제로인해 개발자에게 큰 시련이 다가옵니다. 물론 sleep(2000)을 줬다고해서 동시성문제가 해결된 것은 아닙니다. 트래픽이 클수록 동시성문제가 발생할 확률이 높다는것이죠!!

 

ThreadLocal이란

그럼 우리는 이 문제를 어떻게 해결해야 할까요?? 바로 ThreadLocal을 사용하여 해결 할 수 있습니다.

ThreadLocal을 이해하기 쉽게 은행을 예로들도록 하겠습니다.

고객이 은행에 들어가서 은행원을 통해 고객의 계좌에 있는 돈을 찾아간다고 생각을 해봅시다.

여기서 고객은 Thread이고 은행원은 ThreadLocal입니다. 여러 고객이 같은 은행을 사용하더라도 은행원은 고객을 인식하여 고객별로 계좌에 있는 돈을 반환해주는 것입니다.

 

그럼 간단하게 ThreadLocal의 개념 및 사용방법에대해 알아보고 테스트코드를 통해 동시성문제가 해결되었는지 확인해보도록 하겠습니다.

 

ThreadLocal의 내부는 thread 정보를 key로 하여 값을 저장해두는 Map 구조를 가지고 있습니다.

기본적인 사용에도 Map과 비슷한 get, set 메서드를 이용합니다.

  1. ThreadLocal.set() = 현재 쓰레드의 로컬변수를 저장한다.
  2. ThreadLocal.get() = 현재 쓰레드의 로컬변수를 가져온다.
  3. ThreadLocal.remove() =  현재 쓰레드의 로컬변수를 삭제한다.
public class ThreadLocalServiceTest {

    private ThreadLocalService service = new ThreadLocalService();

    @Test
    void field() {
        log.info("main start");
        Runnable userA = () -> {
            service.logic("userA");
        };

        Runnable userB = () -> {
            service.logic("userB");
        };

        Thread threadA = new Thread(userA);
        threadA.setName("thread-A");
        Thread threadB = new Thread(userB);
        threadB.setName("thread-B");

        threadA.start();
        //sleep(2000); //동시성 문제 발생X
        sleep(100); //동시성 문제 발생O
        threadB.start();

        sleep(3000); //메인 쓰레드 종료 대기
        log.info("main exit");
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadLocalService {

    private ThreadLocal<String> nameStore = new ThreadLocal<>();

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

테스트 코드를 실행하면 아래와 같은 결과가 나옵니다. sleep(100)으로 하더라도 실행되는 쓰레드가 달라서 변수를 공유하지 않기때문에 아래와 같은 결과가 나올 수 있었습니다.

 

ThreadLocal 주의사항

쓰레드 로컬의 값을 사용 후 제거하지 않고, 그냥두면 WAS처럼 쓰레드 풀을 사용하는 방식인 경우 큰 문제가 발생할 수 있습니다. 이것 역시 그림을 통해 이해해 보도록 하겠습니다.

[저장 요청] - 이런경우는 문제가 되지 않음

  • 사용자A가 저장 요청을 보냄.
  • WAS가 쓰레드풀에서 thread-A를 할당함
  • thread-A는 사용자A의 정보를 쓰레드 로컬에 저장한다.
  • 쓰레드 로컬의 사용자A의 정보가 삭제되지 않고 존재한다.

[조회 요청] - 이런경우 문제가 발생

  • 사용자B가 조회 요청을 보냄.
  • WAS가 쓰레드풀에서 하필 thread-A를 할당함(다른 쓰레드가 할당 될 수도 있다)
  • 이번에는 조회요청이기 때문에 thread-A는 쓰레드로컬에서 데이터를 조회한다.
  • 쓰레드로컬에서 삭제하지 않은 사용자A정보를 조회한다.
  • 사용자B는 사용자A의 정보를 조회하게 된다.

이러한 문제때문에 ThreadLocal을 사용할때는 쓰레드가 종료되는 시점에 remove()메서드를 통해서 꼭 제거를 해야한다.

'Java and Spring' 카테고리의 다른 글

[Spring] 빈 후처리기  (0) 2021.12.11
[Spring] 프록시 팩토리  (0) 2021.12.08
[Java] CGLIB(Code Generator Library)  (0) 2021.12.04
[Java] 동적 프록시(Dynamic Proxy)  (0) 2021.12.01
[Spring] Lombok 사용법  (0) 2021.11.07