Java에서 동시성 문제 해결하기

MinSeong Kang·2022년 9월 10일
0

java

목록 보기
2/5

스프링 컨테이너는 등록된 스프링 빈들을 모두 싱글톤으로 관리한다. 따라서 스프링 빈으로 등록된 객체의 인스턴스는 애플리케이션에 딱 하나만 존재하게 된다. 싱글톤으로 관리해서 메모리 낭비를 방지할 수 있지만 조심해야하는 점이 있다. 그것은 필드 동시성 문제이다.

만약 싱글톤으로 관리하는 인스턴스의 필드를 여러 쓰레드가 동시에 접근하여 필드 값을 변경하면서 발생하는 문제이다.

필드 동시성 문제

@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
public class FieldServiceTest {

    private FieldService fieldService = new FieldService();

    @Test
    void field() {
        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();
        threadB.start();
        sleep(3000);
    }

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

  • FieldService 를 싱글톤으로 사용하는 테스트를 해보았다. 두 쓰레드가 FieldService에 동시에 접근하여 FieldService 내 필드인 nameStore의 값을 변경하려고 한다. 이후 로그를 찍어보았다.
  • 그 결과 thread-B가 먼저 nameStore에 userB를 저장했지만, 그 동시에 thread-A가 nameStore 값을 userA로 변경했기 때문에, thread-A와 thread-B 모두 nameStore에 마지막에 저장한 userA를 가르키고 있는 것이다!
  • 필드 동시성 문제가 발생!!

필드 동시성 문제 해결

자바에서 필드 동시성 문제를 해결하기 위해 ThreadLocal 클래스를 제공한다!!
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();
        }
    }
}
  • ThreadLocal을 사용하는 방법은 다음과 같이 싱글톤으로 등록한 객체의 필드를 ThreadLocal 클래스로 생성해주면 된다.
  • 또한 ThreadLocal에서 값을 가져올 때는 get(), 저장할 때는 set() 메서드를 사용하면 된다.
@Slf4j
public class ThreadLocalServiceTest {

    private ThreadLocalService service = new ThreadLocalService();

    @Test
    void field() {
        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();
        threadB.start();
        sleep(3000); // 메인 쓰레드 종료 대기
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
  • ThreadLocal을 적용하기 전 테스트와 동일한 로직으로 ThreadLocal를 적용하여 테스트를 해본 결과, 아래와 같이 thread-A, thread-B 모두 각 쓰레드가 nameStore에 저장한 값을 잘 가져오는 것을 확인할 수 있다.

ThreadLocal 주의할 점

만약 쓰레드 로컬의 값을 사용 후 제거하지 않으면 쓰레드 풀을 사용하는 경우 심각한 문제가 발생할 수 있다.
톰캣과 같이 쓰레드 풀을 사용하여 thread를 재활용하는 경우, 이전에 저장했던 ThreadLocal의 값이 남아있어 원치않는 동작을 할 수 있다. 따라서 쓰레드 풀을 사용하는 경우 반드시 모두 사용 후 ThreadLocal의 값을 remove 메서드를 사용하여 값을 제거해주어야 한다.


Spring Security에서의 ThreadLcoal 사용

 Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

해당 코드는 Spring Security에서 사용자 인증 정보를 가져올 때 사용하는 코드이다. getContext() 메서드 내부가 ThreadLocal로 구현되어 있어, Thread 별로 인증정보를 다르게 가지고 있도록 할 수 있다.

참고자료

https://www.inflearn.com/course/스프링-핵심-원리-고급편/dashboard
https://sabarada.tistory.com/163

0개의 댓글