본문 바로가기

나(다)/책

이펙티브 자바 - 4장 : 클래스와 인터페이스

반응형

4장 클래스와 인터페이스

클래스와 인터페이스는 추상화의 기본 단위로 적절하게 활용할 수 있도록 쓰기 편하고, 견고하며, 유연하게 만드는 방법을 안내한다.

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

내부 구현을 잘 숨기고 구현과 API를 깔끔히 분리하여 정보 은닉, 혹은 캡슐화가 얼마나 잘 되어 있느냐에 따라 잘 설계된 컴포넌트라고할 수 있다.

장점

  1. 여러 컴포넌트를 병렬로 개발할 수 있어 시스템 개발 속도를 높인다.
  2. 빠르게 파악하여 디버깅할 수 있고 컴포넌트 교체의 부담이 덜하여 시스템 관리 비용을 낮춘다.
  3. 정보 은닉 자체가 성능을 높여주지는 않지만, 성능 최적화에 도움을 준다.
    • 다른 컴포넌트에 영향을 주지 않고 원하는 컴포넌트만 최적화할 수 있다.
  4. 의존적이지 않은 독자적은 컴포넌트는 소프트웨어 재사용성을 높인다.
  5. 완성되지 않은 시스템에서 개별적으로 테스트가 가능해 큰 시스템을 제작하는 난이도를 낮춰준다.

모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다.

  • 접근 제한자를 제대로 활용하는게 정보 은닉의 핵심이다.
  • 풀어서 설명하면 소프트웨어가 올바로 동작하는 한 항상 가장 낮은 접근 수준을 부여해야 한다.
  • 톱레벨 클래스나 인터페이스를 public으로 선언하면 공개 API가 되어 하위 호환을 위해 영원히 관리해줘야 한다.
  • package-private으로 선언하면 해당 패키지 안에서만 사용 가능하여 클라이언트에 피해가 없고 언제든지 수정 가능하여 릴리스에서 수정, 교체, 제가할 수 있다.
  • 한 클래스에서만 사용하는 package-private 톱레벨 클래스나 인터페이스는 이를 사용하는 클래스 안에 private static으로 중첩시켜보자
    • 톱레벨로 두면 같은 패키지의 모든 클래스가 접근할 수 있지만, private static으로 중첩시키면 바깥 클래스 하나에서만 접근할 수 있다.
  • public일 필요가 없는 클래스의 접근 수준을 package-private 톱레벨로 좁히는게 중요하다.
  • 멤버에 부여할 수 있는 접근 수준은 네가지다.
    • private ; 멤버를 선언한 톱레벨 클래스에서만 접근 가능하다.
    • package-private : 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다. 접근 제한자를 명시하지 않았을 떄 적용되는 패키지 접근 수준이다(단, 인터페이스의 멤버는 기본적으로 public이 적용된다.)
    • protected : package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다.
    • public : 모든 곳에서 접근할 수 있다.

클래스 공개 API 설계 방법

  1. 공개 할 API를 설계한다.
  2. 그 외에 모든 멤버는 private으로 만든다.
  3. 오직 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한하여(private 제한자를 제거해) package-private으로 풀어주자.
  4. 만약 권한을 풀어 주는 일을 자주 하게 된다면 컴포넌트를 더 분해해야 하는지 검토한다.
  5. private과 package-private 멤버는 모두 해당 클래스의 구현에 해당하여 공개 API에 영향을 보통에 경우에는 주지 않지만 Serializable을 구현한 클래스에서는 의도치 않게 공개 API가 될 수도 있다.

멤버 접근성을 줄이지 못하는 제약

상위 클래스의 메서드를 재정의할 경우 상위 클래스에서보다 좁게 설정할 수 없다.

상위 클래스의 인스턴스는 하위 클래스의 인스턴스를 대체해 사용할 수 있어야 한다는 규칙(리스코프 치환 법칙)을 지키기 위해 필요하다.

public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.

  • final이 아닌 인스턴스 필드를 public으로 선언하면 필드에 담을 수 있는 값을 제한할 힘을 잃게 되어 해당 필드의 불변식을 보장할 수 없고 필드가 수정될 경우 다른 작업을 할 수 없게 되어 public 가변 필드를 갖는 클래스는 일반적으로 스레드 안전하지 않다. final이면서 불변 객체를 참조하더라도 public 필드를 없애는 방식으로는 리팩토링할 수 없게 된다.
  • 정적 필드에서도 마찬가지이지만 추상 개념을 완성하는데 꼭 필요한 구성요소로써의 상수라면 public static final 필드로 공개해도 된다.

길이가 0이 아닌 배열은 모두 변경 가능 하니 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안된다.

// 보안 허점이 숨어 있다. 해결 방법은 두 가지가 있다.
public static final Thing[] VALUES = { ... };

// 첫 번째 : public 배열을 private으로 만들고 public 불변 리스트를 추가하여 해결한다.
private static final Thing[] PRIVATE_VALUES = {new Thing(1)};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

// 두 번째 : private 배열로 만들고 public 메서드를 추가하는 방어적 복사
private static final Thing[] PRIVATE_VALUES = {new Thing(1)};
public static final Thing[] values() {
    return PRIVATE_VALUES.clone();
}
  • 자바 9에서는 모듈 시스템 개념이 도입 되어 두 가지 암묵적 접근 수준이 추가 되었다.
    • 패키지가 클래스들의 묶음이듯, 모듈은 패키지들의 묶음이다.
      • 모듈은 자신에 속하는 패키지 중 공개(export)할 것들을 (관례상 module-info.java 파일에) 선언한다.
      • 모듈 시스템을 활용하면 클래스를 외부에 공개하지 않으면서도 같은 모듈을 이루는 패키지 사이에서 자유롭게 공유할 수 있다.
      • public 혹은 protected 수준이면서 모듈 내부로 한정되는 변종으로 사용할 수 있는 방법이 위에서 언급 된 소스이다.
  • 4개의 기존 접근 수준과 다르게 모듈에 적용되는 새로운 두 접근 수준은 주의해서 사용해야 한다.
    • 모듈의 jar 파일을 자신의 모듈 경로가 아닌 애플리케이션의 클래스패스(classpath)에 두면 그 모듈 안의 모든 패키지는 마치 모듈이 없는 것처럼 행동한다.

완성된 시스템을 프로파일링해 최적화할 컴포넌트를 정한 다음(아이템67), 클래스 안에 private static으로 중첩시켜보자(아이템 24), Serializable을 구현한 클래스에서는 의도치 않게 공개 API가 될 수도 있다.(아이템86,87), 내부 동작 방식을 API 문서에 적어 사용자에게 공개해야 할 수도 있다.(아이템19), 리스코프 치환 법칙(아이템10), public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.(아이템16), 각 단어 사이에 밑줄(_)을 넣는다(아이템 68)

프로그램 요소의 접근성은 가능한 한 최소한으로 하라. 꼭 필요한 것만 골라 최소한의 public API를 설계하고 그 외에는 클래스, 인터페이스, 멤버가 의도치 않게 API를 공개되는 일이 없도록 해야 한다. public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안 된다. public static final 필드가 참조하는 객체가 불변인지 확인하라.


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

// 퇴보한 클래스
public class Item16 {
    public double x;
    public double y;
}

// 접근자와 변경자 메서드를 활용한 데이터 캡슐화
public class Item16 {
    private double x;
    private double y;

    public Item16(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    public void setX(double x) {
        this.x = x;
    }

    public void setY(double y) {
        this.y = y;
    }
}

자바 플랫폼 라이브러리에서 public 클래스의 필드를 직접 노출하여 규칙을 어긴 사례

  • java.awt.package 패키지의 Point와 Dimension 클래스니 절대 따라하지 말자.

데이터 필드에 직접 접근할 수 있으니 캡슐화의 이점을 제공하지 못한다(아이템15)

public 클래스는 절대 가변 필드를 직접 노출해서는 안 된다.


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

불변 클래스란 인스턴스의 내부 값을 수정할 수 없는 클래스로 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.

예로는 String, 기본 타입의 박싱된 클래스들, biginteger, BigDecimal이 있다.

불변 클래스의 이점으로는 가변 클래스보다 설계와 구현하기 쉬우며 오류가 생길 여지도 줄어들어 훨씬 안전하다.

불변 클래스를 만들기 위한 다섯 가지 규칙

  1. 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
  2. 클래스를 확장할 수 없도록 한다.
    • 하위 클래스에서 부주의하게 혹은 나쁜 의도로 객체의 상태를 변하게 만드는 사태를 막아준다.
  3. 모든 필드를 final로 선언한다.
    • 시스템이 강제하는 수단을 이용해 설계자의 의도를 명확히 드러내는 방법이다.
  4. 모든 필드를 private으로 선언한다.
    • 기술적으로는 기본 타입 필드나 불변 객체를 참조하는 필드를 public final로만 선언해도 불변 객체가 되지만 권하지 않는다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
    • 가변 객체를 참조하는 필드가 있으면 클라이언트가 그 객체의 참조를 얻을 수 없도록 가리키게 하지 않아야 되고 접근자 메서드가 필드 그대로 반환해서는 안되므로 방어적 복사를 수행해야 한다.

불변 객체는 단순하다.

public class Complex {
    private final double re;
    private final double im;

    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);
    }

    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 obj) {
        if (obj == this)
            return true;
        if (!(obj instanceof Complex))
            return false;
        Complex c = (Complex) obj;
        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)";
    }
}
  • 피연산자에 함수를 적용해 그 결과를 반환하지만 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다.
  • 절차적 혹은 명령형 프로그래밍에서는 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변하게 된다.
  • 메서드명에 동사(add 같은) 대신 전치사(plus)를 사용한 이유는 객체의 값을 변경하지 않는다는 사실을 강조하려는 의도이다.

장점

  1. 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요 없다.
    • 여러 스레드가 동시에 사용해도 절대 훼손되지 않는 스레드 안전하게 만드는 가장 쉬운 방법이다.
    • 불변 객체는 안심하고 공유할 수 있으므로 최대한 재활용하기를 권한다.
      • 가장 쉬운 재활용 방법은 자주 쓰이는 값들을 상수로 제공한다. public static final
      • 자주 사용 되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 적정 팩터리를 제공한다.
    • 아무리 복사해봐야 원본과 같으므로 방어적 복사도 필요 없다는 결론이 되므로 clone 메서드나 복사 생성자를 제공하지 않는게 좋다.
      • String 클래스의 복사 생성자는 이 사실을 이해하지 못한 자바 초창기에 만들어졌으므로 사용하지 말아야 한다.
  2. 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
    • ex) BigInteger 클래스의 int 부호와 크기를 negate 메서드의 크기 참조 방법
  3. 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
    • ex) 불변 객체는 맵의 키와 셋의 원소로 사용하면 안에 담긴 값이 바뀌면 불변식이 무너지는데 불변 객체를 사용하면 그런 걱정은 하지 않아도 된다.
  4. 블변 객체는 그 자체로 실패 원자성을 제공한다.
    • 상태가 절대 변하지 않아 불일치 상태에 빠지지 않는다.

단점

  1. 불변 클래스에도 단점이 있는데 값이 다르면 반드시 독립된 객체로 만들어야 한다.
    • 값의 가짓수가 많으면 모두 만드는데 큰 비용을 치뤄야 한다.
    • 원하는 객체를 완성하기까지의 단계가 많고, 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 더 불거지는데 해결 방법은 두 가지이다.
      1. 흔하게 쓰이는 다단계 연산들을 예측하여 기본 기능으로 제공하면 각 단계마다 객체를 생성하지 않아도 된다.
      2. 원하는 족잡한 연산들을 정확히 예측할 수 있다면 package-private의 가변 동반 클래스만으로도 충분하고 예측할 수 없다면 public으로 제공하는게 최선이다.
        • 대표적인 예로는 String 클래스이고 String의 가변 동반 클래스는 StringBuilder이다.

자신을 상속하지 못하도록 하기위한 방법

  • final클래스 선언하는 방법도 있지만 생성자를 private으로 하고 public 정적 팩터리를 제공하는 방법
public class Item17 {
    private double x;
    private double y;

    private Item17(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public static Item17 valueOf(int x, int y) {
        return new Item17(x, y);
    }
}

주의 사항

  • BigInteger와 BigDecimal을 사용 할 경우 불변임을 보장 할 수 없으니 인수로 받은 객체가 '진짜' BigInteger(혹은 BigDecimal)인지 반드시 확인해야 하고 신뢰 할 수 없는 하위 클래스의 인스턴스이고 가변이면 방어적 복사를 해야 한다.
public static BigInteger safeInstance(BigInteger val) {
    return val.getClass() == BigInteger.class ? val : new BigInteger(val.toByteArray());
}
  • 어떤 메서드도 객체의 상태 중 외부에 비치는 값을 변경할 수 없다. 라는 규칙으로 계산 비용이 큰 값을 나중에 계산 하여 final이 아닌 필드에 캐시해두었다가 다시 요청하면 반환하여 계산 비용을 절감하기도 한다.
  • 직렬화할 경우 주의 해야 할 점은 Serialzable을 구현하는 불변 클래스의 내부에 가변 객체를 참조하는 필드가 있다면 readObject나 readResolve 메서드를 반드시 제공하거나, ObjectOutputStream.writeUnshared와 ObjectInputStream.readUnshared메서드를 사용해야 한다. 그렇지 않으면 공격자가 클래스의 가변 인스턴스를 만들어낼 수 있다.

내부 표현을 바꾸지 못하므로 권하지 않는다.(아이템15,16), 정적 팩터리(아이템1), 방어적 복사(아이템50), readObject 메서드(아이템 88), clone 메서드나 복사 생성자를 제공하지 않는게 좋다(아이템13), String 클래스의 복사 생성자(아이템6), 블변 객체는 그 자체로 실패 원자성을 제공한다.(아이템76), hashCode 메서드(아이템11), 지연 초기화(아이템83), 성능(아이템67)

정리

  1. 클래스는 꼭 필요한 경우가 아니라면 불변이어야 하므로 무조건 setter를 만들지 말자
  2. 단점은 특정 상황에서의 잠재적 성능 저하가 있다.
  3. String과 BigInteger처럼 무거운 값 객체도 불변으로 만들 수 있는지 고심하고 성능으로 어쩔 수 없다면 불변 클래스와 쌍을 이루는 가변 동반 클래스를 public 클래스로 제공한다.
  4. 불변으로 만들수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이면 객체가 가질 수 있는 상태가 줄어들어 예측이 쉽고 오류가 생길 가능성이 줄어든다.
  5. 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
  6. 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
  7. 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public으로 제공해서는 안된다.
  8. 객체를 재활용 할 목적으로 상태를 다시 초기화하는 메서드도 복잡성만 커지고 성능 이정이 거의 없다.
  • java.util.concurrent 패키지의 CountDownLatch 클래스가 이상의 원칙을 잘 방증하는데 비록 가변 클래스이지만 가질 수 있는 상태의 수가 많지 않고 인스턴스를 생성해 한번 사용하면 파괴된다.

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

이 책에서는 '상속'을 구현 상속을 말하며 인터페이스 상속과는 무관하다.

상속을 잘 사용한 경우

  • 상위 클래스와 하위 클래스를 모두 같은 프로그래머가 통제하는 패키지 안에서라면 안전한 방법이다.
  • 확장할 목적으로 설계되었고 문서화도 잘 된 클래스도 안전한 방법이다.

상속이 위험한 경우

  • 일반적인 구체 클래스를 패키지 경계를 넘어 다른 패키지의 구체 클래스를 상속하는 일은 위험하다.

메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.

  • 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
    • HashSet의 addAll의 add 자기 사용이 되는 경우
    • 상위 클래스에 새로운 메서드가 추가 되는 경우

컴포지션을 구성하는 방법

기존 클래스를 확장하는 대신 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하면 기존 클래스가 새로운 클래스의 구성요소로 쓰이게 된다.

용어

  • 전달 : 새 클래스의 인스턴스 메서드들은(private 필드로 참조하는) 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환한다.
  • 전달 메서드 : 새 클래스의 메서드들
  • 래퍼 클래스 : 다른 인스턴스를 감싸고 있는 클래스
  • 데코레이터 패턴 : 다른 계측 기능을 덧띄우는 경우
  • 위임 : 전달의 넓은 의미로 래퍼 객체가 내부 객체에 자기 자신의 참조를 넘기는 경우에만 해당

래퍼 클래스의 단점 한 가지

  • 콜백 프레임워크와 어울리지 않는다.
    • 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출 할 때 사용하도록 하는데 내부 객체는 자신을 감싸고 있는 래퍼의 존재를 모르니 래퍼 대신 자신(this)의 참조를 넘기고 내부 객체를 호출하게 된다. 이를 SELF 문제라고 한다.

문서화도 잘 된 클래스(아이템19)

상속은 강력하지만 캡슐화를 해치므로 순수한 is-a 관계일경우에만 사용하고 그 외에는 컴포지션과 전달을 사용하자.


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

상속을 허용하는 클래스가 지켜야 할 제약

      상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이요하는지(자기사용) 문서로 남겨야 한다.
      • 공개 된 API 메서드에서 클래스 자신의 또 다른 재정의가 가능한 메서드라면 어떤 순서로 호출하는지, 각각의 호출 결과가 어이지는지, 어떤 영향을 미치는지를 작성해주어야 한다.
      • "implementation Requirements"로 시작하는 절이 내부 동작을 설명하는 위치이고 메서드 주석에 @implSpec 태그를 붙여주면 자바독 도구가 생성해준다.
      클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.
      • java.util.AbstractList의 removeRange 메서드로 예를 들었다. clear를 호출하여 연산 성능을 크게 개선했다.
      어떤 메서드를 protected로 노출할지 결정할지 고민 된다면 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 '#39;하다.
      • 꼭 필요한 protected 멤버를 놓쳤다면 하위 클래스를 작성할 경우 빈자리를 느낄 수 있다.
      • 반대로 하위 클래스를 여러 개 만들었는데 전혀 쓰이지 않는 protected라면 private이었어야 할 가능성이 크다.
      • 이 중 한 가지는 제 3자가 작성해봐야 한다.
      상속용으로 설계한 클래스는 배포 전 반드시 하위 클래스를 만들어 검증해야 한다. 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.
 public class Super {
 // 잘못 된 예 - 생성자가 재정의 가능 메서드를 호출한다.
     public Super() {
         overrideMe();
     }
     public void overrideMe() {
     }
 }

 // 하위 클래스
 public final class Sub extends Super {
     private final Instant instant;
     Sub() {
         instant = Instant.now();
     }

     // 재정의 가능 메서드. 상위 클래스의 생성자가 호출한다.
     @Override public void overrideMe() {
         System.out.println(instant);
     }

     public static void main(String args[]) {
         Sub sub = new Sub();
         sub.overrideMe();
     }
 }
    clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다. 객체를 새로 만드는 생성자와 비슷낸다.
    • readObject의 경우는 하위 클래스의 상태가 역직렬화되기 이전에 재정의한 메서드부터 호출하게 된다.
    • clone의 경우 하위 클래스의 clone 메서드가 복제본의 상태를 수정하기 전에 재정의한 메서드를 호출한다.
    Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갔는다면 protected로 선언해야 한다.

클래스를 상속용으로 설계하려면 엄청난 노력이 들고 클래스에 안기는 제약도 상담함을 알았다.

상속용으로 설계하지 않은 클래스는 상속을 금지한다.

  • 클래스를 final로 선언하여 금지할 수 있다.
  • 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어주면 금지할 수 있다.
  • 상속을 꼭 해야 하면 내부에서는 재정의 기능 메서드를 호출하지 않게 문서를 남긴다.

기계적인 해범

재정의 가능 메서드는 private '도우미 메서드'로 옮기고, 도우미 메서드를 호출하도록 수정하고 재정의 가능 메서드를 호출하는 다른 코드들도 모두 이 도우미 메서드를 직접 호출하도록 수정하면 된다.

추상 클래스나 인터페이스의 골격 구현(아이템20), 불변 클래스(아이템17)

클래스 내부에서 스스로 어떻게 사용하는지 모두 문서로 남겨야 하고 문서를 반드시 지켜야 한다. 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 낫다.


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

추상 클래스를 구현한 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 되기에 확장에 어려움이 있으니 인터페이스를 규약에 지켜 사용하면 어떤 클래스를 상속하든 같은 타입으로 취급한다.

장점

      기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다.
      • 추상 클래스를 구현하는 모든 클래스의 조상이 되어야 하는 구조로 인해 변경이 자유롭지 않다.
      인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.
      • 믹스인이란 클래스의 '주된 타입'외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.
      인터페이스로는 계층구조가 없는 타입 프레임워클르 만들 수 있다.
        가수와 작곡가 인터페이스로 가수와 작곡를 하는 인터페이스를 만들 수 있다.
public class Item20 {
  interface Singer {
      AudioClip sing(String s);
  }

  interface Songwriter {
      String compose(int chartPosition);
  }

  interface SingerSongwriter extends Singer, Songwriter {
      AudioClip strum();
      void actSensitive();
  }
}
    래퍼 클래스 관용구(컴포지션)을 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다. 디폴트 메서드를 이용해 일감을 줄일 수 있다.

인터페이스와 추상 골격 구현 클래스를 함께 제공하는 방식의 템플릿 메서드 패턴

  • 인터페이스로는 타입을 정의하고 골격 구현 클래스는 나머지 메서드들까지 구현하면 단순히 골격을 확장하는 것만으로 인터페이스를 구현한다.
  • 관례상 인터페이스 이름이 interface라면 골격 구현 클래스의 이름은 AbstractInterface로 짓는게 좋다.
  • 추상 클래스처럼 구현을 도와주는 동시에 타입을 정의할 경우 따라오는 심각한 제약에서는 자유롭다.
static List<Integer> intArrayAsList(int[] a) {
        Objects.requireNonNull(a);
        // 다이아몬드 연산자를 이렇게 사용하는건 자바 9부터 가능하다.
        // 더 낮은 버전을 사용한다면 <Integer>로 수정하자.
        return new AbstractList<Integer>() {
            @Override
            public Integer get(int index) {
                return a[index]; // 오토박싱(아이템 6)
            }

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

            @Override
            public Integer set(int index, Integer element) {
                int oldVal = a[index];
                a[index] = element; // 오토언박싱
                return oldVal; // 오토박싱
            }
        };
    }
  • 골격 구현 작성하는 방법
    1. 인터페이스 메서드 중 다른 메서드들의 구현에 사용 되는 기반 메서드를 선정한다.
    2. 선정 된 메서드들은 추상 메서드가 되고 직접 구현할 수 있는 메서드를 모두 디폴트 메서드로 제공한다.
    3. 단, equals와 hashCode 같은 Object의 메서드는 디폴트 메서드로 제공하면 안 된다.
  • 주의 사항
    1. 인터페이스의 메서드 모두가 기반 메서드와 디폴트 메서드가 되면 만들지 않아도 된다.
  • 골격 구현은 기본적으로 상속해서 사용하는걸 가정하므로 아이템 19에서 이야기한 설계 및 문서화 지침을 모두 따라야 한다.

래퍼 클래스 관용구(아이템18), 자바독 태그를 붙여 문서화해야한다(아이템19), 익명 클래스(아이템24)

일반적으로는 다중 구현용 타입으로는 인터페이스가 가장 적합하고 가능 한 디폴트 메서드도 제공하여 구현한 모든 곳에서 활용 되도록하는게 좋지만 인터페이스에 걸려 있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 더 흔하기 때문이다.


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

래퍼클래스(아이템18), 인터페이스를 더 쉽게 구현해 활용할 수 있게끔 해준다.(아이템20)

  1. 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법이다.
  2. 디폴트 메서드는(컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있다.
  3. 디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야 한다.
  4. 인터페이스를 릴리스한 후라도 결함을 수정하는게 가능한 경우도 있겠지만, 절대 그 가능성에 기대서는 안 된다.

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

인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다.

클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 이야기 해주는것이다.

지침에 맞지 않는 사례

      상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예이다. 상수 인터페이스는 메서드 없이 상수를 뜻하는 static final 필드로만 가인터페이스이다.
        클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당하여 내부 구현을 클래스의 API로 노출하는 행위이다.
public class 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;
}
      상수를 공개할 목적인 경우라면 특정 클래스나 인터페이스와 강하게 연관된 상수라면 해당 클래스나 인터페이스 자체에 추가해야 하며 모기본 타입의 박싱 클래스가 대표적이다.(Integer와 Double에 MIN_VALUE와 MAX_VALUE이다.)
  public class PhysicalConstants {
      private 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;
  }
    java 7부터 허용되는 숫자 리터럴의 밑줄은 값에는 아무런 영향이 없으면서 읽기는 편하게 해주므로 3자리, 5자리 이상이라면 밑줄고려하자. 정적 임포트하여 클래스 이름을 생략할 수 있다. (import static java.util.*;)

열거타입(아이템34), 유틸리티클래스(아이템4)

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


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

태그 달린 클래스의 단점 : 태그 달린 클래스는 장화하고, 오류를 내기 쉽고, 비효율적이다.

  • 열거 타입 선언, 태그 필드,switch문 등 쓸데 없는 코드가 많다.
  • 여러 구현이 한 클래스에 혼합돼 있어서 가독성이 나쁘다.
  • 다른 의미를 위한 코드도 있어 메모리도 많이 사용한다.
  • 필드들을 final로 선언하면 필요하지 않는 불필요한 코드가 늘어난다.
  • 태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일뿐이다.
public 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);
        }
    }

}

클래스 계층구조를 활용하는 서브타이핑을 활용하는 방법

  1. 계층구조의 루트가 될 추상 클래스를 정의한다.
  2. 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상메서드로 선언한다.(Figure 클래스의 area 메서드가 해당 됨)
  3. 태그 값에 상관 없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가한다.
  4. 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들도 전부 루트 클래스로 올린다.
public class Item23 {
    abstract class Figure {
        abstract double area();
    }

    class Circle extends Figure {
        final double radius;
        Circle(double radius) { this.radius = radius; }

        @Override
        double area() {
            return Math.PI * (radius * radius);
        }
    }

    class Rectangle extends Figure {
        final double lenth;
        final double width;

        Rectangle(double lenth, double width) {
            this.lenth = lenth;
            this.width = width;
        }

        @Override
        double area() {
            return lenth * width;
        }
    }
}

태그 달린 클래스를 써야 하는 상황은 거의 없다. 태그를 없애고 계층구조로 대체하는 방볍을 생각해보자. 기존 클래스가 태그 필드를 사용하고 있다면 계층구조로 리팩터링을 고민하자.


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

중첩 클래스는 클래스 안에 클래스가 정의 된 클래스로 바깥 클래스에서만 쓰여야 하며, 그 외에 쓰인다면 톱레벨 클래스로 만들어야 한다.

중첩 클래스의 정류는 정적 멤버 클래스, (비정적) 멤버 클래스, 익명 클래스, 지역 클래스가 있으며 정적 멤버 클래스를 제외 한 클래스는 내부 클래스에 해당한다.

중첩 클래스를 언제, 왜 사용해야하는지 이야기한다.

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

  • 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없기 때문에 정적 멤버 클래스로 만들어야한다.
  • 바깥 클래스의 인스턴스 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지는게 보통이지만, 드물게 직접 '바깥 인스턴스의 클래스.new MemberClass(args)를 호출해 수동으로 만들기도 하지만 메모리도 더 차지하고 생성 시간도 더 걸린다.

정적 멤버 클래스

  • 클래스 안에 선언되고 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스와 똑같다.
  • 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 사용된다.

비정적 멤버 클래스

  • 정적 멤버 클래스와 차이는 static이 붙어 있고 차이이며 바깥 클래스의 인스턴스와 암묵적으로 연결되어 있다.
  • 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 클래스명.this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.
  • 어댑터를 정의할 경우 자주 쓰이는데 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용한다.

멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.

익명클래스는 이제 람다가 대체하고 있다.

  • 멤버와 달리 쓰이는 시점에 선언과 동시에 인스턴스가 만들어져 어디서든 만들 수 있다.
  • 익명 클래스를 사용하는 클라이언트는 익명 클래스가 상위 타입에서 상속한 멤버 외에는 호출할 수 없다.

지역 클래스는 네 가지 중첩 클래스 중 가장 드물게 사용된다.

  • 지역 변수를 선언할 수 있는 곳이면 어디서든 선언할 수 있고 유효 범위도 지역변수와 같다.
  • 다른 세 중첩 클래스의 공통점을 하나씩 가지고 있는데 멤버 클래스처럼 이름이 있어 반복해서 사용 가능하며 익명클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있고 정적 멤버는 가질 수 없다.

열거타입(아이템34), 메모리 누수(아이템7), 람다(아이템42)

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


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

소스 파일 하나에 톱레벨 클래스를 여러 개 선언하더라도 문제는 없지만 심각한 위험을 감수해야 한다.

public class Item25 {
    class Utensil{
        static final String NAME = "pan";
    }
    class Dessert {
        static final String NAME = "cake";
    }

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

문제가 생길만한 상황

  • Dessert.java를 생성한다면 컴파일 오류로 중복 정의했다고 알려주지만 참조를 만다면 Item25에 정의 된 Dessert와 Dessert.java 둘 다 찾으려고 하며 둘 중 어느걸 호출 하냐에 따라 결과가 달라진다.

문제를 회피하는 방법

  • 톱레벨 클래스들을 서로 달느 소스 파일로 분리하면 된다.
    • 굳이 한 파일에 담고 싶으면 정적 멤버 클래스를 사용하는 방법을 고민할 수 있다.
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";
    }
}

정적 멤버 클래스(아이템24), private으로 선언하면(아이템15)

소스 파일 하나에는 반드시 톱레벨(혹은 톱레벨 인터페이스)를 하나만 담자.


반응형