Spring AOP는 스프링 프레임워크에서 제공하는 기능 중 하나로 관점 지향 프로그래밍을 지원하는 기술이다.
Spring AOP는 로깅, 보안, 트랙잭션 관리 등과 같은 공통적인 관심사를 모듈화하여 코드의 중복을 줄이고 유지 보수성을 향상하는데 도움을 준다.
객체 지향 프로그래밍 패러다임을 보완하는 기술로 메서드나 객체의 기능을 핵심 관심사(Core Concern)와 공통 관심사(Cross-cutting Concern)로 나누어 프로그래밍 하는 것을 말한다.
핵심 관심사는 객체가 가져야 할 본래의 기능이며, 공통 관심사는 여러 객체에서 공통적으로 사용되는 코드를 말한다.
여러 개의 클래스에서 반복해서 사용되는 코드가 있다면 해당 코드를 모듈화하여 공통 관심사로 분리한다. 이렇게 분리한 공통 관심사를 Aspect로 정의하고 Aspect를 적용할 메서드나 클래스에 Advice를 적용하여 공통 관심사와 핵심 관심사로 분리할 수 있다. 이렇게 AOP에서는 공통 관심사를 별도의 모듈로 분리하여 관리하며, 이를 통해 코드의 재사용성과 유지 보수성을 높일 수 있다.
AOP에서 각 관점을 기준으로 모듈화한다는 것은 코드를 부분적으로 나누어서 모듈화한다는 의미이다. 이때 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 흩어진 관심사라고 부른다.

위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지이다.
| 용어 | 설명 |
|---|---|
| Aspect | 공통적인 기능들을 모듈화 한것을 의미한다. |
| Target | Aspect가 적용될 대상을 의미하며 메소드, 클래스 등이 이에 해당된다. |
| Join point | Aspect가 적용될 수 있는 시점을 의미하며 메소드 실행 전, 후 등이 될 수 있다. |
| Advice | Aspect의 기능을 정의한 것으로 메서드의 실행 전, 후, 예외 처리 발생 시 실행되는 코드를 의미한다. |
| Point cut | Advice를 적용할 메소드의 범위를 지정하는 것을 의미한다. |
| 메서드 | 설명 |
|---|---|
| @Aspect | 해당 클래스를 Aspect로 사용하겠다는 것을 명시한다. |
| @Pointcut | Aspect를 실행 시킬 타겟을 설정해준다. |
| @Before | 대상 메서드가 실행되기 전에 Advice를 실행한다. |
| @AfterReturning | 대상 메서드가 정상적으로 실행되고 반환된 후에 Advice를 실행한다. |
| @AfterThrowing | 대상 메서드에서 예외가 발생”했을 때 Advice를 실행한다. |
| @After | 대상 메서드가 실행된 후에 Advice를 실행한다. |
| @Around | 대상 메서드 실행 전, 후 또는 예외 발생 시에 Advice를 실행한다. |
의존성 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
@Before(value = "execution(* com.sparta.springpersonaltaskv2.service.ScheduleService.createSchedule(..)) && args(requestDto, user)", argNames = "requestDto,user")
public void beforeAddSchedule(ScheduleRequestDto requestDto, User user) {
log.info("[일정 등록 시도] 작성자 : {}, 제목 : {}", user.getUsername(), requestDto.getContent());
}
애너테이션
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface ScheduleLoggingAOP {
}
타겟 메서드에 애너테이션 지정
@ScheduleLoggingAOP
@Transactional
public Long updateSchedule(Long id, ScheduleRequestDto requestDto, User user) {
Schedule schedule = getValidatedSchedule(id, user);
schedule.update(requestDto);
return schedule.getId();
}
타겟 지정
/**
* 애노테이션을 활용한 로깅 AOP
* @param id 일정 ID
* @param requestDto 일정 정보
* @param user 회원 정보
*/
@Before(value = "@annotation(ScheduleLoggingAOP) && args(id,requestDto, user)", argNames = "id,requestDto,user")
public void beforeAddScheduleWithAnnotation(Long id, ScheduleRequestDto requestDto, User user) {
log.info("[일정 수정 전] 수정자 : {}, 일정 아이디 {}", user.getUsername(), id);
}
/**
* 스프링 빈을 활용한 로깅 AOP
* @param joinPoint 조인 포인트 정보
* @return 조인 포인트 정보
* @throws Throwable 예외
*/
@Around("bean(scheduleService)")
public Object arroundScheduleService(ProceedingJoinPoint joinPoint) throws Throwable {
long begin = System.currentTimeMillis();
Object proceed = joinPoint.proceed();
log.info("[ScheduleService 수행시간] {}초", (double) (System.currentTimeMillis() - begin)/1000);
return proceed;
}
/**
* 일정 삭제 Pointcut
*/
@Pointcut(value = "execution(* com.sparta.springpersonaltaskv2.service.ScheduleService.deleteSchedule(..))")
private void deleteSchedulePointcut() {
}
/**
* Pointcut을 활용한 로깅 AOP
* @param id 일정 ID
* @param user 회원 정보
*/
@Before(value = "deleteSchedulePointcut() && args(id, user)", argNames = "id,user")
public void beforeDeleteSchedule(Long id, User user) {
log.info("[일정 삭제 전] username : {}, scheduleId : {}", user.getUsername(), id);
}
/**
* 일정 관련 로깅 AOP 구성
*/
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j(topic = "ScheduleHistoryAspect")
public class ScheduleHistoryAspect {
private final ScheduleHistoryRepository scheduleHistoryRepository;
}
/**
* Before : 일정 등록 시도 전 로깅 AOP
* <p>
* 메서드가 실행되기 전에 Advice를 실행한다.
* </p>
* @param requestDto 일정 정보
* @param user 회원 정보
*/
@Before(value = "execution(* com.sparta.springpersonaltaskv2.service.ScheduleService.createSchedule(..)) && args(requestDto, user)", argNames = "requestDto,user")
public void beforeAddSchedule(ScheduleRequestDto requestDto, User user) {
log.info("[일정 등록 시도] 작성자 : {}, 제목 : {}", user.getUsername(), requestDto.getContent());
}
/**
* After : 일정 등록 메서드 실행 후 로깅 AOP
* <p>
* 메서드가 실행되고 Advice를 실행한다.
* </p>
* @param joinPoint 조인 포인트 정보
* @param user 회원 정보
* @param requestDto 일정 정보
*/
@After(value = "execution(* com.sparta.springpersonaltaskv2.service.ScheduleService.createSchedule(..)) && args(requestDto, user)", argNames = "joinPoint,user,requestDto")
public void afterAddSchedule(JoinPoint joinPoint, User user, ScheduleRequestDto requestDto) {
log.info("[일정 등록 후] 경로 {}, 작성자 : {}, 제목 : {}",
joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(),
requestDto.getTitle(),
user.getUsername());
}
/**
* AfterReturning : 일정 등록 성공 로깅 AOP
* <p>
* 메서드가 정상적으로 실행되고 반환된 후에 Advice를 실행한다.
* </p>
* @param responseDto 일정 정보
*/
@AfterReturning(value = "execution(* com.sparta.springpersonaltaskv2.service.ScheduleService.createSchedule(..))", returning = "responseDto")
public void afterAddScheduleSuccess(ScheduleResponseDto responseDto) {
ScheduleHistory scheduleHistory = new ScheduleHistory(responseDto, AspectType.AFTER_RETURNING);
scheduleHistoryRepository.save(scheduleHistory);
log.info("[일정 등록 성공] 작성자 : {}, 일정 ID : {}", responseDto.getWriter(), responseDto.getId());
}
/**
* AfterThrowing : 일정 등록 실패 로깅 AOP
* <p>
* 메서드에서 예외 발생 시 Advice를 실행한다.
* </p>
* @param e 예외
*/
@AfterThrowing(value = "execution(* com.sparta.springpersonaltaskv2.service.ScheduleService.createSchedule(..))", throwing = "e")
public void afterAddScheduleFailure(Throwable e) {
log.error("[일정 등록 실패] message : {}", e.getMessage());
}
/**
* Around : 일정 등록 전후 로깅 AOP
* <p>
* 메서 실핼 전후로 또는 예외 발생 시 Advice를 실행한다.
* </p>
* @param joinPoint 조인 포인트 정보
* @return 메소드 반환 값
* @throws Throwable 예외
*/
@Around(value = "execution(* com.sparta.springpersonaltaskv2.service.ScheduleService.createSchedule(..))")
public Object aroundAddSchedule(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around before: " + joinPoint.getSignature().getName());
// joinPoint.proceed()를 기준으로 메서드 실행 전후로 나뉨
ScheduleResponseDto proceed = (ScheduleResponseDto) joinPoint.proceed();
log.info("proceed: " + proceed.getWriter());
log.info("Around after: " + joinPoint.getSignature().getName());
return proceed;
}
| 메서드 | 설명 |
|---|---|
| getArgs() | 대상 메서드의 인자 목록을 반환합니다. |
| getSignature() | 대상 메서드의 정보를 반환합니다. |
| getSourceLocation() | 대상 메서드가 선언된 위치를 반환합니다. |
| getKind() | Advice의 종류를 반환합니다. |
| getStaticPart() | Advice가 실행될 JoinPoint의 정적 정보를 반환합니다. |
| getThis() | 대상 객체를 반환합니다. |
| getTarget() | 대상 객체를 반환합니다. |
| toString() | JoinPoint의 정보를 문자열로 반환합니다. |
| toShortString() | JoinPoint의 간단한 정보를 문자열로 반환합니다. |
| toLongString() | JoinPoint의 자세한 정보를 문자열로 반환합니다. |