ThreadLocal

Gongmeda·2023년 9월 10일
0
post-thumbnail

동시성 문제

기본적으로 스프링은 빈을 싱글톤 객체로 등록하기 때문에 스레드간 공유하는 데이터에 대해 동시성 문제가 생길 수 있습니다.

@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();
        }
    }
}
@Slf4j
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(); //A실행
//        sleep(2000); //동시성 문제 발생X
        sleep(100); //동시성 문제 발생O
        threadB.start(); //B실행

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

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

위 코드에서 동시성 문제를 확인할 수 있습니다.

실행결과

[Test worker] main start
[Thread-A]	저장 name=userA -> nameStore=null
[Thread-B]	저장 name=userB -> nameStore=userA
[Thread-A]	조회 nameStore=userB
[Thread-B]	조회 nameStore=userB
[Test worker] main exit

의도한 바와 다르게 Thread-A 는 저장한 데이터와 조회한 데이터가 다른 문제가 발생합니다.
이처럼 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라 합니다.

  • 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타나지 않고, 트래픽이 점점 많아질수록 자주 발생한다.
  • 특히 스프링 빈 처럼 싱글톤 객체의 필드를 변경하며 사용할 때 이러한 동시성 문제를 조심해야 한다.
  • 지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당되기 때문에 지역 변수에서는 발생하지 않는다.
  • 동시성 문제는 값을 읽기만 하면 발생하지 않는다. 어디선가 값을 변경하기 때문에 발생한다.

ThreadLocal

ThreadLocal 사용법

  • 값 저장: ThreadLocal.set(xxx)
  • 값 조회: ThreadLocal.get()
  • 값 제거: ThreadLocal.remove()
@Slf4j
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();
        }
    }
}
@Slf4j
class ThreadLocalServiceTest {

    private ThreadLocalService service = new ThreadLocalService();

    @Test
    void threadLocal() {
        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(100);
        threadB.start();

        sleep(2000);
        log.info("main exit");
    }
    
    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

실행 결과

[Test worker] main start
[Thread-A]	저장 name=userA -> nameStore=null
[Thread-B]	저장 name=userB -> nameStore=null
[Thread-A]	조회 nameStore=userA
[Thread-B]	조회 nameStore=userB
[Test worker] main exit

쓰레드 로컬 덕분에 쓰레드 마다 각각 별도의 데이터 저장소를 가지게 되어 동시성 문제가 해결되었습니다.

주의사항

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

  1. 요청자 A가 Thread-1 을 배정 받고 로컬 저장소에 값을 저장한뒤 스레드를 스레드 풀에 반환한다.
  2. 요청자 B도 동일하게 Thread-1 을 배정 받고 로컬 저장소 값을 조회한다.
  3. 요청자 A가 저장했던 정보가 그대로 있다.

결과적으로 B는 A의 데이터를 확인하게 되는 심각한 문제가 발생하게 됩니다.
이런 문제를 예방하려면 사용자A의 요청이 끝날 때 쓰레드 로컬의 값을 ThreadLocal.remove() 를 통해서 꼭 제거해야 합니다.

profile
백엔드 깎는 장인

0개의 댓글