FireDrago

[Spring] 동시성 문제와 쓰레드 로컬 (Thread Local) 본문

프로그래밍/Spring

[Spring] 동시성 문제와 쓰레드 로컬 (Thread Local)

화이용 2024. 5. 7. 16:35

동시성 문제

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

위 코드는 이름을 입력받고 전역변수 nameStore에 저장하고, 저장 및 조회 로그를 출력하는 코드이다.

logic() 메서드를 서로 다른 두개의 쓰레드가 동시에 호출하면 어떻게 될까? 테스트 코드로 살펴보자

 

@Slf4j
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(); // A 실행
        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();
        }
    }
}

테스트 실행 결과

[main] INFO hello.advanced.threadlocal.FieldServiceTest -- main start
[thread-A] INFO hello.advanced.threadlocal.code.FieldService -- 저장 name=userA -> nameStore=null
[thread-B] INFO hello.advanced.threadlocal.code.FieldService -- 저장 name=userB -> nameStore=userA //userA 소멸
[thread-A] INFO hello.advanced.threadlocal.code.FieldService -- 조회 nameStore=userB //A 쓰레드 조회 결과
[thread-B] INFO hello.advanced.threadlocal.code.FieldService -- 조회 nameStore=userB //B 쓰레드 조회 결과
[main] INFO hello.advanced.threadlocal.FieldServiceTest -- main exit

 

threadA 실행하고, 0.1초 뒤에 threadB를 실행한다. 

이렇게 되면, threadA가 'userA' 값을 저장하고,

출력하는 사이에 threadB가 'userB'값을 저장한다.

userA값은 사라지고, userB값이 초기화 된다.

threadA의 조회값과 threadB의 조회값이 모두

userB로 나타난다. 하나의 인스턴스 필드값을 두 쓰레드가 

공유해서 발생하는 문제이다.

 

이처럼 여러 쓰레드가 동시에 같은 인스턴스의 필드값을

변경하면서 발생하는 문제가 동시성문제이다.

 

동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 

접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상

잘 나타나지 않고, 트래픽이 점점 많아질 수 록

자주 발생한다. 특히 스프링 빈 처럼 싱글톤 객체의 

필드를 변경하며 사용할 때 동시성 문제를 조심해야 한다.

 

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

동시성 문제

1. 여러 쓰레드가 동시에 같은 인스턴스의 필드값을 변경하면서 발생하는 문제

2. 트래픽이 많을 수록, 스프링 빈 처럼 싱글톤 객체의 필드일때 더 조심할것

3. 지역변수에서는 동시성 문제가 발생하지 않는다. 같은 인스턴스의 필드, static 공용 필드에서 발생한다.

4. 값을 읽기만 할때는 동시성 문제가 발생하지 않는다. 값을 변경해야 발생한다.

 

 

ThreadLocal (쓰레드 로컬)

쓰레드 로컬은 멀티쓰레드 환경에서 쓰레드마다 별도의 내부 저장소를 제공한다.

각각의 쓰레드만 개인적으로 사용할 수 있는 비밀 수납장이다.

쓰레드 로컬을 사용하여 동시성 문제를 예방 할 수 있다.

 

ThreadLocal 사용법

메서드 기능 예시
set( ) 쓰레드 로컬에 데이터 저장하기 ThreadLocal.set( );
get( ) 쓰레드 로컬에서 데이터 가져오기 ThreadLocal.get( );
remove( ) 쓰레드 로컬 데이터 제거하기 , 쓰레드가 종료되면 반드시 실행 ThreadLocal.remove( );

쓰레드 작업이 끝나면, 쓰레드는 쓰레드 풀에 반납된다. 이때 ThreadLocal.remove() 호출하지 않으면,

쓰레드 풀에서 해당 쓰레드가 다시 사용될때, 이전에 저장된 값이 다시 반환될 수 있다. 다른 사용자의 데이터가 노출될 수 있는 것이다. 이때문에 쓰레드가 종료될때 항상 remove()를 호출하여 쓰레드 로컬 데이터를 제거해야 한다.

 

ThreadLocal 적용

@Slf4j
public class ThreadLocalService {
	// 쓰레드 로컬을 전역변수로 할당
    private ThreadLocal<String> nameStore = new ThreadLocal<>();

    public String logic(String name) {
        log.info("저장 name={} -> nameStore={}", name, nameStore.get());
        // 쓰레드 로컬에 name 문자열 저장
        nameStore.set(name);
        sleep(100);
        log.info("조회 nameStore={}", nameStore.get());
        // 쓰레드 로컬에서 데이터 가져오기
        return nameStore.get();
    }

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

 

동시성 문제를 발생시킨 전역변수를 ThreadLocal<String> 변경했다.

다시 테스트를 실행해보자

 

@Slf4j
public class ThreadLocalTest {

    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");
    }

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

테스트 실행 결과 콘솔

[main] INFO hello.advanced.threadlocal.ThreadLocalTest -- main start
[thread-A] INFO hello.advanced.threadlocal.code.ThreadLocalService -- 저장 name=userA -> nameStore=null
[thread-A] INFO hello.advanced.threadlocal.code.ThreadLocalService -- 조회 nameStore=userA
[thread-B] INFO hello.advanced.threadlocal.code.ThreadLocalService -- 저장 name=userB -> nameStore=null
[thread-B] INFO hello.advanced.threadlocal.code.ThreadLocalService -- 조회 nameStore=userB
[main] INFO hello.advanced.threadlocal.ThreadLocalTest -- main exit

 

동시성 문제가 해결된 것을 확인 할 수 있다.