[Spring] 동시성과 ThreadLocal

Manx·2023년 8월 29일
0

spring

목록 보기
24/24

기본 지식

  • Client Request는 한 번에 한 개씩 들어오는 것이 아닌, 동시다발적으로 들어 온다. Java/Spring에서는 Thread를 이용해 Request를 처리한다.
    ( 정확하게 말하자면 Tomcat이 미리 만들어 놓은 Thread를 사용하여 )

1. 동시성 문제 발생 예제

다음과 같은 Service Class에 특정 객체를 save하는 로직이 있다고 가정한다.
( @Service는 테스트 코드라 적지 않음. -> 해당 객체는 Singleton )

FieldService

@Slf4j
public class FieldService {
    
    private String nameStore;

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

해당 FieldService는 인스턴스 변수로 nameStore을 가지고 있다.


TestCode

  • userA가 먼저 저장하는 로직을 수행한다.
  • 그 후 userB가 저장하는 로직을 수행한다.
@Slf4j
public class FieldServiceTest {

    private final 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(100);
        threadB.start();


        sleep(200); // Main Thread 대기
        log.info("main exit");
    }

    private void sleep(int i) {

        try {
            Thread.sleep(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

해당 순서되로 진행되므로 기대 결과는 다음과 같다.
1. userA가 저장> nameStore는 초기화 전이므로 null, userA저장
2. userA가 조회> nameStore에 저장되어 있는 userA 표기
3. userB가 저장=> nameStore에 저장되어 있는 userA 표기, userB저장
4. userB가 조회 => nameStore에 userB저장

기대 결과

main start
저장 name=userA -> nameStore=null
조회 nameStore=userA
저장 name=userB -> nameStore=userA
조회 nameStore=userB
main exit

실제 결과

main start
저장 name=userA -> nameStore=null
저장 name=userB -> nameStore=userA
조회 nameStore=userB
조회 nameStore=userB
main exit

이유
-> Spring Bean으로 등록되었다는 가정하에

  1. FieldService는 Singleton으로 선언되어 있다.
  2. Singleton으로 선언되어, nameStore는 한 개인데, 여러 Thread에서 동시에 접근이 가능하다.
  3. userA가 저장하고, 조회를 시작하기 전에 userB가 nameStore에서 저장되어 userA Thread에서 조회 값이 이상하게 변했다.

Thread Local

위에서 살펴본 동시성 문제를 해결하기 위해 Thread 단위로 로컬 변수를 할당하는 기능을 사용할 수 있다.
이를 간단하게 사용할 수 있는 Java의 Thread Local Class가 있다.


Thread Class

public class Thread implements Runnable {
    // ...
    ThreadLocal.ThreadLocalMap threadLocals = null;

Thread Class에는 다음과 같이 ThreadLocal.ThreadLocalMap을 가지고 있다.
여기서 ThreadLocalMap이란, Thread의 주소값을 Key로 하는 Map이다.

해당 threadLocals 변수를 ThreadLocal Class를 통해 간단하게 제어할 수 있다.

  • get()
    currentThread를 가져와 threadLocal에 있는 Entity를 return한다.
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
  • set()
  • remove()

예시

@Slf4j
public class ThreadLocalService {
    
    private ThreadLocal<String> nameStore = new ThreadLocal<>();

    public String logic(String name) {
        log.info("저장 name={} -> nameStore={}", name, nameStore.get());
        
        // Set nameStore
        nameStore.set(name);
        sleep(1000);
        log.info("조회 nameStore={}", nameStore.get());
        
        // Get nameStore
        return nameStore.get();
    }

    private void sleep(int millis) {

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

    }
}

ThreadLocal을 사용하면, 각 Thread 마다 자신만 접근 가능한 변수에 접근하므로 동시성 문제를 해결할 수 있다.


주의 사항

Thread Local을 사용하면, 해당 Thread만 접근할 수 있는 로컬 변수가 생기는 셈이다.

그러나, Thread는 Tomcat에서 Thread Pool에 미리 만들어 놓은 Thread를 이용하기 때문에

remove()를 수행하지 않으면 이전에 저장된 ThreadLocal 변수에
새로운 사용자가 접근해 잘못된 결과가 발생할 수 있다.



  • 스프링 핵심 원리 고급편 (김영한 님)
  • 기존 지식
profile
백엔드 개발자

0개의 댓글