개발의 시작은 Logger 설정부터!
Log란?
프로그램 개발이나 운영 시 발생하는 문제점을 추적하거나 운영 상태를 모니터링하기 위한 텍스트입니다.
System.out.println()
를 사용하여 로그를 확인할 수 있지만 이보다 로그를 기록하는 클래스를 만들어 사용하는 것이 더 나은 방법입니다. (특히 실무에서)
왜 그럴까요?
print()
메서드를 살펴보면 synchronized
로 동기화가 되어있는 것을 볼 수 있습니다. 이렇게 되면 기록을 남길 때마다 쓰레드 lock이 걸리기 때문에 엄청난 성능 저하를 불러일으키게 됩니다. 혹여나 개발 단계에서 이를 사용했다 하더라도 운영시에는 이를 모두 삭제해줘야 하며, 이를 방치하면 I/O 요청이 발생할 때마다 쓸데없는 리소스를 잡아먹게 됩니다.
이외에도 로그를 사용해야 하는 이유에는 상황별 로그 수준을 기록할 수 있다는 장점이 있습니다.
⚠️ 주의!
로깅을 잘못 사용할 경우, 작업이 방대하게 몰리거나 무의미한 로그만 쌓이는 불상사가 발생할 수 있습니다. 기존 로그 쓰기 작업으로 인한 성능 저하에 비해 로그 기록으로 얻는 이점이 더 많기 때문에 사용한 것인데, 잘못 사용하면 로그를 사용하는 이유가 없어지기 때문에 효율적으로 로깅하는 방법을 이해해야 합니다.
로그 수준
SLF4J 기준
심각도 수준: ERROR > WARN > INFO > DEBUG > TRACE
WARN을 로그 레벨로 지정하면, WARN부터 그 위인 ERROR만 찍히게 됩니다.
⛔️ Error: 예상하지 못한 심각한 문제가 발생하는 경우, 즉시 조취를 취해야 할 수준의 레벨
⚠ ️Warn: 로직 상 유효성 확인, 예상 가능한 문제로 인한 예외 처리, 당장 서비스 운영에는 영향이 없지만 주의해야 할 부분
✅ Info: 운영에 참고할만한 사항, 중요한 비즈니스 프로세스가 완료되었을 때
⚙️ Debug: 개발 단계에서 사용하며, SQL 로깅 또한 가능
📝 Trace:모든 레벨에 대한 로깅이 추적되므로 개발 단계에서 사용
보통 Debug와 Trace 레벨로 설정할 경우, 많은 양의 로그가 쌓여 운영 단계에서 용량 감당이 안될 수 있어, 중요하지 않은 정보는 Debug 이하로 설정해 로깅하지 않는 편이 좋습니다.
Debug, Trace 레벨의 로깅은 개발 단계에서만 사용하기!
스프링에서의 로깅
스프링은 기본적으로 Jakarta(Apache) Commons Logging (JCL)을 사용합니다.
JCL은 로깅 추상화 라이브러리이며 애플리케이션의 로깅 라이브러리 선택권은 개발자의 몫입니다. 즉, JCL을 사용해 얼마든지 로깅 구현체를 교체하거나 직접 구현 가능합니다.
다만 JCL에 문제점이 있는데, JCL 구현체를 선택하는 시점이 런타임이라 클래스 로더에 의존적이라는 것입니다. 이때, 발생하는 문제는 런타임 시점에 JCL이 지정된 클래스 로더에 참조하고 있으면 해당 클래스 로더에 의해 로드된 리소스가 가비지 컬렉션 수집을 방해하여 메모리 누수가 발생할 수 있습니다. 한마디로 말하자면 가비지 컬렉션이 제대로 동작하지 않는다는 뜻입니다.
이를 해결하기 위해 클래스 로더 대신 컴파일 시점에 구현체 선택을 하도록 변경하였고, 이게 현재 스프링부트에서 제공하고 있는 SLF4J(Simple Logging Facade for Java)입니다.
SLF4J
SLF4J에서는 Bridging, API, Binding 모듈을 제공하여 컴파일 시점에 로깅 구현체를 결정하게 됩니다.
- SLF4J API는 로깅 인터페이스
- SLF4J Binding은 말 그대로 어댑터 역할을 하여 인터페이스와 로깅 구현체를 연결
- SLF4J Bridge는 로거 호출을 SLF4J 인터페이스로 연결
로깅 프레임워크 종류
1. Log4j (Deprecated)
가장 오래된 Apache 기반 Logging 프레임워크입니다. 다만 서비스가 중단되어 더 이상 사용하지 않고 있습니다.
2. Logback
Log4j 이후에 출시된 Java 기반 Logging 프레임워크. 동일한 개념을 따르지만 더 다양한 기능을 제공하며 성능이 개선되었습니다.
(ex. 더 많은 필더링 기능, 구성 파일 수정시 자동 리로드 등) Logback은 다음과 같이 세 부분으로 구성되어 있습니다.
- Logback-core - 로깅 프레임워크의 핵심 기능을 제공
- Logback-access - 이를 서블릿 컨테이너와 통합하여 HTTP 액세스 로그를 작성하는데 사용
- Logback-classic - SLF4J API를 구현
3. Log4j2
Apache log4j의 다음 버전이자 가장 최근에 등장한 프레임워크입니다.
Logback과 같은 필터링, 자동 리로드 등을 제공하며 이외에도 람다식 지원, Lazy Evaluation, 가비지 프리 기능등을 제공합니다. Logback과 가장 큰 차이로는 Log4j2는 멀티스레드 환경에서 비동기 로거를 사용할 경우, 10배 가까운 처리량을 처리할 수 있습니다.
다만, Log4j2로 비동기 어펜더만 사용한다면 큰 성능 차이가 없다!
Log4j2 vs Logback. 선택 기준은?
실제 저희 프로젝트에는 다음과 같은 이유로 Logback을 사용했습니다.
1. spring-starter-web에 default로 정의되어 있기 때문에 또 다른 dependency를 추가할 필요가 없다.
2. 비동기 로거를 사용한다면 Log4j2가 훨씬 더 좋은 성능을 보이지만, 그렇지 않다면 성능 측면에서는 비슷하기 때문에 아직까지는 비동기 로거의 필요성을 느끼지 못했다. (+ 추후 변경이 필요하다면 SLF4J를 사용했기 때문에 구현체만 갈아끼우면 된다 😊)
로그 파일 작성하기
콘솔 로그의 수준을 변경하는 방법은 application.yml
또는 logback-spring.xml
에서 설정하는 방법이 있습니다. yml로 하는 설정은 손쉽게 가능하나 세부적인 설정이 불편하여 운영에서 사용하기에는 한계가 존재합니다. 하여, logback-spring.xml 를 작성하고 이를 통해 관리하는 편이 더 좋습니다.
로깅 with Filter/Interceptor? or with AOP cross-cuts?
로깅을 Filter/Interceptor로 할 지, AOP로 할 지는 상황에 따라 다르다고 생각됩니다. 하지만, 일반적으로 모든 요청과 응답에 대한 처리라면 Filter/Interceptor에서 처리해주는 것이 관심사에 더 적합해 보입니다.
Filter/Interceptor는 Servlet 단위로 실행되는 반면, AOP는 Interceptor와 Controller 사이에서 동작하기 합니다. 이때, 단순하게 '들어오는 값'과 '내보내는 값'에 대한 로그 처리가 필요하다면 가장 앞단에서 처리하는 것이 더 올바른 구조라 생각됩니다. 또한, HTTP는 Spring 외부 영역(웹서버 레이어)에서 시작되는 작업이기 때문에, 만약 Spring 내 Aspect를 실행하기 전에 오류가 발생한다면 HTTP 요청 로그를 남기지 못하게 될 수 있습니다.
마지막으로 운영 관점에서 볼 때, AOP는 모든 서비스 호출 실행에 오버헤드를 추가하기 때문에 성능 면에서 떨어집니다. 반면, Logging Interceptor는 정상적인 실행 주기를 가진 클래스임을 알아두면 좋을 것 같습니다.
그렇다면 AOP로는 로깅을 하지 말아야 할까요?
아닙니다. AOP로의 로깅도 필요할 수 있습니다!
AOP 로깅은 다음과 같은 특징이 있습니다.
- 특정 서비스 메서드 등에 특별한 로깅을 남기고 싶을 때 사용가능.
- 함수나 클래스의 특정 반환 값에 대해 보다 세분화된 수준의 로깅을 구현하는데 용이
- ex) 요청에 대한 응답 값이 null일 때에만 로깅하고 싶다면?
@AfterReturning(pointcut = "execution(* com.foo.bar..*.*(..))", returning = "retVal")
public void logAfterMethod(JoinPoint joinPoint, Object retVal) {
...
}
그렇다면 Interceptor와 Filter 중에서는 어디가 더 적합할까요?
책임의 영역.
필터는 웹 상에서만 동작하기 때문에 애플리케이션 서버 영역을 벗어나면 사용할 수 없습니다. 반면, 인터셉터는 여러 구성요소에서 사용될 수 있기 때문에 웹 계층에 의존적이지 않으므로 필터보다 더 많은 필드에서 사용이 가능합니다. 즉, 스프링은 웹 계층에서만 사용되지 않기 때문에 스프링 인터셉터로 작성할 경우, Application Context 안에 정의된 것이기 때문에 웹을 벗어나도 쓸 수 있게 됩니다.
하여, 저는 HTTP Request/Response에 대한 도메인은 filter에 더 적합하다고 생각되며, interceptor는 메서드 실행 도메인에 더 적합하다고 생각됩니다. 따라서 웹과 관련된 logging, security, audit, 또는 요청에 대한 처리가 필요하다면 filter를 사용할 것 같습니다.
가능성은 희박하지만, DispatcherServlet에 요청이 들어오기 전에 예외가 발생하면 request 로그를 찍지 못할 수도 있기 때문에, end point인 가장 앞단의 filter에게 로깅을 위임하는 것도 좋은 방법이라고 생각됩니다.
참고
https://tecoble.techcourse.co.kr/post/2021-08-07-logback-tutorial/
댓글