Item 15. 클래스와 멤버의 접근 권한을 최소화하라
- 잘 설계된 컴포넌트는 모든 내부 구현을 완벽히 숨겨, 구현과 API를 깔끔히 분리한다.
- 정보 은닉의 장점
- 시스템 개발 속도를 높인다. 여러 컴포넌트를 병렬로 개발할 수 있기 때문
- 시스템 관리 비용을 낮춘다. 각 컴포넌트를 더 빨리 파악하여 디버깅할 수 있고, 다른 컴포넌트로 교체하는 부담도 적다
- 성능 최적화에 도움을 준다. 완성된 시스템을 프로파일링해 최적화할 컴포넌트를 정한 다음, 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화할 수 있기 때문이다.
- 소프트웨어 재사용성을 높인다. 외부 의존도가 낮은 컴포넌트라면 유용하게 쓰일 가능성이 높다.
- 큰 시스템을 제작하는 난이도를 낮춘다.
- 정보 은닉 방법
- 선언된 위치
- 접근 제한자 (private, package-private, protected, public)
- 기본 원칙: 모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다!
- private: 멤버를 선언한 톱레벨 클래스에서만 접근할 수 있다.
- package-private: 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다. (접근 제한자를 명시하지 않는 경우의 수준, 단, 인터페이스의 멤버는 기본적으로 public 적용)
- protected: package-private 범위를 포함하며 해당 클래스의 하위 클래스에도 접근할 수 있다.
- public: 모든 곳에서 접근할 수 있다.
- 제약: 상위 클래스의 메서드를 재정의할 때는 접근 수준을 상위 클래스에보다 좁게 설정할 수 없다.
- 코드를 테스트하려는 목적으로 접근 범위를 넓힌다 하더라도 private에서 package-private 수준까지 가능하다.
- public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다. public 가변 필드를 갖는 클래스는 일반적으로 스레드에 안전하지 않다. 불변 객체를 참조하더라도 문제가 발생할 수 있다.
- 예외로는 publis static final(상수) 필드로 공개해도 좋다. 하지만 배열인 경우(변경 가능), 해당 필드를 반환하는 접근자 메서드를 제공해서는 안된다.
public static final Thing[] PRIVATE_VALUES = {...}; //안좋은 방법 변경 가능 // 해결방법 1. public을 private으로 두고, public 리스트 생성 private static final Thing[] PRIVATE_VALUES = {...}; public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES)); // 해결방법 2. 배열을 private으로 만들고, 그 복사본을 반환하는 public 메서드를 추가 private static final Thing[] PRIVATE_VALUES={...}; public static final Thing[] values() { return PRIVATE_VALUES.clone(); }
- 예외로는 publis static final(상수) 필드로 공개해도 좋다. 하지만 배열인 경우(변경 가능), 해당 필드를 반환하는 접근자 메서드를 제공해서는 안된다.
Item 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
- public 클래스에 경우 인스턴스를 private으로 선언하고, getter/setter 로 접근하도록 한다.
- package-private, private 중첩 클래스라면 데이터 필드를 노출한다 해도 문제가 없다.
- 클래스 내부 표현에 묶이기는 하나, 패키지 안에서만 동작하는 코드일 뿐이며, 패키지 바깥 코드는 전혀 손대지 않고도 데이터 표현 방식을 바꿀 수 있다.
- private 중첩 클래스의 경우라면 수정 범위가 더 좁아져서 이 클래스를 포함하는 외부 클래스까지로 제한된다.
Item 17. 변경 가능성을 최소화하라.
- 불변 클래스
- String, 기본타입의 박싱된 클래스, BigInteger, BigDecimal
- 규칙
- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다. 클래스에 final을 붙이는 방법과 추가적인 방법이 있다.
- 모든 필드를 final로 선언한다.
- 모든 필드를 private으로 선언함으로써 가변 객체에 대한 접근을 제어한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
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 realPart() { return re; } public double imaginaryPart() { return im; } public Complex plus(Complex c) { return new Complex(c.re + this.re, c.im + this.im); } public Complex minus(Complex c) { return new Complex(this.re - c.re, this.im - c.im); } public Complex times(Complex c) { return new Complex(this.re * c.re - this.im * c.im, this.re * c.im + this.im * c.re); } public Complex dividexBy(Complex c) { double tmp = c.re * c.re + c.im * c.im; return new Complex((this.re * c.re + this.im * c.im) / tmp, (this.im * c.re - this.re * c.im) / tmp); } @Override public boolean equals(Object o) { // TODO Auto-generated method stub if( o == this ) return true; if( !(o instanceof Complex) ) return false; Complex c = (Complex)o; return Double.compare(c.re, this.re) == 0 && Double.compare(c.im, this.im) == 0; } @Override public int hashCode() { // TODO Auto-generated method stub return 31 * Double.hashCode(re) + Double.hashCode(im); } @Override public String toString() { // TODO Auto-generated method stub return "(" + re + " + " + im + "i)"; } }
- 사칙연산 메서드들이 인스턴스 자신을 수정하는 것이 아닌, 새로운 Complex 인스턴스를 만들어 반환하였다.
- 특징
- 불변 객체는 단순하다. 생성 시점부터 파괴될 때까지 그대로 간직한다.
- 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요가 없다.
- 불변 객체는 안심하고 공유할 수 있고, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
- 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다. Map의 키와, Set의 원소로 쓰기에 좋다.
- 불변 객체는 그 자체로 실패 원자성을 제공한다.
- 실패 원자성이란, 메서드에서 예외가 발생한 후에도 그 객체는 여전히 유효한 상태여야 한다는 성질
- 단점은, 값이 다르면 반드시 독립된 객체로 만들어야 한다는 것이다.
- 불변 객체 만드는 법
public class Complex { private final double re; private final double im; private Complex(double re, double im) { this.re = re; this.im = im; } public static Complex valueOf(double re, double im) { return new Complex(re, im); } // 이하 생략
- 객체 설계 시 주의점
- getter가 있다고 해서 무조건 setter를 만들지 말자. 클래스는 필요한 경우가 아니면 불변이어야 한다.
- 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자. -> 합당한 이유가 없다면 모든 필드는 private final
- 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
Item 18. 상속보다는 컴포지션을 사용하라
-
구현상속을 말하며, 클래스가 인터페이스를 구현하거나, 인터페이스를 확장하는 등에 인터페이스 상속과는 무관한다.
- 상속의 주의점
- 메서드 호출과 달리 상속은 캡슐화를 깨트린다.
- 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작이 이상할 수 있다.
- ex) HashSet을 상속한 클래스를 구현할 때 add 메소드에서 사이즈를 +1씩 늘리고, addAll 메서드에서 인자로 받은 Collection에 크기만큼 늘리도록 overrindg을 한다면?
- 예기치 못한 size를 얻을 수 있다. (이미 addAll에서 add를 호출하도록 되어 있기 때문에 2번 size를 늘리는 것)
- ex) HashSet을 상속한 클래스를 구현할 때 add 메소드에서 사이즈를 +1씩 늘리고, addAll 메서드에서 인자로 받은 Collection에 크기만큼 늘리도록 overrindg을 한다면?
- 다음 릴리스에서 상위 클래스에 새로운 메서드를 추가했을 때 하위 클래스에서 허용되지 않는 일이 벌어날 수 있다.
- ex) Hashtable과 Vector를 컬렉션 프레임워크에 포함시키자 이와 관련한 보안 구멍들을 수정해야 하는 사태가 발생했다.
- 메서드를 overriding을 하는 것이 아닌 새로운 메서드를 추가해도 문제가 될 수 있다.
- ex) 하위 클래스에 추가한 메서드와 시그니처가 같고 반환 타입이 다르다면 컴파일 오류가 발생할 수 있고, 상위 클래스의 메서드가 요구하는 규약을 만족하지 못할 수도 있다.
- 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작이 이상할 수 있다.
- 메서드 호출과 달리 상속은 캡슐화를 깨트린다.
- 상속을 피하는 법
- 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자.
- 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 Composition(컴포지션)이라 한다.
- 새 클래스의 인스턴스 메서드들은 (private 필드로 참조하는) 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환하는 데 해당 방식은 전달(forwarding)이라 부르며, 새 클레스의 메서드들은 전달 메서드(forwarding method)라 한다.
// Wrapper Class - 상속대신 컴포지션을 사용 public class InstrumentedSet<E> extends ForwardingSet<E> { private int addCount = 0; public InstrumentedSet(Set<E> s) { super(s); } @Override public boolean add(E e) { this.addCount += 1; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { this.addCount += c.size(); return super.addAll(c); } public int getAddCount() { return this.addCount; } }
public class ForwardingSet<E> implements Set<E> { private final Set<E> s; public ForwardingSet(Set<E> s) { this.s = s; } // 기존 메서드 구현 public void clear() {...} ... }
Item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
- 상속용 클래스를 생성할 때 고려할 점
- 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 한다.
- (재정의 가능 메서드란? public, protected 메서드 중 final이 아닌 모든 메서드)
- “Implementation Requirements”로 시작되는 절을 볼 수 있는데, 이 절은 메서드 주석에 @implSpec 태그를 붙여주면 자바독 도구가 생성해준다.
- @implSpec을 활성화하기 위해서는 명령줄 매개변수로 -tag “implSpec:a:Implementation Requirements:”를 지정해주면 된다.
- 문서를 남기는 것 + 효율적인 하위 클래스를 큰 어려움 없이 만들수 있게 해야 한다.
- 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.
- 상속용 클래스를 테스트하기 위해서는 하위클래스를 만드는 방법 밖에 없기 때문에 배포 전에 하위 클래스를 만들어 검증해야 한다.
- 상속용 클래스의 생성자는 직/간접적으로 재정의 가능 메서드를 호출해서는 안된다.
public class SuperClass { public SuperClass() { this.overrideMe(); } public void overrideMe() {///} } public final class SubClass extends SuperClass { private final Instant instant; public SuperClass() { instant = Instant.now(); } @Override public void overrideMe() { System.out.println(instant); } public static void main(String[] args) { SubClass sub = new SubClass(); sub.overrideMe(); } }
- instant가 2번 실행되지 않고, 첫번째는 null을 출력한다. 인스턴스 필드가 초기화되기 전에 SuperClass 생성자에 있는 overrideMe()가 호출되기 때문이다.
- Clonable 클래스를 구현한 클래스는 확장하기 어렵다.
- clone과 readObject는 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
- Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 이 메서드들은 private아닌 protected를 선언해야 한다.
- 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 한다.
Item 20. 추상 클래스보다는 인터페이스를 우선하라
- 자바가 제공하는 다중 구현 메커니즘은 인터페이스와 추상 클래스가 있다.
- 자바 8 부터 인터페이스도 default method를 제공할 수 있게 되어, 인터페이스와 추상 클래스 모두 구현형태로 제공할 수 있다.
-
추상 클래스와 인터페이스의 가장 큰 차이는 추상 클래스에서 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스여야 한다. 자바는 단일 상속만 가능하므로 제약 사항이 될 수 있다.
- 추상 클래스가 아닌 인터페이스를 선택해야 하는 이유
- 기존 클래스에 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.
- 인터페이스는 믹스인(mixin) 정의에 안성 맞춤이다.
- 믹스인이란 클래스가 구현할 수 있는 타입으로 기존 주된 타입 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다. (혼합(mixed in)한다고 해서 믹스인)
- 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
- Template Method Pattern
- 인터페이스와 추상 골격 구현 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취하는 방법
- 인터페이스로는 타입을 정의하고, 필요하면 디폴트 메서드도 제공한다. 그리고 골격 구현 클래스는 나머지 메서드들까지 구현한다.
- 관레상 인터페이스 이름을 Interface라고 한다면, 골격 구현 클래스의 이름은 Abstractinterface로 짓는다.
- 골격 구현 클래스는 추상 클래스처럼 구현을 도와주는 동시에, 추상 클래스로 타입을 정의할 때 따라오는 제약으로부터 자유롭다.
- 구조상 골격 구현 클래스를 구현하지 못하더라도 default method에 대한 이점을 여전히 누릴 수 있다.
- 골격 구현은 기본적으로 상속해서 사용하는 걸 가정하므로 설계 및 문서화 지침을 모두 따라야 한다.
- 추상 클래스가 아닌 인터페이스를 선택해야 하는 이유
Item 21. 인터페이스는 구현하는 쪽을 생각해 설계하라
- 자바 8 전에는 기존 구현체를 깨트리지 않고, 인터페이스에 메서드를 추가할 방법이 없었다.
- 자바 8 부터는 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트 메서드가 있다.
-
디폴트 메서드를 추가된 이유는 람다를 활용하기 위해서다.
- 디폴트 메서드의 단점
- 구현체에 디폴트 메서드를 재정의하지 않는 경우 해당 메서드를 그대로 물려받게 되며, 규약을 지키지 못할 수 있다.
- 기존 구현체에 런타임 오류를 일으킬 수 있다.
- 디폴트 메서드라는 도구가 생겼더라도! 인터페이스를 설계할 때는 주의해야 한다!
- 디폴트 메서드의 단점
Item 22. 인터페이스는 타입을 정의하는 용도로만 사용하라
-
인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다.
-
상수 인터페이스
public interface PhysicalConstants { // 잘못된 사용 static final double AVOGADROS_NUMBER = ...; static final double BOLTZMAN_CONSTANT = ...; static final double ELECTRON_MASS = ...; }
- 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당한다. 위와 같이 구현하면 API로 노출하는 행위다.
- 특정 클래스나 인터페이스와 강하게 연관된 상수라면 해당 클래스나 인터페이스에 구현하거나 열거 타입으로 공개, 인스턴스화 할 수 없는 유틸리티 클래스로 공개하자.
public class PhysicalConstants { // 상수 유틸리티 클래스 private PhysicalConstants(){} // 인스턴스화 방지 public static final double AVOGADROS_NUMBER = ...; public static final double BOLTZMAN_CONSTANT = ...; public static final double ELECTRON_MASS = ...; }
Item 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라
-
태그달린 클래스란? 두가지 이상의 의미를 표현할 수 있으며, 그 중 현재 표현하는 의미를 태그 값으로 알려주는 클래스
- 태그 달린 클래스의 단점
- 가독성, 메모리 사용, 열거 타입 선언, 태그 필드, switch 문 등 쓸데없는 코드가 많다.
- 장황하고 오류를 내기 쉽고, 비효율적이다.
- 클래스 계층 구조를 어설프게 흉내낼 뿐이다.
- 해결책? 클래스 계층구조!
abstract class Figure { abstract double area(); } class Circle extends Figure { final double radius; Circle(double radius) { this.radius = radius; } @Override double area() { // TODO Auto-generated method stub 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() { // TODO Auto-generated method stub return 0; } }
Item 24. 멤버 클래스는 되도록 static으로 만들어라
- 중첩클래스란 다른 클래스 안에 정의된 클래스
- 중첩클래스는 자신을 감싼 클래스에서만 사용 가능하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.
-
중첩클래스의 종류는 정적 멤버 클래스, (비정적) 멤버 클래스, 익명 클래스, 지역 클래스이며, 정적 멤버 클래스를 제외한 나머지는 내부 클래스이다.
- 정적 멤버 클래스 (Static neted class)
- 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스와 독같다.
- ex) Calculator.Operation.PLUS, Calculator.Operation.MINUS ..
- (비정적) 멤버 클래스
- 정적 멤버 클래스와의 차이는 static 여부
- 밖에 있는 클래스는 내부클래스를 멤버변수처럼 사용할 수 있다. 사용하려면 new 생성자로 생성해야 한다.
- 내부 클래스는 바깥 클래스의 자원을 직접 사용할 수 없다.
- 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자
- 바깥 클래스에 대한 숨은 참조(바깥클래스.this)를 가져올 수 있는데 비용이 들어가며 Garbage Collector가 제때 수거하지 못해 메모리 누수가 발생할 수 있다.
- 익명 클래스
- 익명클래스는 바깥 클래스의 멤버가 아니며, 바깥 클래스를 new 생성자로 생성하는 경우 생성되며, 동시에 부모 클래스를 상속받아 내부에서 오버라이딩해서 사용한다.
- 매개변수로 사용할 수 있다.
- 익명 클래스 내부의 변수나 메소드는 익명클래스의 밖에서 사용 불가하다.
- 내부에 생성자를 작성할 수 없으며, 바깥 클래스의 자원은 final이 붙은 것만 사용할 수 있다.
- 지역 클래스
- 메소드 내부에 클래스를 정의하는 경우로 메소드 내의 지역변수처럼 사용 가능하다
- 메소드 내부에서 new 생성자로 사용해야 한다. 메소드 밖에서는 사용할 수 없다.
Item 25. 톱레벨 클래스는 한 파일에 하나만 담으라
- 단점
- Utensil.java 파일 안에 Utensil class와 Dessert class를 선언하더라도 사용하는 데 아무런 문제가 없다. 하지만 이후 Dessert.java 파일을 생성하면 문제가 발생한다.
- javac Main.java 또는 javac Main.java Utensil.java를 실행하면 Utensil.java 안에 Desert class가 실행되지만, javac Main.java Dessert.java를 사용하면 Dessert.java 안에 있는 클래스가실행된다.
- 해결책
- 소스들을 파일로 분리하면 된다.
- 한 파일에 담고자 한다면 정적 멤버 클래스를 사용하자.