토비의 스프링 3.1 정독기 - AOP

문지은·2021년 9월 13일
0

토비의 스프링

목록 보기
6/6

🚩 AOP의 출발점

만약 로그를 찍는 코드와 비즈니스 로직을 수행하는 코드가 있다고 하자. 비즈니스 로직을 수행하는 코드는 핵심 코드, 로그를 찍는 코드는 부가 기능 코드로 볼 수 있다. 이를 분리해서 결합도를 낮춰보자. 가장 흔하게 쓰이는 방법으로는 함수로 빼는 방식이 있고 다형성을 위해서는 부가 기능 코드를 인터페이스로 선언한 후에 DI 를 받는 방식이 있을 것이다.

하지만 인터페이스로 선언한 뒤에 DI를 받는다고 해도 해당 코드는 핵심 코드와 함께 뒤섞여 있다. 이것을 해결하기 위해서 프록시를 사용한다.

  • 프록시 : 대리자
  • 타깃 : 실제 오브젝트

프록시를 사용하는 이유는 2가지를 꼽을 수 있다.

  1. 클라이언트가 타깃에 접근하는 방법 제한

  2. 타깃에 부가적인 기능 부여

프록시를 이용한 패턴에 대해 알아보자

데코레이터 패턴

타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 사용하는 프록시 패턴이다. 이름이 데코레이터 패턴인 것도 꾸며주는 역할이라는 의미를 가진다. 아래는 대표적인 데코레이터 패턴을 적용한 예시다.

InputStream is =
	new BufferedInputStream
          (
              new FileInputStream("a.txt")
          );

// 텍스트 파일을 읽는 FileInputStream에 BufferedInputStream 이라는 데코레이터 적용

프록시 패턴

일반적으로 프록시는 대리 역할을 맡은 오브젝트를 의미하지만 디자인 패턴인 프록시 패턴에서의 프록시는 타깃에 대한 접근 방식을 제어하려는 목적을 가질 때를 말한다.

타깃이 런타임 전에 미리 만들어질 필요는 없지만 클라이언트 입장에서 레퍼런스가 필요할 수 있다. 이 때 프록시를 넘겨준다. 또는 클라이언트가 리모트에 있는 오브젝트가 필요할 경우 프록시를 넘겨줄 수 있다. 오브젝트에 대한 권한을 제어할 때도 프록시의 메소드를 이용해서 쓰기나 수정을 제어할 수 있다. (unmodifiableCollection()같은 메소드!)

프록시는 이렇게 좋은 기능이 있지만 인터페이스 구현하고 위임하는 코드가 만들기 번거롭고 코드의 중복이 생길 가능성이 크기 때문에 사용하기가 꺼려진다. 이 때 다이내믹 프록시의 리플렉션이라는 프레임 워크를 이용해서 쉽게 만들 수 있다.

다이내믹 프록시

다이내믹 프록시는 타깃을 인터페이스와 동일한 타입으로 오브젝트를 만들어준다. 여기에 우리가 부가 기능을 추가해주면 된다. 추가된 부가 기능은 Invocation Handler 를 구현한 부분에 담긴다.

Hello proxiedHello = 
	(Hello) Proxy.newProxyInstance(
		getClass.getClassLoader()
        , new Class[] {Hello.Class}
        , new UppsercaseHandler(new HelloTarget()));
                        
 // dynamic proxy 생성 예시
 // newProxyInstance 이용. 클래스 로더, 구현할 인터페이스, InvocationHandler 넘겨줘야 함
아래는 다이내믹 프록시를 이용해서 코드를 구성한 예시이다.

public Object invoke(
	Object proxy
    , Method method
    , Object[] args)
{

	// 메소드에 따라서 트랜잭션 경계 설정 할지 안 할지 결정
	
    if (method.getName().startsWith(pattern))
    {
    	return invokeInTransaction(method, args);
    }
    else
    {
    	return method.invoke(target, args);
    }
}

private Object invokeInTransaction(
	Method method
    , Object[] args)
{
    // invoke 통해서 메소드 따라서 부가 기능 실행
    Object ret = method.invoke(target, args);
    
    ...
}

팩토리 빈

이제 다이내믹 프록시를 쓰는 이유도 알았고 사용 방법도 알았다. 이걸 빈으로 만들어서 DI 받으면 정말 좋은 코드가 될 것이다. 하지만 여기서 새로운 문제가 생긴다. 스프링은 리플랙션 API를 사용해서 빈 정의에 나오는 클래스 이름으로 빈 오브젝트를 생성한다. 하지만 다이내믹 프록시는 오브젝트의 클래스 조차도 알 수 없고 런타임시 다이내믹하게 생성되기 때문에 미리 정의할 수 없다. 그렇기에 우리는 Proxy 클래스의 newProxyInstance()라는 스태틱 팩토리 메소드를 사용한다.

빈 팩토리를 이용해서 다이내믹 프록시를 빈으로 만들어서 DI하는 것에는 두가지 장점이 있다.

  1. 다이내믹 프록시의 장점 : 타깃 인터페이스를 일일히 구현하지 않아도 된다. 하나의 핸들러 메소드만을 구현해 놓고 타깃 이름을 추가해주기만 하면 된다.

  2. DI를 적용했을 때 장점 : 다이내믹 프록시 생성 코드를 제거할 수 있다.

하지만 프록시 팩토리 빈에도 한계는 존재한다.

프록시를 통해 타깃에 부가 기능을 제공하는 것은 메소드 단위로 일어나는 일이기에 여러개의 클래스에 공통으로 부가 기능을 제공하는 것은 불가능하다. only 이 클래스 안에서만 가능! 이 말은 코드의 중복이 많이 많이 늘어날 것이라는 것을 의미한다.😓

문제점이 또 있다! 프록시 팩토리 빈 개수만큼 TransactionHandler가 생성된다. 하나의 핸들러로 처리할 수 있지만 빈 등록을 위해서 똑같은 오브젝트를 또 만들어내는 건 분명히 낭비다.

부가 기능을 모든 클래스에서 사용해서 코드의 중복을 줄이고 TransactionHandler를 싱글톤으로 사용하는 방법은 없을까?

ProxyFactoryBean을 사용하면 부가 기능과 프록시 생성에 대한 빈을 별도로 둘 수 있기 때문에 싱글톤으로 사용이 가능하다. ProxyFactoryBean에서 타깃 오브젝트가 없이 부가 기능을 제공하는 데만 집중할 수 있게 한 것이 어드바이스이다. 그리고 이 어드바이스가 적용될 메소드를 선정하는 것이 포인트컷이다.

어드바이저 = 포인트컷(메소드 선정 알고리즘) + 어드바이스(부가 기능)

자동 프록시 생성

ProxyFactoryBean의 어드바이스를 사용해서 싱글톤으로 만드는 문제는 해결했지만 아직 모든 빈의 설정 정보가 중복되는 문제는 해결하지 못했다. 이것은 빈 후 처리기를 사용해서 해결이 가능하다. 빈 후 처리기의 플로우는 다음과 같다.

  1. 빈 오브젝트 생성 후 빈 후 처리기에 전송
  2. 빈 후 처리기는 포인트컷을 이용해서 전달 받은 빈이 프록시 전용 대상인지 확인
  3. 프록시 전용 대상이면 해당 빈에 대한 프록시 생성 후 어드바이저에 전송
  4. 컨테이너는 빈 후 처리기가 전달해준 프록시를 빈으로 등록 후 사용 -> 빈 설정 정보 중복될 필요 없음

포인트컷

위에 플로우에서 확인했듯이 포인트컷이 두가지 기능을 가지고 있다는 것을 알 수 있다.

  1. 프록시를 적용할 클래스인지 확인 (getClassFilter())
  2. 어드바이스를 적용할 메소드인지 확인 (getMethodMatcher())

자동 프록시와 포인트컷을 사용하면 DI를 받아서 사용할 필요가 없기 때문에 ProxyFactoryBean이 제거가 가능해진다.

서비스 추상화 발전 과정 정리해보기

이렇게 우리는 경계 설정 코드라는 여기저기 프로젝트 전반에 흩어져 있고 중복되는 코드를 모듈화하기 위해서 많은 방법을 써왔다.

1. 트랜잭션 서비스 추상화

인터페이스와 DI를 통해서 분리

->해당 코드를 사용하는 부분 남음

2. 프록시와 데코레이터 패턴

트랜잭션 처리하는 코드를 데코레이터레 담은 후에 클라이언트와 비즈니스 로직 사이의 타깃 클래스 사이에 존재하게 하기

-> 독립적으로 만들기 성공

-> 하지만 비즈니스 로직과 클라이언트 사이의 프록시 클래스 일일히 만드는 작업 필요

3. 다이내믹 프록시와 프록시 팩토리 빈

프록시 클래스 없이도 프록시 만드는 다이내믹 프록시 이용. DI를 적용하기 위해 프록시 팩토리 빈 이용

-> 어드바이스와 포인트 컷 분리 가능 및 여러 프록시가 공유 가능

-> 하지만 빈 설정 정보의 중복 발생

4. 자동 프록시 생성 방법과 포인트컷

빈 생성 후 처리 기법 도입

-> 포인트컷으로 프록시 설정할 대상 자동으로 선정해서 빈 설정 정보 중복 없앰

-> 포인트 컷 표현식 활용

👾 최종 보스 : AOP

코드 전반에 흩어져 있는 부가 기능을 모듈화하기 위해서 위의 작업들을 진행해온 것이다. 여기에서 AOP가 등장하게 된 것이다.

Aspect Oriented Programming 즉, 다양한 측면에서 독립적으로 모델링, 설계, 개발할 수 있게 만들어주는 것이다. 핵심 기능이든 부가 기능이든!!

AOP에는 프록시를 이용한 방식, 바이트 코드를 이용한 방식이 있다. 프록시를 이용한 방식은 지금까지 위에서 설명한 방식이고 바이트 코드를 이용한 방식은 프록시를 이용해서 간접적으로 조작하는 것이 아니라 타깃 오브젝트를 뜯어 고쳐서 부가 기능을 직접 넣어주는 방식이다. 해당 방식은 번거롭지만 더 폭 넓은 조작이 가능하다.

@Transactional

위의 어노테이션을 붙이면 해당 오브젝트는 타깃으로 인식되어 포인트컷의 대상이 된다. 트랜잭션이 적용되면 트랜잭션 전파 속성을 이용할 수 있는데 이로 인해서 코드 중복을 피할 수 있으며 더 작은 단위로 쪼개서 개발하는 것이 가능해진다.

트랜잭션 전파 속성 : 앞에서 진행중인 트랜잭션이 있으면 참여하고 없으면 새로운 트랜잭션을 시작해준다.

또한 이 배경에는 AOP가 존재한다.

profile
백엔드 개발자입니다.

0개의 댓글