티스토리 뷰

최근 JDK 17 release 되었고, JDK 11 다음의 LTS 버전으로서 오랫동안 지원이 되는 만큼 업무/개인프로젝트 등에서 적극적으로 사용하고 있다.

 


Stream.toList()

JDK17 기반에서 Stream을 사용해 List로 변환(collect)하다보면 IntelliJ에서 자주 보는 메세지가 있다.

위의 경우인데, collect(toList()) 대신 Stream.toList() 를 사용할 수 있다는 정보성 메시지이다.

 

그래서 나는 보통 JDK 17로 개발을 할 때, 아래와 같이 사용했다.

// Before jdk 17
var peopleName = people.stream()
                        .map(Person::name)
                        .collect(Collectors.toList());

// jdk 17
var peopleName = people.stream()
                        .map(Person::name)
                        .toList();

이게 가능해진 건 Stream.toList() 메서드가 생겼기 때문이다.

  • 해당 메서드는 JDK 16에서 생겼다. (참조)
  • 기존의 collect(Collectors.toList()) 부분을 toList() 메서드 하나로 줄여서 더 간결화했다.

 


Upgrade to JDK 17

그러면 기존의 JDK 16 이전의 프로젝트들에서 JDK 17로 버전 업그레이드를 하며 위에서 말한 list stream collect 부분을 toList 로 다 이전해도 되는걸까?

그건 상황에 따라 고려해봐야한다.

왜냐하면, collect(Collectors.toList())toList() 가 완전히 똑같은 형태의 구현체로 반환되지 않기 때문이다.

 

collect(Collectors.toList())ArrayList 를 반환한다.

  • javadoc에는 아래와 같이 작성되어있다.

There are no guarantees on the type, mutability, serializability, or thread-safety of the List returned

 

toList()Collectors.UnmodifiableList 또는 Collectors.UnmodifiableRandomAccessList 를 반환한다.

  • UnmodifiableList 와 UnmodifiableRandomAccessList 의 차이는 현재 글의 주제와 좀 어긋나니 이후부터는 UnmodifiableList 라고만 이야기하겠다. 그리고 UnmodifiableRandomAccessList 도 사실 UnmodifiableList 를 상속해 구현한 구현체이다.

 

ArrayListUnmodifiableList . 둘의 구현체 차이가 보이는가?

  • 이름에서 나타내는 것처럼 수정이 가능한 구현체와 수정이 불가능한 구현체라는 것이다.

 

 

그럼 이 구현체의 성질이 JDK 17로의 업그레이드 시에 어떤 영향을 미칠까?

예를 살펴보자. 아래와 같은 프로젝트 코드가 있다고하자.

public void someMethod() {
        var peopleName = people.stream()
                               .map(Person::name)
                               .collect(Collectors.toList());
        someBusinessLogic(peopleName);

        ...

}

private void someBusinessLogic(List<String> peopleName) {
        peopleName.add("Name that should always be added");
}

특정 리스트를 stream을 통해 만들고, 마지막에 무조건 어떤 요소를 추가하는 로직이 있는 상황이다.

  • 예제 코드이므로 억지스러워도 넘어가자.

 

이 예제를 JDK 17로 넘어가며 아래처럼 수정하게 된다면, UnsupportedOperationException 예외가 발생할 것이다.

public void someMethod() {
        var peopleName = people.stream()
                               .map(Person::name)
                               .toList(); // UnmodifiableList
        someBusinessLogic(peopleName);

        ...

}

private void someBusinessLogic(List<String> peopleName) {
        peopleName.add("Name that should always be added"); // UnsupportedOperationException
}

위에서 이야기했듯 Stream.toList()UnmodifiableList 를 반환하기에 List 자체에 대한 변경이 불가능하기 때문이다.

 

UnmodifiableList 구현체의 코드는 다음과 같다.

static class UnmodifiableList<E> extends UnmodifiableCollection<E>
                                  implements List<E> {
    ...
    public E get(int index) {return list.get(index);}
    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 int indexOf(Object o)            {return list.indexOf(o);}
    public int lastIndexOf(Object o)        {return list.lastIndexOf(o);}
    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();
    }
    ...
}

보는 것처럼 수정에 관련된 메서드들은 UnsupportedOperationException 예외를 던진다.

 

UnsupportedOperationException 예외는 RuntimeException 의 구현체로서 런타임 시점에 알 수 있기에 코드를 실행하기 이전에는 알지 못한다. 그렇기에 IDE 상에서도 위 예제 코드에서 오류를 예측하지 못한다.

 


그럼 toUnmodifiableList 는 대체가 가능한걸까?

collect(Collectors.toList()) 는 이후에 List 수정로직이 존재한다면 Stream.toList() 로 대체하지 못한다.

그러면 collect(Collectors.toUnmodifiableList()) 코드의 경우 가능한걸까? 아래처럼 말이다.

// Before jdk 17
var peopleName = people.stream()
                        .map(Person::name)
                        .collect(Collectors.toUnmodifiableList());

// jdk 17
var peopleName = people.stream()
                        .map(Person::name)
                        .toList();

 

가능하다. 하지만 둘의 차이가 있긴 있다. (TMI)

collect(Collectors.toUnmodifiableList()) 의 경우 input list에 null 요소를 허용하지 않고, Stream.toList()null 요소를 허용한다는 것이다.

 

예제를 보자.

void someMethod() {
    var peopleName = people.stream()
                             .map(someMappingOtherValue())
                             .collect(Collectors.toUnmodifiableList());

    ...

}

privateFunction<Person,Person> someMappingOtherValue() {
    returnperson-> {
        if (person.age < 20) {
            return null; // 여긴 무조건 들린다는 가정
        }
        returnperson;
    };
}

위 예제의 경우, NPE가 발생한다. Collectors.toUnmodifiableList 메서드 내에서 ImmutableCollections.listFromTrustedArray 메서드를 호출해 새로운 리스트로 요소 데이터를 추가해 생성하게 되는데, 이때 요소들의 null 값 여부를 확인(Objects.requireNonNull)하기 때문이다.

 

하지만 Stream.toList()null 요소를 허용하므로 아래와 같이 수정해도 NPE가 발생하지 않는다.

void someMethod() {
    var peopleName = people.stream()
                             .map(someMappingOtherValue())
                             .toList();

    ...

}

privateFunction<Person,Person> someMappingOtherValue() {
    returnperson-> {
        if (person.age < 20) {
            return null; // 여긴 무조건 들린다는 가정
        }
        returnperson;
    };
}

 


Summary

  • collect(toList())
    • 수정이 허용된다.
    • Null 값이 허용된다.
  • collect(toUnmodifiableList())
    • 수정이 불가능하다.
    • Null 값이 허용되지 않는다.
  • toList()
    • 수정이 불가능하다.
    • Null 값이 허용된다.

Java 버전을 올리면서 새로운 기능을 사용할 때 고려해야하는 점이 참 많은 것 같다. IDE에서는 “이런거 사용할 수 있어~ 바꿔봐~!”라고 계속 유혹하지만, 코드 로직에서 변경된 부분을 적용해도 발생하는 이슈가 없을 지 잘 찾아보고 변경하는 것이 중요하다는 것을 깨달을 수 있었다.

사실 근데 버전 업그레이드마다 이러한 부분을 세세히 알기 어렵다. 그러므로... “테스트 코드"를 작성하는 습관을 들이고 이를 통해 이러한 세세한 오류도 커버할 수 있도록 하는 것이 제일 베스트인 것 같다.

320x100
반응형
댓글
반응형
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함