JAVA/Effective Java

[아이템 47] 반환 타입으로는 스트림보다 컬렉션이 낫다

꾸준함. 2024. 3. 5. 00:42

다양한 선형 자료구조를 반환하는 메서드가 많이 있으며, 해당 메서드의 반환 타입은 다음 중 하나였습니다.

  • Collection, Set, 혹은 List와 같은 컬렉션 인터페이스
  • Iterable 인터페이스
  • String[]과 같은 배열

 

1. Collection 인터페이스

  • 기본적으로 컬렉션 인터페이스를 반환 타입으로 채택

 

2. Iterable 인터페이스

  • for-each 문에서만 쓰이거나
  • 반환된 원소 시퀀스가 contains(Object)와 같은 일부 Collection 메서드를 구현할 수 없을 경우 사용

 

3. 배열

  • 반환 원소들이 primitive 타입이거나
  • 성능에 민감할 경우

 

이처럼 메서드의 반환 타입을 선택할 때는 명확한 기준이 있었습니다.

하지만 자바 8 버전부터 등장한 스트림에 의해 선택하는 기준이 다소 복잡해졌습니다.

  • 스트림이 iteration을 지원하지 않기 때문에 애매해짐

 

스트림은 iteration을 지원하지 않음

  • 스트림은 반복을 지원하지 않음
    • 스트림은 Iterator 인터페이스가 정의한 추상 메서드를 모두 구현하고 있지만 Iterator 인터페이스를 구현하고 있지 않아 for-each로 반복 불가
    • API가 스트림을 반환하도록 작성될 경우, 클라이언트는 반환된 스트림을 for-each로 직접 반복할 수 없어서 불편할 수 있음
    • 따라서, 스트림과 반복을 알맞게 조합해야 좋은 코드가 나옴

 

1. API에서 Stream만 반환하는 케이스

 

ProcessHandle 클래스 내 allProcesses() 메서드가 스트림만 반환합니다.

 

 

앞서 설명했다시피 스트림은 Iterator 인터페이스 구현체가 아니기 때문에 별도 처리 없이 for-each 반복문을 사용하려고 하면 컴파일 오류가 발생합니다.

 

1.1 컴파일 오류 나는 코드

 

 

 

코드 부연 설명

  • iterator 메서드를 통해 for-each 반복문을 돌릴 수 있을 것 같은 코드지만
  • 스트림이 Iterator 구현체가 아니기 때문에 컴파일 오류 발생
  • 이 오류를 잡기 위해서는 메서드 참조를 매개변수화된 Iterable로 적절히 형변환 필요

 

 

 

1.2 컴파일 오류는 해결했지만 아쉬운 코드

 

 

 

코드 부연 설명

  • 작동은 하지만 실전에 쓰기에는 너무 난잡하고 직관성 떨어짐

 

1.3 어댑터를 메서드를 적용해 쓸만한 코드

 

 

 

코드 부연 설명

  • 어댑터 메서드 적용 시 자바 컴파일러가 타입 추론을 잘하여 별도 형변환 불필요
  • 어댑터 메서드 적용 시 어떤 스트림도 for-each문으로 반복 가능

 

2. API에서 Iterator만 반환하는 경우

 

API에서 Iterator만 반환하는 경우 스트림을 주로 사용하는 개발자들은 불편을 겪을 수 있습니다.

다행히도 Iterator를 스트림으로 변환해 주는 어댑터를 쉽게 구현 가능합니다.

 

 

 

객체 시퀀스를 반환하는 메서드를 작성할 때, 메서드가 오직 스트림 파이프라인에서만 쓰인다면 마음 놓고 스트림만 반환하라고 권장할 수 있지만 대부분의 개발자들은 스트림 파이프라인보다는 for-each 반복문에 익숙합니다.

  • 따라서 두 분류의 개발자들을 모두 배려하여 Stream과 Iterable을 동시에 제공할 수 있도록 메서드를 작성하는 것을 권장
  • 이 때문에 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Stream과 Iterable을 동시에 지원하는 Collection이나 하위 타입을 쓰는 것이 일반적

 

Collection 내 시퀀스가 크면 전용 컬렉션을 구현하는 것을 고려

반환하는 시퀀스의 크기가 메모리에 올려도 안전할만큼 작다면 ArrayList 혹은 HashSet과 같은 표준 컬렉션 구현체를 반환하는 것이 최선일 수 있습니다.

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

 

1. 예시 코드

 

다음은 입력 집합의 멱집합을 전용 컬렉션에 담아 반환하는 코드입니다.

 

 

 

코드 부연 설명

  • AbstractCollection을 활용해서 Collection 구현체를 작성할 때는 Iterable용 메서드 외 contains()와 size() 메서드만 구현하면 되고 이는 손쉽게 효율적으로 구현 가능
  • 입력 집합의 원소 수가 30을 넘으면 Power.of 메서드에서 예외를 던지는 이유
    • size() 메서드의 반환 타입은 int이고 이 때문에 최대 길이는 Integer.MAX_VALUE로 제한됨
    • 보편적인 반환 타입인 Collection을 쓸 때는 크기를 고려해야 함
    • 멱집합을 컬렉션으로 반환할 경우 입력 리스트 크기의 거듭제곱만큼 메모리를 차지

 

  • Stream이나 Iterable은 데이터를 나타내는 인터페이스로, 크기에 대한 고민이 필요하지 않음
    • 이들은 요소들의 시퀀스를 나타내며, 그 자체로 크기에 대한 정보를 갖지 않음

 

  • 그러나, Collection을 반환할 때는 해당 컬렉션의 크기에 대한 고려가 필요
    • 반환되는 컬렉션이 몇 개의 요소를 포함하는지 고려해야 하며, 이는 클라이언트 코드에서 사용될 때 유용한 정보

 

2. 스트림을 반환하는 것이 나은 케이스

 

반복이 시작되기 전 시퀀스의 내용을 확정할 수 없는 경우 contains()와 size() 메서드를 구현하는 것이 불가능합니다.

이런 경우 컬렉션보다는 Stream이나 Iterable을 반환하는 것이 낫습니다.

앞서 설명한 멱집합 때처럼 전용 컬렉션을 구현하는 것은 번거로우며 자바에서는 이럴 때 쓸만한 골격 Iterator를 제공하지 않기 때문에 스트림을 반환하는 것이 나은 선택일 수 있습니다.

 

2.1 예시

 

 

 

코드 부연 설명

  • (a, b, c)의 prefixes는 (a), (a, b), (a, b, c)
  • (a, b, c)의 suffixes는 (a, b, c), (b, c), (c)
  • Stream.concat 메서드는 반환되는 Stream에 빈 리스트를 추가하며, flatMap은 모든 Stream을 하나의 Stream으로 만듦
    • 어떤 리스트의 부분 리스트는 단순히 그 리스트의 prefix의 suffix에 혹은 suffix의 prefix에 빈 리스트 하나만 추가하면 됨

 

2.2 반복문으로 작성한 예시

 

 

 

 

코드 부연 설명

  • 앞서의 구현보다 간결하지만 가독성 측면에서는 좋지 않음

 

2.3 Stream 중첩으로 작성한 예시

 

 

 

 

 

 

3. 성능 비교

 

자바에서 Stream을 Iterator로 변환하는 어댑터 대신 Collection을 사용하는 것이 성능적인 이점을 가집니다.

  • Collection은 내부적으로 효율적인 반복을 위한 메커니즘을 구현
    • ex) 특정 컬렉션 구현체는 인덱스를 사용하여 요소에 접근하는데, 이는 일부 상황에서 반복 성능을 향상
    • 반면에 Stream을 이용한 반복은 중간 처리 단계, 병렬 처리 등을 고려해야 하므로 더 많은 오버헤드가 발생 가능

 

  • Stream은 지연 평가를 지원하며, 이는 필요한 경우에만 요소를 생성하고 처리하는 방식
    • 그러나 Iterator로 변환된 Stream은 여전히 Stream의 특성을 유지하면서 반복되므로 중간 처리 단계가 필요한 경우 이로 인한 오버헤드가 발생 가능
    • Collection을 사용할 경우, 이미 모든 요소가 메모리에 적재되어 있어 중간 처리에 대한 오버헤드가 적음

 

 

정리

  • Stream이나 Iterable을 반환하는 API에는 Stream을 Iterable로 혹은 Iterable을 Stream으로 변환해 주는 어댑터 메서드 필요
    • 직접 구현한 전용 Collection보다 덜 복잡하지만 성능이 안 좋음

 

  • 반복문에 익숙한 개발자와 스트림에 익숙한 개발자 모두 충족시키기 위해 반환 타입이 Stream, Iterator을 모두 지원할 수 있게 메서드를 작성하는 것을 권장
    • 보편적인 반환 타입이 Collection인 이유

 

  • 멱집합의 예처럼 원소의 개수가 많을 경우 전용 Collection을 반환하는 것을 고려
  • 추후 자바 버전에서 Stream 인터페이스가 Iterable을 지원하도록 수정될 경우 그때는 안심하고 Stream을 반환하면 됨

 

참고

이펙티브 자바

반응형