애플리케이션을 개발하다 보면 여러 모듈에 걸쳐 반복적으로 적용해야 하는 기능들이 있습니다. 로깅, 보안, 트랜잭션 관리 등이 대표적인 예시죠. 이러한 기능들은 비즈니스 로직과 직접적인 관련이 없음에도 불구하고 코드 곳곳에 흩어져 있어, 핵심 로직의 가독성을 해치고 유지보수를 어렵게 만듭니다. 스프링 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 이러한 “횡단 관심사(Cross-cutting Concerns)”를 깔끔하게 분리하여 코드의 모듈성, 가독성, 유지보수성을 극대화하는 강력한 방법론입니다. 이번 포스팅에서는 스프링 AOP가 무엇인지, 어떻게 적용하는지에 대해 알아보겠습니다.
AOP는 흩어진 부가 기능(횡단 관심사)들을 관점(Aspect)이라는 별도의 모듈로 분리하여 관리하는 프로그래밍 패러다임입니다. 이를 통해 핵심 비즈니스 로직은 자신의 역할에만 집중하고, 부가 기능은 필요한 시점에 자동으로 적용되도록 할 수 있습니다. 스프링 AOP는 프록시 기반으로 동작하며, 주로 메서드 실행 시점을 가로채서 부가 기능을 주입하는 방식으로 구현됩니다.
핵심 용어는 다음과 같습니다.
@Aspect 어노테이션으로 정의하며, 어드바이스(Advice)와 포인트컷(Pointcut)의 조합으로 이루어집니다.@Before, @After, @Around 등으로 정의합니다.execution(* com.example..*.*(..))와 같이 정규식 형태로 정의합니다.이제 간단한 로깅 기능을 AOP로 구현하는 예제를 통해 스프링 AOP의 동작 방식을 살펴보겠습니다.
먼저, 간단한 서비스 클래스를 정의합니다.
// src/main/java/com/example/service/MyService.java
package com.example.service;
import org.springframework.stereotype.Service;
@Service
public class MyService {
public String performTask(String data) {
System.out.println("[Service] Executing performTask with data: " + data);
if (data.contains("error")) {
throw new IllegalArgumentException("Error data detected!");
}
return "Processed: " + data.toUpperCase();
}
public void doSomethingElse() {
System.out.println("[Service] Executing doSomethingElse.");
}
}
다음으로, 로깅 기능을 담당할 애스펙트를 생성합니다.
// src/main/java/com/example/aop/LoggingAspect.java
package com.example.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect // 이 클래스가 애스펙트임을 선언
@Component // 스프링 빈으로 등록
public class LoggingAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
// com.example.service 패키지 내의 모든 클래스, 모든 메서드에 대한 포인트컷 정의
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
// serviceMethods() 포인트컷이 지정하는 메서드 실행 전
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
logger.info("----> [Before] Method: {} arguments: {}",
joinPoint.getSignature().toShortString(),
joinPoint.getArgs());
}
// serviceMethods() 포인트컷이 지정하는 메서드 성공적 실행 후 (리턴 값 있을 때)
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
logger.info("<---- [AfterReturning] Method: {}, result: {}",
joinPoint.getSignature().toShortString(), result);
}
// serviceMethods() 포인트컷이 지정하는 메서드 실행 후 (예외 발생 여부와 관계없이)
@After("serviceMethods()")
public void logAfter(JoinPoint joinPoint) {
logger.info("----- [After] Method: {} finished.",
joinPoint.getSignature().toShortString());
}
// serviceMethods() 포인트컷이 지정하는 메서드 실행 중 예외 발생 시
@AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {
logger.error("!!!!! [AfterThrowing] Method: {}, exception: {}",
joinPoint.getSignature().toShortString(), ex.getMessage());
}
// serviceMethods() 포인트컷이 지정하는 메서드 실행 전/후를 모두 처리 (성능 측정 등에 유용)
@Around("serviceMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().toShortString();
logger.info(">>>>> [Around] Entering method: {}", methodName);
Object result = null;
try {
result = joinPoint.proceed(); // 원본 메서드 실행
} catch (Throwable e) {
logger.error("<<<<< [Around] Exception in method: {}, error: {}", methodName, e.getMessage());
throw e;
} finally {
long endTime = System.currentTimeMillis();
logger.info("<<<<< [Around] Exiting method: {}, execution time: {}ms", methodName, (endTime - startTime));
}
return result;
}
}
마지막으로, 스프링 부트 애플리케이션에 AOP를 활성화해야 합니다. 보통 @SpringBootApplication에 @EnableAspectJAutoProxy 어노테이션을 추가하거나, 설정 파일에 <aop:aspectj-autoproxy/>를 선언하여 활성화합니다. 스프링 부트에서는 spring-boot-starter-aop 의존성만 추가하면 별도의 설정 없이 자동으로 활성화되는 경우가 많습니다.
// src/main/java/com/example/DemoApplication.java
package com.example;
import com.example.service.MyService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy // AOP 자동 프록시 생성 활성화 (starter-aop 포함 시 생략 가능)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public CommandLineRunner run(MyService myService) {
return args -> {
System.out.println("\n--- Calling performTask normally ---");
myService.performTask("Hello AOP");
System.out.println("\n--- Calling doSomethingElse ---");
myService.doSomethingElse();
System.out.println("\n--- Calling performTask with error ---");
try {
myService.performTask("contains error data");
} catch (IllegalArgumentException e) {
System.out.println("Caught expected exception: " + e.getMessage());
}
};
}
}
위 코드를 실행하면 MyService의 메서드가 호출될 때마다 LoggingAspect에 정의된 로깅 어드바이스들이 자동으로 실행되는 것을 확인할 수 있습니다.
스프링 AOP는 핵심 비즈니스 로직과 횡단 관심사를 명확하게 분리하여 코드의 모듈성을 높이고, 유지보수성을 크게 향상시킵니다. 로깅, 보안, 트랜잭션 관리 등 반복적인 부가 기능들을 애스펙트 하나로 관리함으로써 개발자는 핵심 기능 구현에 더욱 집중할 수 있게 됩니다. 깔끔하고 확장 가능한 아키텍처를 구축하고자 한다면 스프링 AOP는 필수적인 도구입니다. 여러분의 프로젝트에도 AOP를 적용하여 더욱 견고하고 관리하기 쉬운 애플리케이션을 만들어 보세요!
Text by Chaelin & Gemini. Photographs by Chaelin, Unsplash.