
자바 Comparable과 Comparator 차이는 인터페이스 이름 두 개를 외우는 문제가 아닙니다. 정렬 기준을 객체 안에 기본 질서로 둘지, 화면과 요구사항에 따라 바깥에서 주입할지를 구분하는 문제에 더 가깝습니다.
관리자 화면은 가입일 최신순, 사용자 목록은 이름순, 랭킹 페이지는 점수순을 원할 때가 많습니다. 이런 장면을 먼저 떠올리면 Comparable과 Comparator의 차이도 훨씬 덜 추상적으로 보입니다.
이번 글은 정의를 기계적으로 나열하지 않고, 언제 Comparable을 구현하고 언제 Comparator로 빼는 게 더 자연스러운지를 실무 판단 기준으로 정리합니다.
정렬 장면부터 보면 왜 두 인터페이스가 갈리는지 보인다
예를 들어 Student 목록이 있다고 해보겠습니다. 시험 성적표 화면에서는 점수 높은 순이 자연스럽고, 출석부 화면에서는 이름순이 더 읽기 쉽고, 최근 가입자 화면에서는 가입일 최신순이 필요합니다.
셋 다 맞는 정렬이지만, 셋 중 하나만 Student의 기본 순서라고 말하기는 어렵습니다. 이런 타입에 기본 정렬을 클래스 안에 고정해버리면, 나중에 다른 화면이 생길 때마다 어색함이 남습니다.
반대로 Version처럼 버전 번호의 크기 비교가 거의 합의되는 값은 이야기가 조금 다릅니다. 이런 경우는 자연 순서가 비교적 분명하므로 Comparable이 잘 맞습니다.
즉 차이는 문법보다 먼저 정렬 기준이 하나로 수렴하는 타입인가, 상황마다 바뀌는가에서 갈립니다.
Comparable은 natural ordering을 클래스 안에 둔다
자바 공식 문서에서 Comparable은 클래스의 natural ordering을 정의하는 인터페이스로 설명합니다. 쉽게 말해, 이 객체는 보통 이렇게 정렬하면 된다고 말할 수 있는 기본 순서를 클래스 내부에 넣는 방식입니다.
날짜, 시간, 숫자, 버전처럼 기본 비교 감각이 비교적 분명한 타입은 Comparable이 자연스럽습니다. 별도 기준을 넘기지 않아도 정렬이 되는 점도 장점입니다.
public class Version implements Comparable<Version> {
private final int major;
private final int minor;
public Version(int major, int minor) {
this.major = major;
this.minor = minor;
}
@Override
public int compareTo(Version other) {
int majorCompare = Integer.compare(this.major, other.major);
if (majorCompare != 0) {
return majorCompare;
}
return Integer.compare(this.minor, other.minor);
}
}List<Version> versions = new ArrayList<>(List.of(
new Version(1, 2),
new Version(2, 0),
new Version(1, 10)
));
Collections.sort(versions);Comparable이 잘 맞는 장면은 대체로 이렇습니다.
- 이 타입의 기본 순서를 여러 곳에서 반복해서 쓴다
- 그 기본 순서를 대부분의 개발자가 비슷하게 받아들인다
- 비교 기준이 화면 정책보다 도메인 의미와 더 가깝다
자연 순서는 편한 기본값이지, 모든 정렬 요구를 대표하는 만능 기준은 아닙니다. 이 점을 놓치면 엔티티에 억지 기본 순서를 심게 됩니다.
Comparator는 정렬 기준을 객체 바깥으로 뺀다
정렬 기준이 화면, 요구사항, 사용자 선택에 따라 바뀐다면 Comparator가 더 자연스럽습니다. Comparator는 객체 바깥에서 비교 규칙을 주입하는 방식입니다.
public class Student {
private final String name;
private final int score;
private final LocalDate joinedAt;
public Student(String name, int score, LocalDate joinedAt) {
this.name = name;
this.score = score;
this.joinedAt = joinedAt;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
public LocalDate getJoinedAt() {
return joinedAt;
}
}Comparator<Student> byName = Comparator.comparing(Student::getName);
Comparator<Student> byScoreDesc = Comparator.comparingInt(Student::getScore).reversed();
Comparator<Student> byJoinedAtDesc = Comparator.comparing(Student::getJoinedAt).reversed();
students.sort(byName);
students.sort(byScoreDesc);
students.sort(byJoinedAtDesc);이 방식의 장점은 분명합니다. 객체에 억지 기본 순서를 심지 않아도 되고, 요구사항이 늘어나도 정렬 기준만 추가하면 되며, 람다와 메서드 참조 덕분에 비교 로직도 훨씬 읽기 좋아집니다.
- 화면마다 정렬 기준이 다를 때
- 사용자 선택에 따라 정렬이 바뀔 때
- 정렬 기준을 정책으로 재사용하고 싶을 때
- null 처리나 역순 정렬 같은 규칙을 유연하게 붙이고 싶을 때
실무의 목록 정렬은 대부분 이쪽이 더 자주 맞습니다. 기본 질서 하나보다 상황별 정렬 여러 개가 더 흔하기 때문입니다.
자바 Comparable과 Comparator 차이: 언제 Comparable을 구현하고 언제 Comparator를 쓸까
판단 기준을 아주 짧게 말하면 이렇습니다. 이 타입에 기본 정렬을 하나 넣어도 나중의 내가 봤을 때 납득되는가를 먼저 묻는 것입니다.
- 버전, 날짜, 숫자처럼 기본 질서가 분명하다 -> Comparable 쪽이 자연스럽다
- 이름순도 맞고 가입일순도 맞고 점수순도 맞다 -> Comparator 쪽이 자연스럽다
- 기본 순서도 있고 특수 정렬도 많다 -> 기본만 Comparable, 나머지는 Comparator로 분리한다
엔티티에 Comparable을 쉽게 넣어버리면 클래스 자체가 특정 화면 요구를 품게 되는 경우가 많습니다. 기본 정렬이 정말 도메인 질서인지, 그냥 첫 화면 편의였는지 헷갈리면 우선 Comparator로 두는 편이 안전합니다.
Comparator.comparing과 thenComparing은 꼭 익혀둘 가치가 있다
요즘은 익명 클래스로 비교기를 길게 쓰지 않아도 됩니다. Comparator가 제공하는 헬퍼 메서드만 잘 써도 실전 정렬 코드가 훨씬 짧고 읽기 좋아집니다.
- Comparator.comparing() : Comparable 키를 뽑아서 비교
- Comparator.comparingInt(), comparingLong(), comparingDouble() : 기본형 키 비교
- thenComparing() : 2차, 3차 정렬 기준 추가
- reversed() : 역순
- nullsFirst(), nullsLast() : null 친화 정렬
- naturalOrder(), reverseOrder() : 자연 순서와 그 역순
Comparator<Student> rankingOrder = Comparator
.comparingInt(Student::getScore)
.reversed()
.thenComparing(Student::getName);Comparator<Student> byNameNullLast = Comparator.comparing(
Student::getName,
Comparator.nullsLast(String::compareTo)
);특히 숫자 필드는 subtraction 비교보다 comparingInt()나 Integer.compare() 쪽이 더 안전하고 의도도 분명합니다.
Comparable과 Comparator에서 자주 생기는 함정
1. compareTo에서 뺄셈으로 비교하기
아래처럼 짧게 쓰는 코드는 초반에는 편해 보입니다. 하지만 subtraction 비교는 오버플로우 위험이 있어 실무에서는 권장하기 어렵습니다.
@Override
public int compareTo(Member other) {
return this.age - other.age;
}@Override
public int compareTo(Member other) {
return Integer.compare(this.age, other.age);
}2. compare 결과 0의 의미를 가볍게 보기
compareTo나 compare가 0을 반환한다는 건 정렬 관점에서 두 객체를 같은 위치로 본다는 뜻입니다. 이게 특히 위험한 곳이 TreeSet과 TreeMap입니다.
Comparator<Student> byNameOnly = Comparator.comparing(Student::getName);
Set<Student> set = new TreeSet<>(byNameOnly);이 경우 이름만 같으면 점수나 ID가 달라도 같은 원소처럼 취급될 수 있습니다. 자바 공식 문서도 equals와 일관되지 않은 ordering이 sorted set/map에서 이상하게 보일 수 있다고 경고합니다.
3. equals와 ordering의 관계를 무시하기
equals는 동등성 규칙이고, Comparable과 Comparator는 정렬 규칙입니다. 둘이 항상 완전히 같아야 하는 것은 아니지만, 정렬 기반 컬렉션에서는 차이가 버그처럼 체감될 수 있습니다.
정렬 기준이 식별과 연결된 컬렉션에 들어갈 예정이라면, compare 결과 0이 무엇을 의미하는지 먼저 분명히 해야 합니다.
4. 도메인 의미 없는 Comparable 남발
User, Order, Article 같은 엔티티는 기본 정렬이 하나로 고정되지 않는 경우가 많습니다. 그런데도 습관처럼 Comparable을 구현해두면, 나중에 왜 기본이 가입일순인지 같은 질문이 생깁니다. 이건 문법 문제가 아니라 설계 문제에 가깝습니다.
실무에서는 이렇게 기억하면 편하다
- 이 타입에 대부분이 동의할 기본 순서가 있다 -> Comparable
- 정렬 기준이 상황마다 바뀐다 -> Comparator
- 기본 순서도 있고 예외도 많다 -> 기본은 Comparable, 나머지는 Comparator
- compare 결과 0의 의미가 애매하다 -> 구현을 다시 보거나 외부 Comparator로 분리
정렬은 오름차순과 내림차순 문법을 아는 문제로 끝나지 않습니다. 결국 이 기준이 객체의 본질인지, 아니면 사용 맥락인지 구분하는 설계 감각과 연결됩니다.
마무리
자바 Comparable과 Comparator 차이는 인터페이스 두 개의 정의를 외우는 문제가 아닙니다. 질문은 하나입니다. 정렬 기준을 객체 안의 기본 질서로 넣을 것인지, 객체 바깥의 요구사항으로 둘 것인지 결정하는 것입니다.
관련해서 컬렉션 선택 기준을 함께 보고 싶다면 자바 HashMap과 TreeMap 차이, 자바 ArrayList와 LinkedList 차이, 자바 String, StringBuilder, StringBuffer 차이 글도 이어서 읽어보면 좋습니다.
공식 기준을 더 정확히 보고 싶다면 Comparable API 문서, Comparator API 문서, List.sort 문서를 함께 확인해보면 좋습니다.