[Spring][Log] 로깅
로깅 라이브러리
스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리( spring-boot-starter-logging
)가 함께 포함된다. 스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.
- SLF4J, Logback
- 로그 라이브러리는 Logback, Log4J, Log4J2 등등 수 많은 라이브러리가 있는데, 그것을 통합해서 인터페이스로 제공하는 것이 바로 SLF4J 라이브러리다. 쉽게 이야기해서 SLF4J는 인터페이스이고, 그 구현체로 Logback 같은 로그 라이브러리를 선택하면 된다. 실무에서는 스프링 부트가 기본으로 제공하는 Logback을 대부분 사용한다.
로그 선언
private Logger log = LoggerFactory.getLogger(getClass());
@Slf4j
: 롬복 사용 가능
//@Slf4j
@RestController
public class LogTestController {
private final Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/log-test")
public String logTest() {
String name = "Spring";
log.trace("trace log={}", name);
log.debug("debug log={}", name);
log.info(" info log={}", name);
log.warn(" warn log={}", name);
log.error("error log={}", name);
//로그를 사용하지 않아도 a+b 계산 로직이 먼저 실행됨, 이런 방식으로 사용하면 X
log.debug("String concat log=" + name);
return "ok";
}
}
- LEVEL: TRACE > DEBUG > INFO > WARN > ERROR
- 개발 서버는 debug 출력
- 운영 서버는 info 출력
로그 레벨 설정
#전체 로그 레벨 설정(기본 info)
logging.level.root=info
#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug
올바른 로그 사용법
log.debug("data="+data)
: 로그 출력 레벨을 info로 설정해도 해당 코드에 있는 “data=”+data가 실제 실행이 되어 버린다. 결과적으로 문자 더하기 연산이 발생한다.log.debug("data={}", data)
: 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않는다. 따라서 앞과 같은 의미없는 연산이 발생하지 않는다.
쓰레드 로컬 - ThreadLocal
필드 동기화 - 개발
TraceId
를 파라미터로 넘기지 않고 동기화를 할 수 없을까?
@Slf4j
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 traceIdHolder;
@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();
}
}
private void complete(TraceStatus status, Exception e) {
...
releaseTraceId();
}
private void releaseTraceId() {
if (traceIdHolder.isFirstLevel()) {
traceIdHolder = null;
} else {
traceIdHolder = traceIdHolder.createPreviousId();
}
}
}
@Configuration
public class LogTraceConfig {
@Bean
public LogTrace logTrace() {
return new FieldLogTrace();
}
}
TraceId
를 동기화 하는 부분만 파라미터를 사용하는 것에서TraceId traceIdHolder
필드를 사용하도록 변경되었다.- 이제 직전 로그의
TraceId
는 파라미터로 전달되는 것이 아니라FieldLogTrace
의 필드인traceIdHolder
에 저장된다.
필드 동기화 - 동시성 문제
FieldLogTrace
는 싱글톤으로 등록된 스프링 빈이다. 이 객체의 인스턴스가 애플리케이션에 딱 1 존재한다는 뜻이다. 이렇게 하나만 있는 인스턴스의FieldLogTrace.traceIdHolder
필드를 여러 쓰레드가 동시에 접근하기 때문에 문제가 발생한다.- 결과적으로
Thread-A
입장에서는 저장한 데이터와 조회한 데이터가 다른 문제가 발생한다. 이처럼 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라 한다. 이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타나지 않고, 트래픽이 점점 많아질 수 록 자주 발생한다. 특히 스프링 빈 처럼 싱글톤 객체의 필드를 변경하며 사용할 때 이러한 동시성 문제를 조심해야 한다. - 동시성 문제가 발생하는 곳은 같은 인스턴스의 필드(주로 싱글톤에서 자주 발생), 또는 static 같은 공용 필드에 접근할 때 발생한다. 동시성 문제는 값을 읽기만 하면 발생하지 않는다. 어디선가 값을 변경하기 때문에 발생한다.
ThreadLocal - 소개
- 쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다. 쉽게 이야기해서 물건 보관 창구를 떠올리면 된다. 쓰레드 단위로 map를 이용하여 필드값을 저장한다.
- 자바는 언어차원에서 쓰레드 로컬을 지원하기 위한
java.lang.ThreadLocal
클래스를 제공한다. - ThreadLocal 사용법
- 값 저장:
ThreadLocal.set(xxx)
- 값 조회:
ThreadLocal.get()
- 값 제거:
ThreadLocal.remove()
- 값 저장:
쓰레드 로컬 동기화 - 개발
FieldLogTrace
에서 발생했던 동시성 문제를 ThreadLocal
로 해결해보자. TraceId traceIdHolder
필드를 쓰레드 로컬을 사용하도록 ThreadLocal<TraceId> traceIdHolder
로 변경하면 된다.
@Slf4j
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 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);
}
private void syncTraceId() {
TraceId traceId = traceIdHolder.get();
if (traceId == null) {
traceIdHolder.set(new TraceId());
} else {
traceIdHolder.set(traceId.createNextId());
}
}
private void complete(TraceStatus status, Exception e) {
...
releaseTraceId();
}
private void releaseTraceId() {
TraceId traceId = traceIdHolder.get();
if (traceId.isFirstLevel()) {
traceIdHolder.remove();
} else {
traceIdHolder.set(traceId.createPreviousId());
}
}
}
쓰레드 로컬 동기화 - 적용
@Configuration
public class LogTraceConfig {
@Bean
public LogTrace logTrace() {
//return new FieldLogTrace();
return new ThreadLocalLogTrace();
}
}
쓰레드 로컬 - 주의사항
- WAS는 HTTP 요청이 들어오면 쓰레드풀에서 쓰레드를 할당한다. 만약 사용자 A의 HTTP요청이 끝나고 나갈때 쓰레드 로컬에서 해당 쓰레드에 대응되는 value(TraceId)값을 삭제하지 않는다면 사용자 A에 대한 데이터는 그대로 남게된다. 이후 사용자 B의 HTTP요청에 대해 같은 쓰레트가 할당된다면 사용자 B는 쓰레드 로컬에서 사용자 A에 대한 데이터를 조회할 수 있게 된다. 이는 매우 심각한 문제이다.
- 이런 문제를 예방하려면 사용자A의 요청이 끝날 때 쓰레드 로컬의 값을
ThreadLocal.remove()
를 통해서 꼭 제거해야 한다. 쓰레드 로컬을 사용할 때는 이 부분을 꼭! 기억하자.
댓글남기기