-
AOP 동작 원리spring/core 2021. 3. 6. 01:05
배경
토비의 스프링 실습 공부
AOP의 등장 배경을 코드로 익히면서 실제 스프링이 재사용 가능한 범용적인 AOP를 어떻게 적용하는지 코드를 통해 알아보자. 개인적으로 내용이 길고 어려워서 몇 번 씩 반복 중이다.
요구사항의 변화
위와 같이 메세지를 전달받으면 간단하게 Hello를 추가한 메세지를 반환하는 오브젝트가 있다고 해보자. Hello서비스를 이용하는 클라이언트는 메세지를 넣으면 자동으로 Hello를 추가해주는 기능에 만족하고 있었다. 이때 클라이언트는 위 메소드가 실행될 때마다 콘솔에 시작을 알리는 텍스트를 찍고 메소드가 끝나기직전에 끝을 알리는 텍스트를 찍어주는 기능을 추가해 달라고 요청했다.
단순하게 구현이 가능하지만, 만약 클래스의 메소드가 100개였다면 어땠을까? 간단하게 100개의 메소드에 동일하게 코드를 추가해주면 된다. 이때 클라이언트가 요구사항을 바꿔 출력되는 START와 END 뒤에 느낌표를 3개로 바꿔달라고 했다. 인내심을 갖고 200번의 코드 수정과 함께 요구사항을 만족시켜줬다. 그런데 만약 변경에 대한 요청이 자주 들어오고 그 요구사항을 충족시키기 위해서 수정해야할 메소드가 10000개 이상이라면?\
이와 같은 문제는 한 객체가 응집도가 떨어지는 구체적인 기능 두가지를 책임지고 있어서 생기는 문제다.
메소드의 시작과 끝에 특정메세지를 출력하는 작업은 Hello클래스가 직접 담당할 문제는 아니다. 출력 역할을 위임하는 방법을 생각해볼 수 있지만, 논리적으로 하나의 작업이 두 코드로 분리되어 있어 Hello클래스가 직접 이 일을 다른 객체에게 위임하는 건 어려울 것 같다.
데코레이터 패턴 적용
반대로 생각해서 출력을 담당한 객체를 새로 생성하고 그 객체가 중간에 Hello의 본기능을 수행하도록 수정한다. 그 과정에서 클라이언트는 Hello기능을 사용한다고 생각해야 한다. 이를 위해 Hello를 인터페이스로 분리하고 작업하자.
Hello인터페이스에 공통 메소드를 정의하고 부가기능을 담은 HelloDecorate이 실제 기능인 HelloImpl을 주입받아 사용하게 만든다. 실제 클라이언트는 HelloDecorate을 사용하게 되지만 내부 속사정은 전혀 모른다.
하지만 이런 방식은 여전히 출력 내용 변경에 대한 대량 수정을 막지 못한다. 역할은 잘 분리했지만 출력이라는 공통적인 내용이 모든 메소드에 노출되어 있다.
템플릿 콜백 적용
이런 코드는 공통되는 템플릿이 모든 메소드에 들어간다는 특징이 있다. 즉, 템플릿을 정해두고 특정 상황만 다르게 동작하는 패턴이 필요한데 이를 템플릿 콜백을 통해 해결한다. 클라이언트가 호출하는 메소드에 따라 HelloDecorate클래스의 구체적인 전략이 결정돼 템플릿의 특정 상황만 다르게 동작하는 방식이다. 특정 상황에서 다른 메서드를 수행하기만 하면 되므로 Runnable콜백을 정의해 넘기는 방식이다.
하지만 아직 문제점이 남아 있다. 결국 이런 방식의 위임코드는 모든 인터페이스에 대한 모든 위임 작업코드를 일일이 작성해야한다는 점이다. 위와 같은 부가기능을 이용하길 원하는 클래스가 늘어나면 그에 맞게 코드 작성은 불가피하다.(부가기능의 범용성을 만족시키지 못한다)
리플렉션을 이용한 프록시 생성 자동화
자바의 리플렉션 유틸과 Proxy의 newProxyInstance를 사용해서 원하는 타깃에 부가기능을 넣은 프록시를 런타임에 생성할 수 있다. 동작과정을 구체화해보자.
1) 두번째 파라미터로 인터페이스를 넘기는데 여기서 리플렉션을 이용해 인터페이스의 메소드 메타정보를 추출한다.
2) 추출한 모든 메소드는 실제로 구현되는데 이때 구현 코드는 내가 만든 InvocationHandler에게 위임한다
3) 위임 시 마찬가지로 추상화한 메타정보를 넘기고 다시 그 메타정보를 분석해 적절한 메소드를 수행한다
대략 위와 같은 프록시클래스가 런타임에 생성될 걸 기대한다. 이제 부가기능을 넣기 위해 매번 인터페이스를 정의하고 새로운 클래스를 생성하는 반복되는 작업은 피할 수 있게 되었다.
지금까지 정리
InvocationHandler는 타킷오브젝트 타입이 Object고 실행하는 invoke()메소드의 파라미터가 전부 범용인터페이스여서 모든 클래스에 범용적이다. 부가기능을 적용하기 원한다면 새로운 프록시 인스턴스에 타깃을 주입해서 빈으로 등록하면 된다.
그런데 부가기능을 적용할 클래스의 개수가 100개라면? 1000개라면? 1000000개라면? 매번 새로운 프록시인스턴스의 InvocationHandler에 원하는 타깃을 주입해 빈으로 등록해줘야할까? 굳이 프록시 등록 코드를 매번 작성하지 않더라도 자동으로 원하는 타깃에 원하는 부가기능을 더해줄 방법은 없을까? 또한 한 클래스에 수십 수백개의 부가기능을 더했을 때 중복되는 빈등록은 어떻게 처리할까?
ProxyFactoryBean을 이용한 프록시 생성
먼저, InvocationHandler와 newProxyInstance()를 이욯한 프록시 생성 방법을 생각해보자. newProxyInstance()는 단지 프록시의 생성에만 관여하지 추가적인 기능을 정의하지 못한다. 이말은 부가기능과 관련한 모든 코드는 InvocationHandler의 invoke()에 넣어져야함을 의미한다. 결국 관련이 없는 기능이 한 클래스에 모아질 수 있다. 우선 InvocationHandler가 타깃을 주입받는다는 점을 생각해보면 재사용이 불가능함을 알 수 있다. 하나의 부가기능 인스턴스만 만들고 이를 원하는 타깃에 넣도록 설계를 분리해보자.
부가기능을 정의한 MethodInterceptor는 타깃의 위임 코드마저 추상화해버렸다. 즉, 하나의 인터셉터를 등록한다면 원하는 타깃에 범용적으로 적용할 수 있음을 의미한다. 위 코드처럼 스프링의 ProxyFactoryBean을 이용하면 addAdvice를 통해 한 타깃에 원하는 만큼 재사용 가능한 부가기능클래스를 등록할 수 있다.(템플릿콜백을 이용) 이제 남은 관문은 재사용 가능한 부가기능을 여러 클래스에 한번에 적용하는 과정이다
포인트컷: 부가기능 적용 대상 선정 방법
잘 생각해보면 부가기능 자체와 부가기능을 어떤 클레스 또는 메소드에 적용할지는 다른 기능이다. 부가기능 스스로가 적용대상을 정하는 건 다른 책임의 응집도가 높은 것이다. 이를 이용해 스프링이 제공하는 ProxyFactoryBean은 부가기능과 그 적용대상을 따로 설정하도록 되어 있다.
포인트컷 또한 다른 상태에 의존하지 않기 때문에 싱글톤으로 등록해 범용적인 사용이 가능하다.
이제 부가기능과 필터기능을 합친 Advisor를 등록한 프록시팩토리빈 하나만 있으면 원하는 모든 클래스에 부가기능을 추가할 수 있다.
정리 및 활용
두 객체의 협력은 논리적인 흐름이 있다. 한 객체가 다른 객체에게 기능을 위임할 때 두 객체만 아는 자연스러운 흐름이 있다. 자바라는 객체지향 언어와 다형성을 이용한 DI는 이러한 객체지향 설계를 아름답게 하도록 도와준다.
반면 객체지향 언어적 특성은 객체의 논리적인 흐름이 아닌 특정 상황에 대한 논리적인 흐름을 제어하는 코드를 만들 때 설계적 어려움에 부딪친다. 이런 어려움을 해결하기 위해 AOP라는 테크닉이 생겼고, 결국 이또한 객체지향의 DI를 활용한 해법이다.
컨테이너를 이용해 빈을 관리하면 이 장점을 극대화하는 아이디어가 떠오른다. 예를 들어, 팩토리프록시빈에 부가기능과 애노테이션기반의 포인트컷을 적용하는 것이다. 그러면 내가 원하는 클래스 또는 메소드에 애노테이션만 붙이고 컨테이너를 이용해 모든 빈이 생성후 이 팩토리프록시빈을 거치도록 하게 만드는 것이다. 그럼 애노테이션이 붙은 클래스 또는 메소드는 내가 원하는 기능으로 빈이 만들어질 것이다.
스프링의 빈후처리기
스프링에서 제공하는 빈후처리기를 빈으로 등록하면 컨테이너의 기능이 확장된다. DefaultAdvisorAutoProxyCreator가 빈으로 등록된 경우 빈 오브젝트가 생성될 때마다 빈에 대한 어드바이저 프록시를 생성해준다. 단순하게 생각하면 컨테이너 빈을 전부 순회하며 포인트컷에 적용이 되는지 확인하고 프록시를 적용하는 것이다.
이렇게 간단하게 빈후처리기와 어드바이저를 빈으로 등록하기만 하면 된다. 이러면 등록된 모든 빈에 대해 모든 어드바이저가 적용되고 포인트컷의 조건에 반족한 빈은 그 어드바이저의 포록시 기능을 포함하여 빈으로 생성된다.
마무리하며
객체의 협력 안에서 어떻게 하면 관점에 대한 부가기능을 코드로 표현할 수 있을까하는 고민이 AOP의 시발점이다. 추상적인 이해와 애노테이션 사용법을 익히면 프로젝트에서 기능을 사용하는 건 크게 무리가 없어 보인다. 하지만 결국 AOP란 기술도 용빼는 재주가 있는 게 아니고, 객체의 DI를 잘 활용해 구현할 수 있음을 확인했다. 결국 DI는 정말 무한한 가능성을 갖고 있고 이 DI를 관리해주는 스프링컨테이너를 잘 다룬다면 그 어떤 작업이라도 아름다운 객체지향 설계를 구현할 수 있을 것 같다.
'spring > core' 카테고리의 다른 글
Spring Interceptor 사용 및 동작 정리 (0) 2021.05.14 스프링 DI 방법 (0) 2021.05.06 스프링 부트 자동설정 과정 (0) 2021.04.06 DispatcherServlet에서 요청을 처리하는 과정 (0) 2021.04.05 HandlerMethodArgumentResolver 동작 원리 (0) 2021.03.08