Effective & Agile Java Generics by 정상혁

본문 내용을 인쇄하실 분은 여기를 누르시기 바랍니다.

문서정보


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

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

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

  1. @SuppressWarnings("unchecked") 를 쓰지 않고도 Generics에 대한 warning이 없고, Casting도 한번도 하지 않아야 하고
  2. 컴파일 시점에서 ListUtils.max 메소드에 Comparable 인터페이스를 구현한 객체들을 쌓은 List가 넘어온다는 것을 검증할 수 있어야 한다.


    @Test

    public void getNullIfEmptyList(){

        List<Integer> numbers = new ArrayList<Integer>();

        Integer max = ListUtils.max(numbers);

        assertThat(max,is(nullValue()));        

    }

테스트 1 :  빈 리스트가 넘어오면 null값 반환

    @Test

    public void getMaxInteger(){

        List<Integer> numbers = new ArrayList<Integer>();

        numbers.add(Integer.valueOf(1));
        numbers.add(Integer.valueOf(2));
        Integer max = ListUtils.max(numbers);

        assertThat(max,is(Integer.valueOf(2)));

    }

테스트 2 :  Integer객체의 최대값 구하기

    @Test

    public void getMaxBigInteger(){

        List<BigInteger> numbers = new ArrayList<BigInteger>();

        numbers.add(BigInteger.ZERO);

        numbers.add(BigInteger.ONE);

        BigInteger max = ListUtils.max(numbers);

        assertThat(max,is(BigInteger.ONE));

    }

테스트 3 :  BigInteger객체의 최대값 구하기

 

    @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));
    }    

테스트 4: java.sql.Date 객체의 최대값 구하기


    @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;
    }

테스트5 : ScheduledFuture를 구현한 객체의 최대값 구하기

(java.util.concurrent.ScheduledFuture  API문서 참조)



풀이와 설명

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

public static <T extends Comparable<T>> T max(List<T> list)

리스트 1: 간단한 max 메서드 선언

 

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

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

public final class Integer extends Number implements Comparable<Integer>

public class BigInteger extends Number implements Comparable<BigInteger>

리스트 2: Integer 와 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.Date는 Comparable<java.sql.Date> 한 것이 아닌 Comparable< java.util.Date> 를 구현한 것이라고 볼 수 있습니다. (두 클래스의 이름이 같아서 혼동이 되실 수도 있습니다. Java Puzzler에서는 이 두 클래스의 예를 들면서 자바 플랫폼 설계자가 이름을 지으면서 깜빡 존 듯하다고 언급하고 있습니다.)

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

public interface ScheduledFuture<V> extends Delayed, Future<V>

public interface Delayed extends Comparable<Delayed>

리스트 3: ScheduledFutureDelayed 인터페이스 선언부

 

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

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;
    }
}

리스트4: ListUtils 구현(다운로드: ListUtils.java

 

  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에 나오는 코드를 보고서 약간 변경을 해 본 것입니다. 원래 책에 나오는 코드는 아래와 같습니다.


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;
}

리스트5: Effective Java 2nd Edition에 있는 max메서드

 

  메서드 선언이 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의 메서드 시그니처는 어떻게 되어 있을까요?


static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

리스트6: java.util.Collections.max 메서드

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

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

static Comparable max(Collection c)

리스트 7: 리스트5의 메서드 선언이 자료형 지우기가 수행된 뒤의 모습

 

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

public static Object max(Collection c)

리스트8: Java5 이전의 Collections.max 메서드

 

 따 라서 리스트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배열이 들어가 있는 것으로 설명되어 있습니다.


getParametersMap.GIF

 저 는 처음에 이 문서를 안 보고 key가 String, 값이 하나일 때는 그냥 String, 값이 2개 이상이면 String배열이 들어가 있지 않을까하는 추측을 바탕으로 한 코드를 짜서 몇번 에러를 냈었었습니다. 결국 API문서를 보고서 어떤 형식으로 자료가 들어가 있는지 알게 되어있습니다. 이 메소드의 리턴타입이 Map<String,String[]>과 같이 선언되어 있었다면, 문서를 안 보고도, Runtime 에러를 안 겪고도 바로 올바른 자료형으로 사용이 가능했을 것입니다. 나중에 저는 이 메서드를 호출하는 부분을 아래와 같이 감싸는 부분을 넣었습니다.


@SuppressWarnings("unchecked")
Map<String,String[]> requestMap = request.getParameterMap();

리스트9:  javax.servlet.ServletRequest.getParameterMap() 메서드를 Generics를 이용한 코드로 감싸기

 

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

  이렇듯 Generics를 잘 활용해보면 이것이 괜히 코드를 복잡하게 만드는 것이 아니고, API설계자와 사용자들에게 많은 도움이 된다는 것을 느끼실 것입니다.


  Sun에서는 공식적으로 JDK1.4의 서비스 기간의 종료를 선언했다고 합니다. 이 시기에 현장에서 Generics를 활용할 줄 아는 Java 개발자의 수는 많이 부족해 보입니다.


관련자료 모음

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에 관한 자료들의 링크는 아래와 같습니다.


테스트 코드 작성 관련자료

혹시나 위에 쓰인 Junit4 방식의 Test annotation이나 assertThat 메소드가 익숙하시지 않으신 분들은 아래 자료를 참고하시기 바랍니다.


그 리고 Eclipse를 사용해서 Junit4를 사용할 때 가장 귀찮은 점인 Ctrl+Shift+O를 누르면 static import의 *까지도 다 펼쳐지는 현상은 아래와 자료를 참고하셔서 Eclipse 설정을 바꾸시면 좀 더 편하시게 코딩하실 수 있습니다.


Concurrent 관련자료

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


List<ScheduledFuture<?>>의 코드가 실전에서 쓰인 것이 없을까해서 찾아보니 반갑게도 Spring 포트폴리오의 일부분인 Spring integration에 있는 소스 코드에서 그런 코드가 발견되었습니다.




덧글

  • 토비 2008/12/18 15:50 # 삭제 답글

    요즘엔 블로그에서 보기 힘든 넘어가는 글이군요. @.@
  • 정상혁 2008/12/19 08:11 #

    처음에는 ScheduledFuture관련 예제만 적을려고 하다가, 그것만 달랑적으면 관심있게 볼 사람이 없을 것 같아서 줄줄이 풀어썼는데, 길어져서 사람들이 더 안 읽을 것 같네요 ^^;
  • 高原万葉 2009/01/15 15:46 # 답글

    좋은 글 감사합니다. 실례를 무릅쓰고 링크해가겠습니다.
  • 정상혁 2009/01/16 06:55 #

    아, 출처만 표기해 주시면 링크, 복사 다 환영합니다~ ^^
  • 엔디 2009/01/19 18:39 # 삭제 답글

    좋은 글 감사해요.^^;

    내공이 느껴지는 포스트네요.^^ㅎㅎ
  • 정상혁 2009/01/19 22:14 #

    내공까지는 아니고; 자세히 쓰다보니 좀 거창해 진 것 같네요 ^^;
  • 용식 2009/04/20 16:23 # 삭제 답글

    정말 좋은 글 감사드립니다.^^
    한번 봐서는 잘 이해가 안되서 몇번은 두고두고 보면서 이해하고 싶어집니다.

    감사합니다. ^^
  • 정상혁 2009/05/11 21:54 #

    더 쉽게 쓸 수 없었는지 시간이 나면 고민해봐야겠네요 ^^;
  • 박성철 2009/05/11 20:39 # 답글

    나름 Generic을 이해하고 있다고 생각했었는데 깨갱 했습니다.
    업무 포기하고 정독했습니다.
    감사합니다. ^^
  • 정상혁 2009/05/11 21:54 #

    저도 맨날 Generics를 이해했다고 생각했다가, 다시 헷갈리고를 반복하고 있습니다 ^^; 위의 글도 제가 써놓고도 다시 읽으니 머리에 확 들어오지 않네요 ;

    java의 Generics가 지금과 같은 모습이 된게 하위호환성등을 생각하면 어쩔 수 없지않나하는 생각도 들고, 이 모습보다 더 좋은 방식은 없었을까..하는 의문도 계속 생기기는 합니다.
  • artist 2009/06/25 10:01 # 삭제 답글

    좋은 글 잘 읽었습니다.
댓글 입력 영역