[이전글] https://javafreak.tistory.com/271

 

java.lang.Comparable과 java.util.Comparator 의 차이 및 활용

이 문서는 정렬에 개입하는 두개의 인터페이스인 java.lang.Comparable 과 java.util.Comparator 에 대해서 설명합니다. 이 두가지 인터페이스를 잘 이해하면 아래와 같은 다양한 JDK 구현체들을 손쉽게 이용할 수..

javafreak.tistory.com

이번 글에서는 이전에 설명했던 Comparator의 다양한 활용을 다뤄봅니다.

다중정렬

2개 이상의 속성으로 주어진 대상들을 정렬하는 것을 의미합니다.

* 나이의 내림차순으로 정렬, 나이가 같으면 이름의 오름차순으로 정렬

* 책 가격의 내림차순으로(높은 가격부터) 정렬, 책 가격이 같으면 출판년도의 오름차순으로(오래된 책부터) 정렬

이런 식으로 주어진 엔티티의 속성들을 기준으로 다양한 조합으로 정렬하는 경우가 있습니다(데이터베이스에서의 order by에 해당함).

여기서는 책에 대해서 다양한 다중 정렬 기능을 다뤄봅니다.

우선 아래와 같이 책을 모델링합니다.

총 6권의 책을 낮은가격부터(가격의 오름차순),가격이 같은 경우 오래전 출판된 책부터(출판일의 오름차순) 출력하고 싶습니다.

아래와 같이 낮은 가격의 책부터 출력되도록 정렬 구현체를 작성합니다.

음수는 왼쪽책(bk0)이 먼저 등장해야 함을 나타내고 양수는 오른쪽 책(bk1)이 왼쪽 책보다 먼저 등장해야 함을 나타냅니다.

0은 우선순위가 같음을 나타냅니다.

위의 코드에서 변수 diff 자체가 이미 두 책의 가격 차이(음수, 양수, 0)의 값을 갖고 있으므로 아래와 같이 간단하게 표현할 수 있습니다(반환값이 반드시 -1, +1일 필요는 없습니다).

또는 아래와 같이 줄일 수 있습니다.

jdk의 정렬 구현체는 리스트에 존재하는 두 개의 책을 골라서 사용자가 작성한 Comparator 구현체에게 두 책의 순서를 물어봅니다. 내부 정렬 알고리즘에 따라서 우선순위를 결정할 책들이 선정되는 과정은 달라집니다만, 정렬 기능을 사용하는 입장에서는 어떤 과정을 거쳐서(구체적인 알고리즘) Comparator 구현체에 두 책 bk0, bk1 이 넘겨지는지 알 필요가 없습니다.

음수, 0, 양수를 반환해서 두 책의 우선순위만 알려주면 두 책을 위치를 바꿀지 말지는 내부 정렬 구현체가 알아서 처리합니다. 

제 컴퓨터에서는 아래와 같이 결과가 출력됩니다(정렬 알고리즘에 따라서 가격이 동일한 책들의 순서가 다를 수도 있습니다)

...더보기

Book [title=자바스크립트 활용, price=23000, pubDate=2015-06-21]
Book [title=파이썬기초, price=28000, pubDate=2014-04-21]
Book [title=css 어려워!, price=34000, pubDate=2018-08-05]
Book [title=자바입문서, price=34000, pubDate=2016-02-11]
Book [title=C언어 활용, price=38000, pubDate=2019-03-01]
Book [title=디자인패턴 입문, price=38000, pubDate=2018-10-02]

가격의 오름차순으로 정렬되었으나 동일한 가격의 책들이 오래된 책부터 출력되지는 않습니다.

따라서 Comparator 구현체를 보완해서 가격이 같은 경우 출판일의 오름차순으로 우선순위를 부여합니다.

두 책의 가격 차이가 0이 아니면 이미 우선순위가 결정되었으므로 반환합니다.

책의 가격이 같다면(diff == 0) 출판일을 한번 더 비교합니다.

여기서 출판일은 문자열(String)로 나타내고 있는데, String은 그 자체로 java.lang.Comparable 인터페이스를 구현하고 있으므로 String의 우선순위 비교 기능을 이용해서 출판일의 오름차순을 쉽게 작성할 수 있습니다.

아래 코드는 JDK에 포함된 String.java 입니다(Comparable<String> 으로 구현되어있음)

개선하기1

가격의 오름차순, 출판일의 오름차순으로 다중 정렬을 손쉽게 구현했습니다.

하지만 정렬 기준은 상황에 따라서 달라질 가능성이 높습니다.

가격(내림)-출판일(내림) 또는 출판일(오름)-가격(내림)-저자명(오름) 등으로 정렬 기준이 바뀌면 위처럼 하나의 구현체에 모든 정렬 기준들을 판단하는 구현은 빈번한 코드 수정을 초래합니다. 사용자로부터 정렬 기준을 그때그때 받아들여서 결과를 반환해줘야 한다면 위와같은 정적인 Comparator 구현은 아무 쓸모가 없습니다.

Composite Pattern

이렇게 정렬 기준들이 다양하게 변할 경우 약간의 설계(디자인 패턴)를 도입해서 Comparator 구현체를 유연하게 만들 수 있습니다.

우선 아래와 같이 출판일, 가격, 제목 등의 단일 속성에 대한 정렬 구현체를 각각 작성합니다.

그리고 위와같은 속성별 Comparator를 모두 보관하고, 책들의 우선순위를 종합적으로 비교하는 구현체를 준비합니다.

이 CompositeComp 구현체는 속성별 Comparator와 달리 자신이 직접 책들을 비교하지 않습니다. 대신에 각각의 속성을 비교하는 Comparator 구현체에게 우선순위 결정을 위임하고, 결과값이 0인 경우(즉, 특정 속성의 우선순위가 같은 경우)에만 그다음 Comparator 에게 우선순위를 물어봅니다.

이렇게 모든 Comparator 들에게 우선순위를 위임한 후에도(for문 종료 후) 주어진 두 책의 종합적인 순서가 똑같으면 0을 반환해서 주어진 두 책이 우선순위가 똑같다고 알려줍니다.

그리고 아래와 같이 원하는 Comparator를 생성자 메소드로 넘겨서 사용합니다.

(출판년도만을 비교하기 위해서 Book 클래스에 연도만 반환하는 메소드를 추가했음)

비교에 사용할 속성들을 CompositeComp 생성자의 인자로 넘기면 다중 정렬 기능을 좀 더 유용하게 활용할 수 있습니다.

개선하기2

현재는 각각의 속성에 대해서 오름차순만 구현되어 있습니다. 속성마다 오름차순, 내림차순을 지정해서 사용하고 싶습니다.

CompositeComp 구현체 안에 PropCompartor라는 내부 클래스를 정의합니다.

이 클래스는 생성자 메소드로 넘어온 Comparator 구현체의 정렬 방향을 반대로 바꿔주기 위해서 사용됩니다. 주어진 Comparator가 오름차순이라면 order에 -1을 지정해서 내림차순으로 변경되도록 합니다. 이 클래스는 CompositeComp 클래스 안에서만 사용할 목적으로 만들었기때문에 visibility를 private으로 설정해서 다른 클래스들이 사용하지 못하게 합니다.

order 를 -1로 지정한다는 것은 상세한 내부 구현이기때문에 외부에서는 이런 잡다한 내용을 알 필요 없이 편하게 사용하도록 도우미 메소드를 만들어둡니다.

이제 내림차순 효과를 주고 싶은 개별 속성 Comparator를 등록할때 desc(...) 메소드로 감싸줍니다.

Posted by yeori
,