네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'dev book/Effective Java'에 해당되는 글 27건


반응형

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


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

### 상수 인터페이스 안티패턴 - 사용 금지
- 상수 인터페이스란 메서드없이 상수를 뜻하는 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;
}
 
- 상수 인터페이스 안티 패턴은 인터페이스를 잘못 사용한 예다.
- 클래스 내부에서 사용하는 상수는 인터페이스가 아니라 내부 구현에 해당한다. 따라서 상수 인터페이스를 구현하는 것은 이 내부 구현을 클래스의 API로 노출하는 행위이다.
- 클래스가 어떤 상수 인터페이스를 사용하든 사용자에게는 아무런 의미가 없다. 반면, 이는 사용자에게 혼란을 주며 클라이언트 코드가 이 상수들에 종속되게 한다.
- final이 아닌 클래스가 상수 인터페이스를 구현한다면 모든 하위 클래스의 이름 공간이 그 인터페이스가 정의한 상수들로 오염되어 버린다.

### 상수 유틸리티 클래스
- 특정 클래스나 인터페이스와 강하게 연관된 상수라면 그 클래스나 인터페이스 자체에 추가해야 한다.
- 열거 타입으로 나타내기 적합한 상수라면 열거 타입으로 만들어 공개하면 된다.
- 또는, 인스턴스화할 수 없는 유틸리티 클래스에 담아 공개하는 방법도 있다.
public class PhysicalConstants {
  private PhysicalConstants() { // 인스턴스화 방지
  }

  // 아보가드로 수 (1/몰)
  public static final double AVOGADROS_NUMBER   = 6.022_140_857e23;

  // 볼츠만 상수 (J/K)
  public static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;

   // 전자 질량 (kg)
  publicstatic final double ELECTRON_MASS      = 9.109_383_56e-31;
}
- 숫자 리터럴에 밑줄을 사용하였다. 자바 7부터 허용된다.
- 이 밑줄은 숫자 리터럴 값에는 영향을 주지 않으며, 가독성은 향상시킨다.
- 고정소수점 수 또는 부동 소수점 수가 5자리 이상이라면 밑줄을 사용하는 것을 고려해보자.
- 십진수 리터럴도 밑줄을 사용해서 세 자리씩 묶어주는 것이 좋다.

### 정적 임포트를 사용해 상수 이름만으로 사용하기
import static PhysicalConstants.AVOGADROS_NUMBER;

public class Test {
    double atoms(double mols){
        return AVOGADROS_NUMBER * mols;
    }
    // PhysicalConstants 를 자주사용하면 정적임포트가 좋다.
}
- 유틸리티 클래스에 정의된 상수를 클라이언트에서 사용하려면 클래스 이름까지 함께 명시해야 한다.
- 유틸리티 클래스의 상수를 빈번히 사용한다면 정적 임포트(static import)하여 클래스 이름은 생략할 수 있다.

## 정리
- 인터페이스는 타입을 정의하는 용도로만 사용해야 한다. 상수 공개용 수단으로 사용하지 말 것.
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

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

생각할 수 있는 상황에서 불변식을 해치지 않는 디폴트 메서드 작성은 어렵다.

* 디폴트 메서드는 구현 클래스에 대해 아무 것도 모른채 합의없이 무작정 '삽입'될 뿐이다.
* Java8: 컬렉션 인터페이스 다수에 디폴트 메서드 추가
* 범용적으로 구현되어 있지만, 모든 구현체와 어울리는 것은 아니다.

## 1. 기존 인터페이스에 default 메서드를 추가함으로써 발생하는 위험
모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어렵다.

- 자바 8부터 Collection 인터페이스에 removeIf() 디폴트 메서드 추가
  default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
      if (filter.test(each.next())) {
        each.remove();
        removed = true;
      }
    }
    return removed;
  }
 
- 람다를 활용하기 위해 추가된 메서드
public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("test");
    list.add("test2");
    list.add("java Spring");
    list.add("java Spring Data JPA");

    SynchronizedCollection<String> sc = SynchronizedCollection.synchronizedCollection(list);

    Predicate<String> startJava =  s-> s.startsWith("java");
    sc.remove("test"); //SynchronizedCollection 가 재정의한 remove() 메서드 호출
    sc.removeIf(startJava::evaluate); // Collection 에 정의된 removeIf() 디폴트 메서드  호출

}
 
- SynchronizedCollection 클래스는 말 그대로 멀티 스레드 환경에서 안정성을 보장해주는 클래스
- java8 이후 추가된 removeIf 디폴트 메서드를 재정의하지 않았기 때문에 멀티 스레드 환경에서 실행하면 예기치 못한 결과 발생

## 정리
- 디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계할 때는 세심한 주의를 기울여야 한다.
- 새로운 인터페이스라면 릴리즈 전에 반드시 테스트를 거쳐야 한다.
- 인터페이스를 릴리즈한 후라도 결함을 수정하는게 가능한 경우도 있겠지만, 그 가능성에 기대서는 안된다.
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

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


자바의 다중 구현 메커니즘: 둘 다 인스턴스 메서드를 구현 형태로 제공할 수 있다. (default method)
* 인터페이스: 다중 상속, 같은 타입 취급
* 추상 클래스: 단일 상속, 하위 클래스(상하 관계)

## 1. 인터페이스의 장점
1. 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.
* 인터페이스: 요구하는 메서드를 추가하고, 클래스 선언에 implements 구문만 추가하면 된다.
* 추상 클래스: 계층 구조상 확장시킨 클래스의 공통 조상이 되어, 클래스 계층 구조를 생각해야한다.

2. mixin(믹스인) 정의에 안성맞춤이다.
* 믹스인: 클래스가 자신의 본래 타입에 추가하여 구현할 수 있는 타입
* 믹스인 인터페이스는 어떤 클래스의 주 기능 이외에 믹스인 인터페이스 기능을 추가적으로 제공하게 해주는 효과를 준다.

- 믹스인의 대표적인 인터페이스: Comparable, Cloneable, Serializable
public class Mixin implements Comparable {
  @Override
  public int compareTo(Object o) {
    return 0;
  }
}

3. 계층 구조가 없는 타입 프레임워크를 만들 수 있다.
public interface SingerSongWriter extends Singer, SongWriter {
    void strum();
    void actSensitive();
}
public abstract class Singer {
    abstract void sing(String s);
}

public abstract class SongWriter {
    abstract void compose(int chartPosition);
}

public abstract class SingerSongWriter {
    abstract void strum();
    abstract void actSensitive();
    abstract void Compose(int chartPosition);
    abstract void sing(String s);
}
 
- 추상 클래스로 만들면 다중 상속이 불가능해 새로운 추상 클래스를 만들어 클래스의 계층을 표현할 수밖에 없다.
- 따라서 계층 구조를 만들기 위해 많은 조합이 필요하고 결국 조합이 폭발한다.

4. 래퍼 클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상 시키는 안전하고 강력한 수단이 된다.
- 타입을 추상 클래스로 정의했을 때: 기능 추가 방법은 상속뿐이다. -> 활용도가 떨어진다.
- 래퍼 클래스의 활용도가 더 높다.

## 2. 인터페이스의 디폴트 메서드 제약
- 디폴트 메서드를 제공할 때는 @implSpec을 붙여 문서화한다.
- equals 와 hashCode는 디폴트 메서드로 정의하면 안된다.
- 인터페이스는 인스턴스 필드를 가질 수 없다.
- public이 아닌 정적 멤버도 가질 수 없다.
- 우리가 만들지 않은 인터페이스에는 디폴트 메서드를 추가할 수 없다.

## 3. 인터페이스와 추상 골격 구현 클래스
1. 개념
- 인터페이스: 타입 + 디폴트 메서드
- 골격 구현 클래스: 나머지 메서드들까지 구현
- 인터페이스 구현에 필요한 대부분의 일들이 완료된다. -> 템플릿 메서드 패턴
- 네이밍 관례: Abstract[Interface명]: AbstractCollection, AbstractSet, AbstractList, AbstractMap

2. 장점
- 추상 클래스처럼 구현을 도와주는 동시에, 추상 클래스로 타입을 정의할 때 따라오는 심각한 제약에서 자유롭다.

## 4. 시뮬레이트한 다중 상속(simulated multiple inheritance)
- 골격 구현 클래스를 우회적으로 이용하는 방식
- 인터페이스를 구현한 클래스에서, 골격 구현을 확장한 private 내부 클래스를 정의하고, 각 메서드 호출을 내부 클래스의 인스턴스에 전달한다.

## 5. 골격 구현 클래스 작성 방법
1. 인터페이스를 잘 살펴 다른 메서드들의 구현에 사용되는 기반 메서드를 선정
2. 기반 메서드들을 사용해 직접 구현할 수 있느 메서드들을 모두 디폴트 메서드로 제공
3. 기반 메서드나 디폴트 메서드로 만들지 못한 메서드가 남아있다면, 이 인터페이스를 구현하는 골격 구현 클래스를 만들어 작성
4. 골격 구현은 기본적으로 상속이므로, 설계 및 문서화 지침을 모두 따라야 한다.

## 6. 단순 구현
- 골격 구현의 작은 변종
- 골격 구현처럼 상속을 위해 인터페이스를 구현했으나 추상 클래스가 아니다.


728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

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

 

## 1. 상속을 고려한 설계와 문서화

- 메서드를 재정의하려면 어떤 일이 일어나는지 정확히 정리하여 문서로 남겨야 한다. 즉, 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 (자기사용) 문서로 남겨야 한다.

- 클래스의 API로 공개된 메서드에서 클래스 자신의 또 다른 메서드를 호출할 수도 있다. 그런데 재정의가 가능한 메서드라면 그 사실을 API 명세에 적시해야 한다.

 

## 2. 좋은 API 문서란 어떻게가 아닌 무엇을 하는지를 설명해야 한다.

- 상속이 캡슐화를 해치기 때문에 안전하게 상속할 수 있도록 하려면 내부 구현 방식을 설명해야 한다. 그래서 특정 클래스의 메서드를 재정의하면 내부의 다른 메서드의 동작에 주는 영향도 정확하게 설명하고 있는 API들이 있다.

 

### @implSpec

- 자기사용 패턴(self-use pattern)에 대해서도 문서에 남겨 다른 프로그래머에게 그 메서드를 올바르게 재정의하는 방법을 알려야 한다.

- 일반적인 문서화 주석은 해당 메서드와 클라이언트 사이의 관계를 설명한다.

- @implSpec 주석은 해당 메서드와 하위 클래스 사이의 관계를 설명하며, 하위 클래스들이 그 메서드를 상속하거나 super 키워드를 이용해 호출할 때 그 메서드가 어떻게 동작하는지를 명확히 인지하고 사용하게 해야 한다.

- tag "implSpec:a:Implementation Requirement" 스위치를 키지 않으면 @implSpec 태그를 무시한다.

 

- 효율적인 하위 클래스를 큰 어려움없이 만들 수 있게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다. 또는 드물게는 protected 필드로 공개해야 할 수도 있다.

 

## 3. 상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출

- protected 메서드 하나하나가 내부 구현에 해당하므로 그 수는 가능한 적어야 한다. 다만, 너무 적게 노출해서 상속으로 얻는 이점을 없애지 않도록 주의해야 한다.

- 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일하다.

- 꼭 필요한 protected 멤버를 놓쳤다면 하위 클래스를 작성할 때 그 빈자리가 확연히 드러난다.

- 거꾸로 하위 클래스를 여러개 만들 때까지 전혀 쓰이지 않는 protected 멤버는 사실 private 였어야 할 가능성이 크다.

 

## 4. 상속용 클래스는 영원히 책임져야 한다.

- 널리 쓰일 클래스를 상속용으로 설계한다면 문서화한 내부 사용 패턴과 protected 메서드와 필드를 구현하면서 선택한 결정을 영원히 책임져야 한다.

- 이 결정들이 그 클래스의 성능과 기능에 영원한 족쇄가 될 수 있다.

- 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.

 

## 5. 상속용 클래스의 생성자는 재정의 메서드를 호출해서는 안된다.

- 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 기능 메서드를 호출해서는 안된다.

- 상위 클래스의 생성자는 하위클래스의 생성자 보다 먼저 실행되고, 하위클래스에서 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면, 의도대로 동작하지 않을 것이다.

- private, final, static 메서드는 재정의가 불가능하니 생성자에서 안심하고 호출해도 된다.

 

### clone과 readObject 메서드

- clone과 readObject 메서드는 생성자와 비슷한 효과를 낸다. (새로운 객체를 만든다.)

- 따라서, 상속용 클래스에서 Cloneable 이나 Serializable을 구현할지 정해야 한다면, 이들을 구현할 때 따라는 제약도 생성자와 비슷하다.

- 즉, clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.

- readObject의 경우 하위 클래스의 상태가 미처 다 역직렬화되기 전에 재정의한 메서드부터 호출하게 된다.

- clone의 경우 하위 클래스의 clone 메서드가 복제본의 상태를 수정하기 전에 재정의한 메서드를 호출한다.

- Serializable을 구현한 상속용 클래스가 readResolve나 withReplace 메서드를 갖는다면 이 메서드들은 private가 아닌 protected로 선언해야 한다.

 

## 6. 클래스를 상속용으로 설계할 때는 주의해야 한다.

- 클래스를 상속용으로 설계하려면 엄청난 노력이 들고 그 클래스에 안기는 제약도 상당하다.

- 추상 클래스나 인터페이스의 골격 구현처럼 상속을 허용하는 게 명백히 정당한 상황이 있고, 불변 클래스처럼 명백히 잘못된 상황이 있다.

- 일반적인 구체 클래스는 final도 아니고 상속용으로 설계되지도 않았고 문서화되지도 않았다. 따라서 그대로 두면 위험하다. 클래스에 변화가 생길 때마다 하위 클래스를 오동작하게 만들 수 있다.

* 이 문제를 해결하는 가장 좋은 방법은 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이다.

 

### 7. 상속을 금지하는 두 가지 방법

1. 클래스를 final로 선언

2. 모든 생성자를 private나 package-private로 선언하고 public 정적 팩터리를 만들어 주는 것

 

- 핵심 기능을 정의한 인터페이스가 있고, 클래스가 그 인터페이스를 구현했다면 상속을 금지해도 개발하는 데 아무런 어려움이 없을 것이다. Set, List, Map이 그 예다. 또는 래퍼 클래스를 이용하는 것도 적절한 대안이 될 수 있다.

 

### 상속을 반드시 허용해야 한다면?

- 클래스 내부에서는 재정의 가능 메서드를 사용하지 않게 만들고 문서로 남겨야 한다.

- 즉, 재정의 가능 메서드를 호출하는 자기사용 코드를 완벽히 제거해야 한다.

- 이렇게 하면 상속해도 그리 위험하지 않다. 메서드를 재정의해도 다른 메서드의 동작에 아무런 영향을 주지 않기 때문이다.

 

## 정리

- 상속용 클래스를 설계한다면, 클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남겨야 한다.

- 일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 한다. 그러지 않으면 그 내부 구현 방식을 믿고 활용하던 하위 클래스를 오동작하게 만들 수 있다.

- 다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야 할 수도 있다.

- 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 낫다.

- 상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만들면 된다.

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

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

- 상속이 안전할 때
* 상위 클래스와 하위 클래스를 모두 같은 프로그래머가 통제하는 패키지 안에서 사용한다.
* 확장할 목적으로 설계되었고 문서화도 잘 되어있다.
- 상속이 안전하지 않을 때
* 일반적인 구체 클래스를 패키지 경계를 넘어 다른 패키지의 구체 클래스를 상속하는 일은 위험하다. 이때 '상속'은 다른 패키지의 구체 클래스를 확장하는 '구현 상속'을 의미하며, 인터페이스의 상속과는 무관하다.

# 하위 클래스가 깨지기 쉬운 이유

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

* 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
* 상위 클래스는 릴리즈마다 내부 구현이 달라질 수 있으며, 그 여파로 코드 한 줄 건드리지 않은 하위 클래스가 오동작할 수 있다.

## 2. 상속의 문제를 해결하는 방법: 컴포지션

- 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하면 된다.
- 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션(composition; 구성)이라 한다.
- 새 클래스의 인스턴스 메서드들은 (private 필드를 참조하는) 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환한다.
- 이 방식을 전달(forwarding)이라 하며, 새 클래스의 메서드들을 전달 메서드(forwarding method)라 부른다.
- 그 결과 새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며, 심지어 기존 클래스에 새로운 메서드가 추가되더라도 전혀 영향받지 않는다.

- 상속 방식은 구체 클래스를 각각 따로 확장해야 하며, 지원하고 싶은 상위 클래스의 생성자 각각에 대응하는 생성자를 별도로 정의해줘야 한다.
- 컴포지션 방식은 한 번만 구현해두면 어떠한 Set 구현체라도 계측할 수 있으며, 기존 생성자들과도 함께 사용할 수 있다.

- 래퍼 클래스와 Self 문제
- 콜백 프레임워크에서는 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출(콜백)때 사용하도록 한다.
- 내부 객체는 자신을 감싸고 있는 래퍼의 존재를 모르니 대신 자신(this)의 참조를 넘기고, 콜백 때는 래퍼가 아닌 내부 객체를 호출하게 되는데, 이를 SELF 문제라고 한다.

## 3. 상속과 is-a

- 상속은 반드시 하위 클래스가 상위 클래스의 '진짜' 하위 타입인 상황에서만 쓰여야 한다.
- 즉, 클래스 B가 클래스 A와 is-a 관계일 때만 클래스 A를 상속해야 한다.
- 만약, is-a 관계가 아니라면 A는 B의 필수 구성 요소가 아니라 구현하는 방법 중 하나일 뿐이다.

## 4. 상속을 사용할 때 주의할 점

- 컴포지션을 써야할 상황에서 상속을 사용하는 건 내부 구현을 불필요하게 노출하는 것과 같다.
- 클라이언트에서 노출된 내부에 직접 접근할 수 있게 된다.
- 최악의 경우, 클라이언트에서 상위 클래스를 직접 수정하여 하위 클래스의 불변식을 해칠 수도 있다.
- 상속은 상위 클래스의 API를 결함까지도 승계하므로 주의해야 한다.

## 정리

- 상속은 강력하지만 캡슐화를 해친다는 문제가 있다.
- 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일 때만 써야 한다.
- is-a 관계일 때도 하위 클래스의 패키지가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해 설계되지 않았을 수도 있기 때문에 안심할 수 없다.
- 상속의 취약점을 피하려면 상속 대신 컴포지션과 전달을 사용한다. 특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다. 래퍼 클래스는 하위 클래스보다 견고하고 강력하다.
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !