본 게시물은 스스로의 공부를 위한 글입니다.
잘못된 내용이 있으면 댓글로 알려주세요!
예제
🎈 스프링 프로젝트 생성 - gradle, lombok 디펜던시 추가
🎈 build.gradle
수정. 테스트에서도 lombok을 사용하기 위해서이다.
dependencies {
...
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
@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();
}
}
}
name
을 전역 변수인 nameStore
에 저장 후, 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(); //A실행
sleep(2000); //동시성 문제 발생X
// sleep(100); //동시성 문제 발생O
threadB.start(); //B실행
sleep(3000); //쓰레드B가 끝날 때까지 메인 쓰레드 종료 대기
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
sleep(3000);
이 있는 이유는 쓰레드 B가 실행을 모두 마치기 전에 메인 쓰레드가 종료하면, 쓰레드 B도 함께 강제 종료되기 때문이다.FieldServiceTest
에서 중간에 sleep(2000);
을 사용한 경우 동시성 문제가 없다.[Test worker] main start
[Thread-A] 저장 name=userA -> nameStore=null
[Thread-A] 조회 nameStore=userA
[Thread-B] 저장 name=userB -> nameStore=userA
[Thread-B] 조회 nameStore=userB
[Test worker] main exit
FieldServiceTest
에서 중간에 sleep(100);
을 사용한 경우 동시성 문제가 있다.[Test worker] main start
[Thread-A] 저장 name=userA -> nameStore=null
[Thread-B] 저장 name=userB -> nameStore=userA
[Thread-A] 조회 nameStore=userB
[Thread-B] 조회 nameStore=userB
[Test worker] main exit
nameStore
에 저장 후 조회를 하는데는 1초의 딜레이 시간이 있다.nameStore
에 접근해서 값을 바꿔버린다면?? 쓰레드A는 바뀐 값을 얻게 된다.이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타나지 않고, 트레픽이 점점 많아질수록 자주 발생한다.
동시성 문제는 지역 변수에서는 발생하지 않는다. 동시성 문제는 인스턴스의 필드(주로 싱글톤), 또는 static 같은 공용 필드에 접근할 때 발생한다.
특히 스프링 빈처럼 싱글톤 객체의 필드를 변경하며 사용할 때 이러한 동시성 문제를 조심해야 한다.
thread-A
는 thread-A
전용 보관소에서 필드 데이터를 반환해준다.예제 수정
@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();
}
}
}
private FieldService fieldService = new FieldService();
부분을 private ThreadLocalService service = new ThreadLocalService();
로 수정값 저장: ThreadLocal.set(xxx)
값 조회: ThreadLocal.get()
값 제거: ThreadLocal.remove()
주의
쓰레드 로컬의 값을 사용 후 제거하지 않고 그냥 두면 메모리 누수가 발생할 수 있다.
또한 WAS(톰캣)처럼 쓰레드 풀을 사용하는 경우에는 해당 쓰레드를 제거하지 않고 나중에 재사용하게 된다.
ThreadLoal
에 값도 그 전 값이 그대로 저장되어있다. 따라서 다른 데이터가 반환되는 심각한 문제가 발생한다.따라서 반드시 ThreadLocal 사용 후에는 .remove()
를 해주자!!
인프런의 '스프링 핵심 원리 고급편(김영한)'을 스스로 정리한 글입니다.
자세한 내용은 해당 강의를 참고해주세요.