Spring 3.1을 활용한 AspectJ 표현식 테스트

AOP에서 AspectJ표현식은 다양하고 강력한 기능을 제공합니다. 예를 들면 Aspect가 결합될 JoinPoint를 아래와 같이 표현을 할 수 있습니다.

  • execution(public void set*(..)) : 리턴 타입이 void이고 메소드 이름이 set으로 시작되고 파라미터가 0개 이상인 메소드

  • execution(* com.benelog.ftpserver..() : com.benelog.ftpserver패키지의 파라미터가 없는 모든 메소드

  • execution(String com.benelog.ftpserver...(..) : 리턴타입이 String이면서 com.benelog.ftpserver패키지 및 하위 패키지에 있는 파라미터가 0개 이상인 메소드

  • execution(String com.benelog.ftpserver.Server.insert(..) : 리턴타입이 String인 com.benelog.ftpserver.Server의 파라미터가 0개 이상인 메소드

  • execution(* get*(*)) : get으로 이름이 시작하고 1개의 파라미터를 갖는 메소드

  • execution(* get*(,)) : get으로 이름이 시작하고 2개의 파라미터를 갖는 메소드 exceution(* read*(Interger,…​)) : read로 이름이 시작하고, 첫번째 파라미터 타입이 Integer이며, 1개 이상의 파라미터를 갖는 메소드

그러나 다양하고 강력한 대신에 모든 문법을 다 익숙하게 쓰기는 힘들고, 컴파일 타임에 체크도 안 되기 때문에 실수할 여지도 큽니다. 조금이라도 표현식을 바꾸었을 때 유지하고 싶은 기존의 동작이 변함없이 작동되는지도 파악하기 어렵습니다.

그래서 AOP는 강력한만큼 통제되지 못한 부작용이 생기기도 쉽습니다. 표현식 한글자를 바꾸어도 어떤 동작을 하는지 테스트를 촘촘히 해봐야하는데, 그럴 때마다 WAS를 띄워서 테스트를 해본다면 시간이 많이 들어갈 것입니다.

Spring 3.1부터는 테스트 코드 안에서 @Configuration , @Bean 과 같은 JavaConfig 설정들이 지원됩니다. 그 기능을 이용하면 XML없이 보다 짧은 코드로 AOP에 대한 다양한 테스트를 편하게 해 볼 수 있습니다. 단순한 예제로 Spring 3.1의 기능으로 AOP 테스트하는 방식을 정리해보았습니다. 아래 설명한 예제들은 gist에 올라가 있습니다.

LogAspect.java

LogAspect.java는 간단하게 로그를 저장소에 입력하는 Aspect입니다. @Before("execution(void *.run())") 라는 표현식으로 run 메소드가 실행되기 전에 로그메시지를 저장하도록 taget 객체와 결합됩니다.

@Aspect
public class LogAspect {
    private LogRepository repository;

    public LogAspect(LogRepository repository){
        this.repository = repository;
    }

    @Before("execution(void *.run())")
    public void log(JoinPoint jp){
        String message = jp.getKind() + ":" + jp.getSignature().getName();
        repository.insertLog(message);
    }
}

LogRepository.java

로그저장소 역할을 하는 클래스입니다. 실무에서라면 DB나 파일 등을 사용하겠지만, 여기서는 단순한 예제를 만들고 싶어서 그냥 String을 화면에 찍었습니다. 정교하게 만든다면 Log정보를 담는 Domain Object를 정의해서 String대신 사용해야 할 것입니다.

public class LogRepository  {
    public void insertLog(String message) {
        System.out.println(message);
    }
}

Taget 클래스

LogAspect와 결합될 Target 클래스입니다. 역시나 단순하게 화면에 메시지를 뿌려줍니다.

public class Printer implements Runnable {
    @Override
    public void run() {
        System.out.println("hello!");
    }
}

실제 코드에서의 applicationContext 설정

applicationContext를 설정하는 xml파일에서 aop: 네임스페이스를 이용해서 aspect J 표현식을 쓸 수 있게 하고, Aspect가 정의된 LogAspect를 bean으로 등록합니다.

<aop:aspectj-autoproxy/>
<bean id="printer" class="edu.tdd.aop.Printer"/>
<bean id="logAspect" class="edu.tdd.aop.LogAspect">
    <constructor-arg>
        <bean class="edu.tdd.aop.LogRepository"/>
    </constructor-arg>
</bean>

LogAspectTest.java

드디어 LogAspect의 표현식을 테스트하는 코드입니다. @Bean으로 Aspect와 tagetObject를 모드 XML설정 대신 java로 한 파일 안에서 표현했습니다.

각 테스트 메소드별로 2가지를 검증했습니다.

(1) proxyCreated() 메소드 : Proxy클래스가 생성되었는지 확인 (2) logInserted 메소드: LogAspect에서 호출하는 LogRepository에 원하는 메시지가 저장되었는지 확인

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class)
public class LogAspectTest {
    @Autowired Runnable targetProxy;
    @Autowired LogRepository repository;

    @Test
    public void proxyCreated() {
        assertTrue(AopUtils.isAopProxy(targetProxy));
    }

    @Test
    public void logInserted() {
        targetProxy.run();
        verify(repository).insertLog("method-execution:run");
    }

    @Configuration
    @EnableAspectJAutoProxy
    static public class TestContext {
        @Bean LogAspect aspect() {
            return new LogAspect(repository());
        }

        @Bean LogRepository repository(){
            return mock(LogRepository.class); // 검증용 객체를 application에다 등록
        }

        @Bean Runnable target(){
            return new Printer();
        }
    }
}

클래스 선언부분에서 @ContextConfiguration(loader=AnnotationConfigContextLoader.class) 를 붙이면 외부의 XML 대신 테스트 클래스 안에 포함된 JavaConfig로된 설정을 읽어올 수 있습니다. TestContext라는 내부 클래스에다 @Configuration을 붙여서 필요한 Bean을 등록했습니다. @EnableAspectJAutoProxy 는 <aop:aspectj-autoproxy/>와 같은 역할을 하는 애노테이션입니다.

아래와 같의 ApplicationContext에 LogRepository는 mock()으로 생성한 가짜 객체를 등록했습니다.

    @Bean LogRepository repository(){
        return mock(LogRepository.class); // 검증용 객체를 application에다 등록
    }

이 객체를 다시 Autowired로 받아온 후에 verify()를 했습니다. XML로 이와 비슷한 일을 하려면 코드가 더 길어지고, 별도의 파일로 분리가 됩니다. Mock을 Application에 등록하는 것이 바람직할지는 고민의 여지가 있지만 Aspect와 결합된 Proxy 클래스는 ApplicationContext를 통해야만 얻을 수 있고, 그 동작을 검증하고자 한다면 이 방식이 편하다고 느껴집니다.

여기서는 Printer객체를 target클래스로 사용했는데, 이 부분도 실제로 원하는 AOP 적용을 원하는 클래스나 제외 되어야할 클래스, 또는 직접 만든 테스트 전용 객체 등을 바꿔끼워가면서 다양한 조건을 검증할 수 있습니다.

덧붙여 verify(repository).insertLog("method-execution:run"); 부분에서 문자열을 직접 검증하는 것도 바람직하지 못한 방식일지도 모릅니다. 문자열 대신에 Log정보를 담는 도메인 객체를 따로 정의했다면, 그 객체에 메시지에 핵심키를 담고, equals를 override했을 것 같습니다. 문자열을 직접 검증하면 메시지의 형식이 바뀔 때마다 테스트가 깨어져서 유지보수가 번거로운 테스트 코드가 됩니다. 이 예제에서는 코드를 짧게 하고 테스트 코드 안에서 검증하고자 하는 동작을 단순하게 나타내려고 이 방식을 택했습니다.

Local 개발환경에서 WAS를 띄우는 여러가지 방법

Local 개발환경에서 WAS를 띄우는 여러가지 방식과 장단점에 대해서 정리해 봤습니다. 저는 상황에 따라서 아래 3가지 방법들을 그때 그때 골라서 씁니다.

  1. Eclipse WTP (+Agent based reloading) : WAS를 올린 상태에서 클래스파일을 고칠 때, class파일 1개만 리로딩하는 것이 좋을 때

  2. Maven Jetty plugin : JSP만 고칠 때나 클래스 파일을 고치면 전체 어플리케이션을 reloading을 하는 것이 유리할 때.

  3. Maven Tomcat plugin : JSP만 고칠때. WTP가 뭔가 꼬인 것 같은데 같은 Tomcat에서 같은 에러가 나는지 확인해보고 싶을 때

1. Eclipse WTP(Web Tools Platform) + Agent Based Reloading

Maven 프로젝트가 <packaging>war</packaging>`로 선언된 경우 `mvn eclipse:eclipse 명령을 치면 Eclipse의 dynamic web project로 설정을 만들어 줍니다. Run as Server혹은 server 설정에 drag & drop으로 넣고 Eclipse안에서 서버를 실행합니다. M2Eclipse를 잘 활용하면 local에서는 수동으로 maven 빌드를 돌릴 일이 거의 없어지기도 합니다.

항상 쓰는 것은 아니지만, SpringSource Tool Suite의 Agent based reloading을 이용해서 class파일을 수정하고 빠르게 리로딩을 하기도 합니다. 모든 상황에서 잘 통하지는 않아서, 그냥 WTP만 쓰는 것이 나을 때도 있습니다. 예를 들면 XML파일을 고쳤을 때에는 Agent based reloading에서는 잘 반영되지 않았습니다. JRebel에서는 보다 다양한 경우를 지원하는 것 같지만, 저는 테스트 코드에서 View를 빼놓고는 왠만한 건 실행해보고 WAS를 올리기 때문에 보다 강력한 JRebel이 크게 절실하지는 않습니다

그런데, Eclipse의 Dynamic web project + WTP 환경에서는 JSP를 고칠때의 반영속도가 미묘하게 느린 것 때문에 web context 폴더를 소스폴더 외에도 별도로 잡고, /WEB-INF/classes나 lib까지 다 그 폴더로 복사해서 'add external web module’로 등록해서 쓰시는 분들도 많습니다. 어디가 소스폴더이고 어디가 목적폴더인지 정리되어 있지 않아서 WEB-INF/lib나 WEB-INF/classes같이 꼭 버전관리가 필요없는 파일까지 같이 SVN에 commit하기도 하고, 복잡하게 svn:ignore를 시키는 수고를 하기도 합니다. WTP에서 'Serve modules without publishing ' 옵션을 써서 이를 해결하기도 합니다. 이에 대한 자세한 내용은 아래 자료에 정리되어 있습니다.

다음에 소개하는 maven jetty plugin과 Jetty나 Maven plugin을 써도 JSP가 더 빠르게 반영되기 때문에, WTP의 JSP반영 속도 때문에 일부러 'add external modules’로 WAS를 띄울 필요성은 적어집니다.

2. Maven jetty plugin

pom.xml에 Maven jetty plugin을 설정하면 따로 별도의 WAS설치과정이 필요없이 mvn jetty:run만 치면 바로 WAS가 뜹니다.

<plugin>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>maven-jetty-plugin</artifactId>
     <configuration>
         <scanIntervalSeconds>3</scanIntervalSeconds>
         <contextPath>/</contextPath>
      </configuration>
       <version>6.1.11</version>
</plugin>

디폴트로 8080포트를 쓰려면 위에서 "connector' 부분의 설정이 없어도 됩니다.

아래의 Tomcat plugin도 마찬가지지만, 이런 plugin이 설정되어 있으면, Eclipse가 없어도 SVN에서 checkout한후바로 명령어 하나로 WAS를 띄워볼 수 있습니다. 그 프로젝트의 개발자가 아닌 사람이 프로젝트를 띄워보거나 가끔 들어가는 프로젝트를 실행할 때도 편합니다.

대부분의 웹어플리케이션은 Servlet Spec만 따라서 개발하므로 Jetty에서 돌아가면 Tomcat에서 거의 돌아간다고 봐도 됩니다. 물론 성능테스트나 프로파일링 같은건 당연히 Tomat을 띄워서 해야겠죠.

다음에 소개할 Maven Tomcat plugin보다 WAS가 뜨는 속도나 리로딩 속도도 빠르다는 느낌입니다. "scanIntervalSeconds" 설정에 따라서 클래스 파일을 고쳐도 리로딩이 잘 됩니다. 'WTP + Agent based reloading’을 쓴다고 해도 뭔가 꼬이는 상황이 발생할 수 있고, 그럴 때는 전체 web context를 로딩하는게 좋습니다. 그런 상황이 많은 개발환경이라면 Jetty plugin이 유리합니다.

단점은 Tomcat이 아닌 Jetty이니 WAS level의 클래스가 던지는 에러메시지가 약간 다르기도 하고, Apache와 연결할때 AJP connector등을 설정할 수 없어서 Apache httpd의 모듈을 사용하는 페이지에는 쓸 수 없다는 점입니다.

3. Maven Tomcat plugin

jetty plugin과 비슷하게 pom.xml에 Tomcat plugin을 설정하고, mvn tomcat:run 을 치면 WAS 설치가 필요없이 Tomcat이 뜹니다.

설정은 아래와 같습니다.

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>tomcat-maven-plugin</artifactId>
    <version>1.1</version>
    <configuration>
        <path>/</path>
        <serverXml>$\{basedir}/src/main/webapp/server.xml</serverXml>
    </configuration>
</plugin>

만약 AJP설정 같은게 필요없어서 server.xml을 지정할 것까지도 없고 포트만 지정하면 된다면 아래처럼 하면 됩니다.

<configuration>
    <path>/</path>
    <port>8080</port>
</configuration>

그리고 port도 지정할 필요가 없이 디폴트인 8080으로 띄우고 싶다면 "<port/>" 부분도 생략해도 됩니다.

장점은 Tomcat이니까 다른 개발서버, 운영서버와 똑같은 환경이고, 같은 에러메시지가 뜬다는 점입니다. 그리고 server.xml을 지정할 수 있으니 AJP연결등으로 Apache와 연동하는 개발에도 쓸 수 있습니다. WTP가 꼬인 것 같을 때 mvn tomcat:run으로 확인해보고 다시 Eclipse의 WTP로 돌아가기도 합니다.

위 Plugin은 org.codehaus.mojo의 아래에 존재했지만, 최신 버전은 아래 링크의 Apache Tomcat 프로젝트 산하로 들어갔습니다.

Tomcat7은 아래와 같이 지정합니다.

    <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.2</version>
        <configuration>
            <path>/</path>
        </configuration>
    </plugin>

mvn tomcat7:run 으로 실행할 수 있습니다.

마찬가지로 Tomcat6 plugin도 활용할 수 있습니다.

    <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat6-maven-plugin</artifactId>
        <version>2.2</version>
        <configuration>
            <path>/</path>
        </configuration>
    </plugin>

mvn tomcat6:run 으로 실행합니다.

참고로 spring-loaded를 함께 쓰면 Tomcat재시작 횟수를 줄일수 있습니다. ( https://gist.github.com/benelog/aee89ac5b6ff896b2e0f 참조 )

Winstone, 경량 Servlet container

Winstone(http://winstone.sourceforge.net/ )은 jar파일 하나로 실행되는 간단한 Servlet Container입니다.

FTP처럼 서버에 있는 파일을 다운로드 할 때나, 웹애플리케이션을 jar파일로 패키징해서 WAS 설치없이 독립적으로 실행되도록 만드는 용도로 사용할 수 있습니다.

참고로, Hudson에서도 winstone을 활용하고 있습니다. hudson.war를 WAS에 설치하지 않아도 java -jar 로 바로 실행시킬 수도 있는데, 이 기능이 Hudson 패키징 파일 안에 내장된 winstone을 이용하는 방식입니다.

이 winstone을 간단하게 활용하는 방법을 정리해봤습니다.

FTP대용으로 쓰기

아래 URL에서 winstone의 jar파일이 제공됩니다.

저는 접근하기 편한 URL에 올려놓고 wget 으로 다운로드하고 있습니다.

wget benelog.net/winstone.jar

받은 파일을 java -jar 로 간단하게 실행하면 됩니다.

현재 디렉토리를 최상위 폴더로 실행하려면 아래와 같이 하면 됩니다.

java -jar winstone.jar --webroot=.

HTTP 포트를 지정하려면 --httpPort 옵션을 붙이면 됩니다.

java -jar winstone.jar --webroot=. --httpPort=18080

그런 다음 해당서버에 지정된 포트로 접근하면 디렉토리의 파일 목록이 뜹니다. FTP와는 다르게 비밀번호가 없이 접근이 되므로 보안문제가 염려된다면 외부에 공개되지 않은 포트로 띄어야 하겠습니다.

winstone

그외의 다양한 옵션은 Winstone의 사이트(http://winstone.sourceforge.net/ )에 자세히 나와 있습니다.

웹애플리케이션을 jar파일 하나로 실행되도록 패키징

앞선 hudson의 사례처럼, 별도의 WAS설치 없이 웹어플리션 배포 파일 자체에 WAS를 내장하는 방식입니다.

Jetty를 애플리케이션 안에서 직접 심어서(Embedding) 실행시켜도 같은 효과가 있습니다. 제가 간단하게 만들었던, dumper라는 도구에서도 그 방식을 활용했습니다.

Jetty를 war파일 안에 패키징해서 독립적을 실행가능한 방식도 있습니다. 아래 자료에 설명되어 있습니다.

그런데 위의 방식은 따로 실행 시작점이 되는 클래스를 만들어줘야하고, maven설정을 다소 많이 고쳐야하는 단점이 있습니다.

이에 반해서 winstone은 소스파일은 하나도 건드릴 필요없이, maven 설정도 건드리지 않거나 약간만 추가해서 혼자서 실행되는 jar파일을 만들어줍니다.

아무런 설정이 없어도 아래와 같이 실행하면 taget 폴더 아래에 .jar파일을 만들어 줍니다.

mvn net.sf.alchim:winstone-maven-plugin:embed

그리고 pom.xml에 아래 설정을 추가하면 'mvn package’로 패키징을 할 때마다 war파일과 함께 독립실행 가능한 jar파일도 만들어줍니다.

      <plugin>
        <groupId>net.sf.alchim</groupId>
        <artifactId>winstone-maven-plugin</artifactId>
        <version>1.2</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>embed</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

제가 만든 예제인 간단하게 서버에 파일을 올리는 프로그램에서도 이 방식을 썼습니다. 아래의 pom.xml에 이 설정에 포함되어 있습니다.

위 프로젝트를 받고서 mvn package를 하면 target 폴더 아래에 uploader.jar가 생기는데 java -jar uploader.jar 만 하면 웹애플리케이션이 실행되는 것이죠.

어디에 쓸까?

직접 서비스 운영과 서버관리를 담당하는 일반적인 웹애플리션에서는 Winstone을 쓸 일이 그다지 많아보이지는 않습니다. 다만 Huson처럼 패키징해서 배포하고, 여러 가지 옵션을 제공하는 패키지성 웹애플리케이션에서는 고려해볼만도 합니다. 유사하게 Lucene바탕의 API서버인 Solr(http://lucene.apache.org/solr/)에서는 Jetty를 이용한 stand-alone 실행모드를 제공합니다. 그리고 간단히 소수의 인원이 쓰는 관리도구나, Desktop application 성격의 프로그램이라도 Swing, AWT대신 Web의 UI 기술을 이용하고 싶을 때도 Winstone할 포함한 패키징을 고려해볼만도 합니다.

하지만 비슷한 용도의 Jetty와 비교해볼 때 winstone이 유망하다고 생각하지는 않습니다. 2008년도 이후에는 프로젝트 업데이트가 안 되고 있어서 앞으로의 전망이 어두워보입니다. 계속 이런 상태라면 Hudson에서도 언젠가는 Jetty로 갈아타지 않을까하는 생각도 듭니다. 별도로 클래스를 만들지 않아도 되는 장점등도 언젠가는 Jetty에서도 제공될수 있어 보입니다. 그리고 jsp를 쓰는 프로젝트에서는 따로 라이브러리를 지정해야 하는 것도 다소 번거롭습니다.

그래도 Wistone은 알아두면 서버에서 파일 주고 받을 때라도 유용하게 쓸 수도 있고, 교육 용도의 실습 프로젝트, 취미용 프로젝트에도 편하게 활용해볼만한 도구입니다.