spring AOP

AOP

  • 객체지향 프로그래밍을 보완하는 개념으로 애플리케이션을 객체지향적으로 모듈화 하여 작성하더라도 다수의 객체들에 분산되어 중복적으로 존재하는 공통 관심사가 여전히 존재한다. AOP는 이를 횡단관심으로 분리하여 핵심관심과 엮어서 처리할 수 있는 방법을 제공한다.
  • 로깅, 보안, 트랜잭션 등의 공통적인 기능의 활용을 기존의 비즈니스 로직에 영향을 주지 않고 모듈화 처리를 지원하는 프로그래밍 기법

장점

  • 중복 코드의 제거 : 횡단 관심(CrossCutting Concerns)을 여러 모듈에 반복적으로 기술되는 현상을 방지
  • 비즈니스 로직의 가독성 향상 : 핵심기능 코드로부터 횡단 관심 코드를 분리함으로써 비즈니스 로직의 가독성 향상
  • 생산성 향상 : 비즈니스 로직의 독립으로 인한 개발의 집중력을 높임
  • 재사용성 향상 : 횡단 관심 코드는 여러 모듈에서 재사용될 수 있음
  • 변경 용이성 증대 : 횡단 관심 코드가 하나의 모듈로 관리되기 때문에 이에 대한 변경 발생시 용이하게 수행할 수 있음

주요개념

용어 설명
Aspect 애플리케이션이 가지고 있어야 할 로직과 그것을 실행해야 하는 지점을 정의한 것(Advice와 Pointcut의 조합).
Advice Aspect)의 실제 구현체로 결합점에 삽입되어 동작할 수 있는 코드. JoinPoint와 결합하여 동작하는 시점에 따라 before advice, after advice, around advice 타입으로 구분.
Joinpoint 횡단 관심 모듈이 삽입되어 동작할 수 있는 실행 가능한 특정 위치(Advice를 적용하는 지점을 의미). 스프링은 method 결합점만 제공.
Pointcut 어떤 클래스의 어느 JoinPoint를 사용할 것인지를 결정하는 선택 기능(Advice가 적용되는 대상을 지정)을 의미. 패키지이름/클래스이름/메서드이름을 정규식으로 지정하여 사용.
Target Advice가 적용되는 클래스를 의미(Aspect를 정의하는 곳)
Weaving Pointcut에 의해서 결정된 JoinPoint에 지정된 Advice를 삽입하는 과정(Advice를 주기능에 적용하는 것을 의미).


Aspect 실행 시점 지정

애너테이션 기능
@Before (이전) 어드바이스 타겟 메소드가 호출되기 전에 어드바이스 기능을 수행
@After (이후) 타겟 메소드의 결과에 관계없이(즉 성공, 예외 관계없이) 타겟 메소드가 완료 되면 어드바이스 기능을 수행
@AfterReturning (정상적 반환 이후) 타겟 메소드가 성공적으로 결과값을 반환 후에 어드바이스 기능을 수행
@AfterThrowing (예외 발생 이후) 타겟 메소드가 수행 중 예외를 던지게 되면 어드바이스 기능을 수행
@Around (메소드 실행 전후) 어드바이스가 타겟 메소드를 감싸서 타겟 메소드 호출 전과 후에 어드바이스 기능을 수행

스프링에서 AOP기능 구현 예제

스프링 AOP 의존성 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

경로지정 방식

@Component
@Aspect
public class PerfAspect {
    @Around("execution(* com.study..*.EventService.*(..))")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
        long begin = System.currentTimeMillis();
        Object retVal = pjp.proceed(); // 메서드 호출 자체를 감싼다
        System.out.println(System.currentTimeMillis() - begin);
        return retVal;
    }
}

@Around 어노테이션은 타겟 메서드를 감싸서 특정 Advice를 실행한다는 의미이다. 즉, execution(* com.study..*.EventService.*(..))는 com.study 아래 패키지 경로의 EventService 객체의 모든 메서드에 이 Aspect를 적용하겠다는 것을 의미한다.

애너테이션 지정 방식

@Component
@Aspect
public class PerfAspect {
    @Around("@annotation(PerLogging)")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable{
        long begin = System.currentTimeMillis();
        Object retVal = pjp.proceed();
        System.out.println(System.currentTimeMillis() - begin);
        return retVal;
    }
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerLogging {
}
@Component
public class SimpleEventService implements EventService {

    @PerLogging
    @Override
    public void createEvent() {
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Created an event");
    }

    @Override
    public void publishEvent() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e){
            e.printStackTrace();;
        }
        System.out.println("Published an event");
    }

    @PerLogging
    @Override
    public void deleteEvent() {
        System.out.println("Delete an event");
    }
}

빈의 모든 메서드에 지정

@Component
@Aspect
public class PerfAspect {
    @Around("bean(simpleEventService)")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable{
        long begin = System.currentTimeMillis();
        Object retVal = pjp.proceed();
        System.out.println(System.currentTimeMillis() - begin);
        return retVal;
    }
}
@Component
public class SimpleEventService implements EventService {

    @Override
    public void createEvent() {
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Created an event");
    }

    @Override
    public void publishEvent() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e){
            e.printStackTrace();;
        }
        System.out.println("Published an event");
    }
    
    @Override
    public void deleteEvent() {
        System.out.println("Delete an event");
    }
}
@Service
public class AppRunner implements ApplicationRunner {

    @Autowired
    EventService eventService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        eventService.createEvent();
        eventService.publishEvent();
        eventService.deleteEvent();
    }
}

표현식을 변수로 활용

@Pointcut 애너테이션은 Aspect에서 변수와 같이 재사용 가능한 포인트컷을 정의할 수 있다.

@Aspect
public class Performance {

    @Pointcut("execution(* com.study.board.BoardService.getBoards(..))")
    public void getBoards(){}

    @Pointcut("execution(* com.study.user.UserService.getUsers(..))")
    public void getUsers(){}

    @Around("getBoards() || getUsers()")
    public Object calculatePerformanceTime(ProceedingJoinPoint proceedingJoinPoint) {
        Object result = null;
        try {
            long start = System.currentTimeMillis();
            result = proceedingJoinPoint.proceed();
            long end = System.currentTimeMillis();

            System.out.println(end - start);
        } catch (Throwable throwable) {
            System.out.println("exception");
        }
        return result;
    }
}

타겟 메서드의 인자를 사용

@Aspect
@Component
public class UserAspect {

    @Autowired
    private UserRepository userRepository;

    @Pointcut("execution(* com.study.user.UserService.update(*)) && args(user)")
    public void updateUser(User user){}

    @AfterReturning("updateUser(user)")
    public void saveHistory(User user){
    	userRepository.save(new History(user.getIdx()));
    }
}

args(user) 표현식을 통해 타겟 메서드의 인자와 어드바이스의 인자가 매칭된다. 즉, updateUser라는 포인트컷이 user라는 인자를 사용하도록 args(user) 표현식으로 지정한 것 이다.

Categories:

Updated:

Comments