공부함

클래스와 인터페이스 본문

Java/이펙티브 자바

클래스와 인터페이스

찌땀 2024. 1. 5. 10:56

클래스와 인터페이스 설계에 사용하는 강력한 요소를 활용하는 방법을 알아보자.

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

잘 설계된 컴포넌트는 내부 구현을 완벽히 숨겨 구현과 API를 깔끔하게 분리한다. APi를 통해서만 다른 컴포넌트와 소통한다. 이것을 정보 은닉, 캡슐화라고 하며 sw 설계의 근간이 되는 원리다. 캡슐화의 장점은 다음과 같다

  • 개발 속도가 높아진다. 여러 컴포넌트 병렬 개발이 가능하기 때문이다.
  • 관리 비용을 낮춘다. 디버깅, 교체 부담이 적다.
  • 성능 최적화에 도움을 준다. 최적화할 컴포넌트를 정하고 다른 컴포넌트에 영향을 주지 않고 최적화 할 수 있다.
  • 재사용성을 높인다. 컴포넌트가 외부에 거의 의존하지 않고 독자적으로 동작할 수 있기 때문이다. 
  • 큰 시스템 제작 난이도를 낮춘다. 시스템이 완성되지 않아도 개별 컴포넌트의 동작을 검증할 수 있기 때문이다.

접근 제한자를 제대로 활용하는 것이 정보 은닉의 핵심이다. 모든 클래스, 멤버의 접근성을 가능한 한 좁혀야 한다.

 

톱레벨 클래스, 인터페이스

톱레벨 클레스, 인터페이스에 부여할 수 있는 접근 수준은 package-private과 public이다.

패키지 외부에서 쓸 이유가 없다면 package private으로 선언하자. 그러면 해당 패키지에서만 사용할 수 있고 내부 구현이 되어 언제든지 클라이언트에 영향을 미치지 않고 변경할 수 있다.

public으로 선언하면 공개 API가 되어 관리해줘야 한다. 

 

한 클래스에서만 사용하는 톱레벨 클래스, 인터페이스는 이를 사용하는 클래스 안에 private static으로 중첩시킬 수 있다. 중첩 private static 클래스는 그 클래스가 속한 클래스에서만 접근할 수 있다. 

 

멤버

멤버 : 필드, 메서드, 중첩 클래스, 중첩 인터페이스
  • private : 멤버를 선언한 클래스에서만 접근 가능 
  • package private : 멤버가 소속된 패키지에서 접근 가능. 디폴트 값 
    • 인터페이스의 멤버의 디폴드 값은 public 
  • protected : package private을 포함하고, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다.
  • public : 모든 곳에서 접근할 수 있다.

공개 API를 설계한 후, 그 외 모든 멤버는 private으로 만들자. 그 후 같은 패키지의 다른 클래스가 접근해야 하는 멤버만 private을 제거해 package-priavet으로 풀어주자. 이런 일이 잦다면 컴포넌트를 더 분해하는 것이 나을 수도 있다. private, package-private 멤버들은 클래스의 구현에 해당하므로 공개 API에 영향을 주지 않는다.

 

public 클래스에서 protected 멤버는 공개 API이므로 영원히 지원돼야 하고, 내부 동작 방식을 문서화 해서 공개해야 할 수도 있다. 따라서 protected 멤버는 적을수록 좋다.

 

상위 클래스의 메서드를 재정의 할 때는 상위 클래스의 접근 수준보다 좁게 설정할 수 없다. 상위 클래스 인스턴스는 하위 클래스 인스턴스로 대체해 사용할 수 있어야 한다는 리스코프 치환 원칙을 지키기 위함이다. 규칙을 어기면 컴파일오류가 난다. 

 

테스트 목적으로 접근 범위를 넓히는 것은 package private까지만 허용한다. 이 이상 넓히면 공개 API가 되고 테스트만을 위해 공개 API로 만들면 안된다. package private까지만 넓히고 테스트코드를 같은 패키지에 작성하면 된다. 

 

public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다. 필드가 가변 객체를 참조하거나 final이 아닌 인스턴스 필드를 public으로 열면 필드에 담는 값을 제한할 수 없다. 따라서 그 필드와 관련된 모든 것은 불변식을 보장할 수 없다. 그리고 필드가 수정될 때 다른 작업을 할 수 없으므로 스레드 안전하지 않다. 필드가 final이면서 불변 객체를 참조하더라도 public이면 public 필드를 없애는 방식으로는 리팩터링 할 수 없다. (공개 API라 수정할 수 없어서 그렇다는 의미인 것 같다)

 

정적 필드도 마찬가지로 되도록 public을 사용하지 않는다. 예외적으로 해당 클래스가 표현하는 추상 개념을 완성하는 데 꼭 필요한 상수라면 public static final 필드로 공개해도 좋다. 관례상 상수 이름은 대문자 알파벳에 사이에 _를 넣는다. 이런 필드는 기본 값이나 불변 객체를 참조해야 한다. 가변 객체를 참조하면 다른 객체를 참조하지는 못하지만 참조된 객체 자체는 수정될 수 있다. 

 

길이가 0이 아닌 배열은 모두 변경 가능하다. 즉 가변 객체다. 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공하면 안된다.  가변 객체를 참조하는 필드이므로 클라이언트가 배열의 내용을 수정할 수 있게 된다. 

// 클라이언트가 배열의 값을 수정할 수 있다. 
public static final Thing[ ] VALUES = {...} 
// 배열은 private으로 선언하고 배열의 값을 갖는 불변 List를 public으로 추가한다.  
private static final Thing[ ] PRIVATE_VALUES = {...} 
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

배열은 private으로 변경하고 public 불변 List를 추가할 수 있다. 

private static final Thing[ ] PRIVATE_VALUES = {...} 
public static final Thing[ ] values() {
    return PRIVATE_VALUES.clone();

마찬가지로 배열은 private으로 변경하고 clone으로 배열을 방어적 복사해서 public으로 추가할 수 있다.

(방어적 복사 : 복사한 객체를 변경해도 원본 객체가 변경되지 않는 복사)

두가지 해결방법 중 적절하게 골라 사용하면 된다.

 

패키지는 클래스들의 묶음이고 모듈은 패키지들의 묶음이다. 모듈은 자신이 속하는 패키지 중 공개(export)할 것들을 관례상 module-info.java 파일에 선언한다.

module my.module {
    exports com.my.package.name;
}

멤버가 protected나 public이더라도 멤버가 속한 패키지를 공개하지 않았다면 모듈 외부에서 접근할 수 없다. 

모듈 시스템을 활용하면 클래스를 외부에 공개하지 않으면서도 같은 모듈을 이루는 패키지 사이에서는 자유롭게 공유할 수 있다. 즉 public 클래스의 public, protected 멤버는 그 공개 범위가 모듈 내로 제한되는 특이 케이스다. 

 

이 2가지 특이 케이스는 사용에 주의해야 한다. 왜냐하면 모듈의 jar 파일을 자신의 모듈 경로가 아닌 애플리케이션의 클래스패스에 두면 모듈 안의 모든 패키지는 모듈이 없는 것처럼 행동한다. 따라서 모듈을 exports 하지 않았더라도 모듈 외부에서 public 클래스의 public, protected 필드에 접근할 수 있게 된다. 

 

모듈 개념이 아직 널리 받아들여질지는 이른 감?이 있다. 당분간은 사용하지 말자. 

 

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

public 클래스에서는 필드를 public으로 선언하면 데이터 필드에 직접 접근할 수 있어 캡슐화의 이점이 없다. API를 수정하지 않고는 내부 표현을 바꿀 수 없고 불변식을 보장할 수 없다. 또한 필드를 읽을 때 부수 작업을 수행할 수 없다. (getter를 사용한다면 getter 메서드 안에서 부수작업을 할 수 있다.)따라서 필드를 private으로 바꾸고 public 접근자(getter)를 추가해야 한다. getter를 제공함으로써 api 수정 없이 클래스 내부 표현을 바꿀 수 있는 유연성이 생긴다. 

 

package-private 클래스 혹은 private 중첩 클래스는 데이터 필드를 노출해도 문제가 없다. 이 경우는 종종 필드를 노출하는 편이 나을 때도 있다. 클래스 선언이나 클라이언트 코드가 접근자 방식보다 깔끔해진다. 클라이언트가 클래스 내부 표현에 묶이지만, package-private 클래스의 경우 클라이언트 코드 역시 패키지 내부의 코드다. 따라서 패키지 욉 코드는 손대지 않고 데이터 표현 방식을 바꿀 수 있다. private 중첩 클래스의 경우 수정 범위가 이 클래스를 포함하는 외부 클래스로 더 좁아진다. 

 

public 클래스의 필드가 불변이라면 직접 노출할 때 단점이 줄어들기는 하지만 여전히 API를 변경하지 않고는 표현 방식을 변경할 수 없으며, 필드를 읽을 때 부수작업을 할 수 없다. 대신 불변식은 보장할 수 있다.  불변값이므로 생성자에서 값을 초기화 할 때 검증하면 불변식을 보장할 수 있다. 

 

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

불변 클래스란 인스턴스의 내부 값을 수정할 수 없는 클래스다. 불변 클래스는 가변 클래스보다 설계, 구현이 쉬우며, 오류가 생길 여지도 적고 안전하다. 

 

불변 클래스 규칙 

  • 변경자(setter)를 제공하지 않는다
  • 클래스를 확장할 수 없도록 한다
    • 하위 클래스에에서 나쁜 의도로 객체 상태를 변하게 만드는 사태를 막아준다. 
    • 클래스를 final로 선언하면 된다. 
  • 모든 필드를 final로 선언한다.
  • 모든 필드를 private으로 선언한다. 
    • 필드가 참조하는 가변 객체를 클라이언트가 직접 접근해 수정하는 것을 막는다. 
    • 기본 타입 필드나 불변 객체는 public final로 선언해도 불변 객체가 되지만 api 변경 없이 내부 표현을 수정할 수 있게 하기 위해 private으로 선언하는 것이 좋다. 
  • 자신 외에는 내부의 가변 컴포넌트에 접근하지 못하도록 한다. 
    • 클래스에 가변 객체를 참조하는 필드가 있으면 클라이언트가 접근하지 못하도록 해야 한다.
    • 가변 객체를 참조하는 필드가 클라이언트가 제공한 객체를 참조하면 안된다. 
    • 접근자(getter)가 이 필드를 그대로 반환해서도 안된다. 
    • 생성자, 접근자, readObject 메서드 모두에서 방어적 복사를 수행하라.
// 코드 17-1 불변 복소수 클래스 (106-107쪽)
public final class Complex {
    private final double re;
    private final double im;

    public static final Complex ZERO = new Complex(0, 0);
    public static final Complex ONE  = new Complex(1, 0);
    public static final Complex I    = new Complex(0, 1);

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public double realPart()      { return re; }
    public double imaginaryPart() { return im; }

    public Complex plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

    // 코드 17-2 정적 팩터리(private 생성자와 함께 사용해야 한다.) (110-111쪽)
    public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
    }

    public Complex minus(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }

    public Complex times(Complex c) {
        return new Complex(re * c.re - im * c.im,
                re * c.im + im * c.re);
    }

    public Complex dividedBy(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp,
                (im * c.re - re * c.im) / tmp);
    }

    @Override public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Complex))
            return false;
        Complex c = (Complex) o;

        // == 대신 compare를 사용하는 이유는 63쪽을 확인하라.
        return Double.compare(c.re, re) == 0
                && Double.compare(c.im, im) == 0;
    }
    @Override public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }

    @Override public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}

위 불변 클래스는 사칙연산 메서드에서 객체 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환한다. 이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다. 반면 메서드에서 피연산자인 자신의 상태가 변하는 프로그래밍 패턴은 절차적 or 명령형 프로그래밍이다. 

메서드 이름으로 add같은 동사가 아닌 plus같은 전치사를 사용한 명명규칙 역시 메서드가 객체 값을 변경하지 않느다는 사실을 강조하려는 의도다. 

 

불변객체는 단순하다. 생성된 시점의 상태를 파괴될 때까지 그대로 간직한다. 모든 생성자가 불변식을 보장하면 클래스를 사용하는 다른 프로그래머가 다른 노력을 들이지 않아도 영원히 불변이다. 반면 가변 객체는 복잡한 상태에 놓일 수 있고 믿고 사용하기 어령루 수 있다.

 

    public static final Complex ZERO = new Complex(0, 0);
    public static final Complex ONE  = new Complex(1, 0);
    public static final Complex I    = new Complex(0, 1);

불변 객체는 스레드 안전해서 동기화 할 필요가 없다. 여러 스레드가 동시에 사용해도 훼손되지 않는다. 불변 객체는 안심하고 공유할 수 있다. 불변 클래스라면 한번 만든 객체를 최대한 재활용하는 것이 좋다. 가장 쉬운 재활용 방법은 자주 쓰이는 값들을 위 예시처럼 상수로 제공하는 것이다. 

 

불변 클래스는 자주 사용되는 객체를 캐싱하여 같은 객체를 중복 생성하지 않게 해주는 정적 팩터리를 제공할 수 있다. 여러 클라이언트가 객체를 공유해서 메모리 사용량과 가비지 컬렉션 비용이 줄어든다. 클래스를 설계할 때 public 생성자 대신 정적 팩터리를 만들어두면 클라이언트르 수정하지 않고도 캐시 기능을 나중에 덧붙일 수 있다. (정적 팩터리 메서드 내부에서 캐시된 객체를 반환하도록 하면 된다..내생각임)

 

불변 객체는 자유롭게 공유할 수 있다. 따라서 방어적 복사도 필요 없다. 아무리 복사해봐야 원본과 같다. 하지만 불변 클래스는 clone 메서드나 복사 생성자를 제공하지 않는 것이 좋다. 불변 클래스인 String 클래스의 복사 생성자는 되도록 이용하지 않는 것이 좋다. 

 

불변 객체는 자유롭게 공유할 수 있고, 불변 객체끼리 내부 데이터를 공유할 수 있다. 어차피 모든 필드가 불변이므로 내부 데이터를 공유해도 문제가 발생하지 않는 것이다. 불변 필드가 가변 객체를 가리키더라도 공유할 수 있다. (BigInteger 예시 - 109p) 의문점 : final 필드가 가변 객체를 가리키면 참조값을 변경하지 못하는 것이지 가변 객체에 접근해서 가변 객체를 변경할 수 있다. 이 경우에 공유가 문제되는 것 아닌가?

 

객체를 만들 때 불변 객체들을 구성요소로 사용하면 이점이 많다. 불변식을 유지하기 수월하다. 불변 객체는 맵의 키와 집합의 원소로 쓰기 안성맞춤이다. 맵, 집합은 안에 담긴 값이 바뀌면 불변식이 허물어지기 때문이다. 

 

불변 객체는 그 자체로 실패 원자성을 제공한다.

 실패 원자성 : 메서드에서 예외가 발생한 후에도 그 객체는 여전히 메서드 호출 전과 똑같은 유효한 상태여야 한다. 불변 객체의 메서드는 내부 상태를 바꾸지 않으니 이 성질을 만족한다.

 

 

불변 클래스의 단점은 값이 다르면 반드시 독립된 객체로 만들어야 한다는 것이다. 값의 가짓수가 많다면 모두 만드는 데 드는 비용이 크다. 객체를 완성하기까지 단계가 많고, 객체를 완성하는 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 더 불거진다. 이에 대한 해결방법은 2가지다.

  • 흔히 쓰일 다단계 연산들을 예측해 기본 기능으로 제공하느 방법. 각 단계마다 객체를 생성하지 않아도 된다.
    • 클라이언트가 원하는 복잡한 연산들을 충분히 예측할 수 있다면 package-private인 가변 동반 클래스로 충분하다. 
  • 클라이언트를 예측할 수 없다면 가변 동반 클래스를 public을 제공한다. 
    • String의 StringBuilder와 StringBuffer가 가변 동반 클래스다.

 

클래스가 불변임을 보장하려면 final로 선언해서 상속하지 못하게 해야 한다. 더 유연한 방법으로 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리르 제공하는 방법이다. 이렇게 하면 패키지 바깥에서 바라볼 때 이 객체는 final 객체다. public, protected 생성자가 없기 때문에 확장이 불가능하기 때문이다. (package-private으로 생성자를 선언할 경우 패키지 안에서는 구현 클래스를 활용해 유연성을 제공할 수 있다.)

 

불변 클래스 규칙에서  모든 필드가 final이고 어떤 메서드도 이를 수정할 수 없어야 한다고 했다. 성능을 위해 이것을 완화해서 "어떤 메서드도 객체의 상태 중 외부에 비치는 값을 변경할 수 없다"로 볼 수 있다. 어떤 불변 클래스는 계산 비용이 큰 값을 처음 쓰일 때 계산해서 final이 아닌 필드에 캐시해놓고 똑같은 값을 요청하면 캐시값을 반환해 계산 비용을 절감한다. (계산 비용이 큰 값이 안 쓰일수도 있다. 불필요한 값을 계산하면 성능상 손해다. 따라서 이 값이 처음 쓰일 때 계산해서 캐시해 놓는다. final 필드는 무조건 초기화해야 하므로 final이 아닌 필드에 캐시해놓는다.) 이 방식은 객체가 불변이므로 매번 계산하더라도 결과가 항상 처음 계산한 값과 같다는 것이 보장되므로 사용할 수 있다. 

 

정리

  • 클래스는 꼭 필요한 경우가 아니면 불변이어야 한다
    • 게터가 있다고 세터를 만들지 말자
  • 불변 클래스는 장점이 많고, 단점은 특저 상황에서의 잠재적 성능 저하 뿐이다.
  • 불변으로 변경할 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자. 
  • 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다. 
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다. 
    • 확실한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public을 제공해서는 안 된다.

 

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

구체 클래스를 패키지 경계를 넘어, 다즉 다른 패키지의 구체 클래스를 상속하는 일은 위험하다. 이 주제에서 말하는 상속은 클래스가 클래스를 상속하는 구현 상속이다. 

 

상속은 캡슐화를 깨트린다. 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수도 있다. 상위 클래스는 릴리스마다 내부 구현이 달라질 수 있고 이에 따라 하위 클래스가 오동작 할 수 있다. 상위 클래스가 확장을 고려하지 않고 문서화도 부실하다면 하위 클래스는 상위 클래스에 맞춰 계속 수정해야 한다

 

public class InstrumentedHashSet<E> extends HashSet<E> {
    // 추가된 원소의 수
    private int addCount = 0;

    public InstrumentedHashSet() {
    }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @Override public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }

    public static void main(String[] args) {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(List.of("틱", "탁탁", "펑"));
        System.out.println(s.getAddCount());
    }
}

출력결과는 6이 나온다.  HashSet의 addAll이 add를 이용해 구현되었다. 즉 자신의 다른 부분을 사용하는 자가사용 방식을 사용했다. 이때 호출하는 add는 재정의한 add가 되므로 addCount에 더하는 행위가 중복된다. 

자가사용을 통해 구현했다는 것을 알아서 addAll을 재정의하지 않는 방식으로 수정할 수 있지만 HashSet 문서에 이런 내용은 없다 (내부 구현이기 때문에) . addAll을 재정의 할 때 super.addAll을 호출하지 않고 주어진 컬렉션을 순회하며 add를 호출하게 구현할 수도 있다. addAll의 자가사용 여부와 상관없이 올바른 결과를 얻을 수 있지만, 상위 클래스의 메서드 동작을 다시 구현하는 이 방식은 어렵고, 시간이 더 들고, 오류를 내거나 성능을 떨어트릴 수 있다. 그리고 private 필드를 참조할 수 없다. 

 

상위 클래스에 새로운 메서드를 추가하면 하위 클래스가 깨진다. 상위 클래스의 원소를 추가하는 메서드를 재정의해 원소 추가 시 조건을 검사하게 했다. 하지만 상위 클래스에 원소를 추가하는 메서드가 추가되면 조건을 검사하지 않은 메서드를 추가할 수 있게 된다. 

 

상위 클래스를 상속하고 메서드 오버라이딩을 하지 않아서 문제를 해결할 수 있을 것 같지만 아니다. 

  • 상위 클래스에 새 메서드가 추가되었고, 이 메서드가 내가 하위 클래스에 추가한 메서드와 시그니처(메서드 이름과 매개변수 리스트)가 같고 반환타입이 다르면 컴파일에러가 발생한다.
  • 시그니처와 반환타입까지 같으면 오버라이딩 한게 되고, 앞에서와 같은 문제가 발생한다.
    • 의도한 오버라이딩이 아니므로 메서드 규약을 만족하지 않을 가능성이 높다.

상속 대신 컴포지션 활용

새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자. 기존 클래스가 새로운 클래스의 구성요소로 쓰이므로 composition이라 한다. 새 클래스의 메서드는 기존 클래스의 메서드를 호출해서 결과를 반환한다. 이것을 forwarding이라 하고 새 클래스 메서드를 forwarding method라 한다. (전달, 전달 메서드) 전달 클래스를 인터페이스 당 하나씩만 만들어두면 원하는 기능을 덧씌우는 전달 클래스들을 손쉽게 구현할 수 있다. 

새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어난다. 기존 클래스의 메서드를 그대로 호출하기 때문이다. 또한 기존 클래스에 새로운 메서드가 추가되도 영향을 받지 않는다. 

public class InstrumentedSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);
    }

    @Override public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
    @Override public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
    public int getAddCount() {
        return addCount;
    }

    public static void main(String[] args) {
        InstrumentedSet<String> s = new InstrumentedSet<>(new HashSet<>());
        s.addAll(List.of("틱", "탁탁", "펑"));
        System.out.println(s.getAddCount());
    }
}

 

public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }

    public void clear()               { s.clear();            }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty()          { return s.isEmpty();   }
    public int size()                 { return s.size();      }
    public Iterator<E> iterator()     { return s.iterator();  }
    public boolean add(E e)           { return s.add(e);      }
    public boolean remove(Object o)   { return s.remove(o);   }
    public boolean containsAll(Collection<?> c)
                                   { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c)
                                   { return s.addAll(c);      }
    public boolean removeAll(Collection<?> c)
                                   { return s.removeAll(c);   }
    public boolean retainAll(Collection<?> c)
                                   { return s.retainAll(c);   }
    public Object[] toArray()          { return s.toArray();  }
    public <T> T[] toArray(T[] a)      { return s.toArray(a); }
    @Override public boolean equals(Object o)
                                       { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

ForwardingSet은 전달 클래스고 재사용 가능하다. 기존 클래스를 필드로 갖고 전달 메서드로 구성되어 있다. 

InstrumentedSet은 임의의 Set에 기능을 덧씌워 새로운 Set으로 만든다. 상속을 사용한다면 각 구체 클래스를 따로 구현해야 한다. 그리고 상위 클래스의 생성자 각각에 대응하는 생성자를 별도로 정의해줘야 한다. 하지만 컴포지션 방식은 한번만 구현해두면 어떠한 Set 구현체로든 갈아끼울 수 있고 기존 생성자를 함께 사용할 수 있다. 

InstrumentedSet은 다른 Set을 감싸고 있으므로 래퍼 클래스라 한다. 그리고 다른 Set에 기능을 덧씌워서 데코레이터 패턴이라고 한다. 컴포지션, 전달의 조합은 위임이라고 부른다.

 

래퍼 클래스 단점은 콜백 프레임워크와 어울리지 않는다는 점이다. 콜백 프레임워크에서는 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출(콜백) 때 사용하도록 한다. 내부 객체는 자신을 감싸고 있는 래퍼의 존재를 모르기 때문에 자신의 참조를 넘기고, 콜백 때는 래퍼가 아닌 내부 객체를 호출하게 된다. 이것을 SELF 문제라 한다. 

 

상속은 하위 클래스가 상위 타입의 진짜 하위 타입인 상황에서만 쓰여야 한다. B ISA A 일때만 B가 A를 상속해야 한다.  ISA관계를 만족하지 않는다면 컴포지션을 사용하자. A는 B의 필수 구성요소가 아니라 구현방법 중 하나일 뿐이다.

컴포지션을 써야 하는데 상속을 사용하면 내부 구현을 불필요하게 노출하고 API가 내부 구현에 묶인다. 클라이언트가 노출된 내부에 직접 접근할 수 있게 된다. 클라이언트가 혼란스럽게 되고 클라이언트가 상위 클래스를 직접 수정해 하위 클래스의 불변식을 해칠 수 있다. 

 

상속을 사용할 때 상위 클래스 API에 결함이 없는지 생각해보자. 상속은 상위 클래스 API의 결함까지도 상속한다. 이러한 결함이 구현 클래스 API에 전파돼도 괜찮은지 자문해보자. 

 

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

상속을 고려한 설계와 문서화가 무엇일까?

 

메서드를 재정의하면 어떤 일이 일어나는지 정확히 정리해 문서화해야 한다. 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다. 자가사용을 한다면, 그리고 자가사용 하는 메서드가 오버라이딩 가능한 메서드라면(재정의 가능한 메서드 : public, protected 이면서 final이 아닌 메서드) 그 사실을 API에 적시해야 한다. 어떤 순서로 호출하고 각각의 호출 결과의 영향에 대해서도 담아야 한다.  

 

API 문서의 메서드 설명 끝에서 Implementation Requirments로 시작하는 절이 메서드의 내부 동작 방식을 설명하는 곳이다. 이 절은 메서드 주석에 @implSpec 태그를 붙이면 javadoc이 생성해준다. 

하지만 좋은 API 문서는 어떻게가 아닌 무엇을 하는지 설명해야 한다. 메서드의 내부 동작을 설명하는 것은 어떻게를 설명하는 것이다. 이것은 상속이 캡슐화를 해치기 때문에 어쩔 수 없이 발생하는 일이다. 즉 상속을 안전하게 할 수 있게 하기 위해 내부 구현을 설명해야 한다. 

 

클래스의 내부 동작 과정 중간에 끼어들 수 있는 hook을 잘 선별하여 protected 메서드로 공개하야 할 수 있다.  구현체의 최종 사용자가 관심이 없는 메서드의 접근 제한자를 protected로 변경한다. 최종 사용자는 관심이 없지만, 구체 클래스를 구현하는데 도움이 되는 protected 메서드를 공개하는 것이다. 어떤 메서드를 공개할 지는 직접 하위 클래스를 구현해보며 결정하는 것이 최선이다. protected 메서드는 내부 구현이므로 가능한 한 적게 공개하는 것이 좋다. 

 

상속용 클래스는 테스트는 직접 하위 클래스를 만들어보는 방식이 유일하다. 하위 클래스 구현에 꼭 필요한 멤버를 protected로 공개하지 않으면 티가 난다. 반면 여러개 하위 클래스를 만드는 동안 쓰이지 않는 protected 멤버는 private으로 수정해야 한다. 하위 클래스 3개정도를 작성해보는 것이 적당하며 하나는 제3자가 작성해보는 것이 좋다. 

 

상속용 클래스는 직, 간접적으로 재정의 가능 메서드를 호출하면 안된다. 상위 클래스 생성자가 하위 클래스 생성자보다 먼저 호출된다. 상위 클래스 생성자에서 재정의 가능 메서드를 호출하면 하위 클래스에서 재정의한 메서드가 호출된다. 하위 클래스에서 재정의 한 메서드가 하위 클래스의 생성자에서 초기화 한 값에 의존한다면 아직 하위 클래스의 생성자가 호출되지 않아 초기화되지 않았으므로 오동작할 것이다. private, final, static 메서드는 재정의가 불가하므로 생성자에서 호출해도 된다. 

 

Cloneable또는 Serializable 인터페이스를 구현한 클래스를 상속할 수 있게 설계하는것은 좋지 않은 생각이다. 클래스를 확장하는데 엄청난 부담이 되기 때문이다. 특별한 방식으로 하위 클래스에서 이 인터페이스들의 구현 여부를 선택하게 할 수 있다. 예를 들어 Object 클래스처럼 protected로 작동하는 clone을 선언해놓는 것이다. 

clone과 readObject 메서드는 새로운 객체를 만든다. 따라서 상속용 클래스에서 clone과 readObject를 구현할 때 생성자와 마찬가지로 직,간접적으로 재정의가 가능한 메서드를 호출하면 안된다. 이유 역시 마찬가지로 메서드 호출 순서가 꼬이기 때문이다. Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 이 메서드들은 private이 아닌 protected로 선언해야 한다. private으로 선언하면 하위 클래스에서 무시된다. 

readObject는 역직렬화 할 때 자동으로 호출되는 메서드다.

상속용으로 설계하지 않은 클래스(일반적인 구체 클래스)는 상속을 금지하는 것이 좋다. 클래스를 final로 선언해도 되고, 모든 생성자를 private이나 package private으로 선언하고 public 정적 팩터리를 만들어 줄 수도 있다. 하지만 많은 프로그래머가 일반적인 구체 클래스를 상속해 기능을 추가해 왔다. 핵심 기능을 정의한 인터페이스가 있고, 클래스가 그 인터페이스를 구현했다면 상속을 금지해도 개발하는데 어려움이 없다. 인터페이스를 구현하면서 기능을 추가한 클래스를 만들면 되기 때문이다. Set, List, Map이 좋은 예다. 래퍼 클래스 패턴을 사용해도 상속 없이 클래스에 기능을 추가할 수 있다. 

 

구체 클래스가 표준 인터페이스를 구현하지 않았는데, 상속을 금지하면 사용하기 불편하다. 상속을 허용한다면 대신 클래스 내부에서는 재정의 가능 메서드를 사용하지 않게 만들고 이 사실을 문서로 남겨야 한다. 자기 사용 코드를 완벽히 제거하면 메서드를 재정의해도 다른 메서드 동작에 아무런 영향이 없기 때문이 합당하다.

 

클래스 동작을 유지하면서 재정의 가능 메서드를 사용하는 코드를 제거하는 방법이 있다.

  • 각각의 재정의 가능 메서드의 본문 코드를 각각의 private 도우미 메서드로 옮긴다.
  • 재정의 가능 메서드의 본문에서는 도우미 메서드를 호출하도록 한다. 
  • 재정의 가능 메서드를 호출하는 메서드에서도 재정의 가능 메서드 대신 도우미 메서드를 호출한다. 

[아이템 20] 추상 클래스보다는 인터페이스를 우선하라

자바에서 다중 구현 메커니즘은 추상 클래스와 인터페이스가 있다. 두 방식 모두 인스턴스 메서드를 구현해서 제공할 수 있다. 추상 클래스를 상속하는 클래스는 반드시 추상 클래스의 하위 타입이 되어야 한다. 인터페이스를 구현한 클래스는 다른 어떤 클래스를 상속했든 같은 타입으로 취급된다. 

 

기존 클래스에도 손쉽게 인터페이스를 구현해넣을 수 있다. 인터페이스가 요구하는 메서드를 구현하고 클래스 선언에 implements 구문을 추가하면 된다. 하지만 클래스 위에 새로운 추상 클래스를 끼워넣기는 어렵다. 두 클래스가 같은 추상 클래스를 확장하길 원한다면 그 추상 클래스는 계층구조상 두 클래스의 공통 조상이어야 한다. 이 방식은 클래스 계층구조에 커다란 혼란을 일으킨다. 새로 추가된 추상 클래스의 모든 자손이 이를 상속하게 된다.

 

인터페이스는 믹스인 정의에 안성맞춤이다. 믹스인은 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 주된 타입 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다. 예를 들어 Comparable은 자신을 구현한 클래스의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스다. 대상 타입의 주된 기능에 선택적 기능을 혼합해서 믹스인이라고 한다. 추상 클래스로는 믹스인을 정의할 수 없다. 기존 클래스에 덧씌울 수 없기 때문이다. 이미 다른 클래스를 상속하고 있는 클래스에 덧씌워 클래스 다중 상속을 할 수 없다. 

 

인터페이스로 계층구조가 없는 타입 프레임워크를 만들 수 있다. 현실에서는 계층이 엄격히 구분하기 어려운 개념도 있다. Singer와 Songwriter를 겸업하는 사람도 있다. Singer와 Songwriter 인터페이스를 모두 구현할 수도 있고, 두 인터페이스를 상속해 SingerSongwriter 인터페이스를 정의할 수도 있다. 반면 같은 구조를 클래스로 만들려면 가능한 조합 전부를 클래스로 만들어야 한다. 속성이 n개라면(가수, 작곡가, 댄서 ..) 만들어야 하는 클래스는 2^n개(가수 + 작곡가, 가수 + 댄서, 작곡가 + 댄서, ... 등)가 되는 조합 폭발 현상이 발생한다. 

 

래퍼클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다(아이템 18의 예시처럼 인터페이스를 구현한 전달클래스와 전달클래스를 상속한 래퍼클래스를 사용하는 방식을 말하는 것 같다). 타입을 추상클래스로 정의하면 기능을 추가하는 방법은 상속뿐이다. 상속해서 만든 클래스는 래퍼클래스보다 활용도가 떨어지고 깨지기 쉽다. 

 

인터페이스 메서드 중 구현방법이 명백한 것이 있으면 디폴트 메서드로 구현을 제공할 수 있다. 디폴트 메서드를 제공할 때는 상속하려는 사람을 위한 설명을 @implSpec 자바독 태그를 붙여 문서화해야 한다. Object의 메서드를 정의한 경우 (equals, hashCode 등), 디폴트 메서드로 제공해서는 안된다. 인터페이스는 인스턴스 필드를 가질 수 없고, public이 아닌 정적 멤버를 가질 수 없다. 직접 만들지 않은 인터페이스는 디폴트 메서드를 추가할 수 없다. 

 

인터페이스와 추상 골격 구현 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취할 수 있다. 인터페이스로는 타입을 정의하고, 필요한 디폴트 메서드도 제공한다. 골격 구현 클래스는 나머지 메서드들까지 구현한 추상 클래스다. 이렇게 해두면 골격 구현을 확장하는 것만으로도 이 인터페이스를 구현하는 데 필요한 일이 대부분 완료된다. 이것이 템플릿 메서드 패턴이다. 관례상 인터페이스 이름 앞에 Abstract를 붙여서 골격 구현 클래스 이름을 짓는다. 골격 구현은 인터페이스를 구현하려는 프로그래머의 일을 많이 덜어준다. 골격 구현 클래스는 추상 클래스처럼 구현을 도와주는 동시에, 추상 클래스로 타입을 정의할 때 따라오는 제약에서는 자유롭다. 골격구현 클래스를 확장하지 못하는 상횡이더라도 인터페이스를 구현해서 인터페이스가 제공하는 디폴트 메서드의 이점을 누릴 수 있다. 

public class IntArrays {
    static List<Integer> intArrayAsList(int[] a) {
        Objects.requireNonNull(a);

        // 다이아몬드 연산자를 이렇게 사용하는 건 자바 9부터 가능하다.
        // 더 낮은 버전을 사용한다면 <Integer>로 수정하자.
        return new AbstractList<>() {
            @Override public Integer get(int i) {
                return a[i];  // 오토박싱(아이템 6)
            }

            @Override public Integer set(int i, Integer val) {
                int oldVal = a[i];
                a[i] = val;     // 오토언박싱
                return oldVal;  // 오토박싱
            }

            @Override public int size() {
                return a.length;
            }
        };
    }

    public static void main(String[] args) {
        int[] a = new int[10];
        for (int i = 0; i < a.length; i++)
            a[i] = i;

        List<Integer> list = intArrayAsList(a);
        Collections.shuffle(list);
        System.out.println(list);
    }
}

List 구현체를 반환하는 정적 팩터리 메서드 intArraysAsList이다. AbstractList 골격 구현 클래스를 활용했다. 익명클래스를 활용해 골격 구현 클래스를 구현했다. 

 

골격 구현 클래스 작성 방법

  1. 인터페이스에서 다른 메서드들의 구현에 사용되는 기반 메서드를 선정한다. 기반 메서드들은 골격 구현 클래스에서 추상 메서드가 된다. (골격 구현 클래스에서 메서드를 다시 선언할 필요는 없다. 골격 구현 클래스를 구현하는 구체 클래스에서 기반 메서드를 구현한다.)
  2. 기반 메서드들을 사용해 직접 구현할 수 있는 메서드들을 디폴트 메세지로 제공한다. 이 때, Object의 메서드는 디폴트 메서드로 제공하면 안된다. 따라서 Object의 메서드들은 골격 구현 클래스에서 구현한다. 
    1. 인터페이스의 모든 메서드가 기반 메서드, 디폴트 메서드가 된다면 골격 구현 클래스를 별도로 만들 필요가 없다.
  3. 기반 메서드나 디폴트 메서드로 만들지 못한 메서드가 있다면, 이 인터페이스를 구현하는 골격 구현 클래스를 만들어 남은 메서드들을 작성한다. 골격 구현 클래스에는 public이 아닌 필드와 메서드를 추가해도 된다. 
public interface Interface {
    // 기반 메서드
    void a();

    // 디폴트 메서드
    default  void b(){
        // 기반 메서드를 사용해 직접 구현한다.
        System.out.println("a");
        a();
    }

    // 기반 메서드나 디폴트 메서드가 아닌 메서드
    void c();
}

public abstract class AbstractInterface implements Interface{

    // 기반 메서드, 디폴트 메서도 아닌 메서드는 골격 구현 클래스에서 구현한다.
    @Override
    public void c() {
        System.out.println("c");
    }
}

public class ConcreteClass extends AbstractInterface {
    // 기반 메서드는 구체 클래스에서 구현한다. 
    @Override
    public void a() {
        System.out.println("a");
    }
}

골격 구현 클래스는 추상 클래스이므로 상속해서 구체화하게 된다. 따라서 [아이템 19]에서 언급한 상속을 고려한 설계, 문서화 지침을 따라야 한다.

시뮬레이트한 다중 상속

public interface Vending {
    void start();
    void chooseProduct();
    void stop();
    void process();
}

public abstract class AbstractVending implements Vending {
    @Override
    public void start() {
        System.out.println("vending start");
    }

    @Override
    public void stop() {
        System.out.println("stop vending");
    }

    @Override
    public void process() {
        start();
        chooseProduct();
        stop();
    }
}

public class VendingManufacturer {
    public void printManufacturerName() {
        System.out.println("Made By JavaBom");
    }
}

public class SnackVending extends VendingManufacturer implements Vending {
    InnerAbstractVending innerAbstractVending = new InnerAbstractVending();

    @Override
    public void start() {
        innerAbstractVending.start();
    }

    @Override
    public void chooseProduct() {
        innerAbstractVending.chooseProduct();
    }

    @Override
    public void stop() {
        innerAbstractVending.stop();
    }

    @Override
    public void process() {
        printManufacturerName();
        innerAbstractVending.process();
    }

    private class InnerAbstractVending extends AbstractVending {

        @Override
        public void chooseProduct() {
            System.out.println("choose product");
            System.out.println("chocolate");
            System.out.println("cracker");
        }
    }
}

골격 구현 클래스를 우회적으로 이용하는 방법이다. 인터페이스를 구현한 클래스에서 골격 구현 클래스를 확장한 private 내부 클래스를 정의한다. 인터페이스를 구현한 클래스의 각 메서드 호출을 private 내부 클래스 인스턴스에 전달한다. 래퍼 클래스와 유사하다. 

 

단순 구현 

단순 구현은 골격 구현의 변종으로 상속을 위해 인터페이스를 구현하되 추상 클래스가 아니다. 즉 동작하는 가장 단순한 구현으로 그대로 써도 되고 확장해도 된다. 

 

[아이템 21] 인터페이스는 구현하는 쪽을 생각해 설계하라 

인터페이스에 메서드를 추가하면 컴파일 오류가 난다. 왜냐하면 인터페이스의 구현체는 인터페이스의 메서드를 모두 구현해야 하기 때문이다. 디폴트 메서드를 선언하면 되지만 디폴트 메서드는 구현 클래스에 대해 아무것도 모른 채 합의 없이 무작정 삽입된다. 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기는 어렵다. 디폴트 메서드는 컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있다. 흔한 일은 아니지만 주의해야 한다. 디폴트 메서드는 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 용도가 아니다. 이런 형태로 인터페이스를 변경하면 기존 클라이언트를 망가트리게 된다. 

 

디폴트 메서드로 메서드를 추가할 수 있더라도 인터페이스를 설계할 때는 세심한 주의를 기울여야 한다. 새로운 인터페이스라면 릴리스 전에 반드시 테스트를 거쳐야 한다. 서로 다른 방식으로 최소한 세가지는 구현해 보자. 클라이언트도 여러개 만들어 보자. 인터페이스를 릴리즈 하기 전에 결함을 찾아내는게 최선이다. 

 

[아이템 22] 인터페이스는 타입을 정의하는 용도로만 사용하라

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

인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다. 즉 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에게 얘기해주는 것이다. 인터페이스는 이 용도로만 사용해야 한다. 

 

상수 인터페이스

public interface PhysicalConstants {
    // 아보가드로 수 (1/몰)
    static final double AVOGADROS_NUMBER   = 6.022_140_857e23;

    // 볼츠만 상수 (J/K)
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;

    // 전자 질량 (kg)
    static final double ELECTRON_MASS      = 9.109_383_56e-31;
}

상수 인터페이스는 인터페이스를 이 용도로 사용하지 않은 안티패턴으로 메서드 없이 static final 필드로만 가득 찬 인터페이스다. 인터페이스를 상수 공개용 수단으로 사용한 것이다. 

  • 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현이다.
  • 상수 인터페이스를 구현하는 것은 내부 구현을 클래스 API로 노출하는 것이다.
  • 클라이언트에게는 클래스가 어떤 상수 인터페이스를 사용하는지는 의미가 없다.
  • 오히려 클라이언트를 혼란스럽게 하고, 클라이언트 코드가 내부 구현인 상수들에 종속되게 한다. 

상수를 공개하는 합당한 방법

  • 특정 클래스, 인터페이스와 강하게 연관된 상수라면 그 클래스, 인터페이스 자체에 추가하자
    • Integer의 MIN_VALUE가 좋은 예다.
  • 열거 타입으로 만들자.
  • 인스턴스화 할 수 없는 유틸리티 클래스에 담아 공개하자. 
public class PhysicalConstants {
  private PhysicalConstants() { }  // 인스턴스화 방지

  // 아보가드로 수 (1/몰)
  public static final double AVOGADROS_NUMBER = 6.022_140_857e23;

  // 볼츠만 상수 (J/K)
  public static final double BOLTZMANN_CONST  = 1.380_648_52e-23;

  // 전자 질량 (kg)
  public static final double ELECTRON_MASS    = 9.109_383_56e-31;
}

 

[아이템 23] 태그 달린 클래스보다는 클래스 계층구조를 활용하라 

두 가지 이상의 의미를 표현할 수 있으며, 그 중 현재 표현하는 의미를 태그 값으로 알려주는 태그 달린 클래스가 있다. 

class Figure {
    enum Shape { RECTANGLE, CIRCLE };

    // 태그 필드 - 현재 모양을 나타낸다.
    final Shape shape;

    // 다음 필드들은 모양이 사각형(RECTANGLE)일 때만 쓰인다.
    double length;
    double width;

    // 다음 필드는 모양이 원(CIRCLE)일 때만 쓰인다.
    double radius;

    // 원용 생성자
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    // 사각형용 생성자
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch(shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError(shape);
        }
    }
}

태그 달린 클래스는 단점이 많다.

  • enum, 태그 필드, switch 문 등 불필요한 코드가 많다
  • 가독성이 나쁘다
  • 메모리를 많이 잡아먹는다
  • 필드를 final로 하려면 해당 의미에 쓰이지 않는 필드까지 초기화해야한다. 
  • 쓰지 않는 엉뚱한 필드를 초기화해도 런타임에 드러난다. 
  • 다른 의미를 추가하려면 코드를 수정해야 한다. 이 때 실수가 발생해도 런타임에 알 수 있다.
  • 인스턴스 타입 만으로는 현재 나타내는 의미를 알 수 없다.

클래스 계층구조를 활용한 서브타이핑을 활용하자

태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류작이다. 태그 달린 클래스를 클래스 계층구조로 변경하는 방법을 알아보자.

  1. 계층구조의 root가 될 추상 클래스를 정의하고 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언한다
  2. 태그 값에 무관하게 동작이 일정한 메서드들을 루트 클래스의 일반 메서드로 추가한다. 
  3. 모든 하위 클래스에서 공통으로 사용하는 데이터 필드도 추상 클래스에 선언한다
  4. 루트 클래스를 확장한 구체 클래스를 의미별로 정의한다. 각자 의미에 해당하는 데이터 필드를 넣는다. 
  5. 루트 클래스가 정의한 추상 메서드를 각자의 의미에 맞게 구현한다. 
class Rectangle extends Figure {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width  = width;
    }
    @Override double area() { return length * width; }
}
  • 간결하고 명확하다
  • 불필요한 필드를 제거했다
  • final 필드들이 살아남았다. 
  • 컴파일 타임 체킹이 가능하다. 

태그 달린 클래스가 있다면 계층구조로 리팩터링을 고려햐자.

 

[아이템 24] 멤버 클래스는 되로록 static으로 만들라 

중첩 클래스는 다른 클래스 안에 정의된 클래스다. 자신을 감싼 바깥 클래스에서만 사용되어야 한다. 중첩 클래스의 종류로는 정적 멤버 클래스, 비정적 멤버 클래스, 익명 클래스, 지역 클래스가 있다. 

정적 멤버 클래스 

  • 다른 클래스 안에 선언되고, 바같 클래스의 private 멤버에 접근할 수 있다는 점을 제외하면 일반 클래스와 같다.
  • 다른 정적 멤버와 똑같은 접근 규칙을 적용받는다.
  • 흔히 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 쓰인다. 
    • Calculator.Opertaion.PLUS

private 정적 멤버 클래스

private 정적 멤버 클래스는 바깥 클래스가 표현하는 객체의 한 부분(구성요소)을 나타낼 때 쓴다. 많은 Map 구현체는 키-값 쌍을 표현하는 Entry 객체들을 가지고 있다. 모든 Entry가 맵과 연관되어 있지만 엔트리의 메서드들인 getKey, getValue등은 맵을 직접 사용하지는 않는다. 따라서 비정적 멤버 클래스로 표현하는 것은 바깥 메모리로의 참조를 갖게 되어 시간, 공간적 낭비다. 따라서 private 정적 멤버 클래스가 가장 알맞다. 

 

멤버 클래스가 공개된 클래스의 public이나 protected 멤버라면 static 여부가 더 중요해진다. 멤버 클래스도 공개 api가 되기 때문에 향후 수정하면 영향을 미치기 때문이다. 

비정적 멤버 클래스 

  • 비정적 멤버 클래스 객체는 암묵적으로 바깥 클래스와 연결된다 
  • 비정적 멤버 클래스 인스턴스의 메서드에서 바깥클래스명.this를 통해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져울 수 있다. 
  • 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다.
    • 비정적 멤버 클래스는 바깥 인스턴스 없이 생성할 수 없기 때문이다. 
  • 바깥 클래스와 비정적 멤버 클래스의 관계는 멤버 클래스 객체가 생성될 때 확립되며 더이상 변경할 수 없다. 

비정적 멤버 클래스는 어댑터를 정의할 때 자주 쓰인다. 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용하는 것이다. Map의 구현체에서 자신의 컬렉션 뷰를 반환할 때, Set, List에서 자신의 반복자를 구현할 때 사용한다. 

 

멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자. 비정적 내부 클래스로 만들면 바깥 인스턴스로의 숨은 외부 참조를 갖게 되고 이 참조를 저장하는 데 시간과 공간이 소비돤다. 또한 가비지컬렉션이 바깥클래스의 인스턴스를 수거하지 못하는 메모리 누수가 발생할 수 있다(내부 클래스가 참조하기 때문에). 참조가 눈에 보이지 않아 원인을 찾기 어렵다. 

익명 클래스

  •  이름이 없고, 바깥 클래스의 멤버가 아니다.
  • 멤버와 달리 쓰이는 시점에 선언과 동시에 객체가 만들어진다. 
  • 비정적인 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있다. 
  • 정적 문맥에서 사용되더라도 상수 이외의 정적 멤버는 가질 수 없다.
  • instanceof 검사나 클래스 이름이 필요한 작업은 수행할 수 없다. 
  • 여러 인터페이스를 구현할 수 없고, 인터페이스를 구현하는 동시에 다른 클래스를 상소갛ㄹ 수 없다. 
  • 익명 클래스를 사용하는 클라이언트는 익명 클래스가 상위 타입에서 상속한 멤버 외에는 호출할 수 없다. 
  • 정적 팩터리 메서드를 구현할 때 자주 쓰인다. 
public class Main {
    public static void main(String[] args) {
        Comparator<Player> comparator = new Comparator<Player>() {
            @Override
            public int compare(Player o1, Player o2) {
                return o1.age-o2.age;
            }
        };
        Player p1 = new Player(1,1);
        Player p2 = new Player(2,2);
        System.out.println(comparator.compare(p1,p2));
    }
}

comparator가 Comparator를 구현한 익명 클래스다. 

지역 클래스

  • 지역변수를 선언할 수 있는 곳이면 어디서든 선언할 수 있다. 
  • 유효 범위도 지역변수와 같다
  • 이름이 있고 반복해서 사용할 수 있다.
  • 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있다. 
  • 정적 멤버는 가질 수 없다.
  • 가독성을 위해 짧게 작성해야 한다 .

[아이템 25] 톱레벨 클래스는 한 파일에 하나만 담으라 

소스파일 하나에 톱레벨 클래스를 여러개 선언하면 한 클래스를 여러가지고 정의할 수 있으며 그 중 어느 것을 사용할지는 어느 소스파일을 먼저 컴파일하냐에 따라 달리지는 문제가 발생한다. 따라서 톱레벨 클래스들을 서로 다른 소스파일로 분리하자. 

 

public class Test {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }

    private static class Utensil {
        static final String NAME = "pan";
    }

    private static class Dessert {
        static final String NAME = "cake";
    }
}

굳이 여러 톱레벨 클래스를 한 파일에 담고 싶다면 정적 멤버 클래스로 만드는 것이 낫다. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'Java > 이펙티브 자바' 카테고리의 다른 글

모든 객체의 공통 메서드  (0) 2024.01.04
객체 생성과 파괴  (0) 2024.01.02