스프링 핵심 원리 고급편 (김영한 강의) - 섹션 2 : 쓰레드 로컬(ThreadLocal)
섹션 목적 : 쓰레드 로컬에 대해 알아보자
1. 필드 동기화 - 개발
(1) 앞서 로그 추적기를 만들때, id와 level을 동기화해야하는 문제가 있었다. 그래서 이 문제를 해결하기 위해 TraceId를 파라미터로 넘기는 방식을 선택
(2) 동기화에는 성공했지만, 모든 메서드에 TraceId를 파라미터로 추가해야하는 문제가 있었다
(3) TraceId를 파라미터로 넘기지 않고 해결할 수 없을까?
(4) 먼저, 파라미터를 넘기지 않는 LogTrace 인터페이스를 만들고 이를 기반으로 개발해보자
(5) 이제, 필드로 TraceId를 동기화할 수 있는 FieldLogTrace 구현체를 만들자
(6) FieldLogTrace 특징
① TraceId를 동기화히기 위해 TraceId를 필드로 갖고 있음. 이제 직전 로그의 TraceId는 TraceHolder에 저장됨
② syncTraceId() : 최초 호출인 경우 TraceId를 새로 생성하고, 직전 로그가 있으면 id를 동기화하고 level을 1 증가시킴
③ releaseTraceId() : level이 0일 경우 null로 바꾸고, 아닐 경우 id를 동기화하고 level을 1 감소시킴
(7) 테스트 -> 정상 동작함
(8) 이제 불필요하게 TraceId를 파라미터로 전달하지 않아도 되고, 애플리케이션의 메서드 파라미터도 변경하지 않아도 됨
2. 필드 동기화 - 적용
(1) 애플리케이션에서 문제없이 동작
(2) 이제 실제 서비스에 배포한다고 가정해보자
3. 필드 동기화 - 동시성 문제
(1) FieldLogTrace는 심각한 동시성 문제를 가지고 있다. 이 문제를 확인하기 위해 1초에 2번 실행해보자
(2) 동시에 여러 사용자가 요청하면 여러 쓰레드가 동시에 애플리케이션 로직을 호출하게 된다
(3) 그런데 이 때, 싱글턴으로 등록된 FieldLogTrace의 TraceHolder 필드를 여러 쓰레드가 동시에 접근하기 때문에 로그가 섞이는 문제가 발생하는 것임
4. 동시성 문제 - 예제 코드
(1) 결국 동시성 문제는 여러 쓰레드가 동시에 '같은 인스턴스'의 필드를 변경하면서 발생하는 문제임
(2) 특히 스프링 빈처럼 싱글턴 객체의 필드를 변경하며 사용할 때, 조심해야 함
(3) 동시성 문제는 지역 변수에서는 발생하지 않음. 싱글턴 인스턴스의 필드 또는 static 같은 공용 필드일 때 발생함
(4) 동시성 문제는 값을 읽기만 할 때는 발생하지 않음. 변경 때문에 발생함
(5) 싱글턴 객체의 필드를 사용하면서 동시성 문제를 어떻게 해결할까?
(6) 이 때 사용하는 것이 'ThreadLocal' 임
5. ThreadLocal - 소개
(1) 쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소임
(2) 물건보관창구와 비슷한 개념임. 여러 사람이 같은 종류의 물건을 창구에 보관하더라도, 창구 직원이 사용자를 인식해서 사용자별로 물건을 보관함
(3) 그래서, 나중에 물건을 꺼낼 때도 창구 직원이 사용자에 따라 물건을 구분해서 주는 것임
6. ThreadLocal - 예제 코드
(1) ThreadLocal<String> nameStore = new ThreadLocal<>(); 로 사용할 수 있음
(2) nameStore.set(xxx), nameStore.get(), nameStore.remove() 메소드를 사용할 수 있음
(3) 쓰레드 로컬을 사용함으로써 쓰레드마다 별도의 데이터 저장소를 갖게되어서 동시성 문제가 발생하지 않음
7. 쓰레드 로컬 동기화 - 개발 및 적용
(1) ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>(); 로 바꿈
(2) 이렇게 해서 쓰레드 마다 다른 TraceId를 가짐으로써 동시성 문제를 해결할 수 있음
8. 쓰레드 로컬 - 주의사항
(1) 쓰레드 로컬의 값은 사용 후 제거하지 않으면 WAS(톰캣)처럼 쓰레드 풀을 사용하는 경우 문제가 발생할 수 있음
(2) 쓰레드 1,2,3이 쓰레드 풀에 있고 쓰레드 로컬의 데이터를 삭제하지 않았다고 가정하자
(3) 사용자 A에 쓰레드 1이 할당되고 반납되고, 사용자 B에 다시 쓰레드 1이 할당되면, 사용자 B는 사용자 A의 데이터가 조회되는 문제가 발생할 수 있다.
(4) 그러므로 사용자 A의 요청이 끝나면 ThreadLocal.remove()를 통해서 꼭 값을 제거해야한다
이펙티브 자바 - 아이템 9 : try-finally보다는 try-with-resources를 사용하라
- 자바 라이브러리에는 close메서드를 호출해 직접 닫아줘야하는 자원이 많다(ex. InputStream, OutputStream ...)
- 자원 닫기는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어지기도 함
- 전통적으로 자원이 제대로 닫힘을 보장하는 수단으로 try-finally가 쓰였다
- try-finally문의 문제점
① 자원을 두 개이상 사용할 때 코드가 지저분해짐
② try문과 finally문에서 모두 예외가 발생할 수 있는데, 이 때 finally에서 발생한 예외가 이전 예외를 먹어버려서 예외 추적을 어렵게 함
- 따라서, try-with-resources문을 사용하자
- 이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 함(close 메소드룰 구현해야하기 때문)
- 이 구조를 사용하면
① 자원을 여러개 사용하더라도 코드가 짧아서 읽기가 더 수월하다
② close에서 발생한 예외는 숨겨지고 그 앞에서 발생한 예외가 기록돼서, 문제를 진단하기도 훨씬 좋다(close에서 발생한 예외는 버려지지는 않고, 스택 추적 내역에 숨겨짐)
- 그러므로, 꼭 회수해야하는 자원을 다룰 때는 try-finally대신 try-with-resources를 사용하자
내일부터는 부트캠프 온보딩 강의와 미션도 시작되니깐 화이팅해보자!!!
'TIL(Today I Learned)' 카테고리의 다른 글
2023.07.06 (0) | 2023.07.06 |
---|---|
2023.07.05 (0) | 2023.07.05 |
2023.07.03 (0) | 2023.07.03 |
2023.06.30 (0) | 2023.06.30 |
2023.06.29 (0) | 2023.06.30 |