개린이 탈출기

[Java] Stream 을 이용한 정렬 중 null 값이 존재할 때! (Compartor.nulls-) 본문

에러 해결 목록

[Java] Stream 을 이용한 정렬 중 null 값이 존재할 때! (Compartor.nulls-)

yooverd 2024. 11. 26. 13:25
728x90
반응형
SMALL

 

스트림을 이용하여 Dto 리스트를 정렬하던 중 NullPointerException 을 마주하게 되었다.

 

 Jpa를 통해 Dto 리스트를 전달받아 자바에서 정렬처리를 하려고 했더니  NullPointerException 예외가 발생한 것이다.

실제로 데이터를 확인해보니 정렬하려는 컬럼이 nullable 하였고 정렬 중 null 값을 마주하여 정상적으로 처리하지 못하고 NullPointerException 이 발생한 것 같아 보였다.

 

해당 상황을 재현해보자면 하단의 코드와 같다.

    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public class Dto {
        private String nullableString;
        private Long nullableLong;
    }
    
    @PostMapping("/testt")
    public void testMethod() {
        List<Dto> testList = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            testList.add(new Dto("str1_" + i, 0L));
            testList.add(new Dto("str2_" + i, 2L));
            testList.add(new Dto(null, null));
            testList.add(new Dto("str3_" + i, null));
            testList.add(new Dto(null, 5L));
        }

        List<Dto> sortedList = testList.stream().sorted(
                Comparator.comparing(Dto::getNullableLong).reversed()
                        .thenComparing(Dto::getNullableString)
        ).toList();

        System.out.println("end method.");
    }

 

 첫번째 nullableLong 값을 역순 정렬하는 도중 nullableLong 값이 null 인 Dto 를 만나면 
Cannot read field "value" because "anotherLong" is null

예외가 발생한다.

따라서 비교 시 null 값이 있는 경우를 어떻게 처리할 지 지정해주어 위의 에러를 피할 수 있었는데 수정한 코드는 다음과 같다.

List<Dto> sortedList = testList.stream().sorted(
        Comparator.comparing(Dto::getNullableLong, Comparator.nullsFirst(Comparator.reverseOrder()))
                .thenComparing(Dto::getNullableString, Comparator.nullsLast(Comparator.naturalOrder()))
).toList();

오라클 db 를 사용하고 있기 때문에 DESC(역순정렬) 을 할 경우, null 값은 상위로 노출되어야 하므로 nullsFirst 메서서드를 호출하였고, 정렬 순서는 역순이므로 reverseOrder 메서드를 호출하였다.
뒤따라 오는 정렬도 동일한 이유로 작성하였다.

 

 


여담

 

스트림이 동작하는 원리와 함꼐 위의 문제가 왜 발생하는지 생각해보았다.
우선 위의 예외 메시지를 읽어 보았을 때, 전달받은 두 개의 Long 타입 파라미터를 Long 타입으로 변환하여 비교하던 중, 어떤 Long 타입 데이터가 null 이기 때문에 문제가 발생한 것으로 보였다.

 

로그를 따라 거슬러 올라가보니 첫번째 문제는 Long.compareTo 에서 발생한 것을 보니 나의 추측이 맞았던 것으로 보였다.

 

스트림의 comparing 메서드를 확인해보니 다음과 같았다.

 

 

    // Comparator.comparing 메서드
   
    public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)
    {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    }

 코드를 보면 전달받은 메서드에 따라 T->Long 클래스로 지정되므로 keyCompartor.compare 는 결국 Long.compareTo 메서드를 호출하게 된다.  그리고 Long.compareTo 는 다음과 같다.

    public int compareTo(Long anotherLong) {
        return compare(this.value, anotherLong.value);
    }

여기서 anotherLong 의 value 메서드를 호출하는 과정에서 넘어온 파라미터가 null 이므로 null -> long 으로 받을 수 없어서 오류가 발생하는 것으로 보인다.

 

 

그렇다면 Long 타입을 원시타입 long 으로 수정하면 null 값이 발생하지 않고 초기화가 자동으로 되니 문제가 발생하지 않을지도 모르겠다는  추측이 들었고 Long 을 long 으로 수정해보니 문제가 발생하지 않았다.

 

그리고 Comparator.nulls-  메서드를 사용했을 때, nullPointer 가 발생하지 않는 이유를 알기 위해 코드를 읽어보니, 결국 null 값에 Long 클래스의 필드를 사용하려고 했기 때문에 발생했던 문제였던 것이고, Comparator.nulls- 메서드를 사용하면 NullComparator 클래스가 새로 생성되어 반환되는데, 이 클래스의 compare 메서드는 null  값을 전달받을 시 어떻게 처리할 지에 대한 로직이 있으므로 문제가 발생하지 않은 것이었다.

    public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator) {
        return new Comparators.NullComparator<>(false, comparator);
    }
    
    
    // NullCompartor 의 compare 메서드
        @Override
        public int compare(T a, T b) {
            if (a == null) {
                return (b == null) ? 0 : (nullFirst ? -1 : 1);
            } else if (b == null) {
                return nullFirst ? 1: -1;
            } else {
                return (real == null) ? 0 : real.compare(a, b);
            }
        }
728x90
반응형
LIST