스프링 프레임워크(Spring Framework) 톺아보기 - 동시성 문제와 쓰레드 로컬(ThreadLocal)

Janek·2023년 2월 3일
0

Spring 톺아보기

목록 보기
6/10
post-thumbnail
post-custom-banner

해당 포스팅은 인프런에서 제공하는 김영한 님의 '스프링 핵심원리 고급편'을 수강한 후 정리한 글입니다. 유료 강의를 정리한 내용이기에 제공되는 예제나 몇몇 내용들은 제외하였고, 정리한 내용을 바탕으로 글 작성자인 저의 언어로 다시 작성한 글이기에 서술이 부족하거나 잘못된 내용이 있을 수 있습니다. 그렇기에 해당 글은 개념에 대한 참고 정도만 해주시고, 강의를 통해 학습하시기를 추천합니다.

동시성 문제

여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라고 하며 쓰레드가 많아질수록, 즉 트래픽이 늘어날수록 자주 발생하게 된다. 지역 변수와 같은 쓰레드 별 메모리 영역이 할당되는 값은 해당되지 않으며 스프링 빈, static 객체와 같이 싱글톤 객체의 필드를 변경하며 사용할 때 발생한다. 이를 해결하기 위해 사용하는 것이 쓰레드 로컬(ThreadLocal)이다.

동시성 문제에 대한 java에서의 처리 방법이 궁금해져서 OpenAi의 ChatGPT에게 물어보았더니 아래와 같은 답변을 얻었다.

Java에서 동시성 문제를 해결하는 방법에는 다음과 같은 것들이 있습니다.

  1. Synchronized 키워드 : Java에서 synchronized 키워드를 사용하여 메소드 또는 블록을 잠글 수 있습니다.

  2. Lock 객체 : Java에서 Lock 객체를 사용하여 동시적으로 접근하려는 자원에 대한 접근을 제어할 수 있습니다.

  3. Atomic 클래스 : Java에서 Atomic 클래스를 사용하여 동시성 문제가 발생하는 경우 자동적으로 락을 걸어주고 해제하여 동시성 문제를 해결할 수 있습니다.

  4. Executor Framework : Java에서 Executor Framework를 사용하여 다중 스레드 환경에서 동시성 문제를 해결할 수 있습니다.

이 외에도 Java 5에서 추가된 Concurrent Package 등의 라이브러리를 사용하여 동시성 문제를 해결할 수 있습니다.

쓰레드 로컬(ThreadLocal)

쓰레드 로컬은 쓰레드 별로 생성되는 별도의 내부 저장소를 지칭한며, 다음과 같은 기능을 제공한다.

  • 값 저장 : ThreadLocal.set()
  • 값 조회 : ThreadLocal.get()
  • 값 제거 : ThreadLocal.remove()

예제 코드

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

@Slf4j
public class ThreadLocalServiceTest {

 	private 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");
 	}
    
    private void sleep(int millis) {
 		try {
 			Thread.sleep(millis);
		 } catch (InterruptedException e) {
			 e.printStackTrace();
 		}
 	}
}

실행 결과

[Test worker] main start
[Thread-A] 저장 name=userA -> nameStore=null
[Thread-B] 저장 name=userB -> nameStore=null
[Thread-A] 조회 nameStore=userA
[Thread-B] 조회 nameStore=userB
[Test worker] main exit

쓰레드 로컬 사용시 주의점

쓰레드 로컬을 모두 사용 후 ThreadLocal.remove()를 통해 쓰레드 로컬에 저장된 값을 제거해주어야 한다. 쓰레드의 생성 비용은 비싸기 때문에 WAS는 사용이 끝난 쓰레드를 제거하지 않고, 보통 쓰레드 풀을 통해 재사용한다. 그렇기에 사용 후 제거하지 않으면 심각한 문제가 발생할 수 있다.

profile
만들고 나누며, 세상을 이롭게 하고 싶습니다.
post-custom-banner

0개의 댓글