본문은 인프런의 [스프링 핵심 원리 - 고급편]를 수강하고 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
스프링 핵심 원리 - 고급편
1. 쓰레드 로컬
2. 템플릿 메서드 패턴과 콜백 패턴
3. 프록시 패턴과 데코레이터 패턴
4. 동적 프록시 기술
5. 스프링이 지원하는 프록시
6. 빈 후처리기
7. @Aspect AOP
8. 스프링 AOP 개념
9. 스프링 AOP 구현
10. 포인트컷
11. 실무 주의사항
본 챕터에서는 멤버 변수를 이용한 로그 추적기에서 동시성 문제가 발생하는 것을 살펴보고, 쓰레드 로컬을 이용함으로써 동시성 문제를 해결하는 것을 공부했다. 먼저 기본 뼈대코드를 간단하게 살펴보자.
TraceId
public class TraceId {
private String id; // 트랜잭션 ID
private int level; // 호출 레벨 (컨트롤러 - 서비스 - 레포지토리 순으로 레벨 증가)
// ...
}
LogTrace (로그 추적기 인터페이스)
public interface LogTrace {
TraceStatus begin(String message); // 로직 시작시 호출
public void end(TraceStatus status); // 로직 종료시 호출
public void exception(TraceStatus status, Exception e); // 예외처리
}
LogTraceConfig
@Configuration
public class LogTraceConfig {
@Bean
public LogTrace logTrace() {
return new FieldLogTrace();
}
}
- LogTraceConfig를 통해 로그 추적기는 싱글톤으로 동작한다
1. 멤버 변수(필드)를 사용한 로그 추적기
FieldLogTrace
@Slf4j
public class FieldLogTrace implements LogTrace {
private TraceId traceIdHolder; // traceId 동기화, 동시성 이슈 발생
@Override
public TraceStatus begin(String message) {
syncTraceId();
TraceId traceId = traceIdHolder;
Long startTimeMs = System.currentTimeMillis();
log.info("[{}] {}{}", traceId.getId(), addSpace(START_PREFIX, traceId.getLevel()), message);
return new TraceStatus(traceId, startTimeMs, message);
}
private void syncTraceId() {
if (traceIdHolder == null) {
traceIdHolder = new TraceId();
} else {
traceIdHolder = traceIdHolder.createNextId();
}
}
// ...
}
먼저 멤버 변수를 사용한 로그 추적기인 FieldLogTrace를 보자. 여기서 중요하게 봐야할 점은 traceIdHolder이다. syncTraceId() 메서드 내에선 traceHolder의 값을 읽고 쓴다. 쓰레드별로 독립적인 FieldLogTrace 인스턴스를 생성한다면 문제가 없겠지만, FieldLogTrace는 싱글톤으로 등록된 스프링 빈이다. 따라서 애플리케이션에 인스턴스가 단 한개만 존재하고, 각 쓰레드마다 FieldLogTrace에서 동시에 접근하므로 동시성 문제가 발생한다.
2. 동시성 문제 발생
동시성 문제란 여러 쓰레드가 하나의 자원(여기서는 같은 인스턴스의 필드 값)을 동시에 접근하여 사용할 때 발생하는 문제를 의미한다. 스프링 빈처럼 싱글톤 객체를 사용할 땐 인스턴스가 한 개이므로 해당 인스턴스의 필드를 변경하며 사용할 때 각별히 주의를 기울여야 한다.
참고로 이런 동시성 문제는 지역 변수에서는 발생하지 않는데, 지역 변수는 쓰레드마다 각기 다른 메모리 영역(스택)에 할당되기 때문이다. 각 쓰레드가 공유하는 힙 영역에 할당되는 인스턴스의 필드나 코드 영역에 할당되는 static 필드에 접근할 때 주로 발생한다.
3. 쓰레드 로컬을 활용한 로그 추적기
ThreadLocalLogTrace
@Slf4j
public class ThreadLocalLogTrace implements LogTrace {
private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>(); // traceId 동기화, 동시성 이슈 발생 X
@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);
}
private void syncTraceId() {
TraceId traceId = traceIdHolder.get(); // 쓰레드 로컬에서 값을 읽음
if (traceId == null) {
traceIdHolder.set(new TraceId()); // 쓰레드 로컬에 값을 씀
} else {
traceIdHolder.set(traceId.createNextId());
}
}
//...
}
ThreadLocalLogTrace는 기존의 FieldLogTrace와 달리 traceIdHolder를 ThreadLocal<TraceId> 타입으로 선언했다. 쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소이다. 이를 이용하면 위의 동시성 문제 없이 각 쓰레드 별로 자원에 접근할 수 있다. syncTraceId() 메서드를 보면 기존 FieldLogTrace와 달리 쓰레드 로컬에서 get(), set() 메서드를 사용해서 값을 읽고 쓰는 것을 확인할 수 있다.
쓰레드 로컬에 대해 좀 더 알아보자. 일반 변수의 수명은 메서드 블록나 for 블록 등의 범위 내로 한정된다.
// num1과 num2는 메서드 내에서만 유효
public void add(int num1, int num2) {
return num1 + num2;
}
// i는 for문 내에서만 유효
for(int i=0; i<numbers.length; i++) {
System.out.println(numbers[i]);
}
그러나 쓰레드을 이용하면 특정 쓰레드가 실행하는 모든 코드에서 쓰레드 로컬에 접근하여 해당 변수 값을 사용할 수 있게 된다. 즉, 변수의 라이클 사이클이 쓰레드와 같아지는 것이다(삭제하지 않는다는 가정하에). 아래는 쓰레드 로컬의 동작방식과 간단한 사용법이다.
// 쓰레드 로컬 변수 생성
ThreadLocal<Resource> threadLocal = new ThreadLocal<Resource>();
// 값 할당
threadLocal.set(newResource);
// 값 조회
Resource oldResource = threadLocal.get();
// 값 제거
threadLocal.remove();
4. 쓰레드 로컬을 사용할 때 주의점
일반적인 WAS에선 매 요청마다 쓰레드를 사용하는 것이 아니라 쓰레드 풀에 미리 쓰레드를 생성한 후 재사용한다. 따라서 매 요청이 끝나는 시점에 ThreadLocal.remove()를 호출하여 쓰레드 로컬의 값을 제거해야 한다. 그렇지 않으면 해당 쓰레드를 사용하는 다음 요청에서 잘못된 값을 참조하는 문제가 발생할 수 있기 때문이다. 쓰레드 로컬의 라이프 사이클은 쓰레드와 동일하다는 것을 항상 명심하자.
쓰레드 로컬에 대해 보다 자세한 내용은 여기를 참고하자.
5. 참고
'Spring > 스프링 핵심 원리 - 고급편' 카테고리의 다른 글
[스프링 핵심 원리 - 고급편] 빈 후처리기 (0) | 2023.03.06 |
---|---|
[스프링 핵심 원리 - 고급편] 스프링이 지원하는 프록시 (0) | 2023.03.06 |
[스프링 핵심 원리 - 고급편] 동적 프록시 기술 (0) | 2023.02.21 |
[스프링 핵심 원리 - 고급편] 프록시 패턴과 데코레이터 패턴 (0) | 2023.02.17 |
[스프링 핵심 원리 - 고급편] 템플릿 메서드 패턴과 콜백 패턴 (0) | 2023.02.09 |