1. 문제 상황
- 예제와 요구사항
본 예제는 일반적인 스프링 애플리케이션 상황을 기반으로 구성되며 아래와 같은 요구사항이 추가되어 개발 중인 상황이다.
- 모든 public 메서드의 호출과 응답 정보를 로그로 출력
- 애플리케이션의 흐름을 변경하면 안된다.
- 로그를 남긴다고 해서 비지니스 로직의 동작에 영향을 주면 안된다.
- 애플리케이션의 흐름을 변경하면 안된다.
- 메서드 호출에 걸린 시간 출력
- 정상 흐름과 예외 흐름을 구분
- 예외 발생 시 예외 정보가 남아야 한다.
- HTTP 요청을 구분
- 트랜잭션 ID를 생성해 하나의 HTTP 요청을 구분해야 한다.
- 현재 상황
현재 위 요구사항을 만족하는 가장 Low한 버전의 로그 추적기가 완성된 상태이다.
HTTP 요청의 트랜잭션 ID를 `Controller` 부터 `Repository`까지 동기화 시키고 호출의 depth를 동기화하기 위해 `LogTrace` 내부에 공유 필드를 가지고 있다. LogTrace의 구체 클래스를 스프링 컨테이너를 통해 싱글톤 빈으로 등록해 어떤 계층에서 호출하여도 동기화된 공유 필드를 요청하고 수정할 수 있도록 만들어져 있다.
public class FieldLogTrace implements LogTrace {
private static final String START_PREFIX = "-->";
private static final String COMPLETE_PREFIX = "<--";
private static final String EX_PREFIX = "<X-";
private TraceId tranceIdHolder; // 공유 필드
@Override
public TraceStatus begin(String message) {
syncTraceId();
TraceId traceId = tranceIdHolder;
Long startTimeMs = System.currentTimeMillis();
log.info("[{}] {}{}", traceId.getId(), addSpace(START_PREFIX, traceId.getLevel()), message);
return new TraceStatus(traceId, startTimeMs, message);
}
.
.
.
}
- 문제 상황
예상되는 문제이지만, 현재 상태는 *동시성 이슈*가 발생한다. 싱글톤 빈으로 등록된 `LogTrace`를 모든 계층에서 접근해 *상태를 공유*하기 때문이다.
실제 Service에서는 Thread.sleep(1000)으로 1초 간 대기하는 흐름을 갖는다. 정상 흐름일 경우 동시 요청에서 뒤늦게 호출한 요청은 잘못된 호출 depth와 트랜잭션 ID를 갖게 된다.
따라서 로그를 확인해보면 기대하는 결과와 전혀 다른 로그를 확인하게 된다.
2. ThreadLocal
- 개념
이러한 필드 공유에서 동시성 이슈를 해결하는 대안으로 `ThreadLocal`이라는 개념이 존재한다.
쓰레드 호컬은 해당 쓰레드만이 접근할 수 있는 특별한 저장소를 의미한다. 쉽게 말해 물건 보관 창구를 떠올리면 된다.
여러 사람이 같은 물건 보관 창구를 사용하더라도, 창구 직원은 사용자를 인식해 사용자 별로 물건을 확실하게 격리해 보관하게 된다.
- 주의사항
쓰레드 로컬을 사용한 뒤 값을 제거하지 않고 그냥 두면 WAS(톰캣)처럼 쓰레드 풀을 사용하는 경우에 심각한 문제가 발생할 수 있다.
아래 예시를 살펴보자.
쓰레드 A를 통해서 사용자 A가 특정 정보를 저장하고 쓰레드 로컬의 값을 지우지 않고 요청을 마무리 지었다.
이후에 사용자 B가 정보를 조회하는 요청을 보내게 되는데, 이 때 공교롭게도 쓰레드 A를 통해 요청을 보내게 되었다.
이 경우 기존 쓰레드 로컬에 존재하는 기존 사용자 A의 정보가 조회되게 된다..
*사실 가능하면 공유 필드를 두지 않는 것 자체가 최선의 선택이지만*, 쓰레드 로컬을 사용해 필드 동기화를 해야한다면 꼭 사용 후 리소스를 정리해주어야한다.
3. 적용(해결)
public class ThreadLocalLogTrace implements LogTrace {
private static final String START_PREFIX = "-->";
private static final String COMPLETE_PREFIX = "<--";
private static final String EX_PREFIX = "<X-";
// private TraceId tranceIdHolder; // 동시성 이슈 발생
private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();
@Override
public TraceStatus begin(String message) {
syncTraceId();
TraceId traceId = traceIdHolder.get();
Long startTimeMs = System.currentTimeMillis();
log.info("[{}] {}{}", traceId.getId(), addSpace(START_PREFIX, traceId.getLevel()), message);
return new TraceStatus(traceId, startTimeMs, message);
}
.
.
.
}
`LogTrace`인터페이스의 새로운 구체 클래스인 `ThreadLocalLogTrace`를 생성했다.
단순히 ThreadLocal을 통해 `TraceId`객체에 접근할 수 있도록 설정해주었다.
동시성 이슈가 해결되었다.
출처 : 인프런, 김영한의 스프링 핵심 원리 - 고급편
'Spring' 카테고리의 다른 글
[Spring] 프록시 패턴과 데코레이터 패턴 (0) | 2024.03.04 |
---|---|
[Spring] 스프링의 템플릿 콜백 패턴 (0) | 2024.03.03 |
[Spring] 스프링 트랜잭션 전파(Propagation) (0) | 2024.02.28 |
[Spring] @Transactional의 다양한 기능과 주의사항 (1) | 2024.02.27 |
[Spring] 스프링의 RuntimeException 추상화 (1) | 2023.12.18 |