본문 바로가기

Programing/자바

[Java] Comparable vs Comparator 비교

Q. 질문

compareble은 정해진 기준으로 정렬할 때 사용하고 comparetor는 사용자가 원하는 기준을 커스텀해서 사용할 때 사용한다고 배웠는데 compareble도 리턴값만 수정하면 사용자가 원하는대로 기준값을 설정 할 수 있는 것 아닌가요?
이 둘의 차이점과 사용 용도의 차이를 잘 모르겟습니다.

인터페이스의 정의

다른 것을 찾아보기전에 각 인터페이스의 정의를 보고 넘어가자.

Comparable 인터페이스 (docs)

package java.lang;

public interface Comparable<T> {
    public int compareTo(T o);
}

 

Comparator 인터페이스 (docs)

package java.util;

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);

    boolean equals(Object obj);
}

 

유사성  및 차이점

일단 같은 점은 둘 다 인터페이스이고 비교에 대한 int 값을 반환한다는 점이다.

 

차이점은 속해있는 패키지가 다르고 비교하는 메서드의 이름 및 인자가 다르다.

패키지의 차이는 Comparable 의 사용시 import를 추가하지 않고 사용할 수 있다는 것의 의미를 넘어 java.lang 패키지에 포함되어 있다는 점에서 클래스의 public 메서드로의 의미가 강하다고 느낄 수 있다.

반면 Comparator 인터페이스는 java.util 패키지에 속해서 유틸리티성 성격(비교 알고리즘) 적인 뉘앙스를 담고 있다고 볼 수 있다.

예시: 비교

Person이라는 클래스가 있다고 하자. 식별아이디(Id), 이름(name), 나이(age)의 속성을 가지고 있다.

public class Person {
    private String id;
    private String name;
    private int age;

    // getter, setter 생략

    public Person(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Comparable 인터페이스를 구현하기 위해서는 compareTo 메서드를 구현해야 한다.

아래는 나이를 기준으로 비교하는 로직으로 구현하였다.

public class Person implements Comparable<Person> {
    // ..
    @Override
    public int compareTo(@Nonnull Person person) {
        return this.age - person.getAge();
    }
}

 

다음과 같이 비교하는 클래스를 만들 수 있다.

public class CompareTest {
    public static void main(String[] args) {
        Person hong = new Person("200729", "홍길동", 22);
        Person bang = new Person("211225", "김방자", 18);

        int result1 = hong.compareTo(bang); // 4
        System.out.println(result1);

        int result2 = bang.compareTo(hong); // -4
        System.out.println(result2);

        int result3 = hong.compareTo(hong); // 0
        System.out.println(result3);
    }
}

비교 결과가 양수이면 비교대상보다 큰 것이고, 0이면 동일하고, 음수이면 비교대상보다 작은 것이다.

hong의 나이는 22이고, bang의 경우 18이므로

hong.compareTo(bang) : 홍길동의 나이는 김방자의 나이보다 많고(4),
bang.compareTo(hong) : 방자의 나이는 홍길동보다 적고(-4),
hong.compareTo(hong) : 홍길동은 홍길동과 나이가 같다(0).

비교 대상의 경우 꼭 동일 타입일 필요는 없다.

아래와 같이 Person과 Animal의 비교도 가능하다.

public class Animal {
    private String id;
    private String name;
    private int age;

    public Animal(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    // getter, setter 생략
}
public class Person implements Comparable<Animal> {
    // ..
    @Override
    public int compareTo(@Nonnull Animal animal) {
        return this.age - animal.getAge();
    }
}
public class CompareTest {
    public static void main(String[] args) {
        Person hong = new Person("200729", "홍길동", 22);
        Animal dog = new Animal("ab1139", "진돗개", 2);

        int result = hong.compareTo(dog);
        System.out.println(result);
    }
}

반면에 Comparator 인터페이스의 경우 동일한 타입에 대해서만 비교가 가능하다.

public class PersonCompareByAge implements Comparator<Person> {
    @Override
    public int compare(Person person1, Person person2) {
        return person1.getAge() - person2.getAge();
    }
}
public class CompareTest {
    public static void main(String[] args) {
        Person hong = new Person("200729", "홍길동", 22);
        Person bang = new Person("211225", "김방자", 18);

        Comparator<Person> compareByAge = new PersonCompareByAge();
        int result1 = compareByAge.compare(hong, bang);
        System.out.println(result1);

        int result2 = compareByAge.compare(bang, hong);
        System.out.println(result2);

        int result3 = compareByAge.compare(hong, hong);
        System.out.println(result3);
    }
}

여기까지만 보면 compareTo와 compare는 별 차이가 없어보인다.

비교를 하여 실제 연산까지 하는 정렬하는 케이스를 보면 좀 더 차이를 느낄 수 있다.

예시: 정렬

만약 아래와 같이 3개의 데이터를 가지고 있다고 한다.

import java.util.List;

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<Person>(3);
        people.add(new Person("200729", "홍길동", 22));
        people.add(new Person("211225", "김방자", 18));
        people.add(new Person("020316", "아버지", 34));

        System.out.println(people);
        Collections.sort(people);
    }
}

출력 결과는 아래와 같이 입력한 순서대로 나온다.

 

[Person{id='200729', name='홍길동', age=18}, Person{id='211225', name='김방자', age=22}, Person{id='020316', name='아버지', age=34}]

정렬을 해보자

Collection (List, Set, Map 같은 컬렉션들)의 동반클래스(companion classe)인 Collections에는 리스트의 정렬을 할 수 있게 도와주는 정적 메서드가 두가지가 있다.

package java.util;

public class Collections {
    // ...
    public static <T extends Comparable<? super T>> void sort(List<T> list) {
        list.sort(null);
    }
    
    public static <T> void sort(List<T> list, Comparator<? super T> c) {
        list.sort(c);
    }
    // ...

1) 위의 것은 List 인터페이스만 입력으로 받아서 정렬을 수행한다. 타입 T에 대한 제약조건이 붙어있는데 <T extends Comparable<? super T>> 라고 되어 있다. 즉, Comparable을 상속받은 객체에 대해 수행을 할 수 있다는 의미이다.

2) 반면 아래의 sort 메서드는 리스트의 원소에 대한 제약이 사라진 대신 Comparator라는 인터페이스를 파라미터를 통해 넘겨받고 있다.

 

즉, 1번의 경우 정렬에 대한 로직을 클래스 내부에 가지고 있는 것이고 2번의 경우 정렬에 대한 로직을 외부로 부터 주입을 받는 구조라고 볼 수 있다.

 

Comparable 인터페이스 예시

만약 Comparable 인터페이스를 통해 정렬에 대한 로직을 클래스 내부에 담고자 한다면 아래와 같이 구현해 볼 수 있을 것이다.

여기서는 ID를 오름차순으로 정렬하도록 하였다. String이 가지고 있는 compareTo를 그대로 사용하였다.

public class Person implements Comparable<Person> { // +
    private String id;
    private String name;
    private int age;

    // getter, setter 생략

    public Person(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Person person) { 		// +
        return this.id.compareTo(person.id);
    }
}

정렬 후 정렬이 잘 되었는지 확인을 위한 main 함수의 수정이다.

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<Person>(3);
        people.add(new Person("200729", "홍길동", 22));
        people.add(new Person("211225", "김방자", 18));
        people.add(new Person("020316", "아버지", 34));

        System.out.println(people);
        Collections.sort(people);   // +
        System.out.println(people); // +
    }
}
[Person{id='200729', name='홍길동', age=22}, Person{id='211225', name='김방자', age=18}, Person{id='020316', name='아버지', age=34}]
[Person{id='020316', name='아버지', age=34}, Person{id='200729', name='홍길동', age=22}, Person{id='211225', name='김방자', age=18}]

윗줄이 입력된 순서의 출력이고, 아래는 ID 기준으로 정렬한 결과이다.

Comparator 인터페이스 예시

만약 ID를 오름차순이 아닌 내림차순으로 정렬을 하고 싶을때는 어떻게 할 수 있을까?

java.util 패키지의 Collections에는 reverseOrder 라는 이름의 정적 메서드가 있어서 역방향 순서의 Comparator 인터페이스를 구현한 구현체를 가지고 있다.

package java.util;

public class Collections {
    // ..
    public static <T> Comparator<T> reverseOrder() {
        return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
    }

    private static class ReverseComparator
        implements Comparator<Comparable<Object>> {

        static final ReverseComparator REVERSE_ORDER = new ReverseComparator();

        public int compare(Comparable<Object> c1, Comparable<Object> c2) {
            return c2.compareTo(c1); // c1.compareTo(c2) 대신
        }

        private Object readResolve() { return Collections.reverseOrder(); }

        @Override
        public Comparator<Comparable<Object>> reversed() {
            return Comparator.naturalOrder();
        }
    }

따라서 아래와 같이 사용하면 반대로 내림차순으로 정렬을 할 수 있다.

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<Person>(3);
        people.add(new Person("200729", "홍길동", 22));
        people.add(new Person("211225", "김방자", 18));
        people.add(new Person("020316", "아버지", 34));

        System.out.println(people);
        Collections.sort(people, Collections.reverseOrder());   // *
        System.out.println(people); // +
    }
}

만약 ID가 아닌 이름을 기준으로 정렬을 하고 싶을 때는 어떻게 할까?

이미 구현된 compareTo를 수정해야 할까?

Comparator 인터페이스를 사용하면 비교에 대한 로직을 외부에서 주입을 받을 수 있다고 했다.

public class PersonCompareByName implements Comparator<Person> {
    @Override
    public int compare(Person person1, Person person2) {
        return person1.getName().compareTo(person2.getName());
    }
}

아래와 같이 사용이 가능하다.

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(3);
        people.add(new Person("200729", "홍길동", 22));
        people.add(new Person("211225", "김방자", 18));
        people.add(new Person("020316", "아버지", 34));

        System.out.println(people);
        Collections.sort(people, new PersonCompareByName()); // 이름을 기준으로 정렬한다.
        System.out.println(people);
    }
}

이번에는 나이를 기준으로 정렬을 하고 싶다고 하면 아래와 같이 나이에 대한 Comparator 인터페이스의 구현을 제공하면 된다.

public class PersonCompareByAge implements Comparator<Person> {
    @Override
    public int compare(Person person1, Person person2) {
        return Integer.compare(person1.getAge(), person2.getAge());
    }
}

사용 코드

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(3);
        people.add(new Person("200729", "홍길동", 22));
        people.add(new Person("211225", "김방자", 18));
        people.add(new Person("020316", "아버지", 34));

        System.out.println(people);
        Collections.sort(people, new PersonCompareByAge()); // 나이로 정렬
        System.out.println(people);
    }
}

결과

[Person{id='200729', name='홍길동', age=22}, Person{id='211225', name='김방자', age=18}, Person{id='020316', name='아버지', age=34}]
[Person{id='211225', name='김방자', age=18}, Person{id='200729', name='홍길동', age=22}, Person{id='020316', name='아버지', age=34}]

로직을 사용 부분에 위치

위에서는 Comparator 인터페이스를 직접 클래스로 구현을 해서 사용을 했는데 정렬 로직이 복잡하지 않다면 다음과 같은 방법의 사용이 가능하다.

익명클래스

import java.util.Comparator;

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(3);
        people.add(new Person("200729", "홍길동", 22));
        people.add(new Person("211225", "김방자", 18));
        people.add(new Person("020316", "아버지", 34));

        System.out.println(people);
        Collections.sort(people, new Comparator<Person>() {
            @Override
            public int compare(Person person1, Person person2) {
                return person1.getName().compareTo(person2.getName());
            }
        });
        System.out.println(people);
    }
}

Java 8+ (람다)

제일 처음에 봤던 Comparator 인터페이스의 시그너처를 유심히 봤던 사람이라면 @FunctionalInterface 가 붙어있다는 것을 알 수 있다.

처음부터 있었던 것은 아니고 자바8에서 람다가 생기면서 해당 인터페이스가 함수형 인터페이스라는 것을 알려주기 위한, 또한 컴파일시 해당 규칙을 지키고 있는지 점검을 위한 annotation이다.

따라서 아래와 같이 람다로 바꿀 수 있다.

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(3);
        people.add(new Person("200729", "홍길동", 22));
        people.add(new Person("211225", "김방자", 18));
        people.add(new Person("020316", "아버지", 34));

        System.out.println(people);
        Collections.sort(people, (person1, person2) -> person1.getName().compareTo(person2.getName()));
        System.out.println(people);
    }
}

또한 메서드 레퍼런스와 Comparator에 정의된 comparing 정적메서드를 이용하면 아래와 같이 더 단순하게 바꿀 수 있다.

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(3);
        people.add(new Person("200729", "홍길동", 22));
        people.add(new Person("211225", "김방자", 18));
        people.add(new Person("020316", "아버지", 34));

        System.out.println(people);
        Collections.sort(people, Comparator.comparing(Person::getName)); // *
        System.out.println(people);
    }
}

복합방법 Comparable + Comparator

그렇다면 Comparable 인터페이스를 통한 구현은 외부에서 주입받을 수 없을까?

가능여부만 따져본다면 일단 "가능하다".

 

아래 예의 경우 생성자를 통한 비교 로직을 주입 받는 방법이다.

import java.util.Comparator;

public class Person implements Comparable<Person> {
    private String id;
    private String name;
    private int age;
    private Comparator<Person> comparator;

    // getter, setter 생략

    public Person(@Nonnull String id, String name, int age,
    				Comparator<Person> comparator) {  // +
        Preconditions.checkNotNull(id);
        this.id = id;
        this.name = name;
        this.age = age;
        this.comparator = comparator;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(@Nonnull Person person) {
        return this.comparator.compare(this, person); // *
    }
}

사용코드

import java.util.Comparator;

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(3);
        people.add(new Person("200729", "홍길동", 22, Comparator.comparing(Person::getName)));
        people.add(new Person("211225", "김방자", 18, Comparator.comparing(Person::getName)));
        people.add(new Person("020316", "아버지", 34, Comparator.comparing(Person::getName)));

        System.out.println(people);
        Collections.sort(people);
        System.out.println(people);
    }
}

실행결과 - 이름 순으로 정렬이 되었음을 알 수 있다.

[Person{id='200729', name='홍길동', age=22}, Person{id='211225', name='김방자', age=18}, Person{id='020316', name='아버지', age=34}]
[Person{id='211225', name='김방자', age=18}, Person{id='020316', name='아버지', age=34}, Person{id='200729', name='홍길동', age=22}]

하지만 매번 생성자마다 정렬 로직을 넣어주고 있어서 중복(DRY)이라는 안티패턴이 보인다.

또한 아래와 같이 일부 객체에는 (실수로) 다른 로직이 들어가면 정렬이 이상하게 된다.

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(3);
        people.add(new Person("200729", "홍길동", 22, Comparator.comparing(Person::getName)));
        people.add(new Person("211225", "김방자", 18, Comparator.comparing(Person::getName)));
        people.add(new Person("020316", "아버지", 34, Comparator.comparing(Person::getAge))); // ?

        System.out.println(people);
        Collections.sort(people);
        System.out.println(people);
    }
}
[Person{id='200729', name='홍길동', age=22}, Person{id='211225', name='김방자', age=18}, Person{id='020316', name='아버지', age=34}]
[Person{id='211225', name='김방자', age=18}, Person{id='200729', name='홍길동', age=22}, Person{id='020316', name='아버지', age=34}]

이를 막기위해 변수로 로직을 모으는 것도 가능하다. (extract to variation)

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(3);
        final Comparator<Person> comparingByName = Comparator.comparing(Person::getName);
        people.add(new Person("200729", "홍길동", 22, comparingByName));
        people.add(new Person("211225", "김방자", 18, comparingByName));
        people.add(new Person("020316", "아버지", 34, comparingByName));

        System.out.println(people);
        Collections.sort(people);
        System.out.println(people);
    }
}

하지만 정렬 로직이 여러 객체에 흩어져 있는 것은 뭔가 이상하다.

 

만약 이런 불편을 줄이고자 로직을 정적 필드에 위임해두고 하는 것을 생각할 수도 있을 것이다.

import java.util.Comparator;

public class Person implements Comparable<Person> {
    private static Comparator<Person> comparator = Comparator.comparing(Person::getId);
    
    private String id;
    private String name;
    private int age;

    public static void setComparator(Comparator<Person> comparator) {
        Person.comparator = comparator;
    }

    // getter, setter 생략

    public Person(@Nonnull String id, String name, int age) {
        Preconditions.checkNotNull(id);
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(@Nonnull Person person) {
        return Person.comparator.compare(this, person);
    }
}

기본적으로는 ID 비교가 된다.

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(3);
        people.add(new Person("200729", "홍길동", 22));
        people.add(new Person("211225", "김방자", 18));
        people.add(new Person("020316", "아버지", 34));

        System.out.println(people);
        Collections.sort(people);
        System.out.println(people);
        Person.setComparator(Comparator.comparing(Person::getName)); // +
        Collections.sort(people);
        System.out.println(people);
    }
}

결과

[Person{id='200729', name='홍길동', age=22}, Person{id='211225', name='김방자', age=18}, Person{id='020316', name='아버지', age=34}]
[Person{id='020316', name='아버지', age=34}, Person{id='200729', name='홍길동', age=22}, Person{id='211225', name='김방자', age=18}]
[Person{id='211225', name='김방자', age=18}, Person{id='020316', name='아버지', age=34}, Person{id='200729', name='홍길동', age=22}]

로직도 전역적으로 하나만 가지고 있으므로 문제가 해결된 것으로 볼 수 있겠지만 이 방법은 멀티스레드 환경에서는 문제의 소지가 많다.

하나의 스레드에서 compareTo를 수행하고 있을 때 다른 스레드에서 setComparator를 호출해서 Person.comparator를 바꾸어버리면 동작이 이상하게 될 수 있다.

처음에는 ID로 비교하다가 중간부터 이름으로 비교가 될 수도 있는 것이다.

 

결국 멀티스레드 환경이 일상이 된 요즘에 이러한 방법을 사용하지 않는 것이 좋다.

요약

Comparable 인터페이스는 다른 객체와의 비교를 위해 사용되는 메서드를 노출한다.

int compareTo(T other);

이름 역시 compareTo 로 a.compareTo(b) 식으로 쉽게 이해할 수 있다.

클래스 내부에 접근을 할 수 있으므로 getter와 같이 외부에 데이터를 노출하지 않고도 로직을 만들 수 있는 장점이 있다.

(위의 예에서는 getter가 아닌 this.id 나 person.id 등으로 직접 프로퍼티에 접근했다.)

Comparable 인터페이스는 객체에 직접 구현을 하기 때문에 참조 데이터 타입이 아닌 기본 데이터 타입에는 적용이 불가능하다.

위의 예에서 String 타입의 멤버변수의 경우 this.id.compareTo(person.id) 처럼 해당 타입에 미리 구현된 compareTo를 호출해서 사용이 가능했지만, age와 같이 int 타입의 경우 메서드 자체가 없으므로 Comparator 인터페이스의 시그너처와 유사한 Integer.compare(int x, int y) 를 통해서 비교를 했다.

 

Comparator 인터페이스는 객체 자체가 아닌 비교 로직을 외부에 제3의 객체에 위치할 수 있게 해준다.

그래서 시그너처역시 두 객체를 받아 들이게 되어 있다.

int compare(T o1, T o2);

하지만 이 방법의 한계는 해당 객체가 외부에 비교를 할 수 있는 값에 대한 접근을 노출해야 한다. (예. getter 등)

만약 비교 로직을 만들 대상 클래스가 jar를 통해 라이브러리의 형태로 제공을 받는 경우 클래스의 직접적인 수정이 불가능하다.

이런 경우 프로퍼티에 대해 접근할 적절한 노출이 되어 있다면 Comparator 인터페이스를 통해 비교 구현이 가능하다.

또한, 이 인터페이스는 함수형 인터페이스이므로 자바8에 추가된 람다와 더불어 사용하면 표현을 간결하게 할 수 있다는 장점이 있다.


NG or More+

List.of 는 사용불가

처음에 리스트를 만들었을 때는 Java 9에 추가된 List.of를 사용했다.

import java.util.List;

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = List.of( // List.of는 Java 9 이상이 필요..
                new Person("200729", "홍길동", 22),
                new Person("211225", "김방자", 18),
                new Person("020316", "아버지", 34));

        System.out.println(people);
    }
}

하지만 이렇게 하면 동작하지 않는데 List.of에서 돌려준 List의 구현체(java.util.ImmutableCollections$AbstractImmutableList)는 불변(Immutable)이기 때문이다.

이미 있는 자바의 sort 인터페이스를 없앨 수가 없기에 내부의 구현체의 sort 메서드는 예외를 던지게 구현되어 있다.

class ImmutableCollections {
        static abstract class AbstractImmutableList<E> extends AbstractImmutableCollection<E>
            implements List<E>, RandomAccess {
        // ..
        @Override
        public void sort(Comparator<? super E> c) {
          throw new UnsupportedOperationException();
        }

Immutable(불변) vs Mutable(가변)

위에서는 List 자체의 값을 바꾸기 위해 Mutable한 ArrayList를 구현체로 사용했다.

하지만 동시성 프로그래밍이 대세가 되면서 이렇게 연산의 결과 객체 자체를 바꾸는 현상은 안티패턴처럼 여겨지게 된다.

자바에서도 Mutable한 Date 클래스 대신 Immutable한 LocalDateTime 같은 타입을 권장하는 것도 이러한 맥락일 것이다.

 

아래와 같이 새로운 정렬된 리스트를 만들어내면 List.of로 불변 리스트를 만들어도 사용이 가능하게 된다.

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class CompareTest {
    public static void main(String[] args) {
        List<Person> people = List.of( // List.of는 Java 9 이상이 필요..
                new Person("200729", "홍길동", 22),
                new Person("211225", "김방자", 18),
                new Person("020316", "아버지", 34));

        System.out.println(people);
        List<Person> peopleSortedByName = people.stream()
                .sorted(Comparator.comparing(Person::getName))
                .collect(Collectors.toList());
        System.out.println(peopleSortedByName);
    }
}

수행결과

[Person{id='200729', name='홍길동', age=22}, Person{id='211225', name='김방자', age=18}, Person{id='020316', name='아버지', age=34}]
[Person{id='211225', name='김방자', age=18}, Person{id='020316', name='아버지', age=34}, Person{id='200729', name='홍길동', age=22}]

NPE 방지

위에서 compareTo 구현을 단순히 id의 compareTo를 이용했는데 이 방법은 id의 값이 null 이거나 파라미터로 넘어온 person이 null일 경우 NullPointerException 가 발생할 수 있다.

public class Person implements Comparable<Person> { // +
    private String id;
    private String name;
    private int age;

    // getter, setter 생략

    public Person(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Person person) { 		// +
        return this.id.compareTo(person.id);
    }
}

따라서 방어해주는 코드를 아래와 같이 넣어주거나 id에는 null을 입력할 수 없다는 계약을 추가하는 것이 바람직하다.

1) 방어 코드 추가

@Override
public int compareTo(Person person) {
    if (Objects.isNull(id)) {
        return 1;  // 뒤로 보낸다.
    }
    if (Objects.isNull(person) || Objects.isNull(person.id)) {
        return -1;  // 앞으로 보낸다.
    }
    return this.id.compareTo(person.id);
}

 

2) 계약에 의한 책임

javax.annotaion에 있는 Nonnull 애너테이션을 붙여서 해당 파라미터가 null 입력을 받을 수 없다는 것을 코드상 표현을 하였다.

추가로 Guava의 Preconditions 을 이용해서 파라미터에 대한 validation을 수행하도록 처리하였다.

import com.google.common.base.Preconditions;
import javax.annotation.Nonnull;

public class Person implements Comparable<Person> {
    private String id;
    private String name;
    private int age;

    // getter, setter 생략

    public Person(@Nonnull String id, String name, int age) {
        Preconditions.checkNotNull(id);
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(@Nonnull Person person) {
        Preconditions.checkNotNull(person);
        return this.id.compareTo(person.id);
    }
}

 

이렇게 하면 ID가 null이 되는 것을 사용하는 클라이언트에게 책임을 넘기는 것이 되고 클라이언트가 null을 넣으려고 할 때 예외가 발생하게 된다. 또한 IDE에서는 이 애너테이션을 인식해서 잘못 사용하고 있다고 개발자에게 알려주기도 한다.

Passing 'null' argument to parameter annotated as @NotNull

이것은 Person 클래스 뿐만 아니라 PersonCompareByName 역시 동일하다.