자바 코드를 작성하다 보면 가끔 Arrays 의 sort 혹은 stream 의 sorted 메소드를 사용할 일이 생긴다.
해당 메소드의 입력 파라미터를 살펴보면 모든 형태의(primitive + reference) 객체를 받는다는 것을 알 수 있는데, 우리가 여기서 주목할 것은 Comparator 를 넘겨 받는 메소드이다.
우선 Comparator 란 무엇일까?
우선 Comparator 자체는 interface 이고, interface 에서 역할 관점에서 Comparator 를 바라보면 왠지 비교를 수행하는 책임을 부여할 것 같다고 생각할 수 있다.
실제로 interface 내부를 살펴보면
compare 과 equals 메소드를 구현할 책임을 부여하는데, 실제로 객체를 생성할 때는 모든 객체가 Object class 를 상속받고 있기 때문에, compare 메소드만 구현해주면 된다.
이 외에 thenComparing 같은 Comparator Chaining 을 구현해주는 메소드도 보인다.
다시 돌아와서 Comparator 가 "비교를 수행하는 객체" 이고, 비교를 수행하기 위해서는 객체 쌍이 필요하기 때문에 두 개의 객체를 넘겨 받는 것을 알 수 있다.
임의의 객체 내부에서 비교를 수행하고 싶은데, 그럴 때 마다 객체 내부에 compare 메소드를 추가하는 것은 그 비용이 너무나도 크기 때문에, Comparator 라는 유용한 비교 객체정보를 넘겨받아서 비교 역할을 위임하는 전략을 사용하는 듯 하다.
다시 돌아와서 Comparator 의 compare 메소드는 개발자의 입맛대로 작성하는 것이 가능한데, 내부에 약속된 문구를 살펴보면 o1 이 o2 보다 크거나, 같거나, 작을 때 각각 양수, 0, 음수 를 반환한다 라고 나와있다.
Compares its two arguments for order. Returns a negative integer,
zero, or a positive integer as the first argument is less than, equal
to, or greater than the second.
실제로 Comparator 구현체가 위와 같은 약속을 따라야지만 기존에 구현된 sort, sorted 메소드의 결과가 예측한 바와 같을 것이다.
즉, 어떻게 compare 메소드를 구현할지는 자유지만 대소 비교는 약속된 규칙에 따라서 값을 반환해야하는 것을 알 수 있다.
보편적으로 compare 의 결괏값이 큰 것이 iterator 의 가장 마지막 index 에 온다.
간혹 Comparable 이라는 interface 도 보이는데, 이것은 어떤 책임을 부여할까?
Comparable 과 Comparator 를 이름이 비슷하다는 이유로 헷갈려 하거나, 비슷한 interface 로 보는 경우가 있는데, 개인적으로 역할의 관점에서 큰 차이를 보인다고 생각한다.
Comparable 구현체는 해당 객체가 말그대로 Comparable 하다는 것을 의미한다.
interface 내부에는 compareTo 메소드 단 하나만이 존재하며, 구현체는 해당 역할만 수행한다면 Comparable 하다는 것을 의미할 수 있다.
Comparable 객체는 특정 객체와 비교할 수 있다
왜 특정 객체라고 물어본다면 compareTo 메소드를 살펴보면 알 수 있다. Comparable 의 제네릭 타입 T 객체를 받는데, 이는 Comparable 객체가 구현체와 다른 객체와도 비교가 가능함을 알 수 있다.
반대로 Comparator 는 Comparator 제네릭 타입의 두 객체를 비교하는 역할을 부여한다. 물론 comparing 메소드를 사용하면 다른 객체와 비교 가능 하지만 interface 가 부여하는 책임을 봤을 때의 이야기를 말하고싶다.
아직 큰 차이를 모르겠다면 다음의 질문에 답해보자
학생 점수 데이터를 가지고 있어요, 근데 어떻게 석차를 내야할지 모르겠어요
-> 이와 같은 경우에는 비교를 수행해줄 객체가 필요하다. 즉 Comparator 를 넘겨준다면 학생 별로 누가 큰지(높은 순위)를 가려서 반환해준다.
팀을 모집할건데 팀 내 인원들은 저희가 팀 내 인원들의 기량을 비교할 방법을 주셔야합니다.
-> 이 같은 경우에는 팀원 객체가 비교 가능 해야지만 모집 요구 사항을 만족시킬 수 있다.
사실 이 글을 쓰게 된 이유도 TreeSet 과 TreeMap 객체를 사용하다가 정리할 필요성을 느꼈기 때문인데, 이 Comparator 와 Comparable 구현체를 데이터 구조를 짜는 단계부터 적극적으로 쓰고 있기 때문이다.
기본적으로 Set 과 Map 의 Tree 구조는
Set 과 Map 으로 저장된 데이터를 일련의 규칙으로 Tree 구조를 형성하는 것이다.
여기서 일련의 규칙이란 Key 혹은 Value 값을 비교해서 오름차순 혹은 내림차순 구조를 만들어 내는 것이다.
비교 작업이 수행되기 위해서는 Comparator 를 받아서 역할을 위임하거나 Comparable 한 객체만을 받는 방법을 생각할 수 있고, 실제로도 그렇게 구현돼있다.
가령 TreeSet/Map 인스턴스를 만들 때 별도로 Comparator 를 제공하지 않고, Non-Comparable 한 객체를 받는다 라고 선언한다면 바로 예외가 발생한다.
내부적으로 입력받은 객체를 Comparable 로 casting 해서 compare 을 수행하는데 타입 캐스팅이 실패했다는 것(Non-Comparable 객체이기 때문에)을 알려준다
이 부분도 Set 내부를 살펴보다가 안 것인데, 우리가 아는 Set 에 넣는 Value 는 사실 Map<Value, Object> 구조로 저장된다는 것이다.
결국 중복검사도 Hashing 과정을 거치거나, 이와 유사한 같은 값이 존재한다는 것을 판별할 로직이 필요한건 Set 이나 Map 이나 마찬가지기 때문에, 조금 더 조건이 붙은 Set 을 Map 으로 구현하는 것은 쉽게 납득이 되었다.
아무튼 이 Tree 구조를 형성할 때 비교 가 필요한 형태는 Comparator 를 제공받거나, Comparable 한 객체만을 취급하거나 둘 중 하나의 선택을 해야하는 것을 알 수 있다.