관점지향 프로그래밍 - AOP(Aspect Oriented Programming)

박민주·2024년 1월 8일
0

AOP

애플리케이션 전반에 걸쳐 적용되는 공통 기능을 비즈니스 로직으로부터 분리해내는 것을 AOP(Aspect Oriented Programming, 관심 지향 프로그래밍)라고 합니다.

스프링 어플리케이션은 특별한 경우를 제외하고는 MVC 웹 어플리케이션으로 만들어진다. 그리고 크게 세가지 Layer로 정의한다.

  • Web Layer : Rest API를 제공하며, Client중심의 로직을 적용한다.
  • Business Layer - 내부 정책의 따른 로직을 개발하며, 주로 해당부분을 개발한다. ex) Service
  • Data Layer - 데이터 베이스 및 외부 연동을 담당. ex) repository

횡단관심

aop는 메소드들 또는 특정구역에 반복되는 로직들을 한 곳에 몰아서 코딩을 할 수 있게 해준다.

어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다. 

예로들어 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.

AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미다. 이때, 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 횡단관심(흩어진 관심사, Crosscutting Concerns)라 부른다. 
출처: https://engkimbs.tistory.com/entry/스프링AOP [새로비:티스토리]

public vodi Aclass() {
	System.out.println("start");
    //TODO : 각 메소드의 로직
    System.out.println("stop");
}

public vodi Bclass() {
	System.out.println("start");
    //TODO : 각 메소드의 로직
    System.out.println("stop");
}

위와 같이 서로 다른 메소드들의 실행시작과 끝을 알아야 하는경우 각 메소드마다 넣어주면 하나라도 변경 될 시 일일히 다 수정해야하는 문제를 받아들여야할 것이다. 이렇게 반복되는 코드들을 횡단관심이라 할 수 있겠다.

AOP적용해보기

AOP를 사용하기 위해서는 Gradle의 경우 다음과 같은 의존성을 추가해야한다.
implementation 'org.springframework.boot:spring-boot-starter-aop'

AOP 주요용어

  • Join point
    추상적인 개념 으로 advice가 적용될 수 있는 모든 위치를 말합니다.
    ex) 메서드 실행 시점, 생성자 호출 시점, 필드 값 접근 시점 등등..
    스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메서드 실행 지점
  • Pointcut
    조인 포인트 중에서 advice가 적용될 위치를 선별하는 기능
    스프링 AOP는 프록시 기반이기 때문에 조인 포인트가 메서드 실행 시점 뿐이 없고 포인트컷도 메서드 실행 시점만 가능
  • Target
    advice의 대상이 되는 객체
    Pointcut으로 결정
  • advice
    실질적인 부가 기능 로직을 정의하는 곳
    특정 조인 포인트에서 Aspect에 의해 취해지는 조치
  • Aspect
    advice + pointcut을 모듈화 한 것
    @Aspect와 같은 의미
  • Advisor
    스프링 AOP에서만 사용되는 용어로 advice + pointcut 한 쌍
  • Weaving
    pointcut으로 결정한 타겟의 join point에 advice를 적용하는 것
  • AOP 프록시
    AOP 기능을 구현하기 위해 만든 프록시 객체
    스프링에서 AOP 프록시는 JDK 동적 프록시 또는 CGLIB 프록시
    스프링 AOP의 기본값은 CGLIB 프록시

주요 Annotation

예제 코드

@Aspect사용시 Bean등록을 해줘야한다.

  • @Bean 으로 수동 등록
  • @Component 로 컴포넌트 스캔 사용해서 자동 등록
  • @Import 를 사용해서 파일 추가
@Aspect // aop로 동작하기 위해서
@Component // 스프링에서 관리하기위해
public class ParameterAop {

    @Pointcut("execution(* com.example.aop.controller..*.*(..))") 
    // controller하위의 모든 메소드에 적용
    private void cut(){}

    @Before("cut()") // Pointcut이 실행될떄 (cut())
    public void before(JoinPoint joinPoint){
        MethodSignature methodSignature = 
        	(MethodSignature) joinPoint.getSignature();
        System.out.println(methodSignature.getName());
        Object[] args = joinPoint.getArgs();

        for(Object obj : args) {
            System.out.println("type : " + obj.getClass().getSimpleName());
            System.out.println("value : " + obj);
        }
    }

    @AfterReturning(value = "cut()", returning = "returnObj") 
    // 들어가는 지점에 대한 정보가 있는 JoinPoint 객체
    public void afterReturn(JoinPoint joinPoint, Object returnObj) {
        System.out.println("return obj");
        System.out.println(returnObj);
    }
}
@RestController
@RequestMapping("/api")
public class RestApiController {

    @GetMapping("/get/{id}")
    public String get(@PathVariable Long id, @RequestParam String name) {
        return id + " " + name;
    }

    @PostMapping("/post")
    public User post(@RequestBody User user) {
        return user;
    }
}

실행결과

Around와 Custom Annotation을 만들어 적용하기

Custom Annotation

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {
}
@Aspect
@Component
public class TimerAop {
    @Pointcut("execution(* com.example.aop.controller..*.*(..))")
    private void cut(){}

    @Pointcut("@annotation(com.example.aop.annotation.Timer)")  
    // Timer annotation이 설정된 메소드만
    public void enableTimer(){}

    @Around("cut() && enableTimer()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Object result = joinPoint.proceed(); 
        //proceed를 호출하면 실질적인 메소드가 실행된다. 
        //특정한 값이나 객체가 return 되면 Object에 들어간다.

        stopWatch.stop();

        System.out.println("total time : " + stopWatch.getTotalTimeSeconds());
    }
}

cut()은 controller 패키지 하위의 모든 메소드들이 실행되면,
enableTimer()는 @Timer annotation가 실행되면 작업을 수행한다.
cut() 메소드와 enableTimer() 메소드가 실행되면 around()메소드가 실행된다.

@Around의 경우 ProceedingJoinPoint를 인자로 받아 타겟 메서드를 실행하는 proceed 코드를 반드시 적어야 target 메서드를 호출하지만 나머지 4개의 애노테이션의 경우, 타겟 메서드를 호출하는 proceed를 명시하지 않아도 알아서 호출됩니다.

  • @Before
    @Before은 조인 포인트 실행 전(타겟 메서드 실행 전)에 작업을 수행합니다.
public void doBefore(JoinPoint joinPoint) {
    log.info("[before] {}", joinPoint.getSignature());
}

@Around와 달리 proceed 코드가 없이 정의한 로직이 수행된 후 자동으로 target 메서드를 호출합니다.

  • @AfterReturing
    @AfterReturing은 조인 포인트가 정상적으로 실행되고 값을 반환할 때 실행됩니다.(타겟 메서드가 예외가 아닌 정상값을 반환할 때)
@AfterReturning(value = "execution(* com.example.mvc.order..*(..))", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
    log.info("[return] {} return={}", joinPoint.getSignature(), result);
}

다른 애노테이션과 다르게 속성값으로 returning이 추가되었습니다.
이 부분에는 Target 메서드가 반환하는 변수명을 적어주고, advice 메서드의 인자로 변수명을 일치시켜준다면 해당 값을 가져와서 사용할 수 있습니다.
여기서 주의할점은 returning 값을 받는 인자의 타입이 해당 리턴 값의 부모타입 혹은 같은 타입이어야만 해당 Advice가 동작합니다.
타입이 부모 혹은 동일 타입이 아니라면 해당 Advice 자체가 동작하지 않으니 주의해야 합니다.

  • @AfterThrowing
    @AfterThrowing는 타겟 메서드 실행이 예외를 던져서 종료될 때 실행됩니다.
@AfterThrowing(value = "execution(* com.example.mvc.order..*(..))", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
    log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
}

@AfterReturning과 비슷하게 throwing 속성이 추가되고 advice 메서드 인자에 변수명을 일치시키면 받아서 사용할 수 있습니다.

@Around
@Around는 타겟 메서드의 실행이 종료되면 무조건 실행됩니다.(try catch의 finally문과 같습니다.)

동일한 @Aspect 안에서는 위와 같은 우선순위로 동작합니다.
즉, 동일한 @Aspect 안에서 여러 개의 Advice가 존재하는데 타겟 메서드가 여러 Advice의 대상이 될 경우 다음과 같이 동작합니다.

Around -> Before -> AfterThrowing -> AfterReturning -> After -> Around

스프링 JoinPoint 에서 제공하는 메소드들.

  • Signature getSignature()
    - 클라이언트가 호출한 메소드의 시그니처 (리턴타입, 이름, 매개변수) 정보가 저장된 Signature 객체를 리턴
  • Object getTarget()
    - 클라이언트가 호출한 비즈니스 메소드를 포함하는 비즈니스 객체를 리턴(해당 클래스 객체를 리턴)
  • Object[] getArgs()
    - 클라이언트가 메소드를 호출할 때 넘겨준 인자 목록을 Object 배열로 리턴

참고

https://velog.io/@backtony/Spring-AOP-%EC%B4%9D%EC%A0%95%EB%A6%AC

https://heidi-mood.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81-AOP-JoinPoint-%EC%99%80-%EB%B0%94%EC%9D%B8%EB%93%9C-%EB%B3%80%EC%88%98

profile
개발자 되고싶다..

0개의 댓글