본문 바로가기
language/java

Java Comparable vs Comparator 완벽 이해하기

by 죄니안죄니 2026. 5. 27.
반응형

Java Comparable vs Comparator 완벽 이해하기

Java에서 정렬(Sort)을 공부하면 반드시 등장하는 핵심 개념이 바로:

Comparable vs Comparator

입니다.

특히 실무에서는:

  • 상품 가격 정렬
  • 회원 이름 정렬
  • 날짜 정렬
  • TreeSet / TreeMap
  • Stream sorted()

전부 Comparator와 연결됩니다.

초보 시절에는 보통:

Comparable = 기본 정렬
Comparator = 사용자 정의 정렬
 

정도로 외우고 넘어갑니다.

하지만 실제로는:

  • 내부 정렬 알고리즘
  • TreeMap 동작 원리
  • 정렬 안정성
  • 람다 정렬
  • 다중 조건 정렬

까지 연결됩니다.

이번 글에서는:

  • Comparable
  • Comparator
  • compareTo()
  • compare()
  • TreeSet 내부 구조
  • 람다 기반 정렬
  • 실무 정렬 전략

까지 깊게 정리해보겠습니다.


1. 왜 정렬 기준이 필요할까?

예:

List<Integer> list =
    Arrays.asList(3, 1, 2);
 

정렬:

Collections.sort(list);
 

가능.

왜냐하면:

Integer는 비교 기준 존재
 

하기 때문.


2. 객체는 어떻게 정렬할까?

예:

class User {
    String name;
    int age;
}
 

현재 JVM은:

어떻게 비교해야 하는지 모름
 

3. 그래서 필요한 것

핵심:

비교 기준 정의
 

입니다.


4. Comparable이란?

Comparable 은:

객체 자신의 기본 정렬 기준 정의 인터페이스

입니다.


5. Comparable 구조

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

핵심 메서드:

 
compareTo()
 

6. compareTo 의미

현재 객체(this)와 상대 객체(o) 비교
 

입니다.


7. 반환 규칙⭐⭐⭐⭐⭐

반환값 의미
음수 현재 객체가 더 작음
0 동일
양수 현재 객체가 더 큼
"compareTo가 음수를 반환하면 현재 객체(this)가 앞에 와야 한다" 라는 규칙을 정렬 알고리즘이 사용됨
(this-상대 : 오름차순, 상대-this 내림차순)

8. 예시

//오름차순으로 정의하고 싶다면
@Override
public int compareTo(User o) {
    return this.age - o.age;
}

//내림차순으로 정의하고 싶다면
@Override
public int compareTo(User o) {
    return o.age - this.age;
}

 

 

사용:

//나이 오름차순으로 정의한 경우
this.compareTo(o)

User a = new User(20);
User b = new User(30);

a.compareTo(b);
정렬결과
20, 30

//나이 내림차순으로 정의한 경우
User a = new User(40);
User b = new User(30);

a.compareTo(b);

정렬결과
40, 30
 
다만 실무에서는 위 같은 코드는 int범위 벗어나면 overflow발생할 수 있기 때문에 
아래처럼 사용
//오름차순
@Override
public int compareTo(User o) {
    return Integer.compare(this.age, o.age);
}

//내림차순
@Override
public int compareTo(User o) {
    return Integer.compare(o.age, this.age);
}

"compareTo를 직접 구현하는 일"은 꽤 있지만, 실무에서는 Comparator.comparing() 스타일이 가장 많이 보인다


9. compareTo (Comparable) 사용 예시

public class User implements Comparable<User> {

    private int age;

    @Override
    public int compareTo(User o) {
        return Integer.compare(this.age, o.age);
    }
}

//Comparable을 구현하면 컬렉션 내부 속성메서드를 사용할 수 있음

사용:

List<User> list = new ArrayList<>();

list.add(new User(30));
list.add(new User(10));
list.add(new User(20));

//정렬 가능
Collections.sort(users);
//또는
users.sort(null);

//정렬 과정에서 내부적으로 user1.compareTo(user2)...를 계속 호출

//정렬결과
20
30
40
 

자동으로:

compareTo()
 

호출.


10. Comparable 장점


장점 설명
기본 정렬 기준 제공 O
코드 단순 O
Collection 정렬 자동 지원 O

이 방식은

TreeSet<User>
TreeMap<User, String>
Collections.sort()
Arrays.sort()
 

등에서 사용됨.


11. Comparable 단점

문제:

정렬 기준 하나만 강하게 고정
 

됩니다.


12. 예시 문제

public class User implements Comparable<User> {

    private int age;

    @Override
    public int compareTo(User o) {
        return Integer.compare(this.age, o.age);
    }
}

//사용
Collections.sort(userList);
//또는
userList.sort(null);

현재:

나이순 정렬
 

만 구현.

그런데:

이름순 정렬
 

도 필요하면?

문제 발생.


13. 그래서 등장한 Comparator 실무에서는 훨씬 많이 사용

Comparator 는:

외부에서 비교 전략 정의하는 인터페이스

입니다.


14. Comparator 구조

 
public interface Comparator<T> {

    int compare(T o1, T o2);
}
 

핵심:

compare()
 

15. compare 의미

두 객체 비교
 

입니다.


16. 예시

 
Comparator<User> ageComparator =
    (u1, u2) -> u1.age - u2.age;
람다식은 반드시 어딘가 대입되어야 함. 우변만 있으면 불완전한 것. 
줄이지 않은 풀코드는 아래
 
Comparator<User> ageComparator =
    new Comparator<User>() {
        @Override
        public int compare(User u1, User u2) {
            return u1.getAge() - u2.getAge();
        }
    };

인터페이스로는 객체 생성 불가함. 익명클래스로 바로 객체 생성하는건 가능.

컴파일러가 구현클래스를 내부적으로 생성해서 new ComparatorImpl()한것과 비슷함.

 

이렇게 익명클래스가 람다가 가능한 이유는

추상 메서드가 하나만 있기 때문. " 함수형 인터페이스(Function Interface)"

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

대표 예시

Comparator
Runnable
Callable
Predicate
Function
Consumer
Supplier

 

(런 내용은 함수형프로그래밍을 다루는 글에서 확인할 수 있음)

 


17. 사용 예시

 
users.sort(ageComparator);
 

sort안에 넣으면 

자바는 sort()메서드 시그니처를 봄.

실제로 List인터페이스에는

void sort(Comparator<? super E> c)

가 있음. 즉, users.sort(???) 안에 Comparator를 넣어야 함

 

그래서 ageComparator를 보고 자바가 Comparator 구현체라는 것을 추론함.

실제로는 아래와 동일 

users.sort(
    new Comparator<User>() {
        @Override
        public int compare(User u1, User u2) {
            return u1.getAge() - u2.getAge();
        }
    }
);

Comparator<User> 객체생성 하나를 만들어서 넘기는데, 그 comp안에는 
Comparator가 User 객체 두 개를 받아서 age를 비교해서 돌려주는 규칙( compare(User u1, User u2) )이 들어있음. 비교로직을 가진 객체.

sort에 전달. 

users.sort(...) 를 컴파일러가 List<User>.sort(...)라고 이해함. List인터페이스 메서드 호출. 

List 인터페이스에 void sort(Comparator<? super E> c) 가 있어서 

E 는 List의 타입 파라미터. List<User>니까 E = User

void sort(Comparator<? super User> c)로 치환됨

users.sort(???) 에서 ???에는 

Comparator<User> 또는 Comparator<Object> 같은 것이 올 수 있음 <? super T>

Comparator<User>는 인터페이스. 

public interface Comparator<T> {

    int compare(T o1, T o2);

}

//T에 User를 넣으면 
public interface Comparator<User> {

    int compare(User o1, User o2);

}

즉 Comparator<User>는 user 두 개를 비교할 수 있는 규칙을 의미

 


18. Comparator 장점⭐⭐⭐⭐⭐


장점 설명
다양한 정렬 기준 가능 O
외부 전략 분리 O
런타임 정렬 변경 가능 O

19. Comparable vs Comparator 핵심 차이


항목 Comparable Comparator
위치 객체 내부 외부
메서드 compareTo compare
목적 기본 정렬 추가 정렬
전략 수(정렬기준) 보통 1개 여러 개 가능

20. 실무에서 Comparator 더 많이 쓰는 이유

실제 서비스에서는:

정렬 조건 매우 다양
 

하기 때문.

예:

  • 가격순
  • 이름순
  • 최신순
  • 인기순
public class User {

    private String name;
    private int age;
    private LocalDate joinDate;
}

//나이순
users.sort(
    Comparator.comparing(User::getAge)
);
//이름순
users.sort(
    Comparator.comparing(User::getName)
);
//가입일순
users.sort(
    Comparator.comparing(User::getJoinDate)
);

//나이 내림차순
users.sort(
    Comparator.comparing(User::getAge)
              .reversed()
);

//여러가지 정렬을 함께 하는 경우 (1차 age기준 오름차순, 2차 name기준 오름차순)
users.sort(
    Comparator.comparingInt(User::getAge)
              .thenComparing(User::getName)
);

 

* 람다식 

User::getAge 를 넣으면 

컴파일러가 

user -> user.getAge()로 바꿔서 사용.

 

람다를 더 줄인 느낌 

Comparator<User> comp =
    (u1, u2) ->
        Integer.compare(
            u1.getAge(),
            u2.getAge()
        );
        
//거의 같은 느낌
Comparator<User> comp =
    Comparator.comparingInt(
        User::getAge
    );

21. Java 8 이후 람다 등장

매우 중요.

예전:

new Comparator<User>() {

    @Override
    public int compare(User o1, User o2) {
        return o1.age - o2.age;
    }
}

//
Collections.sort(users, new Comparator<User>() {
    @Override
    public int compare(User o1, User o2) {
        return o1.getAge() - o2.getAge();
    }
});
 

22. 현대 Java 방식

 
// 1. Comparator 인터페이스 구현한 것. compare(u1, u2)의 결과를 직접 계산.
(users1, users2)
    -> users1.age - users2.age
// 설명: int범위 초과 시 오버플로우. Comparator.comparing(User::getAge)은 int -> Integer 박싱 발생



// 2. Java가 내부적으로 Comparator를 만들어 줌.
users.sort(
    Comparator.comparingInt(User::getAge)
);
//comparingInt() - Overflow 방지해서 안전, 성능도 조금 더 좋음 (오토박싱 없음. int그대로 사용. 객체생성 없음). '기준정렬Age' 가독성좋음
 

훨씬 간단.

 

Comparator.comparingInt도 Comparator생성기. 

내부 개념은 아래와 거의 같음

Comparator<User> comp =
    (u1, u2) ->
        Integer.compare(
            u1.getAge(),
            u2.getAge()
        );

반환 타입은 Comparator<User>


23. Comparator.comparing()

실무에서 매우 많이 사용.

users.sort(
    Comparator.comparing(User::getAge)
);
 

24. Method Reference 사용

 
User::getAge
 

는 람다 축약 문법

 
 

 


25. 내림차순 정렬

 
Comparator.comparing(User::getAge)
          .reversed()
 

26. 다중 조건 정렬

매우 중요.

예:

 
Comparator.comparing(User::getAge)
          .thenComparing(User::getName)
 

의미:

나이 우선
같으면 이름순
 

27. TreeSet과 Comparable

TreeSet 은 내부적으로:

정렬 기준 필요
 

합니다.


28. TreeMap도 동일

TreeMap 역시:

compare 기반 정렬
 

사용.


29. compareTo 잘못 구현 시 문제

매우 중요.

예:

return 1;
 

만 하면?


30. 문제 발생 가능

대표:

  • 정렬 깨짐
  • TreeSet 중복 이상
  • TreeMap 저장 오류

31. compareTo와 equals 관계

매우 중요.

Java 권장:

compareTo == 0 이면
equals도 true 권장
 

32. 왜 중요할까?

TreeSet은:

compareTo == 0
 

이면:

중복 객체 판단
 

가능.


33. 실무 버그 사례

예:

equals는 다른데
compareTo는 0
 

이면:

데이터 사라진 것처럼 보일 수 있음
 

34. String 비교도 Comparable

String 은:

 
Comparable<String>
 

구현 중.

즉:

 
Collections.sort(strings);
 

가능.


35. 내부 정렬 알고리즘

Java는 내부적으로:

  • TimSort
  • MergeSort 계열

등 사용.


36. 정렬 안정성(Stable Sort)

Java 정렬은 대부분:

Stable Sort
 

입니다.

즉:

동일 값의 기존 순서 유지
 

가능.


37. Comparator 실무 패턴⭐⭐⭐⭐⭐

 

null 처리

Comparator.nullsLast(...)
 

문자열 대소문자 무시

String.CASE_INSENSITIVE_ORDER
 

숫자 역순

Comparator.reverseOrder()
 

38. Stream 정렬

Java 8 이후 매우 많이 사용.

 
users.stream()
     .sorted(
         Comparator.comparing(User::getAge)
     )
 

39. 성능 관점

정렬은 기본적으로:

O(N logN)
 

입니다.

대용량 데이터는 비용 큼.


40. DB 정렬 vs Java 정렬

실무에서 매우 중요. DB 정렬을 훨씬 많이 사용함. 

대량 데이터는 보통:

100만건을 조회 후 Java에서 정렬하기 보다 DB에서 ORDER BY 해서 20건만 가져오는 식으로 설계

//SQL
DB ORDER BY 우선
//Java(JPA)
PageRequest.of(
    0,
    20,
    Sort.by("age").ascending()
);
 

권장.

메모리 정렬 비용 큼.

 

 

직접 사용할 일은 많지 않아도 compareTo를 알야 함 

TreeSet<User>

treeSet.add(user); 가 내부적으로  user.compareTo(otherUser)를 계속 호출함

PriorityQueue<User> (우선순위큐)도 compareTo() 또는 Comparator를 사용

compareTo의 반환 규칙(음수/0/양수)은 Comparator도 동일하게 사용하므로 반드시 이해하고 있어야 함.


41. 실무에서 자주 하는 실수

1) compareTo 계약 위반

TreeSet 동작 이상 가능.


2) subtraction 비교 오버플로우

안 좋은 예:

 
return a - b;
 

큰 숫자 위험.

실무에선:

 
Integer.compare(a, b)
 

추천.


3) equals와 compareTo 불일치

데이터 유실처럼 보일 수 있음.


42. 핵심 흐름 요약

Comparable
= 객체 기본 정렬

Comparator
= 외부 정렬 전략
 

43. 가장 중요한 핵심 한 줄

Comparable은 “객체 자체의 기본 정렬 기준”,
Comparator는 “외부에서 주입하는 정렬 전략”이다
 

입니다.


44. 정리

Comparable vs Comparator는 단순 정렬 문법이 아닙니다.

실제로는:

  • TreeMap/TreeSet 동작
  • Collection 정렬 전략
  • Stream API
  • 람다 표현식
  • 객체 비교 계약

전체와 연결되는 Java 핵심 개념입니다.

특히 실무에서는:

  • Comparator.comparing()
  • thenComparing()
  • compareTo 계약
  • 정렬 안정성
  • DB vs 메모리 정렬

을 정확히 이해하는 것이 매우 중요합니다.

다음 글에서는:

Iterator와 Iterable

을 foreach 내부 동작, fail-fast, ConcurrentModificationException, Stream과 차이까지 포함해서 깊게 정리해보겠습니다.

반응형

댓글