[Spring] ThreadLocal을 통한 동시성 문제 해결

서규범·2022년 10월 20일
0

동시성 문제가 발생하는 이유

  • 싱글톤으로 등록된 스프링 빈은 객체의 인스턴스가 애플리케이션에 딱 1개만 존재
  • 이렇게 하나만 있는 인스턴스의 필드를 여러 쓰레드가 동시에 접근할 경우 문제가 발생

동시성 문제 - 예제 코드

FieldService.java

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


FieldServiceTest.java

@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(2000); //동시성 문제 발생X
	// 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();
 	}
 
	}
}


동시성 문제가 일어나는 과정

  • 먼저 thread-A 가 userA 값을 nameStore 에 보관한다.

  • 0.1초 이후에 thread-B 가 userB 의 값을 nameStore 에 보관한다. 기존에 nameStore 에 보관되어 있던 userA 값은 제거되고 userB 값이 저장된다.

  • thread-A 의 호출이 끝나면서 nameStore 의 결과를 반환받는데, 이때 nameStore 는 앞의 2번에서 userB 의 값으로 대체되었다. 따라서 기대했던 userA 의 값이 아니라 userB 의 값이 반환된다.
  • thread-B 의 호출이 끝나면서 nameStore 의 결과인 userB 를 반환받는다.

결과적으로 Thread-A 입장에서는 저장한 데이터와 조회한 데이터가 다른 문제가 발생한다. 이처럼 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라 한다.
이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타나지 않고, 트래픽이 점점 많아질 수 록 자주 발생한다.
특히 스프링 빈 처럼 싱글톤 객체의 필드를 변경하며 사용할 때 이러한 동시성 문제를 조심해야 한다.



ThreadLocal 이란?

  • 쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소


  • thread-A 가 userA 라는 값을 저장하면 쓰레드 로컬은 thread-A 전용 보관소에 데이터를 안전하게 보관한다.


  • thread-B 가 userB 라는 값을 저장하면 쓰레드 로컬은 thread-B 전용 보관소에 데이터를 안전하게 보관한다.


  • 쓰레드 로컬을 통해서 데이터를 조회할 때도 thread-A 가 조회하면 쓰레드 로컬은 thread-A 전용 보관소에서 userA 데이터를 반환
  • thread-B 가 조회하면 thread-B 전용 보관소에서 userB 데이터를 반환
  • 자바는 언어차원에서 쓰레드 로컬을 지원하기 위한 java.lang.ThreadLocal 클래스를 제공


ThreadLocal - 예제 코드


ThreadLocalService.java

@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();
		}
 
	}
}
  • 기존에 있던 FieldService 와 거의 같은 코드인데, nameStore 필드가 일반 String 타입에서 ThreadLocal 을 사용하도록 변경


ThreadLocalServiceTest.java

@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");
  • 쓰레드 로컬 덕분에 쓰레드 마다 각각 별도의 데이터 저장소를 가지게 됨, 결과적으로 동시성 문제도 해결
profile
하려 하자

0개의 댓글