Hyun's Wonderwall

[Java] 이펙티브 자바 - 3장, 4장(1/2) (아이템 10~18) 본문

Study/Java

[Java] 이펙티브 자바 - 3장, 4장(1/2) (아이템 10~18)

Hyun_! 2025. 7. 26. 13:37

3장 모든 객체의 공통 메서드

아이템 10. equals는 일반 규약을 지켜 재정의하라

equals 메서드는 기본적으로 클래스의 인스턴스가 오직 자기 자신과만 같도록 구현되어있다.
- 꼭 필요한 경우가 아니면 equals를 재정의하지 않아도 된다.
- equals 메서드를 재정의하지 않는 것이 좋은 경우 예시

  • 각 인스턴스가 본질적으로 고유 (ex. Thread)
  • 인스턴스의 논리적 동치성을 검사할 일이 없음 (ex. Pattern)
  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞음
  • 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없음

equals를 재정의해야 할 때: 객체 식별성이 아니라 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때 (주로 값 클래스들- Integer, String 등)

  • 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스(Enum 등)라면 equals를 재정의하지 않아도 된다.
  • equals를 논리적 동치성을 확인하도록 재정의하면 Map의 키와 Set의 원소로 사용할 수 있어 유용하다.

equals 메서드를 재정의할 때, equals 메서드는 동치관계를 구현하고 다음의 규약을 만족해야 한다:

  • 반사성: 객체는 자신과 같아야 한다.
  • 대칭성: 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다.
  • 추이성: 첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면, 첫 번째 객체와 세 번째 객체도 같아야 한다.
  • 일관성: 두 객체가 같다면 앞으로도 영원히 같아야 한다.
  • null-아님: 모든 객체가 null과 같지 않아야 한다.

Object 명세에서 말하는 동치관계: 집합을 서로 같은 원소들로 이뤄진 부분집합(동치류, 동치 클래스)으로 나누는 연산.

- equals 메서드가 모든 원소가 같은 동치류에 속한 어떤 원소와도 교환할 수 있어야 한다.

 

좋은 equals 메서드 구현 방법

  1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
  2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.
  3. 입력을 올바른 타입으로 형변환한다.
  4. 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.

데이터 타입별 필드 비교

  • float와 double을 제외한 기본 타입 필드는 == 연산자로 비교하고
  • 참조 타입 필드는 각각의 equals 메서드로
  • float와 double은 Float.NaN, -0.0f 와 같은 특수한 부동소수 값을 다뤄야 해서 Float.compare(float, float), Double.compare(double, double)로 비교한다.
  • 배열 필드는 원소 각각을 비교한다.

어떤 필드를 먼저 비교하느냐가 equals의 성능을 좌우. 다를 가능성이 더 크거나 비교하는 비용이 적게 드는 필드를 먼저 비교하자.

equals를 다 구현했다면 대칭적인지, 추이성이 있는지, 일관적인지 확인하자 - 단위 테스트를 작성해 돌려보자. (AutoValue 프레임워크로


아이템 11. equals를 재정의하려거든 hashCode도 재정의하라

equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다. 그렇지 않으면 hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제가 발생할 수 있다.


Object 명세의 규약

  • equals(Object)가 두 객체를 같다고 판단한다면 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
  • equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.

AutoValue 프레임워크로 equals와 hashCode를 자동 생성할 수 있다.


아이템 12. toString을 항상 재정의하라

Object의 기본 toString 메서드: 클래스_이름@16진수로_표시한_해시코드 반환
-> 모든 모든 구체 클래스에서 Object의 toString을 재정의하자.(상의 클래스에서 이미 알맞게 재정의한 경우는 예외)

 

toString은 해당 객체에 관한 명확하고 유용한 정보를 읽기 좋은 형태로 반환해야 한다. (그 객체가 가진 주요 정보 모두를 반환하는게 좋다)
toString 내부에서 String.format("%03d-%03d-%04d", areaCode, prefix, lineNum); 와 같이 파라미터를 사용해 문자열 포맷을 명시하거나 주석에 작성해주는 것이 좋다.
포맷 여부와 상관없이 toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하는것이 좋다.


아이템 13. clone 재정의는 주의해서 진행하라

Cloneable, clone
새로운 인터페이스를 만들 때는 Cloneable을 확장해서는 안 되며 새로운 클래스도 이를 구현하면 안 된다.
final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다.
복제 기능은 생성자와 팩터리를 이용하자. 단, 배열만은 clone 메서드 방식이 가장 깔끔하다.


아이템 14. Comparable을 구현할지 고려하라

Comparable 인터페이스의 compareTo 메서드는 단순 동치성 비교에 더해 순서를 비교할 수 있다.

Comparable은 타입을 인수로 받는 제너릭 인터페이스 -> compareTo 메서드의 인수 타입 컴파일타임에 정해진다.
- Comparable을 구현했다는 것은 그 클래스의 인스턴스들에서는 natural order가 있음을 뜻한다. (Comparable을 구현한 객체들의 배열은 Arrays.sort(a); 처럼 정렬 가능)
- String도 Comparable을 구현한다.

 

compareTo의 규약

- 생략

- compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 한다.

 

compareTo는 타입이 다른 객체를 신경쓰지 않아도 된다. ClassCastException을 던진다.

 

비교를 활용하는 클래스의 예

- 정렬된 컬렉션인 TreeSet, TreeMap (동치성 확을 equals 대신 compareTo 사용) // HashSet은 equals.
- 검색과 정렬 알고리즘을 활용하는 유틸리스 클래스 Collections, Arrays

 

new BigDecimal("1.0")과 new BigDecimal("1.00") 인스턴스를 같게 보려면 compareTo를 사용해야 한다.

 

Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 비교자(Comparator)를 대신 사용한다.

compareTo 메서드에서 필드의 값을 비교할 때 <와 > 연산자는 쓰지 말아야 한다. 그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.


4장 클래스와 인터페이스 (1/2)

클래스와 인터페이스는 추상화의 기본

아이템 15. 클래스와 멤버의 접근 권한을 최소화하라

정보은닉, 캡슐화: 클래스 내부 데이터와 내부 구현 정보 숨김. 구현과 API를 분리. API를 통해서만 외부 컴포넌트와 소통.

 

정보 은닉의 장점

- 시스템 개발 속도 높임, 관리 비용 낮춤, 재사용성 높임 등

 

자바가 정보 은닉 위해 제공하는 다양한 장치

- 접근 제어 메커니즘: 클래스, 인터페이스, 멤버의 접근성(접근 허용 범위) 명시. 각 요소의 접근성은 그 요소가 선언된 위치와 접근 제한자로 정해진다.

- 모든 클래스와 멤버의 접근성은 가능한 한 최소한으로. 꼭 필요한 것만 골라 public API를 설계하자.

- public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안 된다. public static final 필드가 참조하는 객체가 불변인지 확인하라. (public 가변 필드는 스레드 안전하지 X)


아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라

public 클래스를 필드를 private으로 설정하고 그대신 public 접근자(getter)를 추가하라.

- 패키지 바깥에서 접근할 수 있는 클래스라면 접근자를 제공함으로써 클래스 내부 표현을 언제든 바꿀 수 있는 유연성 획득.

 

public 클래스는 절대 가변 필드를 직접 노출해서는 안 된다. 불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없다. 하지만 package-private 클래스나 private 중첩 클래스에서는 종종 (불변이든 가변이든) 필드를 노출하는 편이 나을 때도 있다.


아이템 17. 변경 가능성을 최소화하라

클래스를 불변으로 만드는 방법:

1. 객체의 상태를 변경하는 메서드를 제공하지 않는다.

2. 클래스를 확장할 수 없도록 한다. (상속하지 못하게)

3. 모든 필드를 final로 선언한다.

4. 모든 필드를 private으로 선언한다.

5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

 

불변 객체 장점(불변 클래스 장점):

- 단순, 스레드 안전하여 따로 동기화할 필요 없음, 안심하고 공유 사용 가능

- 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많음.

- 불변 객체는 그 자체로 실패 원자성을 제공. (상태가 절대 변하지 않으므로)

불변 클래스 단점: 값이 다르면 반드시 독립된 객체로 만들어야 한다.

 

클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.

- 모든 클래스를 불변으로 만들 수는 없지만 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자. 합당한 이유가 없다면 모든 필드를 private final로.

- 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.

- 확실한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public으로 제공해서는 안 된다.


아이템 18. 상속보다는 컴포지션을 사용하라

상속의 취약점을 피하려면 상속 대신 컴포지션과 전달을 사용하자. (특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱)

- 메서드 호출과 달리 상속은 캡슐화를 해친다.

- 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일 때만 써야 한다. (이때도 하위 클래스의 패키지가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해 설계되지 않았다면 문제가 될 수 있음)

- 래퍼 클래스는 하위 클래스보다 견고하고 경력하다.