Stream.collect(Collectors.toList()) 와 Stream.toList() 는 다르다!

Lee Jaehyeon·2024년 8월 6일
1

자바

목록 보기
1/1
post-thumbnail

Stream.collect(Collectors.toList()) 와 Stream.toList() 는 다르다!


어느날 production 의 레거시 로직에서 발생한 UnsupportedOperationException...

무엇이 문제였을까요?


java 로 백엔드 개발을 하다 보면,

가장 흔한 패턴 중 하나가

특정 데이터를 원하는 형태로 변환하거나 추출하는 것입니다.

이때는 Stream API 를 사용하면 편리하고, 일반적으로 좋은 가독성을 유지할 수 있습니다.

아래 코드를 살펴봅시다.

        List<String> imageUrls = articleImageRepository.findByArticle(article)
                .stream()
                .map(ArticleImage::getImageUrl)
                .collect(Collectors.toList());

위 코드 스니펫에서는 articleImage 엔티티 리스트를 쿼리해온 뒤에, ImageUrl 만 List 으로 추출하고 있습니다.

여기서 주목해야 할 부분은 collect(Collectors.toList() 입니다.

해당 부분에 커서를 가져다 대면 intelliJ 에서는 아래와 같은 메세지를 띄워줍니다.

`collect(toList())` can be replaced with 'toList()'

- Replace 'collect(toList())' with 'toList()'?

이 제안을 무심코 수락하다가는, 예상치 못한 잠재적인 버그를 서비스에 심을 수 있습니다.

 🔥 Stream.collect(Collectors.toList()) 와 Stream.toList() 는 명백하게 다르다! 

어떻게 다를까?

주요한 차이점은 아래와 같습니다.


<- Stream.collect(Collectors.toList()) 는 가변 리스트를 반환한다.

- Stream.toList() 는 불변 리스트를 반환한다.

jdk 16 부터 도입된 메서드 인 Stream.toList() 는 불변 리스트를 반환하며, 외부에서 수정 시도 시 UnsupportedOperationException 이 터집니다.


List<String> mutableList = Stream.of("a", "b").collect(Collectors.toList());
mutableList.add("c"); // ✅ 가능

List<String> immutableList = Stream.of("a", "b").toList();
immutableList.add("c"); // 🔥 UnsupportedOperationException 발생

필요한 구현체가 불변 리스트인지, 가변리스트인지 요구사항을 정확하게 파악하고 사용하는 것이 중요합니다.

IntelliJ, SonarQube 등에서 collect(Collectors.toList())Stream.toList() 으로 변경하는 것을 권할 수 있지만,

두 메서드의 기능적인 차이점을 명확히 인지하지 못한 채 받아들이면

상황에 따라 꽤나 심각한 버그가 발생할 수도 있겠죠.

돌다리도 두들겨 보고 건너야 할 것 같습니다.


내부 구현 코드를 따라가서 두 눈으로 검증하기

시간적으로 허용된다면 반드시 내부 구현 코드를 확인해서, 스스로 납득하는 것이 가장 기억에 오래 남는 것 같습니다.

jdk 17 기준으로 살펴보겠습니다.

먼저 Stream.collect(Collectors.toList()) 부터 따라가봅시다.


    /**
     * Accumulates the elements of this stream into a {@code List}. The elements in
     * the list will be in this stream's encounter order, if one exists. The returned List
     * is unmodifiable; calls to any mutator method will always cause
     * {@code UnsupportedOperationException} to be thrown. There are no
     * guarantees on the implementation type or serializability of the returned List.
     *
     * <p>The returned instance may be <a href="{@docRoot}/java.base/java/lang/doc-files/ValueBased.html">value-based</a>.
     * Callers should make no assumptions about the identity of the returned instances.
     * Identity-sensitive operations on these instances (reference equality ({@code ==}),
     * identity hash code, and synchronization) are unreliable and should be avoided.
     *
     * <p>This is a <a href="package-summary.html#StreamOps">terminal operation</a>.
     *
     * @apiNote If more control over the returned object is required, use
     * {@link Collectors#toCollection(Supplier)}.
     *
     * @implSpec The implementation in this interface returns a List produced as if by the following:
     * <pre>{@code
     * Collections.unmodifiableList(new ArrayList<>(Arrays.asList(this.toArray())))
     * }</pre>
     *
     * @implNote Most instances of Stream will override this method and provide an implementation
     * that is highly optimized compared to the implementation in this interface.
     *
     * @return a List containing the stream elements
     *
     * @since 16
     */
    @SuppressWarnings("unchecked")
    default List<T> toList() {
        return (List<T>) Collections.unmodifiableList(new ArrayList<>(Arrays.asList(this.toArray())));
    }


    /**
     * Returns an <a href="Collection.html#unmodview">unmodifiable view</a> of the
     * specified list. Query operations on the returned list "read through" to the
     * specified list, and attempts to modify the returned list, whether
     * direct or via its iterator, result in an
     * {@code UnsupportedOperationException}.<p>
     *
     * The returned list will be serializable if the specified list
     * is serializable. Similarly, the returned list will implement
     * {@link RandomAccess} if the specified list does.
     *
     * @implNote This method may return its argument if the argument is already unmodifiable.
     * @param  <T> the class of the objects in the list
     * @param  list the list for which an unmodifiable view is to be returned.
     * @return an unmodifiable view of the specified list.
     */
    @SuppressWarnings("unchecked")
    public static <T> List<T> unmodifiableList(List<? extends T> list) {
      if (list.getClass() == UnmodifiableList.class || list.getClass() == UnmodifiableRandomAccessList.class) {
        return (List<T>) list;
      }

      return (list instanceof RandomAccess ?
        new UnmodifiableRandomAccessList<>(list) :
        new UnmodifiableList<>(list));
    }

이번에는 collect(Collectors.toList()) 를 봅시다.


    /**
     * Returns a {@code Collector} that accumulates the input elements into a
     * new {@code List}. There are no guarantees on the type, mutability,
     * serializability, or thread-safety of the {@code List} returned; if more
     * control over the returned {@code List} is required, use {@link #toCollection(Supplier)}.
     *
     * @param <T> the type of the input elements
     * @return a {@code Collector} which collects all the input elements into a
     * {@code List}, in encounter order
     */
    public static <T>
    Collector<T, ?, List<T>> toList() {
        return new CollectorImpl<>(ArrayList::new, List::add,
                                   (left, right) -> { left.addAll(right); return left; },
                                   CH_ID);
    }

무엇이 다른가요?

toList() 에서 반환하는 구현체는 Collectors.UnmodifiableRandomAccessList, Collectors.UnmodifiableList 으로 불변 리스트이지만

collect(Collectors.toList()) 에서 반환하는 구현체는 ArrayList 으로 가변 리스트입니다.


불변 리스트의 원소를 수정하려고 하면 UnsupportedOperationException 이 발생하고,

해당 예외는 runtime 에, 해당 line 이 수행될 때에만 발생하므로 찾기 어렵죠.

UnsupportedOperationException 이 triggering 되는 코드는 UnmodifiableList 내부 코드를 확인해서 확신을 얻을 수 있습니다.


public static <T> List<T> unmodifiableList(List<? extends T> list) {
         // ... 생략 ...
    }

    /**
     * @serial include
     */
    static class UnmodifiableList<E> extends UnmodifiableCollection<E>
                                  implements List<E> {
        // ... 생략 ...
        public E set(int index, E element) {
            throw new UnsupportedOperationException();
        }
        public void add(int index, E element) {
            throw new UnsupportedOperationException();
        }
        public E remove(int index) {
            throw new UnsupportedOperationException();
        }
        public boolean addAll(int index, Collection<? extends E> c) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            throw new UnsupportedOperationException();
        }
        @Override
        public void sort(Comparator<? super E> c) {
            throw new UnsupportedOperationException();
        }

        // ... 생략 ...

        public ListIterator<E> listIterator(final int index) {
            return new ListIterator<E>() {
                
                // ... 생략 ...

                public void remove() {
                    throw new UnsupportedOperationException();
                }
                public void set(E e) {
                    throw new UnsupportedOperationException();
                }
                public void add(E e) {
                    throw new UnsupportedOperationException();
                }
            };
          // ... 생략 ...
        }

P.S.

항상 의심해...

References

profile
Backend Developer (Java/Kotlin with Spring)

0개의 댓글