Effective Java 3rd - Ch04
포스트
취소

Effective Java 3rd - Ch04

Java 기초를 다지기 위해 효과적인 자바란 책을 공부 중입니다.

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

  • 추상화의 기본 단위인 클래스와 인터페이스는 자바 언어의 심장과 같다
  • 클래스와 인터페이스를 쓰기 편하고, 견고하며, 유연하게 만드는 방법을 안내한다

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

캡슐화(정보 은닉)의 장점

  • 시스템 개발 속도를 높인다
    • 여러 컴포넌트의 병렬 개발 가능
  • 시스템 관리 비용을 낮춘다
    • 컴포넌트 디버깅이 수월하고 교체 부담이 적다
  • 성능 최적화에 도움을 준다
    • 영향 안주고 해당 컴포넌트만 최적화
  • 소프트웨어 재사용성을 높인다
    • 외부의존 없이 사용 가능하다면 낯선 곳에도 유용하게 쓰일 수 있음
  • 큰 시스템을 제작하는 난이도를 낮춰준다
    • 시스템 전체가 완성되지 않은 상태에서도 개별 검증이 가능

캡슐화(정보 은닉)을 위한 장치 : 접근제어 메커니즘

기본원칙 : 모든 클래스와 멤버의 접근서을 가능한 한 좁혀야 한다.

  • private : 멤버를 선언한 톱레벨 클래스에서만 접근할 수 있다
  • package-private : 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다.
  • protected : package-private 의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근 가능
  • public : 모든 곳에서 접근할 수 있다

public 클래스의 인스턴스 필드는 되도록 public 이 아니어야 한다 public 가변 필드를 갖는 클래스는 일반적으로 스레드에 안전하지 않다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 보안 허점이 숨어 있다 : 접근자의 내용물(배열)을 수정할 수 있다
public static final Thing[] 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();
}

:gift: 자바9 에서는 모듈 시스템 개념이 도입되었음

  • 모듈은 패키지들의 묶음 : module > package > public > protected > package-private > private
  • 모듈은 자신에 속하는 패키지 중 공개(export) 할 것들을 (관례상 module-info.java 파일에) 선언한다.
  • 모듈 시스템을 이용하면 클래스를 외부에 공개하지 않으면서도 같은 모듈 내의 패키지 사이에서는 자유롭게 공유 가능

:bangbang:   핵심정리

  • 프로그램 요소의 접근성은 가능한 한 최소한으로 하라
    • 꼭 필요한 것만 골라 최소한의 public API 를 설계
  • public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안된다
    • public static final 필드가 참조하는 객체가 불변인지 확인하라

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

코드 16-1 이처럼 퇴보한 클래스는 public 이어서는 안된다

1
2
3
4
class Point {
  public double x;
  public double y;
}

코드 16-2 접근자와 변경자(mutator) 메서드를 활용해 데이터를 캡슐화한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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; }
}

:hand: package-private 클래스 혹은 private 중첩 클래스라면 데이터 필드를 노출한다 해도 하등의 문제가 없다.

코드 16-3 불변 필드를 노출한 public 클래스 : 과연 좋은가?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;

  public Time(int hour, int minute) {
    if( hour < 0 || hour >= HOURS_PER_DAY )
      throw new IllegalArgumentException("시간: "+hour);
    if( minute < 0 || minute >= MINUTES_PER_HOUR )
      throw new IllegalArgumentException("분: "+minute);
    this.hour = hour;
    this.minute = minute;
  }

  ... // 생략
}

:bangbang:   핵심정리

  • public 클래스는 절대 가변 필드를 직접 노출해서는 안된다
    • 불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없다
  • 하지만 package-private 클래스나 private 중첩 클래스에서는 종종 필드를 노출하는 편이 나을 때도 있다

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

불변 클래스란 그 인스턴스의 내부 값을 수정할 수 없는 클래스

클래스를 불변으로 만들려면 다음 다섯 가지 규칙을 따르면 된다

  • 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다
  • 클래스를 확장할 수 없도록 한다
  • 모든 필드를 final 로 선언한다
  • 모든 필드를 private 으로 선언한다
  • 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다

코드 17-1 불변 복소수 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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(re + c.re, im + c.im);
  }
  public Complex minus(Complex c) {
    return new Complex(re - c.re, im - c.im);
  }
  public Complex times(Complex c) {
    return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
  }
  public Complex dividedBy(Complex c) {
    double tmp = c.re * c.re + c.im * c.im;
    return new Complex( (re * c.re - im * c.im)/tmp,
                    , (im * c.re - re * c.im)/tmp);
  }

  @Override public boolean equals(Object o) {
    if( o == this ) return true;
    if( !(o instanceof Complex)) return false;
    Complex c = (Complex) o;

    // == 대신 compare 를 사용하는 이유는 63쪽을 확인하라
    return Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0;
  }

  @Override public int hashCode() {
    return 31 * Double.hashCode(re) + Double.hashCode(im);
  }

  @Override public String toString() {
    return "(" + re + " + " + im + "i)";
  }
}

63쪽 : float 와 double 을 특별 취급하는 이유

  • Float.NaN, -0.0f, 특수한 부동소수 값 등을 다뤄야 하기 때문이다
  • Float.equals 와 Double.equals 메서드는 오토박싱을 수반할 수 있으니 성능상 좋지 않다

이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다.

  • cf. 절차적 혹은 명령형 프로그래밍에서는 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변하게 된다
  • 메서드 이름으로 add 대신 plus 를 선택한 것도 주목 : 자신이 변경되지 않음을 강조하려는 의도

코드 17-2 생성자 대신 정적 팩토리를 사용한 불변 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Complex {
  private final double re;
  private final double im;

  // 상속할 수 없다. 사실상 final : 불변 유지
  private Complex(double re, double im) {
    this.re = re;
    this.im = im;
  }

  // 정적 팩토리 valueOf
  public static Complex valueOf(double re, double im) {
    return new Complex(re, im);
  }

  ... // 생략
}

불변 객체의 성격

  • 불변객체는 단순하다
  • 불변객체는 근본적으로 스레드에 안전하여 따로 동기화할 필요가 없다
  • 불변객체는 안심하고 공유할 수 있다
    • public static final Complex ZERO = new Complex(0,0);
  • 불변객체 끼리는 내부 데이터를 공유할 수 있다
  • 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다
    • ex: Map 의 key 와 Set 의 item 으로 쓰기에 안성맞춤
  • 불변객체는 그 자체로 실패 원자성을 제공한다
    • 실패 원자성 : 메서드에서 예외가 발생한 후에도 그 객체는 여전히 유효한 상태를 유지
  • :star: 단점 : 값이 다르면 반드시 독립된 객체로 만들어야 한다

가변 동반 클래스 (불변 객체 만들기를 설명하면서)

  • 원하는 객체를 완성하기까지의 단계가 많고, 그 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 불거진다
  • 대처하는 방법
      1. 흔히 쓰일 다단계 연산들을 예측하여 기본 기능으로 제공하는 방법
        • ex: String 의 가변 동반 클래스는 StringBuilder (구버전으로는 StringBuffer)
      1. 클래스를 public 으로 제공하는 것이 최선

:hand: 불변 객체는 캐싱해서 사용하기 좋다면서 직렬화 이야기로 잠깐 삼천포

:bangbang:   직렬화 할 때 추가로 주의할 점

  • Serializable 을 구현하는 불변 클래스의 내부에 가변 객체를 참조하는 필드가 있다면
    • readObject 나 readResolve 메서드를 반드시 제공하거나
    • ObjectOutputStream.writeUnsharded 와 ObjectInputStream.readUnsharded 메서드를 사용해야 한다
  • 플랫폼이 제공하는 기본 직렬화 방법이면 충분하더라도 ==> (보안) 공격자가 가변 인스턴스를 만들수 있다

:bangbang:   정리

  • 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다
    • getter 가 있다고 해서 무조건 setter 를 만들지 말자
    • PhoneNumber, Complex 처럼 단순한 값 객체는 항상 불변으로 만들자
  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자
    • 변경해야 할 필드를 제외한 모두를 final로 선언하자
    • 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다
    • 생성자와 정적 팩토리 외에는 public 으로 제공해서는 안된다
    • 재활용 목적으로 상태를 초기화 하는 메서드도 안된다

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

상속은 재사용을 위한 강력한 수단이지만, 항상 최선은 아니다. (위험성 존재)

  • 메서드 호출과 달리 상속은 캡슐화를 깨뜨린다
  • 상속은 반드시 하위 클래스가 상위 클래스의 ‘진짜’ 하위 타입인 상황에서만 쓰여야 한다
    • 순수한 is-a 관계일 때만 써야 한다

코드 18-1 잘못된 예 : 상속을 잘못 사용했다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class InstrumentedHashSet<E> extends HashSet<E> {
  // 추가된 필드 : 원소의 수
  private int addCount = 0;

  public InstrumentedHashSet() {}
  public InstrumentedHashSet(int initCap, float loadFactor) {
    super(initCap, loadFactor);
  }

  // add 를 재정의 했다 (HashSet 의 내부구현 방식을 모른체)
  @Override public boolean add(E e) {
    addCount++;
    return super.add(e);
  }

  @Override public boolean addAll(Collection<? extends E> c) {
    addCount += c.size();
    return super.addAll(c);   // 자기 사용(self-use): HashSet 의 addAll 은 add 를 호출
  }

  public int getAddCount() {
    return addCount;
  }
}

///////////////////////////

InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll( List.of("틱","택","톡") ); // List.of 는 자바9부터 지원
// **NOTE: 자바8 사용자는 Arrays.asList 를 사용하면 된다 (= List.of)

// s.getAddCount() 의 결과값은? ==> 6

하위 클래스가 깨지기 쉬운 이유는 더 있다. 다음 릴리스에서 상위 클래스에 새로운 메서드를 추가한다면 어떨까? 하위 클래스에서 재정의하지 못한 그 새로운 메서드를 사용해 ‘허용되지 않은’ 원소를 추가할 수 있게 된다. ex) 실제로 Hashtable과 Vector를 컬렉션 프레임워크에 포함시키자 이와 관련한 보안 구멍들을 수정해야 하는 사태가 벌어졌다.

이상의 문제를 피해가는 묘안은?

기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자

1
2
- 이러한 설계를 '컴포지션(composition)' 이라 한다
- 전달(forwarding) : 새 클래스의 메서드들은 기존 클래스의 결과를 반환

이렇게 함으로써

  • 새로운 클래스는 기존 클래스의 내부구현 방식에서 벗어나고
  • 기존 클래스가 변경되어도 전혀 영향받지 않는다

코드 18-2 래퍼 클래스 : 상속 대신 컴포지션을 사용했다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 다른 set 인스턴스를 감싸고 있다는 뜻에서 '래퍼 클래스'
// 다른 set 에 계측 기능을 덧씌운다는 뜻에서 '데코레이터 패턴'이라 한다.

// HashSet 대신에 전달 클래스인 ForwardingSet 를 사용
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
  // 추가된 원소의 수
  private int addCount = 0;

  public InstrumentedHashSet(Set<E> s) {
    super(s);   // 감쌌다
  }

  @Override public boolean add(E e) {
    addCount++;
    return super.add(e);  // 감쌌다
  }
  @Override public boolean addAll(Collection<? extends E> c) {
    addCount += c.size();
    return super.addAll(c);     // 감쌌다
  }

  public int getAddCount() {
    return addCount;
  }
}

////////////////////

Set<Instant> times = InstrumentedHashSet<>(new TreeSet<>(cmp));
Set<E> s = new InstrumentedHashSet<>(new HashSet<>(INIT_CAPACITY));

static void walk(Set<Dog> dogs) {
  InstrumentedHashSet<Dog> iDogs = new InstrumentedHashSet<>(dogs);
  ... // 이 메서드에서는 dogs 대신 iDogs 를 사용한다
}

코드 18-3 재사용 할 수 있는 전달 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ForwardingSet<E> implements Set<E> {
  // 기존 클래스를 인스턴스로 참조 (구성요소로 삼는다 ==> 컴포지션)
  private final Set<E> s;
  public ForwardingSet(Set<E> s) { this.s = s; }

  // 전달 메소드들 (forwarding)
  public void clear()               { s.clear(); }
  public boolean contains(Object o) { return s.contains(o); }
  public boolean isEmpty()          { return s.isEmpty(); }
  public int size()                 { return s.size(); }
  public Iterator<E> iterator()     { return s.iterator(); }
  public boolean add(E e)           { return s.add(e); }
  public boolean remove(Object o)   { return s.remove(o); }
  public boolean containsAll(Collection<?> c)     { return s.containsAll(c); }
  public boolean addAll(Collection<? extends E> c){ return s.addAll(c); }
  public boolean removeAll(Collection<?> c)       { return s.removeAll(c); }
  public boolean retainAll(Collection<?> c)       { return s.retainAll(c); }
  public Object[] toArray())        { return s.toArray(); }
  public <T> T[] toArray(T[] a)     { return s.toArray(a); }

  @Override public boolean equals(Object o)) { return s.equals(o); }
  @Override public int hashCode() { return s.hashCode(); }
  @Override public String toString() { return s.toString(); }
}

컴포지션과 전달의 조합은 넓은 의미로 위임(delegation)이라 한다.

:bangbang:   래퍼 클래스가 콜백(callback) 프레임워크와는 어울리지 않는다는 점만 주의

:arrow_right: 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출(콜백) 때 사용하도록 하는데, 래퍼가 아닌 내부 객체를 호출하게 된다 (Self 문제)

전달 메서드들을 작성하는게 지루하겠지만, Guava 는 모든 컬렉션 인터페이스용 전달 메서드를 전부 구현해뒀다.

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

  • 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용 등에 관한) 문서로 남겨야 한다.
  • 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.
    • 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 ‘유일’하다
  • 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Super {
  // 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다
  public Super() {
    overrideMe();   // <== 하위 클래스에서 의도를 바꿔버리면?
  }
  public void overrideMe() {
  }
}

//////////////////////

public final class Sub extends Super {
  // 초기화 되지 않은 final 필드. 생성자에서 초기화 한다
  private final Instant instant;

  Sub() {
    // 내부적으로 상위클래스의 생성자인 super() 가 실행된다
    instant = Instant.now();
  }

  // 재정의 가능 메서드. 상위 클래스의 생성자가 호출한다
  @Override public void overrideMe() {
    System.out.println(instant);
  }

  public static void main(String[] args) {
    Sub sub = new Sub();
    sub.overrideMe();
    // instant 를 두번 출력할 줄 알았지만, 첫번째는 null 을 출력한다
    // ==> 상위 클래스의 생성자는 하위 클래스의 생성자가
    //   인스턴스 필드를 초기화 하기도 전에 overrideMe 를 호출하기 때문
  }
}

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

Cloneable 과 Serializable 인터페이스를 구현한 클래스를 상속할 수 있게 설계하는 것은 좋지 않은 생각이다. (힘들다)

  • clone 과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다
  • Serializable 을 구현한 상속용 클래스가 readResolve 나 writeReplace 메서드를 갖는다면 이 메서드들은 private 이 아닌 protected 로 선언해야 한다

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

:arrow_right: 상속시키지 않을거면 상속을 금지하는게 상책

:arrow_right: 핵심기능을 정의한 인터페이스가 있다면 상속을 금지해도 개발하는데 아무런 문제가 없을 것이다

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

자바가 제공하는 다중 구현 메커니즘은 인터페이스와 추상클래스 두가지이다

  • 자바8부터 인터페이스도 디폴트 메서드를 제공할 수 있어 (거의 비슷)
  • 둘의 가장 큰 차이는?
    • 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점
    • 반면 인터페이스가 선언한 메서드를 잘 정의한 클래스라면 다른 어떤 클래스를 상속했든 같은 타입으로 취급된다
    • :hand: 추상 클래스는 단일 상속만(자바 제약), 인터페이스는 다중 구현 가능

:hand: 추상 클래스는 추상 메소드를 포함하고 있는 클래스를 의미. 일반(디폴트) 메소드 정의도 가능

:hand: 자바는 상속을 하나만 허용하고 있음 (제약) : 다이아몬드 상속 문제 때문에

인터페이스 특징

  • 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다
  • 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다
    • ex: Comparable 인터페이스 사용은 순서를 정할 수 있다고 선언하는 의미
  • 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다
  • 래퍼 클래스 관용구와 함께 사용하면 기능을 향상시키는 안전하고 강력한 수단이 된다
  • 명백한 기능을 하는 것이 있다면 디폴트 메소드로 제공해 코딩 부담을 줄여준다
1
2
3
4
5
6
7
8
9
10
11
12
public interface Singer {
  AudioClip sing(Song s);
}

public interface Songwriter {
  Song compose(int chartPosition);
}

public interface SingerSongwriter extends Singer, Songwriter {
  AudioClip strum();
  void actSensitive();
}

템플릿 메서드

인터페이스와 추상 골격 구현 클래스를 함게 제공하는 식으로 인터페이스와 추상클래스의 장점을 모두 취하는 방법

  • 관례상 인터페이스 이름이 Interface 라면, 그 골격 구현 클래스의 이름은 AbstractInterface 로 짓는다
  • 어쩌면 Abstract 보다 Skeletal 이 이름으로 적절했을듯 싶지만

코드 20-1 골격 구현을 사용해 완성한 구체 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static List<Integer> intArrayAsList(int[] a) {
  Objects.requiredNonNull(a);

  // 다이아몬드 연산자를 이렇게 사용하는 건 자바9 부터 가능하다
  // 더 낮은 버전을 사용한다면 <Integer>로 수정하자
  return new AbstractList<>() {
    @Override public Integer get(int i) {
      return a[i];      // 오토 박싱
    }
    @Override public Integer set(int i, Integer val) {
      int oldVal = a[i];
      a[i] = val;       // 오토 언박싱
      return oldVal;    // 오토 박싱
    }
    @Override public int size() {
      return a.length;
    }
  };
}

** 참조 : 자바 : 추상화와 인터페이스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
abstract public class Book {
  String name;
  int page;

  abstract void PrintBookName();
  void SetPage(int page) {
    this.page = page;
  }
}

public class Main {
  public static void main(String[] args) {
    Book b1 = new Book() {
      void PrintBookName() {
        System.out.println("이 책의 이름은 "+this.name);
      }
    };
    Book b2 = new Book() {
      void PrintBookName() {
        System.out.println("이 책의 이름은 "+this.name+" (분량 "+page+")");
      }
    };

    b1.name = "해리포터";   b1.PrintBookName();
    b2.name = "해리포터";   b2.PrintBookName();
  }
}

골격 구현 작성은 (조금 지루하지만) 상대적으로 쉽다.

  • 기반 메서드들을 선정 ==> 골격 구현에서 추상 메서드가 된다
  • 기반 메서드들을 사용해 직접 구현할 수 있는 메서드들 모두 디폴트 메서드로 제공
    • 단, eqauls 와 hashCode 같은 Object 의 메서드들은 안된다
    • 만약 인터페이스의 메서드 모두가 기반 메서드와 디폴트 메서드가 된다면 골격 구현 클래스를 별도로 만들 이유는 없다
  • 기반 메서드나 디폴트 메서드로 만들지 못한 메서드가 남아 있다면, 골격 구현 클래스에 작성해 넣는다

코드 20-2 골격 구현 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {

  // 변경 가능한 엔트리는 이 메서드를 반드시 재정의 해야 한다
  @Override public V setValue(V value) {
    throw new UnsupportedOperationException();
  }

  // Map.Entry.equals 의 일반 규약을 구현한다
  @Override public boolean equals(Object o) {
    if( o == this ) return true;
    if( !(o instanceof Map.Entry)) return false;
    Map.Entry<?,?> e = (Map.Entry) o;
    return Objects.equals(e.getKey(), getKey())
        && Objects.equals(e.getValue(), getValue());
  }

  // Map.Entry.hashCode 의 일반 규약을 구현한다
  @Override public int hashCode() {
    return Objects.hashCode(getKey())
          ^ Objects.hashCode(getValue());
  }

  @Override public String toString() {
    return getKey() + "=" + getValue();
  }
}

Map.Entry 인터페이스나 그 하위 인터페이스로는 이 골격 구현을 제공할 수 없다. 디폴트 메서드는 equals, hashCode, toString 같은 Object 메서드를 재정의 할 수 없기 때문이다.

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

자바 8에서는 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가 되었다. 주로 람다를 활용하기 위해서이다.

하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법이다.

코드 21-1 자바8의 Collection 인터페이스에 추가된 디폴트 메서드

1
2
3
4
5
6
7
8
9
10
11
default boolean removeIf(Predicate<? super E> filter) {
  Objects.requiredNonNull(filter);
  boolean result = false;
  for( Iterator<E> it = iterator(); it.hasNext(); ) {
    if( filter.test(it.next()) ) {
      it.remove();
      result = true;
    }
  }
  return result;
}

디폴트 메서드는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있다.

핵심은 명백하다. 디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야 한다.

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

인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역활을 한다. 오직 이 용도로만 사용해야 한다. 상수 공개용 수단으로 사용하지 말자.

이 지침에 맞지 않는 예로 ‘상수 인터페이스’라는 것이 있다.

코드 22-1 상수 인터페이스 안티패턴 - 사용 금지!

1
2
3
4
5
6
7
8
9
10
// 상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예이다
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;
}

상수를 공개할 목적이라면

  • 강하게 연결된 경우 그 클래스나 인터페이스 자체에 추가해야 한다
  • 열거 타입으로 나타내기 적합한 상수라면 열거 타입으로 만들어 공개하면 된다
  • 그것도 아니라면, 인스턴스화 할 수 없는 ‘유틸리티 클래스’에 담아 공개하자

코드 22-2 상수 유틸리티 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package effectivejava.chapter4.item22.constantutilityclass;

public class PhysicalConstants {
  private 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;
}

// 숫자 리터럴에 사용한 밑줄(_)
// ==> 자바7부터 허용되는 이 밑줄은 숫자 리터럴의 값에는 아무런 영향을 주지 않으면서
//    읽기는 훨씬 편하게 해준다. (고정소수든 부동소수든) 5자리 이상이라면 밑줄 사용을 권한다.
//    십진수 리터럴도 (정수든 부동소수든) 세자리씩 묶어주는 것이 좋다.

코드 22-3 정적 임포트를 사용해 상수 이름만으로 사용하기

1
2
3
4
5
6
7
8
9
10
// PhysicalConstants 를 빈번히 사용한다면 정적 임포트가 값어치를 한다
// ==> 일반 임포트라면 PhysicalConstants.AVOGADROS_NUMBER 로 명시해야 한다
import static effectivejava.chapter4.item22.constantutilityclass.PhysicalConstants.*;

public class Test {
  double atoms(double mols) {
    return AVOGADROS_NUMBER * mols;
  }
  ... // 생략
}

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

코드 23-1 태그 달린 클래스 - 클래스 계층구조보다 훨씬 나쁘다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Figure {
  enum Shape { RETANGLE, CIRCLE };

  // **NOTE: 태그 필드 - 현재 모양을 나타낸다
  final Shape shape;

  // 다음 필드들은 모양이 사각형일 때만 쓰인다
  double length;
  double width;

  // 다음 필드는 모양이 원일 때만 쓰인다
  double radius;

  // 원용 생성자
  Figure(double radius) {
    shape = Shape.CIRCLE;
    this.radius = radius;
  }

  // 사각형용 생성자
  Figure(double length, double width) {
    shape = Shape.RETANGLE;
    this.length = length;
    this.width = width;
  }

  double area() {
    switch(shape) {
      case RETANGLE:
          return length * width;
      case CIRCLE:
          return Math.PI * (radius * radius);
      default:
          throw new AssertionError(shape);
    }
  }
}

태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다.

  • 쓸데없는 코드가 많다
  • 가독성도 나쁘다
  • 메모리도 많이 사용한다
  • 쓰이지 않는 필드들까지 생성자에서 초기화해야 한다
  • 또 다른 의미를 추가하려면 switch 코드를 수정해야 한다
  • 인스턴스 타입만으로는 현재 의미를 알 길이 전혀 없다

태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일 뿐이다.

클래스 계층 구조로 바꾸는 방법

  1. root 가 될 추상 클래스를 정의
    • 태그 값에 따라 동작이 달라지는 메서드를 추상 메서드로 선언
    • 태그에 상관없이 동작이 일정한 메서드들을 일만 메서드로 추가
    • 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들 추가
  2. root 클래스를 확장한 구체 클래스를 하나씩 정의
    • 하위 클래스에 각자의 의미에 해당하는 데이터 필드 추가
    • root 클래스가 정의한 추상 메서드를 각자 구현

코드 23-2 태그 달린 클래스를 클래스 계층구조로 변환

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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; }
}

///////////////////////////////

// 계층 관계의 확장도 깔끔해졌다
class Square extends Rectangle {
  Square(double side){
    super( side, side);
  }
}

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

중첩 클래스(nested class)란 다른 클래스 안에 정의된 클래스

중첩 클래스의 종류

  • 정적 멤버 클래스
    • 흔히 바깥 클래스와 함게 쓰일 때만 유용한 public 도우미 클래스
    • 예를 들어 계산기 클래스의 Operation 열거 타입 : Calculator.Operation.PLUS
  • (비정적) 멤버 클래스
    • 정적 멤버 클래스와는 static 이 붙어있지 않다는 차이
    • 바깥 클래스의 인스턴스와 암묵적으로 연결된다
    • 어댑터를 정의할 때 자주 쓰인다 (다른 클래스의 인스턴스처럼 보이게 하는 뷰)
  • 익명 클래스
    • 이름이 없다. 또한 바깥 클래스의 멤버도 아니다
    • 멤버와 달리 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다 (코드의 어디서든 만들 수 있다)
    • 람다를 지원하기 전에는 익명 클래스를 주로 사용했다
  • 지역 클래스
    • 가장 드물게 사용된다

코드 24-1 비정적 멤버 클래스의 흔한 쓰임 - 자신의 반복자 구현

1
2
3
4
5
6
7
8
9
10
11
public class MySet<E> extends AbstractSet<E> {
  ... // 생략

  @Override public Iterator<E> iterator() {
    return new MyIterator();
  }

  private class MyIterator implements Iterator<E> {
    ... // 생략
  }
}

:bangbang:   핵심 정리

  • 메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길다면 멤버 클래스로 만든다
  • 비정적 클래스
    • 멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면
  • 정적 클래스
    • 비정적 클래스로 하지 않아도 된다면
  • 익명 클래스(이제는 람다가 대체)
    • 중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한 곳이고
    • 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면
  • 지역 클래스
    • 익명 클래스 조건에 맞지 않다면

25) Top 레벨 클래스는 한 파일에 하나만 담으라

(컴파일 한다면 컴파일 오류가 나겠지만,) 컴파일러에 어느 소스 파일을 건네느냐에 따라 동작이 달라지므로 반드시 잡아야 할 문제다.

코드 25-1 두 클래스가 한 파일(Utensil.java)에 정의되었다 - 잘못된 예

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Main.java
public class Main {
  public static void main(String[] args) {
    System.out.println(Utensil.NAME + Dessert.NAME);
  }
}

/////////////////////////////////////////
//  Utensil.java

class Utensil {
  static final String NAME = "pan";
}

class Dessert {
  static final String NAME = "cake";
}

코드 25-2 두 클래스가 한 파일(Dessert.java)에 정의되었다 - 잘못된 예

1
2
3
4
5
6
7
8
9
10
/////////////////////////////////////////
//  Dessert.java

class Utensil {
  static final String NAME = "pot";
}

class Dessert {
  static final String NAME = "pie";
}

서로 다른 소스 파일로 분리하면 그만이지만, 굳이 한 파일에 담고 싶다면 정적 멤버 클래스를 사용하는 것을 고려한다

코드 25-3 톱레벨 클래스들을 정적 멤버 클래스로 바꿔본 모습

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
  public static void main(String[] args) {
    System.out.println(Utensil.NAME + Dessert.NAME);
  }

  private static class Utensil {
    static final String NAME = "pan";
  }
  private static class Dessert {
    static final String NAME = "cake";
  }
}

 
 

끝!   읽어주셔서 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.