자바 컬렉션을 Null Safe 하게 정렬하기

자바 8에 도입된 스트림(Stream)을 사용하면 컬렉션을 쉽게 정렬할 수 있다. (Stream이 아니더라도 List인터페이서의 sort() 메서드를 사용할 수도 있다.) 예를들어 Member를 나이 순으로 정렬한다면 아래와 같이 할 수 있다.

1
2
3
4
5
6
7
List<Member> memberList = Arrays.asList(Member.of(10), Member.of(5), Member.of(20));
List<Member> sortedMemberList = memberList.stream()
.sorted(Comparator.comparing(Member::getAge)) // member의 age 속성을 기준으로 정렬한다.
.collect(toList());
System.out.println(sortedMemberList);

결과

1
[Member(age=5), Member(age=10), Member(age=20)]

위와 같이 Comparator 클래스에서 제공하는 comparing 메서드를 사용하면 원하는 정렬 기준을 람다식으로 표현하여 정렬을 쉽게 할 수 있다. (물론 정렬 기준이 되는 속성은 Comparable 타입이어야 한다. 여기서 Member의 age는 Integer이다.)

Comparator.comparing() 메서드는 두 번째 인자를 받을 수 있는 메서드가 하나 더 오버로드 되어있는데, 두 번째 인자는 정렬의 순서를 결정하는 Comparator를 받는다. 만약 Member를 나이를 기준으로 정렬하되 역순으로 정렬하고 싶으면 다음과 같이 작성한다.

1
2
3
4
5
6
7
List<Member> memberList = Arrays.asList(Member.of(10), Member.of(5), Member.of(20));
List<Member> sortedMemberList = memberList.stream()
.sorted(Comparator.comparing(Member::getAge, Comparator.reverseOrder()))
.collect(toList());
System.out.println(sortedMemberList);

결과

1
[Member(age=20), Member(age=10), Member(age=5)]

정렬 기준 키로 Member::getAge 람다식을 넘기고, 정렬 순서로 Comparator.reverseOrder()를 넘기면 나이를 기준으로 역순 정렬 되는것을 알 수 있다.

정렬 기준이 Nullable 하다면?

자바는 언어의 특성상 항상 null을 주의해야한다. 위의 예제에서 Member의 나이가 null로 설정된 경우가 있다면 어떨까?

1
2
3
4
5
6
7
List<Member> memberList = Arrays.asList(Member.of(10), Member.of(null), Member.of(5));
List<Member> result = memberList.stream()
.sorted(Comparator.comparing(Member::getAge))
.collect(toList());
System.out.println(result);

결과

1
2
3
4
5
6
7
8
java.lang.NullPointerException
at java.lang.Integer.compareTo(Integer.java:1216)
at java.lang.Integer.compareTo(Integer.java:52)
at java.util.Comparator.lambda$comparing$77a9974f$1(Comparator.java:469)
at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355)
at java.util.TimSort.sort(TimSort.java:220)
at java.util.Arrays.sort(Arrays.java:1512)
...

결과는 예상 했던대로 NullPointerException이 발생하게 된다. compareTo()를 호출할 대상 Integer(age)가 null이기 때문이다. 물론 처음부터 null이 설정될 수 없게 해놓으면 좋겠지만 실무를 하다보면 그렇지 않은 경우를 많이 만나게 된다. 뭐 어찌됐던 개발자는 일방통행 도로라고 해도 양쪽을 모두 살피고 길을 건너야 하지 않겠는가.

자바는 이런 경우에 사용할 수 있는 2가지 Comparator를 제공한다. 바로 Comparator.nullsFirst()Comparator.nullsLast()이다.

Comparator.nullsFirst(), Comparator.nullsLast()

위 두개의 Comparator를 사용하면 정렬 기준 값이 null이어도 NPE가 발생하지 않고 안전하게 정렬을 할 수 있다. 두 객체의 차이는 이름에서도 알 수 있듯이 null을 가진 객체를 앞으로 보내느냐 뒤로 보내느냐의 차이이다. 아래 코드처럼 사용할 수 있다.

1
2
3
4
5
6
7
List<Member> memberList = Arrays.asList(Member.of(10), Member.of(null), Member.of(5));
List<Member> result = memberList.stream()
.sorted(Comparator.comparing(Member::getAge, Comparator.nullsLast(Comparator.naturalOrder())))
.collect(toList());
System.out.println(result);

결과

1
[Member(age=5), Member(age=10), Member(age=null)]

Comparator.comparing() 메서드의 두 번째 인자를 넘길 때 Compartor.nullsLast()로 한 번 감싸서 넘겨주게 되면, null을 갖는 객체들은 전부 뒤쪽으로 정렬이 되고 나머지는 주어진 정렬 순서에 맞게 정렬이 된다. 만약 null을 앞으로 정렬하고 나머지는 나이의 역순으로 정렬하고 싶다면 아래와 같이 하면 된다.

1
2
3
4
5
6
List<Member> memberList = Arrays.asList(Member.of(10), Member.of(null), Member.of(5));
List<Member> result = memberList.stream()
.sorted(Comparator.comparing(Member::getAge, Comparator.nullsFirst(Comparator.reverseOrder())))
.collect(toList());
System.out.println(result);

결과

1
[Member(age=null), Member(age=10), Member(age=5)]

위와 같이 Comparator.nullsFirst()Comparator.nullsLast()를 잘 활용하면 null에 안전한 정렬 코드를 작성할 수 있다.

Share