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

김종준·2023년 7월 20일
0

이펙티브자바

목록 보기
40/63

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

원소 시퀀스, 즉 일련의 원소를 반환하는 메서드는 수없이 많다.

자바 7까지는 이런 메서드이 반환 타입으로 Collection, Set, List 같은 컬렉션 인터펭스, 혹은 Iterable 이나 배열을 사용했다.

이 중 가장 적합한 타입은 컬렉션 인터페이스다.

for-each 문에서만 쓰이거나 반환된 원소 시퀀스가 일부 Collection 메서드를 구현할 수 없을 때는 Iterable 인터페이스를 썼다.

반환 원소들이 기본 타입이거나 성능에 민감한 상황이라면 배열을 썼다.

그런데 자바 8이 스트림이라는 개념을 들고 오면서 이 선택이 아주 복잡한 일이 되었다.

원소 시퀀스를 반환할 때는 당연히 스트림을 사용해야 한다는 이야기를 들어봤을지 모르겠지만, 스트림은 반복(iteration)을 지원하지 않는다.

따라서 스트림과 반복을 알맞게 조합해야 좋은 코드가 나온다.

API를 스트림만 반환하도록 짜놓으면 반환된 스트림을 for-each로 반복하길 원하는 사용자는 당연히 불만을 토로할 것이다.

여기서 재미난 것은 사실 Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함할 뿐만 아니라, Iterable 인터페이스가 정의한 방식대로 동작한다.

그럼에도 for-each로 스트림을 반복할 수 없는 까닭은 Stream이 Iterable을 확장하지 않았기 때문이다.

Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 지원한다.

따라서 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection이나 그 하위 타입을 쓰는게 일반적으로 최선이다.

Arrays 역시 Arrays.asList와 Stream.of 메서드로 손쉽게 반복과 스트림을 지원할 수 있다.

반환하는 시퀀스의 크기가 메모리에 올려도 안절할 만큼 작다면 ArrayList나 HashSet 같은 표준 컬렉션 구현체를 반환하는 게 최선일 수 잇다.

하지만 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안 된다.

반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 컬렉션을 구현하는 방안을 검토할 수 있다.

멱집합의 경우 원소의 개수가 n개면 멱집합의 원소 개수는 2^n개가 된다.

그러니 멱집합을 표준 컬렉션 구현체에 저장하려는 생각은 위험하다.

하지만 AbstractList를 이용하면 훌륭한 전용 컬렉션을 손쉽게 구현할 수 있다.

public class PowerSet {
  public static final <E> Collection<Set<E>> of(Set<E> s) {
    List<E> src = new ArrayList<>(s);
    if(src.size() > 30) {
      throw new IllegalArgumentException("집합에 원소가 너무 많습니다.");
    }
    return new AbstractList<Set<E>>() {
      @Override public int size() {
        return 1 << src.size();
      }
      @Override public boolean contains(Object o) {
        return o instanceof Set && src.containsAll((Set) o);
      }
      @Override public Set<E> get(int index) {
        Set<E> result = new HashSet<>();
        for(int i = 0; index != 0; i++, index >>=1) {
          if((index & 1) == 1) {
            result.add(src.get(i));
          }
        }
        return result;
      }
    };
  }
}

AbstractCollection을 활용해서 Collection 구현체를 작성할 때는 Iterable용 메서드 외에 contains와 size 2개만 더 구현하면 된다.

만약 이 두 메서드를 구현하는 게 불가능할 때는 컬렉션보다는 스트림이나 Iterable을 반환하는 편이 낫다.

원한다면 별도의 메서드를 두어 두 방식을 모두 제공해도 된다.

원소 시퀀스를 반환하는 메서드를 작성할 때는, 이를 스트림으로 처리하기를 원하는 사용자와 반복으로 처리하길 원하는 사용자 모두가 있을 수 있음을 떠올리고, 양쪽을 다 만족시키려 노력해야 한다.

그렇기에 컬렉션을 반환할 수 있으면 그렇게 하는 것이 좋다.

반환 전부터 이미 원소들을 컬렉션에 담아 관리하고 있거나 컬렉션을 하나 더 만들어도 될 정도로 원소 개수가 적다면 ArrayList 같은 표준 컬렉션에 담아 반환하는 것이 좋다.

그렇기 않으면 앞서의 멱집합 예처럼 전용 컬렉션을 구현할지 고민하는 것이 좋다.

컬렉션을 반환하는 게 불가능하면 스트림과 Iterable 중 더 자연스러운 것을 반환하자.

만약 나중에 Stream 인터페이스가 Iterable을 지원하도록 자바가 수정된다면, 그때는 안심하고 스트림을 반환하면 된다.

0개의 댓글