[ Spring ] logback 설정하기와 로그 레벨

duck-ach·2022년 11월 8일
0

Spring

목록 보기
12/16

개요

Spring project를 만들면 src/main/resources경로와 src/test/resources경로에 log4j.xml이라는 파일이 생성이 된다.
오늘은 log4j보다 속도가 10배 빠르면서, 향상된 필터링 정책, 기능, 로그 레벨 변경 등에 대해 서버를 재시작할 필요없이 자동 리로딩을 지원한다는 logback 설정방법에 대해 공부해 볼 것이다.

Logging을 하는 이유

우리가 원래 코드가 어느정도까지 동작하는지 알아보는 방법중에 가장 간단하면서도 자주 사용하는 방법이 바로 console에 출력해보는 방법을 많이 사용하지 않는가,
Logging을 하면 출력형식을 지정할 수 있을 뿐만아니라, 로그 레벨에 따라 남기고 싶은 로그를 별도로 지정할 수 있기 때문에 콘솔뿐만 아니라 파일이나 네트워크 등 로그를 별도의 위치에 남길 수 있다.

로깅(Logging)이란 시스템이 동작할 때 시스템의 상태 및 동작 정보를 시간 경과에 따라 기록하는 것을 의미한다.

  • 로깅을 통해 개발자는 개발 과정 혹은 개발 후에 발생할 수 있는 예상치 못한 애플리케이션의 문제를 진단할 수 있고, 다양한 정보를 수집할 수 있다.
  • 사용자 로그의 경우 분석 데이터로도 활용할 수 있다.

하지만, 로깅을 하는 단계에서 적절한 수준의 로그 기록 기준을 잡지 못하면 방대한 양의 로그 파일이 생성되는 문제를 겪거나, 의미 있는 로그를 쌓지 못하는 경우가 발생할 수 있다. 결국 효율적으로 로깅을 하는 방법을 이해하는 것이 중요하다.

Spring에서 로깅(Logging)을 하는 방법

초기의 스프링은 JCL(Jakarta Commons Logging)을 사용해서 로깅을 구현했다.
요즘에는 대표적으로 Log4jLogback으로 스프링부트의 로그 구현체를 사용한다.

  • Log4j는 가장 오래된 프레임 워크이며, Apache의 Java기반 Logging Framework이다.
  • Logback은 SLF4j의 구현체이며 Spring Boot 환경이라면 별도의 dependency 추가 없이 기본적으로 포함되어 있다.

SLF4J

SLF4J(Simple Logging Facade for Java)

  • 다양한 로깅 프레임 워크에 대한 추상화(인터페이스) 역할을 한다.
  • 단독으로 사용이 불가능하며, 최종 사용자가 배포시 원하는 구현체를 선택하여 구현한다.
    (SLF4J가 인터페이스화 되어있기 때문에 SLF4J를 구현하는 클라이언트 코드에서는 실제 구현을 몰라도 된다. 정처기 공부하면서 배웠던 객체지향 5원칙(SOLID)의 개방 폐쇄 원칙, 의존관계 역전 법칙이 잘 사용되었다고 볼 수 있다.

객체지향 5원칙(SOLID)

  • 단일 책임 원칙(Single Responsibility Principle) : 하나의 객체는 반드시 하나의 동작만의 책임을 갖는다는 원칙이다.
  • 개방 폐쇄 원칙(Open-Closed Principle) : 기능이 변하거나 확장 가능하지만 해당 기능의 코드는 수정하면 안 된다는 뜻이다. (ex. 라이브러리를 사용하는 객체의 코드가 변경된다고 해서 라이브러리 코드까지 변경하지 않는다.)
  • 리스코프 치환 원칙(Liskov Subsitution Principle) : 부모 객체와 이를 상속한 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다는 원칙이다.
  • 인터페이스 분리 원칙(Interface Segregation Principle) : 객체는 자신이 호출하지 않는 메소드에 의존하지 않아야 한다는 원칙이다.
  • 의존성 역전 원칙(Dependency Inversion Principle) : 객체는 저수준 모듈 보다 고수준 모듈에 의존해야한다는 원칙이다. (객체는 객체보다 인터페이스에 의존해야 한다.)

Logback

Logback

  • SLF4J의 구현체이며, Log4J를 토대로 만든 프레임워크이다.
  • 현재는 Logback을 많이 사용한다고 한다.

Logback 구조

Logback은 위 그림과 같이 세가지 모듈로 나뉜다.

  • Logback-core : 다른 두 모듈을 위한 기반 역할을 하는 모듈이다. (Appender와 Layout인터페이스가 여기에 속함)
  • logback-classic : logback-core에 속하는 모듈로, SLF4J API를 구현한다. Logger 클래스가 이 모듈에 속함
  • logback-access : Servlet Container와 통합되어 HTTP 액세스에 대한 로깅 기능을 제공한다. 웹 애플리케이션 레벨이 아닌 컨테이너 레벨에서 설치 되어야 한다.

Logback 설정 요소

  • Logger : 어떻게 기록할까? (실제 로깅을 수행하는 구성요소)
  • Appender : 어디에다 기록할까? (로그 메세지가 출력할 대상 결정)
  • Layout : 어떻게 출력할까? (사용자가 지정한 형식으로 표현 될 로그메세지를 변환하는 역할)

pom.xml 설정

나는 Spring Boot가 아닌 Spring Legacy Project로 구현을 하고 있으니 dependency를 설정해주어야 한다.

<!-- Logging -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>${org.slf4j-version}</version>
</dependency>
      
<!-- 더 이상 사용하지 않는 라이브러리
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>${org.slf4j-version}</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>${org.slf4j-version}</version>
    <scope>runtime</scope>
</dependency>
-->
      
<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
<!-- <scope>test</scope> -->
</dependency>
      
      
<!-- Logback 사용을 위해서 제거되는 Log4j 라이브러리
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.15</version>
    <exclusions>
		<exclusion>
    		<groupId>javax.mail</groupId>
    		<artifactId>mail</artifactId>
    	</exclusion>
    	<exclusion>
        	<groupId>javax.jms</groupId>
        	<artifactId>jms</artifactId>
    	</exclusion>
    	<exclusion>
       		<groupId>com.sun.jdmk</groupId>
     		<artifactId>jmxtools</artifactId>
    	</exclusion>
    	<exclusion>
         	<groupId>com.sun.jmx</groupId>
         	<artifactId>jmxri</artifactId>
    	</exclusion>
    </exclusions>
    <scope>runtime</scope>
</dependency>
      -->

Logback관련 dependency를 mvnrepository 에서 사용하는 스프링 버전에 맞게 버전을 찾아 넣어주고, 기본에있던 다른 log4j 라이브러리 관련한 xml들은 지워준다.
나는 무엇을 삭제하였는지 다시 한 번 공부하기 위해 주석처리 하였다.

logback.xml

경로는 프로젝트 - src/main/resources에 log4j라는 xml이 기본적으로 만들어져 있을 것이다.

이 log4j의 이름을 logback으로 변경한 후 작업을 해 주었다.

<?xml version="1.0" encoding="UTF-8"?>

<configuration>
  
	<!-- Appenders -->
	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
		<layout class="ch.qos.logback.classic.PatternLayout">
			<pattern>%date{HH:mm:ss.SSS, Asia/Seoul} [%thread] %-5level %logger{36} - %msg%n</pattern>
		</layout>
	</appender>
	
	<!-- Root Logger -->
	<root level="INFO">
		<appender-ref ref="console" />
	</root>
	
</configuration>

Appender 구성

  • Appender는 <appender> 태그를 통하여 구성되며 nameclass 속성을 필수적으로 가져야만 한다.
  • name속성에는 appender의 이름을 명시하며, class속성에는 인스턴스화 시킬 appender 클래스를 명시하여야 한다.
  • 0개 또는 1개의 <layout> 태그와 0개 이상의 <encoder>, <filter> 태그를 가질 수 있다.

Root Logger

Info 레벨의 로그를 콘솔에 출력하는 것을 말한다.

로그레벨

  • 로그레벨 (치명적인 정도에 따른 순서이다.)
    1. FATAL : 매우 심각한 에러, 프로그램이 종료되는 경우가 많음
    2. ERROR : FATAL보다는 덜 심각한 에러. 의도하지 않은 에러가 발생한 경우이며 프로그램이 종료되지는 않는다.
    3. WARN : 에러가 될 수 있는 잠재적 가능성이 있는 경우
    4. INFO : 명확한 의도가 있는 에러, 요구사항에 따라 시스템 동작을 보여줄 경우
    5. DEBUG : INFO레벨보다 더 자세한 정보가 필요한 경우. 디버깅환경
    6. TRACE : DEBUG보다 세밀한 정보. Dev환경에서 버그를 해결하기 위해 사용된다.

FATAL

응용 프로그램의 중요한 비즈니스 기능 중 하나가 더 이상 작동하지 않는 상태에 진입했을 때 혹은 전체 프로그램이 비즈니스 기능을 수행하지 않을 때 알려주는 로그 수준이다. 데이터베이스와 같은 중요한 저장소에 연결할 수 없는 경우 이에 해당된다.

ERROR

응용 프로그램이 하나 이상의 기능이 제대로 작동하지 않아 일부 기능이 올바르지 않게 작동하지 않을 때 사용한다.

WARN

예기치 않은 일이 발생했음을 나타내는 로그 레벨이지만, 응용 프로그램이 실패했다는 것을 의미하지는 않는다. 예기치 않은 상황에서 사용하지만 기능은 예상대로 작동할 때이다.

INFO

문제가 발생했음을 나타내는 표준 로그 레벨이다. 이 수준으로 기록하는 유용한 정보여야 한다. 서비스 시작이나 중지 등이 이에 해당한다. 일반적으로 정상적인 상황에서는 신경쓰지 않는다.

DEBUG

문제 진단 및 문제 해결을 할 때 도움이 되는 정보이다. 이 외에도 모든 것이 올바르게 실행되고 있는지 확인하기 위해 테스트 환경에서 프로그램을 실행할 때 사용된다.

TRACE

코드를 추적하고 기능의 부분을 찾을 때 사용하는 정보이다.

로깅 vs 디버깅

프로그래밍의 절반은 디버깅이다. 디버깅을 할 수 없는 상황에서는 로깅이 최선의 선택이다.(ex. 실 서버 구동중일 경우)
디버깅을 쓸 수 있다면 디버깅을 최대한 활용하는 것이 좋다.

AOP를 활용한 로깅

AOP란?

관점 지향 프로그래밍이라고도 불린다.
어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나눠보고 그 관점을 기준으로 각각 모듈화 하겠다는 의미이다.

  • 핵심적인 관점 : 개발자가 적용하고자 하는 핵심 비즈니스 로직
  • 부가적인 관점 : 핵심 로직을 수행하기 위해 필요한 DB연결(JDBC), 로깅, 파일 입출력 등...

AOP는 Aspect를 분리하여 핵심기능을 설계 및 구현할 때 객체지향적인 가치를 지킬 수 있도록 도와주는 것이다.

주요키워드

Aspect

여러곳에서 쓰이는 공통 부분 코드를 모듈화 한 것

Target

Aspect가 적용되는 곳(Ex: class, method ...)

Advice

Aspect에서 실질적인 기능에 대한 구현체

Joint point

Advice가 Target에 적용되는 시점
메소드 진입할 때, 생성자 호출할 때, 필드에서 값을 꺼낼 때 등

Point cut

Joint Point의 상세 스펙을 정의한 것

Proxy

클라이언트와 타겟 사이에 투명하게 존재하며 부가기능을 제공하는 오브젝트.
DI를 통해 타겟 대신 클라이언트에게 주입되며 클라이언트의 메소드 호출을 대신 받아서 타겟에 위임하며 이 과정에서 부가기능을 부여한다.


이제 로그를 찍어주는 코드를 짜줄것이다. 자바 클래스로 생성하며, @Component@Aspect라는 애너테이션을 선언해주어야 한다.

아래는 Lagacy Project를 생성하면 만들어지는 HomeController 클래스인데, 여기에도 기본적으로 로그를 찍을 수 있는 코드가 작성되어있는 것을 확인할 수 있다.

Log를 수행하는 Aspect 클래스

package com.gdu.app08.aop;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Component
@Aspect // 안녕. 난 Aspect야. AOP 동작하려면 내가 필요해
public class RequestLoggingAspect {
	
	// Logger
	private static final Logger LOG = LoggerFactory.getLogger(RequestLoggingAspect.class); // 클래스이름은 현재 클래스이름
	
	@Pointcut("within(com.gdu.app08.controller..*)")
	public void setPointCut() {}
	
	
	// 어드바이스 설정
	// 어드바이스 실행 시점
	// @Before, @After, @AfterReturning, @AfterThrowing, @Around(가장많이사용)
	@Around("com.gdu.app08.aop.RequestLoggingAspect.setPointCut()") // setPointCut() 메소드에 설정된 포인트컷에서 동작하는 어드바이스
	public Object executeLogging(ProceedingJoinPoint joinPoint) throws Throwable { 	//@Around는 반드시 ProceedingJoinPoint joinPoint 선언해야 함
		
		// HttpServletRequest를 사용하는 방법
		HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
		// HttpServletRequest를 Map으로 바꾸기
		// 파라미터는 Map의 key가되고, 값은 Map의 value가 된다.
		Map<String, String[]> map = request.getParameterMap();
		String params = "";
		if(map.isEmpty()) {
			params += "[No Parameter]";
		} else {
			for(Map.Entry<String, String[]> entry : map.entrySet()) {
				params += "[" + entry.getKey() + "=" + String.format("%s", (Object[])entry.getValue()) + "]";

			}
		}
		// 어드바이스는 proceed() 메소드 실행 결과를 반환
		Object result = null;
		try {
			result = joinPoint.proceed(joinPoint.getArgs());
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// 무조건 실행되는 영역(여기서 로그를 찍는다)
			// 치환문자 : {}
			LOG.info("{} {} {} > {}", request.getMethod(), request.getRequestURI(), params, request.getRemoteHost());
		}
		return result;
	}
}
profile
자몽 허니 블랙티와 아메리카노 사이 그 어딘가

0개의 댓글