본문 바로가기
Backend/JAVA

[Java] Comparator 로 복잡한 정렬 비즈니스 녹여내기

by 지구 2022. 11. 21.

오랜만에 올리는 게시글은 Java 8 의 Comparator 사용하여 복잡하고 null-safe 한 정렬 비즈니스를 녹였던 방법에 대한 정리 글이다.

* 여기서 복잡하다는 건, 같은 정렬조건이면 한번 더 정렬 할 우선순위가 존재함을 뜻함


전제 조건

1. 정렬할 대상은 서울에 있는 (모든)학교 리스트다. ==> schools
2. 정렬조건
    [1순위] 각 학교에 있는 도서관의 사서 선생님 연세 내림차순 ==> school > library > teacher.age (학교1:도서관1:사서쌤1 관계)
    [2순위] (나이가 같으면) 학교 seq 오름차순
3. 이때 학교 정보가 없을 수도 있고, 사서 선생님의 연세 또한 null 일 수 있다고 가정하자.

 

생각나는대로 적은 정렬 코드

모든학교리스트.sort(
  Comparator.comparing(
    School::getLibaray,
    Comparator.comparing(Library::getTeacher, Comparator.comparing(Teacher::getAge).reversed())
  ).thenComparing(School::getSchoolSeq)
);

 

문제점

너무 길고 장황하다. 한 눈에 들어오지도 않는다.

무엇보다 가장 큰 문제점은 null-safe 하지 않다. 학교 정보가 없거나 학생의 나이가 없은 경우 바로 NPE 가 발생한다.

 

보완할 점

자바는 이런 경우를 대비해 `Comparator.nullsFirst()` 그리고 `Comparator.nullsLast()` 메소드를 지원한다.

직관적인 메소드명 덕분에 해당 메소드가 무슨 역할을 하는지 유추가 가능하다. (유추한게 맞다)

사용은 아래처럼 하면 된다.

// teacher 의 null-safe + teacher.age 의 null-safe 한 Comparator 정의
public static final Comparator<Teacher> BY_TEACHER_AGE = 
  Comparator.nullsLast( // teacher 가 null 이면 맨 뒤로 보낸다.
    Comparator.comparing(
      // teacher.age 를 내림차순으로 정렬하되, 해당 값이 null 이면 맨 뒤로 보낸다.
      Teacher::getAge, Comparator.nullsLast(Comparator.reverseOrder())
    );
  );

 

문제점 인지 후 보완점을 적용한 리팩토링한 코드

모든학교리스트.sort(
    Comparator.comparing(
        (School sc) -> Optional.ofNullable(sc.getLibrary())
            .map(Library::getTeacher)
            .map(Teacher::getAge).orElse(null)
        , Comparator.nullsLast(Comparator.reverseOrder())
    ).thenComparing(School::getSchoolSeq)
);

세 번째 줄에 명시적 형변환 (School sc) 이 포인트다. thenComparing 구문까지 추가하면 선언한 lambda 표현이 많기 때문에 컴파일러가 어떤 객체로 정렬하는지 알 수 없어서 Object 로 인식하기 때문이다. 명시적 형변환을 해주지 않는다면 Optional.ofNullable() 쪽에서 컴파일 오류가 발생하니 유의하자.

=> Cannot resolve method 'getLibrary' in 'Object'

Collections.sort() 가 아닌 Stream 을 사용하고 싶다면 아래 코드처럼 사용하면 된다.

 

Stream 을 사용한 정렬 코드

var sorted = 모든학교리스트.stream()
    .filter(asch -> asch.getSchool() != null)
    .sorted(Comparator.comparing((School sc) -> sc.getLibrary().getTeacher().getAge(), Comparator.nullsLast(Comparator.reverseOrder())).thenComparing(School::getSchoolSeq))
    .collect(toList());

Library 가 있다면 Teacher 가 무조건 있다는 가정하에 작성된 코드다.

이렇게 써놓고 비교해 보니 확실히 Stream 을 사용한게 (개인적으로) 가독성이 좋은듯하다 :)

 


참고 Reference

https://stackoverflow.com/questions/45925825/comparator-nullsfirst-with-null-safe-comparator
https://stackoverflow.com/questions/40500280/comparing-and-thencomparing-gives-compile-error
https://codereview.stackexchange.com/questions/229497/handle-null-in-comparator-class

반응형

댓글