kmg28801 / effective-java

2 stars 0 forks source link

4장 클래스와 인터페이스 #4

Open ky8778 opened 4 months ago

ky8778 commented 4 months ago

CH4. 클래스와 인터페이스

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

내용 컴포넌트를 잘 설계하는 방법의 기본 원칙 : 모든 클래스와 멤버의 접근성을 최대한 좁히기 → 정보은닉 - 캡슐화를 통해 내부 구현을 숨기고 API(ex. 메서드)로 소통 → 결합도와 의존성을 낮출 수 있게됨 #### 정보은닉 - 캡슐화 잘 설계된 컴포넌트는 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼는가에 결정된다. 모든 내부 구현을. 완벽하게 숨겨, 구현과 API 를 깔끔하게 분리하는 것이다. 이를 `정보 은닉` 혹은 `캡슐화` 라고 한다. ##### 정보 은닉 혹은 캡슐화의 장점 정보 은닉 혹은 캡슐화는 객체의 필드와 메서드를 하나로 묶고 실제 구현 내용 일부를 외부에 감추어 은닉하는 것을 말한다. 즉, 외부에서 변수에 직접 접근할 수 없도록 하고 오직 메서드를 통해서만 값이 변경될 수 있도록 한다. 정보 은닉은 시스템을 구성하는 컴포넌트 사이의 결합도와 의존성을 낮춘다. **→ 어떻게?** 구현 코드를 외부에서 변경하지 못하도록 하므로 해당 기능을 변경해야 하는 상황이 발생하는 경우 특정 클래스에만 변경 사항에 영향을 받는다. 즉, 요구 사항 변화에 따른 코드 수정 범위를 최소화할 수 있다. 이를 통해 얻을 수 있는 장점 == 낮은 결합도와 의존성의 장점 - 개발 속도를 높일 수 있다. - 여러 컴포넌트를 병렬 개발할 수 있기 때문. - 어떤 컴포넌트가 문제를 일으키는지 찾기 쉽고 다른 컴포넌트에 영향없이 최적화 할 수 있기 때문에 성능 최적화에도 도움을 준다. - 각 컴포넌트를 더 빨리 파악할 수 있고 다른 컴포넌트로 교체하는 부담도 적기 때문에 관리적인 비용도 줄일 수 있으며 재사용성을 높일 수 있다. - 시스템이 미완성되었어도 개별 컴포넌트의 동작을 검증할 수 있기 때문에 큰 시스템의 제작을 조금 더 쉽게 해준다. ##### 접근 지정자의 종류 - java - private : 해당 클래스 내에서만 접근 가능 - package-private : 동일 패키지의 클래스에서만 접근 가능 - protected : 동일 패키지의 클래스 + 외부 패키지에 있는 해당 클래스를 상속 받은 클래스에서 접근 가능 - pulbic : 모든 곳에서 접근 가능 - kotlin : kotlin 은 클래스 단위인 java 와 달리 파일 단위로 변수나 함수를 클래스 외부에도 따로 정의할 수 있다. - Top-level - pivate : 해당 파일 내에서만 접근 가능 - protected : 사용 불가능 - internal : 같은 모듈 내에서 다 접근 가능 - public : 어디서든 접근 가능 - 클래스 내의 변수, 함수 - pivate : 해당 클래스 내에서만 사용 가능 - protected : 해당 클래스에서 접근 가능 + 해당 클래스의 자식 클래스에서만 접근 가능 - internal : 같은 모듈 내에서 다 접근 가능 - public : 어디서든 접근 가능 - Java 는 외부 클래스에서 내부 클래스의 private 멤버에 접근 가능 / Kotlin 은 불가능 #### 컴포넌트를 설게하는 방법 - 원칙 : 모든 클래스와 멤버의 접근을 최대한 좁힌다. (가장 낮은 접근 지정자 수준을 부여한다.) - 방법 - 패키지 외부에서 사용할 이유가 없다면 package-private 로 선언하자 - API 가 아닌 내부 구현이 되어 언제든 수정이 가능 - 경우에 따라 private static 중첩 클래스를 활용하자 - 하나의 클래스에서만 사용하는 package-private 톱레벨 클래스나 인터페이스가 대상 - 이를 사용하는 클래스 안에 private static 으로 중첩시켜보자 - 바깥 클래스 하나에서만 접근할 수 있다. - 클래스의 공개 API 를 제외한 모든 멤버는 private 로 만들자. - 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한해 package-private 으로 풀어준다. - 다만, 상위 클래스의 메서드를 재정의할 때는 접근 수준을 상위 클래스보다 좁게 설정할 수 없다. - 테스트만을 위해 클래스, 인터페이스 그리고 멤버를 공개 API 로 만들어서는 안된다. - public 클래스의 인스턴스 필드는 되도록 public 이 아니어야 한다. - 불변을 보장하기 어렵고 일반적으로 스레드 안전하지 않다. > *Thread Safe* > *멀티 스레드 프로그래밍에서 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없는 것을 의미. 하나의 함수가 한 스레드로부터 호출되어 실행 중일 때, 다른 스레드가 그 함수를 호출하여 동시에 함께 실행되더라도 각 스레드에서의 함수의 수행 결과가 올바로 나오는 것.* - public static final 필드는 기본 타입이나 불변 객체를 참조해야 한다. - 하지만 public static final 배열 필드를 두거나 이를 반환하는 접근저 메서드는 두면 안된다. - 다른 객채를 참조하도록 바꿀 수는 없지만 참조된 객체 자체가 수정될 수는 있다. Ex. 길이가 0 이 아닌 배열은 모두 변경 가능하다. ```java class Example { public static final Integer[] SOME_VALUES = {1, 2, 3}; } class Test { public static void main(String[] args) { System.out.println(Example.SOME_VALUES[0]); Example.SOME_VALUES[0] = 5; System.out.println(Example.SOME_VALUES[0]); } } ``` 이런 경우 public 으로 선언한 배열을 private 접근 지정자로 변경하고 변경 불가능한 public 리스트로 만드는 방법이 있다. ```java private static final Integer[] SOME_VALUES = {1, 2, 3}; public static final List VALUES = Collections.unmodifiableList(Arrays.asList(SOME_VALUES)); ``` 아니면 배열은 private 로 선언하고 해당 배열을 복사해서 반환하는 public 메서드를 추가할 수도 있다. ```java private static final Integer[] SOME_VALUES = {1, 2, 3}; public static final Integer[] values() { return SOME_VALUES.clone(); } ```

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

내용 데이터 필드에 직접적으로 접근할 수 있는 클래스는 캡슐화의 이점이 없다. ```java class Point { public double x; public double y; } ``` public 클래스의 멤버 필드가 public 으로 선언되었다면 클라이언트가 이를 사용할 소지가 있어 마음대로 변경하기 어려워진다. 예를들어, `java.awt.package` 패키지의 `Point` 와 `Dimension` 클래스가 그렇다. 클래스의 멤버 변수는 private 로 바꾸고 public 접근자(getter)를 추가해서 사용하자. ```java class Point { private double x; private double y; public Point(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; } } ``` package-private 클래스 또는 private 중첩 클래스라면 public 으로 두어도 문제가 없다. 오히려 코드 작성 면에서 getter 를 사용하는 것보다 더 깔끔할 수 있다. 내부에서만 동작하기 때문이다. ```java public class Example { public static class InnerNested { public String memberField; } public void somePrint() { InnerNested instance = new InnerNested(); System.out.println(instance.memberField); } } ``` > *public 클래스는 절대 가변 필드를 public 접근 지정자로 두어서는 안된다.* >

Item 17. 변경 가능성을 최소화하라

내용 불변 클래스란 인스턴스 내부 값을 수정할 수 없는 클래스를 말한다. 객체가 소멸되기 전까지 절대로 달라지지 않는다. 불변 클래스는 가변 클래스보다 설게하고 구현하고 사용하기 쉬우며 오류가 발생한 소지도 적고 훨씬 안전하다. - 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다. - 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄여야 한다. - 다른 합당한 이유가 없다면 클래스의 모든 필드는 private final 이어야 한다. - 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다. - 확실한 이유가 없다면 생성자와 정적 팩터리 외에는 그 어떤 초기화 메서드도 public 으로 제공해서는 안된다. #### 불변 클래스를 만드는 규칙 - 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다. - 클래스를 확장할 수 없도록 한다. - 하위 클래스에서 객체의 상태를 변하기 하는 것을 막는다. - 대표적으로 클래스를 final 로 선언하면 된다. - 모든 필드를 final 로 선언한다. → 개발자의 의도를 명확하게 드러내는 방법 - 모든 필드를 private 로 선언한다. → 필드가 참조하는 가변 객체를 직접 접근하는 일을 막는다. - 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. 클래스를 final 로 선언하여 상속을 막을 수 있지만 모든 생성자를 private 또는 package-private 으로 만들고 public 정적 팩터리를 만드는 더 유연한 방법도 있다. 아래는 생성자 대신 정적 팩터리를 사용한 불변 클래스 예시이다. ```java public class Complex { private final double re; private final double im; // 생성자는 private 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); } } ``` #### 불변 클래스와 불변 객체의 특징 불변 클래스의 객체는 근본적으로 스레드 안전하기 때문에 안심하고 공유할 수 있다. 따라서 불변 클래스라면 한번 만든 인스턴스를 최대한 재활용하면 좋다. 불변 객체는 그 자체로 실패 원자성을 제공한다. > *실패 원자성 (Failure Atomicity)* > *메서드에서 예외가 발생한 후에도 그 객체는 여전히 메서드 호출 전과 똑같은 유효한 상태여야 한다.* ```java public class BigInteger extends Number Implements Comparable { final int signum; final int[] mag; public BigInteger negate() { return new BigInteger(this.mag, -this.signum); } } ``` 불변 클래스의 단점 - 값이 다르다면 반드시 독립된 객체로 만들어야 한다. - 예를 들어 백만 비트짜리 BigInteger 에서 비트 하나를 바꾸기 위해서 새로운 인스턴스를 만들어야 한다. - 객체를 완성하기 까지의 단계가 많고, 중간 단계에서 만들어지는 객체들이 모두 버려지는 성능 문제가 있을 수 있다. - 해결 방법 : 다단계 연산을 기본으로 제공 → 가변 동반 클래스 (Companion class) ex. StringBuilder, StringBuffer

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

이번 Item 에서의 상속 : 클래스가 다른 클래스를 확장하는 구현 상속 인터페이스를 구현(implements)하거나 인터페이스가 다른 인터페이스를 확장(extends)하는 인터페이스 상속과는 무관 상속은 잘못 사용하면 오류를 내기 쉽다. 그렇기 때문에 코드를 재사용 할 수 있는 좋은 수단이지만 항상 최선은 아니다.

내용 #### 상속 (extends) 상속은 코드를 재사용할 수 있는 강력한 수단이지만, 항상 최선이라고 할 수 없다. 메서드 호출과 다르게 캡슐화를 깨뜨리기 때문. (상위 클래스의 구현이 바뀌면 이를 상속한 하위 클래스에도 영향이 있을 수 있다.) Ex. HashSet 을 확장한 TestHashSet 클래스 ```java public class TestHashSet extends HashSet { private int addCount = 0; // 추가된 원소의 개수 @Override public boolean add(T t) { addCount++; return super.add(t); } @Override public boolean addAll(Collection c) { addCount = addCount + c.size(0); return super.addAll(c); } public int getAddCount() { return addCount; } } // 객체 생성 후 3개의 엘리먼트를 addAll 메서드로 추가 TestHashSet(String) testSet = new TestHashSet<>(); testSet.addAll(List.of("t1", "t2", "t3")); System.out.println(testSet.getAddCount()); // 6 ``` 기대값은 3 이었지만 결과값은 6 이다. HashSet 의 addAll 메서드가 add 메서드를 사용하여 구현되었기 때문. ```java // HashSet(AbstractSet)의 addAll 메서드 public boolean addAll(Collection c) { boolean modified = false; for (E e : c) if (add(e)) modified = true; return modified; } ``` 그러니까 addAll 메서드에는 각 요소를 add 메서드를 호출해서 추가하므로 addCount 를 증가시키는 코드가 없어야 한다. #### 어떻게 해야 안전할까? 메서드를 재정의하는 것보다 새로 만드는 게 조금 더 나을 수 있다. 훨씬 더 안전한 방법이긴 하지만 위험 요소가 전혀 없는 것은 아니다. 만일 하위 클래스에 추가한 메서드와 시그니처가 같고 리턴 타입만 다르다면 그 클래스는 컴파일조차 되지 않는다. 리턴 타입도 같다면 재정의가 된다. > *메서드 시그니처 : 메서드의 이름과 파라미터* 기존 클래스를 확장하는 대신에 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하면 된다. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이를 컴포지션(Composition) 이라고 한다. 새로운 클래스의 인스턴스 메서드들은 기존클래스에 대응하는 메서드를 호출해 그 결과를 반환한다. 이를 전달(Forwarding)이라고 하며, 새 클래스의 메서드들은 전달 메서드라고 한다. 이렇게 되면 새로운 클래스는 기존 클래스의 영향이 적어지고 기존 클래스 안에 새로운 메서드가 추가되어도 안전하게 된다. ```java public class TestSet extends ForwardingSet { private int addCount = 0; public TestSet(Set set) { super(set); } @Override public boolean add(T t) { addCount++; return super.add(t); } @Override public boolean addAll(Collection collection) { addCount = addCount + collection.size(); return super.addAll(collection); } public int getAddCount() { return addCount; } } public class ForwardingSet implements Set { private final Set set; public ForwardingSet(Set set) { this.set = set; } public void clear() { set.clear(); } public boolean isEmpty() { return set.isEmpbty(); } public boolean add(T t) { return set.add(t); } public boolean addAll(Collection c) { return set.addAll(c); } // ... 생략 } ``` 다른 Set 인스턴스를 감싸고 있다는 뜻에서 TestSet 과 같은 클래스를 래퍼 클래스라고 하며, 다른 Set 에 게속 기능을 덧씌운다는 뜻에서 데코레이터 패턴(Decorater Pattern)이라고 한다. 컴포지션과 전달의 조합은 넓은 의미로 위임(delegation)이라고 하지만 엄밀히 따지면 래퍼 객체가 내부 객체에 자기 자신의 참조를 넘기는 경우에만 해당한다. #### 언제 상속을 사용? 클래스 B가 클래스 A와 is-a 관계 일때만 사용해야 한다. 반드시 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황에서만 쓰여야 한다. 예를 들어 클래스 A 를 상속하는 클래스 B 를 만들려고 한다면, B 가 정말 A 인가? 를 생각해봐야 한다. 예를 들면 와인 클래스를 상속하는 레드 와인 클래스 그리고 레드 와인은 와인이다. 그 조건이 아니라면 A 를 클래스 B 의 private 인스턴스로 두면 된다. 그러니까, A 는 B 의 필수 구성요소가 아니라 구현하는 방법 중 하나일 뿐이다.

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

내용 #### 문서화 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기 사용) 문서로 남겨야 한다. 재정의 가능한 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야 한다. 여기서 **재정의 가능이란** public과 protected 메서드 중에서 final이 아닌 모든 메서드를 말한다. #### 상속을 고려한 설계를 할 때 주의할 점 - 클래스 설계 시에 어떤 protected 메서드나 필드를 제공해야 하는지 심사숙고해야 한다. - 유지 보수 측면에서는 protected 메서드와 필드를 최소화하는 것이 좋으나, 아예 없는 경우 상속의 의미가 없다. - 상속용 클래스를 테스트하는 방법은 직접 하위 클래스를 만드는 것이다. 꼭 필요한 protected 멤버를 빼먹었다면, 하위 클래스를 만들 때 그 빈자리가 확연히 드러나기 때문이다. - 하위 클스를 여러 개 만들 때까지 전혀 사용되지 않는 protected 멤버는 사실상 private이었어야 할 가능성이 높다. - 상속용 클래스의 생성자는 직접적 또는 간접적으로 재정의 가능 메서드를 호출해서는 안 된다. - 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 호출되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출되기 때문이다. - 단, private, final, static 메서드는 재정의가 불가능하니 생성자에서 호출해도 된다. #### 상속을 금지하는 방법 클래스를 상속용으로 설계하는 것은 엄청난 노력이 필요하고 제약도 많은 것을 명심해야 한다. 상속용으로 설계되지 않은 클래스는 상속을 금지하는 것이 좋다. **상속을 금지하는 방법** 으로는 클래스를 final로 선언하거나, 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법이 있다. 상속용 클래스 설계는 쉽지 않다. 클래스 내부에서 스스로를 어떻게 사용하는지 모두 문서로 남겨야 하며, 문서화한 것은 반드시 따라야한다.

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

내용 자바가 제공하는 다중 구현 매커니즘은 추상 클래스와 인터페이스다. 자바 8부터는 인터페이스에 디폴트 메서드가 추가되어 두 매커니즘 모두 인스턴스 메서드를 구현 형태로 가질 수 있다. 이로써 조금 더 자유로운 확장이 가능해졌다. 디폴트 메서드를 사용하면 인터페이스를 구현한 클래스에서 반드시 재정의를 하지 않아도 되기 때문이다. ```java public interface SomeThings { void walk(); void sleep(); default void eat() { System.out.println("I am eating the food"); } } ``` #### 추상클래스와 인터페이스의 차이 추상클래스가 정의한 타입을 구현한 클래스는 반드시 추상클래스의 하위클래스가 되어야 한다. 단일 상속만 지원하는 자바에서 추상클래스를 상속한 채 새로운 타입을 정의하기는 어렵다. 반면에 인터페이스를 올바르게 구현한 클래스는 어떤 클래스를 상속했든 같은 타입으로 취급된다. 인터페이스는 추상클래스에 비해 자유롭다. 기존 클래스 위에 추상클래스를 상속시키는 것은 어렵다. 두 클래스가 같은 추상클래스를 확장해야 한다면, 그 추상클래스는 계층구조상 두 클래스의 공통 조상이어야 한다. 하지만 기존 클래스에 인터페이스를 구현시킬 때는 정의해야 하는 메서드를 선언하기만 하면 된다. ##### 추상클래스 우선 추상 클래스는 추상 메서드를 가지고 있는 클래스를 말한다. 그렇다면 추상 메서드는 무엇일까? 추상 메서드는 실질적인 구현없이 몸체, 즉 선언만 있는 메서드를 말한다. 조금 다르게 접근해보자. ‘동물’ 일부를 코드로 정의한다고 가정해보자. 그리고 동물의 종류에는 ‘강아지’와 ‘고양이’를 선택했다. 이들의 공통점은 무엇일까? 여러 가지가 있겠지만 소리를 내거나, 사료를 먹는 등이 있다. 그럼 코드로 표현해보자. ```java class Dog { 소리내기() { System.out.println("멍멍!"); } } class Cat { 소리내기() { System.out.println("야옹!"); } } ``` 매우 간단하다. 그런데 이 코드를 혼자가 아니라 친구A와 같이 작성하는 경우는 어떨까? 각각 동물 한 마리씩 맡아서 말이다. 그러면 친구 A가 고양이 클래스를 작성하기로 했는데, 내 예상과 다르게 사료를 먹는 메서드의 이름을 다르게 했다.` ```java class Cat { 야옹() { System.out.println("야옹!"); } } ``` 사실 규모가 작은 프로젝트라면 큰 문제는 되지 않는다. 친구가 작성한 코드에 맞게 내 코드를 수정하면 그만이다. 아니면 반대로 친구가 수정해도 되고… 하지만 더 좋은 방법이 있다. 여기에 추상 클래스를 적용해보는 것이다. ##### 추상클래스 사용법 친구와 나는 ‘동물’의 일부인 ‘강아지’와 ‘고양이’를 코드로 작성하고 있다. ‘동물’을 추상 클래스로 정의하고 ‘공통적인 행위’를 추상 메서드 로 정의해보자. 아래와 같은 코드가 될 것이다. ```java abstract class Animal { abstract void 소리내기(); } ``` 먼저 추상 클래스를 정의하고 이에 필수적인 기능들을 추상 메서드로 정의하면 된다. 그렇게 하면 나와 친구A는 단순히 추상 클래스를 상속해서 필요한 기능들을 구현하고 이를 병합하기만 하면 된다. ```java class Dog extends Animal { @Override void 소리내기() { System.out.println("멍멍!"); } } class Cat extends Animal { @Override void 소리내기() { System.out.println("야옹!"); } } ``` 이처럼 추상 클래스를 정의하면 어떤 기능이 필요한지 쉽게 제시할 수 있다. 특히 어떤 기능을 구현할지 감이 오지 않거나 클래스의 구조나 구성을 명확하게 모르는 경우에 사용하면 편리하다. 또한 메서드의 이름과 같은 규칙을 통일할 수 있기 때문에 통일성과 유지보수 효율을 높일 수 있다. ##### 추상클래스 특징 추상 클래스는 자기 자신의 객체를 생성할 수 없다. 객체를 생성하려고 하면 오류가 발생한다. ```java abstract class MadClass { abstract void printMyName(); } class MadPlay { public static void main(String[] args) { // Cannot instantiate the type MadClass MadClass inst = new MadClass(); } } ``` 추상 클래스라고 반드시 추상 메서드만 가지고 있을 필요는 없다. ```java abstract class MadClass { abstract void printMyName(); // 일반 메서드 public void sayHi() { System.out.println("Hi~"); } } class MadMan extends MadClass { public void sayHello() { System.out.println("Hello~"); } } class ExampleTest { public static void main(String[] args) { MadMan madMan = new MadMan(); manMan.sayHi(); } } ``` MadClass 라는 추상 클래스를 상속한 ManMan 클래스의 인스턴스는 추상 클래스의 일반 메서드를 호출할 수 있다. 물론 추상 클래스의 목적이 상속과 오버라이딩을 하기 위함이기 때문에 오버라이딩해서 사용하는 것이 프로젝트의 규모가 커졌을 때 헷갈리지 않을 것 같다. #### 인터페이스는 믹스인(mixin) 정의에 알맞다. 믹스인은 대상 타입의 주된 기능에 선택적 기능을 혼합(mixed in)하는 것을 말한다. ```java package java.io; public class File implements Serializable, Comparable { ... } ``` 여기서 File 클래스가 Comparable 을 구현 (implements)했다는 것은 File 클래스의 인스턴스끼리는 순서를 정할 수 있다는 것을 뜻한다. 추상 클래스는 기존 클래스에 덧씌우기 어렵고 여러 부모클래스를 가질 수 없는 클래스의 계층 구조에는 믹스인을 사용하기 합리적인 위치가 없다. #### 인터페이스는 계층구조가 없는 타임 프레임워크를 만들 수 있다. ```java public interface Singer { AudioClip sing(Song s); } public interface Songwriter { Song compose(int chartPosition); } // 그렇다면, 노래도 부르고 작곡도 하는 싱어송라이터는? public interface SingerSongWriter extends Singer, Songwriter { AudioClip strum(); void actSensitive(); } ``` #### 인터페이스 + 추상 골격 구현 클래스 인터페이스와 추상 골격 구현 클래스를 함께 제공하여 인터페이스와 추상 클래스의 장점을 모두 갖는 방법도 있다. 인터페이스로는 타입을 정의하고 필요한 경우 디폴트 메서드도 정의한다. 그리고 골격 구현 클래스에는 나머지 메서드들까지 구현한다. 주로 이런 구조는 템플릿 메서드 패턴(Template Method Pattern)에 많이 이용된다. 관례상으로 인터페이스 이름이 XXX라면, 골격 구현 클래스의 이름은 AbstractXXX로 짓는다. 예를 들어 AbstractSet, AbstractList, AbstractMap 등이 핵심 컬렉션 인터페이스의 골격 구현 클래스이다. > *일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다.* >

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

내용 인터페이스에 새로운 메서드를 추가하는 것은 어려운 일이다. 이미 구현한 객체에 오류가 발생할 소지가 있기 때문이다. 자바 8에서 디폴트 메서드가 추가되었지만 모든 상황에서의 불변식을 해치지 않는 디폴트 메서드를 작성하기는 쉽지 않다. 예제를 통해 기존 구현체와 잘 어우러지지 않는 경우를 살펴보자. 아래는 자바8의 Collection 인터페이스에 추가된 디폴트 메서드 `removeIf`이다. ```java default boolean removeIf(Predicate filter) { Objects.requireNonNull(filter); boolean removed = false; final Iterator each = iterator(); while (each.hasNext()) { if (filter.test(each.next())) { each.remove(); removed = true; } } return removed; } ``` Collection 구현체 중 아파치 라이브러리의 `SynchronizedCollection`에서는 이 메서드를 재정의하고 있지 않다. 기존 컬렉션 대신 클라이언트가 제공한 객체로 락(Lock)을 거는 기능들을 추가로 제공하지만 `removeIf` 메서드를 재정의하고 있지 않기 때문에 이 메서드 수행과 관련해서는 스레드 공유 상황에서 오류가 발생할 수 있다. 이처럼 디폴트 메서드가 컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있다. 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니라면 피해야 한다. 그리고 디폴트 메서드가 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 용도가 아님을 명심해야 한다. > *인터페이스를 설계할 때는 세심한 주의와 많은 테스트가 필요하다.* >

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

내용 인터페이스는 자신을 구현(implements)한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다. 그러니까, 인터페이스를 구현한다는 것은 인스턴스로 무엇을 할 수 있는지 클라이언트에게 말하는 것이다. 인터페이스의 이처럼 명확한 용도를 가지고 있으며 이를 지켜야 한다. 잘못 사용된 경우도 있다. 한 가지 예로 상수 인터페이스가 있다. 메서드 없이 상수를 뜻하는 static final 필드로만 구성된 인터페이스를 말한다. 모습은 아래와 같다. ```java // 안티 패턴 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; } ``` 위와 같은 구현은 인터페이스를 잘못 사용한 예시다. 상수는 클래스의 내부에서 사용하는 것인데, 인터페이스로 구현했기 때문에 내부 구현을 API로 노출한 셈이다. 상수를 공개할 목적이라면 다른 방안을 고려해보자. **클래스나 인터페이스 자체에 추가하는 방법도 있다.** 예를 들어, Integer와 Double 클래스의 `MIN_VALUE`와 `MAX_VALUE` 상수가 있다. 또 다른 방법으로 **열거(enum) 타입으로** 표기할 수도 있고, 아래와 같이 인스턴스화할 수 없는 **유틸리티 클래스** 를 구현하여 제공하는 것도 좋다. ```java public class PhysicalConstants { private PhysicalConstants() { // 인스턴스화 하지 못하도록 한다. throw new AssertionError("Cannot instantiate !!!"); } static final double AVOGADROS_NUMBER = 6.022_140_857e23; static final double BOLTZMANN_NUMBER = 1.380_648_52e-23; static final double ELECTRON_NUMBER = 9.109_383_56e-31; } ```

Item 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라

내용 태그 달린 클래스 : 어떤 기능을 갖고 있는지 나타내는 필드가 있는 클래스 > *태그 달린 클래스를 쓰는 상황은 거의 없다.* ```java 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 로 선언하려면 해당 의미에 사용되지 않는 필드까지 생성자에서 초기화해야 한다. → 사용하지 않는 필드를 초기화하는 코드가 생겨난다. - 상태가 추가되려면 코드를 수정해야 한다. - 인스턴스 타입만으로는 현재 나타내는 의미를 파악하기 어렵다. 개선 ```java 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; } } ``` 간결하고 명확해졌으며, 쓸데없는 코드들이 모두 사라졌다. 각 의미를 독립된 클래스에 담았기 때문에 관련 없던 데이터 필드는 모두 제거 되었다. 게다가 실수로 빼먹은 switch 구문의 case 문장 때문에 런타임 오류가 발생할 이유도 없다. 타입 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성은 물론 컴파일 타임에서의 타입 검사 능력도 높여준다. 또한 클래스 계층 구조라면, 아래와 같이 정사각형(Square)가 추가될 때도 간단하게 반영할 수 있다. ```java class Square extends Rectangle { Square(double side) { super(side, side); } } ```

Item 24. 멤버 클래스는 되도록 static 으로 만들자

내용 중첩 클래스의 종류 - 정적 멤버 클래스 - 비정적 멤버 클래스 - 익명 클래스 - 지역 클래스 #### 정적 멤버 클래스와 비정적 멤버 클래스 - 정적 멤버 클래스 : 다른 클래스 안에 선언되며 바깥 클래스의 private 멤버에도 접근 가능한 것을 제외하면 일반 클래스와 동일하다. 정적 멤버 클래스와 비정적 멤버 클래스는 코드 상에서 static의 유무만 보일 수 있으나 의미상의 차이는 더 크다. - 비정적 멤버 클래스 : `비정적 멤버 클래스`의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다. 그래서 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 통해 바깥 인스턴스의 메서드를 호출한다거나 바깥 인스턴스를 참조할 수 있다. 여기서 정규화된 this란, `클래스명.this` 형태로 바깥 클래스의 이름을 명시하는 용법을 말한다. ```java class A { int a = 10; public void run() { System.out.println("Run A"); B.run(); C c = new C(); c.run(); } // 정적 멤버 클래스 public static class B { public static void run() { System.out.println("Run B"); } } // 비정적 멤버 클래스 public class C { public void run() { // 정규화된 this를 통해 참조 가능하다. // 정규화된 this란 클래스명.this 형태로 이름을 명시하는 용법을 말한다. System.out.println("Run C: " + A.this.a); } } } ``` ```java public class Example { public static void main(String[] args) { // 정적 멤버 클래스는 이렇게 외부에서 접근 가능하다. A.B.run(); A a = new A(); a.run(); A.C c = a.new C(); c.run(); } } // 출력 결과 // Run B // Run A // Run B // Run C: 10 // Run C: 10 ``` **멤버 클래스에서 바깥에 위치한 인스턴스에 접근할 필요가 있다면 무조건 static을 추가하여 정적 멤버 클래스로 만드는 것이 좋다.** static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 되는데, 이 참조를 저장하려면 시간과 공간적인 리소스가 소비된다. 더 심각한 문제로 가비지 컬렉션이 바깥 클래스의 인스턴스를 정리하지 못할 수 있다. #### 익명 클래스와 지역 클래스 - 익명 클래스 - 이름이 없으며 바깥 클래스의 멤버가 되지도 않는다. 사용되는 시점에 선언과 동시에 인스턴스가 만들어지며 코드 어디에서든 만들 수 있다. - 상수 변수만 멤버로 가질 수 있으며 instanceof 연산자를 통한 타입 검사가 불가능하다. 또한 여러 개의 인터페이스를 구현할 수 없으며 인터페이스 구현과 동시에 다른 클래스를 상속할 수도 없다. ```java Thread th = new Thread() { // 익명 클래스 final int value = 5; public void run() { System.out.println("Hello Thread: " + value); } }; ``` - 지역 클래스 - 지역 변수를 선언할 수 있는 곳이면 어디서든 선언할 수 있으며 유효 범위(scope)도 지역변수와 같다. - 이름이 있으며 반복해서 사용할 수 있다. - 또한 비정적 문맥에서만 바깥 인스턴스를 참조할 수 있으며, 정적 멤버는 가질 수 없고 가독성을 위해 짧게 작성되어야 한다. ```java class Test { public void say() { class LocalInnerClass { // 지역 클래스 public void sayHello() { System.out.println("Hello!!!"); } } LocalInnerClass lic = new LocalInnerClass(); lic.sayHello(); } } ```

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

내용 하나의 소스 파일에 톱레벨 클래스를 여러 개 선언하더라도 자바 컴파일러는 오류를 발생시키지 않는다. 하지만 A 라는 파일에 클래스 2개가 정의되어 있는데, B 라는 다른 파일에도 같은 이름으로 2개의 클래스가 정의되어 있으면 문제가 다르다. 컴파일에 실패하거나 컴파일 순서에 따라서 동작이 다를 수 있다. 단순히 톱레벨 클래스들을 서로 다른 파일에 분리해서 작성해주면 해결할 수 있다. 꼭 하나의 파일에 담고 싶다면, 정적 멤버 클래스를 사용하는 방법을 고민해보자. 가독성도 좋고 private 로 선언한 경우에는 접근 범위도 최소로 관리할 수 있기 때문이다.