Backend/Spring

동시성 문제 해결을 위한 ThreadLocal 이해하기

누구세연 2024. 11. 2. 17:08

Spring 웹 애플리케이션은 요청마다 새로운 스레드를 생성하여 처리합니다.

이 멀티스레드 환경에서 각 요청이 독립적이어야 하지만, 공용 데이터에 동시에 접근하면 동시성 문제가 발생할 수 있습니다.

ThreadLocal은 이러한 문제를 해결하는 데 유용한 도구로, 각 스레드에 독립된 저장 공간을 제공하여 안전하게 데이터를 관리할 수 있게 합니다. 이번 글에서는 ThreadLocal의 개념과 Spring 프레임워크에서의 활용 방법을 살펴보겠습니다.😽

 

ThreadLocal이란?

📝 `ThreadLocal`은 각각의 스레드가 독립적으로 변수 값을 저장할 수 있도록 하는 클래스

멀티스레드 환경에서는 여러 스레드가 하나의 자원을 동시에 접근하는 경우가 많은데 이때 발생하는 동시성 문제를 해결하기 위해 각 스레드에 고유한 변수를 할당할 수 있도록 도와줍니다.

 

간단히 말해, 스레드마다 별도의 변수 복사본을 가지고 작업하도록 하는 개념입니다.

웹 애플리케이션에서 각 사용자의 요청을 별개의 스레드로 처리할 때 특정 스레드에만 접근 가능한 데이터를 저장하고 싶은 경우에 유용합니다.

 

ThreadLocal의 동작 원리

`ThreadLocal`의 작동 원리를 이해하기 위해 `ThreadLocal`의 핵심 메서드인 `get()`과 `set()`을 살펴보겠습니다.

 

먼저 `ThreadLocal` 변수를 설정할 때는 set() 메서드를 사용합니다.

`set()` 메서드는 현재 스레드에서만 접근 가능한 데이터를 `ThreadLocal`에 저장합니다.

set 메서드

  • `Thread.currentThread()`를 호출하여 현재 스레드를 가져옵니다.
  • `getMap()` 메서드를 통해 현재 스레드의 `ThreadLocalMap` 객체를 가져옵니다.
    이 `ThreadLocalMap`은 해당 스레드에 저장된 모든 `ThreadLocal` 데이터들을 관리합니다.
  • 만약 `ThreadLocalMap`이 없다면 `createMap()`을 호출해 새로운 Map을 생성합니다.

`ThreadLocalMap`은 각 스레드가 자기만의 데이터 저장소를 갖도록 하는데, 이 구조 덕분에 다른 스레드와 데이터가 공유되지 않습니다.

 

`get()`메서드는 현재 스레드의 `ThreadLocalMap`에서 데이터를 가져옵니다.

get 메서드

  • 현재 스레드의 `ThreadLocalMap`에서 값을 조회하며 만약 값이 없으면 `setInitialValue()`를 호출해서 초기값을 설정합니다.

이로 인해 각 스레드는 독립적으로 값을 저장하고 읽을 수 있게 됩니다.

 

ThreadLocal의 활용 예시

웹 애플리케이션에서 사용자의 인증 정보나 트랜잭션 정보와 같은 데이터를 각 스레드마다 독립적으로 저장해야 할 때 유용합니다.

예를 들어, 트랜잭션 정보를 `ThreadLocal`을 통해 저장하고 읽어오는 코드의 예는 다음과 같습니다.

public class TransactionManager {
    private static final ThreadLocal<Transaction> transactionHolder = new ThreadLocal<>();

    public static void startTransaction() {
        transactionHolder.set(new Transaction());  // 각 스레드에 개별 트랜잭션 저장
    }

    public static Transaction getTransaction() {
        return transactionHolder.get();           // 현재 스레드의 트랜잭션 가져오기
    }

    public static void endTransaction() {
        transactionHolder.remove();               // 현재 스레드에 트랜잭션 제거
    }
}

이 코드를 통해 각 요청(스레드)은 별도의 트랜잭션을 유지하게 됩니다. 이는 트랜잭션이 종료될 때까지 다른 스레드에서 접근할 수 없습니다.

 

ThreadLocalMap의 내부 구조

ThreadLocalMap은 ThreadLocal 객체를 키로 사용해 데이터 저장소를 관리합니다. 이 구조를 통해 스레드가 종료될 때까지 ThreadLocal 변수에 저장된 값이 유지되며, 다른 스레드에서는 이 값에 접근할 수 없습니다.

 

ThreadLocalMap에서 중요한 점은 메모리 누수 문제인데, 스레드가 종료되지 않으면 ThreadLocalMap의 데이터가 계속해서 메모리를 점유할 수 있습니다. 이를 방지하기 위해 remove() 메서드를 사용해 데이터를 명시적으로 제거해야 합니다.

 

Spring에서의 ThreadLocal활용

Spring 프레임워크는 `ThreadLocal`을 활용하여 `@Transaction`이나 `@RequestScope` 같은 어노테이션이 내부적으로 어떻게 동작하는지 설명할 수 있습니다.

@Transactional

  • 이 어노테이션을 사용하면 트랜잭션이 시작됩니다.
  • Spring은 트랜잭션 정보를 ThreadLocal에 저장합니다.
    이로 인해 현재 요청과 관련된 트랜잭션을 안전하게 관리할 수 있습니다.
  • 예를 들어, 여러 요청이 동시에 처리될 때 각 요청은 자신의 트랜잭션 정보를 가지고 있기 때문에 서로의 트랜잭션에 영향을 주지 않습니다.

@RequestScope

  • 이 어노테이션은 HTTP 요청에 대한 데이터를 저장하는 데 사용됩니다.
  • `ThreadLocal`을 활용하여 각 요청이 끝날 때까지 데이터를 유지할 수 있습니다.
  • 즉, 사용자가 요청을 보낼 때 생성된 데이터는 그 요청이 처리되는 동안만 존재하고 요청이 끝나면 자동으로 사라집니다.

 

💡 ThreadLocal을 통해 Spring에서는 요청마다 독립적인 데이터 관리를 할 수 있습니다. 멀티스레드 환경에서 발생할 수 있는 데이터 충돌이나 동시성 문제를 효과적으로 해결할 수 있습니다.