6.5.1 프록시 생성의 문제
- 부가기능이 타깃 오브젝트마다 새로 만들어지는 문제는 ProxyFactoryBean의 어드바이스를 통해 해결 되었음
- 남은 문제는 부가기능의 적용이 필요한 타깃 오브젝트마다 비슷한 내용의 ProxyFactoryBean 빈 설정정보(XML)를 추가해야하는 부분임
- 빈 후처리기를 사용하면 설정정보를 자동으로 추가할 수 있음
6.5.2 빈 후처리기를 이용한 자동 프록시 생성기
- 스프링은 컨테이너로서 제공하는 기능 중에서 변하지 않는 핵심적인 부분외에 대부분 확장할 수 있도록 확장 포인트를 제공하고 있음
- 관심을 가질만한 확장 포인트는 BeanPostProcessor 인터페이스를 구현해서 만드는 빈 후처리기임
- 스프링은 빈 후 처리기가 빈으로 등록되어 있으면 빈 오브젝트가 생성될 때 마다 빈 후처리기에 보내서 후처리 작업을 요청함
- 빈 후처리기는 후처리 작업을 함으로써 빈 오브젝트의 프로퍼티를 강제로 수정하거나 별도의 초기화 작업을 할 수 있음
- 또한, 설정정보를 통해 만들어진 빈 오브젝트가 아닌 다른 오브젝트를 빈으로 등록할 수 있음
- 이를 활용하여 스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수 있음
- 이것이 자동 프록시 생성 빈 후처리기임
- 스프링이 제공하는 빈 후처리기중 하나인 DefaultAdvisorAutoProxyCreator를 사용할 것임
- DefaultAdvisorAutoProxyCreator가 빈으로 등록되어 있으면 스프링은 빈 오브젝트를 만들 때마다 후처리기에 빈을 보냄
- DefaultAdvisorAutoProxyCreator는 빈으로 등록된 모든 어드바이저 내의 포인트 컷을 이용해 빈이 프록시 적용 대상인지 확인함
- 프록시 적용 대상이면 내장된 프록시 생성기에 의해 프록시를 만들고, 어드바이저를 연결함
- 프록시가 만들어지면 컨테이너가 전달해준 빈 오브젝트 대신 프록시 오브젝트를 컨테이너에 반환함
6.5.3 확장된 포인트컷
- 앞에서 포인트컷은 메소드에 부가기능을 적용할지 선정하는 역할이라 했음
- 위에서 포인트컷으로 오브젝트인 빈이 프록시 적용 대상인지 확인한다 했으니 말의 앞뒤가 맞지 않음
- 사실, 포인트컷은 클래스 필터 및 메소드 매처 두 가지 기능을 가지고 있음
- 클래스 필터로 적용 대상 오브젝트를 확인하고 조건에 맞으면, 매소드 매처로 적용 대상 메소드를 확인함
- 앞서 사용한 NameMatchMethodPointCut는 메소드만 선정하기 위해 모든 클래스를 받아들이도록 만들어진 특별한 포인트컷임
- 앞서 사용한 ProxyFactoryBean에서 NameMatchMethodPointCut를 사용한 이유는 이미 타킷 오브젝트가 정해졌기 때문임
- 빈 후처리기인 DefaultAdvisorAutoProxyCreatorDefaultAdvisorAutoProxyCreator는 클래스 및 메소드 선정 알고리즘을 모두 갖고있는 포인트컷이 필요함
- 정확히는 그런 포인트컷과 어드바이스가 결합된 어드바이서가 필요함
6.5.4 포인트컷 테스트
- 포인트컷에 대한 학습 테스트임
- NameMatchMethodPointCut는 클래스 필터 기능이 없음
- 💡 정확히는 모든 클래스를 수용하는 클래스 필터를 가지고 있는것임
- 때문에 NameMatchMethodPointCut를 확장하여 클래스 필터를 추가한 포인트컷을 만들어 테스트 하는 것임
public class DynamicProxyTest {
...
@Test
public void classNamePointcutAdvisor(){
NameMatchMethodPointcut classMethodPointcut = new NameMatchMethodPointcut(){
@Override
public ClassFilter getClassFilter() {
return new ClassFilter() {
@Override
public boolean matches(Class<?> clazz) {
//class 이름이 HelloT로 시작하는 것만 선정
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) {
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"));
}
}
}
6.5.5 클래스 필터를 적용한 포인트컷 작성
- NameMatchMethodPointCut를 상속하고, ClassFilter 추가
- 내부 클래스로 SimpleClassFilter 생성
- PatternMatchUtils의 simpleMatch()는 와일드카드(*)가 들어간 문자열 비교를 지원하는 스프링 유틸리티 메소드임
public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut {
public void setMappedClassName(String mappedClassName){
this.setClassFilter(new SimpleClassFilter(mappedClassName));
}
private class SimpleClassFilter implements ClassFilter {
String mappedName;
public SimpleClassFilter(String mappedClassName) {
this.mappedName = mappedClassName;
}
@Override
public boolean matches(Class<?> clazz) {
return PatternMatchUtils.simpleMatch(mappedName,
clazz.getSimpleName());
}
}
}
6.5.6 어드바이저를 이용하는 자동 프록시 생성기 등록
- DefaultAdvisorAutoProxyCreator를 빈에 등록하면 자동으로 등록된 빈 중에서 Advisor 인터페이스를 구현한 빈을 모두 찾음
- 찾은 빈에 포인트컷을 적용하여 프록시 적용 대상 선정을 함
- 프록시 생성이 끝나면 기존의 빈 오브젝트와 바꿔치기 함
- DefaultAdvisorAutoProxyCreator 등록은 한줄이면 충분함
- DefaultAdvisorAutoProxyCreator를 참조하는 빈은 존재하지 않기 때문에 id가 없음
<beans
...>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<beans/>
6.5.7 포인트컷 등록
- 기존의 포인트컷 설정을 삭제하고 새로만든 클래스 필터 지원 포인트컷 등록
<beans
...>
<bean id="transactionPointcut"
class="com.ksb.spring.NameMatchClassMethodPointcut">
<property name="mappedClassName" value="*ServiceImpl"/>
<property name="mappedName" value="upgrade*"/>
</bean>
<beans/>
6.5.8 어드바이스와 어드바이저
- 기존의 어드바이스 및 어드바이저를 수정할 필요가 없음
- 기존에 등록된 transactionAdvisor는 DefaultAdvisorAutoProxyCreator가 빈으로 등록됐으므로 자동으로 후처리가 될 것임
- 즉, transactionAdvisor를 명시적으로 DI하는 빈은 존재하지 않음
6.5.9 ProxyFactoryBean 제거와 서비스 빈의 원상복구
- 더이상 명시적으로 프록시 팩토리 빈을 등록하지 않음
- 프록시를 도입했던 때부터 아이디를 바꾸고 프록시에 DI돼서 간접적으로 사용됐던 UserServiceImpl 빈의 아디를 다시 UserService로 되돌림
<beans
...>
<bean id="userService" class="com.ksb.spring.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<property name="mailSender" ref="mailSender"/>
</bean>
<beans/>
6.5.10 자동 프록시 생성기를 사용하는 테스트
- 기존의 upgradeAllOrNothing()의 테스트에 문제가 생김
- 현재까지 예외상황을 위한 테스트를 하기 위해 수동 DI로 구성을 바꿨음
- 자동 프록시 생성기를 적용한 후 더 이상 가져올 ProxyFactoryBean 같은 팩토리 빈이 존재하지 않음
- 때문에 강제 예외 발생용 TestUserService를 직접 빈으로 등록
- TestUserService에는 두 가지 문제가 존재함
- UserServiceImpl의 스태틱 내부 클래스
- public static class TestUserService extends UserServiceImpl {
- 클래스 필터 조건이 “*ServiceImpl”임
- <property name="mappedClassName" value="*ServiceImpl"/>
- 첫 번째는 내부 클래스를 빈에 등록하기 위해 “$”사용
- 두 번째는 클래스 필터 조건에 맞게 클래스 이름 수정
- tesetUserService의 parent를 통해 userService 빈의 설정 내용을 상속 받을 수 있음
- 상속을 받아 userDao나 mailSender 프로퍼티를 지정할 필요가 없음
public class UserServiceImpl implements UserService {
...
public static class TestUserServiceImpl extends UserServiceImpl {
//예외를 위해 user의 세 번째 값 id로 고정
private String id = "k2";
@Override
protected void upgradeLevel(User user) {
if (user.getId().equals(this.id)) throw new TestUserServiceException();
super.upgradeLevel(user);
}
}
}
public class UserServiceTest {
@Autowired
UserService userService;
@Autowired
UserService testUserService;
...
@Test
public void upgradeAllOrNothing() {
userDao.deleteAll();
for (User user : users) userDao.add(user);
try {
this.testUserService.upgradeLevels();
fail("TestUserServiceException expected");
} catch (UserServiceImpl.TestUserServiceException e) {
}
checkLevelUpgraded(users.get(0), false);
}
}
<beans
...>
<bean id="testUserService" class="com.ksb.spring.UserServiceImpl$TestUserServiceImpl"
parent="userService">
</bean>
<beans/>
6.5.11 포인트컷 표현식
- 지금까지 사용했던 포인트컷은 클래스 필터와 메소드 매처 오브젝트로 비교해서 선정하는 방식임
- 필터나 매처에서 클래스와 메소드의 메타정보를 제공받기 때문에 단순히 이름 비교를 넘어 복잡하고 세밀한 선정방식을 만들 수 있음
- 하지만, 조건이 달라질 때 마다 포인트컷 구현 코드를 직접 수정해야 하는 단점이 있음
- 또한, 클래스 필터와 메소드 매처 두 가지를 각각 제공해야함
- 포인트컷 표현식(Pointcut Expression)은 간단하고 효과적인 포인트컷의 클래스와 메소드를 선정할 수있음
- 포인트컷 표현식은 클래스와 메소드 선정을 동시에 할 수 있음
- 스프링에서 포인트컷 표현식은 AspectJExpressionPointcut을 사용하면 됨
- 사실, AspectJExpressionPointcut은 AspectJ라는 유명 프레임워크에서 제공하는 것을 가져와 일부 문법을 확장해서 사용하는 것임
- 때문에 AspectJ 포인트컷 표현식이라고도 함
6.5.12 포인트컷 표현식 문법
- AspectJ 포인트컷 표현식은 포인트컷 지시자를 이용해 작성함
- 대표적 지시자는 execution()이 있음
- []는 옵션 항목으로 생략 가능
- |는 OR 조건임
execution([접근제한자 패턴] 타입패턴 [타입패턴.]이름패턴 (타입패턴 | "..", ...) [throws 예외 패턴])
- 복잡해 보이지만 메소드 풀 시그니처를 문자열로 비교하는 개념과 비슷함
- 출력 내용
- public
- 접근제한자
- 생략시, 접근제한자의 조건을 부여하지 않는다는 의미임
- int
- 리턴값
- 필수항목이며, 반드시 하나의 타입을 지정하거나 와일드카드(*)를 써 모든 타입을 선택하도록 해야함
- springbook.learning.spring.pointcut.Target.
- 패키지와 타입 이름을 포함한 클래스의 타입 패턴
- 와일드 카드를 사용하거나, “..”를 사용하여 한 번에 여러 개의 패키지 선택 가능
- minus
- 메소드 이름 패턴
- 모든 메소드 선택시 와일드카드 사용
- (int, int)
- 파라미터의 타입 패턴
- “,”로 구분하면서 순서대로 적어야 함
- “..”는 파라미터의 타입과 개수에 상관없이 모두 다 허용하는 패턴
- “...”는 뒷부분의 파라미터 조건만 생략 가능
- throws java.lang.RuntimeException
- 예외 패턴으로 생략 가능 함
- public
6.5.12 포인트컷 표현식 학습 테스트 - 1
- 포인트 컷 테스트용 클래스를 만들어 학습 테스트 진행
- 메소드 시그니처인 execution()안에 포인트컷 표현식을 작성
- 포인트컷 표현식 테스트이기 때문에 타깃 클래스는 null
package com.ksb.spring.pointcutexpression;
public interface TargetInterface {
void hello();
void hello(String s);
int minus(int a, int b) throws RuntimeException;
int plus(int a, int b);
}
public class Target implements TargetInterface{
@Override
public void hello() {}
@Override
public void hello(String s) {}
@Override
public int minus(int a, int b) throws RuntimeException {
return 0;
}
@Override
public int plus(int a, int b) { return 0; }
public void method() {}
}
public class Bean {
public void method() throws RuntimeException{}
}
public class PointCutTest {
@Test
public void methodSignaturePointcut() throws NoSuchMethodException {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(public int " +
"com.ksb.spring.pointcutexpression.Target.minus(int,int) " +
"throws java.lang.RuntimeException)");
//Target.minus
//성공
assertThat(pointcut.getClassFilter().matches(Target.class) &&
pointcut.getMethodMatcher().matches(
Target.class.getMethod("minus", int.class, int.class),null
), is(true));
//Target.plus
//메소드 매처에서 실패
assertThat(pointcut.getClassFilter().matches(Target.class) &&
pointcut.getMethodMatcher().matches(
Target.class.getMethod("plus", int.class, int.class),null
), is(false));
//Bean.method
//클래스 필터에서 부터 실패
assertThat(pointcut.getClassFilter().matches(Bean.class) &&
pointcut.getMethodMatcher().matches(
Target.class.getMethod("method"),null
), is(false));
}
}
6.5.12 포인트컷 표현식 학습 테스트 - 2
- 접근 제한자, 클래스 타입 패턴 등 옵션 부분을 생략하면 매우 간단해짐
- execution(int minus(int,int))
- 단, 생략한 부분은 모든 경우를 다 허용하기 때문에 느슨한 포인트컷이 됐음
- 와일드 카드를 사용하면 더욱 간단한 포인트컷을 만들 수 있음
execution(* minus(int,int)) //리턴 타입 무시
execution(* minus(..)) //파라미터 무시
execution(* *(..)) //메소드 무시
public class PointCutTest {
...
@Test
public void pointcut() throws Exception{
targetClassPointcutMatches("execution(* *(..))",
true, true, true, true, true, true);
//나머지 생략 표 6-1
}
private void targetClassPointcutMatches(String expression, boolean... expected)
throws Exception{
pointcutMatches(expression, expected[0], Target.class, "hello");
pointcutMatches(expression, expected[1], Target.class, "hello", String.class);
pointcutMatches(expression, expected[2], Target.class, "plus",int.class, int.class);
pointcutMatches(expression, expected[3], Target.class, "minus",int.class, int.class);
pointcutMatches(expression, expected[4], Target.class, "method");
pointcutMatches(expression, expected[5], Bean.class, "method");
}
private void pointcutMatches(String expression, boolean expected,
Class<?> clazz, String methodName, Class<?>... args)
throws Exception {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(expression);
assertThat(pointcut.getClassFilter().matches(clazz) &&
pointcut.getMethodMatcher().matches(clazz.getMethod(methodName, args)
, null), is(expected));
}
}
6.5.13 포인트컷 표현식을 이용하는 포인트컷 적용
- AspectJ 포인트컷 표현식은 메소드를 선정하는데 편리하게 쓸 수 있는 강력한 표현식 언어임
- execution() 외에도 몇 가지 표현식 스타일을 가지고 있음
- 대표적으로 bean()을 사용하면 괄호 내부 조건에 맞는 아이디를 만족하는 모든 빈을 선택함
- 💡 bean(*Service)는 아이디가 Service로 끝나는 모든 빈을 선택함
- 또한 특정 애노테이션이 타입, 메소드, 파라미터에 적용되어 있는 것을 보고 메소드를 선정하는 포인트컷을 만들 수 있음
- 💡 @annotation(org.springframework.transaction.annotation.Transactional)을 사용하면 애노테이션 중 @Transaction 애노테이션이 적용된 메소드를 선정
- 기존 xml에 존재하는 포인트컷은 클래스 이름, 메소드 이름에 대한 패턴임
<bean id="transactionPointcut"
class="com.ksb.spring.NameMatchClassMethodPointcut">
<property name="mappedClassName" value="*ServiceImpl"/>
<property name="mappedName" value="upgrade*"/>
</bean>
- 포인트컷 표현식을 사용한 빈 설정은 value에 표현식 하나만 적으면 됨
<bean id="transactionPointcut"
class="org.springframework.aop.aspectj.AspectJExpressionPointcut">
<property name="expression" value="execution(* *..*ServiceImpl.upgrade*(..))"/>
</bean>
6.5.14 타입 패턴과 클래스 이름 패턴
- 이전에 네이밍을 통한 포인트컷 선정 때문에 TestUserService을 TestUserServiceImpl로 변경했음
- 현재 포인트컷 표현식 역시 *ServiceImpl임(단, NameMatchMethodPointcut을 사용하지 않고, AspectJExpressionPointcut으로 변경했음)
<property name="expression" value="execution(* *..*ServiceImpl.upgrade*(..))"/>
- 여기서 TestUserServiceImpl를 TestUserService로 클래스 이름을 변경하면 표현식에 위반이 되니 테스트 실패할 것이라 생각 하겠지만, 사실 테스트가 성공해야 정상임
- 그 이유는 포인트컷 표현식의 클래스 이름에 적용되는 패턴은 클래스 이름 패턴이 아니라 타입 패턴이기 때문임
- TestUserService는 UserServiceImpl를 상속하고, UserServiceImpl는 UserService를 상속함
- 즉, TestUserService는 UserServiceImpl타입이라 할 수 있음
- 표 6-1의 16번에서 TargetInterface 인터페이스 표현식을 사용했을 때 Target오브젝트가 포인트컷에 선정된 이유가 바로 이 이유임
- Target은 TargetInterface를 구현했기 때문에 Target 오브젝트는 TargetInterface 타입임
6.5.15 UserService에 트랜잭션을 적용해온 과정
- 트랜잭션 서비스 추상화
- 비즈니스 로직에 트랜잭션 기술이 등장하면서 트랜잭션 기술 종속적인 문제가 발생
- 트랜잭션 적용이라는 추상적인 작업 내용을 유지한 채로 구체적인 구현 방법을 자유롭게 바꾸기 위해 서비스 추상화 도입
- 런타임 시에 다이내믹하게 연결하는 DI 방법을 활용한 접근 방법
- 프록시와 데코레이터 패턴
- 서비스 추상화를 하더라도 여전히 비즈니스 로직에 트랜잭션을 적용하고 있다는 사실이 드러나 있음
- 단순한 추상화와 메소드 추출 방법으로 제거할 수 없음
- DI를 사용해 데코레이터 패턴 적용
- 클라이언트가 인터페이스와 DI를 통해 접근하도록 설계하고, 데코레이터 패턴을 적용해 비즈니스 로직을 영향주지 않으면서 부가기능을 자유롭게 부여하는 구조를 만듦
- 데코레이터는 부가기능만 추가하고 비즈니스 로직은 타깃 또는 다른 데코레이터에 위임
- 클라이언트가 프록시 역할을 하는 트랜잭션 데코레이터를 거쳐 타깃에 접근
- 다이내믹 프록시와 프록시 팩토리 빈
- 데코레이터 패턴으로 비즈니스 로직에서 트랜잭션 코드는 모두 제거할 수 있었음
- 하지만, 트랜잭션 기능을 부여하는 코드를 일일이 만들고 트랜잭션 기능이 필요하지 않은 메소드조차 프록시로서 위임을 해야하기 때문에 전부 구현해야 함
- JDK 다이내믹 프록시 기술을 적용해 부가기능 코드가 중복되는 문제 해결
- 하지만, 동일한 기능의 프록시를 여러 오브젝트에 적용할 경우 오브젝트 단위로 중복이 발생함
- JDK 다이내믹 프록시와 같은 프록시 기술을 추상화한 프록시 팩토리 빈을 이용해서 다이내믹 프록시 생성 방법에 DI 적용
- 내부적으로 템플릿/콜백 패턴을 활요하는 스프링의 프록시 팩토리 빈 덕분에 부가기능을 담은 어드바이스와 부가기능 선정 알고리즘을 담은 포인트컷을 프록시에서 분리함
- 자동 프록시생성 방법과 포인트컷
- 여전히 트랜잭션 적용 대상이 되는 빈마다 일일이 프록시 팩토리 빈을 설정해야 하는 문제가 남았음
- 스프링 컨테이너의 빈 생성 후처리 기법을 활용해 자동으로 프록시를 만드는 방법 도입
- 어드바이스와 프록시를 완전히 분리
- 포인트컷 표현식을 사용해 포인트컷 클래스를 만들지 않아도 됐음
- 부가기능의 모듈화
6.5.16 부가기능의 모듈화
- 지금까지 관심사가 같은 코드를 객체지향 설계 원칙에 따라 분리했음
- 덕분에 낮은 결합도를 가지며, 유연하게 확장을 할 수 있었음
- 하지만, 트랜잭션 적용 코드는 기존에 써왔던 방법으로는 간단히 분리해서 모듈화할 수 없었음
- 이유는 트랜잭션 경계설정 기능은 다른 모듈의 코드에서 부가적으로 부여되는 특징이 있기 때문
- 때문에 트랜잭션 코드를 한데 모을 수 없고, 애플리케이션 전반에 걸쳐 흩어져 있음
- 트랜잭션 같은 부가기능은 핵심기능과 같은 방식으로 모듈화하기 매우 힘듦
- 핵심기능은 그 자체로 독립적으로 존재할 수 있으며, 독립적으로 테스트가 가능하고, 최소한의 인터페이스를 통해 다른 모듈과 결합해 사용할 수 있음
- 이와 대조적으로 부가기능은 핵심기능과 같은 레벨에서 독립적으로 존재할 수 없음
- 부가기능을 모듈화하기 위해 DI, 데코레이터 패턴, 다이내믹 프록시, 오브젝트 생성 후처리, 자동 프록시 생성, 포인트컷과 같은 기술을 사용했음
6.4.17 AOP : 애스펙트 지향 프로그래밍
- 트랜잭션과 같은 부가기능을 어떻게 모듈화할 것인가 연구한 사람들은, 부가기능 모듈화 작업이 기존의 객체지향 설계 패러다임과 구분과는 새로운 특징이 있다고 생각했음
- 이런 부가기능 모듈을 객체지향 기술에서 주로 사용되는 오브젝트와 다른 애스펙트(Aspect)라 부름
- 애스팩트란, 그 자체로 애플리케이션의 핵심기능을 담지 않지만, 애플리케이션을 구성하는 중요한 한 가지 요소이고, 핵심기능에 부가되어 의미를 갖는 특별한 모듈임
- 애스팩트는 부가될 기능인 어드바이스와, 어드바이스를 어디에 적용할지 결정하는 포인트컷을 함께 가지고 있음
- 그림 6-21 왼쪽은 부가기능을 분리하기 전의 상태임
- 그림 오른쪽은 부가기능을 독립적인 모듈인 애스펙트로 분리한 것임
- 2차원 평면적 구조를 3차원 다면체 구조로 가져가면서 각각 성격이 다른 부가기능은 다른 면에 존재하도록 했음
- 이렇게 독립된 측면에 존재하는 애스펙트로 핵심기능은 순수하게 그 기능을 담은 코드만으로 존재함
- 여러 다른 측면에 존재하는 부가기능은 결국 핵심기능과 함께 어우러져서 동작함
- 이렇게 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법을 애스펙트 지향 프로그래밍(AOP, Aspect Oriented Programming)라 함
- AOP는 OOP를 돕는 보조적 기술이지, OOP를 완전히 대체하는 새로운 개념은 아님
- AOP는 애스펙트를 분리함으로써 핵심기능을 설계하고 구현할 때 객체지향적인 가치를 지킬 수 있도록 도와주는 것임
- AOP는 결국 애플리케이션을 다양한 측면에서 독립적으로 모델링하고, 설계하고, 개발할 수 있도록 만들어주는 것임
- 그로인해 애플리케이션을 다양한 관점에서 개발할 수 있게 도와줌
- AOP를 특정 관점을 기준으로 바라볼 수 있게 해준다는 의미로 관점 지향 프로그래밍이라고도 함
6.4.18 프록시를 이용한 AOP
- 스프링은 IoC/DI 컨테이너와 다이내믹 프록시, 데코레이터 패턴, 프록시 패턴, 자동 프록시 생성 기법, 빈 오브젝트의 후처리 조작 기법 등의 다양한 기술을 조합해 AOP를 지원하고 있음
- 프록시를 만들어서 DI로 연결된 빈 사이에 적용해 타킷의 메소드 호출 과정에 참여해서 부가기능을 제공하도록 했음
- 어드바이스가 구현하는 MethodInterceptor 인터페이스는 다이내믹 프록시의 InvocationHandler와 마찬가지로 프록시로부터 메소드 요청정보를 전달받아 타깃 오브젝트를 호출하는 전후에 다양한 부가기능을 제공함
- 다이내믹하게 적용해주기 위해 가장 중요한 역할을 맡고 있는 게 바로 프록시임
- 그래서 스프링 AOP는 프록시 방식의 AOP라고 할 수 있음
6.4.19 바이트코드 생성과 조작을 통한 AOP
- 프록시 방식 AOP가 대부분이나, 프록시 방식이 아닌 AOP 역시 존재함
- AspectJ 프레임워크가 대표적인 프록시 방식이 아닌 AOP 기술임
- AspectJ는 프록시처럼 간접적인 방법이 아닌, 타깃 오브젝트를 뜯어고쳐서 부가기능을 직접 넣어주는 직접적인 방법을 사용함
- 컴파일된 타깃 클래스의 파일 저체를 수정하거나 클래스가 JVM에 로딩되는 시점을 가로채서 바이트 코드를 조작함
- 물론, 소스코드를 수정하지 않으므로 개발자는 계속해서 비즈니스 로직에 충실할 수 있음
- 장점
- 스프링과 같은 DI 컨테이너의 도움을 받지 않아도 AOP 적용이 가능함
- 바이트 코드를 직접 조작해 오브젝트 생성, 필드값 조작 등 프록시보다 훨씬 강력하고 유연한 AOP 가능
6.4.20 AOP의 용어
- 타깃
- 부가기능을 부여할 대상
- 핵심기능을 담은 클래스일 수 있지만, 경우에 따라 다른 부가기능을 제공하는 프록시 오브젝트 일 수 있음
- 어드바이스
- 타깃에 제공할 부가기능을 담은 모듈
- 오브젝트로 정의하기도 하지만 메소드 레벨에서 정의 가능
- MethodInterceptor처럼 메소드 호출 과정에 전반적으로 참여하는 것도 있지만, 예외가 발생했을 때만 동작하는 어드바이스처럼 메소드 호출 과정의 일부에서만 동작하는 어드바이스도 존재
- 조인 포인트(Join Point)
- 어드바이스가 적용될 수 있는 위치
- 스프링의 프록시 AOP에서 조인 포인트는 메소드의 실행 단계뿐임
- 타깃 오브젝트가 구현한 인터페이스의 모든 메소드는 조인 포인트가 됨
- 포인트컷
- 어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 그 기능을 정의한 모듈
- 스프링의 AOP의 조인 포인트는 메소드의 실행이므로 스프링의 포인트컷은 메소드를 선정하는 기능을 가짐
- 때문에 메소드의 실행이라는 의미인 execution으로 시작하고, 메소드의 시그니처를 비교하는 방법으로 주로 이용함
- 메소드는 클래스 안에 존재하는 것이기 때문에 메소드 선정이란 결국 클래스를 선정하고 그 안의 메소드를 선정하는 과정을 거치게 됨
- 프록시
- 클라이언트와 타깃 사이에 투명하게 존재하면서 부가기능을 제공하는 오브젝트
- DI를 통해 타깃 대신 클라이언트에게 주입됨
- 클라이어트의 메소드 호출을 대신 받아서 타깃에 위임 및 과정에서 부가기능 부여
- 스프링은 프록시를 이용해 AOP 지원
- 어드바이저
- 포인트컷과 어드바이스를 하나씩 갖고 있는 오브젝트
- 스프링은 자동 프록시 생성기가 어드바이저를 AOP 작업의 정보로 활용함
- 어드바이저는 스프링 AOP에서만 사용되는 특별한 용어임
- 일반적인 AOP에서는 사용되지 않음
- 애스펙트
- OOP의 클래스와 마찬가지로, 애스펙트는 AOP의 기본 모듈임
- 한 개 이상의 포인트 컷과 어드바이스의 조합으로 만들어지며 보통 싱글톤 형태의 오브젝트로 존재함
- 스프링의 어드바이저는 아주 단순한 애스펙트라 볼 수 있음
6.4.21 스프링 프록시 방식 AOP를 위한 네 가지 빈
- 어드바이스를 제외한 세 가지 빈 모두 스프링이 직접 제공하는 클래스를 빈으로 등록하고 프로퍼티만 설정
- 네 가지 빈
- 자동 프록시 생성기
- 스프링의 DefaultAdvisorAutoProxyCreator 클래스 빈으로 등록
- 다른 빈을 DI 하지도 않고 자신도 DI 되지 않아 독립적으로 존재
- 따라서 id도 굳이 필요 없음
- 애플리케이션 컨텍스트가 빈 오브젝트를 생성하는 과정에서 빈 후처리기로 참여
- 빈으로 등록된 어드바이저를 이용해서 프록시를 자동으로 생성
- 어드바이스
- 부가기능을 구현한 클래스를 빈으로 등록
- TransactionAdvice는 AOP 관련 빈 중에서 유일하게 직접 구현한 클래스를 사용
- 포인트컷
- AspectJExpressionPointcut을 빈으로 등록하고 expression 프로퍼티에 포인트컷 표현식을 넣음
- 때문에, 코드 작성 불필요
- 어드바이저
- DefaultPointcutAdvisor 클래스를 빈으로 등록해서 사용
- 어드바이스와 포인트컷 프로퍼티로 참조하는 것 외에는 기능이 없음
- 자동 프록시 생성기에 의해 자동 검색되어 사용됨
- 자동 프록시 생성기
6.4.22 AOP 네임스페이스
- 스프링은 AOP와 관련된 태그를 정의해둔 aop 스키마를 제공함
- aop 스프키마에 정의된 태그는 별도의 네임스페이스를 지정해서 디폴트 네임스페이스의 <bean> 태그와 구분해서 사용할 수 있음
- aop 스키마에 정의된 캐그를 사용하려면 성정파일에 aop 네임 스페이스 선언을 추가해야 함
- aop:config, aop:pointcut, aop:advisor 세 가지 태그를 정의해두면 그에따른 세 개의 빈이 자동으로 등록됨
- 💡 어드바이스는 직접 등록해야 함!
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<bean id="transactionAdvice" class="com.ksb.spring.TransactionAdvice">
<property name="transactionManager" ref="transactionManager"/>
</bean>
<aop:config>
<aop:pointcut id="transactionPointcut" expression="execution(* *..*ServiceImpl.upgrade*(..))"/>
<aop:advisor advice-ref="transactionAdvice" pointcut-ref="transactionPointcut"/>
</aop:config>
...
</beans>
- 포인트컷을 내장한 어드바이저 태그로 만들 수 있음
- 포인트컷을 내장하는 경우 aop:advisor 태그 하나로 두 개의 빈이 등록 됨
- 즉, 애트리뷰트 설정에 따라 등록되는 빈의 개수와 종류가 달라질 수 있음
<aop:config>
<aop:advisor advice-ref="transactionAdvice" pointcut="execution(* *..*ServiceImpl.upgrade*(..))"/>
</aop:config>
'토비의 스프링 정리' 카테고리의 다른 글
토비의 스프링 - 6.7 애노테이션 트랜잭션 속성과 포인트컷 (0) | 2022.10.20 |
---|---|
토비의 스프링 - 6.6 트랜잭션 속성 (0) | 2022.10.20 |
토비의 스프링 - 6.4 스프링의 프록시 팩토리 빈 (0) | 2022.10.07 |
토비의 스프링 - 6.3 다이내믹 프록시와 팩토리 빈 (0) | 2022.10.07 |
토비의 스프링 - 6.2 고립된 단위 테스트 (0) | 2022.10.06 |