클래스와 인터페이스에서는 추상화의 기본 단위인 클래스와 인터페이스를 쓰기 편하고, 견고하며, 유연하게 만드는 방법을 안내합니다.
아이템 15. 클래스와 멤버의 접근 권한을 최소화하라
- 컴포넌트의 설계에 대한 수준은 클래스 내부 데이터와 구현 정보를 외부로부터 얼마나 잘 숨겼느냐에 결정됨
- 즉, 잘 설계된 컴포넌트는 내부를 잘숨겨 구현과 API를 깔끔히 분리됨
- 이는 캡슐화라고 하는 소프트웨어 설계의 근간이 되는 원리
자바에서 캡슐화시 주의점
- 모든 클래스와 멤버의 접근성을 가능한 좁혀야함
- public 클래스의 인스턴스의 멤버는 되도록 public이 아니어야 함
- 클래스의 멤버로 public static final 배열이 존재하거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안됨
아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
- public 클래스의 멤버를 public하면 캡슐화의 이점을 제공하지 못함
- 멤버를 private로 정의하고 접근자(getter) 메서드를 제공해야함
- package-private 클래스 또는 private 중첩클래스라면 멤버가 public이어도 상관없음
아이템 17. 변경 가능성을 최소화하라
- 불변 클래스란 인스턴스 내부의 멤버를 수정할 수 없는 클래스를 의미함
- 자바에는 여러가지의 불변 클래스가 존재
- String
- 기본타입의 박싱된 클래스
- BigInteger, BigDecimal
- 클래스를 불변으로 만들려면 다음과 같은 규칙을 따르면됨
- 객체의 상태를 변경하는 메서드를 제공하지 않음
- 상속을 막음 (final선언 등)
- 모든 필드를 final로 선언
- 모든 필드를 private로 선언
- public final은 다음 배포에서 수정할 수 없다는 단점이 존재
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 함
- 생성자, 접근자, readObject메서드에서 모두 방어적 복사를 수행해야함
- 불변 객체는 근본적으로 스레드에 안전하며 따로 동기화 할 필요가 없음
- 객체를 만들때 구성요소로 불변 객체를 사용하면 이점이 많음
- 구조가 아무리 복잡하더라도 불변식을 유지하기 훨씬 수월하기 때문
- 불변 객체는 그 자체로 실패 원자성을 제공
- 상태가 절대 변하지 않기 때문에
- 불변 클래스에도 단점은 존재
- 값이 다르면 반드시 독립된 객체로 만들어야한다는 점
- 클래스는 꼭 필요한 경우가 아니라면 불변이어야 함
- 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소화해야함
- 합당한 이유가 없다면 모든 필드는 private final이어야 함
아이템 18. 상속보다는 컴포지션을 사용하라
- 상속은 코드를 재사용하는 강력한 수단이지만, 잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게 됨
- 다른 패키지의 구체 클래스를 상속하는일은 위험함
- 메서드 호출과 달리 상속은 캡슐화를 깨뜨림
- 상위 클래스의 구현에 따라 하위 클래스에 이상이 생길수 있음
- 상위 클래스의 구현이 달라질 수 있음
- 만약 다음과 같이 HashSet을 상속해서 사용한다고 가정했봄
public class InstrumentedHashSet<E> extends HashSet<E> {
...
}
- 원소가 몇개 존재하는지 확인하는 getAddCount 메서드가 존재
- addAll을 통해 3개의 원소를 추가했을경우 getAddCount의 반환값이 3이라고 예상될 수 있지만 실제로는 그렇지 않음
- 이유는 HashSet의 addAll이 add 메서드를 사용하기 때문에 super.addAll 내부에서 InstrumentedHashSet에 재정의된 add를 호출, addCount++ 의 호출로 입력개수보다 2배의 카운팅이 적용됨
- 이경우 하위 클래스에서 addAll을 재정의하지 않음으로써 해결은 가능
- 이처럼 자신의 다른 부분을 사용하는 가지사용(self-use) 여부는 해당 클래스의 내부 구현 방식에 해당하며 자바 플랫폼 전반적인 정책인지, 다음 릴리스에서 유지될지 알수 없으므로 InstrumentedHashSet은 깨지기 쉬움
- 메서드 재정의 대신 새로운 메서드를 정의해서 사용하는것도 해결책이 되지만 다음 릴리즈때 내가 추가한 메서드와 시그니처가 같은 메서드가 상위 클래스에 정의될 수 있음, 이경우 컴파일도 안됨
상속이 아닌 컴포지션
- 위와 같이 상속시 발생하는 문제를 해결하기 위한 가장 좋은 방법은 상속대신 private 멤버로 해당 클래스를 참조하는것
- 이러한 설계를 컴포지션(composition)이라 함
- 새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환
- 이방식을 전달(forwarding)이라 하며, 새 클래스의 메서드들을 전달 메서드(forwarding method)라 함
public class InstrumentedSet<E> extends ForwardingSet<E> {
...
}
public class ForwardingSet<E> implements Set<E> {
...
}
Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp));
Set<E> s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));
- 다른 Set 인스턴스를 감싸고 있다는 뜻에서 InstrumentedSet 같은 클래스를 래퍼 클래스라고 함
- 다른 Set에 계측 기능을 덧씌운다는 뜻에서 데코레이터 패턴(Decorator pattern) 이라고 함
- 컴포지션과 전달의 조합은 넓은 의미로 위임(delegation)이라고 부름
- 래퍼 클래스는 단점이 거의 없지만, 콜백 프레임워크와는 어울리지 않는점만 주의하면 됨
- 상속은 하위클래스가 상위클래스와 is-a 관계일 경우만 사용되어야 함
아이템 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
- 메서드를 재정의하면 어떤일이 일어나는지 정확하게 정리하여 문서화해야함
- 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야함
- 재정의 가능한 메서드에서 다른 재정의 가능한 메서드를 호출할 경우 문서로 남겨야함
- API문서의 끝에 implementation requirements로 시작하는 절을 볼 수 있는데 메서드의 내부 동작 방식을 설명하는 곳임
- @implSpec 태그를 붙여주면 자바독 도구가 생성해줌
- 이처럼 내부 매커니즘을 문서로 남기는 것만이 상속을 위한 설계의 전부는 아님
- 효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면, 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있음
- 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는것이 유일함
- 상속용 클래스의 생성자에서 재정의 가능한 메서드를 호출해서는 안됨
- Cloneable과 Serializable 인터페이스는 상속용 설계의 어려움을 한층 더해줌
- clone과 readObject 모두 재정의 가능 메서드를 호출해서는 안됨
- 클래스를 상속용으로 설계하려면 엄청난 노력이 들고 그 클래스에 안기는 제약도 상당함
아이템 20. 추상 클래스보다는 인터페이스를 우선하라
- 자바는 다중 구현 메커니즘으로 인터페이스와 추상 클래스를 제공
-
자바8 부터는 디폴트 메서드를 제공할 수 있게되어 이제는 두 메커니즘 모두 인스턴스 메서드를 구현 형태로 제공가능 *
- 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있음
- 인터페이스가 요구하는 메서드를 추가하고 클래스에서 인터페이스에 대한 implements를 추가하면됨
- 인터페이스는 믹스인(mixin) 정의에 안성맞춤
- 믹스인이란 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 주된 타입 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 줌
- 예로들면 Comparable은 자신을 구현한 클래스들 끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스
-
인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있음
-
레퍼 클래스 관용구(아이템18)와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 됨
- 단순 구현(simple implementation)은 골격 구현의 작은 변종
- AbstractMap.SimpleEntry가 좋은 예 *
아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라
- 자바 8부터는 인터페이스에 디폴트 메서드 선언이 가능하여 구현체의 수정없이 새로운 메서드를 인터페이스에 추가 가능
- 디폴드 메서드를 선언하면, 그 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 됨
- 하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운일
- 디폴트 메서드는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 발생시킬 수 있음
- 인터페이스를 릴리스한 후라도 결함을 수정하는게 가능한 경우도 있지만, 절대 그 가능성에 기대서는 안됨
아이템 22. 인터페이스는 타입을 정의하는 용도로만 사용하라
- 인터페이스는 자신을 구현하는 클래스를 참조하는 타입역할을 함
- 참조역할 아닌 인터페이스 내부에 상수를 구현해서 사용하는 것은 안티패턴임
public interface MathConstants {
static final double PI = 3.14;
}
- 클래스 내부에서 사용할 상수를 인터페이스에 정의하는 것은 내부 구현을 외부로 노출하는 행위, 사용자에게 의미도 없고 혼란만 줌
- 해당 인터페이스를 구현한 클래스나 구현한 클래스의 하위 클래스의 이름공간이 인터페이스에 정의된 상수로 오염됨
아이템 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라
- 다음과 같이 하나의 클래스에 현재 상태표현을 태그로 알려주는 클래스보다는 계층구조를 사용해야함
- 다음 클래스는 Shape 멤버로 현재 상태를 의미
class Figure {
enum Shape { RECTANGE, CIRCLE} ;
double length;
double width;
double radius;
}
태그 기반 표현의 단점
- 내부에 열거형 선언, 태그 필드, 공통로직에서 swtich 등 쓸데없는 코드가 많아짐
- 한가지 타입에 포함될 것이지만 두가지 타입을 처리하는 로직이 함께 존재해 메모리 사용랴잉 많아짐
- 사용하지 않을 타입에서 사용할 final 필드도 같이 초기화 해야함
- 즉 클래스가 장황해지고 오류를 내기 쉽고 비효율적임
클래스 계층구조를 이용
- 위 클래스를 계층구조로 나타내면 다음과 같음
abstract class Figure {...}
class Circle extends Figure {
double radius;
...
}
class Rectangle extends Figure {
double length;
double width;
...
}
아이템 24. 멤버 클래스는 되도록 static으로 만들라
- 중첩 클래스(nested class)란 자신을 감싼 클래스에서만 사용되어야함
- 만약 다른곳에서도 사용된다면 톱레벨 클래스로 변경해야함
- 다음과 같은 종류의 중첩 클래스가 존재
- 정적 멤버 클래스
- 비정적 멤버 클래스
- 익명 클래스
- 지역 클래스
- 첫 번째 외에는 내부 클래스(inner class)
- 이번 아이템에서는 각각의 중첩 클래스가 어떤경우에 사용되는지 정리함
정적 멤버 클래스
- 다른 클래스안에 선언됨
- 바깥 클래스의 private 멤버에 접근가능
- 바깥 클래스와 함께 사용될 때만 유용한 public 도우미 클래스로 사용됨
- 비정적과는 static 구문만 차이나지만 의미상으로는 차이가 큼
비정적 멤버 클래스
- 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결됨
- 비정적 멤버 클래스의 인스터스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있음
- 정규화된 this란 클래스이름.this 란 형태로 바깥 클래스의 이름을 명시하는 용법을 의미
- 따라서 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적인 존재라면 정적 멤버 클래스로 만들어야함
- 비정적 멤버 클래스는 어댑터를 정의할 때 자주 사용됨
- 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용됨
- 외부에서 바깥 인스턴스의 클래스.new MemberClass(args)로 생성되기도 함
- 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들어야함
익명 클래스
- 익명 클래스는 이름이 없고 선언과 동시에 인스턴스가 생성됨
- 오직 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조가능
- 정적 문맥에서도 상수 이외의 정적 맴버를 가질 수 없음
- 즉 final 기본타입과 문자열 필드만 가질 수 있음
지역 클래스
- 지역 변수를 선언할 수 있는 곳이면 어디서든 선언가능
- 지역 변수와 유효범위가 같음
- 맴버 클래스처럼 이름이 있음
- 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있음
- 정적 맴버는 가질 수 없음
아이템 25. 톱레벨 클래스는 한 파일에 하나만 담으라
- 하나의 소스 파일에는 하나의 톱 레벨 클래스(또는 톱 레벨 인터페이스)만 작성해야함
- 톱 레벨 클래스란 멤버 클래스가 아닌 클래스