일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- WooWaCon
- 명령어 대체
- 제어역전
- hashicorp
- Approval Test
- json.tool
- JSR-330
- shanta #bahadur
- CQRS
- Unseal
- backtick
- external_url
- AWS SNS
- Shamir
- 하만카돈 #오라 #스튜디오 #2 #harman #kardon #aura #studio #fix #repair #수리 #shutdown #bluetooth
- Vault
- auth method
- 샤미르
- InheritableThreadLocal
- MSA
- RequestFacade
- gitlab-ctl
- approle
- 멱등성
- KMS
- gitlab.rb
- AWS SQS
- secretid
- 우아콘
- Session invalidate
- Today
- Total
인생은 여행
다른 사용자를 죽이다; ThreadLocal 본문
황당한 오류 사례가 보고되었다. 한 사용자가 인트라넷에 접속해서 특정 업무를 수행하면 옆에 있는 다른 사용자가 로그아웃되어버린다는 것이다. 에이 설마.. 하며 개발자가 담당자와 같이 확인해보니 똑 같이 재현된다는 것이다. 제목에 스포되어 있어서 독자는 ThreadLocal이 원인이라는 것을 알아차렸겠지만, 분석 단계에서는 단서가 많지 않아 원인을 찾기가 아주 힘들었다.
단서가 될만한 사실은, 해당 업무가 웹화면을 통해 수행되기는 하지만 배치성 작업이라 비동기로 수행이 되며 수행 결과는 웹소켓을 이용하여 푸시한다는 것이다.
합리적인 의심; 다른 업무에서는 발생하지 않고 유독 이 작업에서만 현상이 발생하는건 혹시...
1번 용의자 웹소켓!
푸시를 위해 특정 솔루션을 사용하고 있는데 푸시 서버가 별도로 있어 WAS에서 웹소켓으로 푸시를 의뢰하는 구조이다. 웹소켓을 사용해본 경험이 없어서 정확한 동작원리를 몰랐던 탓에 웹소켓의 세션이 꼬이면서 웹 세션까지 영향을 미친건 아닌지 의심을 했었지만, 푸시 서비스를 내리고 테스트를 해 봐도 현상이 재현되었다. 1번 용의자 알리바이 성립.
새로운 사실이 확인 됐다. 희생자의 세션이 끊기는 과정을 확인하기 위해 로그를 추가하였는데, 세션에서 사용자ID를 가져와서 비교하는 코드에서 엉뚱한 ID가 확인된 것이다. 그 ID는 바로 작업을 돌린 사용자의 것. 여기에 서핑을 하다 읽은 'Spring Security는 ThreadLocal에 정보를 보관한다'는 정보가 겹치면서 두뇌가 어지럽게 돌아가기 시작했다. 이것은 치정에 얽힌 세션 살인 사건!!!
사건의 개요는 이랬다.
ThreadLocal & InheritableThreadLocal
하나의 요청(request)을 처리하는 과정에서 method 호출 흐름과는 상관 없이 어떤 데이터를 공유하기 위해서 ThreadLocal을 사용한다. 해당 서비스에서도 근간(Framework; 이하 fw)에 그런 기능이 지원되고 있었다. 그런데 ThreadLocal은 동일 쓰레드 내에서만 정보가 공유될 수 있기 때문에 멀티쓰레드 환경에서는 InheritableThreadLocal을 사용한다. 쓰레드가 생성될 때마다 InheritableThreadLocal에 있는 정보를 복제해주기 때문이다. fw에서도 InheritableThreadLocal을 사용하고 있는데 저장되는 값 중에 하나가 HttpServletRequest였다.
HttpServletRequest & RequestFacade
그런데 Spring에서 전해주는 HttpServletRequest를 toString 해 보면 RequestFacade로 감싸서(wrapping) 넘겨주고 있다는 것을 알 수 있다. 그리고 실제 구현 객체는 요청이 종료되면서 사라지는 것으로 보인다. 그렇다면 InheritableThreadLocal을 통해 HttpServletRequest을 물려 받은 자식 쓰레드들은 요청이 종료되면 알맹이는 사라지는 빈껍데기 RequestFacade만 들고 있게 된다. 그래서 request.getSession() 해봐야 null 만 떨어질 뿐이다. 아 사기사건인가? 그리고 실제 자식 쓰레드를 사용하는 코드에서는 request.getSession() 해서 사용자 정보를 구하고 있었다.
그런데 사건의 본질은 살인사건이지 사기사건이 아니지 않은가? 뭔가 앞뒤가 맞지 않는다. 그냥 NullPointerException 떨어지고 말아야지 왜 엉뚱한 세션이 죽는다는 말인가? (정확하게는 다른 집에서 용의자의 속옷이 발견된 것이다. 치정 맞잖아)
이 부분은 좀 더 들여다봐야겠지만 단서는 있다. 우리는 항상 쓰레드를 새로 만들어 쓰는게 아니라는 것이다. 효율을 위해 ThreadPool을 사용하고 있고 WAS 자체도 TheadPool을 이용하여 각 요청마다 쓰레드를 할당하고 있다. 쓰레드를 재사용한다면 쓰레드가 생성될 때 상속 받은 InheritableThreadLocal의 값들은 어떻게 되는 것인가? 입양한 자식 쓰레드에게도 유산을 물려줄 수 있는건가? 단지 사용자 조회를 했을 뿐인데 다른 사용자 세션이 꼬여버리는 것도 여전히 의문이다.
좀 더 들여다 봐야겠다.
유력한 용의자는 있지만 아직 미제 사건이다. 몇 가지 남은 퍼즐이 맞춰지지 않았다.
To be continued...
'Troubleshooting' 카테고리의 다른 글
Docker 이미지 오래된 버전 정리 스크립트 (0) | 2022.01.28 |
---|---|
git 'remote-http' is not a git command 해결 (0) | 2021.01.06 |