안녕하세요. 도서관 토이 프로젝트에서 logging을 하는 기능을 추가하려고 했습니다. 이 중에, client의 ip address와 Host name, 그리고 요청 id 등을 추가해야 했는데요. 이를 어떻게 처리해야 하나 찾던 도중에 logback의 MDC를 알게 되었습니다. 그리고, 이것이 어떤 식으로 동작하는지 간단하게 질문글들을 찾아보니, 내부적으로 ThreadLocal을 쓴다는 답변이 있었습니다.

 

 java의 ThreadLocal은 처음 들어봤는데요. 꽤 오래 전부터 있었던 듯 해서, 이번 기회에 간단하게 정리해 보았습니다.

 

 


 먼저, ThreadLocal에는 get, set, remove 이 정도밖에 없어요. 저 3개를 쓰는 간단한 예제 먼저 보고, 어떤 식으로 동작하는지 간단하게 내부만 보도록 하겠습니다.

 

 

  먼저 ThreadLocal 객체 하나를 생성해서 1로 set 합니다. 이것을 A라 하겠습니다. 다음에 ThreadLocal 객체 하나를 생성해서 2로 set 합니다. 이것을 B라 하겠습니다. ThreadLocal로부터 B 객체를 얻어옵니다. 다음에, B객체를 remove 한 다음에 Threadlocal로부터 B를 얻어옵니다.

 

 

 결과는 2와 null이 나옵니다. 여기까지만 보면 처음에는 B 객체를 Threadlocal로부터 얻었고, 두 번째에는 Threadlocal로부터 B 객체가 제거되었으니, null이 출력되었구나. 정도만 알 수 있습니다. 여기까지만 보고 얘네들이 뭘 하는지 감을 잡기란 여간 쉬운 일이 아닐 겁니다. 어떻게 동작하는지 클래스 내부를 보도록 하겠습니다.

 

 


 생성자로 들어가 보면 별 게 없습니다. 필드를 보겠습니다.

 

 

 보면, threadLocalHashCode는 그냥 final int로 선언되어 있고, nextHashCode가 static으로 선언이 되어 있어요. 이 말인 즉슨, nextHashCode는 클래스 공통으로 작용하는 속성이라는 의미이고요. threadLocalHashCode는 인스턴스별로 별개로 돌아가는 친구라는 겁니다.

 

 그러면 new ThreadLocal(); 이 호출될 때 마다, nextHashCode()가 불릴 건데요.

 

 

 이 메서드의 내부를 보면, 그냥 nextHashCode에 HASH_INCREMENT를 증가시킵니다. threadLocalHashCode는 애지간하면 다르다. 이렇게 이해하셔도 무방합니다. 그런데, nextHashCode가 AtomicInteger인데요. 원자적인 이라는 말이 들어가 있으니 쓰레드 안전하다. 뭐 이런 이야기로 들려요?

 

 왜 이렇게 해 놓았는가? 여러 쓰레드들이 new ThreadLocal()을 호출한다 해 봅시다. 그러면 인스턴스 변수는 다른 공간을 보겠지만, 클래스 변수는 이들이 같이 볼 수 있어요.

 

 

 그래서 요래 해 놓은 것이 아닐까 싶어요. 그러면, 이 세 개의 hashCode와 관련 있을법한 친구들이 어디에 이용되는지 볼 필요가 있겠네요.

 

 


 이제 set 내부만 보겠습니다. 사실 이 친구만 분석하면 나머지는 어떻게 돌아가는지 유추가 가능해서 그렇습니다.

 

 

 끊어서 보겠습니다. 먼저, 현재 Thread 객체를 얻어오는 작업을 200번째 줄에서 합니다. 다음에, 201번째 줄에서 getMap 메서드를 호출하는데요. 이 메서드 내부로 들어가 봅시다.

 

 

 보면, 단순히 t의 threadLocals를 얻어옴을 볼 수 있는데요.

 

 

 Thread 내부에 있는 인스턴스라고 생각하면 됩니다. 여기까지의 상황을 그림으로 다시 정리해 봅시다.

 

 

각 thread마다 관리하고 있는 속성들이 있는데요. 이 중에 threadLocals라는 녀석이 있어요. getMap은 단지, thread 안에서 별도로 관리되고 있는 threadLocals를 얻어온 것 뿐입니다. 여기에서 중요한 것은 Thread 마다 별도로 관리되는 속성이라는 점입니다. 이제 ?의 정체를 봐야 하는데요. map.set 부분을 봅시다.

 

 

 내부를 보시면, Entry 배열이 보이는데요. 테이블로 관리하는가? 라는 의문이 듭니다. 463번째 줄에서 key.threadLocalHashCode와 len-1을 and 연산 하는데, 이 부분은 나머지 연산 처리하는 것으로 보여요. 보통 버킷 갯수가 2^k꼴일 때 and 연산으로 나머지 연산을 할 수 있거든요.

 

 

 다음에 이 부분을 보면, hash값을 토대로 찾은 index부터 쭉 돌리는데요. 이 nextIndex는 1씩 증가하게 됩니다. 일단 tab[i]가 null이면 그리 어려운 건 없습니다. 문제는, tab[i]가 null이 아닌 경우인데요. e.get을 했을 때 k가 key인 경우는 그냥 대체시켜주면 끝나는 문제입니다. 결국 같은 키가 있었기 때문입니다.

 

 문제는 k가 null인 경우인데요. 저건 또 뭘 의미하는가? 일단 get에 마우스를 올려서 정보를 봅시다.

 

 

 e가 참조하고 있던 객체가 cleared가 되었을 때 null을 리턴합니다. 아 잠깐. 이게 무슨 이야기인지 잘 모르겠으니, Entry 클래스를 보는 게 낫겠군요.

 


 보면 WeakReference를 상속받는데요. 이건 또 뭘까요?

 

 

 공식 문서를 읽어보면 대강 유추해 볼 수는 있습니다. gc가 판단을 내릴 때, weakly reachable까지 clear 할 수 있다. 어? 그러면 이건 무엇을 의미할까요? weakly reference로만 연결되어 있는 객체인 경우, gc의 판단에 따라 clear가 될 수도 있다는 의미입니다. 이게 무슨 이야기인가? 테스트 코드를 하나 만들어 보겠습니다.

 

 

 400만개의 ThreadLocal 객체를 새로 생성한 다음에 값을 set 합니다. 다음에, 또 ThreadLocal 객체를 생성한 다음에, -1로 set 하겠습니다. 그러면, 최소한 threadLocal에서 관리하는 맵의 size는 400만 1개보다는 크거나 같아야 할 겁니다. 그런데, 디버깅 모드로 강제로 gc를 돌린 이후에 상황을 봅시다.

 

 

  그러면 뜬금없이 3459540개로 400만 1보다는 작은데요. 심지어, 이 부분은 gc를 강제로 호출하기도 전입니다.

 

 

 이는 중간에 gc가 일어난 후에, e가 null이 아닌데, e.get()이 null인 상황이 벌어졌기 때문입니다.

 

 

 set 하는 메서드를 잘 보면, 끝 부분에 cleanSomeSlots가 있는데요. 추가하는 과정에 왜 저런 게 있는지 의문이 들 겁니다. 사실 이것도 Weak reference와 제거하는 알고리즘과 관련이 깊습니다. cleanSomeSlots 메서드의 내부를 보면, e가 null이 아니지만, e.get이 null인 조건문이 있어요. 이는 어떤 경우를 의미하는가?

 

 

  Entry 클래스를 토대로 그려 보면, 실제 Value로 들어가는 객체는 요렇게 그려질 겁니다. 간단하게 요약하면 value obj는 약한 참조로 연결되고, 문제의 프로그램을 봐도, 사실상 value obj에 접근하는 방법이 요 방법밖에는 없어요. 여기서 점선은 약한 참조를 의미해요. Integer의 경우에는 작은 수에 대해서는 caching 해서 저정하겠지만 그렇지 않으면 방법이 사실상 점선을 거쳐가는 것밖에 없습니다. 그래서, GC가 저 value obj를 필요에 따라 클리어 했다면, 656번째 줄이 true가 되어버리기 때문에, 아래 작업을 수행하게 됩니다.

 

 

 즉, Entry로 연결되는 참조를 제거합니다. 그러면 Entry 객체도 가비지가 되어서, 언젠가는 수거될 겁니다.

 

 

 ThreadLocalMap에 객체를 set 하는 부분 맨 마지막에 cleanSomeSlots가 있는 이유이기도 합니다. 이 글에서는 쓰레드별로 관리하는 ThreadLocalMap의 세세한 내부 구조에 대해서 매우 깊게 파헤치지는 않을 겁니다. 해당 글은 나중에 레퍼런스 분석에 쓸 예정이고요. 여기에서는 단지, 아 이게 뭐를 하는 클래스네. 정도만 보시면 됩니다.

 

 나중에 이 부분은 Reference 부분을 보시면 도움이 많이 되실 듯 합니다.

 

 


 어찌 되었던 set 메서드를 간단하게 보았는데요. 이를 토대로 정리를 해 봅시다.

 

 

 일단 ThreadLocals는 쓰레드마다 관리되는 Context? 혹은 속성값 정도를 저장할 때 유용하게 쓰일 수 있어요. set을 호출하면 내부적으로 관리되는 table에 저장이 되는데요. 이 value를 저장할 때 ThreadLocal의 hash 값을 가지고 탐색할 첫 위치를 찾게 됩니다. 다음에 ThreadLocal 객체 value2의 hash값을 가지고, 탐색할 첫 위치가 0번째였다고 해 봅시다.

 

 

  보니까 이미 있긴 한데요. 문제는, Entry가 Weak reference이기 때문에, 한 가지 작업이 더 필요하다고 했어요. 실제로, entry가 가리키는 value가 null이지 않은지 체크하는 것은 gc라는 변수 때문에 그렇습니다.

 

 그런데 비어 있지 않네요? 따라서 1번째 위치가 비어있는지 확인하는데, 1번째 위치는 비어 있어요.

 

 

 그래서 1번째 위치에 넣어버리게 됩니다. get도 비슷한 알고리즘으로 키를 찾아낼 거고요. 1칸씩 찾는 특성 때문에, 최악의 경우에는 버킷 갯수만큼 탐색하게 될 텐데요. 이런 걸 방지하기 위해서, 중간에 버킷 갯수를 줄이기도 합니다. 이 부분은 상세 분석에서 볼 기회가 있을 듯 하니 그 때 보는 걸로 하겠습니다. 여기에 언급하면 글이 3배 이상 길어질 듯 하니 이 쯤에서 끊는 걸로 하겠습니다.