3 분 소요

 

Comparator는 정렬이 필요한 곳에서 빠지지 않는 녀석입니다. 코틀린에서는 sortWith() 함수의 인수로, PriorityQueue와 같이 정렬의 기준이 필요한 자료형의 parameter로 사용됩니다. 이 Comparator는 자바에서도 있었던 인터페이스로, 그 사용 방법을 코틀린은 물려받았습니다. 하지만 여기서 멈추지 않고, 코틀린은 함수형 패러다임에 맞는 편리한 방법을 제공합니다. 이번 포스팅은 comparator를 생성하는 전통적인 방식(자바로부터 물려받은)과 코틀린만의 새로운 방식을 소개하고, 참고 사항으로, 비교하면 빠질 수 없는 comparable도 간단히 소개합니다.

Comparator 인터페이스

Comparator는 인터페이스입니다. 이 말은 comparator를 사용하기 위해서는 overriding(재정의) 해야 할 함수가 존재한다는 걸 의미합니다. 다음은 Comparator 자바 공식 문서 입니다. 다양한 함수들을 제공하는 인터페이스이지만, 이를 사용하기 위해서 우리가 재정의해야 하는 추상메서드는 compare() 하나입니다.

override fun compare(o1: T, o2: T): Int

compare 메서드는 2개의 객체를 매개변수로 하려, 정수 값을 반환합니다. 만약 반환값이 양수라면 들어온 매개변수의 순서를 변경하고([o1, o2] 으로 들어오면 [o2, o1] 로 바뀝니다.), 음수면 매개변수의 순서를 유지([o1, o2] 상태 유지)합니다. 조건문을 이용하여 두 객체를 비교하여 양수 혹은 음수를 반환하면 됩니다(구체적인 예시는 아래에서).

내가 이해한 방법 sorting에서 기본은 오름차순입니다. Arrays.sort(), Collections.sort() 같은 함수에서 알 수 있듯이 기본적으로는 오름차순으로 정렬합니다. [1, 3, 5]처럼 오름차순으로 정렬된 배열이 있을 때, 앞 요소 - 뒤 요소는 음수입니다. 즉, 두 수를 비교했을 때, 음수가 나오면 그 순서를 바꾸지 않는 것입니다.

이러한 Comparator를 구현하는 방법을 여러가지가 있습니다. 그 중 object를 이용한 singleton 방식, 람다식을 이용한 방식, 함수를 이용한 방식을 설명하겠습니다.

다음과 같이 Tree 클래스의 인스턴스를 위한 Comparator를 구현해보겠습니다.

data class Tree(
    val height: Int,
    val width: Int,
    val name: String
)

1. Singleton pattern을 이용한 방식

singleton pattern이란 소프트웨어 디자인 패턴의 하나로, 어떤 클래스의 인스턴스가 그 프로그램 내에서 단일하게 생성되도록 보장하는 패턴입니다. 즉, 어떤 인스턴스가 어떤 클래스의 단일한 인스턴스임이 보장되는 경우 singleton pattern을 따른다고 볼 수 있습니다. 어떤 클래스를 따르는 객체가 하나만 필요할 경우 사용합니다. 코틀린에서는 object 키워드를 이용하여 손쉽게 구현할 수 있습니다.

object SortTreeHeight: Comparator<Tree>{
    override fun compare(tree1: Tree, tree2: Tree): Int{
        return if(tree1.height > tree2.height)
            // 앞 객체가 큰 상황이므로, 오름차순으로 배열하기 위해서는 순서를 바꿔야 합니다. 그러므로 양수를 반환합니다.
            1
        else if(tree1.height < tree2.height)
            // 같다면 바꾸지 않아도 되기에 음수를 반환합니다. 물론 0을 반환해도 괜찮습니다.
            -1
        else // tree1.height == tree2.height
        	0
        
    }
} 

2. Lambda를 이용한 함수형 인터페이스 구현 방식

Comparator는 함수형 인터페이스입니다. 함수형 인터페이스란, 단일한 추상 메서드를 갖는 인터페이스입니다. 이 때문에 SAM(Single Abstract Method)라고도 불립니다. 이러한 인터페이스는 람다식을 이용해 간단하게 구현할 수 있습니다. 추상 메서드의 시그니처와 동일한 구성의 람다식을 이용하여 구현합니다.

val SortTreeHeightDescending = Comparator<Tree>{ tree1, tree2 ->
      	if(tree1.height > tree2.height)
            // 앞 객체가 큰 상황이므로, 내림차순으로 배열하기 위해서는 순서를 바꾸면 안됩니다. 그러므로 음수를 반환합니다.
            -1
         else if(tree1.height < tree2.height) 
            1
         else // tree1.height == tree2.height
            0
}

3. Comparator를 반환하는 함수를 이용한 방식

코틀린에는 Comparator를 반환하는 함수들이 존재합니다. 코틀린 공식 문서 다음은 코틀린의 compariosons 패키지로 Comparator 인스턴스를 생성하는 함수들을 설명합니다. 그 중, 우리가 살펴볼 함수는 compareBy()와 thenBy()입니다.


< compareBy >

fun <T> compareBy(
    vararg selectors: (T) -> Comparable<*>?
): Comparator<T>

이 함수는 인자들을 기준으로 오름차순으로 정렬하는 Comparator를 반환합니다. 이 함수는 매개변수로 가변 인자를 받기 때문에, 여러 개의 람다식을 전달할 수 있습니다. 람다식은 순서대로 처리되어 먼저 전달된 인자가 기준이 되고, 그 기준에서 순서 같을 경우 다음의 람다식이 기준이 되어 값의 순서를 판단하는 Comparator 객체를 반환합니다.


< thenBy >

inline fun <T> Comparator<T>.thenBy(
    crossinline selector: (T) -> Comparable<*>?
): Comparator<T>

이 함수는 Comparator의 확장 함수입니다. 특정 기준으로 정렬하는 Comparator에 새로운 기준을 추가하여, 여러 개의 기준으로 정렬하는 Comparator를 반환합니다. 보통 compareBy()로 생성한 Comparator에 새로운 기준을 부여하고 싶을 때 사용합니다.

compareBy(), thenBy() 모두 기본적으로 오름차순으로 정렬하는 Comparator를 반환합니다. 만약 내림차순을 하고 싶다면, compareByDescending()과 thenByDescending()을 사용하면 됩니다. 추가로, thenByComparator()는 매개변수로 Comparator 객체를 받습니다. 다음은 이들을 조합한 사용 예시입니다.

// height를 기준으로 오름차순, name을 기준으로 내림차순
val treeByHeightAscByNameDescComp = compareBy<Tree> { it.height }.thenByDescending { it.name }

// height를 기준으로 오름차순 후, width를 기준으로 오름차순
val treeByHeightWidAscComp = compareBy<Tree> (
    { it.height },
    { it.width }
)
// 작명하는 게 제일 어려운 것 같다...

Comparator 사용처

Comparator는 정렬이 필요한 대부분의 곳에서 사용이 가능하다.

번외) Comparable과의 비교

객체 정렬하면 빼놓을 수 없는 것이 하나 더 있습니다. 그것은 Comparable입니다. Comparator와 마찬가지로 Comparable 또한 인터페이스입니다. 정렬을 위한 인터페이스인 점은 같지만 다양한 점에서 차이가 있습니다.

1. 사용 이유와 구현방법

Comparable은 자연적인 순서를 부여하기 위해 사용됩니다. 반면, Comparator는 새로운 순서를 제시합니다. 보통 Comparable은 다른 클래스 내에서 구현합니다. Comparable을 구현하기 위해서는 compareTo를 overriding해야 합니다. compareTo 역시 Int를 반환합니다. 하지만 매개변수로 하나의 객체만 존재합니다.

인수를 하나만 받는데 비교를 한다는 건 이상해보입니다. 왜냐하면 비교를 위해선 최소 2개가 필요하기 때문입니다. 하지만 비교를 합니다. 다른 하나는 무엇일까요? 바로 자기 자신입니다. 인수로 받은 객체와 자기 자신을 비교하여 Int를 반환합니다. 반대로 생각하면, 타 객체에서 구현되도록 하기 위해, 1개의 인수만 받게 한 거라고 생각할 수 있습니다.

Reference

  • https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.comparisons/
  • https://kotlinlang.org/docs/fun-interfaces.html
  • https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html#compare-T-T-/
  • 댓글남기기