AOP란?
- 핵심 비즈니스 로직과 부가적인 관심사(로깅, 트랜잭션, 보안 등)를 분리하여 모듈화하는 프로그래밍 기법
- 관심사의 분리(Separation of Concerns, SoC)를 통해 코드의 재사용성, 유지보수성을 높인다
AOP의 필요성
- 로깅, 예외 처리, 트랜잭션 관리 등은 여러 클래스에서 반복적으로 등장한다.
- 이를 각 클래스에 흩뿌려 두면 코드가 중복되고, 수정이 어려워진다.
- AOP를 사용하면 공통 기능을 한 곳에 모아 관리할 수 있다.
AOP 핵심 용어
- Aspect: 공통 기능(로깅, 트랜잭션 등)을 모듈화한 단위
- Join Point: Advice가 적용될 수 있는 지점(메서드 실행 등)
- Pointcut: Join Point 중 실제 Advice가 적용될 지점을 지정
- Advice: 언제(언제 실행되는지: before/after 등) 어떤 기능을 실행할지 정의
- Weaving: Advice를 실제 대상 코드에 적용하는 과정
@Aspect
@Component
public class LoggingAspect {
// Pointcut: MyService 클래스의 모든 메서드 실행 시점 (Join Point 대상)
@Pointcut("execution(* com.example.service.MyService.*(..))")
public void myServiceMethods() {}
// Advice: Join Point 실행 '전'에 실행될 코드
@Before("myServiceMethods()")
public void beforeAdvice(JoinPoint joinPoint) {
// Join Point: 핵심 비즈니스 메서드가 호출되는 시점(joinPoint.getSignature())
System.out.println("[Before] " + joinPoint.getSignature().getName() + " 메서드 실행 전");
}
// Advice: Join Point 실행 '후'에 실행될 코드
@AfterReturning(pointcut = "myServiceMethods()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
System.out.println("[AfterReturning] " + joinPoint.getSignature().getName() + " 메서드 실행 후, 반환값: " + result);
}
}
// 핵심 비즈니스 클래스
public class MyService {
public String doSomething() {
System.out.println("핵심 비즈니스 로직 실행");
return "결과값";
}
}
아래는 실제 구현 코드 X, 어떻게 동작하는지 표현한 것
- @SpringBootApplication 어노테이션을 사용하면
- 스프링 부트가 자동으로 ApplicationContext를 생성하고, 빈(Bean) 등록, 의존성 주입, AOP 프록시 생성(Weaving) 등도 모두 자동으로 처리한다.
// 스프링 애플리케이션 실행 코드
public class Application {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// Weaving(위빙)된 프록시 객체를 받아온다.
// 실제 MyService 객체가 아니라 프록시 객체다.
MyService myService = context.getBean(MyService.class);
// 아래 호출 시점이 Join Point
// 프록시가 Advice 코드를 끼워 넣어 Before, AfterReturning이 실행된다.
String result = myService.doSomething();
System.out.println("최종 반환값: " + result);
}
}
Pointcut 지정하는 법
execution 표현식
- 형식: execution([접근제어자] [반환타입] [패키지.클래스.메서드](매개변수))
- public → 접근제어자
- * → 모든 반환 타입
- com.example.service.* → service 패키지 내 모든 클래스
- .*(..) → 모든 메서드 이름, 모든 매개변수
@Aspect
@Component
public class LoggingAspect {
// com.example.service 패키지 내 모든 public 메서드 대상
@Pointcut("execution(public * com.example.service.*.*(..))")
public void allPublicServiceMethods() {}
@Before("allPublicServiceMethods()")
public void beforeServiceMethods(JoinPoint joinPoint) {
System.out.println("[Before] 호출 메서드: " + joinPoint.getSignature());
}
}
@annotation 표현식
- 형식: @annotation(어노테이션클래스)
- 타겟 메서드에 특정 노테이션이 붙어 있어야 한다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}
@Aspect
@Component
public class ExecutionTimeAspect {
// @LogExecutionTime 붙은 메서드 대상
@Pointcut("@annotation(com.example.LogExecutionTime)")
public void annotatedWithLogExecutionTime() {}
@Around("annotatedWithLogExecutionTime()")
public Object measureExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object retVal = pjp.proceed();
long end = System.currentTimeMillis();
System.out.println("[ExecutionTime] " + pjp.getSignature() + " 수행 시간: " + (end - start) + "ms");
return retVal;
}
}
@Service
public class UserService {
@LogExecutionTime
public void createUser() {
// 로직 수행
}
}
Advice 종류
- @Before: 대상 메서드 실행 전에 수행
- @After: 대상 메서드 실행 후 (성공/예외와 무관하게) 수행
- @AfterReturning: 대상 메서드가 정상적으로 실행된 후 수행
- @AfterThrowing: 예외가 발생한 경우 수행
- @Around: 대상 메서드 실행 전/후를 모두 감싸서 제어
@Before
- 용도: 메서드 실행 전에 수행할 작업 (예: 로그 기록, 권한 체크)
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint jp) {
System.out.println("[Before] 메서드 호출 전: " + jp.getSignature());
}
@After
- 용도: 메서드 실행 후 무조건 실행 (성공, 예외 구분 없음)
@After("execution(* com.example.service.*.*(..))")
public void afterAdvice(JoinPoint jp) {
System.out.println("[After] 메서드 호출 후: " + jp.getSignature());
}
@AfterReturning
- 용도: 정상적으로 메서드가 리턴된 후 실행 (예외 발생 시 실행 안 됨)
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturningAdvice(JoinPoint jp, Object result) {
System.out.println("[AfterReturning] 메서드 정상 리턴: " + jp.getSignature() + ", 반환값: " + result);
}
@AfterThrowing
- 용도: 메서드가 예외를 던진 후 실행
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void afterThrowingAdvice(JoinPoint jp, Throwable ex) {
System.out.println("[AfterThrowing] 예외 발생: " + jp.getSignature() + ", 예외: " + ex);
}
@Around
- 용도: 메서드 실행 전후 모두 제어 가능, 실행 흐름 직접 조작 가능
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("[Around] 메서드 실행 전: " + pjp.getSignature());
Object result = pjp.proceed(); // 타겟 메서드 호출
System.out.println("[Around] 메서드 실행 후: " + pjp.getSignature());
return result;
}
실무 적용 사례
- 서비스 로깅: 모든 서비스 메서드 호출 로그 남기기
- 트랜잭션 처리: @Transactional이 내부적으로 AOP 활용
- 보안 처리: 메서드 접근 권한 체크
- 성능 측정: 실행 시간 측정
주의할 점
- 프록시 기반의 한계: 내부 메서드 호출은 AOP가 적용되지 않는다.
- private 메서드에는 적용되지 않음
- 순환 참조 주의: Aspect가 빈 간의 의존성을 만들 때 주의
'Spring > 문법' 카테고리의 다른 글
@EntityGraph - fetch join을 어노테이션으로 처리하기 (0) | 2025.06.11 |
---|---|
테스트 커버리지 - JaCoCo 설정 (3) | 2025.06.11 |
테스트 코드 개념 2 + Mock 을 이용한 테스트 코드 작성 예제 (0) | 2025.06.11 |
테스트 코드 개념 (0) | 2025.06.11 |
JSON 포맷 변경하기: Jackson 설정 커스터마이징 (1) | 2025.05.23 |