Backend/SpringBoot

AOP 및 Slf4j + logback 로깅

Dean83 2025. 9. 1. 11:55

AOP 는 코드의 재사용성을 늘려 중복된 코드를 줄이도록 하는 것으로, Aspect Oriented Programming의 약자이다. 

주로 로깅 등 유틸성 기능에서 코드 중복을 줄이기 위해서 사용한다. 특히 로깅, 시간측정 할때. 

사실 이 개념을 처음 봤다. 다른 코드에서는 없던 방식(?) 이었기 때문에. 스프링부트는 이런 부분이 잘 되어 있는거 같다. 

 

C# 에선 로깅을 위해 이런 코드들이 많았다. 

...

public static void WriteLog(string msg)
{
	....
    Console.Write(msg);
    ...
}


...
public void TestA()
{
	...
    WriteLog("aa");
    ....

}

...
public void TestB()
{
	...
    WriteLog("bb");
    ....

}

 

로깅을 하는 함수는 따로 만들었고, 각 함수에서 이를 호출하는것은 코드 중복이라고 생각하지 않았다. 그러나 스프링부트 에서는 이것 또한 중복된 코드로 보는것이다. 

 

일단 그래들에 다음을 추가 한다.

implementation 'org.springframework.boot:spring-boot-starter-aop'

 

...

@Aspect
public class ClassA
{
	//서비스 경로를 넣어주고, 해당 경로(하위까지) 에 적용됨
	@Before ("execution(* com.example.service..*(..))")
    public void MethodA()
    {
    	...
        System.out.println("시작전 로깅할 내용들");
        ...
    }
    
    @After ("execution(* com.example.service..*(..))")
	public void MethodB()
    {
    	...
        System.out.println("종료 후 로깅할 내용들");
		...
    }
}


...
package com.example.service;
...
@Service
public class ClassB
{
	//원하는 코드 작성
    public void testMethod()
    {
    	...
    }
}
  • 서비스인 ClassB 의 testMethod 가 호출될 경우에 ClassA 에 명시한 MethodA, MethodB의 각 로그들이 찍히게 된다.
  • 만일 동일 패키지의 다른 서비스 클래스가 있다면, 해당 클래스의 메소드가 실행할때도 마찬가지로 로그들이 찍히게 된다. 
  • 로그 뿐 아니라 실행 시간 측정에서도 사용된다. 

 

AOP를 이용하면 로깅 조차도 중복 코드 없이 빠르게 작업을 할 수 있을거 같지만 이거만으로 충분해 보이지는 않는다. 
아마 기존방식과 이 방식 모두 사용해야 하지 않을까? 생각이 든다. 

 

추가, Slf4j + logback 을 통해 로깅하기

springboot 에서 주소 slf4j를 이용해서 로깅을 하게 된다. 사용법은 간단한데, 일단 lombok 을 그래들에 추가하도록 한다.

dependencies {
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

 

로깅을 사용하고자 하는 클래스에 @Slf4j 어노테이션을 붙여주면 된다.

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class 클래스명 {

    public void 메서드명(String 인자값) {
        log.info("내용: {}", 인자값);
        
        ...
        try
        {
        
        }
        catch(Exception ex)
        {
        	//stacktrace 포함
        	log.error("내용 : {}",인자값, e);
            
        }
    }
}

 

설정 예시는 다음과 같다

logging:
  level:
    root: INFO
    com.example.myapp: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n%ex"
  file:
    name: logs/app.log
  • %-5level : 좌측에 최소 5간 공간을 확보하여 로그레벨을 출력함 (INFO등) -를 빼면 우측정렬
  • [%thread] : 로그를 출력한 스레드 이름 표시. 멀티스레드 환경에서 유용
  • %logger{36} : 로그를 찍은 클래스 이름 출력 (36자 까지)
  • %ex : stacktrace 정보 출력

그러나, 위의 properties의 설정 보다는 별도의 logback-spring.xml로 설정 하는것이 좋다.

  • 저장위치 : resources 폴더 밑에 위치
  • 파일 생성 타입 (예 :  하루마다 새로운 파일 생성) 및 파일 보존기간 등을 설정 할 수 있다.
    • yaml 에서도 가능하다 하나, 기능이 제한적이라 xml에 별도로 하는것이 좋다고 한다.
<configuration>
    <property name="LOG_PATH" value="logs"/>

    <appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/app.log</file>
        
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 하루마다 새로운 로그 파일 생성 -->
            <fileNamePattern>${LOG_PATH}/app.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 보존 기간: 30일 -->
            <maxHistory>30</maxHistory>
            <!-- 압축해서 저장 가능 -->
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="ROLLING"/>
    </root>
</configuration>
  • <fileNamePattern>: 로그 파일 이름 패턴, %d{yyyy-MM-dd} → 일별 파일 생성
  • <maxHistory>: 로그 파일 보존 일수, 30일이면 30일 지난 로그 자동 삭제
  • <cleanHistoryOnStart>: 애플리케이션 시작 시 오래된 파일 자동 삭제

 

  • 상황에 따라 logger를 여러개 두어서 따로따로 저장할 수 있다. 
    • 예 : 일반 로거, 보안 로거 등
    • 아래의 예는, 콘솔, 파일로 저장하는 로그 설정 예
    • json형식으로 저장
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true">

    <!-- Spring Profile 읽기 -->
    <springProperty name="springProfile" source="spring.profiles.active" />

    <property name="LOG_PATH" value="logs"/>

    <!-- ============================= -->
    <!-- 🔹 일반 로그 Appender -->
    <!-- ============================= -->
    <appender name="APP_JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp/>
                <logLevel/>
                <threadName/>
                <loggerName/>
                <message/>
                <mdc/>
                <stackTrace/>
                <jsonProvider class="net.logstash.logback.composite.GlobalCustomFieldsJsonProvider">
                    <customFields>{"env":"${springProfile}","type":"app"}</customFields>
                </jsonProvider>
            </providers>
        </encoder>
    </appender>

    <appender name="APP_JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/app-${springProfile}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/app-%d{yyyy-MM-dd_HH-mm}.log</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp/>
                <logLevel/>
                <threadName/>
                <loggerName/>
                <message/>
                <mdc/>
                <stackTrace/>
                <jsonProvider class="net.logstash.logback.composite.GlobalCustomFieldsJsonProvider">
                    <customFields>{"env":"${springProfile}","type":"app"}</customFields>
                </jsonProvider>
            </providers>
        </encoder>
    </appender>

    <!-- ============================= -->
    <!-- 🔹 보안 로그 Appender -->
    <!-- ============================= -->
    <appender name="SEC_JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/security-${springProfile}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/security-%d{yyyy-MM-dd_HH-mm}.log</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp/>
                <logLevel/>
                <threadName/>
                <loggerName/>
                <message/>
                <mdc/>
                <stackTrace/>
                <jsonProvider class="net.logstash.logback.composite.GlobalCustomFieldsJsonProvider">
                    <customFields>{"env":"${springProfile}","type":"security"}</customFields>
                </jsonProvider>
            </providers>
        </encoder>
    </appender>

    <!-- ============================= -->
    <!-- 🔹 Logger 정의 -->
    <!-- ============================= -->

    <!-- 일반 애플리케이션 로그 -->
    <logger name="com.example.app" level="INFO" additivity="false">
        <appender-ref ref="APP_JSON_CONSOLE"/>
        <appender-ref ref="APP_JSON_FILE"/>
    </logger>

    <!-- 보안 관련 로그 -->
    <logger name="com.example.security" level="INFO" additivity="false">
        <appender-ref ref="SEC_JSON_FILE"/>
    </logger>

    <!-- Root Logger -->
    <root level="INFO">
        <appender-ref ref="APP_JSON_CONSOLE"/>
    </root>

</configuration>

 

 

각 클래스 코드에서 로거를 생성해야 한다. 이렇게 할 경우, 로깅 내용에 클래스가 표시되므로, 어떤 클래스에서 로깅했는지 알 수 있다.

생성할때, 패키지명 별로 구분되므로 클래스별로 바꿔줘야 한다.

(기본적으로 @Slf4j 어노테이션을 쓰면, 빌드시 LoggerFactory.getLogger를 통해 1개의 로거를 생성해 준다. )

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class LoggingExample {

    // 일반 애플리케이션 로그
    private static final Logger appLogger = LoggerFactory.getLogger("com.example.app");

    // 보안 로그
    private static final Logger secLogger = LoggerFactory.getLogger("com.example.security");

    ....
}