본문 바로가기

💻 개발/Java

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

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

 

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);

결과

 

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

 

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

 

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

 

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);

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

 

정렬 기준이 Nullable 하다면?

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

 

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);

결과

 

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을 가진 객체를 앞으로 보내느냐 뒤로 보내느냐의 차이이다. 아래 코드처럼 사용할 수 있다.

 

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);

결과

 

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

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

 

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);

결과

 

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

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