표준 메시지 로그 메시지 포맷부터 먼저 알아보자.
2021-07-25 16:11:01.434 INFO o.s.b.w.embedded.tomcat.TomcatWebServer: Tomcat started on port(s): 8080 (http) with context path
로그 메시지는 타임스탬프, 심각도, 앱의 어느 파트에서 메시지를 남겼는지 등의 정보가 포함된다.
- 2021-07-25 16:11:01.434: 앱이 메시지를 기록한 시점으로, 시간순으로 정렬된다.
- INFO: 메시지의 중요도이다.
- o.s.b.w.embedded.tomcat.TomcatWebServer: 로그 메시지를 남긴 모듈과 클래스를 보통 표시한다.
- Tomcat started on port(s): 8080 (http) with context path: 무슨 일이 일어나는지 쉽게 읽고 이해할 수 있도록 기술한다.
개발자가 문제를 조사할 때는 무엇보다 로그를 가장 먼저 확인해야 한다.
로그를 보면 이상한 동작이 바로 보이기 때문에 어디서부터 조사를 시작해야 할지 정확하게 진단할 수 있기 때문이다.
1. 로그를 이용하여 조사하기
로그 메시지의 가장 큰 장점은 주어진 시간에 특정 코드의 실행을 시각화하는 능력이다.
로그는 과거 특정 기간의 앱 실행에 초점을 맞추며, 로그 메시지는 시간과 아주 밀접한 관계가 있다.
따라서 로그를 볼때는 앱이 실행되는 시스템의 표준 시간대를 제일 먼저 체크하는 것이 중요하다.
1.1 로그에 기록된 예외 식별
로그는 문제가 발생한 이후 근본 원인을 찾는데 유용하다.
보통 조사를 어디서부터 시작할지 로그를 보고 결정한다.
그런 다음 디버거나 프로파일러 같은 다른 도구 및 기법을 적용해서 조사를 진행한다.
예외 스택 트레이스가 로그에서 발견되면 앱 기능에 문제가 있다고 볼 수 있다.
예외 메시지를 보면 문제가 발생한 위치에 관한 힌트를 얻게 된다.
예를 들어, NullPointerException은 어떤 커맨드가 변수나 애트리뷰트를 통해 존재하지 않는 객체 인스턴스 레퍼런스를 읽었을 때 발생하는 예외다.
var invoice = getLastIssuedInvoice();
if(client.isOverdue()) {
invoice.pay(); //NullPointerException이 발생하면 invoice 변수가 null이라는 뜻이다.
}
예외가 발생한 라인이 항상 문제의 근원은 아니다. 더 근본적인 원인의 결과에 지나지 않을지도 모른다.
위에서는 getLastIssuedInvoice()가 왜 null을 리턴했는지, 원인을 밝혀야 한다.
단순히 아래와 같은 검증 코드를 추가한다고 문제가 해결되는 것이 아니다.
var invoice = getLastIssuedInvoice();
if(client.isOverdue()) {
if(invoice != null) { //해당 코드 추가
invoice.pay();
}
}
1.2 예외 스택 트레이스로 어디서 메서드를 호출했는지 식별
다소 특이하지만 실전에서 유용한 기법이 있다.
바로 예외 스택 트레이스를 로깅해 특정 메서드를 어디에서 호출했는지 알아내는 것이다.
원격 디버깅 작업이 불가능할 때 로깅을 적극적으로 사용하는 것이 좋다.
자바 예외에는 흔히 사람들이 관심을 두지 않는 기능이 한 가지 있다.
바로 실행 스택 트레이스를 추적하는 기능이다.
실행 스택 트레이스는 예외 스택 트레이스라고 부르기도 한다.
예외 스택 트레이스는 어떤 예외를 일으킨 메서드 호출 체인을 표시하는데, 우리는 해당 예외를 던지지 않아도 이 정보에 액세스할 수 있다.
코드에서 다음과 같이 예외를 사용하면 충분하다.
new Exception().printStackTrace();
자바 앱은 모든 스레드가 각자 이름을 가지고 있다.
이 이름은 개발자가 부여하거나 JVM이 Thread-X 형식으로 명명한다.
멀티 스레드 아키텍처에서는 스레드에 이름을 붙여야 로그를 활용해 나중에 문제를 조사할 때 편하다.
2. 로깅을 구현하는 방법
2.1 로그 메시지 저장
로그를 읽어야 무슨 일이 일어났는지 알 수 있으므로 앱은 나중에 읽어볼 수 있게 어딘가 로그를 저장해야 한다.
로그 메시지를 저장하는 방식은 로그의 유용성과 앱 성능에 영향을 미칠 수 있다.
보통 아래 3가지 방법으로 로그를 저장한다.
- 비관계형(NoSQL) DB에 로그 저장
- 파일에 로그 저장
- 관계형(SQL) DB에 로그 저장
비관계형 DB에 로그 저장
비관계형 DB는 성능과 일관성 사이의 균형을 맞출 수 있는 좋은 방법이다.
성능이 좋고, 로그 메시지가 소실되거나 앱이 기록한 순서대로 저장되지 않을 가능성이 있다.
그러나 일반적으로 로그 메시지에는 메시지가 저장된 타임 스탬프가 있고 메시지 시작부에 포함되어 있어 크게 문제가 되지 않는다.
요즘은 NOSQL에 로그를 저장하는 것이 일반적이며, 로그를 저장하고 로그 메시지를 조회, 검색, 분석하는 완전한 기능을 갖춘 ELK 스택, 스펄렁크 등의 엔진을 사용하는 경우가 대부분이다.
파일에 로그 저장
과거에는 로그를 파일로 저장했다.
이 방식은 속도가 떨어지고 로깅된 데이터를 검색하기 어렵기 때문에 사용 빈도는 점점 줄어들고 있다.
관계형 DB에 로그 저장
드물긴 하지만 DB에 로그를 저장할 수 있다.
관계형 DB는 데이터 일관성을 보장하고 로그 메시지가 소실되는 일은 없고, DB에서 로그는 언제든지 조회가 가능하다.
다만 성능 저하라는 비용이 수반된다.
로그를 저장하면서 제일 중요한 것은 성능이다.
그러나 엄격한 규제가 붙는다면 (예시 금융, 정부 앱) 일관성이 더 중요하다.
2.2 로깅 레벨을 정의하고 로깅 프레임워크를 사용하는 방법
심각도라고도 하는 로깅 레벨은 조사 중요도에 따라 로그 메시지를 분류한다.
메시지마다 중요도가 다르며, 그중에는 즉시 확인해봐야 할 이벤트가 포함된 메시지도 있을 것이다.
일반적으로 가장 많이 사용하는 로그 레벨은 다음과 같다.
- Error: 아주 중대한 문제가 발생한 것으로, 이런 이벤트는 반드시 기록해야 한다. 보통 자바 앱에서 처리되지 않은 예외는 에러로 기록한다.
- Warn: 잠재적으로 에러일 수 있으나 앱이 처리한 이벤트다. 예를 들어 타사 시스템과의 데이터 연동이 처음에는 실패했지만 두 번째 시도에는 성공했다면 경고로 기록한다.
- Info: '상시(common) 로그 메시지' 대부분의 상황에서 앱이 어떻게 동작하고 있는지 이해하는데 유용한, 주요한 앱 실행 이벤트를 나타낸다.
- Debug: Info 메시지만으로 불충분한 경우에 한하여 매우 세분화된 정보를 남긴다.
위에서부터 아래로 더 많고, 자세하고, 덜 중요한 정보다. 상용에서 보통 Debug는 꺼둔다.
로그 메시지를 심각도에 따라 분류하면 앱이 저장하는 로그 메시지 수를 최소화할 수 있다.
가장 연관성이 높은 세부 정보만 기록하되, 더 자세한 정보가 필요할 때에만 더 많은 로그를 남기도록 설정한다.
로깅 레벨은 간단히 구현할 수 있다. 오늘날 자바 세상에는 로그백, Log4j, 자바 로깅 API 등의 다양한 로깅 프레임워크가 있다.
책에서는 Log4j를 설명하지만 필자는 LogBack을 기준으로 포스팅을 진행한다.
Log4와 LogBack 둘 다 SLF4J의 구현체로 LogBack이 성능이 약간 더 좋다.
아래는 LogBack 예시 설정 .xml 파일이다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%clr(%d{HH:mm:ss.SSS}){faint}|%clr(${level:-%5p})|%32X{traceId:-},%16X{spanId:-}|%clr(%-40.40logger{39}){cyan}%clr(|){faint}%m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<logger name="org.springframework" level="INFO"/>
<logger name="template.springboot" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
LogBack와 Log4j는 크게 3가지 항목을 설정 파일에서 세팅한다.
로거(Logger)
로거는 로깅 작업의 중심으로, 애플리케이션 코드에서 로그 메시지를 기록할 때 사용하는 인터페이스다.
로거는 메시지의 레벨에 따라 로그를 생성하거나 무시할 수 있으며, 어펜더를 통해 로그 메시지를 전달하는 역할을 한다.
위 설정서 logger 태그의 name은 패키지 경로를 나타낸다.
즉 template.springboot에서 DEBUG 이상의 로그 메시지만 기록하도록 설정한 것이다.
어펜더(Appender)
어펜더는 로거로부터 전달된 로그 메시지를 특정한 출력 대상으로 전송하는 역할을 한다.
로그 메시지를 콘솔에 출력하거나, 파일에 기록하거나, 원격 서버에 전송할 수 있도록 다양한 어펜더가 제공된다.
앱에서도 여러 어펜더를 지정할 수 있다.
- ConsoleAppender: 로그를 콘솔에 출력
- FileAppender: 로그를 파일에 저장.
- RollingFileAppender: 지정된 조건에 따라 새로운 파일로 교체(롤링)되며, 로그 파일이 무한정 커지지 않도록 관리한다.
- AsyncAppender: 비동기 방식으로 로그를 기록하여 성능을 향상시킨다
- SocketAppender: 로그 메시지를 소켓을 통해 외부로 전송한다.
포매터(Formatter)
포매터 또는 **패턴 레이아웃(Pattern Layout)**은 로그 메시지의 형식을 지정한다.
로그 메시지에 포함할 항목(날짜, 시간, 로그 레벨, 클래스 이름 등)을 설정해, 로그가 출력되는 방식을 일관되게 유지하도록 한다.
포매터는 일반적으로 로그백에서 어펜더의 <encoder>에 설정된다.
2.3 로깅 때문에 발생하는 문제와 예방 조치
로그 때문에 생길 수 있는 세 가지 문제와 이를 방지하는 방법을 알아보자.
- 보안 및 프라이버시 문제: 로그 메시지에 개인정보가 노출된다.
- 성능 문제: 지나치게 큰 로그 메시지를 과도하게 생성하면 문제가 된다.
- 유지보수 문제: 로그를 남기는 커맨드 때문에 소스 코드의 가독성이 떨어진다.
보안및 프라이버시 문제
로그 때문에 앱에 보안 취약점이 유발되는 경우가 많다.
많은 개발자가 로그 메시지에 남기는 세부 정보에 제대로 신경을 쓰지 않기 때문이다.
로그 안의 내용은 액세스할 권한을 가진 사람이라면 누구나 다 볼 수 있다는 사실을 명심하자.
그가 해당 로그를 봐도 되는지, 다른 문제는 없는지 반드시 고려하라.
민감한 정보가 로그 메시지에 그대로 노출되면,
시스템을 망가뜨리거나 보안 사고를 일으키려는 악의적인 해커에게 유용한 정보를 갖다 바치는 것과 다름 없다.
여러 사례를 보여주는데 로그에는 개인 정보는 남기지 말자가 결론이다.
성능 문제
로그를 쓰려면 세부 정보를 I/O 스트림을 통해 앱 외부의 어딘가로 보내야 한다.
설정에 따라 단순히 앱의 콘솔(터미널)로 보낼 수 있겠지만, 파일이나 DB에 저장할 수도 있다.
즉 너무 많은 로그 메시지를 추가하면 앱 성능이 떨어질 수 있다.
반드시 상용 앱에서는 필요한 메시지만 저장하고, 너무 많은 로그 메시지를 저장하지 말자.
그리고 로깅 레벨은 서비스를 재시작하지 않아도 바꿀 수 있게 구현하는 것이 좋다.
유지보수성
앱의 유지보수성을 해치지 않고 로그를 남기려면 어떻게 해야 할까?
- 코드에 있는 커맨드를 하나하나 로깅하지 마라. 어떤 커맨드가 가장 연관성이 큰 세부 정보를 제공하는지 파악하라.
기존 로그 메시지로 충분하지 않으면 나중에 얼마든지 로그를 더 추가할 수 있다. - 메서드의 인숫값과 리턴값 정도만 남기도록 로깅 코드를 적당히 구현하라.
- 프레임워크를 사용하면 일부 로깅 코드를 메서드에서 디커플링할 수 있다.
예를 들어 스프링에서 커스텀 애스펙트를 이용하면 메서드의 실행 결과 (인숫값 + 실행 후 메서드가 리턴한 값)을 기록할 수 있다. - AOP와 로거를 감싼 래퍼 클래스가 깔끔하다고 생각하긴 하지만.. 이것도 결국 비용이니 적절한 트레이드 오프가 중요하다.
3. 로그와 원격 디버깅
기능 | 로깅 | 원격 디버깅 |
원격 실행되는 앱의 동작을 파악하는 데 사용할 수 있다. | V | V |
특별한 네트워크 퍼미션 또는 구성이 필요하다. | X | V |
실행 단서를 영구 저장한다. | V | X |
특정 코드 라인에서 실행을 중단시켜 앱이 무슨 일을 하는지 파악할 수 있다. | X | V |
실행 로직을 방해하지 않고 앱 동작을 이해하는 데 사용할 수 있다. | V | X |
프로덕션 환경에 권장한다. | V | X |
참고 자료
https://product.kyobobook.co.kr/detail/S000213029114
댓글