[토비의 스프링 3.1] 7주차 스터디 - 6장 AOP(1)

리리·2024년 9월 16일
0

토비의 스프링

목록 보기
1/2

6장 AOP

  • 6.3 다이내믹 프록시와 팩토리 빈
  • 6.4 스프링의 프록시 팩토리 빈
  • 6.5 스프링 AOP


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

6.3.1 프록시와 프록시 패턴, 데코레이터 패턴

image

  • 부가기능 외의 나머지 모든 기능은 원래 핵심기능을 가진 클래스로 위임해줘야 한다. 핵심기능은 부가기능 클래스의 존재를 모르며, 부가기능이 핵심기능을 사용하는 구조가 된다.
  • 클라이언트가 핵심기능을 가진 클래스를 직접 사용해버리면 부가기능이 적용될 기회가 없다는 문제가 존재한다.

image 1

  • 부가기능은 자신이 핵심기능을 가진 클래스처럼 꾸며서, 클라이언트가 자신을 거쳐 핵심기능을 사용하도록 만든다.
  • 클라이언트는 인터페이스를 통해서만 핵심기능을 사용하게 하고, 부가기능도 같은 인터페이스를 구현한 뒤에 자신이 그 사이에 끼어들어야 한다.

프록시

image 2

자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리자, 대리인과 같은 역할을 한다고 해서 프록시라고 부른다. 프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트를 타깃 또는 실체라고 부른다.

  • 프록시의 특징
    • 타깃과 같은 인터페이스를 구현한다.
  • 프록시의 사용 목적
    1. 클라이언트가 타깃에 접근하는 방법을 제어하기 위함

    2. 타깃에 부가적인 기능을 제공해주기 위함

      → 이 사용 목적에 따라 두 개는 다른 디자인패턴으로 구분된다.


데코레이터 패턴

타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴 (2에 해당)

  • 부가적인 기능을 다이내믹하게 부여
    = 컴파일 시점에 (즉, 코드 상에서) 어떤 방법과 순서로 프록시-타깃이 연결되어 사용되는지 정해져있지 않다.
  • 데코레이터
    = 제품이나 케이크를 여러 겹으로 포장하고 그 위에 장식을 붙이는 것처럼 실제 내용물은 동일하지만 부가적인 효과를 부여해줄 수 있다는 의미
    • 프록시는 한 개로 제한되지 않으며, 프록시가 직접 타깃을 사용하도록 고정시킬 필요도 없다.
  • 데코레이터 패턴 적용 예
    image 3

    • 프록시가 여러 개인 만큼 순서를 정해서 단계적으로 위임하는 구조를 만든다.
    • 부가적인 기능을 프록시로 만들어두고 런타임 시에 이를 적절한 순서로 조합해서 사용한다.
  • 프록시로 동작하는 각 데코레이터는 위임하는 대상에도 인터페이스로 접근하기 때문에 자신이 최종 타깃으로 위임하는지, 아니면 다음 단계의 데코레이터 프록시로 위임하는지 알지 못한다.

    → 데코레이터의 다음 위임 대상은 인터페이스로 선언하고 생성자나 수정자 메소드를 통해 위임 대상을 외부에서 런타임 시에 주입받을 수 있도록 만들어야 한다.

  • 데코레이터 패턴이 적용된 대표적인 예시

    InputStream is = new BufferdInputStream(new FileInputStream("a.txt"));
    • Component 인터페이스: InputStream이 공통 인터페이스 역할을 한다.
    • Concrete Component & 타깃: FileInputStream은 기본 기능을 제공하는 구체적인 구현이다.
    • Decorator: BufferedInputStream은 FileInputStream을 감싸서 버퍼링 기능을 추가해 read() 메서드가 효율적으로 동작할 수 있도록 돕는다.
  • UserService 인터페이스를 구현한 타깃인 UserServiceImpl에 트랜잭션 부가기능을 제공해주는 UserServiceTx를 추가한 것도 데코레이터 패턴의 적용이라고 볼 수 있다.

    <bean id="userService" class="springbook.user.service.UserServiceTx">  
    		<property name="transactionManager" ref="transactionManager" />
    		**<property name="userService" ref="userServiceImpl" />**  
    	</bean>
    
    <bean id="**userServiceImpl**" class="springbook.user.service.UserServiceImpl">

    → UserServiceImpl로 선언된 타깃 빈이 DI를 통해 데코레이터인 UserService빈에 주입되도록 설정

  • 언제 유용할까?

    타깃의 코드를 손대지 않고, 클라이언트가 호출하는 방법도 변경하지 않은 채로 새로운 기능을 추가할 때

프록시 패턴

클라이언트가 타깃에 접근하는 방법을 제어하려는 목적을 가진다. (1에 해당)

  • 타깃의 기능을 확장/추가해주진 않지만 클라이언트가 타깃에 접근하는 방식을 변경해준다.
    • 타깃 오브젝트가 필요해지는 시점까지 오브젝트를 생성하지 않는 편이 좋지만 타깃 오브젝트에 대한 레퍼런스가 미리 필요할 수 있는데, 이때 프록시 패턴을 적용하면 좋다.
    • 클라이언트에게 타깃에 대한 레퍼런스를 넘겨야 할 때 실제 오브젝트 대신 프록시를 넘겨준다. 그리고 프록시의 메소드를 사용해 타깃을 사용하려고 시도하면 그때 프록시가 타깃 오브젝트를 생성하고 요청을 위임해준다.
      → 프록시를 통해 타깃의 생성을 최대한 늦춤
  • 특별한 상황에서 타깃에 대한 접근권한을 제어

    • 수정 가능한 오브젝트가 특정 레이어에서는 읽기 전용으로 사용되어야 할 때, 오브젝트의 프록시를 만들 수 있다. 프록시의 특정 메소드를 사용하려고 하면 접근이 불가능하다고 예외를 발생시킨다.
      • Collections의 unmodifiableCollection()을 통해 만들어지는 오브젝트가 전형적인 접근권한 제어용 프록시
      • 파라미터로 전달된 Collection의 프록시를 만들어서 add(), remove() 등 수정 메소드를 호출하면 예외를 발생하게 해준다.
  • 프록시 패턴의 경우 자신이 만들거나 접근할 타깃 클래스의 정보를 알고 있는 경우가 많다.

  • 프록시 패턴 + 데코레이터 패턴
    image 4


6.3.2 다이내믹 프록시

프록시를 일일이 모든 인터페이스를 구현해서 클래스를 새로 정의하지 않고도 편리하게 만들어 사용할 방법이 있다. 몇 가지 API를 이용해 프록시처럼 동작하는 오브젝트를 다이내믹하게 생성하는 것이다.

프록시의 구성과 프록시 작성의 문제점

public class UserServiceTx implements UserService {
	UserService userService;
	
	// 프록시의 기능(1). 메소드 구현과 위임
	public void add(User user) {
		this.userService.add(user);
	}

	public void upgradeLevels() {
		// 프록시의 기능(2). 부가기능 수행
		TransactionStatus status = this.transactionManager
				.getTransaction(new DefaultTransactionDefinition());
		try {

			userService.upgradeLevels(); // (1). 메소드 구현과 위임

			this.transactionManager.commit(status);
		} catch (RuntimeException e) {
			this.transactionManager.rollback(status);
			throw e;
		}
	}
}

프록시를 만들기 번거로운 이유

  1. 부가기능 코드가 중복될 가능성이 많다.

    → 중복되는 코드를 분리해서 어떻게든 해결할 수 있다.

  2. 인터페이스를 구현하고 위임하는 코드를 작성하기 번거롭다. 부가기능이 필요없는 메소드도 구현해서 타깃으로 위임하는 코드를 일일이 만들어줘야 한다.

    JDK의 다이내믹 프록시를 이용해 해결할 수 있다.


리플렉션

다이내믹 프록시는 리플렉션 기능을 이용해서 프록시를 만들어준다. 리플렉션은 자바의 코드 자체를 추상화해서 접근하도록 만든 것을 말한다.

  • 자바의 모든 클래스는 그 클래스 자체의 구성정보를 담은 Class 타입의 오브젝트를 하나 갖는다.
    • 클래스 코드에 대한 메타정보를 가져오거나 오브젝트를 조작할 수 있다.
      • 클래스 이름, 어떤 클래스를 상속하는지, 어떤 인터페이스를 구현했는지, 어떤 필드를 갖고 있고, 각각의 타입은 무엇인지, 메소드는 어떤 것을 정의했고, 메소드의 파라미터와 리턴 값은 무엇인지 등
  • 리플렉션 API 중, 메소드에 대한 정의를 담은 Method 인터페이스를 이용해 메소드 호출하는 방법
    //Method를 이용해 리플렉션 방식으로 호출
    Method lengthMethod = String.class.getMethod("length");
    // 특정 오브젝트의 메소드를 실행시킬 수도 있다.
    public Object invoke(Obeject obj, Object ...args)
    
    int length = lengthMethod.invoke(name); // int length = name.length();

프록시 클래스

  1. Hello 인터페이스

    interface Hello { 
    	String sayHello(String name);
    	String sayHi(String name);
    	String sayThankYou(String name);
    }
  2. 타깃 클래스

    public class HelloTarget implements Hello { 
    	public String sayHello(String name) { return "Hello " + name;}
    	public String sayHi(String name) { return "Hi " + name;}
    	public String sayThankYou(String name) { return "Thank You " + name;
    	} 
    }
  3. 프록시 클래스

    • 데코레이터 패턴을 적용해 부가기능을 추가한다.
    public class HelloUppercase implements Hello { 
    	Hello hello;
    
    	public HelloUppercase(Hello hello) { 
    		this.hello = hello;
    	}
    	
    	public String sayHello(String name) { 
    		return hello.sayHello(name).toUpperCase(); // 위임과 부가기능 적용
    	}
    	
    	public String sayHi(String name) { 
    		return hello.sayHi(name).toUpperCase();
    	}
    	
    	public String sayThankYou(String name) { 
    		return hello.sayThankYou(name).toUpperCase();
    	}
    
    }

    → 인터페이스의 모든 메소드를 구현해 위임하도록 만들어야 하고, 부가기능인 리턴값을 대문자로 바꿔주는 기능이 모든 메소드에 중복된다는 프록시 적용의 일반적인 문제점 두가지를 모두 갖는다.


다이내믹 프록시 적용

image 5

  • 다이내믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어진다. 클라이언트는 다이내믹 프록시 오브젝트를 타깃 인터페이스를 통해 사용할 수 있다.
    • 인터페이스를 구현하고 위임하는 코드를 작성하기 번거롭다는 문제 해결
    • 프록시 팩토리에게 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 클래스의 오브젝트를 자동으로 만들어주기 때문
  • 다이내믹 프록시가 인터페이스 구현 클래스의 오브젝트를 만들어주지만, 부가기능 제공 코드는 직접 작성해야 한다.
    • 부가기능은 프록시 오브젝트와 독립적으로 InvocationHandler를 구현한 오브젝트에 담는다.
  • InvocationHandler 인터페이스는 다음의 메소드 하나만을 갖는다.
    public Object invoke(Object proxy, Method method, Object[] args) 
    → 다이내믹 프록시 오브젝트는 클라이언트의 모든 요청을 리플렉션 정보로 변환해서 InvocationHandler 구현 오브젝트의 invoke() 메소드로 넘기는 것.
  • 각 메소드 요청은 어떻게 처리할까? InvocationHandler 구현 오브젝트가 타깃 오브젝트 레퍼런스를 갖고 있다면 리플렉션을 이용해 간단한 위임 코드를 만들어낼 수 있다.

image 6

    public class UppercaseHandler implements InvocationHandler {
    	Hello target; // 타깃의 정보를 갖고 있다.
    	
    	public UppercaseHandler(Hello target) {
    		this.target = target;
    	}
    	
    	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    		String ret = (String)method.invoke(target, args); //타깃으로 위임
    		return ret.toUpperCase(); // 부가기능 제공
    	}
    }
    // 프록시 생성
    Hello proxiedHello = (Hello)Proxy.newProxyInstance(
    		getClass().getClassLoader(),
    		new Class[] { Hello.class }, //구현할 인터페이스
    		new UpperCaseHandler(new HelloTarget())); // 부가기능과 위임코드를 담은 InvocationHandler

다이내믹 프록시의 확장

다이내믹 프록시를 사용하면 직접 정의해서 만든 프록시보다 훨씬 유연하고 많은 장점이 있다.

  1. Hello 인터페이스의 메소드가 늘어나도 일일이 메소드를 구현할 필요가 없다.

    → 다이내믹 프록시가 생겨날때 추가된 메소드가 자동으로 포함되기 때문

  2. InvocationHandler는 리플렉션의 Method 인터페이스를 이용해 타깃의 메소드를 호출하므로, 타깃의 종류에 상관없이 적용이 가능하다.


6.3.4 다이내믹 프록시를 위한 팩토리 빈

  • 스프링은 지정된 클래스 이름을 가지고 리플렉션을 이용해 해당 클래스의 오브젝트를 만든다.
    Date now = (Date) Class.forName("java.util.Date").newInstance();
  • 다이내믹 프록시 오브젝트는 클래스가 어떤 것인지 알 수 없다. 클래스 자체도 내부적으로 다이내믹하게 정의해서 사용하기 때문에 사전에 클래스 정보를 알아내서 스프링 빈에 정의할 방법이 없다.

팩토리 빈

스프링을 대신해 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈을 말한다.

  • FactoryBean 인터페이스를 구현해 팩토리 빈을 생성할 수 있다.
  • 스프링은 FactoryBean 인터페이스를 구현한 클래스가 빈의 클래스로 지정되면, 팩토리 빈 클래스의 오브젝트의 getObject() 메소드를 이용해 오브젝트를 가져오고, 이를 빈 오브젝트로 사용한다.
    public class MessageFactoryBean implements FactoryBean {
    	String text;
    	
    	public void setText(Strint text) {
    		this.text = text);
    	}
    	
    	public Message getObject() throws Exception {
    		return Message.newMessage(this.text);
    	}
    }

다이내믹 프록시를 만들어주는 팩토리 빈

  • 팩토리 빈을 사용하면 다이내믹 프록시 오브젝트를 스프링의 빈으로 만들어줄 수 있다.
    • getObject() 메소드에 다이내믹 프록시 오브젝트를 만들어주는 코드를 넣으면 된다.

      public class MessageFactoryBean implements FactoryBean {
      	public Object getObject() throws Exception { 
      		TransactionHandler txHandler = new TransactionHandler();
      		txHandler.setTarget(target);
      		txHandler.setTransactionManager(transactionManager);
      		txHandler.setPattern(pattern);
      		return Proxy.newProxyInstance( getClass().getClassLoader(),new Class[] { serviceInterface }, txHandler);
      	}
      }
  • 팩토리 빈은 타깃 오브젝트인 UserServiceImpl에 대한 레퍼런스를 프로퍼티를 통해 DI 받아야 한다. → TransactionHandler에게 타깃 오브젝트를 전달해주기 위함

image 7


6.3.5 프록시 팩토리 빈 방식의 장점과 한계

프록시 팩토리 빈의 한계

  1. 프록시를 통해 타깃에 부가기능을 제공하는 것은 메소드 단위로 일어나는 일이다.
    1. 하나의 클래스 안에 존재하는 여러 개의 메소드에 부가기능을 한 번에 제공하는 것은 가능하지만, 여러 클래스에 공통적인 부가기능을 제공하는 것은 불가능하다.

      → 프록시 팩토리 빈의 중복설정으로 이어질 수 있다.

  2. 같은 타깃 오브젝트에 트랜잭션 프록시 뿐만 아니라, 다른 프록시도 추가하고 싶다면 프록시 팩토리 빈 설정이 부가기능의 개수만큼 늘어나게 된다.
  3. Handler 오브젝트가 팩토리 빈 개수만큼 늘어난다.
    1. 타깃 오브젝트를 프로퍼티로 갖고 있으므로, 타깃 오브젝트가 달라질 때마다 새로운 Handler 오브젝트를 만들어야 한다.


6.4 스프링의 프록시 팩토리 빈

스프링은 이런 문제에 어떤 해결책을 제시할까?

6.4.1 ProxyFactoryBean

스프링은 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈을 제공한다.

  • 스프링의 ProxyFactoryBean은 프록시 생성 작업만 담당하고, 부가기능은 별도의 빈에 둘 수 있다.
    • 부가기능은 MethodInterceptor 인터페이스를 구현해 만든다.
  • MethodInterceptor와 InvocationHandler의 차이
    • InvocationHandler의 invoke() 메소드가 타깃 오브젝트에 대한 정보를 제공하지 않기 때문에 InvocationHandler를 구현한 클래스는 타깃을 알고 있어야 한다.
    • MethodInterceptor의 invoke() 메소드는 ProxyFactoryBean으로부터 타깃 오브젝트에 대한 정보까지도 함께 제공받는다.
      static class UppercaseAdvice implements MethodInterceptor { 
      		public Object invoke(MethodInvocation invocation) throws Throwable { 
      				String ret = (String)invocation.proceed();
      				return ret.toUpperCase();
      		} 
      }

어드바이스: 타깃이 필요 없는 순수한 부가기능

  • MothodInvocation은 일종의 콜백 오브젝트로, proceed() 메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행해주는 기능이 있다.

    • 일종의 공유가능한 템플릿처럼 동작한다.
    • ProxyFactoryBean은 템플릿 역할을 하는 MethodInvocation을 싱글톤으로 두고 공유할 수 있다.
  • ProxyFactoryBean에는 여러 개의 MethodInvocation을 추가할 수 있다.

    → ProxyFactoryBean 하나만으로 여러 개의 부가기능을 제공하는 프록시를 만들 수 있다.

  • 어드바이스 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트


포인트컷: 부가기능 적용 대상 메소드 선정 방법

MethodInterceptor 오브젝트는 타깃 정보를 갖고 있지 않기 때문에 스프링의 싱글톤 빈으로 등록된다.

그렇다면 부가기능을 적용하고 싶은 메소드를 어떻게 선별해야할까?

  • MethodInterceptor는 InvocationHandler와 달리 클라이언트로 받는 요청을 일일이 전달받을 필요가 없다.
  • MethodInterceptor에는 순수한 부가기능 제공 코드만 남겨두고 프록시에 부가기능을 적용할 메소드를 선택하는 기능을 넣자.
    • 프록시의 핵심 가치는 타깃을 대신해 클라이언트의 요청을 받는데 있으므로, 메소드 선별기능은 다시 프록시에서 분리하는게 낫다.
  • 스프링의 ProxyFactoryBean 방식은 두 가지 확장기능인 부가기능과 메소드 선정 알고리즘을 활용하는 유연한 구조를 제공한다.

image 8

  • 어드바이스와 포인트컷은 모두 프록시에 DI로 주입돼서 사용된다.
    → 여러 프록시에서 공유가능하도록 만들어지기 때문에 스프링 빈으로 등록 가능하다.

    1. 프록시는 클라이언트로 요청을 받으면 먼저 포인트컷에게 부가기능을 부여할 메소드인지 확인해달라는 요청을 보낸다.
    2. 확인받으면 프록시는 MethodInterceptor타입의 어드바이스를 호출한다.
      1. 어드바이스는 타깃에 의존하지 않도록 일종의 템플릿 구조로 설계되어 있다.
    3. 어드바이스가 부가기능을 부여하는 중에 타깃 메소드 호출이 필요하면 프록시로부터 전달받은 MethodInvocation 타입 콜백 오브젝트의 proceed() 메소드를 호출한다.
    4. 위임대상인 타깃 오브젝트의 레퍼런스를 갖고 있고 이를 이용해 타깃 메소드를 직접 호출하는건 Invocation 콜백이 수행한다.
  • 어드바이저 = 포인트컷(메소드 선정 알고리즘) + 어드바이스(부가기능)

    ProxyFactoryBean pfBean = new ProxyFactoryBean();
    
    pfBean.addAdvisor(new DefaultPointCutAdvisor(pointcut, new UpperCaseAdvice());

    → 포인트컷과 어드바이스를 따로 등록하게 되면 포인트컷을 어떤 어드바이스에 적용할지 애매하기 때문



6.5 스프링 AOP

6.5.1 자동 프록시 생성

프록시 팩토리 빈 방식의 접근 방법의 한계를 되짚어보자.

  1. 부가기능이 타깃 오브젝트마다 새로 만들어지는 문제

    → 스프링 ProxyFactoryBean의 어드바이스를 통해 해결

  2. 부가기능 적용이 필요한 타깃 오브젝트마다 비슷한 내용의 ProxyFactoryBean 설정 정보를 추가해야함

    → ?


중복문제의 접근 방법

변하지 않는 타깃으로의 위임과 부가기능 적용 여부 판단이라는 부분은 코드 생성 기법을 이용하는 다이내믹 프록시 기술에 맡기고, 변하는 부가기능 코드는 별도로 만들어서 다이내믹 프록시 생성 팩토리에 DI로 제공하는 방법을 사용했다.

한 번에 여러 개의 빈에 프록시를 적용하기 위해 ProxyFactoryBean 설정 문제는 어떻게 해결해야 할까?


빈 후처리기를 이용한 자동 프록시 생성기

스프링은 OCP의 가장 중요한 요소인 유연한 확장 개념을 스프링 컨테이너 자신에게도 적용하고 있다.

0개의 댓글