[Spring] 스프링 AOP

Zoe·2022년 3월 10일
0

Spring

목록 보기
9/9
post-thumbnail

스프링 AOP


  • 지금까지 해왔던 작업의 목표 : 비즈니스 로직에 반복적으로 등장해야만 했던 트랜잭션 코드를 깔끔하고 효과적으로 분리해내는 것
  • 이렇게 분리해낸 트랜잭션 코드는 투명한 부가기능 형태로 제공돼야 함
  • 투명하다 : 부가기능을 적용한 후에도 기존 설계와 코드에는 영향을 주지 않는다.

✅ 자동 프록시 생성

  • 아직 남아있는 문제 : 부가기능의 적용이 필요한 target object마다 거의 비슷한 내용의 ProxyFactoryBean 빈 설정정보를 추가해주는 부분 -> 중복 제거 필요

1️⃣ 중복 문제의 접근 방법

  • JDBC API를 사용하는 DAO 코드 : 바뀌지 않는 부분과 바뀌는 부분을 구분해서 분리하고, 템플릿과 콜백, 클라이언트로 나누는 방법을 통해 해결(전략 패턴과 DI적용)
  • 프록시 클래스 코드(반복적인 위임 코드가 필요했음) : 다이내믹 프록시라는 런타임 코드 자동생성 기법을 이용.
  • 반복적인 ProxyFactoryBean 설정 문제는 설정 자동등록 기법으로 해결할 수 없을까? 또는 프록시가 자동으로 빈으로 생성되게 할 수는 없을까?
  • 마치 다이내믹 프록시가 인터페이스만 제공하면 자동으로 모든 메소드에 대한 구현 클래스를 자동으로 만들듯이, 일정한 target bean의 목록을 제공하면 자동으로 각 target bean에 대한 프록시를 만들어주는 방법이 있다면 ProxyFactoryBean 타입 빈 설정을 매번 추가해서 프록시를 만들어내는 수고를 덜 수 있을 것 같음
  • 하지만 지금까지 살펴본 방법에서는 한 번에 여러 개의 빈에 프록시를 적용할 만한 방법은 없음.

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

  • 스프링은 컨테이너로서 제공하는 기능 중에서 변하지 않는 핵심적인 부분 외에는 대부분 확장할 수 있도록 확장 포인트를 제공해줌

  • 빈 후처리기 : BeanPostProcessor 인터페이스를 구현해서 만드는 확장 포인트. 이름 그대로 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브젝트를 다시 가공할 수 있게 해줌.

  • DefaultAdvisorAutoProxyCreator : 빈 후처리기 중 하나. 어드바이저를 이용한 자동 프록시 생성기. 이를 잘 이용하면 스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수도 있음.

  • 빈 후처리기를이용한 자동 프록시 생성 방법 : DefaultAdvisorAutoProxyCreator 빈 후처리기가 등록되어 있으면 스프링은 빈 오브젝트를 만들 때마다 후처리기에게 빈을 보냄. DefaultAdvisorAutoProxyCreator 는 빈으로 등록된 모든 어드바이저 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용대상인지 확인. 프록시 적용 대상이면 프록시를 만들게 하고 어드바이저 연결해줌. 빈 후처리기는 프록시가 생성되면 빈 오브젝트 대신 프록시 오브젝트를 컨테이너에 돌려줌. 최종적으로 빈 후처리기가 돌려준 오브젝트를 빈으로 등록하고 사용.

3️⃣ 확장된 포인트컷

  • 한 가지 이상한 점 : 포인트컷이란 target object의 메소드 중에서 어떤 메소드에 부가기능을 적용할지를 선정해주는 역할이었음. 여기서는 갑자기 등록된 빈 중 어떤 빈에 프록시를 적용할지 선택하는 역할.
  • 두 가지 기능 모두 가짐.
public interface Pointcut { 
	ClassFilter getClassFilter();//프록시를 적용할 클래스인지 확인해줌
	MethodMatcher getMethodMatcher();//어드바이스를 적용할 메소드인지 확인해줌
}

4️⃣ 포인트컷 테스트

@Test public void classNamePointcutAdvisor() {
// 포인트컷 준비
	NameMatchMethodPointcut classMethodPointcut = new NameMatchMethodPointcut() { 
    	public ClassFilter getClassFilter() { 
        	return new ClassFilter() { 
            	public boolean matches(Class<?> clazz) { 
            		return clazz.getSimpleName().startsWith("HelloT");
				} 
      		};
		} 
    };
	
    classMethodPointcut.setMappedName("sayH*");

	// 테스트
	checkAdviced(new HelloTarget(), classMethodPointcut, true);//적용 클래스다.

	class HelloWorld extends HelloTarget {};
	checkAdviced(new HelloWorld(), classMethodPointcut, false);//적용 클래스가 아니다!

	class HelloToby extends HelloTarget {};
	checkAdviced(new HelloToby(), classMethodPointcut, true);//적용 클래스다.
}

//적용 대상인가?
private void checkAdviced(Object target, Pointcut pointcut, boolean adviced) { [3] 
	ProxyFactoryBean pfBean = new ProxyFactoryBean();
	pfBean.setTarget(target);
	pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
	Hello proxiedHello = (Hello) pfBean.getObject();

	if (adviced) { 
    	assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
		assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
		assertThat(proxiedHello.sayThankYou("Toby"), is("Thank You Toby"));
	} else { 
    	assertThat(proxiedHello.sayHello("Toby"), is("Hello Toby"));
		assertThat(proxiedHello.sayHi("Toby"), is("Hi Toby"));
		assertThat(proxiedHello.sayThankYou("Toby"), is("Thank You Toby"));
	} 
}

✅ DefaultAdvisorAutoProxyCreator의 적용

  • 실제로 적용해보기

1️⃣ 클래스 필터를 적용한 포인트컷 작성

  • 메소드 이름만 비교하던 포인트컷인 NameMatchMethodPointcut을 상속해서 프로퍼티로 주어진 이름 패턴을 가지고 클래스 이름을 비교하는 ClassFilter를 추가하도록 만들 것
//클래스 필터가 포함된 포인트컷
package springbook.learningtest.jdk.proxy;
...
public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut { 
	public void setMappedClassName(String mappedClassName) { 
    	this.setClassFilter(new SimpleClassFilter(mappedClassName));
	}

	static class SimpleClassFilter implements ClassFilter { 
    	String mappedName;

		private SimpleClassFilter(String mappedName) { 
        	this.mappedName = mappedName;
		}

		public boolean matches(Class<?> clazz) { 
        	return PatternMatchUtils.simpleMatch(mappedName, clazz.getSimpleName());
		} 
    } 
}

2️⃣ 어드바이저를 이용하는 자동 프록시 생성기 등록

  • 적용할 자동 프록시 생성기인 DefaultAdvisorAutoProxyCreator는 등록된 빈 중에서 Advisor 인터페이스를 구현한 것을 모두 찾음
  • 등록 :
<bean class = 
	"org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />

3️⃣ 포인트컷 등록

//포인트컷 빈
<bean id="transactionPointcut" 
	class="springbook.service.NameMatchClassMethodPointcut"> 
    <property name="mappedClassName" value="*ServiceImpl" /> 
    <property name="mappedName" value="upgrade*" /> 
</bean>

4️⃣ 어드바이스와 어드바이저

  • 어드바이스 : transactionAdvice -> 수정 필요 없음
  • 어드바이저 : transactionAdvisor -> 수정 필요 없음
  • 어드바이저로서 사용되는 방법이 바뀐 것
  • ProxyFactoryBean으로 등록한 빈에서처럼 transactionAdvisor를 명시적으로 DI하는 빈은 존재하지 않음
  • 대신 어드바이저를 이용하는 자동 프록시 생성기인 DefaultAdvisorAutoProxyCreator에 의해 자동수집되고 프록시 대상 선정 과정에 참여하며, 자동 생성된 프록시에 다이내믹하게 DI 돼서 동작하는 어드바이저가 됨

5️⃣ ProxyFactoryBean 제거와 서비스 빈의 원상복구

//프록시 팩토리 빈을 제거한 후의 빈 설정
<bean id="userService" class="springbook.service.UserServiceImpl"> 
    <property name="userDao" ref="userDao" />
    <property name="mailSender" ref="mailSender" /> 
</bean>

6️⃣ 자동 프록시 생성기를 사용하는 테스트

//수정한 테스트용 UserService 구현 클래스
static class TestUserServiceImpl extends UserServiceImpl {
	private String id = "madnite1";//테스트 픽스처의 users(3)의 id 값을 고정시켜버렸다.
	
    protected void upgradeLevel(User user) { 
    	if (user.getId().equals(this.id)) throw new TestUserServiceException();
		super.upgradeLevel(user);
	} 
}
//테스트용 UserService의 등록
<bean id="testUserService" 
	class="springbook.user.service.UserServiceTest$TestUserServiceImpl" 
    parent="userService" />//프로퍼티 정의를 포함해서 userService 빈의 설정을 상속받는다.
//testUserService 빈을 사용하도록 수정된 테스트
public class UserServiceTest { 
	@Autowired UserService userService;
	@Autowired UserService testUserService;
    //같은 타입의 빈이 두 개 존재하기 때문에 필드 이름을 기준 으로 주입될 빈이 결정된다. 
    //자동 프록시 생성기에 의해 트랜잭션 부가기능이 testUserService 빈에 적용됐는지를 확인하는 것이 목적이다.
	...

	@Test
    public void upgradeAllOrNothing() { 
    //스프링 컨텍스트의 빈 설정을 변경하지 않으므로 @ DirtiesContext 애노테이션은 제거됐다. 
    //모든 테스트를 위한 DI 작업은 설정파일을 통해 서버에서 진행되 므로 테스트 코드 자체는 단순해진다.
    	userDao.deleteAll();
		for(User user : users) userDao.add(user);


		try {
			this.testUserService.upgradeLevels();
			fail("TestUserServiceException expected");
		} 
        catch(TestUserServiceException e) { 
        }
		
        checkLevelUpgraded(users.get(1), false);
	} 
}

7️⃣ 자동생성 프록시 확인

//클래스 필터용 이름을 변경한 포인트컷 설정
<bean id="transactionPointcut" 
		class="springbook.user.service.NameMatchClassMethodPointcut"> 
    <property name="mappedClassName" value="*NotServiceImpl" /> 
    <property name="mappedName" value="upgrade*" /> 
</bean>
//자동생성된 프록시 확인
@Test public void advisorAutoProxyCreator() { 	
	assertThat(testUserService, is(java.lang.reflect.Proxy.class));
}
//프록시로 변경된 오브젝트인지 확인한다.
profile
iOS 개발자😺

0개의 댓글