[Spring] AOP

·2024년 6월 5일

spring

목록 보기
15/18

1. Spring AOP란?

Spring AOP는 스프링 프레임워크에서 제공하는 기능 중 하나로 관점 지향 프로그래밍을 지원하는 기술이다.

Spring AOP는 로깅, 보안, 트랙잭션 관리 등과 같은 공통적인 관심사를 모듈화하여 코드의 중복을 줄이고 유지 보수성을 향상하는데 도움을 준다.

1.1 관점 지향 프로그래밍(Aspect-Oriented Programming)이란?

객체 지향 프로그래밍 패러다임을 보완하는 기술로 메서드나 객체의 기능을 핵심 관심사(Core Concern)와 공통 관심사(Cross-cutting Concern)로 나누어 프로그래밍 하는 것을 말한다.

핵심 관심사는 객체가 가져야 할 본래의 기능이며, 공통 관심사는 여러 객체에서 공통적으로 사용되는 코드를 말한다.

여러 개의 클래스에서 반복해서 사용되는 코드가 있다면 해당 코드를 모듈화하여 공통 관심사로 분리한다. 이렇게 분리한 공통 관심사를 Aspect로 정의하고 Aspect를 적용할 메서드나 클래스에 Advice를 적용하여 공통 관심사와 핵심 관심사로 분리할 수 있다. 이렇게 AOP에서는 공통 관심사를 별도의 모듈로 분리하여 관리하며, 이를 통해 코드의 재사용성과 유지 보수성을 높일 수 있다.

AOP에서 각 관점을 기준으로 모듈화한다는 것은 코드를 부분적으로 나누어서 모듈화한다는 의미이다. 이때 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 흩어진 관심사라고 부른다.

위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지이다.

2. Spring AOP 개념 이해하기

2.1 AOP 특징

  • 프록시 패턴 기반의 AOP 구현체, 프록시 객체를 쓰는 이유는 접근 제어 및 부가기능을 추가하기 위해서이다.
  • 스프링 빈에만 AOP를 적용 가능
  • 모든 AOP 기능을 제공하는 것이 아닌 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제(중복 코드, 프록시 클래스 작성의 번거로움, 객체들 간의 관계 복잡도 증가 등등)에 대한 해결책을 지원하는 것이 목적이다.

2.2 주요 용어

용어설명
Aspect공통적인 기능들을 모듈화 한것을 의미한다.
TargetAspect가 적용될 대상을 의미하며 메소드, 클래스 등이 이에 해당된다.
Join pointAspect가 적용될 수 있는 시점을 의미하며 메소드 실행 전, 후 등이 될 수 있다.
AdviceAspect의 기능을 정의한 것으로 메서드의 실행 전, 후, 예외 처리 발생 시 실행되는 코드를 의미한다.
Point cutAdvice를 적용할 메소드의 범위를 지정하는 것을 의미한다.

2.3 주요 애노테이션

메서드설명
@Aspect해당 클래스를 Aspect로 사용하겠다는 것을 명시한다.
@PointcutAspect를 실행 시킬 타겟을 설정해준다.
@Before대상 메서드가 실행되기 전에 Advice를 실행한다.
@AfterReturning대상 메서드가 정상적으로 실행되고 반환된 후에 Advice를 실행한다.
@AfterThrowing대상 메서드에서 예외가 발생”했을 때 Advice를 실행한다.
@After대상 메서드가 실행된 후에 Advice를 실행한다.
@Around대상 메서드 실행 전, 후 또는 예외 발생 시에 Advice를 실행한다.

3. Spring AOP 사용하기

의존성 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

3.1 타켓(Target) 지정

패키지 경로 지정

@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());
}
  • value 속성을 통해 Aspect를 적용할 Target 경로를 설정할 수 있다.

애너테이션

애너테이션

@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);
    }
  • value 속성에 생성한 애너테이션을 지정해준다.

스프링 빈

/**
 * 스프링 빈을 활용한 로깅 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;
}
  • value 속성에 스프링 빈을 지정하여, 해당 스프링 빈의 모든 메서드에 Aspect를 적용할 수 있다.

@Pointcut

/**
 * 일정 삭제 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);
}
  • @Pointcut을 통해 타겟의 경로를 지정할 수 있다.
  • value 속성에 Pointcut을 여러개 지정하여 사용할 수도 있다.
    ex) value = "deleteSchedulePointcut() && exmplePointcut()"

3.2 실행 시점(Join point) 지정

@Aspect

/**
 * 일정 관련 로깅 AOP 구성
 */
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j(topic = "ScheduleHistoryAspect")
public class ScheduleHistoryAspect {

    private final ScheduleHistoryRepository scheduleHistoryRepository;
}
  • @Aspect로 Aspect를 나타내는 클래스라는 것을 명시하고, @Component로 스프링 빈으로 등록한다.

@Before

/**
 * 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());
}
  • value 속성에 args()를 통해 파라미터를 가져올 수 있다.

@After

/**
 * 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());
}
  • value 속성을 통해 Aspect를 적용할 Target 경로를 설정할 수 있다.
  • value 속성에 args()를 통해 파라미터를 가져올 수 있다.

@AfterReturning

/**
 * 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());
}
  • value 속성을 통해 Aspect를 적용할 Target 경로를 설정할 수 있다.
  • value 속성에 args()를 통해 파라미터를 가져올 수 있다.
  • returning 속성을 통해 반환 값을 받아올 수 있다.

@AfterThrowing

/**
 * 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());

}
  • value 속성을 통해 Aspect를 적용할 Target 경로를 설정할 수 있다.
  • value 속성에 args()를 통해 파라미터를 가져올 수 있다.
  • throwing을 통해 에러를 받아온다

@Around

/**
 * 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;
}
  • value 속성을 통해 Aspect를 적용할 Target 경로를 설정할 수 있다.
  • value 속성에 args()를 통해 파라미터를 가져올 수 있다.
  • @Around를 사용할 경우 메서드의 파라미터로 ProceedingJoinPoint를 꼭 넣어줘야 한다.
  • proceed()를 통해 타켓을 호출한다.

3.3 JoinPoint 인터페이스 메서드

메서드설명
getArgs()대상 메서드의 인자 목록을 반환합니다.
getSignature()대상 메서드의 정보를 반환합니다.
getSourceLocation()대상 메서드가 선언된 위치를 반환합니다.
getKind()Advice의 종류를 반환합니다.
getStaticPart()Advice가 실행될 JoinPoint의 정적 정보를 반환합니다.
getThis()대상 객체를 반환합니다.
getTarget()대상 객체를 반환합니다.
toString()JoinPoint의 정보를 문자열로 반환합니다.
toShortString()JoinPoint의 간단한 정보를 문자열로 반환합니다.
toLongString()JoinPoint의 자세한 정보를 문자열로 반환합니다.

0개의 댓글