7장. AOP 프로그래밍

2021. 10. 6. 13:54Spring

728x90

교수님 요약

스프링 프로그래밍 주요 사항

제공 기능들을 빈 컨테이너로 구현

  • 컨테이너의 내용은 설정클래스 혹은 설정파일로 기술
  • 컨테이너 기능의 사용은 자바 어노테이션으로 기술

스프링 MVC 등 관련 프레임워크들을 제공

  • 제작 어플리케이션의 골격(아키텍쳐) 코드 제공

객체 사이의 느슨한 결합

  • 스프링 DI (외부 설정에 의한 의존 주입)
    • 자바 : 접근제한자에 의한 캡슐화 달성

구현 코드의 높은 응집력

  • 스프링 AOP (관점에 따른 구현 객체 분리)
    • 자바 : 상속에 의한 중복코드 축소 및 응집력 강화

AOP : 관점지향 프로그래밍

  • 구현 코드의 응집력을 높이기 위해 객체 구현시 공통기능 객체와 핵심기능 객체를 분리하여 제작하는 기술
    • 즉 객체가 수행해야할 작업을 관점 중심으로 분리하여 별도로 객체로 구현한 후 런타임 때 프록시를 사용하여 이들 객체의 구현 코드를 통합 (Weaving) 하여 수행.
  • 사용자 인증, 트랜잭션 처리, 캐싱 수행, 수행시간 측정 등 많은 응용에서 활용.

반복 vs 순환

  • 절차지향의 알고리즘 구현 혹은 플로우차트 작성시에 중요시하는 항목.
  • 패키지 (타입 관리 단위)
  • 타입 (실체 생성 단위)
  • 멤버 (메서드 : 호출 단위)
  • 문장 (실행 단위)
  • 토큰 (문법 최소 단위)
  • 등 각 단위의 의미를 잘 활용.
    • 반복 처리는 함수 단위를 활용하지 못하고 있고
    • 순환 처리는 함수 단위를 잘 활용하고 있음.

스프링 AOP

1. AOP 설정 구현

  • 자바 설정 vs xml 설정 비교
    • 사용 언어
    • 설정 파일의 위치
    • AOP 설정

2. Proxy 생성방식

  • 인터페이스 구현
  • 클래스 상속

3. Pointcut 표현식

  • execution 명시자
  • within 명시자
  • bean 명시자

4. Advice 적용 순서

  • 하나의 조인포인트에 여러 어드바이스가 적요될 때

5. Pointcut 재사용

  • 여러 어드바이스에서 동일 포인트컷을 사용할 때.

1. 프로젝트 준비

AOP 사용하기 위해서는

  • pom.xml에 spring-context 모듈 추가 필요

    ⇒ 위 모듈을 추가하면 spring-aop도 함께 의존대상에 포함됨.

  • pom.xml에 aspectweaver 의존 추가 필요.

    ⇒ 위 라이브러리 포함해야 AOP관련 어노테이션 사용 가능


2. 프록시와 AOP

프록시를 사용하면 기존 코드를 수정하지 않고 코드 중복도 피할수 있다.

공통 기능 객체

⇒ 여러 객체에서 공통으로 적용할 수 있는 기능을 가지는 객체.

프록시 객체

책 설명은 잘못되었다고 함.

  • 설정 정보를 사용하여 스프링이 자동 생성하는 AOP 실행 대행자.

  • 핵심 기능에 공통 기능이 붙어서 돌아갈 수 있게 도와주는 객체.

  • 구현 자체는 핵심 기능의 인터페이스로 되어있어야 한다.

  • 핵심 기능의 인터페이스를 구현하고 있고, 그 프록시 안에서 공통 기능도 호출하고 핵심기능도 호출해서 핵심기능과 공통기능을 사용할 수 있게 해주는 객체.

    ⇒ 책에서는 핵심기능을 호출하고 공통기능을 제공한다고 되어있는데 잘못된 설명임. 공통기능을 제공하는것은 공통기능객체임.

  • 핵심기능과 공통기능이 분리되어있지만 두개가 다 돌아갈수 있도록 해주는것이 프록시 객체

    교수님

    클라이언트는 핵심기능을 쓰는줄알고 들어갔는데 프록시가 붙어서 공통기능 객체를 끌고와서 공통적인 기능을 수행하고, 핵심적인 기능도 수행하게 해주는게 프록시라요

※ AOP에서의 대상 객체

⇒실제 핵심기능을 실행하는 객체

공통 기능 구현과 핵심 기능 구현을 분리하는것이 AOP의 핵심이다.

또한 공통기능과 핵심기능이 분리되어있지만 두개가 작동할 수 있도록 도와주는것이 프록시 객체.

※ 같은말을 계속 반복하는거 같지만, 그만큼 중요하단 소리같으니 잘 기억하기.

2-1. AOP

  • AOP는 Aspect Oriented Programming의 약자.
  • 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법.
  • 핵심기능과 공통기능의 구현을 분리함으로써 핵심 기능을 구현한 코드의 수정없이 공통 기능을 적용할 수 있게 만들어준다.

AOP의 기본 개념은 핵심 기능에 공통 기능을 삽입하는 것이다. 즉 핵심 기능의 코드를 수정하지 않으면서 공통 기능의 구현을 추가하는것이 AOP이다.

⇒ 관점에 따른 구현분리를 통해 구현 코드의 높은 응집력을 얻음.

핵심 기능에 공통기능을 삽입하는 방법

  1. 컴파일 시점에 코드에 공통 기능을 삽입하는 방법

    ⇒ AOP 개발도구가 소스코드를 컴파일 하기 전에 공통 구현 코드를 소스에 삽입하는 방식으로 동작.

  2. 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입하는 방법

    ⇒ 클래스를 로딩할 때 바이트 코드에 공통 기능을 클래스에 삽입하는 방식으로 동작.

  3. 런타임에 프록시 객체를 생성해서 공통 기능을 삽입하는 방법

    스프링이 제공하는 AOP 방식은 프록시를 이용한 세번째 방식임. 두번째 방식을 일부 지원하지만 널리 사용되는 것은 프록시를 사용하는 방법임.

프록시 방식은 아래 그림처럼 실제 객체의 기능을 실행하기 전과 후에 공통기능을 호출함.

스프링 AOP는 프록시 객체를 자동으로 만들어 줌.

※ AOP 주요 용어

  • Aspect : 여러 객체에 공통으로 적용되는 공통 기능.

    ⇒ 트랜잭션이나 보안 등이 Aspect의 좋은 예.

    교수님 : 공통기능 자체

  • Advice : 언제 공통 기능을 핵심 로직에 정의할 지를 정의. 타이밍이 고려된 공통 기능.

    ⇒ 예를들어 '메서드를 호출하기 전'(언제) 에 '트랜잭션 시작'(공통 기능) 기능을 적용한다는것을 정의.

    ※ Advice의 종류

    이 중에서 널리 사용되는 것은 Around Advice 이다. (책에서는 Around Advice만 다룸.)

    교수님 : Aspect의 시점 혹은 범위를 묶어서 Advice라 함.

  • Pointcut : Joinpoint의 부분 집합으로서 실제 Advice가 적용되어야 하는 Joinpoint를 의미.

    ⇒ 스프링에서는 정규표현식이나 AspectJ의 문법을 이용하여 Pointcut을 정의할 수 있다.

    교수님 : Joinpoint를 실제 적용하는 지점

    핵심에다 공통을 적용할 대상을 표현 (Pointcut) 그 대상중에 어느것 하나가 걸렸다 걸렸다 하면 이게 조인포인트라요(Joinpoint)

    동일한 공통 기능객체를 적용할 객체들의 모음 같은 느낌. 실체 호출된 객체의 정보는 JoinPoint

    포인트컷은 대상객체, 핵심기능객체를 지목하는거라요. 어느 한개가 아니라 묶음으로.

  • Joinpoint : Advice가 적용되는 현재 메서드. 현재 적용되고 있는 AOP 적용 지점.

    ⇒ 메서드 호출, 필드값 변경 등이 Joinpoint에 해당.

    스프링은 프록시를 이용해서 AOP를 구현하기 때문에 메서드 호출에 대한 Joinpoint만 지원.

    교수님 : 적용할 대상을 묘사

    현재 호출된 핵심기능객체(대상객체)의 모든 정보를 담고있는 객체

    pointcut 집합속에서 현재 적용되고있는 메서드가 joinpoint

  • Weaving : 공통 기능을 핵심 기능에 적용하는 행위. Advice를 핵심 로직 코드에 적용하는 것


3. 스프링 AOP 구현

스프링 AOP를 이용해서 공통기능을 구현하고 적용하는 방법

  1. 공통기능객체(Aspect)로 사용할 클래스에 @Aspect 어노테이션을 붙인다.

  2. @Pointcut 어노테이션으로 공통 기능을 적용할 Pointcut을 정의한다.

  3. 공통 기능을 구현한 메서드에 @Around 어노테이션을 적용한다

    3-1 @Aspect, @Pointcut, @Around를 이용한 AOP 구현

    • 개발자는 공통기능을 제공하는 Aspect 구현클래스를 만들고,

    • 자바 설정을 이용해서 Aspect를 어디에 적용할지 설정.

      공통기능(Aspect) 클래스

    • @Aspect 어노테이션을 적용한 클래스는 Advice와 Pointcut을 함께 제공한다.

      @Pointcut은 공통기능을 적용할 대상을 설정한다.

      → 여기서는 chap07 패키지와 그 하위 패키지에 위치한 public 메서드를 설정.

      어노테이션에 사용된 execution 명시자는 뒤에서 살펴봄.

    • @Around 어노테이션은 Around Advice를 설정함.

      @Around 의 어노테이션값이 "publicTarget()"인데 이는 publicTarget() 메서드에 정의한 Pointcut에 공통기능을 적용한다는것을 의미.

      publicTarget() 메서드는 chap07 패키지와 그 하위 패키지에 위치한 public메서드를 Pointcut으로 설정하고 있으므로, 해당하는 메서드에 @Around가 붙은 measure() 메서드를 적용한다.

    • 19, 22행의 ProceedingJoinPoint는 실체 호출된 객체의 정보를 담고있는 객체.

      ⇒ 22행의 proceed() 메서드를 사용해서 실제 대상 객체의 메서드를 호출한다.

      proceed() 메서드를 호출하면 대상 객체의 메서드가 실행되므로 이 코드 이전과 이후에 공통기능을 위한 코드를 위치시키면 된다.

      ※ 시그니처

      • 자바에서 메서드 이름과 파라미터를 합쳐서 메서드 시그니처라고 한다.

      • 메서드 이름이 다르거나 파라미터 타입, 개수가 다르면 시그니처가 다르다고 표현한다.

        ⇒ 오버라이딩 할때 시그니처로 판단.

      • 자바에서 메서드의 리턴타입이나 익셉션 타입은 시그니처에 포함되지 않는다.

        위의 예제에서는 26, 28, 29행에서 시그니처를 구하는데 메서드 동작은 아래와 같다.

      • 26행 ProceedingJoinPoint객체.getSignature() : 호출한 메서드의 시그니처

        ⇒ 이후 getName() 메서드를 이용해서 호출한 메서드의 이름을 구함.

        → factorial

      • 28행 ProceedingJoinPoint.getTarget() : 대상 객체

        ⇒ 호출한 대상객체를 getClass()로 리플렉션 후 getSimpleName()으로 클래스의 이름을 구함.

        → RecCalculator

      • 29행 ProceedingJoinPoint.getArgs() : 호출한 메서드의 인자 목록

        → [5]

[리스트 7.7] 설정파일

@Configuration
@EnableAspectJAutoProxy // 프록시 만드는 어노테이션
public class AppCtx {
    @Bean
    public ExeTimeAspect exeTimeAspect() {
        return new ExeTimeAspect(); // @Aspect 클래스
    }

    @Bean
    public Calculator calculator() { // 인터페이스 타입으로 빈을 설정
        return new RecCalculator(); // @Pointcut에 속하므로 Aspect 적용
    }
}
  • @Aspect 어노테이션을 붙인 클래스를 공통 기능으로 적용하려면 @EnableAspectJAutoProxy 어노테이션을 설정클래스에 붙여야 한다.

  • @EnableAspectJAutoProxy 어노테이션을 추가하면 스프링은 @Aspect 어노테이션이 붙은 빈 객체를 찾아서 빈 객체의 @Pointcut 설정과 @Around 설정을 사용한다.

    ⇒ 여기서는 ExeTimeAspect()클래스가 bean으로 만들어지고 이를 사용.

  • Calculator 타입은 chap07 패키지에 속하므로,

    ※ Enable류 어노테이션

    • @Enable로 시작하는 어노테이션은 관련 기능을 적용하는데 필요한 다양한 스프링 설정을 대신 처리한다.
    • 예를들어 @EnableAspectJAutoProxy 어노테이션은 프록시 생성과 관련된 객체를 빈으로 등록한다.
    • @Enable류의 어노테이션은 복잡한 스프링 설정을 대신하기 때문에 개발자가 쉽게 스프링을 사용할 수 있도록 만들어준다.

[리스트 7.8] MainAspect

public class MainAspect {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = 
                new AnnotationConfigApplicationContext(AppCtx.class);

        Calculator cal = ctx.getBean("calculator", Calculator.class);
        long fiveFact = cal.factorial(5);
        System.out.println("cal.factorial(5) = " + fiveFact);
        System.out.println(cal.getClass().getName());
        ctx.close();
    }
}

실행결과

1번째 줄은 measure()가 실행되어 출력된 것.

★실행흐름★ 중요함. 외워 그냥

클라이언트에서는 RecCalc를 불렀는데 프록시가 만들어진다는 겁니다.

  1. main에서 factorial(5)를 실행.

  2. 프록시 객체가 만들어지고

  3. Aspect인 measure() 가 실행됨

  4. measure() 안에서 joinpoint.procced() 실행

    ⇒ 실제 대상 객체의 메서드 호출

  5. 실제 호출 메서드가 동작 (factorial(5))

  6. finally를 통해 return이후 동작을 수행.

3-2 ProceedingJoinPoint의 메서드

joinpoint에 관한 정보를 모두 가지고 있다. 아래 함수는 잘 몰라도 됨

  • proceed() : 실제 대상 객체의 메서드를 호출한다.

  • Signature getSignature() : 호출되는 메서드에 대한 시그니처 정보를 구한다.

    Signature 인터페이스는 다음 메서드를 제공함.

    • String getName() : 호출되는 메서드의 이름을 구한다.
    • String toLongString() : 호출되는 메서드를 완전하게 표현한 문장을 구한다. (메서드의 리턴타입, 파라미터 타입이 모두 표시)
    • String toShortString() : 호출되는 메서드를 축약해서 표현한 문장을 구한다. (기본 구현은 메서드의 이름만을 구한다)
  • Object getTarget() : 대상 객체를 구한다.

  • Object[] getArgs() : 파라미터 목록을 구한다.

4. xml vs 자바 설정

설정파일의 위치

  • xml : src/main/resourecs
  • 자바 : src/main/java

AOP 설정

  • xml : 모든 설정이 xml 파일에 다 들어있음.

    ⇒ 이건 객체클래스(java)고, 이전 설정파일(xml)이고 ... 등의 설정파일과 객체클래스 명확히 구분 가능. 형식적으로 좋음.

    ⇒ 자바 코드만 보고는 내용을 알 수가 없음. xml을 보면서 해야함.

  • 자바 : 큰 설정은 설정클래스에서, 세부적인 설정은 객체 클래스에서. 설정정보 분산.

    ⇒ 공통기능이 적용될 핵심[대상] 기능 객체에 대한 Pointcut 설정이 공통 기능 객체 클래스에 표현되어 내용 파악에 좋음. 직관적임.

    ⇒ 설정이 분산되어서 설정이 한눈에 안들어옴. 형식적으로 불편.

5. 프록시 생성 방식

프록시 생성방식에는 크게 두 가지 방법이 있음.

  1. 인터페이스 구현

    ⇒ 핵심 대상 객체의 인터페이스를 구현하는 프록시 클래스로부터 만든 프록시 객체

    ⇒ 업캐스팅 되어 인터페이스의 내용만 사용 가능

  2. 클래스 상속

    ⇒ 핵심 대상 객체의 클래스를 상속받는 프록시 클래스로부터 만든 프록시 객체

    ⇒ 인터페이스에는 없는 핵심 대상의 멤버를 모두 사용 가능

스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 빈 객체가 인터페이스를 상속하면 인터페이스를 이용해서 프록시를 생성한다.

오른쪽처럼 빈의 실제 타입이 RecCalculator라고 하더라도, "calculator" 이름에 해당하는 빈 객체의 타입은 왼쪽 그림처럼 Calculator 인터페이스를 상속받은 프록시 타입이 된다.

즉 RecCalculator와 프록시 객체는 형제관계 이므로 업,다운캐스팅이 불가능하다.

RecCalculator의 자식으로 프록시를 만드려면 추가 방법이 필요함.

@EnableAspectJAutoProxy 어노테이션의 proxyTargetClass 속성을 true로 해주면 리턴하는 객체가 상속한 인터페이스를 상속받는것이 아닌 현재 리턴하는 객체의 클래스를 상속하는 프록시를 만들어 줌.

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppCtxWithClassProxy {
    ...
}

6. 명시자 표현식

특수문자 의미

  • 각 패턴은 '*'을 이용하여 모든 값을 표현할 수 있다.
  • '..'을 이용하여 0개 이상이라는 의미를 표현할 수 있다.

execution 명시자

execution 명시자는 Advice를 적용할 메서드를 세세하게 지정할 때 사용한다.

execution 명시자는 within과 bean을 모두 포함하고 있음.

기본형식

⇒ execution([수식어] 리턴타입 [클래스이름].메서드이름(파라미터))

  • 수식어패턴 : 생략 가능. public, protected 등이 온다. 스프링 AOP는 public에만 적용할수 있기에 사실상 public만 의미있다.
  • 클래스패턴 (생략가능)
    • ..* : ..앞의 패키지 및 하위 패키지 포함 ex) chap07..* ⇒ chap07 패키지 및 하위패키지의 모든 클래스
    • .* : 해당 패키지. ex) chap07.* ⇒ chap07 패키지에 있는 모든 클래스
  • 파라미터패턴
    • (*) → 1개 파라미터
    • `(, )` → 2개 파라미터
    • (..) → 0개 이상의 파라미터
    • 비워두면 파라미터 없는걸 의미.

within 명시자

within 명시자는 메서드가 아닌 특정 타입에 속하는 메서드를 Pointcut으로 설정할 때 사용.

⇒ ex) 특정 클래스에 존재하는 모든 메서드, 특정 패키지에 존재하는 모든 클래스의 메서드.

bean 명시자

bean 명시자는 스프링 빈 이름을 이용하여 Pointcut을 정의한다.

bean 명시자는 빈 이름의 패턴을 갖는다.

7. Advice 적용순서

한 Pointcut에 여러 Advice를 적용할 수 있음.

Advice를 여러개 적용할때는, 공통기능이 다음 공통기능을 부르고, 최종 공통기능이 핵심기능을 부르기 때문에, 중간에 리턴해 버리면 뒤의 부분이 실행이 안됨.

현재는 실행 순서가 Cache를 부르고 Exetime을 부르므로,

캐시가 있다면 바로 return을 해버리므로, ExeTime이 실행이 되지않음.

  1. Cache의 execute()메서드의 proceed()가 다음 공통기능인 measure()를 호출
  2. measure()에서 proceed()가 실제 공통기능인 factorial()을 호출
  3. facorial이 수행이 끝나고, measure()로 돌아와서 다음작업 수행
  4. measure()이 끝나면 execute()로 돌아와서 다음작업 수행

advice의 실행순서를 바꾸기 위해서는 @Order 어노테이션을 이용.

공통객체 클래스에 @Order 어노테이션을 주면 Order의 값이 작은순으로 먼저 실행됨.

8. Pointcut 재사용

@Pointcut 어노테이션이 아닌 @Around 어노테이션의 execution 명시자를 적용할 수 있음.

이를 이용해 private Pointcut 설정 메서드를 public으로 바꿔서 재사용 가능

@Pointcut(execution(* chap07..*(..))")
private publicTarget(){}

@Pointcut(execution(* chap07..*(..))")
public publicTarget(){}

이후 Around 어노테이션에서 해당 Pointcut이 붙은 메서드를 지정해주면 됨.

보통 아래와 같이 응용

출처 : 최범균,『스프링5 프로그래밍 입문』, 가메출판사

728x90