4장. 클래스와 인터페이스

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

핵심 정리

프로그램 요소의 접근성은 가능한 한 최소한으로 하라. 꼭 필요한 것만 골라 최소한의 public API를 설계하자.
그 외에는 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개되는 일이 없도록 해야 한다.

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

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

핵심 정리

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

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

불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.
 - 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요가 없으며, 안심하고 공유할 수 있다.

불변 클래스를 만들려면 5 가지 규칙을 따르면 된다.

  • 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
  • 클래스를 확장할 수 없도록 한다.
    • 하위 클래스에서 객체의 상태를 변하게 만드는 사태를 막아준다.
    • final 클래스
    • private 필드, private 생성자, 정적 팩토리 사용
  • 모든 필드를 final로 선언한다.
    • 설계자의 의도롤 명확히 드러내는 방법
  • 모든 필드를 private로 선언한다.
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

핵심 정리

getter가 있다고 해서 무조건 setter를 만들지는 말자!! 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.

  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.

다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다.

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

핵심 정리

상속은 강력하지만 캡슐화를 해친다는 문제가 있다. 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일 때만 써야 한다.
하지만 is-a 관계일 때도 안심할 수만은 없는게, 하위 클래스의 패키지가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해 설계되지 않았다면 여진히 문제가 될 수 있다.
상속의 취약점을 피하려면 상속대신 컴포지션과 전달을 사용하자. 특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다.
래퍼 클래스는 하위 클래스보다 견고하고 강력하다.

19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라.

핵심 정리

상속용 클래스를 설계하기란 결코 만만치 않다. 클래스 내부에서 스스로를 어떻게 사용하는지 모두 문서로 남겨야 하며, 일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 한다. 그러지 않으면 내부 구현 방식을 믿고 활용하던 하위 클래스를 오작동하게 만들 수 있다.
다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야 할 수도 있다. 그러니 클래스를 확장해야 할 명확한 이유가 없다면, 상속을 금지하는 편이 나을 것이다.
상속을 금지 하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만들면 된다.

20. 추상 클래스보다는 인터페이스를 우선하라.

핵심 정리

일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다. 복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 꼭 고려해보자.
골격 구조 구현은 ‘가능한 한’ 인터페이스의 디폴트 메서드로 제공하여 그 인터페이스를 구현한 모든 곳에서 활용하도록 하는 것이 좋다.
‘가능한 한’이라고 한 이유는, 인터페이스에 걸려 있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 더 흔하기 때문이다.

21. 인터페이스는 구현하는 쪽을 생각해 설계하라.

핵심 정리

default 메서드를 이용해서 기존 인터페이스에 메서드를 추가 할 수 있게 되었다. 하지만, 기존 구현체에서도 잘 동작 할 지는 보장 할 수 없다. 핵심은 명백하다. 디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계 할 때는 여전히 세심한 주의를 기울여야 한다.

22. 인터페이스는 타입을 정의하는 용도로만 사용하라.

핵심 정리

인터페이스는 타입을 정의하는 용도로만 사용해야 한다. 상수 공개용 수단으로 사용하지 말자.

안티 패턴 : 상수 인터페이스

public interface Constants {
    static final double AVG = 6.2345235123;
    static final double AVG = 9.11111115222;
    static final double AVG = 1.3542345235;
}

클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당한다. 따라서 상수 인터페이스가 구현하는 것은 내부 구현을 클래스의 API로 노출하는 행위 이다. 이보다는 상수 유틸클래스를 사용하자.`

//유틸 클래스
public class Constants {
    private Constants(){}
    
    public static final double AVG = 6.2345235123;
    public static final double AVG = 9.11111115222;
    public static final double AVG = 1.3542345235;
}

23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라

활용 예제

태그 달린 클래스를 써야 하는 상황은 거의 없다.
새로운 클래스를 작성하는 데 태그 필드가 등장한다면 태그를 없애고 계층구조로 대체하는 방법을 생각해보자. 기존 클래스가 태그 필드를 사용하고 있다면 계층구조로 리펙토링 하는 걸 고민해보자.

태그 달린 클래스 예

public class Animal {
    enum Type{ DOG, CAT }
    
    final Type type;
    
    //dog에서만 쓰는 필드
    final String name;
    
    //cat에서만 쓰는 필드
    final int age;
    
    //dog용 생성자
    public Animal(String name){
        this.name = name;
    }
    
    //cat용 생성자
    public Animal(int age){
        this.age = age;
    }
    
}
태그 달린 클래스의 단점

장황하고, 오류를 내기 쉽고, 비효율적이다.

24. 맴버 클래스(중첩클래스)는 되도록 static으로 만들라

핵심 정리

중첩 클래스는 4가지가 있으며, 각각의 쓰임새가 다르다. 메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길다면 멤버 클래스로 만든다.
멤버클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로, 그렇지 않으면 정적으로 만들자.
중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한곳이고 해당 타입으로 쓰기엔 적합한 클래스나 인터페이스가 이미 있다면, 익명 클래스로 만들고, 그렇지 않으면 지역 클래스로 만들자.

정적 멤버 클래스와 비정적 멤버 클래스

구문상 차이는 단지 static이 붙어 있고 없고 뿐이지만, 의미상 차이는 의외로 꽤 크다.

  • 정적 멤버 클래스
    • 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 사용하자.
      • 비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없기 때문
    • 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.

비정적 맴버 클래스

숨은 외부(바깥 클래스의 인스턴스) 참조를 갖게 되고, 이는 더 많은 시간과 공간(메모리)을 차지하게 된다. 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하게 되면 메모리 누수 가 발생 할 수 있다. 이렇게 되면 문제의 원인을 파악하기 어려워 심각한 상황을 초래 할 수 있다. 맴버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 맴버 클래스로 만들자.

25. 톱 레벨 클래스는 한 파일에 하나만 담으라

핵심 정리

소스 파일 하나에는 반드시 톱 레벨 클래스(혹은 톱 레벨 인터페이스)를 하나만 담자.
이 규칙만 따른다면 컴파일러가 한 클래스에 대한 정의를 여러 개 만들어 내는 일은 사라진다. 소스 파일을 어떤 순서로 컴파일 하든 바이너리 파일이나 프로그램의 동작이 달라지는 일은 결코 일어나지 않을 것이다.