일반적으로, 변수(객체)의 종류는 크게 지역변수
와 전역변수
2가지로 나눌 수 있다.
지역변수는 thread마다 할당되는 stack 메모리 영역에 저장되며,
전역변수는 모든 thread가 공유하는 heap 메모리 공간에 저장된다.
아래 예제 코드를 확인해보자.
class VariableTest {
companion object {
var staticValue = 0 // 전역변수, 자바에서 static 과 같다.
}
@Test
fun localValueTest() {
var localValue = 0 // 지역변수
}
}
지역변수는 하나의 메소드, if문, for문 등 짧은 범위 내에서의 생명주기를 갖는다.
단일 스레드에서는 상관 없지만, 멀티 스레드 환경에서 전역변수를 사용할 때에는 주의해야 한다.
class VariableTest {
companion object {
var staticValue = 0 // Java 의 static 변수, 전역변수
}
@Test
fun singleThreadTest() {
object : Thread() { // 새로운 스레드를 익명으로 생성함
var localValue = 0
override fun run() {
repeat(10000) {
localValue += 1
staticValue += 1
}
Assertions.assertEquals(10000, localValue)
}
}.start()
Thread.sleep(100)
Assertions.assertEquals(10000, staticValue)
}
}
Process finished with exit code 0
class VariableTest {
companion object {
var staticValue = 0 // Java 의 static 변수, 전역변수
}
@Test
fun multiThreadTest() {
object : Thread() { // 새로운 스레드를 익명으로 생성함
var localValue = 0
override fun run() {
repeat(10000) {
localValue += 1
staticValue += 1
}
Assertions.assertEquals(10000, localValue)
}
}.start()
object : Thread() { // 새로운 스레드를 익명으로 생성함
var localValue = 0
override fun run() {
repeat(10000) {
localValue += 1
staticValue += 1
}
Assertions.assertEquals(10000, localValue)
}
}.start()
Thread.sleep(100)
Assertions.assertEquals(20000, staticValue)
}
}
org.opentest4j.AssertionFailedError:
Expected :10000
Actual :16336
1을 10,000번 더하는 스레드 2개가 있기 때문에 20,000을 기대했지만
실제로는 그보다 작은 값이 나왔다.
Actual 값은 매 실행마다 달라지는 것을 확인할 수 있다.
class VariableTest {
companion object {
var userName = ""
}
fun setUserName(name: String){
userName = name
}
fun getUserName(): String {
return userName
}
fun logic(){
Thread.sleep((20..50).random().toLong())
}
@Test
fun realTest() {
val request1 = object: Thread() { // 새로운 스레드를 익명으로 생성함
override fun run() {
setUserName("김씨")
logic()
Assertions.assertEquals("김씨", getUserName())
}
}
val request2 = object : Thread() { // 새로운 스레드를 익명으로 생성함
override fun run() {
setUserName("박씨")
logic()
Assertions.assertEquals("박씨", getUserName())
}
}
request1.start()
request2.start()
Thread.sleep(100)
}
}
2개의 요청이 있다.
각 요청의 플로우는, 이름 설정 -> (로직, 20~50ms 소요됨) -> 이름 불러오기
로 이루어진다.
이를 실행해보면 잘 될때도 있지만, 오류가 발생할 때도 있다.
Exception in thread "Thread-0" org.opentest4j.AssertionFailedError: expected: <김씨> but was: <박씨>
...
이처럼 멀티 스레드에서 공유자원을 할당하고 불러오는 것은 잘못된 결과를 초래할 수 있다.
Thread
Local은 이름에서도 볼 수 있듯, 쓰레드와 관련된 기능이다.
java 1.2 부터 지원하며, java.lang 패키지에 위치한다.
ThreadLocal은 각 스레드마다 저장공간을 할당해준다.
ThreadLocal을 전역변수로 할당하고, 각 스레드에서는 편하게 값을 읽고 쓰면 된다.
ThreadLocal을 사용하기 위해서는 생성 방식과, 3가지 메소드만 기억하면 된다.
val threadLocal = ThreadLocal<String>()
public class ThreadLocal<T> {
public void set(T value) {...}
public T get() {...}
public void remove() {...}
...
}
class VariableTest {
companion object {
var userName = ThreadLocal<String>()
}
fun setUserName(name: String){
userName.set(name)
}
fun getUserName(): String {
return userName.get()
}
fun release() {
userName.remove()
}
fun logic(){
Thread.sleep((20..50).random().toLong())
}
@Test
fun realTestWithThreadLocal() {
val request1 = object: Thread() { // 새로운 스레드를 익명으로 생성함
override fun run() {
setUserName("김씨")
logic()
Assertions.assertEquals("김씨", getUserName())
release() // ThreadLocal 사용 후에는 반드시 remove
}
}
val request2 = object : Thread() { // 새로운 스레드를 익명으로 생성함
override fun run() {
setUserName("박씨")
logic()
Assertions.assertEquals("박씨", getUserName())
release() // ThreadLocal 사용 후에는 반드시 remove
}
}
request1.start()
request2.start()
Thread.sleep(100)
}
}
이제 멀티스레드 환경에서도 각 스레드마다 값을 관리할 수 있다.
ThreadLocal 후에는 반드시 release 해주는 것을 잊지 말자.
(뒤에서 발생할 수 있는 이슈를 설명)
ThreadLocal은 내부적으로 데이터를 ThreadLocalMap 으로 관리하는데,
ThreadLocalMap의 key는 Thread 정보이다.
Spring-MVC의 내장 톰캣같이 Thread를 재활용하는 경우, 이전에 사용하던 데이터가 남아있을 수 있다. 따라서 반드시 ThreadLocal을 사용한 뒤에는 remove()를 호출해주자.
Webflux는 내부 WAS를 Tomcat이 아닌 Netty를 사용한다.
Tomcat은 request마다 thread를 할당하여 사용하는 반면
Netty는 NIOEventLoop 방식을 사용하기 때문에, 1개의 request 안에서 thread가 바뀔 수 있다.
그래서 ThreadLocal의 값을 보장할 수 없고, 다른 request에서 값을 조회해갈 수도 있다.
-> Webflux를 사용한다면 ThreadLocal 을 사용하면 안된다.