인프런 스프링 고급편 강의를 듣던 중, 동시성 처리의 방법 중 하나로 Thread Local이라는 개념이 나왔다. 나온김에 간단히 정리하고 넘어가자.
쓰레드 로컬은 특정 쓰레드와 일대일 관계로 해당 쓰레드에서만 참조 가능한 일종의 전역 변수를 생성하게 해준다
. 그렇기 때문에 두 개 이상의 쓰레드가 같은 값을 참조하는 동시성 문제가 발생하는 경우 쓰레드 로컬을 사용하여 이를 풀어낼 수 있다.
동시성 문제가 발생하는 상황과 이를 쓰레드 로컬로 해결하는 예제를 살펴보자.
class FieldService(
private var nameStore: String? = null // 클래스 변수에 값을 저장하고, 1초뒤에 그 값을 반환한다.
) {
fun logic(name: String): String? {
log.info("저장 name={} -> nameStore={}", name, nameStore)
nameStore = name
sleep(1000)
log.info("조회 nameStore={}", nameStore)
return nameStore
}
private fun sleep(millis: Long) {
try {
Thread.sleep(millis)
} catch(e: InterruptedException) {
e.printStackTrace()
}
}
companion object : Log
}
class FieldServiceTest {
private val fieldService = FieldService()
@Test
fun field() {
log.info("main start")
val userA = Runnable { fieldService.logic("userA") }
val userB = Runnable { fieldService.logic("userB") }
val threadA = Thread(userA)
threadA.name = "thread-A"
val threadB = Thread(userB)
threadB.name = "thread-B"
threadA.start()
//sleep(2000) // 동시성 문제 x
sleep(100) // A가 반환되기 전에 B가 세팅됨 (동시성 문제 발생)
threadB.start()
sleep(3000); //메인 쓰레드 종료 대기
log.info("main exit");
}
private fun sleep(millis: Long) {
try {
Thread.sleep(millis)
} catch(e: InterruptedException) {
e.printStackTrace()
}
}
companion object : Log
}
위 예제 코드를 실행시켜보면, 다음과 같이 로그가 찍힌다.
'userA'가 반환되기 전에 b로직이 시작되고 그만큼 sleep을 충분히 주지 않았기 때문에 동시성 문제가 발생하게 된다.
[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 변수를 쓰레드로컬을 사용해서 각 쓰레드에서 별도로 사용하도록 만들어버리면, 동시성 문제를 걱정하지 않아도 된다.
class ThreadLocalFieldService(
private val nameStore: ThreadLocal<String> = ThreadLocal<String>()
) {
fun logic(name: String): String? {
log.info("저장 name={} -> nameStore={}", name, nameStore)
nameStore.set(name)
sleep(1000)
log.info("조회 nameStore={}", nameStore)
return nameStore.get()
}
private fun sleep(millis: Long) {
try {
Thread.sleep(millis)
} catch(e: InterruptedException) {
e.printStackTrace()
}
}
companion object : Log
}
결과
[Test worker] main start
[Thread-A] 저장 name=userA -> nameStore=null
[Thread-B] 저장 name=userB -> nameStore=null
[Thread-A] 조회 nameStore=userA
[Thread-B] 조회 nameStore=userB
[Test worker] main exit
Type
>()value
)