728x90
AOP를 활용한 로깅 구현: @EnableAspectJAutoProxy와 사용자 정의 Aspect 클래스 비교 가이드
스프링 부트 프로젝트에 로직의 이해를 돕기 위해 AOP를 로깅에 적용하는 방법에 대해서 설명해 보겠다.
1. 왜 AOP를 로깅에 사용하는가?
- 문제 인식
- 복잡한 애플리케이션에서는 다양한 프로세스와 로직이 수행된다. 이를 추적하고 이해하기 위해 로깅은 필수적이다.
- AOP의 역할
- AOP(Aspect-Oriented Programming)는 공통 관심 사항(예: 로깅, 보안, 트랜잭션 관리 등)을 애플리케이션의 핵심 로직으로부터 분리하여 관리할 수 있게 해 준다. 이를 통해 코드의 재사용성을 높이고, 유지보수를 용이하게 한다.
- 로깅에 AOP 적용
- 로깅은 여러 클래스와 메소드에 걸쳐 반복적으로 필요한 작업이다. AOP를 사용하면, 중복 코드 없이 로깅을 일관되게 적용할 수 있다.
2. @EnableAspectJAutoProxy 사용 방법과 특성
- 특성
- @EnableAspectJAutoProxy 어노테이션은 스프링 AOP를 사용할 수 있도록 활성화한다.
- 이는 AspectJ 프록시 기반의 AOP를 제공하며, 실행 시점에 동적으로 프록시를 생성한다.
- 적용 시점
- 프로젝트에서 스프링의 AOP 기능만을 사용하기 원할 때 적합하다.
- 설정이 간단하고, 스프링과의 호환성이 뛰어나다.
- 사용 방법
- 스프링 설정 클래스에 @EnableAspectJAutoProxy 어노테이션을 추가한다.
- Aspect 클래스를 정의하고, @Aspect 어노테이션을 클래스에 추가한다.
- 로깅을 수행할 메소드나 클래스에 @Before, @After, @Around, @Pointcut 등의 어드바이스 어노테이션을 사용한다.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
@Aspect
public class AopConfig {
@Before("execution(* com.yourpackage..*.*(..))")
public void logBefore() {
System.out.println("메소드 실행 전 로깅");
}
@After("execution(* com.yourpackage..*.*(..))")
public void logAfter() {
System.out.println("메소드 실행 후 로깅");
}
@Around("execution(* com.yourpackage..*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("메소드 실행 전 로깅");
Object result = joinPoint.proceed(); // 타겟 메소드 실행
System.out.println("메소드 실행 후 로깅");
return result;
}
}
💡 @Pointcut 어노테이션 사용하는 예시 코드는 아래 3. 사용자 정의 LoggingAspect 클래스 사용 방법과 특성에서 설명하겠다.
- logAround 메소드에서 파라미터로 받은 ProceedingJoinPoint joinPoint
- ProceedingJoinPoint 객체는 스프링 AOP 프레임워크에서 @Around 어드바이스에 자동으로 주입해 준다.
- 이 객체를 통해 타겟 메소드에 대한 다양한 정보에 접근할 수 있으며, 특히 proceed() 메소드를 호출하여 타겟 메소드를 실행할 수 있다.
- 개발자는 ProceedingJoinPoint 객체를 직접 생성하거나 관리할 필요가 없으며, 스프링 AOP 프레임워크가 자동으로 해당 객체를 어드바이스 메소드에 제공한다.
- 따라서 개발자는 이 객체를 사용하여 타겟 메소드의 실행 전후에 필요한 로직을 구현하면 된다.
- 간단히 말해, ProceedingJoinPoint 객체는 스프링 AOP가 어드바이스 메소드에 자동으로 주입하는 매개변수로, 타겟 메소드에 관한 정보와 제어 기능을 제공한다.
- joinPoint.proceed();
- 의미: ProceedingJoinPoint 객체의 proceed() 메소드는 @Around 어드바이스에서 타겟 메소드(즉, 어드바이스가 적용되는 실제 비즈니스 로직)를 실행시키는 역할을 한다.
- 작동 방식: proceed() 메소드를 호출하면, AOP 프레임워크는 타겟 메소드를 실행시킨다. 이 때 proceed() 메소드는 타겟 메소드의 반환값을 반환한다.
- 사용 이유: @Around 어드바이스는 타겟 메소드의 실행 전후로 추가적인 작업을 할 수 있게 해준다. proceed() 메소드를 호출함으로써, 어드바이스 안에서 타겟 메소드를 제어할 수 있다.
- return result;
- 의미: result 변수는 joinPoint.proceed();의 호출 결과, 즉 타겟 메소드의 반환값을 담고 있다.
- 반환 대상: @Around 어드바이스는 타겟 메소드의 결과를 변경하거나, 타겟 메소드 실행 전후에 추가적인 작업을 수행할 수 있다. return result;는 이 변경된 혹은 원래의 타겟 메소드의 반환값을 호출자에게 반환한다.
- 중요성: 타겟 메소드가 반환값을 가지고 있는 경우, 이 반환값을 어드바이스를 통해 다시 호출자에게 전달하는 것이 중요하다. 그렇지 않으면, 타겟 메소드의 반환값이 무시되어 예상치 못한 동작을 초래할 수 있다.
❗ return result; 항목에서 "중요성" 부분이 잘 이해가지 않을 수 있다. 더 상세하게 설명해 보겠다.
이 부분은 @Around 어드바이스에서 타겟 메소드의 실행 결과를 다시 호출자에게 반환하는 역할을 한다.
이 과정이 왜 중요한지를 이해하기 위해서는 AOP와 어드바이스의 동작 방식을 알아야 한다.
어드바이스에서 반환값의 역할
1. 타겟 메소드 실행
@Around 어드바이스 내에서 joinPoint.proceed();를 호출하면 타겟 메소드가 실행된다. 타겟 메소드가 반환값을 가진다면, joinPoint.proceed();는 그 값을 반환한다.
2. 어드바이스의 반환값
@Around 어드바이스는 타겟 메소드의 실행 결과를 받고, 이를 어드바이스 메소드의 반환값으로 다시 반환한다. 이 반환값은 타겟 메소드를 호출한 원래의 위치로 다시 전달된다.
반환값 전달의 중요성
1. 예상된 결과의 전달
타겟 메소드가 중요한 계산이나 데이터 조회를 수행하고 그 결과를 반환하는 경우, 이 결과는 후속 처리나 로직에 필요하다. 만약 어드바이스에서 이 반환값을 무시하고 다른 값을 반환하거나 아예 반환하지 않는다면, 타겟 메소드의 호출자는 예상치 못한 결과를 받게 된다.
2. 애플리케이션의 정확성 유지
반환값을 적절히 다루지 않으면 어플리케이션의 동작에 부정적인 영향을 줄 수 있다.
예를 들어, 데이터베이스 조회 결과, 계산 결과 등이 올바르게 전달되지 않으면, 잘못된 데이터 처리나 의도치 않은 오류가 발생할 수 있다.
예시
예를 들어, 어떤 타겟 메소드가 사용자 정보를 데이터베이스에서 조회하여 반환한다고 가정하자. @Around 어드바이스에서 이 메소드를 감싸 로깅을 수행하고 있다. 이 때, 타겟 메소드의 반환값을 @Around 어드바이스에서 그대로 반환하지 않는다면, 최종적으로 사용자 정보를 요청한 부분은 필요한 정보를 받지 못하게 된다. 따라서 어드바이스에서 타겟 메소드의 원래 반환값을 그대로 다시 반환하는 것이 중요하다.
3. 사용자 정의 LoggingAspect 클래스 사용 방법과 특성
- 특성
- 개발자가 직접 Aspect 클래스를 정의하여 로깅 로직을 세밀하게 제어할 수 있다.
- 더 복잡하고 특정한 로깅 요구 사항을 충족시킬 수 있다.
- 적용 시점
- 표준 스프링 AOP 기능으로는 해결할 수 없는 복잡한 로깅 요구 사항이 있을 때 적합하다.
- 로그 수준, 출력 형식, 특정 조건에서의 로깅 등을 사용자가 정의할 수 있다.
- 사용 방법
- @Aspect 어노테이션을 사용해 Aspect 클래스를 정의한다.
- 포인트컷을 사용해 로깅 대상을 정의하고, @Before, @After, @Around, @Pointcut 등의 어드바이스로 로직을 구현한다.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
// 포인트컷 정의
@Pointcut("execution(* com.yourpackage..*.*(..))")
public void applicationPackagePointcut() {
// 포인트컷 시그니처
// 본문을 비워 두는 것이 일반적
}
// 메소드 실행 전 로깅
@Before("applicationPackagePointcut()")
public void logBefore() {
System.out.println("메소드 실행 전 로깅");
}
// 메소드 실행 후 로깅
@After("applicationPackagePointcut()")
public void logAfter() {
System.out.println("메소드 실행 후 로깅");
}
// 메소드 실행 전후 로깅
@Around("applicationPackagePointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("메소드 실행 전 로깅");
Object result = joinPoint.proceed(); // 타겟 메소드 실행
System.out.println("메소드 실행 후 로깅");
return result;
}
}
- 포인트컷 정의
- @Pointcut 어노테이션을 사용하여 로깅을 적용할 메소드의 패턴을 정의한다.
- 위의 예시에서는 com.yourpackage 패키지 아래의 모든 메소드를 대상으로 한다.
- 본문 작성할 때 이 메소드의 본문은 비워두어야 한다.
- @Pointcut 어노테이션으로 정의된 메소드는 포인트컷 표현식을 지정하는 역할만 하며, 실제 실행 로직은 포함하지 않는다.
그리고 사용자 정의 Aspect는 표준 AOP 설정보다 더 많은 유연성과 세밀한 제어를 가능하게 한다.
몇 가지 예시를 들어보겠다.
조건부 로깅
- 예를 들어, 개발 환경에서는 모든 메소드 호출을 로깅하고, 프로덕션 환경에서는 특정 레벨 이상의 중요도를 가진 메소드만 로깅하고 싶은 경우, 사용자 정의 Aspect를 통해 이를 구현할 수 있다.
- 포인트컷 표현식을 사용하여 특정 조건(예: 메소드 이름, 메소드 파라미터)에 맞게 로깅 로직을 적용할 수 있다.
@Aspect
@Component
public class ConditionalLoggingAspect {
@Before("execution(* com.yourpackage..*.*(..)) && @annotation(devLog)")
public void logInDevelopment(JoinPoint joinPoint, DevLog devLog) {
if (isDevelopmentEnvironment()) {
System.out.println("개발 환경에서 메소드 실행 전 로깅: " + joinPoint.getSignature());
}
}
@Before("execution(* com.yourpackage..*.*(..)) && @annotation(prodLog)")
public void logInProduction(JoinPoint joinPoint, ProdLog prodLog) {
if (isProductionEnvironment() && prodLog.level() >= REQUIRED_LEVEL) {
System.out.println("프로덕션 환경에서 중요도 높은 메소드 실행 전 로깅: " + joinPoint.getSignature());
}
}
// 환경 확인 메소드 (실제 구현은 환경에 맞게 조정)
private boolean isDevelopmentEnvironment() {
// 개발 환경 확인 로직
return true;
}
private boolean isProductionEnvironment() {
// 프로덕션 환경 확인 로직
return false;
}
}
- 위 코드 설명
- 이 코드는 @Before 어드바이스를 사용하여 개발 환경과 프로덕션 환경에서 다른 로깅 전략을 적용한다.
- 개발 환경에서는 모든 메소드를 로깅하고, 프로덕션 환경에서는 특정 중요도 레벨 이상의 메소드만 로깅한다.
- @annotation 포인트컷 디자인에이터를 사용하여 메소드에 특정 어노테이션이 적용된 경우에만 로깅하도록 설정한다.
메소드 실행 시간 로깅
- 메소드의 실행 시간을 측정하고 이를 로깅하고 싶은 경우, @Around 어드바이스를 사용하여 메소드 실행 전후의 시간을 기록할 수 있다.
- 이를 통해 성능 분석 및 모니터링에 유용한 데이터를 제공할 수 있다.
@Aspect
@Component
public class PerformanceLoggingAspect {
@Around("execution(* com.yourpackage..*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
System.out.println(joinPoint.getSignature() + " 실행 시간: " + (endTime - startTime) + "ms");
return result;
}
}
- 위 코드 설명
- @Around 어드바이스를 사용하여 메소드의 실행 전후로 시간을 측정하고, 실행 시간을 로깅한다.
- 이를 통해 애플리케이션의 성능 모니터링에 도움이 되는 데이터를 제공할 수 있다.
매개변수 및 반환값 기반 로깅
- 특정 메소드의 매개변수나 반환값에 따라 다른 로깅 로직을 적용하고 싶은 경우, 사용자 정의 Aspect를 이용하여 이를 구현할 수 있다.
- 예를 들어, 특정 유형의 매개변수를 받는 메소드나 특정 조건을 만족하는 반환값을 가진 메소드에 대해 추가적인 로깅 처리를 할 수 있다.
@Aspect
@Component
public class ParameterAndReturnLoggingAspect {
@Before("execution(* com.yourpackage..*.*(..)) && args(param,..)")
public void logMethodParameter(JoinPoint joinPoint, Object param) {
System.out.println(joinPoint.getSignature() + " 호출됨, 매개변수: " + param);
}
@AfterReturning(pointcut = "execution(* com.yourpackage..*.*(..))", returning = "result")
public void logMethodReturn(JoinPoint joinPoint, Object result) {
System.out.println(joinPoint.getSignature() + " 반환값: " + result);
}
}
- 위 코드 설명
- @Before 어드바이스를 사용하여 메소드 호출 시 매개변수를 로깅하고, @AfterReturning 어드바이스를 사용하여 메소드의 반환값을 로깅한다.
예외 발생 시 특별한 로깅
- 메소드에서 발생하는 특정 유형의 예외에 대해 상세한 로그를 남기고자 할 때, @AfterThrowing 어드바이스를 사용하여 예외 정보와 함께 로깅할 수 있다.
- 이를 통해 오류 분석 및 디버깅에 필요한 상세한 정보를 제공할 수 있다.
@Aspect
@Component
public class ExceptionLoggingAspect {
@AfterThrowing(pointcut = "execution(* com.yourpackage..*.*(..))", throwing = "ex")
public void logException(JoinPoint joinPoint, Throwable ex) {
System.out.println(joinPoint.getSignature() + "에서 예외 발생: " + ex.getMessage());
}
}
- 위 코드 설명
- @AfterThrowing 어드바이스를 사용하여 메소드 실행 중 발생하는 예외를 로깅한다.
- 이 방법은 오류 분석 및 디버깅에 유용한 정보를 제공한다.
이러한 예시들은 표준 AOP 설정으로는 다루기 어려운 복잡하고 특정한 로깅 요구사항을 충족시키는 방법을 보여준다.
사용자 정의 Aspect를 사용하면 이러한 고급 로깅 요구사항을 보다 쉽게 해결할 수 있다.
4. 마무리
결론적으로, 두 방법의 기능적 차이는 크지 않으나, 프로젝트의 요구사항과 개발 환경에 따라 더 적합한 방법을 선택할 수 있다. 단순하고 빠른 설정을 원한다면 @EnableAspectJAutoProxy를, 더 복잡하고 세밀한 로깅 로직을 원한다면 사용자 정의 LoggingAspect 클래스를 선택하는 것이 좋을 것 같다.
이번 글을 작성하며 로깅 기능의 다양성과 유연성에 대해 새롭게 인식하게 되었다. 이번 스터디를 진행하면서 이러한 다양한 기능들을 실험해 보는 것이 흥미롭고 유익할 것 같다.
이 글이 다른 개발자들에게도 로깅의 깊이와 넓이를 탐색하는 데 도움이 되기를 바란다. 스프링 AOP와 로깅의 세계는 탐험할 가치가 충분한 영역인 것 같다.
'Spring > Spring Boot' 카테고리의 다른 글
[Spring Boot] Spring Framework 4.2 이후 버전에서 스프링 이벤트 적용하기 (SpringBoot 3 버전에 spring event 적용하기) (1) | 2023.11.15 |
---|---|
[Spring Boot] 스프링 부트에서 파라미터화된 로깅{}과 String.format 사용법 (0) | 2023.11.15 |
[Spring Boot] Quartz 적용하기 (2) | 2023.11.11 |
[Spring Boot] 스프링 스케줄러 적용하기 (0) | 2023.11.11 |
[Spring Boot] Spring Batch, Spring Scheduler, Quartz의 차이 (0) | 2023.11.11 |