AOP란?
AOP(Aspect-Oriented Porgramming)은 관점 지향 프로그래밍이라고 한다. 어플리케이션의 공통 관심사를 모듈화 하여 코드의 중복성을 줄이고 유지보수를 용이하게 하는 프로그래밍 패러다임이다. Spring AOP는 프레임워크에서 제공하는 구현체로 런타임 시점에 동적으로 Proxy를 생성하여 메서드 호출 전후에 특정 로직을 추가할 수 있다.
로깅 시스템에서 AOP를 사용하는 이유는 다음과 같다.
- 로깅은 비즈니스 로직과는 독립적이지만 공통 관심사이다. AOP를 통해 이를 비즈니스 코드와 분리하여 관리할 수 있다.
- 여러 클래스와 메소드에서 발생하는 로깅을 하나의 코드에서 처리할 수 있다.
- 하나의 코드에서 관리하기 때문에 유지보수와 수정, 확장이 용이하다.
Log4j2 설정 방법 및 파일 구성
// log4j2
implementation 'org.apache.logging.log4j:log4j-api:2.20.0'
implementation 'org.apache.logging.log4j:log4j-core:2.20.0'
implementation ('org.apache.logging.log4j:log4j-slf4j-impl:2.20.0') {
exclude group: 'ch.qos.logback'
}
기존 Slf4j나 Log4j보다 성능이 우수한 Log4j2를 사용하기 위해 build.gradle에 위와 같이 의존성을 추가한다.
이 때 기존 spring dependency에 존재하는 logback이 겹쳐 버전문제가 발생할 수 있기에 exclude 구문도 추가해준다.
의존성을 추가한 후 Log4j2 설정파일을 구성한다.
src/main/resources/log4j2.xml 경로에 파일을 생성하여 설정을 해주는데 내용은 다음과 같다.
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<!-- Console Appender -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n" />
</Console>
<!-- File Appender -->
<File name="File" fileName="logs/app.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n" />
</File>
<RollingFile name="UserLogFile" fileName="logs/user.log" filePattern="logs/user-%d{yyyy-MM}-%i.log.gz">
<PatternLayout>
<Pattern>%d [%t] %-5level %logger{36} - %msg%n</Pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="10MB" />
<TimeBasedTriggeringPolicy />
</Policies>
<DefaultRolloverStrategy max="10" />
</RollingFile>
<Loggers>
<!-- Application Logger -->
<Logger name="app.ebel" level="info" additivity="false">
<AppenderRef ref="Console" />
<AppenderRef ref="File" />
</Logger>
<Logger name="UserLogger" level="info" additivity="false">
<AppenderRef ref="UserLogFile"/>
</Logger>
<!-- Root Logger -->
<Root level="info">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
굉~~장히 복잡해 보이는 설정파일이지만. 주요한 설정은 다음과 같다.
- Configuration status: log4j2 내부 로깅 수준을 의미한다. 사실상 설정파일이 제대로 읽히지 않거나 에러가 발생했을 때 문제를 디버깅만 하면 되므로 WARN으로 설정하였다.
- Appender: 로그 메시지를 출력하는 대상을 정의한다. 로그를 콘솔, 파일, DB 등으로 출력할 수 있다. 또한 여러개의 Appenders를 설정하여 로그를 여러 대상으로 동시에 보낼 수 있다.
- ConsoleAppender: 로그를 콘솔에 출력
- FileAppender: 로그를 파일에 출력
- RollingFileAppender: 로그 파일 크기가 지정된 크기를 초과하면 새로운 파일로 롤링
- AsyncAppender: 비동기 방식의 로그 처리
- Logger: 로그 메시지를 실제로 생성하는 어플리케이션의 로깅 컴포넌트를 정의한다. 특정 클래스 또는 패키지에 대해 로그 수준과 Appender를 설정할 수 있다. Root logger는 기본 전역 로깅 설정이고, Named Logger는 특정 클래스나 패키지에 대해 로깅 설정을 한다.
- PatternLayout: 로그 메시지의 출력 형식을 정의하며 주요 변수는 다음과 같다.
- %d: 로그가 출력될 날짜와 시간. timestamp 포맷을 사용 가능.
- %t: 로그를 생성한 스레드 이름
- %p, %level: 로그 수준(TRACE, DEBUG, INFO, WARN, ERROR)
- %c, %logger: 로그를 생성한 클래스나 로거 이름
- %m, %msg: 실제 로그 메시
- %F: 로그를 생성한 파일 이름
- %M: 로그를 생성한 메소드 이름
- %ex, %throwable: 예외 스택 trace (있을 때만 출력)
PatternLayout의 예시는 다음과 같다.
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %c{1} - %msg%n"/>
# 실제 출력
2025-01-13 10:15:30 [main] INFO MyClass - Application started successfully
AOP를 이용한 로깅 코드 작성
AOP를 이용하여 로깅을 구현하려면 @Aspect를 사용하여 정의하고 Pointcut, Around 등을 활용하여 메소드 호출을 가로채야한다.
@Aspect
@Component
@RequiredArgsConstructor
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
private final HttpServletRequest request;
@Pointcut("execution(* app.ebel.steadybucks.controller..*(..))")
public void controllerAPIs() {}
@Around(controllerAPIs())
public Object logApiCall(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
// API 요청 정보
String method = request.getMethod(); // GET, POST 등
String url = request.getRequestURI();
String queryParams = request.getQueryString();
String ip = request.getRemoteAddr();
String sessionId = request.getRequestedSessionId();
String user = request.getRemoteUser() != null ? request.getRemoteUser() : "Anonymous";
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
Object result;
int statusCode = 200; // 기본 상태 코드
try {
result = joinPoint.proceed(); // 실제 메서드 실행
// ResponseEntity에서 상태 코드 가져오기 (ResponseEntity일 경우만 처리)
if (result instanceof ResponseEntity) {
statusCode = ((ResponseEntity<?>) result).getStatusCode().value();
}
} catch (Exception e) {
long endTime = System.currentTimeMillis();
logger.error("[USER: {}, SESSION: {}, METHOD: {}, URL: {}, FROM: {} IP: {}, EX: {}, MSG: {}, Execution Time: {}ms]",
user, sessionId, method, url, ip, className + '.' + methodName, e.getClass().getSimpleName(), e.getMessage(), (endTime - startTime));
throw e;
}
long endTime = System.currentTimeMillis();
logger.info("[USER: {}, SESSION: {}, METHOD: {}, URL: {}, IP: {}, StatusCode: {}, Execution Time: {}ms]",
user, sessionId, method, url, ip, statusCode, (endTime - startTime));
return result;
}
}
위 코드는 프로젝트 내 모든 controller에서 수행되는 method에 대하여 logging을 수행한다.
@Pointcut 으로 적용할 메소드의 범위를 정의한다.
@Around는 pointcut 내부 범위에 포함되는 메소드 실행 전후의 동작을 정의한다.
처리 메소드의 파라미터로 ProceedingJoinPoint 라는 객체가 들어오는데, 이 객체가 실제 메소드 호출을 제어한다.
로깅에 필요한 정보 수집 -> 실제 메소드 호출 joinPoint.proceed() -> 로그 출력.
해당 순서로 내부 로직을 수행하게 된다. 정상적으로 메소드를 수행했다면 INFO log를 출력한다.
try-catch 문으로 만약 메소드에서 exception을 뱉는다면 ERROR log를 출력하고 다시 throw하여 handling 한다.
'Backend' 카테고리의 다른 글
[Java Spring Boot] JWT 토큰 알아보기 (0) | 2025.01.17 |
---|---|
[Java Spring Boot] Custom Exception과 Exception Handler 구현하기 (@RestControllerAdvice) (0) | 2025.01.14 |
[Java Spring Boot] Entity 복합 키 설정하기 - @Embeddable, @EmbeddedId (2) | 2025.01.13 |
[Java Spring Boot] Repository에서 복잡한 구조의 DTO 매핑하기.(QueryDSL, JPQL, JDBC Template) (0) | 2025.01.13 |
[Java Spring Boot] QueryDSL 사용하기. (0) | 2025.01.13 |