[ 김영한 스프링 고급편 #2 ] - 동시성 이슈와 쓰레드 로컬

정동욱·2023년 8월 1일
post-thumbnail

지난 글에 이어 동시성 이슈에 대해 다시 한 번 정리해보겠습니다.

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

이런 동시성 문제는 지역 변수에서는 발생하지 않는다. 지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당된다. 동시성 문제가 발생하는 곳은 같은 인스턴스의 필드(주로 싱글톤에서 자주 발생), 또는 static 같은 공용 필드에 접근할 때 발생한다. > 동시성 문제는 값을 읽기만 하면 발생하지 않는다. 어디선가 값을 변경하기 때문에 발생한다.


이를 해결하기 위해 이번 강의에서는 쓰레드 로컬을 이용합니다. 쓰레드 로컬이란 쓰레드마다 저장소를 제공하는 개념인데요, 그림으로 나타내면 아래와 같습니다.

그러니까 각 쓰레드마다 싱글톤 데이터에 대해 별도의 저장소를 할당해서, 해당 저장소에서 데이터를 조회하고 수정하는 것이죠. 이때 ThreadLocal 객체가 사용되는데요, 코드는 아래와 같습니다.

@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();
        }
    }

}

set() 매서드를 통해 쓰레드 로컬의 저장소에 값을 저장하고, get() 매서드를 통해 저장소에 있는 값을 꺼냅니다. 주의할 점은 하나의 쓰레드 로컬에 대해서 1개의 데이터만 저장할 수 있습니다. 즉, set() 매서드를 두 번 이상 쓸 경우 가장 최근에 넣은 값만이 저장됩니다.

또 하나 더 주의할 사항이 있는데요, 바로 요청이 끝날 때 쓰레드 로컬을 지워줘야 한다는 것입니다. 예를 들어 사용자 A가 웹서버로 요청을 했고 쓰레드 A가 쓰레드 로컬을 이용해 데이터를 저장하고 응답한 뒤 다시 쓰레드풀에 되돌아 왔다고 가정해봅시다. 이 때, 여전히 쓰레드 A의 쓰레드 로컬에는 사용자 A가 저장한 값이 그대로 남아있기 때문에 만약 사용자 B의 요청에 대해 쓰레드 A가 동작한다면, 사용자 B에게는 올바르지 않은 데이터가 응답될 가능성이 생기기 때문입니다. 그래서 꼭 remove()를 사용해 직접 지워주거나 필터를 사용해 지워주어야 합니다.

이번 글에서 동시성 이슈와 이에 대한 해결 방법 중 하나인 쓰레드 로컬에 대해 알아보았습니다. 다음 글에서는 스프링의 템플릿 매서드 패턴에 대해 알아보겠습니다.

profile
거인의 어깨 위에서 탭댄스를

0개의 댓글