본문 바로가기
Spring Framework

관점 지향 프로그래밍(AOP)

by mozzi329 2022. 8. 17.
728x90

 

    (사실 전부 다 중요합니다. 오해 ㄴㄴ)

    📌 객체 지향 프로그래밍

    이전과 현재에 많은 프로젝트는 객체지향 프로그래밍(OOP, Object Oriented Programming) 패러다임을 지향하며 프로그래밍하고 있다. 정의된 기능들을 재사용하기 위해 동작보다는 객체를 중심으로 프로그래밍하는 OOP가 등장했다.

    OOP의 핵심은 공통된 목적을 띈 데이터와 동작을 묶어 하나의 객체로 정의하는 것이다. 객체를 적극적으로 활용함으로써 기능을 재사용할 수 있는 것이 큰 장점이다. 객체를 잘 활용하기 위해선 관심사 분리(Separation of Concerns)의 디자인 원칙을 준수해야 한다.

     

    특정 관심사 업무 코드에 트랜잭션, 보안, 로깅 등의 코드가 함께 존재한다. 트랜잭션, 보안, 로깅 코드는 비록 업무와는 관련이 없지만 애플리케이션에 필수적인 부가기능이다. 이러한 코드들은 불특정 다수의 클래스에 존재할 수밖에 없다. 관심사 관점에서는 트랜젝션, 보안, 로깅 코드와 같은 부차적인 코드들을 횡단 관심사(Cross-Cutting Concerns, 부가 기능)라고 한다. 

     

    그리고 업무 관련 코드를 핵심 관심사(Core Concerns, 핵심 기능)라고 한다.

     

    비즈니스 클래스에는 이러한 횡단 관심사와 핵심 관심사가 공존하게 된다.

    이에 따라 메소드의 복잡도가 증가하며 비즈니스 코드의 파악이 어려워진다. 또한 부가 기능의 불특정 다수 메소드가 반복적으로 구현됨에 따라 횡단 관심사의 모듈화가 어려워질 수 있다.

     

    이러한 문제점들을 보완하기 위해 AOP와 같은 개념이 등장하였다.

     

    📌 관점 지향 프로그래밍

    Aspect는 부가 기능과 해당 부가 기능을 어디에 적용할지를 정의한다. 관점 지향 프로그래밍(AOP, Aspect Oriented Programming)은 흩어진 Aspect들을 모듈화할 수 있는 프로그래밍 기법이다.

    기존의 OOP만을 사용해선 횡단 관심사 코드를 깔끔하게 분리하고 비즈니스 코드에 적용하기가 어려웠다. 이러한 OOP의 관심사 분리에 대한 한계적인 부분을 해결하고자 AOP가 등장하게 되었다.

     

    모듈화의 핵심 단위가 클래스인 OOP와는 다르게 AOP의 모듈의 핵심 단위는 관점(Aspect)이다. 관점은 여러 유형과 객체 간에 발생하는 문제(ex - 트랜젝션, 보안, 로깅 등)에 대해 모듈화가 가능하다.

     

    ✔️ AOP의 핵심 기능과 부가 기능

    애플리케이션 로직은 크게 핵심 기능과 부가 기능으로 나눌 수 있다.

    핵심 기능과 부가 기능

    • 핵심 기능(Core Concerns)
      객체가 제공하는 고유의 기능(업무 로직, 비즈니스 로직 등을 포함)들을 말한다.

    • 부가 기능(Cross-Cutting Concerns)
      핵심 기능을 보조하기 위해 제공되는 기능들을 말한다.
      - 로그 추적, 보안, 트랜잭션 기능 등이 있다.
      - 단독으로 사용되지 않고 핵심 기능과 함께 사용된다.

     

    ✔️ 여러 곳에서 공통으로 사용하는 부가 기능

    부가 기능은 보통 여러 클래스에 걸쳐서 함께 사용된다. 아래와 같은 특징의 부가 기능은 횡단 관심사가 된다.

    • 부가 기능을 여러 곳에 적용하려면 번거롭고 중복 코드가 생기게 된다.
    • 부가 기능에 수정이 필요하게 된다면 사용되는 클래스에 모두 하나씩 찾아가면서 수정을 해야한다.

     

    ✔️ AOP가 필요한 이유

    소프트웨어 개발에서 변경 지점은 하나가 될 수 있도록 잘 모듈화되어야 한다.

    부가 기능처럼 특정 로직을 애플리케이션 전반에 적용하는 문제는 일반적인 OOP 방식으로는 해결이 어렵기 때문에 핵심 기능과 부가기능을 분리하는 AOP 방식이 필요하다.

     

    📌 AOP 용어

    ✔️ 타겟(Target)

    핵심 기능을 담고 있는 모듈로 타겟은 부가기능을 부여할 대상이 된다.

    • 어드바이스를 받는 객체이며 포인트컷으로 결정된다.

     

    ✔️ 애스팩트(Aspect)

    객체지향 모듈을 오브젝트라 부르는 것과 비슷하게 부가기능(공통 기능) 모듈을 애스펙트라고 부른다. 

    • 애스팩트는 부가될 기능을 정의한 어드바이스어드바이스를 어디에 적용할지를 결정하는 포인트컷을 함께 갖고 있다.
    • 참고로 AOP라는 뜻 자체가 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리시켜 애스팩트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법을 말한다.

     

    ✔️ 어드바이스(Advice)

    실질적으로 부가기능을 담은 구현체를 말한다.

    • 어드바이스의 경우 타겟 오브젝트에 종속되지 않기 때문에 순수하게 부가기능에만 집중할 수 있다.
    • 어드바이스는 애스펙트가 '무엇'을 '언제'할지를 정의한다.

     

     ✔️ 프록시(Proxy)

    타겟을 감싸서 타겟의 요청을 대신 받아주는 랩핑(Wrapping) 오브젝트이다.

    • 호출자(클라이언트)에서 타겟을 호출하게 되면 타겟이 아닌 타겟을 감싸고 있는 프록시가 호출된다.
    • 타겟 메서드의 실행 전에 선처리, 타겟 메서드를 실행한 후의 후처리를 실행시키도록 구성되어 있다.
    • 스프링에서의 AOP 프록시는 JDK 동적 프록시(인터페이스 관점) 또는 CGLIB 프록시(클래스 관점)를 말한다.

     

    ✔️ 조인포인트(Join Point)

    어드바이스가 적용될 수 있는 위치(메서드)를 이야기한다.

    • 클래스의 초기화, 객체 인스턴스화, 메서드 호출, 필드 접근, 예외 발생과 같은 애플리케이션 실행 흐름에서의 특정 포인트를 의미한다.
    • 애플리케이션에 새로운 동작을 추가하기 위해 조인포인트에 관심 코드(Aspect Code)를 추가할 수 있다.
    • 횡단 관심은 조인포인트 전/후에 AOP에 의해 자동으로 추가된다.
    • 스프링 AOP에서는 프록시 방식을 사용하므로 조인 포인트는 항상 메서드를 가리키는 것으로 제한된다.
    • 어드바이스 적용이 필요한 곳은 애플리케이션 내에 메서드를 갖는다.

     

    ✔️ 포인트컷(PointCut)

    부가기능이 적용될 대상(메서드)을 선정하는 방법을 이야기한다.

    • 어드바이스를 적용할 조인포인트를 선별하는 기능을 정의한 모듈이다.
    • AspectJ 표현식을 사용해서 지정한다.
    • 프록시를 사용하는 스프링 AOP는 메서드 실행 지점만 포인트컷으로 선별이 가능하다.

     

    ✔️ 위빙(Weaving)

    포인트컷으로 결정한 타겟의 조인 포인트에 실제적으로 어드바이스를 적용한다.

    • 어드바이스를 핵심 코드에 적용한다.
    • 핵심 기능 코드에 영향을 주지 않으면서도 부가 기능을 추가할 수 있다.
    • AOP 적용을 위해 AspectJ 컴파일러가 핵심 로직에 부가 기능 로직을 붙이기 위해 코드를 조작한다.
      - 컴파일 타임 시점
      - 컴파일 후 위빙 시점
      - 로드 타임(클래스 로딩) 시점
    • 런타임 시점(스프링 AOP는 런타임, 프록시 방식이다)

    확대해서 보세요..

    ✔️ 어드바이저(Adivsor)

    하나의 어드바이스와 하나의 포인트 컷으로 구성되며, 스프링 AOP에서만 사용되는 용어이다.

     

    📌 AOP를 구현하는 방법(보완 요)

    자바 애플리케이션에서 AOP를 구현하는 대표적인 방법으로 Spring AOP와 AspectJ가 있다.

    ✔️ Sping AOP

    Spring AOP는 스프링에서 사용할 수 있는 간단한 AOP 기능만을 제공한다.

    • CGlib라는 바이트코드를 조작하여 프록시 객체를 생성해주는 코드 생성 라이브러리를 사용한다.
    • 런타임 시점에 동적으로 변할 수 있는 프록시 객체를 이용하기에 앱 성능에 영향을 끼칠 수 있다. (Runtime weaving)

     

    ✔️ AspectJ

    AspectJ는 자바에서 완벽한 AOP 솔루션 제공을 목표로하는 기술이다.

    • [ .aj 파일]을 이용한 assertj 컴파일러를 추가로 사용하여 컴파일 시점이나 JVM 클래스 로드시점에 조작한다.
    • 런타임 시점에는 영향끼치지 않는다. 즉 컴파일이 완료된 이후에는 앱 성능에 영향이 없다.
    • Spring AOP에 비해 사용방법이 다양하고 내부 구조가 굉장히 복잡하다.

     

    📌 애스팩트(Aspect)

    @Aspect는 AspectJ 방식의 AspectJ 5 애너테이션 스타일로 스프링 AOP에서도 @AsepectJ 애너테이션 스타일을 지원하고 있다.

    스프링 AOP에서도 AspectJ의 AspectJ 5와 동일한 애너테이션을 해석한다.
    스프링은 포인트컷 구문 분석 및 일치를 위해 AspectJ 프레임워크가 제공하는 라이브러리를 사용하여 AspectJ 5와 동일한 애너테이션을 해석한다. 그렇지만 AOP 런타임 자체는 여전히 순수한 스프링 AOP이며, 별도의 AspectJ 컴파일러나 위버에 의존하지 않는다.

     

    ✔️ @AspectJ 지원 활성화

    Spring 설정에서 @AspectJ 지원을 활성화하기 위해서는 @EnableAspectAutoProxy 애너테이션을 추가해야 한다.

    @Configuration
    @EnableAspectJAutoProxy
    public class AppConfig {
    
    }

     

    ✔️ Aspect 선언

    @AspectJ 지원이 활성화되면 @AspectJ 관점(@Aspect 애너테이션이 있는)이 있는 클래스로 애플리케이션 컨텍스트에 정의된 모든 빈이 Spring에서 자동으로 감지되고 Spring AOP를 구성하는데 사용된다.

    import org.aspectj.lang.annotation.Aspect;
    
    @Aspect
    public class NotVeryUsefulAspect {
    
    }

     

    📌 어드바이스(Advice)

    어드바이스는 Aspect를 언제 핵심 코드에 적용할지를 정의한다. 스프링의 어드바이스 관련 애너테이션들을 통해 부가 기능을 제공하고 특정 조인 포인트에서 Aspect를 통해 조취를 취할 수 있다.

    어드바이스는 포인트컷 표현식과 연관되며 포인트컷과 일치하는 메서드 실행 전후에 실행된다. 해당 포인트컷 표현식은 명명된 포인트컷에 대한 단순 참조이거나 제자리가 선언된 포인트컷 표현식일 수 있다.

     

    ✔️ Advice 순서

    어드바이스는 기본적으로 순서를 보장하지 않는다.

    • 순서를 지정하고 싶으면 @Aspect 적용 단위로 org.springframework.core.annotation.@Order 애너테이션을 적용해야한다.
    • 어드바이스 단위가 아닌 클래스 단위로 적용할 수도 있다.
    • 하나의 Aspect에 여러 어드바이스가 존재한다면 순서를 보장받을 수 없다.
    • Aspect를 별도의 클래스로 분리해야 한다.

     

    ✔️ Advice 종류

    @Before

    조인 포인트 실행 이전에 Aspect를 실행한다.

    @Before("hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("[before] {}", joinPoint.getSignature());
    }
    • 타겟 메서드가 실행되기 전에 처리해야할 필요가 있는 부가 기능을 호출 전에 공통 기능을 실행한다.
    • Before 애노테이션을 구현한 메서드는 일반적으로 리턴타입이 void이다.
      (리턴 값이 존재하더라도 어드바이스 적용과정엔 영향을 주지 않는다)
    • 메서드에서 예외를 발생시킬 경우 대상 객체의 메서드가 호출되지 않을 수 있다.
    • 작업 흐름은 변경이 불가능하다.
    • 메서드 종료 시 자동으로 다음 타겟이 호출된다. (예외가 발생하면 다음 코드 호출 x)

     

    @AfterReturning

    조인 포인트가 정상 완료된 후 Aspect를 실행한다.

    @AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
    public void doReturn(JoinPoint joinPoint, Object result) {
        log.info("[return] {} return={}", joinPoint.getSignature(), result);
    }
    • 메서드가 예외 없이 실행된 이후에 공통 기능을 실행한다.
    • 메서드 실행이 정상적으로 반환될 때 실행된다.
    • returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름(result)와 일치해야 한다.
    • returning 절에 지정된 타입의 값을 반환하는 메서드만 대상을 실행한다.

     

    @AfterThrowing

    메서드가 예외를 던지는 경우에 실행된다.

    @AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
    public void doThrowing(JoinPoint joinPoint, Exception ex) {
        log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
    }
    • 메서드를 실행하는 도중 예외가 발생한 경우 공통 기능을 실행한다.
    • throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름(ex)와 일치해야한다.
    • throwing 절에 지정된 타입과 맞는 예외를 대상으로 실행한다.

     

    @After(finally)

    조인 포인트의 동작(정상 또는 예외)과는 상관없이 실행한다.

    • 예외 동작의 finally를 생각하면 된다.
    • 메서드 실행 후 공통 기능을 실행한다.
    • 일반적으로 리소스를 해제하는데 사용한다. (사용된 리소스의 후처리)

     

    @Around

    메서드 호출 전/후에 수행되며 가장 강력한 어드바이스이다.

    • 조인 포인트 실행 여부를 선택하거나 반환 값을 변환, 예외를 변환하는 기능들을 수행한다.
    • 메서드 실행 전/후, 예외 발생 시점에 공통 기능을 실행한다.
    • 가장 강력한 어드바이스
      조인 포인트 실행 여부 선택 - joinPoint.proceed()
      전달 값 변환 - joinPoint.proceed(args[])
      반환 값 변환
      예외 변환 - try ~ catch ~ finally가 들어가는 구문 처리가 가능하다.
    • 어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint를 사용해야 한다.
    • proceed()를 통해 대상을 실행한다.
    • proceed()를 여러번 실행할 수 있다.

     

    ✔️ @Arround만 있어도 모든 기능 수행이 가능하다. 그러나...

    강력한 어드바이스이며 대부분의 기능을 제공하지만 타겟 등 고려해야할 사항이 있을 때 정상적으로 작동이 되지 않는 경우가 있다.

     

    @Before, @After와 같은 어드바이스는 기능은 적지만 그 기능이 명확하고 일관된 기능을 제공하며, 코드도 단순하다. 각 애너테이션만 보더라도 타겟 실행 전에 어떤 일을 하는지 명확하게 알 수 있다.

     

    이러한 이유로 @Around만 사용하는 것이 능사가 아니다.

    제약을 가지더라도 @Around를 통해 발생할 수 있는 실수를 미연에 방지하여 역할을 명확히 밝히는 것이 좋을 수 있다.

     

    📌 조인포인트(Join Point)

    추상적인 개념으로써 AOP를 적용할 수 있는 지점을 의미한다. 스프링 AOP에서는 조인포인트를 인터페이스로 제공한다.
    • 어드바이스가 적용될 수 있는 위치, 메서드 실행, 생성자 호출, 필드 값 접근, static 메서드 접근 같은 프로그램 실행 지점을 나타낸다.
    • AspectJ를 사용해서 컴파일 시점과 클래스 로딩 시점에 적용하는 AOP는 바이트코드를 실제 조작하기 때문에 해당 기능을 모든 지점에 다 적용할 수 있다.
    • 프록시 방식을 사용하는 스프링 AOP는 메서드 실행 지점에만 AOP를 적용할 수 있다.
    • 프록시는 메서드 오버라이딩 개념으로 동작한다.
    • 생성자나 static 메서드, 필드 값 접근에는 프록시 개념이 적용될 수 없다.
    • 프록시를 사용하는 스프링 AOP의 조인 포인트는 메서드 실행으로 제한된다.
    • 프록시를 사용하는 스프링 AOP는 스프링 컨테이너가 관리할 수 있는 스프링 빈에서만 AOP를 적용할 수 있다.
    • JoinPoint 메서드는 어드바이스의 종류에 따라 사용방법이 다소 다르지만 기본적으로 어드바이스 메서드에 매개변수로 선언만 하면 된다.

     

    ✔️ JoinPoint 인터페이스의 주요 기능

    • JoinPoint.getArgs()
      JoinPoint에 전달된 인자를 배열로 반환한다.

    • JoinPoint.getThis()
      AOP 프록시 객체를 반환한다.

    • JoinPoint.getTarget()
      AOP가 적용된 대상 객체를 반환한다.
      클라이언트가 호출한 비즈니스 메서드를 포함하는 비즈니스 객체를 반환한다.

    • JoinPoint.getSignature()
      조언되는 메서드에 대한 설명을 반환한다.
      클라이언트가 호출한 메소드의 시그니처(리턴타입, 이름, 매개변수) 정보가 저장된 Signature 객체를 반환한다.
      Signature
      객체가 선언하는 모든 연산은 연산의 이름, 매개변수로 받아들이는 객체를 시그니처라고 한다.
      Signature가 제공하는 메서드
      ▾ String getName() : 클라이언트가 호출한 메서드의 이름을 반환한다.
       String toLongString() : 클라이언트가 호출한 메서드의 리턴타입, 이름, 매개변수를 패키지 경로까지 포함하여 반환한다.
       String toShortString() : 클라이언트가 호출한 메서드 시그니처를 축약한 문자열로 반환한다.

    • JoinPoint.toString()
      조언되는 방법에 대한 설명을 출력한다.

     

    ✔️ ProceedingJoinPoint 인터페이스의 주요 기능

    • proceed()
      다음 어드바이스나 타겟을 호출한다.

     

    📌 포인트컷(PointCut)

    포인트컷 애너테이션은 관심 조인 포인트를 결정하므로 어드바이스가 실행되는 시기를 제어할 수 있다. AspectJ는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공한다.

     

    ✔️ 포인트컷 선언

    포인트컷 선언은 이름과 매개변수를 포함하는 서명과 우리가 관심 있는 메서드 실행을 정확히 결정하는 포인트컷 표현식으로 구성된다.

    @Pointcut("execution(* transfer(..))") // 포인트컷 표현식
    private void anyOldTransfer() {} // 포인트컷 서명

    ※ 포인트컷 표현식 : AspectJ pointcut expression(AspectJ)가 제공하는 포인트컷 표현식을 줄여서 표현한 표현식이다.

     

    ✔️ 포인트컷 표현식 구성 요소

    포인트컷 구성 요소

     

    ✔️ 포인트컷 지시자

    포인트컷 표현식은 execution과 같은 포인트컷 지시자(PointCut Designator)로 시작한다.

    @Pointcut("execution(public * *(..))")

     

    ❗️ 포인트컷 지시자 종류

    종류 설명
    execution 메서드 실행 조인 포인트를 매칭한다.
    스프링 AOP에서 가장 많이 사용하며, 기능도 복잡하다.
    within 특정 타입 내의 조인 포인트를 매칭한다.
    args 인자가 주어진 타입의 인스턴스인 조인 포인트이다.
    this 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트이다.
    target Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트이다.
    @target 실행 객체의 클래스에 주어진 타입의 애너테이션이 있는 조인 포인트이다.
    @within 주어진 애너테이션이 있는 타입 내 조인 포인트이다.
    @annotation 메서드가 주어진 애너테이션을 가지고 있는 조인 포인트를 매칭한다.
    @args 전달된 실제 인수의 런타임 타입이 주어진 타입의 애너테이션을 갖는 조인 포인트이다.
    bean 스프링 전용 포인트컷 지시자이며, 빈의 이름으로 포인트컷을 지정한다.

     

    ✔️ 포인트컷 표현식 결합

    포인트컷 표현식은 &&, ||, ! 를 사용하여 결합할 수 있다. 또한 이름으로 pointcut 표현식을 참조할 수도 있다.

    @Pointcut("execution(public * *(..))")
    private void anyPublicOperation() {} // (1)
    
    @Pointcut("within(com.xyz.myapp.trading..*)")
    private void inTrading() {} // (2)
    
    @Pointcut("anyPublicOperation() && inTrading()")
    private void tradingOperation() {} // (3)

    (1) anyPublicOperation()은 메서드 실행 조인 포인트가 공용 메서드의 실행을 나타내는 경우 일치한다.

    (2) inTrading()은 메서드의 실행이 거래 모듈에 있는 경우 일치한다.

    (3) tradingOperation()은 메서드 실행이 거래 모듈의 공용 메서드를 나타내는 경우 일치한다.

     

    ❗️일반적인 포인트컷 표현식들

    표현식 설명
    execution(public * *(..)) 모든 공개 메서드 실행
    execution(* set*(..)) set 다음 이름으로 시작하는 모든 메서드 실행
    execution(* com.xyz.service.AccountService.*(..)) AccountService 인터페이스에 의해 정의된 모든 메소드의 실행
    execution(* com.xyz.service.*.*(..)) 서비스 패키지에 정의된 메서드 실행
    execution(* com.xyz.service..*.*(..)) 서비스 패키지 또는 해당 하위 패키지 중 하나에 정의된 메서드 실행
    within(com.xyz.service.*) 서비스 패키지 내의 모든 조인 포인트
    within(com.xyz.service..*) 서비스 패키지 또는 하위 패키지 중 하나 내의 모든 조인 포인트
    (스프링 AOP 전용)
    this(com.xyz.service.AccountService) AccountService 프록시가 인터페이스를 구현하는 모든 조인 포인트
    (스프링 AOP 전용)
    target(com.xyz.service.AccountService) AccountService 대상 객체가 인터페이스를 구현하는 모든 조인 포인트
    (스프링 AOP 전용)
    args(java.io.Serializable) 단일 매개변수를 사용하고 런타임에 전달된 인수가 Serializable과 같은 모든 조인 포인트
    (스프링 AOP 전용)
    @target(org.springframework.transaction.annotation.Transactional) 대상 객체에 @Transactional 애너테이션이 있는 모든 조인 포인트
    (스프링 AOP 전용)
    @annotation(org.springframework.transaction.annotation.Transactional) 실행 메서드에 @Transactional 애너테이션이 있는 조인 포인트
    (스프링 AOP 전용)
    @args(com.xyz.security.Classified) 단일 매개 변수를 사용하고 전달된 인수의 런타임 유형이 @Classified 애너테이션을 갖는 조인 포인트
    (스프링 AOP 전용)
    bean(tradeService) tradeService 라는 이름을 가진 스프링 빈의 모든 조인 포인트
    (스프링 AOP 전용)
    bean(*Service) 와일드 표현식 *Service 라는 이름을 가진 스프링 빈의 모든 조인 포인트

     

    댓글