Effective Java - Chapter 4

12 분 소요

Effective Java

[Effective Java 3/E] 4장 클래스와 인터페이스

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

잘 설계된 컴포넌트란?

클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 잘 숨긴 컴포넌트다. 구현과 API를 깔끔히 분리해 모든 내부 구현을 숨긴다. 그래서 다른 컴포넌트와 소통할 때 서로의 내부 동작 방식을 몰라야 한다. 이러한 숨기는 작업을 캡슐화 또는 정보 은닉이라 한다. 정보 은닉으로 병렬화를 시켜 시스템 개발 속도를 높일 수도 있고, 소프트웨어 재사용성을 높이기도 한다. 개별 컴포넌트가 독자적으로 동작할 수도 있고, 동작을 검증할 수도 있기 때문이다.

접근제어자

public: Top-level Class, Interface에 부여하면 공개 API가 된다. public 클래스의 instance field는 되도록 public이 아니어야 한다.
protected: 상속 관계라면 상위 클래스의 멤버에 접근할 수 있다. 공개 API가 된다.
pakage-private: 패키지 외부에서 쓸 이유가 없을 경우. API가 아니기 때문에 언제든 수정할 수 있다.
private: 멤버를 선언한 Top-level Class에서만 접근할 수 있다. API 외의 모든 멤버를 private이나 package-private으로 해 준다.

고려할 것 1: 리스코프 치환 원칙: 재정의할 때 상위 클래스의 메서드 접근 수준을 좁게 설정할 수 없다.
고려할 것 2: 테스트코드는 같은 패키지에 두고 package-private으로 설정하면 안전하다.
고려할 것 3: public static final에 있는 값이 reference라면, Getter에서 그대로 반환하지 말고 다른 배열에 deepcopy해서 반환하자 (방어적 복사).

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

public, protected같이 패키지 바깥에서 접근할 수 있는 클래스라면 접근자를 제공해야 한다.
package-private, private은 데이터 필드를 노출해도 괜찮다.
final을 붙이면 노출해도 괜찮다.

// 16-3 final 필드를 노출한 public class
public final class Time{
  private static final int HOURS_PER_DAY = 24;
  private static final int MINUTES_PER_HOUR = 60;

  public final int hour;
  public final int minute;
  // final은 불변인데 Time생성자를 여러 번 호출하면 바꿀 수 있다.
  public Time(int hour, int minute){
    ....(중략)
    this.hour = hour;
    this.minute = minute;
  }
}

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

클래스를 불변으로 만드는 다섯 가지 규칙

객체의 상태를 변경하는 메서드를 제공하지 않는다.
클래스를 확장할 수 없도록 한다. (final / private으로 하고 public 정적 팩터리 제공)
모든 필드를 final로 선언한다.
모든 필드를 private으로 선언한다.
자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. (레퍼런스 값에 외부 컴포넌트가 접근할 수 없도록 한다.)

불변 객체는 단순하다. Thread-safe하기 때문에 따로 동기화 할 필요도 없다. 객체를 만들 때 불변 객체들을 구성요소로 하면 구조가 복잡하더라도 불변식을 유지하기 쉽다. 불변 객체는 예외가 발생하더라도 예외 발생 전과 후가 변함이 없다(failure atomicity). 단, 불변 객체는 비싸다. 값의 가짓수가 많다면 그만큼 인스턴스 수도 늘어난다.
=> 다단계 연산(multistep operation)을 기본 기능으로 제공하자. (ex. 가변 동반 클래스를 두고 package-private이나 public으로 한다.)
클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
단순한 값 객체는 항상 불변으로 만들자. 어쩔 수 없다면 가변 동반 클래스를 쌍으로 이루게 하자.
객체가 가질 수 있는 상태의 수를 줄이자. 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다. 생성자는 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.

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

상속은 캡슐화를 깨뜨린다.상위 클래스에 하위클래스가 의존하기 때문에 상위클래스에서 변경이나 오류가 생긴다면 하위클래스에도 영향이 간다.
Composition : 기존 클래스를 새로운 클래스의 구성요소로 쓰는 설계
새 클래스의 인스턴스 메서드들이 기존 클래스의 메서드를 호출해서 결과를 반환한다. (forwarding)
기존 클래스를 감싸고 있는 새 클래스를 Wrapper Class라고 한다.
기존 클래스에 새 클래스를 덧씌우니까 Decorator 패턴이라고도 한다.
상속은 반드시 하위 클래스가 상위 클래스의 ‘진짜’ 하위 타입인 상황에서만 쓰여야 한다. (is-a 관계일때만)

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

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

재정의할 수 있는 메서드를 어떻게 사용해야 하는지 문서화해야 한다.

=> 메서드 주석에 @implSpec 태그를 붙이면 자바독 도구가 API문서에 Implementation Requirements로 만들어 준다.
클래스 내부 동작 과정에서 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개한다.
하위 클래스를 만들어 보고, 필요한 멤버는 protected로, 굳이 필요하지 않은 멤버는 private으로 바꾼다.
=> 상속용 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.
상속용 클래스의 생성자는 재정의 가능한 메서드를 호출해서는 안 된다.
=> 상위 클래스에서 정의된 메서드가 하위 클래스에서 재정의된 메서드보다 먼저 호출되기 때문. Cloneable과 Serializable 인터페이스를 구현한 클래스는 상속할 수 있게 하지 말자. 차라리 하위 클래스에서 구현하게 하자. 어쩔 수 없다면 재정의 될 수 있는 메서드를 protected로 선언해야 한다.

상속 금지하는 방법

클래스를 final로 선언한다.
모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어 준다.

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

추상 클래스 vs 인터페이스

공통점 : 둘 다 인스턴스 메서드를 구현할 수 있다. (Java 8부터)
차이점 : 추상 클래스를 구현하는 클래스는 반드시 추상 클래스를 상속받아야 한다. 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다(유연하다).

믹스인(mixin)

: 대상 타입의 주된 기능에 선택적 기능(인터페이스 기능)을 혼합하는 것.
=> 인터페이스를 사용하는 것이 권장된다. Item 19에서 설명하듯이 상속은 클래스가 깨지기 쉽다.

템플릿 메서드 패턴

인터페이스와 추상 골격 구현 (skeletal implementation) 클래스를 함께 제공하여 인터페이스와 추상 클래스의 장점을 모두 취하는 방식이다.
인터페이스로는 타입을 정의하고, 골격 구현 클래스는 나머지 메서드들을 구현해둔다. 이렇게 하면 골격 구현 클래스를 상속 받기만 해도 인터페이스를 구현하는 데 필요한 일이 대부분 완료된다. 골격 구현 클래스가 템플릿이라고 보면 될 것 같다.
골격 구현 클래스의 이름은 관례상 ‘Abstract인터페이스이름’이다.

골격 구현 클래스 작성 방법

인터페이스에서 다른 메서드들의 구현에 사용되는 기반 메서드들을 선정
기반 메서드들을 사용해 직접 구현할 수 있는 메서드를 인터페이스에서 디폴트 메서드로 제공하게 한다.
나머지 메서드는 골격 구현 클래스에 추상 메서드로 넣는다.
주의! Object 메서드들은 디폴트 메서드로 제공해서는 안 된다.

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

디폴트 메서드

자바 8에 추가된 디폴트 메서드는 구현체에 대해 아무것도 모른 채 삽입된다. 그래서 디폴트 메서드는 컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있으니 설계할 때 주의를 기울여야 한다. 인터페이스를 릴리즈하기 전에 꼭 테스트를 거쳐야 한다. 최소한 세 가지 정도는 구현해 보자.

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

인터페이스의 역할

자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다. (ex. ArrayList는 List 인터페이스를 구현했고, List 용도로 사용된다는 것을 의미한다)

상수 인터페이스

메서드 없이 static final로만 이루어진 인터페이스다. 상수 공개를 위해 만들어졌다지만 문제점이 많다. 상수를 공개하려거든 상수를 사용하는 곳에 직접 추가하자. 열거 타입으로 만들어도 좋다. 차라리 인스턴스화가 불가능한 유틸리티 클래스에 담는 게 낫다.

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

태그 달린 클래스

태그로 클래스가 표현하는 핵심 의미를 정할 수 있지만 하지 말자. circle과 square을 한 클래스에 표현하고 싶어도 하지 말자. Shape에 공통 코드를 빼고 클래스를 두 개로 만드는 게 더 효율적이다.

태그 달린 클래스 리팩터링 방법

계층 구조의 root가 될 추상 클래스를 정의하고, 태그 값에 따라 동작이 달라지는 메서드를 추상 메서드로 선언한다. 공통 동작은 일반 메서드로 한다.
루트 클래스를 확장한 구체 클래스를 태그 의미별로 하나씩 정의한다. 추상 메서드를 태그 의미에 맞게 재정의한다.

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

중첩 클래스(nested class)

다른 클래스 안에 정의된 클래스.
자신을 감싼 outer class에서만 쓰여야 하고, 그 외에 쓰임새가 있다면 top-level 클래스로 만들어야 한다. (클래스 파일 하나 더 만들란 얘기다.)
종류: static 멤버 클래스, non-static 멤버 클래스, 익명 클래스, 지역 클래스 static 멤버 클래스를 제외한 나머지는 다 inner class에 해당한다.

정적 멤버 클래스 (static member class)

다른 클래스 안에 선언되고, outer 클래스의 private 멤버에도 접근할 수 있다.
바깥 인스턴스와 독립적으로 존재할 수 있다.
멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 정적 멤버 클래스로 만들어야 한다. (static 안붙이면 인스턴스마다 참조 생겨서 비효율적임 + GC가 찾지 못해서 메모리 누수 생김) private 정적 멤버 클래스는 outer class가 표현하는 객체의 구성요소를 나타낼 때 쓴다.

비정적 멤버 클래스 (non-static member class)

outer class의 인스턴스와 암묵적으로 연결된다.
this로 outer class의 인스턴스를 호출할 수도 있다.
바깥 인스턴스 없이는 생성할 수 없다.
어댑터를 정의할 때 자주 쓰인다.

익명 클래스(anonymous class)

outer class의 멤버가 아니다.
선언과 동시에 인스턴스가 만들어진다.
instanceof나 클래스의 이름이 필요한 작업은 수행할 수 없다.
여러 인터페이스를 구현할 수 없다.
인터페이스 구현과 상속을 동시에 할 수 없다.
10줄 이하가 권장된다.
정적 팩터리 메서드를 구현할 때 쓰인다.

지역 클래스(local class)

지역변수를 선언할 수 있는 곳이면 어디든 선언할 수 있다.
scope도 지역변수와 동일하다.
이름이 있다.
static member는 가질 수 없다.
익명클래스가 여러 번 쓰일 경우 지역 클래스로 만들자.

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

파일 하나에는 톱레벨 클래스(인터페이스)를 하나만 넣자! 소스 파일을 어떤 순서로 컴파일 하든 프로그램이 바뀌지 않게 된다.

댓글남기기