우아한테크코스 레벨1, 자동차 경주 미션 중입니다!
private int position 값을 getter로 꺼내지 않고,
List<Car> 중 우승자를 가려내기 위해 Comparable과 Comparator를 알아보게 되었습니다!
Comparable, Comperator 왜 필요할까요?! ✍️
이미 정해져있는 규칙, 개발자가 만들어야 하는 규칙.
정렬의 사전적 정의는 데이터를 특정한 조건에 따라 일정한 순서가 되도록 다시 배열하는 일 입니다.
즉, 정렬에는 정렬 대상과 정렬 조건 두 가지가 필수적으로 필요합니다.
아래 코드를 살펴볼까요?
class Car {
private final String name;
private int position;
public Car(final String name, final int position) {
this.name = name;
this.position = position;
}
public String getName() {
return name;
}
public int getPosition() {
return position;
}
@Override
public String toString() {
return String.format("%s : %d", this.name, this.position);
}
}
@Test
@DisplayName("정렬 기준 전달하지 않고 문자열을 담은 리스트 정렬하기")
void sortStringsWithoutSortingStandard() {
// given
List<String> strings = Lists.newArrayList("abc", "def", "ghi");
// when
List<String> sortedStrings = strings.stream()
.sorted()
.collect(Collectors.toList());
// then
assertThat(sortedStrings.get(0)).isEqualTo("abc");
}
@Test
@DisplayName("정렬 기준 전달하지 않고 자동차 객체를 담은 리스트 정렬하기")
void sortCarsWithoutSortingStandard() {
// given
List<Car> cars = Lists.newArrayList(
new Car("abc", 3),
new Car("Def", 3),
new Car("ghi", 3)
);
// when
List<Car> sortedCars = cars.stream()
.sorted()
.collect(Collectors.toList());
// then
assertThat(sortedCars.get(0).getName()).isEqualTo("Def");
}
위에 작성된 List<String>은 테스트를 통과하지만,
아래 작성된 List<Car>는 테스트를 통과하지 못합니다.
String에는 정렬 기준이 있지만, Car에는 정렬 기준이 없기 때문입니다.
문자열은 모두가 알고 있는 알파벳 순 정렬 기준이 있습니다.
String에는 기본 정렬 기준이 있는 것입니다.
반면, 제가 정의한 Car 클래스의 정렬 기준은 제가 정의하기 전까지 누구도 알 수 없습니다.
마치 개발자 정의 객체의 동등성 비교를 위해 equals(), hashCode() 를 오버라이드 해야하듯이,
개발자 정의 객체를 정렬하기 위해서는, 개발자가 정렬 기준을 작성해서 전달해야 합니다.
이때 사용되는 것이 Comparable, Comparator 입니다
Comparable - 객체에게 스스로 비교할 수 있는 힘을! 💪
객체에 Comparable 인터페이스를 구현함으로써 기본 정렬 기준을 내장시킬 수 있습니다.
String 클래스는 Comparable 인터페이스를 구현하고 compareTo() 메소드를 오버라이드합니다.
그 결과, strings.stream().sorted() 가 호출했을 때, compareTo 메소드가 정렬 기준 역할을 하며 비교가 수행됩니다.
그러면 Car 클래스도 Comparable 인터페이스를 구현해서 정렬이 가능하게 해볼까요?
class Car implements Comparable<Car> {
...
@Override
public int compareTo(Car competitor) {
return this.name.compareTo(competitor.name);
}
}
위 코드를 적용하고 나면, 앞서 실패했던 테스트가 통과합니다.
이제는 그냥 Car 가 아니라, Comparable Car 가 되었기 때문입니다.
비교할 줄 아는 자동차!
자기 스스로, 자신과 같은 자료형을 만나서 비교를 할 수 있는 객체가 된 것입니다.
달리 말하면 String 클래스가 그렇듯이, Car 자료형도 자신만의 기본 정렬 기준이 생긴 것입니다.
이러한 기본 정렬 기준을 natural ordering 이라고도 합니다.
Comparable 의 단점 - 기준이 변한다면.. 😰
String의 알파벳 오름차순처럼, Car도 변하지 않는 기준을 가질 수 있을까?
위 예시에서는 자동차의 String name 필드를 알파벳 오름차순을 기준으로 정렬했습니다.
이러한 기준을 자동차 클래스에 Comparable을 구현함으로써 스스로 비교가능하도록 했습니다.
그런데 만약, int position 을 기준으로 정렬하고 싶다면 어떻게 해야 할까요?
아니면 알파벳 역순으로 정렬해야 한다면? 대소문자 구분없이 정렬해야 한다면요? 😥
정렬 기준이 바뀔 때마다 compareTo 구현부를 수정할 순 없을 겁니다.
뿐만 아니라 도메인이 되는 클래스에 대한 수정은 최대한 자제해야 합니다.
저의 리뷰어였던 범블비(ddaaac)의 코멘트를 첨부합니다.
다른 개발자들이 Car implements Comparable<Car> 만 보고도,
어떤 기준으로 정렬될지 알 수 있을까요? 그리고 이 기준이 바뀌지 않을 거라 장담할 수 있을까요?
Comparator - 나에게 두 객체를 맡겨줘! 내가 비교할게! 🚦
Comparator 를 이용하면 도메인 클래스에 수정을 가하지 않고도 정렬 기준을 필요에 따라 바꿀 수 있습니다.
이름에서 알 수 있듯이, Comparator는 우리가 정렬하고자 하는 클래스가 아닌 제3의 클래스입니다.
두 객체를 전달 받아 내부에 구현된 기준에 따라 비교하고 위치를 바꿔줌으로써 정렬이 가능케 합니다.
Comparator<Car> carNameComparator = new Comparator<>() {
@Override
public int compare(Car car1, Car car2) {
return car1.getName().compareTo(car2.getName()) * -1;
}
};
cars.sort(carNameComparator);
List.sort 메소드에는 정렬 기준을 정의한 Comparator를 전달합니다.
이때, 객체 내부에 구현되어 있는 Comparable은 Comparator에 의해 오버라이드 됩니다.
또한 sort 메소드에 파라미터로 null을 전달할 경우, 객체 내부에 구현된 Comparable 을 이용해 정렬합니다.
단, 이 경우 Comparable이 구현되어 있지 않은 경우 맨 위 코드처럼 오류가 발생합니다.
Comparator는 도메인 클래스에 정렬을 위한 소스코드를 추가하지 않아도 되는 장점을 가집니다.
또한 런타임에 동적으로 정렬 기준을 할당하여 정렬 기준이 변경되더라도 수정을 최소화할 수 있습니다.
자주 쓰이는 Arrays.sort, Collections.sort 의 경우,
두번째 파라미터로 comparator를 받는 메소드가 오버로딩 되어있습니다.
따라서 natural ordering 으로 정렬하기 원할 경우, 정렬 대상만 전달하면 되고,
정렬 기준을 동적으로 할당하고 싶을 경우, 두번째 파라미터로 Comparator를 전달하면 됩니다.
사용 예시 🚘
사용 방법이 정말 다양다양 합니다 🤣
cars.sort(null); // Comparable 구현부 사용하기
cars.sort(Comparator.naturalOrder()); // Comparable 구현부 사용하기
cars.sort(Comparator.reverseOrder()); // Comparable 구현부 역순으로 사용하기
cars.sort(Comparator.comparing(Car::getPosition)); // position 으로 오름차순
// null 먼저, 이후 이름 알파벳 오름차순
cars.sort(
Comparator.nullsFirst(
Comparator.comparing(Car::getName)));
// null 마지막에, position 내림차순
cars.sort(
Comparator.nullsLast(
Comparator.comparingInt(Car::getPosition).reversed()));
// position 내림차순, position 같을 경우 이름으로 오름차순
cars.stream()
.sorted(Comparator.comparingInt(Car::getPosition).reversed()
.thenComparing(Car::getName))
.collect(Collectors.toList());
드디어, Comparable vs Comparator 그리고 기타.. ✨
트레이드오프와 의도를 이해하고 사용해보아요! 💁
Comparable 은 객체에게 기본 정렬 기능을 부여하기 위해 사용됩니다. (natural ordering)
Comparable 을 구현한 클래스는, compareTo를 오버라이드하여 전달받은 비교대상과 비교합니다.
Comparable 은 java.lang 패키지에 있어서 import 가 필요하지 않습니다.
Comparable 은 정렬해야 하는 클래스 소스코드에 수정이 필요합니다.
Comparator 는 전달 받은 두 객체를 비교합니다.
Comparator 는 compare를 오버라이드하여 두 대상을 비교합니다.
Comparator 는 java.util 패키지에 있어서 import 가 필요합니다.
Comparator 는 정렬해야 하는 클래스 소스코드에 수정을 가하지 않습니다.
compare, compareTo 모두 int를 반환합니다.
양수 혹은 1이 리턴되면 두 객체의 순서를 바꿉니다. 자리바꾸기 true, 1 로 기억하면 쉽습니다.
음수 혹은 -1이 리턴되면 두 객체의 순서를 바꾸지 않습니다. 자리바꾸기 false 로 기억하면 쉽습니다.
0은 순서가 같다는 의미입니다.
compare, compareTo를 구현할 때, 뺄셈을 사용할 경우, stackoverflow 가능성에 유의해야 합니다.
정렬의 기본값은 오름차순 입니다.
따라서 reversed() 는 내림차순입니다.
느낀 점! ⚡️
공부할 수록 공부할 내용이 너무 많이 튀어나왔습니다.. 흐엑... ㅠ
Comparable, Comparator 계속 헷갈려왔는데 이제 속이 좀 풀리는 것 같습니다! 하하 ㅋ
정렬 기준도 equals, hashCode처럼 개발자가 정해줘야하는 규칙 중 하나인 것 같아요!
직전에 공부했던 것과 같은 의도를 담고 있는 것 같아서 재밌었습니다.
Comparable 보다는 Comparator 를 사용하는 게 더 마음이 갑니다.
정렬 규칙을 객체에게 위임할 수도 있지만, 변경에 유연하지 않은 것 같아요.
그리고 List<Car> 를 그냥 .sort() 로 정렬하면, 코드를 읽을 때 구현부를 다시 살펴야할 거예요.
반면 Comparator 는 도메인 클래스에 변경을 가하지 않고도 변경에 유연하고,
정렬 기준을 코드 자체로 나타내기 때문에 협업에도 더 좋은 것 같습니다!
뿐만 아니라, 다양한 정렬 기준이 빈번하게 사용된다면,
이들을 유틸 클래스로 분리해서 여러 정렬기준들을 가져다 필요할 때 편하게 사용할 수 있을 것 같아요.
학습 출처
https://www.baeldung.com/java-comparator-comparable
https://www.baeldung.com/java-8-sort-lambda
https://www.baeldung.com/java-8-comparator-comparing
'우아한테크코스 4기' 카테고리의 다른 글
함께 자라기 - 우아한테크코스에 적용된 애자일 (0) | 2022.02.21 |
---|---|
우아한테크코스 - 첫 팀 프로젝트 회고, 첫 화음 🎶 (4) | 2022.02.18 |
동일성(Identity) vs 동등성(Equality) - feat. equals() hashCode() (6) | 2022.02.14 |
우아한테크코스 1주차 - TDD 자동차 경주 게임, 페어 프로그래밍, 그리고... (2) | 2022.02.12 |
우아한테크코스 4기 오리엔테이션 (3) | 2022.02.09 |