스레드 안정성의 문서화와 JCIP 애노테이션

스레드 안전성을 어떻게 문서에 표기할까?

Javadoc에서는 메소드 선언에 synchronized가 있는지 표시하지 않습니다. 그 이유는 동기화 여부는 세부 구현 방식이기 때문에 문서에까지 그것을 노출하지 않는 것은 적절하지 않기 때문이라고 합니다.

선언부에 synchronized가 있는 메소드도 메소드 안의 모든 블럭이 synchronized(this)와 감싸진 코드와 의미입니다. 즉 아래의 코드는

synchronized void run() {
//do something
}

다음과 똑같은 역할을 하는 코드입니다.

void run(){
  synchronized(this) {
  // do something
  }
}

구현방식을 개선하면서 동기화 구간을 메소드의 일부 구간으로 좁힐 수도 있고, this 객체 말고 다른 객체로 lock을 바꿀 수도 있습니다. 이런 세부구현 상황은 외부 인터페이스보다 더 쉽게 바뀔 여지가 있습니다. 그리고 lock으로 보호되는 모든 구간을 javadoc의 메소드 선언부에 표시하기도 어렵고, this로 전체 메소드를 lock으로 잡는 메소드인 synchronized 메소드 여부를 표시하는 것만으로 충분히 문서화가 되었다고 생각하기도 어렵습니다.내부적으로 더 효율적인 방식으로 동기화가 되어 있을 수도 있으니,Javadoc에서 synchronized가 있느냐 없느냐가 스레드 안정성을 판단할 수 있는 일관적인 기준도 되지 못합니다.

따라서 현재 javadoc의 정책은 적절하다고 생각됩니다. Effective Java 2nd Edition의 Item 70에서도 그렇게 언급되었습니다.

결국 현재 javadoc의 규약으로는 어떤 메소드가 멀티스레드에서의 안전한지 여부를 표시해야 하는 강제성은 없다는 이야기입니다.

Effective Java 2nd Edition에서는 스레드 안정성을 다음 4가지 분류로 구분해서 문서화하라고 권장합니다.

  • Immutable

    • 상태가 없습니다. 당연히 Thread-safe합니다.

    • 예) String, Long, Integer, BigDecimal

  • 무조건 Thread-safe

    • 상태가 있으나 내부적으로 알아서 동기화되어 있습니다.

    • 예) ConcurrentHashMap

  • 조건부 Thread-safe

    • 어떤 메소드는 외부에서 sync가 필요한 클래스입니다.

    • 예) Collections.synchronizedList로 받은 List의 iterator는 ConcurrentModificationException을 발생시킬수 있습니다.

  • Not Thread-safe

    • 외부에서 synchronized를 해야 함

    • 예) HashMap, ArrayList

  • Thread-hostile

    • 외부에서 synchronized를 해도 멀티쓰레드에서는 못 쓰는 클래스입니다. 다행히 Java에는 거의 없습니다.

    • 예) System.runFinalizersOnExit

저렇게 잘 구분해서 Javadoc 문서의 제일 앞에 설명을 해준다면 좋겠지만, 강제적인 표준이 없다보니 기존의 문서들은 클래스 설명의 중간에서 스레드 안정성을 설명합니다.

몇가지 클래스의 사례

그렇다면 현재 Java 클래스가 스레드 안정성을 문서화하고 있는지 몇가지 예를 살펴보겠습니다.

java.util.LinkedList

LinkedList의 javadoc에서는 클래스 설명의 중간 쯤에 볼드체로 강조해서 이 구현은 'not synchronized’되었다고 적혀 있습니다.

LinkedList

java.text.SimpleDateFormat

SimpleDateFormat의 javadoc에서는클래스 설명의 마지막 즈음에 synchronization이라는 제목의 절에서 'not synchronized’라고 설명합니다.

SimpleDateFormat

org.springframework.batch.item.file.FlatFileItemWriter

스프링배치의 클래스인 FlatFileItemWriter는 not의 앞 뒤로 별표를 붙여서 'not thread-safe’라고 적어놨습니다.

FlatFileItemWriter

이렇게 클래스가 스레드 안정성을 표시하는 방법은 제 각각이고, 나름대로는 강조하고 있지만 주의깊게 API 문서를 주의 깊게 본 사람이 아니라면 지나치기 쉽습니다. 차라리 '스레드 안정성은 클래스별 설명 제일 윗줄에 넣도록 하고, 반드시 빨간색 Bold로 표시한다' 같은 규칙이 있었으면 얼마나 좋을까하는 생각까지도 듭니다.

JCIP annotation

JCIP annotation은 'Java concurrent in practice' 책에서 제안된 스레드 안정성을 표시하는 기법입니다.아래 4개의 종류의 애노테이션을 제공합니다.

  • @GuardedBy : 해당 객체가 어떤 Lock으로 보호되고 있는지 표시. 필드에 메소드에 사용 가능

  • @Immutable : 불변객체

  • @NotThreadSafe : 스레드 안전하지 않음

  • @ThreadSafe : 스레드 안전함

Apache Httpclient에서 활용 사례

실제 이 JCIP 애노테이션이 활용된 사례는 Apache Httpclient가 있습니다. 이 라이브러리의 클래스인 HttpGet, DefaultHttpClient, SingleClientConnManager는 아래와 같이 @NotThreadSafe, @ThreadSafe를 클래스 선언에 붙었습니다.

@NotThreadSafe
public class HttpGet extends HttpRequestBase {

@ThreadSafe
public class DefaultHttpClient extends AbstractHttpClient {

@ThreadSafe
public class SingleClientConnManager implements ClientConnectionManager {

이 애노테이션들은 원래의 JCIP 애노테이션의 패키지인 net.jcip.annotations 대신에 org.apache.http.annotation 패키지에 들어가 있기는 합니다.그런데 이 패키지에 있는 @ThreadSafe등의 javadoc를 봐도이 애노테이션들은 JCIP책에서 유래하였다고 설명합니다.

이 애노테이션이 붙어 있는 클래스의 avadoc에서는 클래스 설명 단락의 위 쪽에 이 애노테이션을 보여줍니다.

DefaultHttpClient

일관성 있는 위치에 표시되기 때문에 한 눈에 스레드 안정성 여부를 인식할 수 있습니다.

FindBugs에서의 지원

오픈소스 정적분석 도구인 Findbugs에서는 JCIP 애노테이션 중 @Immutable을 인식합니다. 버전 2.0부터 JCIP라는 버그 패턴으로 등록이 되어 있습니다. 아래 페이지에서 확인할 수 있습니다.

Findbugs Eclipse plugin를 설치하면 보다 편하게 FindBugs가 주는 경고를 확인할 수 있습니다..

만약 아래와 같이 @Immutable로 선언된 클래스에 final이 아닌 필드가 있다면

import net.jcip.annotations.Immutable;

@Immutable
public class Memo {
  private String content;
  public void setContent(String content) {
   this.content = content;
  }
  public String getContent() {
   return content;
  }
}

Eclipse에서 경고를 보여줍니다.

findbugs

그러나 FindBugs에서는 @Immutable외의 애너테이션은 특별히 확인하지는 않습니다.즉 @ThreadSafe 로 선언된 객체에서 @NotThreadSafe 로 선언된 멤버변수를 접근하더라구도 아무런 경고를 보내지는 않습니다. JCIP 애노테이션과 Findbugs를 동시에 쓰면서 많은 기대를 하지는 말아야 합니다.

Findbugs의 자세한 설명방법은 아래 포스트를 참고하시기 바랍니다.

정리

Java에서 어떤 클래스가 멀티스레드에서 의도하지 않게 사용될 때 그 부작용은 심각하지만, 문제가 되는 지점을 추적하기는 어렵습니다. 그렇게 때문에 스레드 안정성은 반드시 엄격하게 문서에 언급 되어야 합니다.그러나 기존의 클래스를 보면 제 각각의 표현 방식으로, 때로는 눈에 잘 띄지 않게 문서에 적혀 있습니다.

JCIP 애노테이션은 일관된 방식으로 스레드 안정성을 문서화하는데 도움이 됩니다. 이 애노테이션은 Apache httpclient 프로젝트에 적용되었고, 강력하지는 않지만 findbugs에서 JCIP annotation이 걸린 클래스를 정적 분석을 해주기도 합니다.

대부분의 웹프로젝트에서는 비지니스 레이어를 상태가 없는 클래스로 멀티스레드에서도 안전하게 만드는 방식이 권장됩니다. 그러나 때로는 스레드 안전하지 않은 클래스를 만들게 될 수도 있습니다. 예를 들면 멀티스레드에서 공유되면 안 되는 외부 라이브러리의 클래스를 사용하는데, 그 클래스를 생성자로 받아서 멤버변수에 할당해야 하는 경우입니다. 또, 배치나 데몬서버 등을 만드는 프로젝트에서는 스레드 안전한 클래스와 그렇지 않은 클래스가 혼재하게 될 때도 많습니다. 그런 클래스들을 명확하게 구분하고 싶다면 JCIP 애노테이션으로 표시하는 것을 고려해볼만 합니다.

참고자료

  • [EJ2] : Effective Java 2nd Edition - Item 70

  • [JCIP] : Java concurrency in practice

초간단한 FTP server, One-FTPServer

배경

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

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

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

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

그래서 Apache FtpServe의 모듈을 이용한 간단한 프로그램을 직접 만들어서 Github에 올려 보았습니다.'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옵션을 테스트 할 때 편리했습니다.

최상위권 n등의 점수 구하기

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을 참고하면 이 상황에서도 더 좋은 방식이 있을 듯합니다.