
Spring에서는 싱글톤 패턴을 사용해서 Bean을 만든다. 물론 프로토타입 방식으로 만들어서 Bean의 생성까지만 관리해주기도 한다. 다만, 가장 많이 쓰이는 패턴이 싱글톤 패턴인데, 이러한 싱글톤 패턴은 인스턴스 하나만으로 여러 사용자들이 인스턴스의 필드를 공유하게 된다.
여기서 생기게 되는 문제점은 모두가 공유하기 때문에, 값의 변동이 생기게 될 경우 데이터의 불일치 문제를 야기하게 된다.
예를 들면, A라는 사람이 필드 값을 조회했을 때, 1이라는 값을 가지고 있어서 1이라는 값을 가지고 로직을 실행했으나 로직을 실행하는 도중에 B라는 사람으로 인해 필드 값이 2로 변경된 경우 A는 결과값으로 2를 받게 될 수도 있는 문제가 생기게 된다.
이렇게 동시에 어떠한 필드값에 접근했을 때 생기는 문제를 동시성 이슈라고 하는데, 트래픽이 높지 않은 상황에서는 자주 발생하지 않지만 반대로 높은 트래픽에서는 흔하게 발생할 수 있는 이슈이다.
*참고로 지역변수에서는 동시성 이슈가 발생하지 않는데, 지역 변수는 쓰레드마다 별도의 메모리 공간에서 관리가 되기 때문이다. 추가로 값을 읽기만 하는 경우에는 동시성 이슈가 생기지 않지만 위의 예시와 같이 값이 변경되는 경우에는 발생할 가능성이 충분히 존재하게 된다.
사용자만 사용할 수 있도록 쓰레드에서만 유효한 쓰레드 로컬 변수(Thread Local Variable)를 사용하게끔 하면 된다.
package com.inflearn.advancedspring.util;
import lombok.extern.slf4j.Slf4j;
@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();
}
public void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
[2] 테스트
package com.inflearn.advancedspring;
import com.inflearn.advancedspring.util.ThreadLocalService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
public class ThreadLocalTest {
ThreadLocalService threadLocalService = new ThreadLocalService();
@Test
void threadLocalTest(){
// 실행하기 전 세팅
Runnable userA = () -> {
threadLocalService.logic("userA");
};
Runnable userB = () -> {
threadLocalService.logic("userB");
};
// 실행할 로직을 가지고 Thread 생성
Thread threadA = new Thread(userA);
Thread threadB = new Thread(userB);
// Thread 이름 지정
threadA.setName("Thread-A");
threadB.setName("Thread-B");
threadA.start();
sleep(100);
threadB.start();
sleep(2000);
log.info("main exit");
}
private void sleep(int mills){
try {
Thread.sleep(mills);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
[3] 콘솔 출력
17:51:48.079 [Thread-A] INFO com.inflearn.advancedspring.util.ThreadLocalService -- 저장 Name = userA -> NameStore -> null
17:51:48.180 [Thread-B] INFO com.inflearn.advancedspring.util.ThreadLocalService -- 저장 Name = userB -> NameStore -> null
17:51:49.086 [Thread-A] INFO com.inflearn.advancedspring.util.ThreadLocalService -- NameStore -> userA
17:51:49.185 [Thread-B] INFO com.inflearn.advancedspring.util.ThreadLocalService -- NameStore -> userB
17:51:50.184 [Test worker] INFO com.inflearn.advancedspring.ThreadLocalTest -- main exit
출력을 보면 알 수 있겠지만, Thread-A와 Thread-B 각각 다른 쓰레드에서 실행을 하였고, B가 nameStore를 조회하였을 때 정상적으로 null이 출력되는 걸 볼 수 있다. 이후에 Sleep()을 통해서 A의 값이 출력되기 전에 Thread-B가 setName()메서드를 실행하였는데, Thread-A에서 설정된 값이 성공적으로 유지가 된다.
각 쓰레드마다 별도의 내부 저장소를 제공하고, 이러한 내부 저장소에서 사용할 수 있는 변수를 쓰레드 로컬 변수라고 한다. 요청 1개가 1개의 쓰레드를 사용한다고 하면, 해당 쓰레드에서만 변수의 값이 공유가 되기 때문에 동시에 접근을 하더라도 동시성 이슈를 피해갈 수 있게 된다.
*주의해야 할 점은 WAS 서버에서 쓰레드 풀을 사용하는 경우에 쓰레드는 항상 쓰레드 로컬 변수를 사용하고 나면 remove() 메서드를 통해서 자원을 삭제해야 한다. 그렇지 않으면 쓰레드 로컬에 남아있던 데이터들이 계속해서 유지가 되기 때문에 쓰레드를 반환한다고 해도 다음 사람이 사용할 때 남아있던 데이터에 의해서 문제를 야기하게 될 수도 있다.
잘못된 정보는 지적해주시면 감사하겠습니다.