Thread Local

bp.chys·2021년 12월 17일
0

TIL(Today I Learned)

목록 보기
6/11

인프런 스프링 고급편 강의를 듣던 중, 동시성 처리의 방법 중 하나로 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

쓰레드 로컬 사용법

  • 정의: ThreadLocal<Type>()
  • 값 저장: ThreadLocal.set(value)
  • 값 조회: ThreadLocal.get()
  • 값 제거: ThreadLocal.remove()

주의할점: remove

  • WAS처럼 쓰레드 풀을 사용하는 경우, 쓰레드 로컬을 사용하면 심각한 문제가 발생할 수 있다.
  • 쓰레드 풀에서는 사용이 끝난 쓰레드를 풀에 다시 넣는데 이때 쓰레드는 계속 살아있게 된다.
  • 다음 요청에서 해당 쓰레드를 사용할 때 만약 쓰레드 로컬이 초기화되지 않은 상태라면 이전 요청에서 사용했던 값을 그대로 사용하게 된다.
  • 따라서 쓰레드 로컬 사용을 완료했다면, remove를 통해 값을 제거해주는 작업이 꼭 필요하다.
profile
하루에 한걸음씩, 꾸준히

0개의 댓글