Effective & Agile Java Generics

수정이력
  • 2019.04.13

    • 유효하지 않은 JavaDoc 문서 링크 최신화

    • 예제에서 Diamond 문법 활용

    • JUnit4에 대한 일반적인 설명 링크 제거

    • `ServletRequest.getParameterMap()`에 대한 최신 JavaDoc 내용 추가

    • `List<ScheduledFuture<?>>`가 쓰인 예시를 Spring Integration의 소스에서 LogBack의 것으로 변경

Generics가 들어간 테스트 코드를 통과시켜 봅시다.

아래에 있는 테스트 1~5까지의 테스트 코드들을 모두 한번에 통과시키는 ListUtils.max메서드는 어떻게 선언하고 구현해야 할까요?

Generics를 써보신 분이라면 ListUtilsTest.java를 다운 받으셔서 한번 풀어 보시기 바랍니다. Collections.max()를 아시는 분도 그 메소드를 참고하시지 마시고 직접 메서드를 만들어 보시면 재미있으실 겁니다.

제약조건은 다음과 같습니다.

  • @SuppressWarnings("unchecked") 를 쓰지 않고도 Generics에 대한 warning이 없고, Casting도 한번도 하지 않아야 하고

  • 컴파일 시점에서 ListUtils.max 메소드에 Comparable 인터페이스를 구현한 객체들을 쌓은 List가 넘어온다는 것을 검증할 수 있어야 한다.

테스트 1 : 빈 리스트가 넘어오면 null값 반환
    @Test
    public void getNullIfEmptyList(){
        List<Integer> numbers = new ArrayList<>();
        Integer max = ListUtils.max(numbers);
        assertThat(max,is(nullValue()));
    }
테스트 2 : Integer객체의 최대값 구하기
    @Test
    public void getMaxInteger(){
        List<Integer> numbers = new ArrayList<>();
        numbers.add(Integer.valueOf(1))
        numbers.add(Integer.valueOf(2));
        Integer max = ListUtils.max(numbers);
        assertThat(max,is(Integer.valueOf(2)));
    }
테스트 3 : BigInteger객체의 최대값 구하기
    @Test
    public void getMaxBigInteger(){
        List<BigInteger> numbers = new ArrayList<>();
        numbers.add(BigInteger.ZERO);
        numbers.add(BigInteger.ONE);
	BigInteger max = ListUtils.max(numbers);
        assertThat(max,is(BigInteger.ONE));
    }
테스트 4: java.sql.Date 객체의 최대값 구하기
    @Test
    public void getMaxDate(){
        java.sql.Date now = new java.sql.Date(new Date().getTime());
        java.sql.Date afterAWhile = new java.sql.Date(new Date().getTime()+6000);
        List<java.sql.Date> dates = new ArrayList<java.sql.Date>();
        dates.add(now);
        dates.add(afterAWhile);
        java.sql.Date max = ListUtils.max(dates);
        assertThat(max,is(afterAWhile));
    }
테스트5 : ScheduledFuture를 구현한 객체의 최대값 구하기
    @Test
    public void getMaxScheduledFuture() throws InterruptedException{
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
        ScheduledFuture<?> after1Second = executor.schedule(getTask("first"),1,TimeUnit.SECONDS);
        ScheduledFuture<?> after2Seconds = executor.schedule(getTask("second"),2,TimeUnit.SECONDS);
        List<ScheduledFuture<?>> futures = new ArrayList<ScheduledFuture<?>>();
        futures.add(after1Second);
        futures.add(after2Seconds);
        ScheduledFuture<?> max = ListUtils.max(futures);
        long maxDelay = max.getDelay(TimeUnit.SECONDS);
        assertThat(maxDelay,is(after2Seconds.getDelay(TimeUnit.SECONDS)));
        Thread.sleep(3000);
        assertTrue(max.isDone());
    }
    private Runnable getTask(final String message) {
        Runnable task = new Runnable(){
            public void run() {
                System.out.println(message);
            }
        };
        return task;
    }

풀이와 설명

테스트 1,2,3번 까지의 코드만이라면 아래와 같이 선언하셔도 됩니다.

리스트 1: 간단한 max 메서드 선언
public static <T extends Comparable<T>> T max(List<T> list)

이렇게 <T extends Comparable<T>> 처럼 Type parameter가 그 자신이 포함된 표현으로 그 범위가 선언되는 것을 recursive type bound라고 합니다.

Integer와 BigDecimal의 클래스 선언을 보면 아래와 같습니다.

리스트 2: Integer 와 BigInteger 클래스 선언부
public final class Integer extends Number implements Comparable<Integer>

public class BigInteger extends Number implements Comparable<BigInteger>

두 클래스 모두 자신의 타입이 Parameterized type으로 들어간 Comparable 인터페이스를 구현하고 있기 때문에 리스트3의 메소드 선언으로도 Integer나 BigInteger가 담긴 리스트를 받을 수 있습니다.

그러나 리스트 1의 선언으로는 테스트4,5에 있는 메서드에서 컴파일 에러가 날 것입니다. 그 이유는 다음과 같습니다.

테스트4의 java.sql.Datejava.util.Date를 상속한 클래스입니다. 그런데 java.sql.Date 는 따로 comparesTo 메서드를 재정의하고 있지 않고, 상위클래스인 java.util.Date 에 있는 메서드를 그대로 쓰고 있습니다. java.sql.DateComparable<java.sql.Date> 한 것이 아닌 Comparable<java.util.Date> 를 구현한 것이라고 볼 수 있습니다. (두 클래스의 이름이 같아서 혼동이 되실 수도 있습니다. Java Puzzler에서는 이 두 클래스의 예를 들면서 자바 플랫폼 설계자가 이름을 지으면서 깜빡 존 듯하다고 언급하고 있습니다.)

그리고 테스트5의 java.util.concurrent.ScheduledFuture 인터페이스는 Comparable<ScheduledFuture> 를 구현한 것이 아닌, Delayed라는 인터페이스를 상속한 것이고, 이 Delayed는 Comparable<Delayed> 를 상속한 인터페이스입니다. 리스트 3의 인터페이스 선언을 보시면 쉽게 이해가 되실 것입니다.

리스트 3: Delayed 인터페이스 선언부
public interface ScheduledFuture<V> extends Delayed, Future<V>

public interface Delayed extends Comparable<Delayed>

이런 경우도 모두 통과할 수 있게 ListUtils.max() 메서드를 선언하고 구현하면 아래와 같습니다.

리스트4: ListUtils 구현
public class ListUtils {
    public static <T extends Comparable<? super T>> T max(List<T> list){
        T result = null;
        for(T each : list) {
            if (result==null) result = each;
            if(each.compareTo(result)>0) result = each;
        }
        return result;
    }
}

public static <T extends Comparable<? super T>> T max(List<T> list) 라는 긴 메서드 선언입니다. 이 선언 안에는 recursive type bound, wild card, upper bound, lower bound가 다 들어가 있습니다. 이 정도 메서드를 설계할 수 있어야지, Java generics를 제대로 아는 것이라고 할 수 있겠습니다.

bounded wild card를 적용하는 기준은 Effective Java 2nd Edition에 나와 있는 PECS(Producer-extends, Consumer-super)원칙을 기억하시면 도움이 됩니다. 원래 PECS의 뜻은 가슴 근육이라는군요.

<T extends Comparable…​.

Comparable인터페이스를 구현한 클래스가 그 대상이어야 max내부에서 Comparable.compareTo를 이용해서 최대값을 구할 수 있습니다. 그래서 타입 T는 T extends Comparable이 되어야 합니다. PECS원칙으로도 리턴값으로 생산되는 (Producer) 타입이 T이므로 extends를 쉽게 연상하실 수 있습니다.

Comparable<? super T> 부분

max 메서드 내부에서 타입 T는 Comparable.compareTo(T o)메서드 뒤에 파라미터로 넘어가는, 소비되는(Consumer) 대상으로 쓰이기에 PECS원칙으로 super로 연결시킬 수 있습니다. 테스트5의 코드를 예로 보면, ScheduledFuture는 ScheduledFuture의 상위 인터페이스인 Delayed가 Comparable의 Parameterized type으로 넘어가는 Comparable<Delayed>형태의 Comparable인터페이스를 상속하고 있습니다. T를 ScheduledFuture로 봤을 때 Comparable<? super T>는 Comparable<Delayed>와 잘 맞아떨어집니다.

이 리스트4의 ListUtils.max 메서드는 Effective Java 2nd Edition의 Item28에 나오는 코드를 보고서 약간 변경을 해 본 것입니다. 원래 책에 나오는 코드는 아래와 같습니다.

리스트5: Effective Java 2nd Edition에 있는 max메서드
public static <T extends Comparable<? super T>> T max(List<? extends T> list){
  Iterator<? extends T> i = list.iterator();
  T result = i.next();
  while(i.hasNext()){
            T t = i.next();
            if (t.compareTo(result)>0) result = t;
   }
   return result;
}

메서드 선언이 public static <T extends Comparable<? super T>> T max(List<? extends T> list) 로 예제보다 더 늘어난 부분이 있습니다. 끝에 있는 List<? extends T> 가 추가된 것입니다. 이 부분은 PECS원칙에 따르면 List객체로부터 T를 생산(Producer)해 오기 때문에 ? extends T 로 하는 것이 적절해 보이는 합니다. 그러나 테스트1~5의 코드에서는 List<T> 만으로도 컴파일러가 수행하는 형추론(type inference)에 문제가 없었기에 제가 만든 코드인 리스트4에는 추가하지는 않았습니다. 컴파일러가 수행하는 Type inference는 굉장히 복잡하고, Java Language Spec에서 16페이지나 차지한다고 합니다.

그리고 리스트5에서는 길이가 0인 List가 넘어간 값일 때는 첫번째 i.next();에서 NoSuchElementException을 내게 되어 있습니다.

java.util.Collections.max 메서드에서도 같은 결과가 나오는 것으로 보아서, 유사한 구현방식이 쓰인 것으로 추측됩니다. 제가 만든 문제에서는 Collection.max와 약간 다른 부분을 만들어 보고 싶어서 길이가 0일 list가 올 때는 null을 반환하는 방식으로 바꾸어 보았습니다.

그렇다면 `java.util.Collections.max`의 메서드 시그니처는 어떻게 되어 있을까요?

리스트6: java.util.Collections.max 메서드
static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

일단 대상이 List보다 확장된 Collection이니 Parameter가 Collection인 것이 눈에 들어 옵니다. 그런데 T의 제약조건이 <T extends Object & Comparable<? super T>>로 선언되어 있는 것이 리스트5의 코드보다 'Object &' 부분이 더 들어가 있습니다.

이것은 java1.4와의 하위 호환성을 위한 것입니다. Java에서는 하위호환성 지원 때문에 컴파일 시에 Generics관련 정보를 모두 검사한 후에는 실제로는 Generics 정보가 전혀 없는 바이트코드를 생성하게 되어 있습니다. 그래서 리스트5처럼 메서드를 선언했을 때에는 런타임시에는 리스트 7과 같은 코드와 같은 바이트코드가 생성됩니다.

리스트 7: 리스트5의 메서드 선언이 자료형 지우기가 수행된 뒤의 모습
static Comparable max(Collection c)

그러나 이전 버전에서의 max메서드의 모습은 다음과 같았습니다.

리스트8: Java5 이전의 Collections.max 메서드
public static Object max(Collection c)

따라서 리스트7처럼 Comparable을 반환하게 된다면 이것은 이전버전의 메서드 Signature를 바꿔버린 것이 되므로 하위버전에서 컴파일된 코드에서 Collections.max를 호출할 때 에러를 발생시키게 됩니다. 그래서 Object &이 더 추가된 것이죠. (Agile Java의 Lesson 12 중 Additional Bounds에서 참조)

여기까지 이해하셨으면, 실무에서 어떤 Generics 관련 코드를 봐도 이제 쉬워보이실 겁니다.

Generics의 표현력

하나의 예제로 Generics의 많은 부분을 설명하기 위해서 다소 복잡한 코드를 보여드렸습니다. 혹시나 Generics를 이제 막 적용하시고 싶으신 분들의 마음을 어둡게 한 것이 아닌가 걱정이 되기도합니다. 그러나 대부분의 Generics적용 사례는 훨씬 간단하고, 특히 Collection 선언에 genercics를 활용하는 정도는 어렵지 않습니다.

Generics는 컴파일시점에서의 에러 검출 영역을 넓혀줘서 보다 이른 시점에 버그를 잡을 수 있게 해주고, 코드I의 설명력을 높여줘서 API사용자들이 보다 쉽게 API를 쓸 수 있게 해줍니다. 컴파일타입의 에러체크 능력은 위의 예제를 통해서 설명했으니, 표현력에 대해서도 제가 겪은 사례를 이야기해 볼까 합니다.

오래 전에 저는 Java 인터페이스를 엑셀파일로 만드는 산출물 작업을 했었는데, 리턴타입이 List인 메서드들은 그 안에 어떤 객체들이 들어있는지 메서드 시그니처만으로는 표현할 수 없어서 답답했던 적이 있었습니다. 그래서 아예 List대신 배열을 쓸까도 고민하다가 List가 가진 편의성들을 버릴 수가 없어서 List를 쓰고 따로 문서에 그 안에 어떤 객체가 들어가 있는지를 적을 수 밖에 없었습니다.

javax.servlet.ServletRequest.getParameterMap()를 사용할 때는 API사용자로서 아쉬움을 느꼈었습니다. API문서를 보면 이 메서드가 반환하는 Map에는 key로 String이, value로 String배열이 들어가 있는 것으로 설명되어 있습니다.

request getParametersMap

저는 처음에 이 문서를 안 보고 key가 String, 값이 하나일 때는 그냥 String, 값이 2개 이상이면 String배열이 들어가 있지 않을까하는 추측을 바탕으로 한 코드를 짜서 몇번 에러를 냈었었습니다. 결국 API문서를 보고서 어떤 형식으로 자료가 들어가 있는지 알게 되어있습니다. 이 메소드의 리턴타입이 Map<String,String[]> 과 같이 선언되어 있었다면, 문서를 안 보고도, Runtime 에러를 안 겪고도 바로 올바른 자료형으로 사용이 가능했을 것입니다. 이렇듯 Generics를 잘 활용할 수 없는 API를 쓸 때에는 소비자 입장에서 불편합니다.

나중에 저는 이 메서드를 호출하는 부분을 아래와 같이 감싸는 부분을 넣었습니다.

리스트9: javax.servlet.ServletRequest.getParameterMap() 메서드를 Generics를 이용한 코드로 감싸기
@SuppressWarnings("unchecked")
Map<String,String[]> requestMap = request.getParameterMap();

@SuppressWarnings("unchecked") 은 어쩔 수 없는 경우에만 써야 하고, 쓸 때도 클래스 단위, 메서드 단위가 아닌 이런 최소 라인 단위로 써야 합니다. Effective Java 2nd Edition Item 24참조) 이 경우는 Generics지원하지 않는 외부 인터페이스를 호출하는 것이라서 불가피한 경우입니다. 형에 대해서는 API 문서에 명시된 내용라서 이렇게 @SuppressWarnings을 선언해도 문제가 없습니다. 필요에 따라서 이 requestMap을 리턴해 준다면 그것을 쓰는 코드에서는 더 이상 이 안에 무엇이 들어있는지 문서를 찾아보지 않아도 됩니다.

JavaEE6 버전의 JavaDoc을 보니 이제는 request.getParamersMap`의 반환형이 `Map<String,String[]> 으로 바뀌어 있음을 확인할 수 있습니다.

request getParametersMap ee6

관련자료 모음

Generics 관련 자료

이 포스트는 주로 아래 두 책을 보면서 얻은 정보를 통해 작성되었습니다.

Agile java처럼 테스트 코드를 먼저 보여주었고 , ListUtils.max 메서드는 Effective Java의 내용을 주로 참조해서 작성했습니다. Effective Java에서는 ScheduledFuture의 경우에 대해서 언급만 되어 있고 예제코드가 없는 것이 아쉬워서 테스트5의 코드를 작성했고, 비슷한 사례의 보다 친숙한 클래스를 찾다가 Agile java에서 java.sql.Date 클래스가 예제에 많이 쓰인 것을 보고 테스트4를 추가했습니다.

Effective Java 2nd Edition 에 포함된 내용 중 Java5 관련 내용은 Joshua Bloch이 했던 발표에 잘 요약되어 있습니다.

그외 Generics에 관한 자료들의 링크는 아래와 같습니다.

Concurrent 관련자료

Effective Java에서 언급한 ScheduledFuture를 이용한 예제코드를 만들다 보니 Concurrent관련 API들이 몇개 포함되었습니다. 그 API들에 관심이 있으신 분은 아래 자료를 참조하시면 됩니다.

List<ScheduledFuture<?>> 의 코드가 실전에서 쓰인 것이 없을까해서 찾아보니 로깅 프레임워크인 Logback의 소스 코드에서 그런 코드가 발견되었습니다.

스프링 프레임웍의 최근 소식들 모음 등

역시 최근 들어 스프링소스의 발걸음이 더욱 빨라졌습니다. Grails, Spring Python, tc Server, 3.0M1 발표.. 현란하게 소식이 쏟아지는군요. 그만큼 공부할 것도 더 늘겠지만, 그래도 반갑고 기대감을 가지게 하는 소식들입니다.

대규모 투자를 받고, 일을 더 벌이고 하면서 스프링소스가 돈맛에 초심을 잃을까봐 걱정을 하시는 분도 많이 계시는 듯 합니다. 그래도 성공적인 사업 구조가 정착되어서 그 수익이 개발자들에게 도움이 되는 제품들을 더 잘 개발하는데 쓰인다면야 굳이 돈을 많이 벌게 되는 것이 나쁘다고는 생각하지 않습니다. 유료로 제공되는 제품이 많아진다고 해도, 그럴만한 가치가 있는 제품이면 돈을 내고, 그것이 아니라도 프레임웍이나 서버, 개발도구의 시장은 독점 시장이 아니니 다른 선택을 할 수 있는 대안이 많을 것입니다. 물론 지금까지의 스프링의 행보를 봐서는 충분히 앞으로도 가치있는 결과물을 만들 것이라고 기대하고 있습니다.

여러 소식들 중에 솔직히 가장 재미있었던 것은, Spring one America 2008에 참석하신 분들을 통해 알게된, 로드존슨의 나이였습니다. 스프링 핵심 개발자인 유겐할러의 말에 의하면 로드존슨은 38살 정도라고 하네요.

왜 저는 로드존슨을 당연히 40대라고 생각했을까.. 하고 분석해보니, Expert one-on-one J2EE Design and Developement 책 때문에 그런 선입관을 가지게 된 것 같습니다.

이 책이 나온 것이 2003년인데, 그렇다면 이 때는 33살이였단 말이되는군요. 이 책을 쓰는데도 2년 정도가 걸렸다고 들은 것 같습니다. 그리고 책 표지에 보면 로드존슨은 음악학(Musicology)에 박사학위가 있다는 말도 있습니다. 그래서, 다른 분야도 박사까지 하고 컴퓨터 분야에서도 이정도 경지까지 가려면 적어도 이 책 출판시에 30대 중후반은 되지 않았을까.. 하는 추측을 한 것이죠.

FindBugs + Eclipse + Maven2 + Hudson

FindBugs를 이용한 코드검사를 Maven2을 통해 실행하고, Hudson을 통해 확인하는 설정을 정리해 봅니다.

Eclipse에서 findbugs로 코드검사를 해볼 수 있는 툴은 http://findbugs.cs.umd.edu/eclipse/ 를 update site로 지정하면 설치할 수 있습니다. 설치가 잘 되었다면 소스 폴더를 선택하고 마우스 우클릭을 하면 'Find Bugs’라는 메뉴가 생긴 것이 보일 것입니다. 그 메뉴를 통해 원하는 프로젝트를 검사하고, Bug Explorer 탭을 선택하보면 아래와 같은 화면이 나옵니다.

eclipse-findbugs.jpg

Bug Explorer 탭에서는 버그를 유형별로 정리해서 보여주고, 소스탭에서는 해당하는 코드에 벌레 모양 아이콘을 찍어줍니다. 그리고 Problems 창에서는 Eclipse에서 잡아내는 다른 경고처럼 warning으로 해당 소스를 표시해 줍니다. Bug Details 탭을 누르면 버그에 대한 자세한 설명도 볼 수 있습니다.

프로젝트의 Properties 메뉴에서 FindBugs 설정란으로 가면 검사할 규칙 등을 선택할 수 있습니다.

eclipse-findbugs-config1.jpg

이 설정화면에서 'Run FindBugs automatically’를 선택하면, 소스가 바뀔 때마다 자동으로 검사를 수행해 줍니다. 이 기능이 선택되어 있지 않다면, 지적된 소스를 수정해도 다시 수동으로 검사를 돌려야지 경고메시지 지워지므로, 이클립스가 아주 느리다는 느낌이 안 들정도라면 선택하는 것이 좋습니다. 이 기능을 선택해서, Eclipse의 Problems 탭에서 코드 작성 즉시 에러와 경로를 확인할 수 있게 되었다면 .project 파일에 아래와 같은 부분이 추가되어 있을 것입니다.

<natures>

    ....

  <nature>edu.umd.cs.findbugs.plugin.eclipse.findbugsNature</nature>

</natures>

FindBugs 설정 화면 중 Detector Configuration 탭에서는 검사할 규칙들을 지정할 수 있고, Reporter configuration 탭에서는 보고해 줄 버그의 경고단계와 분류를 선택할 수 있습니다.

eclipse-findbugs-config2.jpg

Filter files 탭에서는 별도의 XML파일로 선언된 포함하거나 제외시킬 버그와 파일에 대한 설정을 가지고 올 수 있습니다.

eclipse-findbugs-config3.jpg

아래에 자세히 설명하겠지만, Maven 설정에서 참조하는 findBugsExclude.xml을 Eclipse plugin에서도 똑같이 지정해서 Maven과 Eclipse에서 같은 기준으로 검사가 수행되도록 했습니다.

Maven2의 findbugs-maven-plugin은 pom.xml에 아래와 같이 설정됩니다.

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>findbugs-maven-plugin</artifactId>
  <version>2.4.0</version>
  <configuration>
    <findbugsXmlOutput>true</findbugsXmlOutput>
    <findbugsXmlWithMessages>true</findbugsXmlWithMessages>
    <xmlOutput>true</xmlOutput>
    <excludeFilterFile>$\{basedir}/findBugsExclude.xml</excludeFilterFile>
   </configuration>
</plugin>

위의 설정에 들어가는 속성들에 대해서는 findbugs-maven-plugin 설명 페이지에서 자세한 내용을 보실 수 있습니다.

저는 제외할 검사규칙을 지정하기 위해서 excludeFilterFile속성에 findBugsExclude.xml을 지정했습니다.

findBugsExclude.xml의 내용은 아래와 같이 설정했습니다.

....
<FindBugsFilter>
....
....
  <Match>
....
....
   <Bug code="Se,SnVI,Dm,UwF,EI,EI2" />
....
....
  </Match>
....
....
</FindBugsFilter>
....

제외할 것을 선언하는 파일에 이렇게 적었으니 Bug code가 "Se,SnVI,Dm"에 해당하는 버그검사는 제외한다는 의미입니다. Filter의 설정 방법에 대해서는 findbugs의 매뉴얼을 참조하시면 됩니다.

버그 코드 중 Se,SnVI는 serialVersionUID에 관한 것이고 Dm은 String.toUpperCase 등의 메소드에서 Local설정을 권유하는 검사입니다. (버그 코드에 대한 설명 페이지 참조)

이렇게 설정을 하고 mvn findbugs:findbugs 로 maven을 실행시키면 필요한 라이브러리들을 다운로드 받고 빌드가 실행됩니다. 실행이 성공했다면 목적지 폴더에 findbugs.xml과 findbugsXml.xml파일이 생성이 되었을 것입니다.

이것을 Hudson을 통해서 보기 위해서는 Hudson에서도 findbugs plugin을 설치해야 됩니다.

Hudson 첫 화면에서 Manage Hudson - Manage Plugins 메뉴를 찾아갑니다. Available 탭에서 findbugs를 선택하고 화면 우측하단의 'install’버튼을 누르면 Hudson이 알아서 라이브러리를 다운 받아줍니다. 설치한 plug-in이 실행되기 위해서는 Hudson을 재시작해야 합니다.

그런 다음에 findbugs를 적용하고자 하는 프로젝트에 가서 Configure메뉴를 선택하면 아래와 같이 Publish FindBugs Analysis Result라는 부분이 추가된 것을 보실 수 있을 것입니다.

hudson-findbugs-config.jpg

이것을 선택하고 원하는 기준값이 있을 경우 입력한 뒤에 "save’버튼을 누르고 build를 해보면 됩니다. 물론 build에는 findbugs:findbugs goal이 포함되어야 하겠죠.

빌드가 성공하는 것을 보고 프로젝트의 메뉴를 보면 FindBugs Warnings라는 메뉴가 추가된 것을 확인하실 수 있습니다.

hudson-findbugs-menu.jpg

그 메뉴를 누르면 생성된 보고서가 보입니다.

hudson-findbugs-report.jpg

warning이 존재할 경우 건수를 클릭하면 해당하는 클래스들이 나오고, 클래스를 선택하면 소스에서 warning을 발생시키는 부분까지 보여줍니다.

만약 hudson의 findbugs plugin을 실행할 때 Cannot find setter nor field in org.apache.maven.plugins.site.SiteMojo for 'xmlOutput' 와 같은 에러가 난다면 Hudson plugin 수동으로 빌드&업로드를 참조해서 최신 버전으로 플러그인을 업데이트 해보시기 바랍니다.