본문 바로가기

더 나은 엔지니어가 되기 위해/지금은 안쓰는 자바

[스프링 프레임워크 핵심 기술] AOP

인프런에서 백기선님의 스프링 프레임워크 핵심 기술 을 공부하며 개인적으로 정리한 글입니다.

AOP 란

개념

AOP(Aspect-Oriented Programming) 는 OOP를 보완하는 수단으로, 흩어진 Aspect 를 모듈화 할 수 있는 프로그래밍 기법이다.
즉, 여러 곳에서 쓰이는 공통 기능을 모듈화하고, 쓰이는 곳에 필요할 때 연결함으로써, 유지 보수 혹은 재사용에 용이하도록 프로그래밍 하는 것.

주요 개념

  • Aspect
    • 여러 곳에서 쓰이는 코드(공통 부분)를 모듈화한 것
  • Target
    • Aspect 가 적용되는 곳
  • Advice
    • Aspect 에서 실질적인 기능에 대한 구현체
  • Joint point
    • Advice 가 Target 에 적용되는 시점
    • 메서드 진입할 때, 생성자 호출할 때, 필드에서 값을 꺼낼 때 등등
  • Point cut
    • Joint point 의 상세 스펙을 정의한 것

AOP 구현체

  • AspectJ
  • 스프링 AOP

AOP 적용 방법

  • 컴파일 (AspectJ)
  • 로드 타임 (AspectJ)
  • 런타임 (스프링 AOP)

스프링 AOP

특징

  • 프록시 기반의 AOP 구현체
  • 스프링 빈에만 AOP 를 적용할 수 있다.
  • 동적 프록시 빈을 만들어 등록시켜준다.
    • 빈 라이프사이클 중 실행되는 BeanPostProcessor 구현체를 구현함
    • AbstractAutoProxyCreator implements BeanPostProcessor

구현

먼저 다음 AOP 를 위해 다음 의존성을 추가한다.

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

간단한 인터페이스를 구현하는 클래스 하나를 빈으로 정의해보자.

public interface EventService {
  public void created();
  public void operation();
  public void deleted();
}
@Component
public class SimpleServiceEvent implements EventService {

  @Override
  public void created() {
    long begin = System.currentTimeMillis();
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("created");
    System.out.println(System.currentTimeMillis() - begin);
  }

  @Override
  public void operation() {
    System.out.println(System.currentTimeMillis() - begin);
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("operation");
    System.out.println(System.currentTimeMillis() - begin);

  }

  @Override
  public void deleted() {
    System.out.println("deleted");
  }
}

created()operation() 안에는 수행시간을 측정하는 코드를 담고있다.
이제 이 클래스를 활용하는 Runner 를 다음과 같이 만들자.

@Component
public class AppRuner implements ApplicationRunner {

  @Autowired
  EventService eventService;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    eventService.created();
    eventService.operation();
    eventService.deleted();
  }
}

실행하면 다음과 같은 결과가 출력된다.

created
1011
operation
2004
deleted

1) execution expression

이제 이를 AOP 를 이용하여 개선해보자.
위 코드를 보면 수행 시간을 재는 다음 코드가 여러 곳에서 사용되고 있다.

long begin = System.currentTimeMillis();
... (수행) ...
System.out.println(System.currentTimeMillis() - begin);

이 코드는 Aspect 로 묶어서 관리될 필요가 있어보인다.
이제 다음과 같이 Aspect 클래스를 정의하자.

// Aspect 정의
@Component
@Aspect
public class PerfAspect {

  // Advice 정의 (Around 사용)
  // Point Cut 표현식
  // (com.example.demo 밑에 있는 모든 클래스 중 EventService 안에 들어있는 모든 메쏘드에 이 행위를 적용하라.)
  @Around("execution(* com.example..*.EventService.*(..))")
  public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
    long begin = System.currentTimeMillis();
    Object retVal = pjp.proceed();
    System.out.println(System.currentTimeMillis() - begin);
    return retVal;
  }
}
  • @Aspect 로 Aspect 클래스임을 정의한다.
  • Advice 를 정의한다. Point cut 시점은 @Around 형태로 정의했다.
    이 외에도 다음과 같은 어노테이션이 있다.
    • @Around
    • @Before
    • @After
  • execution... 으로 시작하는 부분은 execution expression 이라고 한다.
    Point cut을 설정하는 부분이다.

AOP 를 적용했으므로, 이제 적용될 클래스에서 Aspect 로 대체된 부분은 삭제한다.

@Component
public class SimpleServiceEvent implements EventService {

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

  @Override
  public void operation() {
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("operation");
  }

  @Override
  public void deleted() {
    System.out.println("deleted");
  }
}

이제 다시 Runner 를 실행하면 다음과 같이 출력된다.

created
1011
operation
2004
deleted
0

2) annotaion 기반

위와 같이 했더니 하나 문제가 있다.
우리는 created()operation() 에만 Apsect 를 적용하고 싶은데 delete() 에도 적용이 됐기 때문이다.

이를 어노테이션 기반 Advice 정의로 해결해보자.
먼저 다음과 같은 어노테이션을 만든다.

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerfLogging {
}

Aspect 클래스를 다음과 같이 수정한다.

@Component
@Aspect
public class PerfAspect {

  @Around("@annotation(PerfLogging)")
  public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
    long begin = System.currentTimeMillis();
    Object retVal = pjp.proceed();
    System.out.println(System.currentTimeMillis() - begin);
    return retVal;
  }
}

execution expression 을 "@annotation(PerfLogging)") 으로 대체한 것이다.
이제 적용될 클래스의 메쏘드에 @PerfLogging 을 붙여주자.

@Component
public class SimpleServiceEvent implements EventService {

  @PerfLogging
  @Override
  public void created() {
    ...
  }

  @PerfLogging
  @Override
  public void operation() {
    ...
  }

  @Override
  public void deleted() {
    ...
  }
}

다시 Runner 를 실행하면 @PerfLogging 을 붙인 메쏘드만 Aspect 가 붙는 것을 확인할 수 있다.

created
1011
operation
2004
deleted

3) 특정 bean 기반

다음과 같이 simpleServiceEvent 내 모든 public 메쏘드에다가 적용하는 방법도 있다.

// Aspect 정의
@Component
@Aspect
public class PerfAspect {

  // 빈이 가지고있는 모든 퍼블릭 메쏘드
  @Around("bean(simpleServiceEvent)")
  public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
    long begin = System.currentTimeMillis();
    Object retVal = pjp.proceed();
    System.out.println(System.currentTimeMillis() - begin);
    return retVal;
  }
}
created
1011
operation
2004
deleted
0