AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)은
공통된 부가 기능을 모듈화하여 핵심 비즈니스 로직과 분리하는 프로그래밍 기법을 말한다.
Spring 프레임워크에서 매우 자주 사용되며, 특히
등과 같은 횡단 관심사(cross-cutting concerns)를 분리하는 데 사용된다.
| 용어 | 설명 |
|---|---|
| Aspect (관점) | 공통 기능(예: 로깅, 트랜잭션)을 모듈화한 단위 |
| Join Point (조인 포인트) | Advice(공통 로직)가 적용될 수 있는 코드 실행 지점 (ex. 메서드 호출 시점) |
| Pointcut (포인트컷) | Advice를 실제로 적용할 대상 메서드를 지정하는 표현식 |
| Advice (어드바이스) | 실제로 실행되는 공통 기능 코드 (ex. 메서드 실행 전/후에 로그 출력) |
| Weaving (위빙) | 핵심 로직 코드에 공통 관심사를 삽입하는 과정 (Spring에서는 런타임 프록시 기반) |
| 상황 | 설명 |
|---|---|
| 로깅 (Logging) | 모든 API 요청/응답 로그 기록 |
| 트랜잭션 관리 | 메서드 실행 전 트랜잭션 시작 → 정상 종료 시 커밋, 예외 시 롤백 |
| 성능 측정 | 메서드 실행 시간 측정 |
| 보안 검사 | 메서드 실행 전 권한 체크 |
| 예외 처리 | 공통 예외 로깅 및 응답 포맷 통일 |
| 구분 | 내용 |
|---|---|
| 핵심 목적 | 공통 관심사를 분리하여 코드 중복 제거 |
| 핵심 구성요소 | Aspect, Advice, Pointcut, JoinPoint, Weaving |
| 구현 방법 | 프록시 기반 런타임 AOP |
| 주요 사용 예시 | 로깅, 트랜잭션, 보안, 성능 측정 |
| 핵심 어노테이션 | @Aspect, @Around, @Before, @After 등 |
| Advice 유형 | 실행 시점 | 설명 |
|---|---|---|
@Before | 메서드 실행 이전 | 실행 전 로깅, 인증 검사 등에 사용 |
@AfterReturning | 메서드가 정상 종료 후 | 결과값 로깅, 리턴 데이터 후처리 |
@AfterThrowing | 예외 발생 시 | 예외 로깅, 에러 전송 처리 |
@After | 무조건 실행 | finally 블록 역할 |
@Around | 메서드 전/후 모두 제어 | 가장 강력한 형태, 메서드 실행 자체를 감쌈 (ProceedingJoinPoint) |
@Aspect
@Component
public class LoggingAspect {
// 서비스 계층의 모든 메서드 실행 전/후에 적용
@Around("execution(* com.example.service.*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("[AOP] 실행 메서드: " + joinPoint.getSignature());
Object result = joinPoint.proceed(); // 실제 메서드 실행
long end = System.currentTimeMillis();
System.out.println("[AOP] 실행 시간: " + (end - start) + "ms");
return result;
}
}
Pointcut 표현식 예시:
execution(* com.example..*(..)) : 특정 패키지 이하 모든 메서드@annotation(org.springframework.transaction.annotation.Transactional) : 특정 어노테이션이 붙은 메서드within(@org.springframework.stereotype.Service *) : @Service가 붙은 클래스 내 모든 메서드Spring은 프록시(Proxy) 기반의 AOP를 사용한다.
→ 런타임 시점에 프록시 객체를 생성하여 메서드 호출 전후에 부가 로직을 삽입한다.
Client → Proxy(부가기능 + 실제 객체) → Target Bean
예시 흐름:
Spring은 기본적으로 두 가지 프록시 전략을 사용한다:
proxyTargetClass=true 설정 시)// build.gradle
dependencies {
implementation "org.springframework.boot:spring-boot-starter-aop"
}
package com.example.demo.common.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class LoggingAspect {
// Controller, Service 패키지 모두 적용
@Around("execution(* com.example.demo..controller..*(..)) || execution(* com.example.demo..service..*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
long startTime = System.currentTimeMillis();
log.info("▶ START [{}#{}] args={}", className, methodName, joinPoint.getArgs());
Object result = null;
try {
result = joinPoint.proceed(); // 실제 메서드 실행
return result;
} finally {
long endTime = System.currentTimeMillis();
log.info("◀ END [{}#{}] time={}ms result={}", className, methodName, (endTime - startTime), result);
}
}
}
package com.example.demo.controller;
import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public String getUser(@PathVariable Long id) {
return userService.getUserInfo(id);
}
}
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public String getUserInfo(Long id) {
// DB 조회 로직 대신 단순 예시
return "User#" + id;
}
}
GET /users/10
INFO ▶ START [UserController#getUser] args=[10]
INFO ▶ START [UserService#getUserInfo] args=[10]
INFO ◀ END [UserService#getUserInfo] time=5ms result=User#10
INFO ◀ END [UserController#getUser] time=7ms result=User#10
| 구분 | Spring AOP | AspectJ AOP |
|---|---|---|
| 설명 | Spring 프레임워크에서 제공하는 프록시 기반 AOP 구현체 | Java 언어 수준에서 지원되는 컴파일/바이트코드 조작 기반 AOP 프레임워크 |
| 제공 주체 | Spring Framework | Eclipse 재단 (AspectJ 프로젝트) |
| 핵심 목적 | 런타임 프록시로 빠르고 간편하게 부가 기능 적용 | 더 정밀하고 강력한 AOP 제어 (모든 join point 지원) |
| 항목 | Spring AOP | AspectJ AOP |
|---|---|---|
| Weaving(위빙) 시점 | 런타임 (Runtime) | 컴파일/로딩 시점 (Compile-time, Load-time) |
| Weaving 방식 | 프록시 객체 생성 (Proxy 기반) | 바이트코드 조작 (Bytecode Instrumentation) |
| 필요한 설정 | Spring Bean에 @Aspect 붙이면 끝 | 별도의 컴파일러(ajc) 또는 LTW(Load-Time Weaving) 설정 필요 |
| 구분 | Spring AOP | AspectJ AOP |
|---|---|---|
| 지원 Join Point | 메서드 실행(execution())만 가능 | 생성자, 필드 접근, static 블록, 예외 처리 등 모든 join point 지원 |
| 적용 대상 | Spring IoC 컨테이너에서 관리되는 Bean | 모든 Java 객체 (일반 POJO 포함) |
예를 들면,
@Service, @Repository 등 Bean에만 적용됨new 키워드, 필드 접근(get/set), 클래스 초기화 등에도 적용 가능| 구분 | Spring AOP | AspectJ AOP |
|---|---|---|
| 설정 난이도 | 간단 (@Aspect + @EnableAspectJAutoProxy) | 복잡 (ajc 컴파일러, LTW 설정 필요) |
| 의존성 | Spring AOP 라이브러리만으로 충분 | AspectJ Runtime + Compiler 필요 |
| 프록시 기반 | ✅ (JDK Dynamic Proxy / CGLIB) | ❌ (바이트코드 직접 조작) |
| 런타임 오버헤드 | 약간 있음 (프록시 객체 경유) | 거의 없음 (직접 바이트코드 변경) |
| 디버깅 난이도 | 쉬움 | 상대적으로 어려움 (바이트코드 변환으로 인한 추적 어려움) |
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Spring AOP 실행 전: " + joinPoint.getSignature());
}
}
@Aspect
public class LoggingAspect {
@Before("execution(* com.example..*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("AspectJ 실행 전: " + joinPoint.getSignature());
}
}
추가 설정 필요 (예: Maven 플러그인)
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</version>
<configuration>
<complianceLevel>17</complianceLevel>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
.class 파일을 직접 변환해 AOP 삽입| 항목 | Spring AOP | AspectJ AOP |
|---|---|---|
| 구현 방식 | Proxy 기반 (JDK / CGLIB) | Bytecode Weaving |
| 적용 시점 | 런타임 | 컴파일 / 로드타임 |
| 지원 Join Point | 메서드 실행만 | 모든 Join Point |
| 적용 대상 | Spring Bean만 | 모든 Java 객체 |
| 성능 | 상대적으로 느림 | 빠름 |
| 설정 난이도 | 쉬움 | 복잡함 |
| 주요 사용 목적 | 트랜잭션, 로깅, 보안 등 간단한 AOP | 성능 분석, 시스템 단위 로깅 등 고급 AOP |
| 상황 | 추천 |
|---|---|
| Spring 기반 애플리케이션 | ✅ Spring AOP (기본 제공, 간단, 유지보수 용이) |
| 비-Spring 환경 or 정교한 Join Point 필요 | ⚙️ AspectJ AOP (성능 최적화, 깊은 수준의 AOP 제어 필요 시) |