본문 바로가기

개발/이펙티브 자바

Effective Java ( 이펙티브 자바 ) - 아이템 47

반환 타입으로는 스트림보다 컬렉션이 낫다


  원소 시퀸스를 반환하는 메서드는 수없이 많다. 자바 7까지는 Collections, Set, List 같은 컬렉션 인터페이스, 혹은 Iterable이나 배열을 썼다. for-each 문에서만 쓰이거나 반환된 원소 시퀸스가 일부 Collection 메서드를 구현할 수 없을 때는  Iterable 인터페이스를 썼다. 반환 원소들이 기본타입이거나 성능에 민감한 상황이라면 배열을 썼다. 그런데 자바 8이 스트림이라는 개념을 들고 오면서 이 선택이 아주 복잡한 일이 되어버렸다.

  

  아이템 45에서 이야기했듯이 스트림은 반복을 지원하지 않는다. 따라서 스트림과 반복을 알맞게 조합해야 좋은 코드가 나온다. 여기서 재미난 사실 하나! 사실 Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함할 뿐만 아니라, Iterable 인터페이스가 정의한 방식대로 동작한다. 그럼에도 for-each로 스트림을 반복할 수 없는 까닭은 바로 Stream이 Iterable을 확장하지 않아서다. 

 

  안타깝게도 이 문제를 해결해줄 멋진 우회로는 없다.

for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
   // 프로세스를 처리한다.
}

아쉽게도 이 코드는 컴파일 오류를 낸다.

메소드 참조를 적절히 형변환해주면 아래와 같다.

for (ProcessHandle ph : (Iterable<ProcessHandle>)
                        ProcessHandle.allProcess()::iterator) {
    // 프로세스를 처리한다.
}

작동은 하지만 실전에 쓰기에는 너무 난잡하고 직관성이 떨어진다. 다행히 어댑터 메서드를 사용하면 상황이 나아진다. 다음 코드와 같이 쉽게 만들어낼 수 있다. 

public static <E> Iterable<E> iterableOf(Stream<E> stream) {
   return stream::iterator;
}

 

어댑터를 사용하면 어떤 스트림도 for-each 문으로 반복할 수 있다.

for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
   // 프로세스를 처리한다.
}

 

  반대로, API가 Iterable만 반환하면 이를 스트림 파이프라인에서 처리하려는 프로그래머가 성을 낼 것이다. 자바는 이를 위한 어댑터도 제공하지 않지만, 역시 손쉽게 구현할 수 있다.

public static <E> Stream<E> streamOf(Iterable<E> iterable) {
   return StreamSupport.stream(iterable.spliterator(), false);
}

 

  객체 시퀸스를 반환하는 메서드를 작성하는데, 이 메서드가 오직 스트림 파이프라인에서만 쓰일 걸 안다면 마음 놓고 스트림을 반환하게 해주자. 반대도 마찬가지다. 하지만 공개 API를 작성할 때는 스트림 파이프라인을 사용하는 사람과 반복문에서 쓰려는 사람 모두를 배려해야 한다. 사용자 대부분이 한 방식만 사용할 거라는 그럴싸한 근거가 없다면 말이다. 

 

  Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 지원한다. 따라서 Collection이나 그 하위 타입을 쓰는 게 일반적으로 최선이다. Arrays 역시 Arrays.asList와 Stream.of 메서드로 손쉽게 반복과 스트림을 지원할 수 있다. 

  반환하는 시퀸스의 크기가 메모리에 올려도 안전할 만큼 작다면 ArrayList, HashSet 같은 표준 컬렉션 구현체를 반환하는 게 최선일 수 있다. 하지만 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀸스를 메모리에 올려서는 안 된다.

 

  반환할 시퀸스가 크지만 표현을 간결하게 할 수 있다면 전용 컬렉션을 구현하는 방안을 검토해보자. 여기서는 AbstractList를 통하여 멱집합을 반환하고 있다. 

 

  AbstractCollection을 활용해서 Collection 구현체를 작성할 때는 Iterable용 메서드 외에 2개만 더 구현하면 된다. 바로 contains, size다. 반복이 시작되기 전에는 시퀸스의 내용을 확정할 수 없는 등의 사유로.. contains와 size를 구현하는 게 불가능할 때는 컬렉션보다는 스트림이나 Iterable을 반환하는 편이 낫다.

 

  때로는 단순히 구현하기 쉬운 쪽을 선택하기도 한다.