AspectJ와의 연동을 고려한다면, 포인트컷을 최소화 해야합니다.

무슨 이야기냐면, 최소 권한 원칙인가… 그런거랑 비슷한겁니다.

바로 예제를 보면서 살펴보죠.

@Aspect
public class HibernateExceptionToDataAccessException {

    @Pointcut(“@within(org.springframework.stereotype.Repository)”)
    public void accountHibernateExceptionInDao(){}

    @AfterThrowing(pointcut=”accountHibernateExceptionInDao()”, throwing=”e”)
    public void translateHibernateException(HibernateException e){
        throw SessionFactoryUtils.convertHibernateAccessException(e);
    }

}

이 애스팩트는 하이버네이트 예외를 스프링의 DataAccessException으로 변환해주는 애스팩트입니다. 이 녀석은 하이버네이트로 DAO 만드는 여러 방법 중에 가장 깔끔한 방법을 사용할 때 쓰면 좋고 안 써도 별로 상관없는(하이버네이트 버전이 올라가면서 하이버네이트 예외도 uncatched exception으로 바꼈으며 계층 구조도 세밀하게 나눠뒀기 때문입니다.) 그런 애스팩트입니다. 그래도, 버전 올리기 힘든 하이버 예전 버전을 사용하는 라이브러리를 사용해야 한다면 유용하겠죠.

어쨋든, 본론으로 돌아가서..

저렇게 만들어둔 애스팩트에 문제점이 보이나요? 저도 방금전까진 몰랐습니다. 일단, 저 애스팩트의 포인트컷은 이해가 가시죠? @Repository 애노테이션을 가지고 있는 클래스의 모든 조인포인트를 나타낸 겁니다.

문제는 바로 이 조인포인트.. 이게 핵심입니다. 스프링에서는 메소드 실행 조인포인트만 사용하기 때문에, 저 애스팩트를 스프링 AOP에서 사용할 땐, 원하던 메소드에만 적용이 될 겁니다. 하지만, 만약 저 애스팩트를 AspectJ와 연동해서 사용한다면? 어떤 일이 벌어질까요?

사용자 삽입 이미지
캬오.. 43개??? 말도 안돼. 내가 테스트 할려고 만든 DAO가 몇개나 된다고.. 그 안에 메소드도 거의 한 두 개밖엔 안만들었는데.. 왠 43개…. 바로 크로스 레퍼런스 뷰를 열고 확인해봤습니다.

사용자 삽입 이미지
캬오~~~~ 마이 미스테이크… 처방이 필요합니다. 처방은 간단하기 때문에 비밀! 캬캬캬.(이번주 KSUG 세미나에서 공개하도록 하죠.) 처방후에는..

사용자 삽입 이미지
이렇게 AspectJ에서도 메소드 실행 조인포인트에만 걸 수 있습니다. 음하하하..

AspectJ로 final method에도 위빙하기

소스 코드는 이전과 동일합니다. 그 상태에서 프록젝트에 AspectJ Nature를 추가해줍니다.

사용자 삽입 이미지
그런 다음에 프로젝트 클린을 하여 기존의 클래스파일을 비우고 다시 컴파일하게 합니다.(Alt + P, N, 엔터) 지금, 저는 아무런 .aj 파일도 만들지 않았습니다. 그냥 이전에 만들어둔 Spring @AOP를 사용하고 있을 뿐입니다.

@Aspect
public class FinalHelloAspect {

    @Before(“execution(* org.opensprout.sandbox.proxy.withfinal.FinalHello.*(..))”)
    public void withFinalAdvice() {
        System.out.println(“before advice from FinalHelloAspect”);
    }

}

이 녀석이죠. AspecJ 프로젝트가 됐기 때문에, 빌드타임에 저 애스팩트의 포인트컷에 해당하는 클래스 파일을 조작해서 “새로운 .class 파일”을 생성하게 될 겁니다.

사용자 삽입 이미지
컴파일이 끝난 후 AspectJ IDE가 발동한 모습이니다. 포인트컷 마다 저런 화살표가 표시됩니다. 보시면 final 메소드에도 화살표가 표시되어 있습니다. 해당 코드와 Cross References 뷰를 동기화 시켜두면, 해당 지점에 적용될 어드바이스까지 표시됩니다.

사용자 삽입 이미지
놀랍습니다. 안 그러세요? @Aspect 캬오~ 하긴, 그리 놀랍지도… @Aspect 애노테이션이 aspectj 프로젝트에 속한 애노테이션이니까,..

사용자 삽입 이미지
이 결과를 보면 더이상 프록시를 사용하고 있는것이 아니기 때문에, Advised 라는 인터페이스는 사용할 수 없다는 것을 볼 수 있습니다. ClassCastException이 보이시죠?

흠냐;;

회사 청소해야되서 이만.. 황급히 마무리 합니다.

CGLIB 프록시 제약 사항 테스트.

@Component
public class FinalHello implements Hello {

    public String hi() {
        return “hi”;
    }
    
    public final void finalHi(){
        System.out.println(“안녕”);
    }
    
}

위 클래스에 모든 메소드 호출을 포인트컷으로 간단한 Adivce를 적용하여 CGLIB 프록시를 생성했을 때, 다음과 같은 결과를 확인할 수 있었습니다.

1. final 메소드를 호출할 경우 Advice를 적용하지 않음.
2. final이 아닌 메소드에는 Advice가 적용됨.

이런 간단한 테스트를 해보기 전에는 막연히 final 메소드가 있으면 CGLIB 프록시를 못 만드는 건가 싶었는데, 그게 아니었습니다.

한 가지 더 확인해봤습니다. 기본 생성자(인자가 없는 생성자)가 반드시 있어야 한다고 본것 같아서, 그걸 확인해봤습니다. 위의 경우에는 기본 생성자가 있는 상태로 테스트를 했으니까 기본 생성자 없이 테스트를 한 번 더 해보면 되겠죠.

@Component
public class FinalHello implements Hello {
   
    String hi;
   
    public FinalHello(String hi) {
        this.hi = hi;
    }

    public String hi() {
        return hi;
    }
   
    public final void finalHi(){
        System.out.println(“안녕”);
    }
   
}

그리고 위에서 실행했던 테스트 코드와 애스팩트를 그대로 사용해봤습니다.

java.lang.IllegalStateException: Failed to load ApplicationContext
    at org.springframework.test.context.TestContext.getApplicationContext(TestContext.java:203)
    at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:109)
    at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:75)
    at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:255)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:93)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.invokeTestMethod(SpringJUnit4ClassRunner.java:130)
    at org.junit.internal.runners.JUnit4ClassRunner.runMethods(JUnit4ClassRunner.java:51)
    at org.junit.internal.runners.JUnit4ClassRunner$1.run(JUnit4ClassRunner.java:44)
    at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:27)
    at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:37)
    at org.junit.internal.runners.JUnit4ClassRunner.run(JUnit4ClassRunner.java:42)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:38)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘finalHello’ defined in file [C:\workspace\osaf\target\test-classes\org\opensprout\sandbox\proxy\withfinal\FinalHello.class]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [org.opensprout.sandbox.proxy.withfinal.FinalHello]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.opensprout.sandbox.proxy.withfinal.FinalHello.<init>()
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:883)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:839)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:440)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory$1.run(AbstractAutowireCapableBeanFactory.java:409)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:380)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:264)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:221)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:261)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:185)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:164)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:429)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:729)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:381)
    at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:84)
    at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:42)
    at org.springframework.test.context.TestContext.loadApplicationContext(TestContext.java:173)
    at org.springframework.test.context.TestContext.getApplicationContext(TestContext.java:199)
    … 16 more
Caused by: org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [org.opensprout.sandbox.proxy.withfinal.FinalHello]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.opensprout.sandbox.proxy.withfinal.FinalHello.<init>()
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:58)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:877)
    … 33 more
Caused by: java.lang.NoSuchMethodException: org.opensprout.sandbox.proxy.withfinal.FinalHello.<init>()
    at java.lang.Class.getConstructor0(Class.java:2706)
    at java.lang.Class.getDeclaredConstructor(Class.java:1985)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:54)
    … 34 more

맞네요. 두 번쨰 제약 사항까지 살펴봤습니다.

CGLIB 프록시가 JDK 프록시에 비해 성능도 좋고, concrete 클래스의 프록시도 만들어 주기 때문에, 좋긴 한데, 위의 두 개의 제약 사항(final 메소드에는 어드바이스 적용 불가(상속을 못하니까.), 기본 생성자 필요.)에 주의 하면서 사용해야겠습니다.

만약에 이런 경우엔 어떻게 해야 할까요.

1. 구현하는 인터페이스 없음 ==> JDK 프록시 사용불가
2. 기본 생성자 없음 ==> CGLIB 사용불가
3. 어드바이스 적용하고자 하는 메소드가 fianl ==> 역시 CGLIB 사용불가
4. 하지만 AOP 하고파. ==> Spring AOP로는 불가능.

결론(지금은 머리로만 생각한 겁니다. 검증은 조금 뒤에..)
==> Spring AOP + AspectJ 연동해서, 빌드 타임에 aspectj-waever를 사용하던가, 클래스 로딩 타임에 loadtime-weaving을 하면 될 것입니다. AspectJ를 사용한 위빙은 프록시를 만드는게 아니라, 바이트코드랑 .aj 파일을 조작해서 타겟 클래스에 대한 .class파일을 다시 생성하고 그 코드를 사용하는 것이기 때문에 런타임시에 부하도 없고 위와 같은 Spring AOP 제약에서 벗어날 수 있으리라 봅니다.

Spring AOP 어디다 쓰면 좋을까?

어드바이스 종류 별로 생각해 보는게 좋을 듯 합니다. 어드바이스 특징이나 기타 자세한 설명은 생략하겠습니다. 제 블로그 어딘가에 다 있을 겁니다.ㅋ

1. Before 어드바이스
– 보안에 쓰면 좋겠다. 해당 메소드 실행 전에 인증과 권한을 체크해서, 없으면 예외를 던지도록 할 수 있겠다.
– 로깅은 어떨까.. 어떤 메소드를 실행하기 전에 해당 메소드 실행하겠다고 로그 메시지를 남기는거..

2. After returning 어드바이스
– 결과값을 확인해야 할 경우. 결과값이 특정 값이면 예외를 던지도록…
– 흠.. 매우 사용처가 난감하네. 이미 메소드 호출 한 다음이라.. 뭘 해야 한담.
– 메소드 호출 한 뒤에 로그 메시지를 남기도록 할까?

3. Throws 어드바이스
– 예외 로그 처리. 특정 예외가 발생했을 때 로그를 남길 수 있겠다.
– 예외 계층 구조를 바꿀 수 있겠다. Hibernate 예외를 스프링 예외로 변환하는 작업을 해본적이 있었지..

4. Around 어드바이스
– 트랜잭션 처리는 아마 이 녀석이 할 것 같다. 특정 메소드 실행 전에 트랜잭션 열고, 메소드 호출하고 트랜잭션을 커밋하거나 롤백해야 하니까.
– 로깅을 이걸로 할까. 그러면 Before, After, Throws에서 하려던 로깅을 이걸로 다 할 수 있으니..
– 성능측정. 스프링 레퍼런스에 StopWatch 예제가 있었던 것 같다. 개발 할 때만 만들어서 돌려보고 병목지점 발견하는데 요긴하게 쓸 수 있을 것 같기도 하다.

전반적으로 AOP는 기존 코드를 건드리지 않고 애플리케이션에 특정 행위를 추가할 수 있다는 것이 매력적인 것 같다.

[Spring Masters] 스프링 AOP 학습 참가 시험 문제 공개

Spring AOP 학습 이전에 필수로 알고 있어야 할 스프링 지식들을 확인하는 시험입니다. 24시간 안에 답을 적어서 저에게 메일로 제출해주세요.

1.    FactoryBean에 대해서 설명하세요.
2.    FactoryBean에서 getObject()가 돌려주는 객체 말고, FacotryBean 자체를 받아오려면 어떻게 해야 하나요?
3.    BeanFactory와 ApplicationContext의 공통적인 Life-cycle 클래스들의 나열하고 각자의 역할들을 설명하세요.
4.    Bean을 생성하고 소멸시킬 때 특정 메소드를 호출하는 방법에는 어떤 것들이 있으며, 그 방법 들 중에 어떤 방법이 더 좋을지, 이유와 함께 설명하세요.
5.    Bean들 사이에서 Circular Dependency가 있을 경우 어떤 일이 발생하는지, 그리고 해결할 수 있는 방법들을 나열해 주세요.
6.    Autowiring을 사용하여 byType으로 빈을 주입할 때, 같은 타입의 빈이 두 개 이상일 때 어떤 현상이 발생하나요?
7.    Lookup method injection과 Arbitrary method replacement가 무엇인지 설명하세요.
8.    @Autowired는 빈의 타입을 사용하여 종속성을 주입합니다. 이 때, 같은 타입의 빈이 여러 개라면, 어떻게 해야 하나요?
9.    <context:component-scan> 설정을 등록해보세요. 기본 패키지는 me.whiteship 이며, 단, org.springframework.stereotype.Controller 애노테이션이 붙어있는 것들은 컴포넌트 스캔 대상에서 제외합니다.
10.    <tx:annotation-driven transaction-manager=”transactionManager”/> 이 빈 설정에는 불필요한 설정이 있습니다. 그 설정이 무엇이며 이유가 뭔가요?

다음은 OX 퀴즈입니다.

11.    웹 서버에서 동작할 스프링의 Singleton Scope 빈은 반드시 Thread-Safe 해야 한다.
12.    스프링에서 선언적인 트랜잭션 관리를 할 때는 반드시 Proxy(CGLib이던 JDK의 Proxy건)를 사용해야 한다.
13.    ApplicationContext는 모든 Singleton 스코프의 빈을 초기에 생성한다.
14.    JtaTransactionManager는 nested transactions을 지원한다.
15.    ApplicationContext – B가 ApplicationContext – A를 상속하고 있을 때, 즉 A가 부모, B가 자식 관계일 때, A에 있는 빈이 B에 있는 빈을 참조할 수 있다.

마지막으로 Spring Masters에서 스프링 AOP 학습을 하려는 이유와 목적이 무엇인지 말씀해주세요. 수고하셨습니다.

시험을 치르고자 하시는 분들은 저에게 메일을 주시면 위와 동일한 문제를 담고 있는 워드 파일을 보내드립니다. 시험 방식은 오픈북이며, 시간 제한은 24시간입니다. 오늘 보내드리면, 적어도 내일 저녁까지는 제 메일 함으로 와야 합니다. 그럼 제가 답변들을 검토한 다음 피드백과 함께 참가 가능 여부를 메일로 보내드리겠습니다.

위 문제들이 너무 쉽다고 스터디도 쉽게 보시면 안 됩니다. 위 문제들을 풀 수 있느냐가 중요 한 게 아닙니다. 이 시험은 스프링 AOP 스터디 참가 조건 중 하나 일 뿐이라는걸 알아주세요.

본 글에 댓글로 질문에 대한 답변이나 또 다른 질문을 하시는 건 자제해주세요. 경우에 따라서는 삭제할 수도 있습니다. 😉