티스토리 뷰
동시성 문제
스프링은 기본적으로 싱글톤 빈을 등록한다.
지역변수는 쓰레드마다 각각의 다른 메모리 영역이 할당되기 때문에 동시성 문제가 발생하지 않지만, 싱글톤으로 등록된 인스턴스의 필드를 여러 쓰레드가 동시에 접근하는 경우 문제가 발생한다. 또한 동시성 문제는 값을 읽기만 해선 발생하지 않고, 어디선가 값을 변경하기 때문에 발생한다.
아래와 같은 경우는 동시성 문제가 발생하는 코드이다.
코드를 살펴보며 문제를 확인해보자.
FieldService 클래스
@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();
}
}
}
- 비지니스 로직을 처리할 클래스
- log.info("저장 name={} nameStore={}", name, nameStore);
logic
을 호출하는 사용자의 이름과 현재nameStore
에 저장되어 있는 사용자의 이름을 출력한다.
- nameStore = name;
- 호출한 사용자의 이름을
nameStore
에 저장한다.
- 호출한 사용자의 이름을
- log.info("조회 nameStore={}", nameStore);
- 1초 후,
nameStore
를 다시 출력한다.
- 1초 후,
FieldServiceTest 클래스
@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(); // thread-A 비지니스 로직 실행
sleep(2000);
threadB.start(); // thread-B 비지니스 로직 실행
sleep(3000); // 메인 쓰레드 종료 대기
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- thread-A와 thread-B는 각각 비지니스 로직을 실행한다.
- thread-A가 logic을 실행하고, 2초 후에 thread-B가 logic을 실행한다.
결과
동시성 문제가 발생하지 않았다.
이번에는 0.1초후에 바로 실행해보자.
@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(); // thread-A 비지니스 로직 실행
sleep(100);
threadB.start(); // thread-B 비지니스 로직 실행
sleep(3000); // 메인 쓰레드 종료 대기
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- thread-A가 logic을 실행하고, 0.1초 후에 thread-B가 logic을 실행한다.
결과
동시성 문제가 발생하였다.
FieldService.logic()
메서드 내부에는 sleep(1000)으로 1초의 지연시간이 있다. 따라서 thread-A
가 FieldService.logic()
내에서 작업이 끝나기전에 thread-B
가 들어와 nameStore
의 값을 바꾸었기 때문에 thread-A
의 조회 결과는 userB
가 되었다.
ThreadLocal 적용해보기
위와 같은 동시성 문제를 해결하기 위해서는 ThreadLocal
을 이용하는 방법이 있다.
ThreadLocal
은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다.
즉, thread-A
가 userA
라는 값을 저장하면 ThreadLocal은 thread-A
전용 보관소에 데이터를 저장하고, thread-B
가 userB
라는 값을 저장하면 ThreadLocal은 thread-B
전용 보관소에 데이터를 저장한다.
조회할 때도 thread-A
가 조회하면 userA
데이터를 반환해주고, thread-B
가 조회하면 userB
데이터를 반환해준다.
이번에는 ThreadLocal을 사용하여 동시성 문제를 해결해보자.
ThreadLocalService 클래스
@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
와는 다르게ThreadLocal
를 사용하여nameStore
를 선언하였다.
ThreadLocalServiceTest 클래스
@Slf4j
public class ThreadLocalServiceTest {
private ThreadLocalService service = new ThreadLocalService();
@Test
void field() {
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(); // thread-A 비지니스 로직 실행
sleep(100);
threadB.start(); // thread-B 비지니스 로직 실행
sleep(3000); // 메인 쓰레드 종료 대기
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- thread-A가 logic을 실행하고, 0.1초 후에 thread-B가 logic을 실행한다.
결과
쓰레드 로컬 덕분에 쓰레드 마다 각각 별도의 데이터 저장소를 가지게 되었다. 결과적으로 동시성 문제도 해결되었다.
Referemce
김영한. 스프링 핵심 원리 - 고급편. 인프런.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard
'Spring' 카테고리의 다른 글
[Spring] Advice 만들어보기 (0) | 2022.05.17 |
---|---|
[Spring] 리플렉션(Reflection) (0) | 2022.05.15 |
[Spring] 트랜잭션 AICD (0) | 2022.04.20 |
[Spring] JDBC(Java Database Connectivity) (0) | 2022.04.17 |
[Spring] ApplicationContext (0) | 2022.03.13 |
- Total
- Today
- Yesterday
- 스프링부트
- 스프링
- MySQL
- spring boot
- 스프링 부트
- 문자열
- Spring
- 알고리즘
- Real MySQL
- 리팩토링
- 그리디
- 노마드코더
- webflux
- 노마드
- 파이썬
- 구현
- 자료구조
- leetcode
- 데이터베이스
- 백준
- kotlin
- 코테
- 김영한
- 정렬
- 북클럽
- Algorithm
- 릿코드
- mysql 8.0
- 인프런
- 코틀린
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |