이번 시간에는 Spring Framework에서 동시성 문제를 해결해 줄 ThreadLocal에 대해서 알아보겠습니다.
스프링은 빈을 싱글톤으로 등록한다. 애플리케이션에 딱 1개만 존재한다는 뜻이다.
이렇게 하나만 있는 인스턴스에 하나의 필드에 여러 쓰레드가 동시에 접근하여 수정하게 되면 A 사용자가 B 사용자의 데이터를 보게 되는 사고가 발생될 수 있다. 아래 목차와 같이 순서대로 동시성 원인부터 해결 방안까지 알아보겠습니다.
<목차>
1. 동시성 문제 발생하는 예시
2. ThreadLocal을 이용해서 동시성 문제 해결 방안
3. ThreadLocal의 주의사항
1. 동시성 문제 발생하는 예시
FieldService.java
1. nameStore을 맴버변수로 선언
2. nameStore 변수에 값을 저장하고 1초 뒤에 저장한 nameStore 출력
@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
1. threadA.setName("thread-A"); -- Thread 이름 설정
2. Thread A 시작하고 2초 잠시 멈추기 때문에 동시성 발생 안함.
3. Thread B 시작하고 sleep(2000) 안하면 쓰레드 끝나기 전에 테스트 코드가 종료되서 모든 로그가 출력이 안되서 추가해야 한다.
@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"); // 1. thread 이름 설정
Thread threadB = new Thread(userB);
threadB.setName("thread-B");
threadA.start();
sleep(2000); // 2. 동시성 문제 발생x
threadB.start();
sleep(2000); // 3. 메인 쓰레드 종료 대기
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
TestCode 결과
1. thead-A, thread-B 위에서 설정한 이름
2. 정상적으로 thread-A에 userA thread-B에 userB 출력
동시성 문제 발생
FieldServiceTest.java
1. ThreadA가 시작하고 0.1초만에 ThreadB 접근
@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();
sleep(100); // 1. 동시성 문제 발생
threadB.start();
sleep(2000);
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
TestCode 결과
1. thread-A에 조회할 때 그 전에 thread-B가 nameStore을 수정해서 동시성 문제 발생
2. ThreadLocal을 이용해서 동시성 문제 해결 방안
ThreadLocal은 해당 쓰레드만 접근할 수 있는 별도의 특별한 내부 저장소이다.
ThreadLocalService.java
1. String nameStore -> ThreadLocal<String> nameStore 변경
2. nameStore.set() - 값 저장
nameStore.get() - 값 조회
nameStore.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();
}
}
}
ThreadLocalServiceTest.java
1. FieldService만 ThreadLocalService로 수정
@Slf4j
public class ThreadLocalServiceTest {
private ThreadLocalService threadLocal = new ThreadLocalService();
@Test
void field() {
log.info("main start");
Runnable userA = () -> {
threadLocal.logic("userA");
};
Runnable userB = () -> {
threadLocal.logic("userB");
};
Thread threadA = new Thread(userA);
threadA.setName("thread-A");
Thread threadB = new Thread(userB);
threadB.setName("thread-B");
threadA.start();
sleep(100); // 동시성 문제 발생x
threadB.start();
sleep(2000); // 메인 쓰레드 종료 대기
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
TestCode 결과
1. ThreadLocal이라는 별도의 쓰레드 저장소를 사용함으로써 동시성 문제를 해결하였습니다.
3. ThreadLocal의 주의사항
ThreadLocal 값을 사용 후 제거하지 않고 그냥 두면 WAS에서 쓰레드 풀을 사용하는 경우 심각한 문제가 발생할 수 있다.
보통 was는 쓰레드 생성 비용이 비싸기 때문에 쓰레드를 사용하면 반납하고 재사용하게 된다.
그러므로 thead-A를 사용하고 쓰레드 값을 제거하지 않을 상태에서 반납 후 다른 사용자 HTTP 요청이 thead-A에 할당되면 thead-A 보관소에 그대로 값이 있어서 다른 사용자에게 잘못된 정보가 노출될 수 있다.
이런 문제를 예방하기 위해서는 사용자 요청이 끝날 때 쓰레드 로컬의 값을 ThreadLocal.remove()를 통해서 꼭 제거해야 한다.
* Spring Framework 같은 경우 Filter나 Interceptor를 이용해서 깔끔하게 제거해 주는 것이 안전하다.
참조
- inflearn 강의(스프링 핵심 원리 - 고급편, 김영한)
'Spring' 카테고리의 다른 글
토이 프로젝트_STEP 04(Jenkins pipeline 배포) (0) | 2022.11.20 |
---|---|
[Spring] @Value 애노테이션 null 점검 (0) | 2022.10.20 |
토이 프로젝트 _ STEP 03(Web Application 개발) (0) | 2022.08.15 |
[SpringBoot] Interceptor 적용하기 (0) | 2022.06.23 |
[SpringBoot] Filter 적용하기 (0) | 2022.06.22 |