📌 4장 : 클래스와 인터페이스

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

  • 잘 설계된 컴포넌트 : 모든 내부 구현을 완벽히 숨겨, 구현과 API를 깔끔하게 분리한다.
  • 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개되는 일이 없도록 해야 한다.
  • public 클래스에서 public static final 이외에 어떤 public 필드가 있어서는 안된다.
  • public static final 필드가 참조하는 객체가 불변인지 확인한다.
  • 정보 은닉의 장점
    • 시스템을 구성하는 컴포넌트들을 서로 독립시켜서 개발, 테스트, 최적화, 적용, 분석, 수정을 개별적으로 할 수 있게 해주는 것과 연관되어 있다.
    • 시스템의 개발 속도를 높인다. (여러 컴포넌트를 병렬로 개발)
    • 시스템 관리 비용을 낮춘다. (각 컴포넌트 교체 부담이 적다.)
    • 성능 최적화에 도움을 준다.
    • 소프트웨어 재사용성을 높인다. 외부에 거의 의존하지 않고, 독자적으로 동작 가능한 컴포넌트는 낯선 환경에서도 유용하게 쓰일 수 있다.
    • 큰 시스템을 제작하는 난이도를 낮춰준다.
  • 접근 제어 메커니즘
    • 클래스, 인터페이스, 멤버의 접근성(접근 허용 범위)을 명시한다.
    • 각 요소의 접근성은 선언된 위치와 접근 제한자로 정해진다.
    • 접근 제한자를 제대로 활용하는 것이 정보 은닉의 핵심
  • 기본 원칙
    • 모든 클래스와 멤버의 접근성을 가능한 좁혀야 한다.
  • private : 멤버를 선언한 톱레벨 클래스에서만 접근 가능
  • package-private : 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다.
    • 클라이언트에 피해 없이 다음 릴리스에서 수정, 교체, 제거 가능
  • protected : package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근 가능
  • public : 모든 곳에서 접근 가능. 공개 API, 하위 호환을 위해 계속 관리 필요
  • 상위 클래스의 메서드를 재정의할 때는 접근 수준을 상위 클래스에서보다 좁게 설정할 수 없다.
    • 상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체해 사용할 수 있어야 한다.(리스코프 치환 원칙)
  • public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.
    • 필드가 가변 객체를 참조하거나 final이 아닌 인스턴스 필드를 public으로 선언하면 그 필드에 담을 수 있는 값을 제한할 힘을 잃는다.
    • 꼭 필요한 구성요소의 상수일 경우 public static final 필드로 공개해도 된다.
  • 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공하면 안된다.
    • 길이가 0이 아닌 배열은 모두 변경이 가능하다.
    • 클라이언트에서 이런 접근자를 제공하면 그 배열의 내용을 수정할 수 있게 되기 떄문이다.
  • 첫 번째 방법 : public 배열을 private으로 만들고 public 불변 리스트를 추가한다.
private static final Thing[] PRIVATE_VALUES = { ... }; 
public static final List<Thing> VALUES =
    Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
  • 두 번째 방법 : 배열을 private 으로 만들고 복사본을 반환하는 public 메서드를 추가(방어적 복사)
private static final Thing[] PRIVATE_VALUES = { ... }; 
public static final Thing[] values() {
    return PRIVATE_VALUES.clone(); }

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

  • 데이터 필드에 직접 접근할 수 있지만, 캡슐화의 이점을 제공할 수 없는 상황
class Point {
    public double x;
    public double y; 
}

필드들을 모두 private으로 바꾸고 public 접근자(getter) 추가

class Point {
    private double x; 
    private double y;
    
    public Point(double xf 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; }
}
  • 접근자와 변경자(mutator) 메서드를 활용해 데이터를 캡슐화한다.
  • package-private 클래스 혹은 private 중첩 클래스에서는 데이터 필드를 노출해도 문제가 없다.
  • 패키지 바깥 코드는 변경하지 않아도 데이터 표현 방식을 바꿀 수 있다.

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

  • 불변 클래스 : 인스턴스 내부 값을 수정할 수 없는 클래스
    • 가변 클래스보다 설계, 구현, 사용이 간단하고 오류가 생길 여지가 적다.
  • 자바 플랫폼 라이브러리의 불변 클래스 : String, 기본 타입의 박싱된 클래스, BigInteger, BigDecimal
  • 불변 클래스 만드는 5가지 규칙
    • 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
    • 클래스 확장을 할 수 없게 한다. ex. final
    • 모든 필드를 final으로 선언한다.
    • 모든 필드를 private으로 선언한다.
    • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
      • 클래스에 가변 객체를 참조하는 필드가 있으면, 클라이언트에서 객체를 참조할 수 없게 해야 한다.
      • 생성자, 접근자, readObject 메서드에서 방어적 복사를 수행해라
  • 함수형 프로그래밍
    • 피연산자에 함수를 적용해서 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴
    • 절차적 혹은 명령형 프로그래밍은 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변한다.
public final class Complex { 
    private final double re; 
    private final double im;

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

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

    public Complex plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }
    
    ...
}
  • 불변 객체는 근본적으로 스레드 안전하여 따로 동기화가 필요없다.
  • 불변 클래스는 한 번 만든 인스턴스를 최대한 재활용한다.
  • 가장 쉬운 재활용 방법 : 자주 쓰이는 값들을 상수로 제공한다. ex. public static final
  • 불변 클래스는 자주 사용되는 인스턴스를 캐싱해서 같은 인스턴스를 중복 생성하지 않도록 정적 팩터리를 제공할 수 있다.
  • 정적 패터리를 사용하면 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용을 줄어든다.
  • public 생성자 대신에 정적 팩터리를 만들어두면, 클라이언트를 수정하지 않아도 캐시 기능을 덧붙일 수 있다.
  • 방어적 복사도 필요 없기 때문에 clone 메서드나 복사 생성자를 제공하지 않는 것이 좋다.
  • 불변 객체는 자유롭게 공유할 수 있고, 불변 객채꼐리 내부 데이터를 공유할 수 있다.
  • 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
  • 불변 객체는 그 자체로 실패 원자성을 제공한다.
  • 단점으로는 값이 다르면 반드시 독립된 객체로 만들어야 한다는 점이다.
  • 중간 단계에서 만들어진 객체들이 모두 버려지는 경우 성능 문제
    • 문제 대처 방법 : 다단계 연산(multistep operation) 들을 예측해서 기본 기능으로 제공하는 방법
      • 클라이언트가 원하는 복잡한 연산들을 정확하게 예측하면 package-private의 가변 동반 클래스만으로도 충분하다.
  • 모든 클래스를 불변으로 만들 수 없다. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄인다.
  • 다른 합당한 이유가 없으면 모든 필드는 private final 이어야 한다.
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.

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

  • 상속 : 클래스가 다른 클래스를 확장하는 구현 상속
  • 메서드 호출과 다르게 상속은 캡슐화를 깨뜨린다.
    • 상위 클래스에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
  • 잘못된 상속 예시 : addAll 메서드로 원소 3개를 더했을 때
InstrumentedHashSet<String> s = new InstrumentedHashSeto();
s.addAlKList.of("틱", "탁탁", "펑,,));
  • getAddCount 메서드를 호출하면 3을 반환해야 하지만 6을 반환한다.
  • HashSet의 addAll 메서드가 add 메서드를 사용해 구현되어서 addCount에 값이 중복해서 더해져서 최종 값이 6으로 늘어났다.
  • 하위 클래스에서 addAll 메서드를 재정의하지 않으면 문제를 고칠 수 없다.
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.sizeO;
        return super.addAll(c);
    }
    
    public int getAddCount() { 
        return addCount;
    }
}
  • 컴포지션(composition) : 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하는 방법
  • 새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해서 결과를 반환한다.
  • 래퍼 클래스
    • 다른 Set 인스턴스를 감싸고 있는 뜻에서 래퍼 클래스라고 한다.
    • 다른 계측 기능을 덧씌운다는 뜻에서 데코레이터 패턴(decorator pattern) 이라고도 부른다.
    • 단점이 거의 없지만, 래퍼 클래스가 콜백(callback) 프레임워크와는 어울리지 않는다는 점을 주의해야 한다.
public class InstrumentedSet<E> extends Fon시ardingSet<E> {
    private int addCo나nt = 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;
}
  • 재사용할 수 있는 전달(forwarding) 클래스
public class ForwardingSet<E> implements Set<E> { 
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }
    
    public void clear() { s.clearO; }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty() { return s.isEmpty(); }
    public int size() { return s.size(); }
    public Iterator<E> iterater() { 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.addAH(c); } 
    public boolean removeAll(Collection<?> c){ return s. removeAH(c); } 
    public boolean retainAlKCollection<?> c){ return s. retainAlKc); } 
    public Object[] toArrayO { 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.hashCodef); } 
    @Override public String toString() { return s.toString(); }
}

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

  • 상속용 클래스는 재정의할 수 있는 메소드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
  • 내부 메커니즘을 문서로 남기는 것이 상속을 위한 설계의 전부는 아니다.
  • 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있따.
  • 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일하다.
  • clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
  • clone이 잘못되면 복제본말고도 원본 객체에도 피해를 줄 수 있다.
  • 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이 나을 수 있다.
  • 상속을 금지하는 방법 2가지
    • 클래스를 final으로 선언하는 방법
    • 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법
      • 생성자 모두를 외부에서 접근할 수 없도록 만든다.

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

  • 자바가 제공하는 다중 구현 메커니즘 : 인터페이스, 추상클래스
  • 두 메커니즘 공통점 : 인스턴스 메서드를 구현 형태로 제공 가능
  • 차이점 : 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다.
    • 추상 클래스 방식은 새로운 타입을 정의하는 데 커다란 제약이 생긴다.
    • 인터페이스 방식은 선언한 메서드를 모두 정의하고 일반 규약을 잘 지킨 클래스라면 어떤 클래스를 상속해도 같은 타입으로 취급된다.
  • 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.
  • 인터페이스가 요구하는 메서드를 추가하거나 클래스 선언에 implements 구문만 추가하면 된다.
  • 추상 클래스 방식은 클래스 계층 구조에 혼란을 일으켜 새로 추가된 추상 클래스의 모든 자손이 상속을 하게 되는 문제가 생길 수 있다.
  • 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.
  • 믹스인 : 클래스가 구현할 수 있는 타입.
    • 믹스인을 구현한 클래스에 원래의 '주된 타입' 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.
    • 대상 타입의 주된 기능에 선택적 기능을 혼합한다고 해서 믹스인이라고 부른다.
  • 인터페이스로 계층 구조가 없는 타입 프레임워크를 만들 수 있다.
  • 타입을 계층적으로 정의하면 개념을 구조적으로 잘 표현 가능하지만, 표현이 어려운 개념도 있다.
public interface Singer { 
    AudioClip sing(Song s);
}

public interface Songwriter {
    Song compose(int chartPosition);
}
  • 작곡도 하는 가수가 있기 때문에 가수 클래스가 Singer와 Songwriter 모두를 구현해도 문제가 되지 않는다.
  • 가능한 조합 전부를 각각의 클래스로 정의한 복잡한 계층 구조가 만들어질 수 있다.
  • 래퍼 클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다.
  • 인터페이스와 추상 골격 구현(skeletal implementation) 클래스를 함께 제공하는 방법으로 인터페이스와 추상 클래스 장점을 모두 취하는 방법도 있다.
  • 템플릿 메서드 패턴 : 인터페이스로 타입 정의, 필요하면 디폴트 메서드도 제공, 골격 구현 클래스는 나머지 메서드까지 구현한다.
  • 인터페이스 이름이 Interface 이면, 골격 구현 클래스 이름은 AbstractInterface
  • 골격 구현을 사용해 완성한 구체 클래스 예시
static List<Integer> intArrayAsList(int[] a) {
    Objects.requireNonNull(a);
    
    // 다이아몬드 연산자를 이렇게 사용하는 건 자바 9부터 가능하다. 
    // 더 낮은 버전을 사용한다면 <Integer>로 수정하자.
    return new AbstractListo() {
        @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;
        } 
    };
}

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

  • 자바 8 이전에는 추가된 메서드가 우연히 기존 구현체에 이미 존재할 가능성이 낮기 때문에, 기존 구현체를 깨뜨리지 않으면 인터페이스에 메소드를 추가할 방법이 없었다.
  • 자바 8 이후부터는 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트가 생겼지만, 위험이 아예 없어진 것은 아니다.
    • 디폴트 메서드를 선언하면, 인터페이스 구현 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다.
    • 모든 기존 구현체들이 매끄럽게 연동되는 보장이 없다.
  • 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드 작성은 어렵다.
  • 디폴트 메서드는 컴파일은 성공해도, 기존 구현체에 런타임 오류를 일으킬 수 있다.
  • 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 것이 아니면 피해야 한다.
  • 새로운 인터페이스를 만드는 경우에는 표준적인 메서드 구현을 제공하는 데 유용한 수단이 된다.

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

  • 인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할
  • 자신의 인스턴스로 무엇을 할 수 있는지 클라이언트에게 알려줄 수 있다.
  • 지침에 맞지 않는 예시 : static final 필드로만 가득찬 인터페이스
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; 
}

아이템 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);
        }
    }
}
  • 태그가 있는 클래스 단점
    • 열거 타입 선언, 태그 필드, switch 문 등등 쓸데없는 코드가 너무 많아 가독성이 나쁘다.
    • 많은 메모리 사용
    • 필드를 final로 선언하려면 쓰이지 않는 필드도 생성자에서 초기화해야 한다.
    • 태그 달린 클래스는 장황하고, 오류가 생기기 쉽고, 비효율적이다.
  • 자바와 같은 객체지향 언어에서는 타입 하나로 다양한 의미의 객체를 표현하는 나은 수단을 제공한다.
    • 클래스 계층 구조를 활용하는 서브타이핑(subtyping)
  • 태그 달린 클래스를 클래스 계층 구조로 변환
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 length;
    final double width;
    
    Rectangle(double length, double width) { 
        this.length = length;
        this.width = width;
    }

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

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

  • 중첩 클래스(nested class)
    • 다른 클래스 안에 정의된 클래스
    • 자신을 감싼 바깥 클래스에서만 사용해야 한다.
    • 종류 : 정적 멤버 클래스. (비정적) 멤버 클래스, 익명 클래스, 지역 클래스
    • 정적 멤버 클래스를 제외한 나머지는 내부 클래스(inner class)
  • 정적 멤버 클래스
    • 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에 접근할 수 있다.
    • 바깥 클래스와 함께 쓰일 때 유용한 public 도우미 클래스로 쓰인다.
    • 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있으면 정적 멤버 클래스로 만들어야 한다.
  • 비정적 멤버 클래스
    • 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.
    • 정규화된 this : 클래스명.this
    • 바깥 인스턴스 없이 생성할 수 없다.
    • 비정적 멤버 클래스의 인스턴스 안에 관계 정보가 만들어져 메모리 공간을 차지하고, 생성 시간이 더 걸린다.
    • 어댑터 정의에 주로 사용된다.
    • 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없으면 정적 멤버 클래스로 만들어준다.
  • 익명 클래스
    • 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다.
    • 선언한 지점에서만 인스턴스를 만들 수 있고, 클래스 이름이 필요한 작업은 수행할 수 없다.
    • 여러 인터페이스를 구현할 수 없고, 인터페이스를 구현하는 동시에 다른 클래스를 상속할 수 없다.
    • 주로 사용되는 경우는 팩터리 메서드 구현
  • 지역 클래스
    • 가장 드물게 사용되는 중첩 클래스
    • 지역변수를 선언할 수 있는 곳이면 어디서든 선언 가능
    • 유효 범위도 지역변수와 같다.
    • 멤버 클래스처럼 이름이 있고 반복해서 사용 가능
    • 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있고, 정적 멤버는 가질 수 없고, 가독성을 위해 짧게 작성해야 한다.

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

  • 컴파일러가 한 클래스에 대한 정의를 여러 개 만들지 않도록, 소스 파일 하나에 톱레벨 클래스를 하나만 담는다.

+ Recent posts