AspectJ를 이용한 코드젠과 프레임워크 2

이전 글 AspectJ를 이용한 코드젠과 프레임워크에 이어지는 글로 현재는 Service과 Controller까지도 적용을 마치고 실행 후 잘 돌아가는 모습을 살펴보습니다. 지금은 웹 쪽을 REST 스타일로 고칠겸, 새 프로젝트로 깔끔하게 정리하고, 새로 디자인한 것까지 모두 합치고 있습니다. (역시 적당한 단계별로 해야지 한꺼번에 하려니깐 정신없고 오래 걸리는 듯합니다.)

1. AJ 내에서 코드 젠을 이용하여 상속을 없앨 수 있다.

AJ 코드를 어떻게 작성하느냐에 따라 다르겠지만, ROO가 제공하는 AJ를 스타일대로 작성하면, 상속을 이용하지 않고도 추가 기능을 넣을 수 있다. 장담점이 있다고 생각된다. 상속을 없앤건 좋은 일이라고 생각된다. 프레임워크 코드가 아닌, 비즈니스 도메인에 의한 상속을 사용할 수 있을테니말이다. 하지만, 상속을 포기하고 일일히 다 주입하는 방법을 택하다보니, 비슷한 코드가 여러 AJ에 널려있게 된다. 이 때는 또 한 가지 변수 AJ 파일의 길이를 고려해야 할 것 같다. AJ가 일일히 작성하기에 너무 길다고 생각 된다면, AJ 상속 .. 보다는 AJ에도 Generic을 도입할 수 있다면 편할지 모르겠다. 또한 AJ 작성에 걸리는 시간을 감안할 때 AJ를 코드젠하는 기술은 거의 필수라고 생각된다. 하지만 AJ가 코드젠 없이도 만들만큼 분량이 적다면? 그렇다면 굳이 코드젠 없이 그냥 손수 AJ 파일을 만들어 쓰면 될 것 같다. 하지만 내가 오늘 작업해본 AJ 코드는 일일히 작성하기에는 너무 길었다. 코드젠이 필요하다고 느껴졌다. 

2. 테스트가 필요하다.

코드가 눈에 보이면 고치기도 간편하다. 그래서인지, 왠만큼 돌아갈 것으로 만단되는 코드는 테스트를 잘 하지 않는다. 복잡한 로직이나 확신이 없는 코드에 대한 테스트만 작성하는 편이다. 자신감이라기 보다는 게으름이 맞을 것이다. 아무튼.. 그런데 이 게으름을 부리지 못할 정도로 날 불안하게 만드는 것이 바로 AJ다. 내가 아직 AJ에 익숙하지 않아서 일지도 모른다. 그래서 인지 불안하다. 그래서 인지 테스트가 절실하다.


컨틀로러 코드는 비어있지만, 아래 보시다시피 엄청나게 많은 것들이 추가됐다. 잘 동작하는지 어떻게 확신할 수 있겠는가? 글쎄.. 확신하지 못하겠다. 오랜만에 컨트롤러 테스트를 해보게 생겼다.

가짜 데이터 -> 컨트롤러 메서드 -> 결과 확인

스타일로 해볼 생각이다. 예를 들어, 위에 보이는 것중에 delete(int)가 있다. 가짜 데이터는 int 타입 변수 하나가 될 것이고, 메서드에 넘겨주고, service.delete()가 호출되는지 확인할 것이다.

3. 자동완성과 코드 네비게이션에 대한 대안을 찾았다.

위에 보이는 Cross References 뷰를 이용하는 것이다. Ctrl + 스페이스와 Ctrl + 클릭에 비할바는 못 되지만, 어떤 것들이 주입되었는지 한눈에 볼 수 있고 클릭을 하면 바로 AJ 파일로 이동해 주기 때문에 어느 정도 B급 대안이 될 수 있지 않을까 생각된다.

4. 배포는 어떨까?

어제도 포스팅 했지만, 메이븐에서 AJ를 컴파일 하도록 설정해주기만 하면 배포에도 전혀 문제가 없다. AJ는 스프링 AOP와 달리 컴파일 시점 위빙을 사용할 수 있기 때문에 더 간편한 것 같다. 스프링 AOP처럼 agent 설정을 하지 않아도 되니 얼마나 좋은가. 심지어 메이븐 설정을 스프링 ROO예제에서 그대로 배낄 수도 있다.ㅋㅋ

5. 활용 방법이 여러 가지다.
 
잘 모르겠지만, 기존의 자바 프레임워크는 (이클립스 플러긴 같은 툴은 빼고) 아마도 Generic을 활용한 DAO, Service, Controller 추상 계층과, View 자동 생성 그리고 유틸 라이브러리를 제공해주는 것일 것이다. 여기에 AspectJ 활용을 극대화 하여 새로운 프레임워크를 만드는 방법이 몇 가지 있겠다.

a. 기존 프레임워크에 AJ 파일 소수(하나 내지 두개)를 추가하여 자동 (또는 수동으로) 생성되는 코드를 깔끔하게 만드는 방법이 있다.
b. 스프링 ROO처럼 기존에 Generic 클래스 상속으로 제공하던 기능들을 일일히 AJ로 주입해주는 것이다.
c. 위 두 가지 방법을 혼용하는 것이다.

이 셋의 차이점은 좀 더 코딩하면서 느껴봐야 확실해 지겠지만, 지금까지 느낀점을 간략하게 정리하자면 이렇다.

a: AJ 코드량과 생성된 코드량이 모두 적다. 생성된 코드가 POJO 처럼 보이지만 사실은 이전과 동일하다.(extends Generic머시기..) 코드젠이 필요없거나, 간단한 코드젠이 필요하다.

b: AJ 코드량이 엄청 많지만, 생성된 코드량은 적다. 생성된 코드는 POJO에 가깝다. 다소 복잡한 코드젠이 필요할 수 있다. AJ가 커서 여러 AJ로 나누는 것이 깔끔해 보일 수 있다.(스프링 ROO) 중요 로직이 눈에 확 띈다.

c: a방법을 사용하는 AJ와 b방법을 사용하는 AJ를 분리하는게 좋다. 결국엔 코드젠이 필요하다.

오늘의 결론

어떻게 하나 결국에 생성되는 코드는 (실제 그런지 어떤지를 떠나서) 깔끔해 보이기 때문에 핵심 로직은 눈에 확 들어 올 듯 하다. 하지만 그러기 위해서 드는 노고를 생각해보면… 이걸 내가 꼭… 해야되는건가.. 싶기도 하다. 특히 a 방법을 쓸꺼라면.. 그냥 겉치례에 불과하다고 느껴진다. 그럴바엔 그냥 기존 방식을 쓰는게 나아보인다. 많은 노고를 들여서 b 방법으로 개발해 낸다면.. 멋질 것이다. 스프링 ROO 처럼 말이다. 단, 스프링 ROO는 DDD 다른 또 다른 학습 부담이 있어서 망설여지지, 그렇지 않고 기존의 계층형 아키텍처 스타일로 b 형태의 프레임워크를 제공한다면, 어떨까?? OSAF-ROO가 되는건가..

AspectJ를 이용한 코드젠과 프레임워크

오늘은 도메인과 DAO쪽에만 AOP를 적용하는 AspectJ 파일을 만들어보았습니다. ROO를 참고하면서 말이죠. ROO와 다른 점은 프레임워크 코드를 이용한다는 거죠. (ROO는 제품 코드에서 ROO 코드는 하나도 이용하지 않는 완전한 non-intrusive 내지 transparent
코드젠 기술을 제공합니다.) 이런 식으로 새로운 형태의 OSAF도 만들어 낼 수도 있겠습니다. 하지만.. 할지 말지는 고민을 해봐야겠네요.

그 고민에 대한 시작으로, 아직은 충분한 예제를 못 만들었지만, 일단 여기까지 AspectJ를 이용한 프레임워크를 만들면서 느낌점을 정리해봐야겠습니다.

1. 자동완성 기능 사용 못 합.

이전 글처럼, AspectJ로 (메서드를 추가하거나 클래스 또는 인터페이스 상속을 추가하여) 어떤 클래스에 추가적인 기능들을 줬지만, 막상 이클립스에서 해당 클래스를 써먹을 때 코드 자동 완성을 사용할 수 없다는 점입니다. 원래 해당 클래스가 가지고 있던 멤버는 당연히 자동 완성이 되지만, AspectJ로 주입한 기능들은 참조가 되지 않습니다. 이 점은 AJDT에서 개선해주면 가능할지 싶은데… STS 최신 버전에선 어떨지 모르겠네요. 아무튼.. 이게 안 된다면.. 아.. 불편해..

2. 대체 뭐하는 녀석이람?

위 얘기랑 이어지는 이야기일 수도 있는데 해당 클래스가 하는 일이 숨겨져(?) 있다보니, 대체 어떤 일을 하는 지 눈치 채기가 쉽지 않습니다. 작명을 잘 해줘야겠죠.

3. 핵심 로직은 눈에 확 들어올 듯.

AJ 파일로 빼내는 로직들은 대부분 공통적인 내용일 겁니다. CRUD가 대부분이고 ROO의 경우에는 finder도 제공해주겠죠. 즉 감춰져 있는 부분이 무엇인가를 명확히 인지하고 있다면, 그 뒤에는 핵심 로직만 작성하면 될테고 코드에서 확 들어나게 되어 있겠죠. 이렇게 되면 2번에서 대체 뭐하는 녀석인가?라고 고민하는 시간도 줄어들테지요.

4. 코드 네비게이션 불편.

역시나 AJDT가 개선해 주길 바라지만, 현재로서는 AspectJ로 추가한 메서드나 필드로 Ctrl + 클릭으로 이동하는 것이 안 됩니다. 툴 측면에서 보면 1번과 비슷한 불편사항으로 볼 수 있겠습니다.

5. 성능?

AspectJ를 사용해서 컴파일 시점에 위빙을 하면 런타임 시에 성능 문제는 거의 없겠지만, 이 컴파일 작업이 매번 테스트를 돌릴 때 마다 일어나기 때문에 일반적인 테스트를 돌리는 것 보다는 조금 오래 걸리는 것이 사실입니다. 하지만 뭐 그정도 차이는 무시할만 하더군요.

오늘의 결론..

툴 지원이 조금만 더 보완된다면, AspectJ를 활용한 코드젠과 프레임워크를 활용하여 좀 더 깔끔하고 유연한 코딩을 즐길 수 있을 것으로 예상 됩니다. 코드 자동 완성과 네비게이션이 불편한 지금도 만약 AspectJ로 추가한 코드에 접근 할 필요가 없다는 가정을 한다면, 해볼 만 하다고 생각합니다.

Roo처럼 콘솔까지 제공하고, 변경 사항을 트래킹하여 롤백한다거나 코드젠 이후에 직접 코드를 수정해도 유기적으로 반영해주는 기능을 구현하긴 힘들겠지만, 단순한 코드젠으로 AspectJ를 생성하고 이 AspectJ가 (OSAF 같은) 프레임워크 코드를 이용하도록 한다면, 기존의 프레임워크를 한 단계 업그레이드 할 수 있는 방안이 되지 않을까 생각합니다.

ps: 흠.. 일주일을 쉬고 왔더니 머리가 빙빙 도네요. 색시한테 9까지 간다고 했는데, 9시에 출발하겠네.. 쏘리 쏘리 쏘리 쏘리~

[AspectJ] Extension and Implementation

http://www.eclipse.org/aspectj/doc/released/progguide/semantics-declare.html#extension-and-implementation

    declare parents: EmpDao extends GenericDao<Emp, EmpParams>;
    declare parents: EmpDaoImpl extends HibernateGenericDao<Emp, EmpParams>;
    declare @type: EmpDaoImpl: @org.springframework.stereotype.Repository;

EmpDao 클래스가 GenericDao 클래스를 상속 받도록 설정.
EmpDaoImpl 클래스가 HibernateGenericDao 클래스를 상속 받도록 설정.
EmpDaoImpl 클래스에 @Repository 애노테이션 추가.

문법이 복잡해 보였는데 막상 사용해보니 간단 간단 하네요.

public interface EmpDao {

}

public class EmpDaoImpl implements EmpDao {

}

이런 기초적인 코드만 존재하지만..

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={“/applicationContext.xml”, “/applicationContext-datasource.xml”})
@Transactional
public class EmpDaoImplTest {

    @Autowired
    EmpDaoImpl daoImpl;

    @Test
    public void daoInterface() throws Exception {
        assertNotNull(daoImpl);
        daoImpl = new EmpDaoImpl();
        GenericDao<Emp, EmpParams> gdao = daoImpl;
        HibernateGenericDao<Emp, EmpParams> hgdao = daoImpl;
    }

    @Test
    public void crud() throws Exception {
        Emp emp = new Emp();
        daoImpl.add(emp);
        daoImpl.flush();
        assertEquals(1, daoImpl.getAll().size());
    }

}

이런 테스트를 돌릴 수 있다는거…

[AspectJ] privileged aspect

public class Emp {

    private String name;

    private String email;

}

이런 클래스가 있습니다. 이게 전부입니다.

public class EmpTest {

    @Test
    public void javaBean() throws Exception {
        Emp emp = new Emp();
        emp.setName(“keesun”);
        assertEquals(“keesun”, emp.getName());
        emp.setEmail(“keesun@email.com”);
    }

}

이런 테스트가 돌아갈까요? 훗.. 그럴리가요. 있지도 않은 메서드(게터, 세터)를 마구 썼는데 될리가 없죠. 그러나..  잘 돌아갑니다.


어떻게 된걸까요? privileged aspect를 사용하면 타겟의 private 또는 protected 멤버에도 접근할 수 있습니다.

http://www.eclipse.org/aspectj/doc/released/progguide/semantics-aspects.html#aspect-declaration

스프링 AOP로 이런 일을 하려면 Introduction을 사용 해야겠는데.. 그게 참.. 그리 쉽지 않았던 기억이 납니다. 하지만 AspectJ로는 간단하네요~

privileged aspect EmpAspect {

    //JavaBean
    public String smdis.model.Emp.getName() {
        return this.name;
    }

    public void smdis.model.Emp.setName(String name) {
        this.name = name;
    }

    public String smdis.model.Emp.getEmail() {
        return this.email;
    }

    public void smdis.model.Emp.setEmail(String email) {
        this.email = email;
    }

}

이렇게 추가할 메서드를 넣어주고 마치 자기가 가지고 있는 변수처럼 사용하면 됩니다.

스프링 AOP에서 this와 target 포인트컷 표현식 구분 하시는 분?

Pro Spring 2.5에서 this 표현식에 대한 정의입니다.

the semantics of the this pointcut are such that it would match any method execution on an object whose class matches the specified expression, but how could we match any class in the com package and its subpackages? Therefore, the only allowed syntax is this(class-name),

Pro Spring 2.5에서 target 표현식에 대한 정의 입니다.

Because the target expression defines a pointcut that would match execution of any method on an object whose class matches the specified expression, we cannot use the wildcards.

뭐가 달라 보이나요??? 앞 부분에 어순이 약간 바뀐것 빼고는 도무지 차이를 알 수가 없습니다.

레퍼런스를 보죠.

this – limits matching to join points (the execution of methods when using Spring AOP) where the bean reference (Spring AOP proxy) is an instance of the given type

target – limits matching to join points (the execution of methods when using Spring AOP) where the target object (application object being proxied) is an instance of the given type

레퍼런스는 그나마 좀 차이가 보입니다. this일 때는 프록시 객체고 target 일 때는 프록시를 적용할 타겟 객체가 주체가 됩니다.

즉, this 표현식은 주어진 타입에 해당하는 스프링 AOP Proxy의 조인포인트에 대응하는 표현식이고.. target 표현식은 주어진 타입에 해당하는 타겟 객체의 조인포인트에 대응하는 표현식이라는 것인데..

어차피 프록시 객체의 타입이 곧 타겟 객체의 타입과 동일하기 때문에 결과는 똑같을 것 같은데 말이죠. 차이를 모르겠습니다. 아흐.. 머리야..

마지막으로 AspectJ 문서를 보겠습니다.

this(Type or Id)
    Picks out each join point where the currently executing object (the object bound to this) is an instance of Type, or of the type of the identifier Id (which must be bound in the enclosing advice or pointcut definition). Will not match any join points from static contexts.

target(Type or Id)
    Picks out each join point where the target object (the object on which a call or field operation is applied to) is an instance of Type, or of the type of the identifier Id (which must be bound in the enclosing advice or pointcut definition). Will not match any calls, gets, or sets of static members.

this는 동적이고 target은 정적으로 타입을 지칭하는 차이밖에 없어보입니다. 흠.. 과연..

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations=”applicationContext.xml”)
public class FooAspectTest {

    @Autowired
    FooBean fooBean;

    @Test
    public void isProxy(){
        assertTrue(fooBean instanceof Advised);
        fooBean.hi();
    }

}

@Aspect
public class FooAspect {

    @Pointcut(“this(org.opensprout.spring.aop.thisandtarget.FooBean)“)
    public void thisPointcut(){}

    @Before(“thisPointcut()”)
    public void say(){
        System.out.println(“this pointcut executed.”);
    }

}

스프링 AOP에서는 this를 target으로 고쳐도 똑같이 동작합니다. 콘솔에 메시지가 한 번 출력되죠. 그러나 AJDT로 실행하면, this의 경우 같은 메시지가 세 번 찍히고, target의 경우 같은 메시지가 네 번찍힙니다. 일단 두 번은 클래스 로딩이나 생성자 호출 조인포인트에서 찍혔다고 생각하고, 하나는 테스트에서 hi 호출 할 때 찍힌거고 가장 의아한건 target 표현식일 때의 마지막 한 번.

디버깅을 돌려본 결과 저 테스트에 두 번 들어가던데;; 왜 그런지 몰겠네요. 아~ 미궁이로 구나..