정상혁정상혁

배경

서버 작업을 하다보면, PC에 있는 파일을 서버에 올리거나, 서버에 있는 로그파일을 PC로 받고 싶을 때가 있습니다.그런 용도로 저는 다운로드를 할 때는 Winstone을, 업로드를 할 때는 간단한 웹애플리케이션으로 만든 Uploader 를 사용합니다.

제 블로그의 아래 포스트를 통해서 그 방법을 설명했었습니다.

또는 파이썬이 설치되어 있다면 'python -m SimpleHTTPServer’를 실행하면 간단한 웹서버를 띄울 수도 있습니다.

그런데, 여러 개의 파일을 올리거나 받을 때는 아무래도 FTP가 더 편하기도 합니다.그럴 때는 저는 Apache FtpServer를 사용합니다.Apache FtpServer가 설정이 간단한 편이지만, 기본 패키지를 wget으로 다운로드 받고, 압축을 풀고, 설정 파일을 편집하는 과정을 여러 번 하다보면 좀 번거롭기도 합니다.

그래서 Apache FtpServe의 모듈을 이용한 간단한 프로그램을 직접 만들어서 Github에 올려 보았습니다.'https://github.com/benelog/one-ftpserver[One-FTPServer]'라고 이름을 붙여봤습니다.

https://gist.github.com/2711965#%EC%82%AC%EC%9A%A9%EB%B2%95사용법

간단하게 아래와 같이 다운로드를 받고

wget benelog.net/one-ftpserver.jar

Jar파일을 직접 실행하면 끝입니다.

java -jar one-ftpserver.jar
[source]

아무런 옵션이 없으면 익명 사용자(anonymous) 로그인을 통해 2121번 포트로, FTP서버가 실행 중인 디렉토리를 홈디렉토리로 접속할 수 있습니다.

포트, 아이디, 비밀번호 등을 명령행에서 바로 지정할 수 있습니다.

java -jar one-ftpserver.jar port=10021 id=benelog password=1234

그외 passive port, SSL적용여부 등 총 6가지 설정변수를 지원합니다.

java -jar one-ftpserver.jar port=10021 passivePorts=10125-10199 ssl=true id=benelog password=1234 home=/home/benelog/programs

자세한 파라미터의 설명은 https://github.com/benelog/one-ftpserver에 나와 있습니다. 혹시나 외국사람들도 쓸 일이 생길가봐 잘 되지 않은 어색한 영어로 적는다고 힘들었습니다;

다양한 FTP클라언트를 쓸 수 있지만,저는 wget이나 curl을 즐겨 사용합니다. 만약 benelog.net이라는 서버에 10021포트로 id가 benelog, 비밀번호 1234로 서버를 띄었다면, 아래와 같이 접근합니다.

재미있었던 코드

사용자 인증 정책 구현 클래스

Apache FtpServer는 UserManager라는 인터페이스로 사용자 인증 정책을 구현하도록 설계되어 있습니다. 기본 구현클래스로 Property파일이나 데이터베이스에서 사용자 정보를 읽어오는 클래스가 제공됩니다.

이 프로그램에서는 UserManager를 구현한 클래스에서 단순히 사용자 한명의 아이디, 비밀번호 값만을 가지고 인증을 하는 SingleUserManager라는 클래스를 만들었습니다.

혹시나 다른 인증정책이 필요한 FTP서버를 만들게되더라도, 이 클래스만 간단하게 구현해주면 됩니다.

Apache Commons Net의 FTPClient모듈을 이용한 통합 테스트

특별히 어렵지 않는 프로그램이지만 여러 조합의 옵션이 다 의도대로 동작하는지 확인하는데에는 반복적인 테스트가 필요할 것 같습니다.그래서 Apache Commons Net에서 제공하는 FTP클라언트 모듈로 통합 테스트를 만들었습니다.

아래와 같이 Console의 명령행에서 넘어가는 것과 똑같은 옵션을 넣어서 서버를 실행시킨 후, FTPClient로 접속했습니다.

@Test
public void loginFail() throws Exception {
  startServer(new String[]{"port=3131","ssl=true","id=benelog","password=1234", "home=" + home});
  client.connect("127.0.0.1", 3131);    // login
  boolean authorized = client.login("benelog", "13234");
  assertFalse(authorized);
}

이 통합테스트 덕분에 처음에 만들 때 디버깅 시간도 줄였고, 나중에 발견된 버그도, 버그를 드러내는 통합테스트를 먼저 추가해서 실패한 테스트를 만든 후에 그 것을 통과시키도록 수정해서 해결했습니다.

FTPClient모듈의 사용법도 덤으로 익혔습니다. Apache Commons Net에서는 FTP와 FTPS클라이언트가 동일한 인터페이스가 제공되어서 ssl옵션을 테스트 할 때 편리했습니다.

정상혁정상혁

Effective & Agile Java Generics 글에 이은, Generics 활용 사례입니다.

배치 프로그램을 짜다가 수십만개의 점수 중에 상위 0.1%, 0.01%에 해당하는 값이 무엇인지 구해야하는 일이 생겼습니다. 기존의 프로그램은 Linux shell로 되어 있었는데, Linux의 sort를 이용해서 전체 정렬을 한 후에 찾고자하는 등수가 있는 줄을 찾아가는 방식이였습니다. 전체 건수에 비해서 구하고자 하는 등수가 상위 0.1%, 0.01% 등 아주 작은 비율임에도 불구하고, 전체 Sort를 하는 것은 비효율적이라는 생각이 들었습니다.

그래서 이 프로그램의 일부를 Java로 바꾸는 작업을 하다가 이 순위를 구하는 부분도 필요한만큼만 정렬을 하도록 바꾸고 싶었습니다. 저는 OS를 우분투를 쓰기 떄문에 상관없지만, Java로 만든 프로그램에서 중간에 Shell을 호출하는 부분이 남아있으면 다른 개발자들은 Local PC에서는 확인할 수 없는 부분이 생겨서 불편해질까봐 걱정되었던 이유도 있었습니다.

등수를 구하는 대상 데이터형은 Integer와 Float이였지만, Integer를 위한 클래스 따로 만들고 Float를 위한 것을 따로 만들기 싫어서 java.lang.Comparable를 구현한 클래스명을 다 사용할 수 있게 했습니다. 이정도 유연성을 두는게 추가 개발은 거의 없이 그냥 Generics 선언을 잘하면 되는 일이였으므로 그다지 과도한 확장은 아니였다고 생각했습니다. 덕분에 String형의 문자열도 비교할 수 있습니다.

즉, 문제를 다시 정리하면 아래와 같습니다.

  1. 데이터는 점수값만이 1차원으로 나열된다. (예: 1,5,3,10 …​. )

  2. java.lang.Comparable형의 클래스를 모두 계산 대상으로 받을수 있고, 명시적인 캐스팅이 필요없게 사용할 수 있어야한다.

  3. 전체 데이터 중에 지정한 등수를 구한다. 전체 데이터는 파일에 있고, 수십만건이라서 메모리에 모두 올릴 수 없으나, 지정한 등수까지는 메모리에 올릴 수 있을 정도로 최상위권의 값이 지정된다. 즉 예를 들면 80만건중 100등의 점수가 몇점인지를 구하는 상황이다

Integer, Float, String형을 대상으로 작성한 테스트 코드는 아래와 같습니다.

public void get3rdRankOf5() {
    //given
    List<Integer> rawData = asList(400,400,100,500,200);
    int rankToFind = 3;

    //when
    RankFinder<Integer> rankFinder = new RankFinder<Integer>(rankToFind);
    addAll(rawData, rankFinder);
    Integer rankedValue = rankFinder.getRankedValue();

    //then
    assertThat(rankedValue, is(400));
    assertThat(rankFinder.getTopValues(), is(asList(400,400,500)));
}

@Test
public void get2ndRankOf5WithFloat() {
    //given
    List<Float> rawData = asList(10.9F, 10.2F, 10.3F, 10.4F, 10.5F);
    int rankToFind = 2;

    //when
    RankFinder<Float> rankFinder = new RankFinder<Float>(rankToFind);
    addAll(rawData, rankFinder);
    Float rankedValue = rankFinder.getRankedValue();

    //then
    assertThat(rankedValue, is(10.5F));
    assertThat(rankFinder.getTopValues(), is(asList(10.5F,10.9F)));
}

@Test
public void get4thOfWord() {
    //given
    List<String> rawData = asList("e","fc","a","b","k");
    int rankToFind = 2;

    //when
    RankFinder<String> rankFinder = new RankFinder<String>(rankToFind);
    addAll(rawData, rankFinder);
    String rankedValue = rankFinder.getRankedValue();

    //then
    assertThat(rankedValue, is("fc"));
}

private <T> void addAll(List<T> rawData, RankFinder<? super T> rankFinder) {
    for (T num : rawData) {
        rankFinder.addTargetValue(num);
    }
}

이를 통과시키는 실행 코드와 테스트 코드 전체는 GIST에 올렸습니다.

더 효율적으로 개선할 여지가 많은 방식이지만, 1시간이 걸리는 전체 배치 중 1초 미만의 구간이였기에 속도향상이 큰 의미가 없어서 더 이상 리팩토링을 진행하지는 않았습니다. 알려진 selection alorithm을 참고하면 이 상황에서도 더 좋은 방식이 있을 듯합니다.

정상혁정상혁

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했을 것 같습니다. 문자열을 직접 검증하면 메시지의 형식이 바뀔 때마다 테스트가 깨어져서 유지보수가 번거로운 테스트 코드가 됩니다. 이 예제에서는 코드를 짧게 하고 테스트 코드 안에서 검증하고자 하는 동작을 단순하게 나타내려고 이 방식을 택했습니다.