본문 바로가기

STUDY/스프링 철저 입문

1-5) 스프링 AOP(Aspect Oriented Programming) : @Aspect, @Pointcut, @Before, @After, @AfterReturning, @AfterThrowing, @Around

- AOP(Aspect Oriented Programming)란?

 

과거 프로그램의 규모가 커지면서 중복된 코드를 줄이고 유지보수성을 높이기 위해 OOP, 객체지향 프로그래밍이 등장하였다. 객체지향 프로그래밍은 각각의 역할을 분리하고 서로 필요할 기능을 호출하도록 하여 그 목표를 달성하였다. 웹서비스의 구조를 보면, 사용자 입장에서는 서로 다른 기능으로 보일지라도 여러 서비스와 레포지토리 객체를 공통으로 사용한다.

 

 

각각의 역할을 하는 객체들이 서로 호출하며 동작한다.

 

이렇게 공통 기능을 객체로 분리하여 코드의 재사용성을 높혔지만, 여전히 객체마다 중복해서 들어가야하는 요소들이 존재했다. 예를들어 로그의 경우, 각 객체마다 로그를 남기기 위한 별도의 코드가 삽입되어야 했다.  이렇게 각 객체를 관통하여 존재하는 중복을 제거할 필요성이 생겼다. 그리고 이를 위해 탄생한 개념이 AOP(Aspect Oriented Programming), 즉 관점지향프로그래밍이다.

 

 

각 객체들을 관통하는 관심사항(보안, 로그, 트랜잭션)

 

 

AOP는 각 객체를 관통하는 관심사항(보안, 로그, 트랜잭션 등)에 관한 기능을 처리하는 개념이며, 이를 통해 코드의 중복을 줄인다.

 

 

- 스프링(Spring)에서의 AOP 사용

 

스프링에서 AOP를 사용하기 위해서는 먼저, spring-aop와 aspectjweaver를 임포트 해주어야 한다. 메이븐 사용자의 경우, pom.xml에 다음과 같은 코드를 넣어주면 된다.

 

...
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>4.3.25.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.8.14</version>
</dependency>
...

 

그리고 AOP 공통 기능을 구현하기 위한 객체를 만든다.

 

@Aspect
@Component
public class ComponentLoggingAspect {
}

 

마지막으로 스프링 ContextConfig에 @EnableAspectJAutoProxy를 추가하고, ComponentLoggingAspect의 패키지 위치를 ComponentScan에 추가해주면 준비는 끝난다.

 

@Configuration
@ComponentScan(basePackages = {"com.study.spring.component", "com.study.spring.aop"})
@EnableAspectJAutoProxy
public class ContextConfig{
}

 

 

- AOP 실습 : @Pointcut

 

AOP에서 Pointcut이란 @Aspect 객체에서 작성한 코드가 실행될 시점을 나타내기위한 어노테이션이다. 그리고 Pointcut을 사용하기 위해서는 시점을 나타내기위한 표현식을 알아야 한다. 예를 들어 특정 반환값, 패키지 경로, 메소드명을 기준으로 실행 시점 지정하려면 excution, 어노테이션을 기준으로 하려면 @annotaion을 사용해야 한다. Pointcut으로 사용할 수 있는 표현식은 다음과 같다.

 

 - excution : 반환타입, 타입, 메소드, 파라미터를 기준으로 실행될 시점을 지정한다.

 - within : 특정 경로의 타입을 기준으로 실행될 시점을 지정한다.

 - this / target : 특정 타입의 객체를 기준으로 실행될 시점을 지정한다.

 - args : 특정 타입의 파라미터를 가지는 메소드를 기준으로 실행될 시점을 지정한다.

 - @target : 특정 어노테이션을 가지는 객체를 기준으로 실행될 시점을 지정한다.

 - @args : 특정 어노테이션의 파라미터를 가지는 메소드를 기준으로 실행될 시점을 지정한다.

 - @within : 특정 경로의 어노테이션을 기준으로 실행될 시점을 지전한다.

 - @annotaion : 특정 어노테이션을 기준으로 실행될 시점을 지정한다.

 

각각의 사용법은 아래의 실습을 통해 더 자세히 알아보겠다.

 

 

- AOP 실습 : @Befor 

 

@Before은 AOP가 적용될 메소드가 실행되기 전의 시점을 말한다. 

 

@Aspect
@Component
public class ComponentLoggingAspect {
  @Pointcut("execution(public void com.study.spring.component.*.*(..))")
  private void componentMethodPointcut() {}

  @Before("componentMethodPointcut()")
  public void beforeComponentMethod(JoinPoint jp) {
    System.out.println("beforeComponentMethod " + jp.getSignature());
  }
}

 

먼저 excution 형식의 Pointcut을 만들었다. "excution(접근제어자 반환형 패키지경로.클래스타입.메소드명(파라미터))"와 같은 형식으로 되어있고, 여기서 접근제어자는 생략이 가능하다. 예제 코드를 해석하면 public의 void 반환타입을 가지는 com.study.spring.component 패키지의 모든 클래스의 모든 이름과 파라미터를 가지는 메소드를 실행시점으로 지정한 것이다.

 

@Before 어노테이션을 사용하여, 위에서 만든 포인트컷 시점 이전에 실행되는 메소드를 만들었다. "beforeComponentMethod" 이라는 문구와 실행하는 메소드 정보를 출력한다.

 

...
public static void main(String[] args) {
  ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfig.class);
  Component1 comp1 = context.getBean("component1", Component1.class);
  comp1.method1();
}
...

 

그리고 main 함수에서, com.study.spring.component 패키지의 클래스를 만들고 메소드를 실행해보면 다음과 같이 AOP코드가 실행된 걸 볼 수 있다.

 

beforeComponentMethod void com.study.spring5.component.Component1.method1()

 

또 메소드로 전달되는 파라미터를 확인해 볼 수도 있다. JoinPoint 객체의 getArgs()는 메소드로 전달되는 파라미터 객체 배열을 반환해준다.

 

@Aspect
@Component
public class ComponentLoggingAspect {
  ...
  @Before("componentMethodPointcut()")
  public void beforeComponentMethod(JoinPoint jp) {
    System.out.println("beforeComponentMethod " + jp.getSignature());
    for (Object parameter : jp.getArgs()) {
      System.out.println(parameter);
    }
  }
  ...
}

 

...
public static void main(String[] args) {
  ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfig.class);
  Component1 comp1 = context.getBean("component1", Component1.class);
  comp1.method2("Hello");
}
...

 

아까의 AOP 코드 아래에 jp.getArgs() 반환값을 출력하는 코드를 추가하였다. 그리고 String 객체를 전달 받는 method2를 만들고 호출하도록 하였다.

 

beforeComponentMethod void com.study.spring5.component.Component1.method2(String)
Hello

 

- AOP 실습 : @After

 

@After은 AOP가 적용될 메소드가 실행된 이후의 시점을 말한다.  메소드가 성공적으로 수행된 경우와 에러가 발생하여 Exception이 생긴 경우 모두 해당된다.

 

@Aspect
@Component
public class ComponentLoggingAspect {
  @Pointcut("within(*..*)")
  private void allMethodPointcut() {}

  @After(value = "allMethodPointcut()")
  public void afterAllMethod(JoinPoint jp) {
    System.out.println("afterAllMethod " + jp.getSignature());
  }
}

 

...
public static void main(String[] args) {
  ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfig.class);
  Component1 comp1 = context.getBean("component1", Component1.class);
  comp1.method1();
}
...

 

이번에는 within 형식의 Pointcut을 만들었다. "within(패키지경로.클래스타입.메소드명)"와 같은 형식으로 되어있다. 예제 코드를 해석하면 모든 경로의 모든 클래스의 모든 메소드를 실행시점으로 지정한 것이다. 그리고 main에서 아래와 같이 실행시켜보면 다음과 같은 결과가 나온다.

 

 

afterAllMethod void org.springframework.beans.factory.SmartInitializingSingleton.afterSingletonsInstantiated()
afterAllMethod void com.study.spring5.component.Component1.method1()

 

모든 메소드에 해당되는 포인트 컷을 만들었으므로 당연히 method1이 실행된 이후 출력되는 내용이 보인다. 하지만 그위에 알 수 없는 메소드의 내용 역시 출력되고 있다. 이는 우리가 만든 클래스는 아니지만, Spring이 관리하는 내장 클래스의 메소드가 실행될 때, 모든 메소드라는 AOP의 Pointcut에 해당되어서 출력된 것이다.

 

 

- AOP 실습 : @AfterReturning

 

@AfterReturning은 AOP가 적용될 메소드가 에러없이 성공적으로 실행된 이후의 시점을 말한다. 

 

@Aspect
@Component
public class ComponentLoggingAspect {
  ...
  @Pointcut("target(com.study.spring5.component.Component1)")
  private void component1MethodPointcut() {}

  @AfterReturning(value = "component1MethodPointcut()", returning = "obj")
  public void afterReturningComponent1Method(JoinPoint jp, Object obj) {
    System.out.println("afterAllMethod " + jp.getSignature());
    System.out.println("return: " + obj);
  }
  ...
}

 

...
public static void main(String[] args) {
  ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfig.class);
  Component1 comp1 = context.getBean("component1", Component1.class);
  comp1.method1();
}
...

 

이번에는 target 형식의 Pointcut을 만들었다. "target(패키지경로.클래스타입)"와 같은 형식으로 되어있다. 위에서 나온 다른 표현식과 달리, target은 정확한 클래스 경로와 타입을 적어주어야 한다. 존재하지 않는 클래스 경로와 타입을 넣으면 Spring 실행 중 에러를 발생시킨다.

 

@AfterReturning에서는 메소드의 반환값을 확인해 볼 수 있다. 어노테이션의 returning 속성에 파라미터로 받고자 하는 값을 넣고, 해당이름의 파라미터를 넣으면, 메소드의 반환 값을 알 수 있다. main을 실행시켜보면 다음과 같은 결과가 나온다.

 

afterAllMethod String com.study.spring5.component.Component1.method3()
return: null

 

method1은 반환형이 void이므로 null이 출력된 것을 확인할 수 있다.

 

 

- AOP 실습 : @AfterThrowing

 

@AfterThrowing은 AOP가 적용될 메소드가 에러가 발생하여 Exception을 던지는 시점을 말한다. 

 

@Aspect
@Component
public class ComponentLoggingAspect {
  ...
  @Pointcut("args(String)")
  private void methodWithStringParameterPointcut() {}

  @AfterThrowing(value = "methodWithStringParameterPointcut()", throwing = "e")
  public void afterThrowingWithStringParameterMethod(JoinPoint jp, Exception e) {
    System.out.println("afterThrowingWithStringParameterMethod " + jp.getSignature());
    System.out.println("e: " + e);
  }
  ...
}

 

...
public static void main(String[] args) {
  ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfig.class);
  Component1 comp1 = context.getBean("Component1", Component1.class);
  try {
  	comp1.method3("Hello");
  } catch (Exception e) {
  	return;
  }
}
...

 

이번에는 args형식의 Pointcut을 만들었다. "args(패키지경로.클래스타입, ...)"와 같은 형식으로 되어있다. 정확한 클래스 경로와 타입을 적어주어야 하지만 String은 기본 객체이므로 바로 타입만 적어주어도 동작 한다.

 

@AfterThrowing에서는 메소드가 던진 예외을 확인해 볼 수 있다. 어노테이션의 throwing 속성에 파라미터로 받고자 하는 값을 넣고, 해당이름의 파라미터를 넣으면, 메소드가 던진 예외 알 수 있다. String을 파라미터로 받고, Exception을 던지는 메소드를 만들고, main을 실행시켜보면 다음과 같은 결과가 나온다.

 

afterThrowingWithStringParameterMethod void com.study.spring5.component.Component1.method3(String)
e: java.lang.Exception

 

 

- AOP 실습 : @Around

 

@Around는 AOP가 적용될 메소드의 시작부터 끝까지 전반적인 시점을 모두 관리한다. 

 

@Aspect
@Component
public class ComponentLoggingAspect {
  ...
  @Pointcut("@target(org.springframework.stereotype.Component)")
  private void componentAnnotationMethodPointcut() {}

  @Around("componentAnnotationMethodPointcut()")
  public Object aroundComponentAnnotationMethodPointcut(ProceedingJoinPoint jp) {
    Object[] args = jp.getArgs();

    System.out.print("기존 파라미터: ");
    for (Object obj : args) {
      System.out.print(obj.toString() + " ");
    }
    System.out.println();

    Object[] new_args = {"World"};
    Object response = null;
    try {
      response = jp.proceed(new_args);
    } catch (Throwable e) {
      e.printStackTrace();
    }

    return response;
  }
  ...
}

 

@Component
public class Component1 {
  ...
  public void method4(String str) {
    System.out.println("str: " + str);
  }
  ...
}

 

public static void main(String[] args) {
  ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfig.class);
  Component1 comp1 = context.getBean("Component1", Component1.class);
  comp1.method4("Hello");
}

 

이번에는 @target형식의 Pointcut을 만들었다. "@target(패키지경로.어노테이션타입)"와 같은 형식으로 되어있다. 해당 어노테이션타입이 적용된 객체의 메소드를 기준으로 한다.

 

@Around의 경우, 파라미터로 JoinPoint가 아닌 ProceedingJoinPoint를 사용한다. 이 파라미터를 사용하여 AOP가 적용된 메소드의 시작부터 끝까지 과정을 관리한다. 예제를 보면 jp.getArgs() 실행될 코드가 받은 파라미터를 출력한다. 그 아래에서 new_args라는 객체를 만들고 jp.proceed() 메소드를 호출하면서 new_args를 파라미터로 넘긴다. 이렇게 되면, 실행된 메소드는 원래 전달 받은 파라미터가 아니라, AOP에 의해 바꿔치기한 파라미터를 전달받는다.

 

또, 메소드의 파라미터 뿐만 아니라 반환값도 관리할 수 있다. jp.proceed()의 반환값이 실행된 메소드의 반환값이다. 즉, 코드에서 response에 comp1.method4()의 반환값이 들어가는 것이다. 그리고 그대로 reponse를 반환할 수도 있지만, 필요하다면 이 값을 수정할 수 있다. main을 실행시켜보면 다음과 같은 결과가 나온다.

 

기존 파라미터: Hello 
str: World

 

 

- 정리

 

AOP를 사용하면 여러 메소드에 걸쳐서 중복 사용되는 코드를 한곳에서 관리할 수 있다. 아주 강력한 기능이고 사용법도 크게 어렵지 않아서 자주 사용되는 기능이므로 꼭 배우고 넘어가는 것이 좋다. 각 실행 시점을 정리한 사진을 보여주면서 마무리 하겠다.

 

728x90