Spring boot AOP

25gStroy·2022년 3월 24일
0

Springboot

목록 보기
9/41

Springboot AOP

AOP - 핵심 기능과 부가기능

애플리케이션 로직은 크게 핵심기능과 부가 기능으로 나눌 수 있다.
핵심기능은 해당 객체가 제공하는 고유의 기능이다.
부가기능은 핵심기능을 보자하기위해 제공되는기능이다.(로구추적기 등등..)

위 기능들이 하나의 객체에 모두 들어있게되면 여러가지 문제점이 발생 한다.
1. 공통로직이 많이 생긴다.(부가기능은 어느 객체에서나 똑같이 작동하기 때문)
2. 로직의 복잡성이 증가한다.
3. 부가기능 적용 클래스가 100개면 100개에 다 추가해야한다.
4. 부가기능 코드가 복잡해진다면 더욱더 헬오픈이다.
5. 부가기능의 수정요구가 들어오면 수정하기위해서 헬이 오픈된다.

소프트웨어 개발에서 변경지점은 하나가 될수 있도록 잘 모듈화 해야한다.이러한 문제를 해결하기위해서 기존에 OOP의 방식으로는 해결이 어렵다.

AOP - Aspect

핵심기능과 부가기능을 분리
누군가는이러한 부가 기능 도입의 문제점들을 해결하기 위해 오랜기간 고민해왔다.
그 결과 부가 기능을 핵심 기능에서 분리하고 한곳에서 관리하도록 개발됐다.
그리고 해당 부가기능을 어디에 적용할지 선택하는 기능도 만들어졌다.
이렇게 부가기능과 부가기능을 어디에 적용할지 다 합해서 하나의 모듈로 만든것이 Aspect이다.

Aspect는 관점이라는 뜻인데 애플리케이션을 바라보는 관점을 하나하나 기능에서 횡단 관심사의 관점으로 달리 보는것이다.
이러한 Aspect를 활용한 프로그래밍 방식을 관점지향 프로그래밍 AOP라고 한다.

참고로 AOP는 OOP를 대체하기 위한 것이 아니라 횡단 관심사를 깔끔하게 처리하기 어려운 OOP의 부족한 부분을 보조하는 목적으로 개발됐다.

AspectJ 프레임워크

AOP의 대표적인 구현으로 AspectJ 프래임워크 가 있다. 스프링도 AOP를 지원하지만 대부분 AspectJ의 문법을 차용하고 AspectJ가 제공하는 기능의 일부만 제공한다.

AOP 적용 방식

AOP를 사용하면 핵심 기능과 부가기능이 코드상 완전히 분리되어서 관리된다.
그렇다면 AOP를 사용할때 부가 기능 로직은 어떤 방식으로 실제 로직에 추가되나?

  1. 컴파일시점
  2. 클래스 로딩 시점
  3. 런타임 시점(프록시)

컴파일 시점

.java: 소스코드를 컴파일러를 사용해서 .class를 만드는 시점에 부가기능 로직을 추가할 수 있다. 이때 컴파일되는 타이밍에
컴파일러가 부가기능의 로직이 쏙 들어가서 부가기능이 추가된 .class파일이 되는것이다.
이렇게 원본 로직에 부가기능 로직이 추가되는것을 위빙이라고 한다.

  • 단점:

  • 컴파일 시점에 부가기능을 적용하려면 특별한 컴파일러도 필요하고 복잡다.

클래스 로딩시점

자바언어는 .class파일을 JVM내부의 클래스 로더에 보관한다. 이 시점에 중간에 .class파일을 조작한다음 JVM에 올릴수 있다.

단점

  • 로드타임 위빙은 자바를 실행할때 특별한 옵션을 통해 클래스 로더 조작기를 지정해야하는데 이부분이 번거롭고 운영하기 어렵다.

런타임 시점

런타임 시점은 컴파일도 다 끝나고 클래스 로드에 클래스도 이미 다 올라가서 이미 자바가 다 실행된 다음을 말한다. main이 실행된상태
그렇기때문에 자바언어의 문법 법위안에서 실행하는것이다. 최종적으로 프록시를 통해 스프링 빈에 부가기능을 적용하는것이다.
프록시를 사용하기 때문에 AOP기능의 일부 제약이 있다.하지만 특별한 컴파일러나 자바를 실행할때 복잡한 옵션과 클래스 로더 조작기를 설정 하지 않아도 된다.
스프링만 있으면 얼마든지 AOP를 적용할 수 있다.

AOP 적용 위치

AOP는 적용가능 지점(조인포인트 ) : 생성자, 필드값 접근, static 메서드 접근, 메서드 실행

  • AspectJ를 사용해서 컴파일 시점과 클래스 로딩 시점에 적용하는 AOP는 바이트코드를 실제 조작하기 때문에 해당기능을 모든 지점에 다 적용할 수있다
  • 하지만! 프록시 방식을 사용하는 스프링 AOP는 메서드 실행 지점에만 AOP를 적용할 수 있다.
  • 프록시를 사용하는 스프링 AOP의 조인포인트는 메서드 실행으로 제한된다.
  • 프록시 방식을 사용하는 스프링aop는 스프링 컨테이너가 관리하는 스프링빈에만 적용할 수 있는 단점이 있다.

AOP 용어 정리

  • 조인포인트
    • 어드바이스가 적용될 수 있는 위치
    • 조인포인트는 추상적인 개념이다. AOP를 적용할 수 있는 모든 지점
    • 스프링 AOP는 프록시 방식을 사용하므로 조인포인트는 항상 메소드 실행 지점으로 제한된다.
  • 포인트컷
    • 조인포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능
    • 주로 AspectJ 표현식을 사용해서 지정
    • 프록시를 사용하는 스프링 AOP는 메서드 실행 지점만 포인트 컷으로 선별가능
  • 타켓
    • 어드바이스를 받는 객체, 포인트 컷으로 결정
  • 어드바이스
    • 부가기능
    • 특정 조인 포인트에서 Aspect에 의해 취해지는 조치
    • Around(주변),Before(전),After(후)같은 다양한 종류의 어드바이스가 있다.
  • Aspect
    • 어드바이스 + 포인트컷을 모듈화 한것
    • 여러 어드바이스와 포인트컷이 함께존제
  • 어드바이저
    • 하나의 어드바이스와 하나의 포인트 컷으로 구성
    • 스프링AOP에만 사용되는 특별한 용어
  • 위빙
    • 포인트컷으로 결정한 타겟의 조인 포인트에 어드바이스를 적용하는것
    • 위빙을 통해 핵심 기능 코드에 영향을 주지않고부가기능을 추가할수있다.

포인트컷 분리

@Pintcut을 사용한다.

  • ex) /AspectV2

어드바이스 순서

클래스로 따로 빼서 @Order로 순서 지정

  • ex) /AspectV5Order

어드 바이스 종류

  • @Around: 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인포인트 실행여부 선택, 예외 변환 등이 가능
    • 메서드 실행의 전후를 모든 작업을 제어한다.
    • 조인포인트 실행여부 선택
    • 전달값 변환
    • 반환값 변환
    • 예외 변환
    • 트랜젝션처럼 try/catch문을 사용할 수 있다.
    • ProceedingJoinPoint.proceed()를 꼭 사용해서 타겟을 잡아줘야한다.
  • @Before: 조인포인트 실행 이전에 실행
    • @Around와는 다르게 흐름을 변경할 수 는 없다.
  • @AfterReturning : 조인포인트가 정상 완료후 실행
    • returning속성에는 어드바이스 메서드의 매개변수 이름과 일치해야한다. 타입도 맞춰야함
  • @After Throwing: 메서드가 예외를 던지는 경우 실행
    • throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치 예외타입또한 일치해야한다.
  • @After : 조인포인트가 정상 또는 예외 상관없이 실행(finally)

@Around를 제외한 나머지 어드바이스들은 어라운드가 할 수 있는 일 의 일부만 제공할 뿐이다.

참고

모든 어드바이스는 joinPoint를 첫번째 파라밍터에 사용할 수 있다.
단 @Around는 ProceedingJoinPoint를 사용해야한다.
왜냐하면 다음어드바이스나 타겟을 호출하는 proceed가 있기 때문이다.

JoinPoint인터페이스의 주요기능

  • getArgs: 메서드 인수반환
  • getThis : 프록시 객체 반환
  • getTarget : 대상 객체 반환
  • getSignature : 조인되는 메서드에 대한 설명 반환
  • toString : 조인되는 방법에 대한 유용한 설명 인쇄

@Around는 ProceedingJoinPoint.proceed() 를 호출해야 다음 대상이 호출된다. 만약 호출되지 않으면
다음대상이 호출 되지 않는다.

AOP 포인트 컷

포인트컷 지시자

애스팩트 J는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공한다.
PCD라고도 한다.

포인트컷 지시자의 종류 종류

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

execution 문법

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-
pattern(param-pattern)
throws-pattern?)
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
  • 메소드 실행 조인 포인트를 매칭한다.
  • ?는 생략할 수 있다.
  • *같은 패턴사용가능
  • 파라미터에서 ..은 파라터 타입과 파라미터 수가 상관없다는 뜻이다.

within

해당타입이 매칭되면 그안에서 메서드들이 자동으로 매칭된다. execution에서 타입부분만 사용한다고 보면 된다.

주의해야할 점이 표현식에 부모타입을 지정하면 안된다.정확하게 타입이 맞아야 한다.

this, target

this : 스프링 빈 객체 스프링 aop프록시를 대상으로 하는 조인포인트
target: 스프링 aop프록시가 가르키는 실제 대상을 대상으로 하는 조인포인트

this vs target
단순히 하나를 정하면 되는데 this와 target은 어떤 차이가 있을까?

스프링에서 AOP를 적용하면 실제 target 객체 대신에 프록시 객체가 스프링 빈으로 등록된다.
this는 빈에 등록된 프록시 객체를 대상으로 포인트컷을 매칭
target은 실제 target객체를 대상으로 포인트컷을 매칭한다.

왜 이렇게 두개가 나눠졌냐?
프록시 생성 방식의 차이에서 온다.
JDK동적프록시와 CGLIB의 프록시 생성방식에 차이가 있기 때문이다.

this는 jdk동적 프록시 할당에서 프록시의 할당받는 객체를 알지 못한다.그럼 그 객체는 aop의 적용대상이 안된다.
target은 빈에 등록된 객체를 찾아오기때문에 aop의 대상이 될 수 있다.

스프링이 제공하는 @Transcatinal이 대표적인 AOP이다.

스프링 AOP 실무 주의사항

프록시 내부 호출 문제

스프링은 프록시 방식의 AOP를 사용한다.
따라서 AOP를 적용하려면 항상 프록시를 통해서 대상 객체를 호출해야 한다.
이렇게 해야 프록시에 먼저 어드바이스를 호ㅜㄹ하고 이후에 대상 객페를 호출한다.
만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고 어드바이스 호출되지 않는다.
AOP를 적용하면 스프링은 대상객체 대신에 프록시를 스프링 빈으로 등록한다. 다라서 스프링 의존관계 주입시에 항상프록시 객체를 주입한다.
프록시 객체가 주입되기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발새하지 않는다. 하지만 대상객체 내부에서 메서드 호출이 발생하면 프록시를
거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다. 실무에서 반드시 한번은 만나서 고생하는 문제라 꼭이해해야한다.

자바 언어에서 메서드 앞에 별도의 참조가 없으면 this로 자기 자신의 인스턴스를 가리킨다.
결과적으로 자기 자신 내부 메서드를 호출하는 메서드는 실제 대상 객체의 인스턴스를 뜻한다. 결과적으로 이러한 내부 호출은 프록시를 거치지 않는다.
즉 어드바이스가 적용이 안된다.

프록시 방식의 AOP한계

스프링은 프록시 방식의 AOP를 사용한다. 프록시 방식의 AOP는 메서드 내부 호출에 프록시 적용할 수 없다.

프록시 내부 호출문제 - 대안1 자기자신을 의존관계주입

자기 자신을 DI를 해서 내부에서 호출할때 프록시.내부호출() 이런 방식으로 외부메서드를 호출하는 것처럼 해서 해결하는 방법이 있다.
이때 주의 사항은 DI를 할때 생성자가 순환참조 되기때문에 setter로 의존관계를 주입한다.
이렇게하면 생성자를 di하는 타이밍과 setter를 di하는 타이밍이 다르기때문에 해결할 수 있다.
이렇게하면 프록시 인스턴스를 통해서 호출하기때문에 AOP적용이 가능하다.

프록시 내부 호출문제 - 대안2 지연 조회

ObjectProvider 받아서 내부 호출을할때 주입받은 ObjectProvider 있는 빈을 주입받아서 나중에 호출하는 방법도 있다.
ObjectProvider는 객체를 스프링 컨테이너에서 조회하는 것을 스프링 빈 생성 시점이 아니라 실제 객체를 사용하는 시점으로 지연할 수 있다.
callServiceProvider.getObject()를 호출하는 시점에 스프링 컨테이너에서 빈을 조회한다.
여기서는 자기자신을 주입받는 것이 아니기 때문에 순환 사이클이 발생하지 않는다.

프록시 내부 호출문제 - 대안3 구조변경

내부 호출 메서드를 별도의 클래스로 분리하고 DI를 통해서 사용하는 방법이 있다.

클라이언트에서 둘다 호출하도록 변경하는 방법도 있다.

대부분 위 방법처럼 억지로 해결하는 방법보다 대안3이그나마 자연스러운 해결방안이다.

참고

AOP는 주로 트랜잭션 적용이나 주요 컴포넌트의 로그 출력 기능에 사용한다. 인터페이스에 메서드가 나올정도의 규모에 AOP를 적용하는 것이 적당하다.
더 풀어서 이야기하면 AOP는 public메서드만 적용한다 private메서드처럼 작은 단위에는 AOP를 적용하지 않는다.
하지만 AOP적용을 위해 private메서드를 외부 클래스로 변경하고 public으로 변경하는 일은 거의 없다. 그러나 위 예제와 같이 public메서드에서 public메서드를
내부호출하는 경우에는 문제가 발생한다. 실무에서 꼭 한버는 만나는 문제이다.
aop가 잘 적용되지 않는다면 내부호출을 의심해보자.

프록시 기술과 한계

JDK동적 프록시와 CGLIB를 사용해서 AOP프록시를 만드는 방법에는 각각 장단점이 있다.
JDK동적 프록시는 인터페이스가 필수이기 때문에 인터페이스가 있는경우에 JDK동적 프록시와 CGLIB를 선택하는 경우에문제가 발생한다.

JDK동적 프록시 한계

구체 클래스로 타입 캐스팅이 불가능한 한계가 있다.
즉 빈에 등록된 인터페이스로 프록시를 만들었기때문에 해당 인터페이스의 구현 클래스로는 캐스팅을 할 수 없다는 뜻이다.

이때 CGLIB로 구체클래스를 기반으로 프록시를 생성되기때문에 인터페이스의 구체 클래스를 캐스팅하는 것이 가능하다.

  • 정리
    • JDK동적 프록시 대상 객체인 구체 클래스로 캐스팅 할수 없다.
    • CGLIB프록시 대상 객체인 구체클래스로 캐스팅 할수 있다.

의존관계 주입

어떠한 인터페이스와 그 인터페이스의 구체클래스가 동시에 DI를 했을때 JDK동적프록시를 하면 인터페이스를 기반으로 프록시를 주입하기때문에
구체클래스 타입에 캐스팅이 불가능해서 오류가 발생한다.
이럴땐 CGLIB로 구체클래스를 상속받아서 만들기 때문에 그 부모인 구체클래스의 인터페이스까지 캐스팅이 가능해 지는것이다.

  • JDK동적 프록시를 실제로 개발을 할때는 인터페이스가 있으면 인터페이스를 기반으로 의존관계주입을 받는것이 맞다.
    DI의장점은 DI받는 클라이언트 코드의 변경 없이 구ㅕㄴ 클래스를 변경할 수 있다는 것이다 .이렇게 하려면 인터페이스를 기반으로 의존관계를 주입받아야한다. r
    그체 클래스 타입으로 의존관계 주입을 받는 것 처럼 구현 클래스에 의존관계를 주입하면 향후 구현 클래스를 변경할 때 의존관계 주입을 받는
    클라이언트의 코드도 함께 변경해야한다.
    올바르게 잘 설계된 애플리케이션이라면 이런 문제가 자주 발생하지는 않는다
  • 하지만 테스트 등의 여러가지 이유로 AOP프록시가 적용된 구체 클래스를 직접 의존관계 주입 받아야 하는 경우가 있을 ㅅ ㅜ있다.
    이때는 CGLIB를 통해 구체 클래스 기반으로 AOP프록시를 적용하면 된디.

JDK동적 프록시의 한계점들을 보면 무조건 CGLIB를 사용하는 것이 좋아보인다. CGLIB의 단점은 무엇일까

CGLIB 단점

  • 대상 클래스에 기본 생성자 필수
  • 생성자 2번호출 문제
  • final 키워드 클래스 , 메서드 사용불가

대상 클래스에 기본 생성자 필수

CGLIB는 구체 클래스를 상속받는다. 자바 언어에서 상속을 받으면 자식클래스의 생성자를 호출할때 자식 클래스의 생성자에서
부모 클래스의생성자도 호출해야한다.(이부분이 생략되어 있다면 자식 클래스의 생성자 첫줄에 부모클래스의 기본 생성자를 호출하는 super() 가 자동으로 들어간다.)

생성자 2번 호출문제

CGLIB는 구체클래스를 상속받는다 자바 언어에서 상속을 받으면 자식 클래스의 생성자를 호출할때 부모 클래스의 생성자도 호출해야한다.
1. 실제 target의 객체를 생성할때
2. 프록시 객체를 생성할때 부모 클래스의 생성자 호출

final 키워드 클래스, 메서드 사용 불가

final 키워드가 클래스에 있으면 상소기 불가능하고 메서드에 있으면 오버라이딩이 불가능하다.
CGLIB는 상속을 기반으로 하기때문에 두경우 프록시가 생성되지 않거나 정상 동작하지 않는다.
하지만 일반적인 웹 에플리케이션을 개발할때는 final키워드를 잘 사용하지 않는다.

스프링의 해결책

스프링은 AOP프록시 생성을 편리하게 제공하기 위해 오랜시간 고민하고 문제들을 해결했다.
스프링 3.2는 스프링 내부에 CGLIB에 함께 패키징했다.

CGLIB 기본 생성자 필수 문제 해결

스프링 4.0부터 objenesis 라는 특별한 라이브러리를 사용해서 기본생성자 없이 객체 생성이 가능하다.
이라이브러리는 생성자 호출없이 객체를 생성할 수 있게 해준다.
그리고 덕분에 생성자가 1번만 호출한다.

CGLIB는 스프링 부트 2.0 기본이 됐다.

위 문제가 다 해결했기때문이다.

profile
애기 개발자

0개의 댓글