[번역] Spring (3) Aspect Oriented Programming with Spring

rin·2020년 6월 3일
0

Document 번역

목록 보기
3/22
post-thumbnail

ref. Aspect Oriented Programming with Spring를 번역합니다.

Aspect Oriented Programming with Spring

Aspect-oriented Programming (AOP)은 프로그램 구조에 대한 다른 사고 방식을 제공함으로써 Object-oriented Programming (OOP)을 보완한다. OOP의 주요 모듈 단위는 클래스인 반면 AOP의 모듈 단위는 "aspect"이다. aspect는 여러 유형과 객체를 흩어진(transaction 관리와 같은) 문제의 모듈화를 가능하게한다. (이런 문제는 종종 AOP 문헌에서는 크로스 컷팅(crosscutting) 문제라고 한다.)

AOP 프레임워크는 Spring의 핵심 컴포넌트 중 하나이다. 스프링 IoC 컨테이너는 AOP에 의존하지 않지만(원한다면 AOP를 사용하지 않아도 됨을 의미한다), AOP는 스프링 IoC를 보완하여 뛰어난 미들웨어 솔루션을 제공한다.

AOP는 Spring 프레임워크에서 다음을 위해 사용된다.

  • 선언적인 엔터프라이즈 서비스 제공하라. 그런 서비스가 가장 중요한 것은 선언적 트랜잭션 관리이다.
  • AOP와 함께 OOP를 사용하는 것을 보완하면서 커스텀 aspect를 구현하라.

일반적인 선언적 서비스나 다른 사전에 패키징된 선언적 미들웨어 서비스에만 관심이 있다면, 스프링 AOP를 직접 작업할 필요가 없으며 이 장의 대부분을 생략할 수 있다.

AOP Concepts

Aspect

여러 클래스에 흩어진 문제의 모듈화. 트랜잭션 관리는 엔터프라이즈 Java 애플리케이션에서 크로스 컷팅 문제의 좋은 예이다. 스프링 AOP에서 Aspect는 @Aspect 어노테이션 또는 정규 클래스를 사용해 구현된다.

Join point

메서드의 실행, 예외 처리와 같은 전체 프로그램이 실행되는 중간의 어떤 지점. Spring AOP에서 join point는 항상 메서드의 실행을 나타낸다.

Advice

특정한 join point에서 aspect에 의해 수행되는 작업. 다른 종류의 advice에는 "around", "before", "after" advice가 포함될 수 있다. (어드바이스의 종류는 나중에 논하기로 한다.) 스프링을 비롯한 많은 AOP 프레임워크는 어드바이스를 인터셉터로 모델링하고, join point 주의에 인터셉터 체인을 유지한다.

Pointcut

join point와 일치하는 서술어. 어드바이스는 포인트컷의 표현법과 관련이 있으며 포인트 컷에 매칭되는 모든 조인 포인트에서 실행된다(예를 들어, 특정한 이름의 메소드 실행). AOP의 중심은 포인트컷 표현식과 일치하는 조인 포인트의 개념이며 스프링은 기본적으로 AspectJ 포인트컷 표현 언어를 사용한다.

Introduction

타입을 대신해 추가적인 메소드나 필드를 선언하는 것. 스프링 AOP는 어떤 advice가 수행되는 객체에 새로운 인터페이스(혹은 인터페이스 구현체)를 소개(Introduce)한다. 예를 들어, Introduction을 사용해 IsModified 인터페이스의 구현체 빈을 만들어 쉽게 캐싱을 사용할 수 있다. For example, you could use an introduction to make a bean implement an IsModified interface, to simplify caching. (intoriduction은 AspectJ 커뮤니티에서 inter-type 선언이라고 불린다.)

Target object

An object being advised by one or more aspects. Also referred to as the “advised object”. Since Spring AOP is implemented by using runtime proxies, this object is always a proxied object.

AOP proxy

aspect contracts(e.g 어드바이스 메소드 실행)를 이행하기 위해 AOP 프레임워크에 의해 만들어진 오브젝트. 스프링 프레임 워크에서 AOP 프록시는 JDK dynamic proxy 또는 CGLIB proxy를 사용한다.

Weaving

어드바이스를 가지는 오브젝브를 만들기 위해 다른 어플리케이션 타입이나 오브젝트를 aspect와 연결하는 것. 컴파일 타임 (예를 들어 AspectJ 컴파일러를 사용할 때), 로드 타임, 런타임에 수행 할 수 있다. 다른 순수 Java AOP 프레임 워크와 마찬가지로 스프링 AOP는 런타임에 weaving을 수행한다.

types of advice

스프링 AOP에는 다음과 같은 유형의 advice가 있다.

Before advice
join point 이전에 실행되지만 "예외가 발생하지 않는 한" 실행 흐름이 join point로 진행되는 것을 막을 수 있는 기능이 없다.

After returning advice
join point가 정상적으로 완료된 후에 실행된다. 예를 들어, 메소드가 예외를 발생시키지 않고 리턴하는 경우

After throwing advice
예외에 의해 메소드가 종료되면 실행된다.

After (finally) advice
join point가 종료되는 방법 (정상적인 종료이든, 예외 발생이든)에 관계없이 실행된다.

Around advice
메서드 호출같은 join point를 둘러싼 어드바이스. 가장 강력한 조언이다. Around advice는 메소드 호출 전후에 사용자가 정의한 행동을 수행 할 수 있다. 또한 자체적으로 값을 반환하거나 예외를 발생시켜 join point로 진행할 것인지, advise를 가지는 메소드의 실행을 단축시킬 것인지 선택할 수 있다.


Around advice는 가장 일반적인 어드바이스의 종류이다. AspectJ와 같은 스프링 AOP는 광범위한 어드바이스 유형을 제공하므로 필요한 동작을 구현할 수 있는 가장 강력한 어드바이스 유형을 사용하는 것이 좋다. 예를 들어, 메소드의 리턴 값으로 캐시만 갱신해야하는 경우, Around advice도 수행할 수 있겠지만 after return advice를 구현하는 것이 가장 적합할 것이다. 가장 구체적인 어드바이스 유형을 사용하는 것은 오류 가능성이 적은 간단한 프로그래밍 모델을 제공하는 것이다. 이전의 예에서, Around advice에 사용되는 JoinPoint에 대한 proceed() 메소드를 호출할 필요가 없기 때문에, 이를 호출하다가 실패할 일은 없다.

모든 어드바이스 파라미터는 정적으로 유형이 정해지므로 오브젝트 배열이 아닌 적절한 타입으로 작업할 수 있다. (e.g. 메소드 실행에 의한 반환 값 타입)

포인트 컷과 일치하는 join point의 개념은 AOP의 핵심이며, 인터셉트만 제공하는 오래된 기술과는 다르다. 포인트 컷을 사용하면 객체 지향 계층에 독립적으로 어드바이스를 지정할 수 있다. 예를 들어, 서비스 레이어에서의 모든 비즈니스 작업과 같이 여러 객체에 흩어져있는 일련의 메소드에 선언적 트랜잭션 관리를 제공하는 around advice를 적용할 수 있다.

기능과 목표

스프링 AOP는 순수 Java로 구현되기때문에 컴파일 과정이 필요없다. 스프링 AOP는 클래스 로더 계층을 제어할 필요가 없으므로 서블릿 컨테이너나 어플리케이션 서버에서 사용하기에 적합하다.

현재 스프링 AOP는 메소드 실행 join point만 제공한다. (스프링 빈에서 메소드의 실행을 어드바이스한다.) Field 인터셉션은 구현되지 않았지만 스프링 AOP API의 핵심을 손상시키지 않고 필드 인터셉션에 대한 지원을 추가할 수 있다. 필드 엑세스를 어드바이스하고 join point를 업데이트 해야한다면 AspectJ 같은 언어를 고려해라.

스프링 AOP의 AOP 접근 방식은 대부분의 다른 AOP 프레임 워크와는 다르다. 목표는 가장 완벽한 AOP 구현체를 제공하는 것이 아니다. (Spring AOP에서 충분히 가능함에도 불구하고.) 오히려 AOP 구현과 스프링 IoC 간의 긴밀한 통합을 제공하여 엔터프라이즈 어플리케이션의 일반적인 문제를 해결하는 것이 목표이다.

예를 들어 스프링 프레임워크의 AOP 기능은 일반적으로 Spring IoC 컨테이너와 함께 사용된다. Aspect의 구성은 일반적인 빈을 정의하는 구문을 사용한다. (강력한 "auto-proxying" 기능을 허용할지라도) 이는 다른 AOP 구현체와의 주요한 차이점이다. 아주 미세한 객체(일반적으로 도메인 오브젝트)를 어드바이스하기 위해 스프링 AOP를 사용하면 쉽고 효율적으로 작업을 수행할 수 없다. 이런 경우에는 AspectJ가 최선의 선택이다. 그러나 경험에 의하면 Spring AOP는 AOP를 준수하는 엔터프라이즈 Java 어플리케이션의 대부분의 문제에 대한 탁월한 솔루션을 제공한다.

스프링 AOP는 포괄적인 AOP 솔루션을 제공하기 위해 AspectJ와 경쟁하지 않는다. Spring AOP와 같은 프록시 기반 프레임워크와 AspectJ와 같은 포괄적인 프레임워크는 모두 가치가 있으며 경쟁 대신 서로를 보완 할 수 있다고 생각한다. Spring은 스프링 AOP와 IoC를 AspectJ와 완벽하게 통합하여 일관된 스프링 기반 어플리케이션 아키텍처 내에서 AOP를 모두 사용할 수 있도록 한다. 이러한 통합은 스프링 AOP API나 Alliance API에 영향을 미치지 않는다. 스프링 AOP는 이전 버전과 호환된다.

🌱🌱🌱
스프링 프레임 워크의 핵심 원칙 중 하나는 비침투성이다. 이것은 비즈니스나 도메인 모델에 프레임 워크의 특정한 클래스나 인터페이스를 도입하도록 강요하지 않는다는 것이다. 그러나 스프링 프레임워크는 당신의 코드베이스에 스프링 프레임워크의 고유한 의존성을 추가할 수 있는 옵션을 제공하기도 한다. 이러한 옵션을 제공하는 이론적 근거는 특정한 시나리오에서는 이런 방법을 도입하는 것이 쉽게 읽고 코드를 작성할 수 있게 해주기 때문이다. 하지만 스프링 프레임워크는 (거의) 항상 옵션을 제공한다. 특정한 사용 사례나 시나리오에 가장 적합한 옵션을 추가하는 것은 자유롭게 결정할 수 있다.

이 장과 관련하여 그러한 선택 중 하나는 어떤 AOP 프레임 워크 (혹은 어떤 AOP 스타일)을 선택하는 것이다. AspectJ나 Spring AOP, 혹은 둘 다 선택할 수도 있다. @AspectJ 어노테이션 접근법 혹은 스프링 XML configuration 중 하나를 선택할 수도 있다. 이 장에서 @AspectJ 스타일 접근 방식을 먼저 소개하는 것이 스프링 팀이 스프링 XML configuration 스타일보다 @AspectJ 어노테이션 스타일 접근 방식을 선호한다는 표시로 간주되면 안된다.

각 스타일의 "whys and wherefores"에 대한 자세한 내용은 사용할 AOP 선언 스타일 선택하기를 참조하라

Proxies

스프링 AOP의 기본값은 AOP 프록시에 표준 JDK dynamic proxies를 사용하는 것이다. 이를 통해 모든 인터페이스(or 인터페이스 셋)의 프록시를 제공 할 수 있다.

CGLIB 프록시를 사용할 수도 있다. 인터페이스가 아닌 클래스의 프록시를 제공하는데 필요하다. 비즈니스 오브젝트가 인터페이스를 구현하지 않으면 기본적으로 CGLIB가 사용된다. 따라서 클래스 보다 인터페이스로 프로그래밍하는 것이 관행적으로 더 좋다. 비즈니스 클래스는 일반적으로 하나 이상의 비즈니스 인터페이스를 구현한다. 인터페이스에서 선언되지 않은 메소드를 어드바이스해야하거나 프록시 객체를 메소드에 구체적인 유형으로 전달해야하는 경우 CGLIB를 강제로 사용할 수 있다.

✔️스프링 AOP가 프록시 기반이라는 사실을 파악하는 것이 중요하다. 이 구현체의 세부 사항이 실제로 무엇을 의미하는지 자세히 알고싶다면 AOP 프록시의 이해를 참조하라.

AOP 프록시의 이해

Spring AOP는 프록시 기반이다. 직접 aspects를 만들거나 스프링 프레임워크가 제공하는 스프링 AOP 기반의 aspect를 사용하기 전에 마지막 명령문의 실제 의미를 파악하는 것은 매우 중요하다.

검소하고, 프록시되지 않고, 특별한 정보가 없는 직선의 객체 참조가 있는 시나리오를 고려해보자. :

public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

객체 레퍼런스에서 메소드를 호출하면 다음 이미지처럼 해당 객체 레퍼런스에서 메소드가 직접 호출된다.

public class Main {

    public static void main(String[] args) {
        Pojo pojo = new SimplePojo();

        // 'pojo' 레퍼런스에서 직접 메소드를 호출
        pojo.foo();
    }
}

클라이언트 코드에 대한 레퍼런스가 프록시인 경우 상황이 약간 달라진다.

public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();
        // 프록시에서 메소드를 호출한다.
        pojo.foo();
    }
}

여기서 이해해야하는 핵심 키는 Main 클래스의 main(..) 메소드 내부의 클라이언트 코드는 프록시에 대한 참조를 가지고 있다는 것이다. 이는 해당 객체 레퍼런스에 대한 메소드 호출이 프록시에 대한 호출임을 의미한다. 결과적으로 프록시는 해당 특정 메소드 호출과 관련된 모든 인터셉터(advice)에 위임 할 수 있다는 것이다. 하지만, 호출이 마지막으로 대상 객체 (이 경우에는 SimplePojo)에 닿는 순간, this.bar()this.foo()같이 대상 객체가 스스로 호출하는 것과 같은 메소드는 프록시가 아닌 이 레퍼런스를 참조할 것이다. 이는 매우 중요한 의미를 갖는데, 자체 호출로 인해 메소드 호출과 관련된 advice가 실행 기회를 얻기 못하게 됨을 의미한다. (위임을 프록시가 하기 때문)

그럼 이것을 어떻게 해결해야할까? 최선의 방법은 자체 호출이 발생하지 않도록 코드를 리팩토링하는 것이다. 여기에는 약간의 작업이 수반되지만 가장 침투성이 적은 방법이다.

다음 접근 방식은 아주아주 좋지 못한데, 이를 사용할 것이라면 여러번 고민하길 바란다. 다음 예제와 같이 클래스 내 논리를 Spring AOP에 완전히 묶을 수 있다.

public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}

이 방법은 코드를 스프링 AOP에 완전히 결합시키고 클래스가 AOP 컨텍스트에 사용됨을 클래스 자체에게 알린다. 또한 다음 예에서 알 수 있듯이 프록시가 작성 될 때 특별한 추가 구성이 필요합니다. :

public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true); //here

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}

마지막으로, AspectJ는 프록시 기반 AOP 프레임워크가 아니기 때문에 이러한 자체 호출에 대해 문제가 없다.

profile
🌱 😈💻 🌱

0개의 댓글