vs OOP(Object Oriented Programming)
: 객체 지향 프로그래밍, 객체로 정의
AOP(Aspect Oriented Programming)
: 관점 지향 프로그래밍, 프로그래밍을 관심사로 분리하여 모듈로 사용

핵심기능: 메서드의 주요 기능
부가기능: 주요기능이 아닌 부가기능
횡단관심사: 여러 곳에서 공통적으로 발생하는 부가기능
ex) 수업 생성 서비스가 있는데 메서드가 작동하는 시간 측정을 해보자고 한다.
/**
* 수업 생성 서비스
*/
public CourseCreateResponseDto createCourse(CourseCreateRequestDto data) {
// 측정 시작
long startTime = System.currentTimeMillis();
try {
// 검증: 수업 이름이 중복 확인
boolean flag = courseRepository.existsByNameAndIsDeleted(data.getName(), false);
if (flag) {
throw new DuplicateCourseNameException();
}
// 생성: 수업(course) 엔티티
Course newCourse = Course.createNewCourse(data.getName());
// 저장: 코스 저장
Course savedCourse = courseRepository.save(newCourse);
// 반환
return new CourseCreateResponseDto(savedCourse.getId());
} finally {
// 측정 완료
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
log.info("::: ExecutionTime: {}ms", executionTime);
}
}
그런데,
다른 메서드도 측정하고자 한다면?
시간 측정 단위가 변경된다면?
시간 측정 로직에 새로운 기능이 추가된다면?
ex) Advice 예시: 시간측정, 로깅, 인증인가 처리, 트렌젝션 처리
ex) PointCut 예시: 패키지 내의 모든 메서드, 특정 클래스 내의 모든 메서드, 특정 어노테이션

Target은 AOP에서 Advice가 적용되는 객체이다. 예를 들어, PointCut으로 적용된 특정 클래스가 Target이 되면, 그 클래스 내의 모든 메서드들이 Advice의 적용을 받는다.
execution(int createCourse(int, int)) // 반환타입 int, createCourse 이라는 매서드, 매개변수는 int 형 2개
execution(* createCourse(int, int)) // 반환타입 상관없음, createCourse 이라는 매서드, 매개변수는 int 형 2개
execution(* createCourse(..)) // 반환타입 상관없음, createCourse 이라는 매서드, 매개변수 상관없음
execution(* *(..)) // 반환타입 상관없음, 메서드이름 상관없음, 매개변수 상관없음

JoinPoint는 Advice(횡단관심사)가 적용될수 있는 실행지점을 의미한다. 메서드 기준으로 Advice를 동작시킴으로 메서드들이 JointPoint가 된다.
Aspect는 Advice(횡단관심사)와 PointCut(횡단관심사를 어디에 적용할지)를 하나로 묶은 module이다.


Advice는 반복되는 횡단관심사를 정의해 놓은곳이다.
그 횡단관심사를 어느 범위에 적용할지 선택하는 것이 PointCut이다.
그리고 그 범위 안에서 선택받은 객체들 혹은 객체가 Target이 되는 것이다.
그 타겟 내에서 Advice를 실제로 실행되는 시점을 JoinPoint라고 한다.
package com.standard.sparta.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Slf4j
@Aspect
/*
* Aspect : Advice와 PointCut을 하나로 묶은 Module
*/
public class AspectPractice {
// PointCut
/**
* PointCut: Service package 기반
*/
@Pointcut("execution(* com.standard.sparta.service..*(..))")
private void serviceLayer() {} // serviceLayer() 메서드는 PointCut을 식별하는 역할을 한다. 이 메서드를 다른 Advice 메서드에서 참조해서 이 PointCut을 사용할 수 있다.
// execution
// : 메서드 실행 지점을 PointCut으로 지정
//
// *
// : 리턴 타입, 모든 리턴 타입
// com.standard.sparta.service..
// - com.standard.sparta.service: 패키지 경로
// - .. : 하위 패키지를 포함 및 하위 패키지의 모든 클래스를 포함
//
// *(..)
// - * : 메서드 이름, 모든 메서드 뜻함
// - (..): 메서드의 매개변수, ..은 매개변수 0개 이상일 수 있음을 의미.
/**
* Advice: @Before
* 메서드 실행 전에 수행되는 로직을 처리할때 사용한다.
*/
@Before("serviceLayer()")
public void beforeMethod() {
log.info("::: BEFORE 실행 :::");
}
/**
* Advice: @AfterReturning
* JoinPoint(메서드)가 정상적으로 반환된 후에 실행된다.
* 예외가 발생하지 않고 정상적으로 결과값을 반환했을때만 동작한다.
* @param result 성공시 반환
* 즉, 예외가 발생하지 않고 정상적으로 실행되었다면, CourseListResponseDto 타입의 반환 값이 result에 담긴다.
*/
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void afterReturningMethod(Object result) {
log.info("::: AFTER RETURNING :::" + result);
}
/**
* Advice: @AfterThrowing
* JoinPoint(메서드) 실행 중 예외가 발생했을 때만 실행된다.
* @param joinPoint
* @param ex 발생환 예외 객체
*/
@AfterThrowing(pointcut="serviceLayer()", throwing="ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
// 서비스에서 발생환 예외 ex.getMessage()
log.info("::: AFTER THROWING :::" + ex.getMessage());
}
/**
* Advice: @After
* JoinPoint(메서드)가 정상적으로 실행되던 예외가 발생하던 메서드가 완료된 후에 항상 실행된다.
*/
@After("serviceLayer()")
public void afterMethod(JoinPoint joinPoint) {
log.info("::: AFTER ::: {}", joinPoint);
// jointPoint: getCourseList()이다.
}
/**
* Advice: @Around
* 가장 강력한 어드바이스, 전체 흐름을 제어할 수 있는 어드바이스
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("serviceLayer()") // 어노테이션 기반으로 포인트컷 설정
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("::: BEFORE :::");
try {
Object result = joinPoint.proceed(); // CourseListResponseDt
System.out.println(result);
log.info("::: AFTER RETURNING ::: ");
return result;
} catch(Exception e) {
log.info("::: AFTER THROWING :::");
throw e;
} finally {
log.info("::: AFTER :::");
}
}
/**
* 어드바이스: 패키지 범위 기반
*/
@Around("serviceLayer()")
public Object advicePackage(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
log.info("::: ExecutionTime: {}ms", executionTime);
}
}
// Annotation 기반
@Pointcut("@annotation(com.standard.sparta.aop.annotation.TrackTime)")
private void trackTimeAnnotation() {}
@Around(("trackTimeAnnotation()"))
public Object adviceAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
// 측정 시작
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
return result;
} finally {
// 측정 완료
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
log.info("::: ExecutionTime: {}ms", executionTime);
}
}
}

스프링에서 객체를 빈으로 등록할 때 빈후처리기를 거친다. 이때, 빈후처리기는 AOP 설정 적용이 된 객체(Target)을 감지하고, 해당 객체를 proxy 객체로 감싼 후 스프링 컨테이너에 등록한다.

호출 시 실제 대상 객체가 아닌 proxy 객체가 호출된다.
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/bean")
public class BeanController {
private final ApplicationContext applicationContext;
@GetMapping
public void checkBeanAPI() {
Object aopServiceBean = applicationContext.getBean("courseService");
Class<?> aopServiceClass = aopServiceBean.getClass();
log.info(aopServiceClass.getName());
}
}
호출 결과
com.standard.sparta.service.CourseService // APO 미적용: 실제 객체 사용
com.standard.sparta.service.CourseService$$SpringCGLIB$$0 // AOP 적용: Proxy 객체 사용