티스토리 뷰
Item14의 핵심은 작성하는 클래스가 순서가 필요하다면 Comparable을 구현하라는 것이다
모던 자바 인 액션에서 스트림의 다양한 사용법을 배우는데 그때 Comparable을 사용하는 코드도 많다
당시엔 단순 사용법만 익히느라 어떤 원리로 돌아가는지 몰랐는데 이번에는 깊게 읽어봤다
왜 Comparable을 구현하라고 했을까?
보통 알파벳, 숫자, 연대 같이 순서를 가진 값 클래스를 이용하는 과정에 유용하게 쓰일 수 있기 때문이다
책에서는 전화번호를 지역번호, 앞자리, 뒷자리로 잘라서 표현하는 클래스를 작성하는데
비지니스 로직에 따라 지역번호 별 정렬, 앞자리 별 정렬 등이 필요한 경우가 있을 수 있다
또한 자연적인 순서를 가지고 있다면 Arrays.sort()를 이용해 간단히 정렬시킬 수도 있다
Comparable 구현을 자세히 알아보기에 앞서 Item14와는 상관없지만
왜 전화번호를 지역번호, 앞자리, 뒷자리로 자를 필요가 있는지부터 고민해보자
국내는 당연히 010-XXXX-XXXX 형태로 사용하거나 지역번호-XXXX-XXXX 형태로 사용하기에
전화번호를 하나의 정수 타입 혹은 문자 타입으로 받아도 상관없겠다
어느 날 서비스가 인기 폭발해 글로벌화된다면?
해외 기준으로 클래스를 새로 만들어야 할지도 모른다
국가별로 사용하는 국제 지역번호, 번호 자릿수 등이 다르기 때문인데 이들을 하나로 뭉뚱그려 받아버리면
사용할 때마다 if 중첩 검사 혹은 문자열 파싱으로 검사해 사용해야 한다
아래는 나무위키의 전화번호 & 국가번호 검색 결과다
https://namu.wiki/w/%EC%A0%84%ED%99%94%EB%B2%88%ED%98%B8
https://namu.wiki/w/%EA%B5%AD%EA%B0%80%EB%B2%88%ED%98%B8
INT 형으로 만들어뒀다면 국가 번호가 끼어드는 순간 터질 것이고 (int는 21억까지의 수만 표현 가능하므로)
문자열이라면 그나마 낫지만 몇 번째 자리로 국가 번호를 판단할지 애매해진다
애초에 나눠서 지역번호, 앞자리, 뒷자리로 저장했다면 고민하지 않을 문제였다
이 처럼 현재 사용하는 방식에만 시야가 매몰된다면 확장성을 고려하지 못한다
나도 개인 프로젝트를 진행할 때 별생각 없이 String 타입으로 만들었는데 오늘부터 확장성 머신으로 새로 태어났다
다시 본론으로 돌아가서 Comparable을 구현할 때 아래와 같은 방식으로 작성해주면 된다
다음으로 compareTo() 메서드를 재정의해야 하는데 일반 규약을 지키면서
작성자가 원하는 순서대로 필드를 지정해 1번 기준, 2번 기준.. 순으로 나열시키면 된다
public class T implements Comparable<T> {
@Override
public int compareTo(PhoneNumber o) {
int result = Integer.compare(areaCode, o.areaCode);
if (result == 0) {
result = Integer.compare(prefix, o.prefix);
if (result == 0) {
result = Integer.compare(lineNum, o.lineNum);
}
}
return result;
}
이펙티브 자바의 예제 코드를 가져왔는데 필드를 세 개만 써서 그나마 읽을 수 있으나
비교할 필드가 많아질수록 가독성이 떨어지고 어떤 결과가 반환될지 혼란해진다
다행히도 자바8 부터 Comparator를 활용해 가독성을 높일 수 있다
private static final Comparator<PhoneNumber> COMPARATOR =
Comparator.comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
Comparator를 이용해 미리 비교자를 만들어 두는 방법이다
Comparator<PhoneNumber>로 타입을 알려줬지만 comparingInt에서
타입을 인지하지 못하기 때문에 직접 형 변환을 해줘야 한다
비교자를 작성할 때 의문의 빨간 줄이 뜬다면 형 변환 체크를 해주자
이는 jdk17에서도 고쳐지지 않았기 때문에 자바 사용자는 향후 5년 이상 형변환 체크를 직접 해줘야 하니 미리 알아두자
다만 이 방식을 사용할 때는 약 10%의 성능 저하가 뒤따른다고 한다
그 이유가 궁금해 찾아봤는데 (https://www.javacodegeeks.com/2015/01/java8-sorting-performance-pitfall.html)
thenComparingInt를 수행한 후 다시 병합하는 과정에서 발생하는 오버헤드 때문이라고 한다
또한 comparing 보다 comparingInt 가 비교할 타입을 명시해줬기 때문에 약간 빠르다고 한다
그럼 성능을 위해서 가독성을 포기하고 필드 직접 비교로 우리가 직접 작성해야 하는가 하면 그건 아니다
애플리케이션 병목의 대부분은 네트워크와 쿼리 문제지 자잘한 로직 상의 문제가 아니다
10%의 성능 차이는 전체가 아닌 저 로직만의 문제임을 기억하자
저 정도의 성능이 문제 된다면 자바8이 제공하는 stream도 대부분 사용해선 안 되고 직접 if, for를 다루는 게 낫다
따라서 그냥 Comparator 사용하되 APM 툴을 사용해 저 부분이 병목지점일 때 고민해도 늦지 않다
Comparator를 사용한 깔끔한 방식은 아래와 같이 된다
@Override
public int compareTo(PhoneNumber o) {
return COMPARATOR.compare(this, o);
}
하나 더 주의할 점으로는 compareTo로 수행한 동치성 테스트의 결과가 equals와 같아야 한다는 점이다
이는 일반 규약 중 필수조건이 아니기 때문에 지키지 않아도 상관없지만
지키지 않은 경우 Collection에 담아 사용할 때 문제가 될 수 있다
List, Set, Map 등은 equals 규약을 따른다고 하지만 정렬된 컬렉션은 동치성을 비교할 때 compareTo를 사용한다
equals의 경우엔 자기 자신과 비교하고 타입 검사 후 필드 검사를 수행하기 때문에
간소화를 위해 Comparable<T>로 타입을 명시해놔서 타입 검사를 따로 하지 않는 compareTo를 사용하는 걸로 보인다
이건 규약을 지키지 않은 Collection Framework의 잘못이다라고 우기지 말고
compareTo 동치성 결과를 equals에 매칭 시켜 따르도록 하자
Collection에서 정의한 대로 동작하지 않는다면 으잉스러운 상황이 발생하고 이런 상황이 해결하기 제일 난감한 상황이다
동작은 제대로 하는데 계속해서 원하는 결과가 나오지 않는다면 어디서 문제가 발생한 건지 파악하기 힘들어지기 때문이다
마지막으로 compareTo 안티 패턴으로 인자로 받는 값의 차나, 인자의 hashCode 차이를 이용한 방법이 있다
compareTo로 A와 B를 비교 시 A > B -> 양수, A == B -> 0, A < B -> 음수만 주면 된다고 생각해
단순하게 차이로 반환해버리는 경우가 있을 수 있는데 스택 오버플로우 또는 언더플로우의 위험을 갖고 있다
A가 Integer.MIN_VALUE이고 B가 1인 경우에 A - B는 언더 플로우 발생한다
결론으로 순서가 있는 값 클래스를 작성한다면
1. 사용 편의성을 위해 Comparable을 구현하도록 하고
2. 일반 규약을 지키며
3. 보너스로 compareTo - equals 관계를 고려하자
4. 값의 차이로 반환하지 말고 비교자를 사용하거나 비교를 위한 박싱 타입의 비교 메서드를 사용하자
Integer.compare(), Double.compare() 등등
순서가 있는 값 클래스지만 사용하지 않을 거라면 Comparable을 구현할 필요가 없다
필요한 몇몇 경우가 생긴다면 Comparator를 이용해 사용할 때마다 비교하자
'Java > Effective Java' 카테고리의 다른 글
[Item16] 잘 숨겨야 발전한다 (0) | 2022.02.22 |
---|---|
[Item15] 우아한 제약 걸기 (0) | 2022.02.21 |
[Item13] Cloneable 금지 (0) | 2022.02.20 |
[Item12] toString 정말 필요한가 (0) | 2022.02.19 |
[Item11] @EqualsAndHashCode (2) | 2022.02.19 |