모르지 않다는 것은 아는것과 다르다.

Spring

Spring AOP

채마스 2022. 2. 26. 01:15

AOP 란?

  • AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다.
  • 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다.
  • 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 흩어진 관심사 (Crosscutting Concerns)라 부른다.
  • 아래와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.

 

프록시 패턴

  • 위에서 보듯 Client는 이 interface 타입으로 Proxy 객체를 사용하게 된다.
  • Proxy 객체는 기존의 타겟 객체(Real Subject)를 참조하고 있다.
  • Proxy 객체와 타겟 객체(Real Subject) 의 타입은 같고, Proxy는 원래 해야 할 일을 가지고 있는 Real Subject를 감싸서 Client의 요청을 처리한다.
  • Proxy 와 타겟 객체(Real Subject) 가 타입이 같은 이유는 기존 코드의 변경 없이 접근 제어 또는 부가 기능 추가를 위해서다.

 

AOP 의 세가지 처리 방식

  • AOP의 핵심 기능은 코드를 수정하지 않으면서 공통 기능의 구현을 추가하는 것이라고 강조하고 있습니다.
  • 핵심 기능에 공통 기능을 추가하는 방법에는 아래와 같이 3가지 방법이 존재합니다.
    • 컴파일 : 자바 파일을 클래스 파일로 만들 때 바이트코드를 조작하여 적용된 바이트코드를 생성
    • 로드 타임 : 컴파일은 원래 클래스 그대로 하고, 클래스를 로딩하는 시점에 끼워서 넣는다.
    • 런타임 : A라는 클래스를 빈으로 만들 때 A라는 타입의 프록시 빈을 감싸서 만든 후에, 프록시 빈이 클래스 중간에 코드를 추가해서 넣는다.
  • 스프링에서 많이 사용하는 방식은 프록시를 이용한 세 번째 방법입니다.
  • 스프링 AOP는 프록시 객체를 자동으로 만들어줍니다.

 

AOP 주요 개념

  • Aspect : 위에서 설명한 흩어진 관심사를 모듈화 한 것. 주로 부가기능을 모듈화함.
  • Target : Aspect를 적용하는 곳 (클래스, 메서드 .. )
  • Advice : 실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체
    • Advice 의 동작 시점
      • Before: 메소드 실행 전에 동작
      • After: 메소드 실행 후에 동작
      • After-returning: 메소드가 정상적으로 실행된 후에 동작
      • After-throwing: 예외가 발생한 후에 동작
      • Around: 메소드 호출 이전, 이후, 예외발생 등 모든 시점에서 동작
  • JointPoint : Advice가 적용될 위치, 끼어들 수 있는 지점.
    • 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능
  • PointCut : JointPoint의 상세한 스펙을 정의한 것.
    • 'A란 메서드의 진입 시점에 호출할 것'과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있음

 

PointCut 표현식

  • 포인트컷을 이용하면 어드바이스 메소드가 적용될 비즈니스 메소드를 정확하게 필터링할 수 있다.
  • 지시자(PCD, AspectJ pointcut designators)의 종류
    • execution : 가장 정교한 포인트컷을 만들수 있다. 리턴타입 패키지경로 클래스명 메소드명(매개변수)
    • within : 타입패턴 내에 해당하는 모든 것들을 포인트컷
    • bean : bean이름으로 포인트컷
  • 리턴타입 지정
    • *: 모든 리턴타입 허용
    • void: 리턴타입이 void인 메소드 선택
    • !void: 리턴타입이 void가 아닌 메소드 선택
  • 패키지 지정
    • com.devljh.domain: 정확하게 com.devljh.domain 패키지만 선택
    • com.devljh.domain..: com.devljh.domain 패키지로 시작하는 모든 패키지 선택
  • 클래스 지정
    • UserBO: 정확하게 UserBO 클래스만 선택
    • *BO: 이름이 BO로 끝나는 클래스만 선택
    • BaseObject+: 클래스 이름 뒤에 '+'가 붙으면 해당 클래스로부터 파생된 모든 자식 클래스 선택, 인터페이스 이름 뒤에 '+'가 붙으면 해당 인터페이스를 구현한 모든 클래스 선택
  • 메소드 지정
    • *(..): 모든 메소드 선택
    • update*(..): 메소드명이 update로 시작하는 모든 메소드 선택
  • 매개변수 지정
    • (..) 모든 매개변수
    • (*) 반드시 1개의 매개변수를 가지는 메소드만 선택
    • (com.devljh.domain.user.model.User): 매개변수로 User를 가지는 메소드만 선택. 꼭 풀패키지명이 있어야함
    • (!com.devljh.domain.user.model.User): 매개변수로 User를 가지지않는 메소드만 선택
    • (Integer, ..): 한 개 이상의 매개변수를 가지되, 첫 번째 매개변수의 타입이 Integer인 메소드만 선택
    • (Integer, *): 반드시 두 개의 매개변수를 가지되, 첫 번째 매개변수의 타입이 Integer인 메소드만 선택

 

JoinPoint Interface

  • Signature getSignature() : 클라이언트가 호출한 메소드의 시그니처(리턴타입, 이름, 매개변수) 정보가 저장된 Signature 객체 리턴
    • getName() : 클라이언트가 호출한 메소드의 이름 리턴
    • String toLongString() : 클라리언트가 호출한 메소드의 리턴타입, 이름, 매개변수를 패키지 경로까지 포함해서 리턴
    • String toShortString() : 클라이언트가 호출한 메소드 시그니처를 축약한 문자열로 리턴
  • Object getTarget() : 클라이언트가 호출한 비즈니스 메소드를 포함하는 비즈니스 객체 리턴
  • Object[] getArgs() : 클라이언트가 메소드를 호출할 때 넘겨준 인자 목록을 Object 배열로 리턴
    public class BeforeAdvice { 
        public void beforeLog(JoinPoint jp) { 
            String method = jp.getSignature().getName(); 
            Object[] args = jp.getArgs(); System.out.println(생략...); 
        } 
    }

 

Advice Annotation

  • AOP는 Advice를 스프링 컨테이너에서 설정해주지 않아도 간단한 어노테이션을 이용하여 Advice를 주입하는 것이 가능하다.
  • 아래와 같이 구현할 수 있다.
@Service @Aspect
public void LogAdvice(){ 

    @Pointcut("execution(생략...)"); 
    public void allPointcut(){}; 

    @before("allPointcut") 
    public void printLog(){} 

}
  • 여기서 포인트컷은 아래와 같다.
    @Pointcut("execution(생략...)"); 
    public void allPointcut(){};
  • 그리고 어드바이스 메소드는 아래와 같다.
    @before("allPointcut") 
    public void printLog(){} 
  • 위와 같이 어드바이스 메소드가 결합될 포인트컷을 참조해야 한다.
  • AOP 기능에서 가장 중요한 Aspect 설정은 @Aspect 어노테이션을 사용하여 설정한다.
  • Aspect를 설정하기 위해서는 반드시 포인트컷+어드바이스 결합이 필요하다.
  • Advice에서의 5가지 종류 중 After-returning과 After-Throwing 어드바이스는 특이하게 포인트컷 지정을 pointcut 속성을 사용하여 포인트컷을 참조한다.
    • 그 이유는 비즈니스 메소드 수행 결과를 받아내기 위해 바인드 변수를 지정해야 하기 때문이다.
// AfterReturning은 returning 바인드 변수를 통해 비즈니스 로직의 결과 값을 받을 수 있다.
@AfterReturning(pointcut="getPointcut()", returning="returnObj")
// AfterThrowing은 throwing 바인드 변수를 통해 예외 처리 객체의 값을 받을 수 있다.
@AfterThrowing(pointcut="getPointcut()", throwing="exceptObj")

 

독립된 클래스로 포인트컷 분리

  • 어노테이션으로 Aspect를 설정할 때 가장 큰 문제점은 어드바이스 클래스마다 포인트컷 설정이 포함되면서, 비슷하거 같은 포인트컷이 반복 선언되는 재사용 불가의 문제가 발생한다.
  • 그래서 스프링은 이런 문제를 해결하고자 포인트컷을 아래와 같이 외부에 독립된 클래스에 따로 설정한다.
// 포인트컷을 독립적으로 관리하는 클래스를 설정.
@Aspect
public class PointCommon {
    @Pointcut("execution(생략...)")
    public void allPointcut(){}

    @Pointcut("execution(생략...)")
    public void allPointcut(){}
}
// 공통적으로 설정한 PointcutCommon의을 참조하여 포인트컷을 사용할 수 있다.
@Service
@Aspect
public class BeforeAdvice {
    @Before("PointcutCommon.allPointcut()")
    public void beforeLog(){//생략...}
} 
  • 이런식으로 독립적으로 관리하는 것이 포인트컷을 사용하는데 있어 더욱 효율적이고 유지보수에도 용이하다.

 

지시자(PCD, AspectJ pointcut designators)별 구현 방법

  • 먼저 아래와 같은 코드가 있다고 해보자
@Component
public class AppRuner implements ApplicationRunner {

  @Autowired
  EventService eventService;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    eventService.created();
    eventService.operation();
    eventService.deleted();
  }
}
@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");
  }
}
  • 이제 위 코드에 3가지 방법으로 AOP를 구현해 보겠다.
    1. execution expression 기반
// 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;
  }
}
    1. Annotation 기반
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerfLogging {
}
@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;
  }
}
  • 아래와 같이 적용시킬 메소드에 @PerfLogging 을 붙여주면 된다.
@Component
public class SimpleServiceEvent implements EventService {

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

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

  @Override
  public void deleted() {
    ...
  }
}
  • 위의 코드를 실행시키면 @PerfLogging 가 붙은 created(), operation() 메소드만 AOP가 적용된 것을 확인할 수 있다.
  • 특정 bean 기반
// 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;
  }
}
  • 위와 같이 빈으로 등록된 simpleServiceEvent 내 모든 public 메소드에다가 적용시킬 수도 있다.

 

 

REFERENCES

'Spring' 카테고리의 다른 글

BindingResult  (0) 2022.02.26
Bean Validation (BindingResult 개념을 먼저 숙지 해야된다.)  (0) 2022.02.26
ApplicationRunner,CommandLineRunner  (0) 2022.02.26
API 예외처리  (0) 2022.02.26
@Valid, @Validated  (0) 2022.02.26