클래스를 직렬화하기로 결정했다면 어떤 직렬화 형태를 사용할 지 신중하게 고려해야 한다.
자바 기본 직렬화 형태는 직렬화한 결과가 객체의 논리적 표현과 부합하는 경우에만 사용하고,
그 외에는 커스텀 직렬화 형태를 사용하자.

한번 직렬화 형태에 포함한 필드는 마음대로 제거할 수 없다. 즉 잘못된 직렬화 형태는 해당 클래스에 영구히 부정적인 영향을 남기므로, 커스텀 직렬화 형태를 설계할 때에는 많은 시간을 들여 신중히 설계하도록 하자.

기본 직렬화 형태를 사용하는 경우

• 객체의 기본 직렬화 형태는 그 객체가 포함한 데이터들과 그 객체에서 접근할 수 있는 모든 객체 등 다양한 데이터를 인코딩한다.
   따라서 기본 직렬화 형태는 객체의 물리적 표현과 논리적 내용이 같은 경우에만 사용하도록 하자.

• 기본 직렬화 형태를 사용해도 무방한 케이스

    ◦  논리적으로 성명은 성, 이름, 중간이름 3개의 문자열로 구성되며, 위 코드의 인스턴스 필드들은 이 논리적 구성요소를 정확히 반영함

    ◦  세 필드 모두 private 임에도 문서화 주석이 달려 있다 : 클래스의 직렬화 형태에 포함되는 공개 API는 모두 문서화 해야 하기 때문

          -  @serial : private 필드의 설명을 API 문서에 포함하라고 알려주는 역할 수행

          -  @serial 태그로 기술한 내용은 API 문서에서 직렬화 형태를 설명하는 특별한 페이지에 기록된다.

• 또한 불변식 보장과 보안을 위해 readObject 메서드가 어떤 기능을 제공해야 하는지 확인하자.

    ◦  위 Name 클래스의 경우 readObject 메서드가 lastName 과 firstName 필드가 null이 아님을 보장해 주어야 한다.


기본 직렬화 형태를 사용하면 안되는 경우

• 객체의 물리적 표현과 논리적 표현의 갭이 클 때 기본 직렬화 형태를 사용하면 크게 네가지 면에서 문제가 발생한다.

    ◦  공개 API가 현재의 내부 표현 방식에 영구히 종속되어 버린다.

    ◦  너무 많은 메모리 공간을 차지할 수 있다.

    ◦  시간이 너무 많이 소요될 수 있다.

    ◦  스택 오버플로우를 일으킬 수 있다.

• 예시와 함께 살펴보자. 아래 코드는 기본 직렬화 형태에 적합하지 않은 예시이다.

public final class StringList implements Serializable {
    private int size = 0;
    private Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }
    ...
}

• 이 클래스는 논리적으로 일련의 문자열을 표현하고, 물리적으로 문자열들을 이중 연결 리스트로 저장한다.

    ◦  객체의 물리적 표현과 논리적 내용의 차이가 크다

    ◦  여기서 기본 직렬화 형태를 사용하면 위에서 살펴본 네가지 문제가 발생한다.

 

          -  공개 API가 현재의 내부 표현 방식에 영구히 종속되어 버린다.

              private 클래스인 StringList.Entry 가 공개 API 가 되어 버리고, 다음 릴리즈에서 내부 표현 방식을 바꾸더라도

              StringList 클래스는 여전히 연결 리스트로 표현된 입력도 처리할 수 있어야 한다.

              즉 연결 리스트를 더이상 사용하지 않게 되더라도 과거의 코드를 제거할 수 없게 된다.

 

          -  너무 많은 메모리 공간을 차지할 수 있다.

              위 코드에 직렬화를 적용하면 연결 리스트의 모든 엔트리와 연결 정보까지 기록하게 된다.

              하지만 엔트리와 연결 정보는 내부 구현에 해당하므로 직렬화 형태에 포함시킬 필요가 없다.

              그러나 이런 불필요한 부분 때문에 직렬화 형태가 너무 커져 디스크에 저장하거나 네트워크로 전송하는 속도가 느려진다.

 

          -  시간이 너무 많이 소요될 수 있다.

              문자열들의 길이가 평균 10 이라면, 이후 살펴볼 개선 버전의 StringList 직렬화 형태는 원래 버전의 절반 정도의 공간만 차지하며

              속도 역시 두배 정도 빠르다.

 

          -  스택 오버플로우를 일으킬 수 있다.

              기본 직렬화 과정은 객체 그래프를 재귀 순회하는데 이 작업은 중간 정도 크기의 객체 그래프에서도 스택 오버플로우를

              일으킬 수 있다. StringList 에 원소를 1000~1800 개 정도 담으면 직렬화 과정에서 StackOverflowError 가 발생한다.

 

• 다만 이 경우는 유연성과 성능이 떨어졌더라도 객체를 직렬화한 후 역직렬화하면 원래 객체를 그 불변식까지 포함해 제대로 복원해낸다.

• 불변식이 세부 구현에 따라 달라지는 객체에서는 이 마저 깨질 수 있다.

    ◦  예) 해시테이블 Hash Table

          -  key - valueList 의 형태로 객체를 저장하는 자료구조

          -  특정 객체가 어느 key 의 valueList 에 들어갈지는 보통 hash function에 의해 결정된다.

          -  그러나 이 hash function의 연산 방식은 구현에 따라 달라질 수 있으며 계산할 때마다 달라지기도 한다.

          -  여기에 기본 직렬화를 사용하면 심각한 버그로 이어질 수 있다 : 직렬화 할 때와 역직렬화 할 때의 hash function 동작 방식이

             다르면 객체가 훼손됨


기본 직렬화 형태 대신 커스텀 직렬화 형태를 사용하는 경우

• 위에서 살펴본 StringList 의 경우, 리스트에 포함된 문자열의 개수와 그 뒤로 문자열들을 나열하는 수준의 직렬화면 충분하다.

• 물리적인 상세 표현은 배체한 채 논리적인 구성만 담는 것이다.

• 아래 코드는 위의 방식대로 커스텀 직렬화 형태를 적용한 것이다.

public final class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;

    // 이번에는 직렬화 하지 않는다.
    private static class Entry {
        String data;
        Entry next;
        Entry previous;
    }

    // 문자열을 리스트에 추가한다.
    public final void add(String s) { ... }

    // StringList 인스턴스를 직렬화한다.
    private void writeObject(ObjectOutputStream stream) throws IOException {
        stream.defaultWriteObject();
        stream.writeInt(size);

        // 모든 원소를 순서대로 기록한다.
        for (Entry e = head; e != null; e = e.next) { s.writeObject(e.data); }
    }

    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
        stream.defaultReadObject();
        int numElements = stream.readInt();
        for (int i = 0; i < numElements; i++) { add((String) stream.readObject()); }
    }

}

• writeObjectreadObject 가 직렬화 형태를 처리한다.

• transient 한정자는 해당 인스턴스 필드가 기본 직렬화 형태에 포함되지 않는다는 표시이다.

    ◦  이 때 StringList 의 모든 필드가 transient 더라도 writeObjectreadObject 는 각각 가장 먼저 defaultWriteObject 와

         defaultReadObject 를 호출한다.

    ◦  클래스의 인스턴스 필드가 모두 transient 이면 defaultWriteObject 와 defaultReadObject 를 호출할 필요가 없어 보이나,

         이 작업이 없으면 문제가 생긴다.

          -  향후 릴리즈에서 transient 가 아닌 인스턴스 필드가 추가되었을 때 상위와 하위 버전이 호환되지 않는다.

             신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화하면 새로 추가된 필드들은 무시된다.

          -  구버전 readObject 메서드에서 defaultReadObject 를 호출하지 않으면 역직렬화할 때 StreamCorruptedException 이 발생함

• writeObject 는 private 메서드 임에도 문서화 주석이 달려 있다

    ◦  클래스의 직렬화 형태에 포함되는 공개 API는 모두 문서화 해야 하기 때문

    ◦  필드용의 @serial 태그처럼 메서드에 달린 @serialData 태그는 자바독 유틸리티에 이 내용을 직렬화 형태 페이지에 추가하도록

         요청하는 역할을 한다.


transient 한정자의 사용

• 객체의 논리적 상태와 무관한 필드라고 확신될 때에만 transient 한정자를 생략하자.

    ◦  defaultWriteObject 메서드를 호출하면 transient 로 선언하지 않은 모든 인스턴스 필드가 직렬화된다.

    ◦  transient 로 선언해도 되는 인스턴스 필드에는 모두 transient 한정자를 붙여야 한다.

          -  캐시된 해시 값 처럼 다른 필드에서 유도되는 필드

          -  JVM을 실행할 때 마다 값이 달라지는 필드 (네이티브 자료구조를 가리키는 long 필드 등)

• 커스텀 직렬화 형태를 사용한다면 StringList 예에서처럼 대부분의 인스턴스 필드를 transient 로 선언하자

• 기본 직렬화를 사용하면 transient 필드들은 역직렬화될 때 기본값으로 초기화된다.

유형 초기화 값
객체 참조 필드 null
숫자 기본 타입 필드 0
boolean 필드 false

• 기본값을 그대로 사용해서는 안 된다면

    ◦  readObject 메서드에서 defaultReadObject 를 호출한 다음, 해당 필드를 원하는 값으로 복원하자.

    ◦  그 값을 처음 사용할 때 초기화 하는 방법도 있다.


동기화 매커니즘과 직렬화

• 객체의 전체 상태를 읽는 메서드에 적용해야 하는 동기화 메커니즘은 직렬화에도 적용해야 한다.

• 모든 메서드를 synchronized 로 선언하여 thread safe 만든 객체에서 직렬화를 사용하려면 writeObject 메서드도 다음 코드처럼

    synchronized 로 선언해야 한다.

private synchronized void writeObject(ObjectOutputStream stream)
        throws IOException {
    stream.defaultWriteObject();
}

• writeObject 메서드 안에서 동기화 하고 싶다면 클래스의 다른 부분에서 사용하는 락 순서를 똑같이 따라야 한다.
    그렇지 않으면 자원 순서 교착상태(resource-ordering deadlock)에 빠질 수 있다.


직렬화와 직렬 버전 UID

• 직렬화 가능 클래스 모두에 직렬 버전 UID를 명시적으로 부여하자.

    ◦  이렇게 하면 직렬 버전 UID가 일으키는 잠재적인 호환성 문제가 사라진다.

    ◦  성능도 조금 빨라진다. 직렬 버전 UID를 명시하지 않으면 런타임에 이 값을 생성하느라 복잡한 연산을 수행하기 때문이다.

• 직렬버전 UID 선언은 각 클래스에 아래와 같은 한 줄만 추가해주면 끝이다.

// 무작위로 고른 long 값
private static final long serialVersionUID = 0204L;

• 어떤 long 값을 선택하든 상관 없다.

    ◦  클래스 일련 번호를 생성해주는 serialver 유틸리티를 사용해도 된다.

    ◦  그냥 생각나는 아무 값이나 넣어줘도 된다.

    ◦  반드시 고유한 값일 필요도 없다.

• 단, 직렬 버전 UID가 없는 기존 클래스를 구버전으로 직렬화된 인스턴스와 호환성을 유지한 채 수정하고 싶다면

    구버전에서 사용한 UID 값을 그대로 사용해야 한다.

    ◦  이 값은 직렬화된 인스턴스가 존재하는 구버전 클래스를 serialver 유틸리티에 입력으로 주어 실행하면 얻을 수 있다.

• 기본 버전 클래스와의 호환성을 끊고 싶다면 단순히 직렬 버전 UID의 값을 바꿔주면 된다.

    ◦  이렇게 하면 기존 버전의 직렬화된 인스턴스를 역직렬화할 때 InvalidClassException이 던져진다.

    ◦  단, 구버전으로 직렬화된 인스턴스들과의 호환성을 끊으려는 경우를 제외하고는 직렬 버전 UID를 절대 수정하지 말자.


참고자료

 

[이펙티브 자바 3판] 아이템 87. 커스텀 직렬화 형태를 고려해보라

[Effective Java 3th Edition] Item 85. Consider using a custom serialized form

madplay.github.io

 

스레드 스케줄러Thread.yield, 스레드 우선순위에 의존하지 말자. 견고성과 이식성을 모두 해치는 행위이다.
스레드 우선순위는 프로그램의 서비스 품질을 높이는데 드물게 쓰일 수는 있지만 프로그램을 고치는 용도로 사용해서는 안된다.

• 정확성이나 성능이 운영체제 고유의 스레드 스케줄러에 따라 달라지는 프로그램일수록 다른 플랫폼에 이식하기 어렵다.

• 견고하고 이식성 좋은 프로그램을 작성하기 위해서 고려해야 할 것들

    ◦  실행 가능한 스레드의 평균 개수를 프로세서 수 보다 지나치게 많아지지 않도록 관리할 것

    ◦  즉 실행가능한 스레드 수를 가능한 적게 유지하는 것이 중요하다.


실행 가능한 스레드 수를 적게 유지하는 방법

• 실행 준비가 된 스레드들은 맡은 작업을 완료할 때 까지 계속 실행되도록 할 것

    ◦  단 실행 가능한 스레드 수 와 전체 스레드 수는 구분해야 한다. (대기 중인 스레드 = 실행 불가)

• 스레드는 당장 처리해야 할 작업이 없다면 실행 되어서는 안된다.

• 스레드는 Busy Waiting 상태가 되면 안된다.

    ◦  Busy Waiting 상태

-  스레드 스케줄러 환경의 변화에 취약하다. (이식성이 낮다)

-  프로세서에 부담을 주어 다른 작업이 실행의 기회를 박탈당한다. (성능이 낮다)

-  Busy Waiting이 발생하는 코드 예시 (작가평 : 끔찍한 코드이다!)

public class SlowCountDownLatch {
  private int count;

  public SlowCountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException(count + " < 0");
    this.count = count;
  }

  public void await() {
    while (true) {
      synchronized(this) { if (count == 0) return; }
    }
  }

  public synchronized void countDown() {
    if (count != 0) count--;
  }

}

Thread.yield 사용을 주의하라

• Thread.yield : 다른 스레드에게 실행을 양보하는 메서드

    ◦  실행 중인 스레드를 RUNNABLE (실행 대기) 상태로 바꾼다.

          -  스레드가 실행되었는데, 그 실행이 잠시동안 무의미한 경우가 있을 수 있다.

          -  이런 경우 스레드를 잠시 실행 대기 상태로 돌려놓으면 CPU의 자원의 소모를 방지할 수 있다.

          -  이 때 Thread.yield 메소드가 유용하게 사용될 수 있다.

• 특정 스레드가 다른 스레드에 비해 CPU 자원을 할당받지 못하는 경우에도 Thread.yield 사용은 지양하자

    ◦  증상이 호전될 수 있어도 여전히 이식성이 나쁘다

          -  JVM 종류에 따라 차이가 미미할수도, 악화될수도 있다.

• 차라리 어플리케이션 구조를 바꿔 동시에 실행 가능한 스레드 수를 줄이는 것이 좋다.


스레드 우선순위 조정에 주의하라

• 스레드 우선순위는 자바에서 이식성이 가장 나쁜 특성 중 하나이다.

    ◦  A 타입의 JVM 에서 1-2-3 의 순서로 실행되었다고 하더라도, B 타입의 JVM에서 동일한 순서로 실행되리라는 보장이 없다.

          -  A 타입에서 1-2-3 의 순서로 실행된 것이 B 타입의 JVM 에서는 3-1-2 의 순서로 실행될 수 있음

          -  즉 1-2-3 이 최적의 성능을 보장하는 우선순위 였다고 해도, 다른 JVM 에서 3-1-2 의 순서로 실행되어 버리면 다시 성능이 나빠진다.

• 특히 응답 불가 문제는 스레드 우선순위로 해결해선 안된다.

    ◦  진짜 원인을 찾아 수정하기 전까지 같은 문제가 반복해서 발생할 것이다.

메서드가 던지는 예외는 그 메서드를 올바로 사용하는데 아주 중요한 정보이므로, 꼭 문서화 해주자

1. 검사 예외를 문서화하라

• 검사 예외가 발생하는 상황을 자바독의 @throws 태그를 사용해 정확히 문서화하자.

• 공통 상위 클래스 하나로 뭉뚱그려 선언하지 말 것

    ◦  예) 메서드가 단순히 Exception 이나 Throwable을 던진다고 선언하면 안 된다.

          -  메서드 사용자가 세부적인 예외에 대처할 수 있는 힌트를 주지 못한다.

          -  같은 맥락에서 발생할 여지가 있는 다른 예외들까지 삼켜버릴 수 있다.

• 이렇게 해도 되는 유일한 메서드 : main 메서드

    ◦  main 메서드 를 호출하는 주체는 JVM 뿐이므로 Exception 을 던진다고 선언해도 괜찮다.


2. 비검사 예외를 문서화하라

• 비검사 예외는 일반적으로 프로그래밍 오류를 뜻한다.

    ◦  즉 일으킬 수 있는 오류가 무엇인지 알면 프로그래머스는 해당 부분을 주의하여 코드를 작성할 수 있다.

    ◦  잘 정비된 비검사 예외 문서는 그 메서드를 성공적으로 수행하기 위한 전제 조건이 된다.

• public 메서드라면 필요한 전제 조건을 문서화해야 하는데(아이템 56), 그 수단으로 가장 좋은 것이 비검사 예외들을 문서화하는 것이다!

• 비검사 예외를 문서로 남기는 일은 인터페이스 메서드에서 특히 중요하다.

    ◦  이 조건이 인터페이스의 일반 규약에 속하게 되어 그 인터페이스를 구현한 모든 구현체가 일관되게 동작하도록 해주기 때문


3. 검사 예외와 비검사 예외를 구분하라

• 예외를 @throws 로 문서화하되, 비검사 예외는 메서드 선언의 throws 목록에 넣지 말자.

• 검사 예외인지, 비검사 예외인지에 따라 해야할 일이 달라지므로 둘을 확실히 구분해주는 것이 좋다.

• 자바독 유틸리티는 아래 두 상황을 시각적으로 구분해주니까 잘 활용하자.

    ◦  메서드 선언의 throws 절에 등장하고, 메서드 주석의 @throws 태그에도 명시한 예외

    ◦  @throws 태그에만 명시한 예외


4. 비검사 예외의 문서화가 어려운 상황

• 비검사 예외도 문서화하라고 했지만, 현실적으로 불가능할 때도 있다.

    ◦  예시 상황

          -  다른 사람이 작성한 클래스를 사용하는 메서드가 있다고 하자.

          -  발생 가능한 모든 예외를 문서화 했다!

          -  후에 이 외부 클래스가 새로운 비검사 예외를 던지게 되었다!

          -  아무 수정도 되지 않은 메서드는 문서에 언급되지 않은 새로운 비검사 예외를 전파하게 된다!


5. 공통 예외는 메서드 단이 아닌, 클래스 단에 적자

• 한 클래스에 정의된 많은 메서드가 같은 이유로 같은 예외를 던지는 경우, 그 예외를 각 메서드가 아닌 클래스 설명에 적어주는 방법도 있다.

    ◦  NullPointerException 이 가장 흔한 사례

    ◦  클래스의 문서화 주석에 ‘이 클래스의 모든 메서드는 인수로 null 이 넘어오면 NullPointerException 을 던진다’ 라는 식으로

         설명을 적어주면 된다.


6. 정리

• 메서드가 던질 수 있는 모든 예외를 문서화하라. (검사 예외 / 비검사 예외 / 추상 메서드 / 구체 메서드)

• 자바독 @throws 태그를 사용하라.

• 검사 예외는 메서드 선언의 throws 목록에 달고, 비검사 예외는 메서드 선언 쪽에는 달지 말자.

• 발생 가능한 예외를 문서로 남기지 않으면 클래스,인터페이스를 효과적으로 사용하기 어려울 수 있다.

예외는 오직 예외 상황에만 사용할 것

예외를 제어 흐름용으로 사용하지 말자

• 예외는 오직 예외 상황에서만 사용해야 한다.

• 일반적인 제어 흐름용으로 사용해서는 안됨

    ◦  코드를 헷갈리게 한다(코드가 직관적이지 못하다).

    ◦  성능 저하의 원인이 될 수 있다.

    ◦  엉뚱한 곳에서 발생한 버그를 숨겨버릴 수 있다.

 

예외를 제어 흐름용으로 사용한 예시

try {
	int i = 0;
	while(true) { mountainList[i++].climb(); }
} catch(ArrayIndexOutOfBoundsException e) {...}
for(Mountain m : mountainList) { m.climb(); }

• 두 코드 모두 mountainList 배열의 원소를 순회하는 목적으로 쓰여졌다.

• 예외를 제어흐름용으로 사용하니 코드가 전혀 직관적이지 못하다.

• 이 작자는 코드를 왜 이렇게 짰을까?

    ◦  JVM 은 배열에 접근할 때 마다 경계를 넘지 않는지 검사한다.

    ◦  for 문은 배열의 경계에 도달하면 반복문을 종료한다.

    ◦  for 문 안에서 배열에 접근하고 있으니, 배열의 경계에 대한 검사를 2번 하고 있다고 생각할 수 있다.

    ◦  중복 검사를 피하면 성능을 향상시킬 수 있지 않을까? → 예외를 사용에 제어 흐름을 구현하면 되겠다!

 

    ◦  이 추론이 잘못된 이유

          -  예외는 예외 상황에 쓸 용도로 설계된 것이다. JVM 개발자가 최적화에 신경쓰지 않았을 가능성이 크다.

          -  코드를 try-catch 블록 안에 넣으면 JVM 이 적용할 수 있는 최적화가 제한된다.

          -  배열을 순회하는 for 문은 앞서 걱정한 중복 검사를 수행하지 않는다. JVM 이 알아서 최적화해주기 때문.

          -  결과적으로 성능이 개선되지 않는다! (실제로 코드를 돌려보면 예외를 사용한 쪽이 2배 이상 느리다)

 

    ◦  뿐만 아니라 반복문 안에 버그가 숨어 있다면 흐름 제어에 쓰인 예외가 이 버그를 숨겨 디버깅을 훨씬 어렵게 할 수 있다.

          -  반복문의 몸체 내부에서 다른 배열을 사용하는 경우, 해당 배열에서 ArrayIndexOutOfBound 예외가 발생할 수 있다.

          -  표준 관용구

              -  예외처리 하지 않고 스택 추적 정보를 남긴 다음 해당 스레드를 종료시킨다.

          -  예외를 사용한 반복문

              -  ArrayIndexOutOfBound 예외 발생 = 배열 순회 종료 라고 간주하고 있음

              -  즉 실제 버그 상황에서 발생한 예외를 반복문 종료 상황으로 오해하고 넘어간다.

 

코드를 짤 때는 꼼수를 쓰지 말자

• 표준적이고 쉽게 이해되는 관용구를 사용하라

• 성능 개선을 목적으로 과하게 머리를 쓴 기법은 자제하라.

    ◦  꼼수로 성능이 개선되더라도 자바 플랫폼은 꾸준히 업데이트 되기 때문에 상대적인 성능 우위는 오래가지 않는다.

    ◦  반면 꼼수에 숨겨진 미묘한 버그와 어려워진 유지보수 문제는 계속된다.


상태 검사 메소드 / 옵셔널 / 특정 값

상태 의존적 메소드와 상태 검사 메소드

• 지금까지 살펴본 원칙은 API 설계 시에도 유의해야하는 부분이다.

• 특히 특정 상태에서만 호출할 수 있는 상태 의존적 메소드를 제공하는 클래스의 경우, 상태 검사 메소드를 함께 제공해 사용자가

    예외 처리를 제어 흐름에 사용하지 않도록 해줘야 한다.

    ◦  상태 의존적 메소드 예시 : Iterator 의 next

          -  Iterator 에 다음 원소가 있어야 next 메소드 호출이 가능하므로

    ◦  상태 검사 메소드 예시 : Iterator 의 hasNext

          -  Iterator 에 다음 원소가 있는지 여부를 hasNext 메소드를 통해 검사할 수 있다.

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) { Foo foo = i.next(); }

• 상태 검사 메소드 없이 상태 의존 메소드만 있었다면, 사용자는 아래와 같이 코드를 작성할 수 밖에 없을 것이다.

try {
	Iterator<Foo> i = collection.iterator();
	while(true) { Foo foo = i.next(); }
} catch (NoSuchElementException e) {...}

 

 

상태 검사 메소드 / 옵셔널 / 특정 값

• 상태 검사 메소드 대신 올바르지 않은 상태일 때 빈 옵셔널을 반환하거나, null 같이 사전에 정의해둔 특정 값을 반환하는 방법도 있다.

• 아래는 이 세가지 방식 중 하나를 선택하는 것에 대해 책에서 소개하는 지침이다.

    ◦  옵셔널 혹은 특정 값을 반환하는 방식

          -  외부 동기화 없이 여러 스레드가 동시에 접근할 수 있는 경우

          -  외부 요인으로 상태가 변할 수 있는 경우

              → 상태 검사 메소드와 상태 의존적 메소드를 호출하는 사이에 객체의 상태가 변할 수 있기 때문

          -  성능이 중요한 상황, 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행하는 경우

    ◦  상태 검사 메서드를 사용하는 방식

          -  위의 상황을 제외한 다른 모든 경우

              → 가독성이 더 좋다 + 상태 검사 메서드 호출이 누락된 경우 상태 의존적 메소드가 이를 잡아주기 때문에 버그를 잡기 쉽다.

          -  단, 특정 값은 검사하지 않고 지나쳐도 발견하기 어렵다는 단점이 있다.

객체는 클래스가 아닌 인터페이스로 참조하라

클래스가 아닌 인터페이스로 참조하는 예시

• 적합한 인터페이스가 있다면 객체를 클래스 타입이 아닌 인터페이스 타입으로 선언하는 것이 좋다.

    ◦ 이렇게 되면 구체적인 클래스 타입은 객체를 생성할 때 생성자에서만 사용하면 된다

    ◦ 아래는 Set 인터페이스를 구현한 LinkedHashSet 변수를 선언하는 코드이다.

LinkedHashSet implements Set

코드의 유연성을 높이는 설계

• 기존에 사용하던 구현 클래스 타입을 다른 것으로 바꿔야 하는 상황이 종종 있다.

    ◦ 더 성능이 좋은 클래스 타입을 사용하고 싶은 경우

    ◦ 더 다양한 신기능을 사용할 수 있는 클래스 타입이 있는 경우

         -  HashMap 보다 EnumMap 이 속도도 더 빠르고 순회 순서도 키의 순서와 같아 순서 예측이 가능하다.

         -  EnumMap 은 키가 열거 타입인 경우에만 사용할 수 있다. 이 때 LinkedHashMap 을 사용하면 키 타입과 상관없이

            사용할 수 있으면서 순회 순서를 예측할 수 있다.

• 인터페이스 타입으로 객체를 참조했다면 이런 변동사항에 훨씬 유연한 대처가 가능하다.

    ◦ 구현 클래스를 교체하고 싶다면 다른 클래스의 생성자를 호출해주기만 하면 되기 때문

 

• Example

    ◦ LinkedHashSet 대신 HashSet 를 사용하려고 코드를 수정하는 경우

    ◦ Set 인터페이스를 사용한 경우 우변의 생성자 외 부분은 수정이 필요 없다

    ◦ LinkedHashSet 클래스를 사용한 경우 좌, 우변 모두 수정이 필요하다.

 

    ◦ 그러나 누군가는 이렇게 생각할 수 있다

         -  굳이 인터페이스 써야 하나? 그냥 클래스 쓰고 나중에 수정할 때 좌, 우변 다 바꿔주면 되는거 아님?

         -  그러나 이 방법은 자칫하면 컴파일 에러가 발생할 수 있다.

         -  처음에 객체를 인터페이스로 만들었다면, 메소드를 설계할 때 파라미터로 인터페이스 객체를 받도록 유도할 수 있다.

         -  처음에 객체를 클래스 만들었다면, 메소드를 설계할 때 인터페이스가 아닌 클래스 객체를 파라미터로 받게 해버리는 상황이

            발생할 수 있다.

         -  인터페이스를 사용하면 이후 구체 클래스가 바뀌어도 컴파일 문제가 없으나 클래스를 사용하면 구체 클래스가

            바뀔 때 컴파일 문제가 생긴다. (즉 하나가 바뀔 때 수정해야 하는 부분이 더 많아진다 : 유연성 낮음)

         -  즉 애초에 변수를 인터페이스 타입으로 선언해 이 문제를 방지하자.

 

• 주의사항

    ◦ 기존에 사용하던 구현 클래스 타입을 다른 것으로 바꿀 때 유의사항

    ◦ 원래의 클래스가 인터페이스 레벌에서는 없는 특별한 기능을 제공했고, 주변 코드가 이 기능에 의존하여 동작하고 있었다면

      새로운 클래스도 동일한 기능을 제공해야 코드가 깨지지 않는다.

         -  LinkedHashSet 는 반복자의 순회 순서를 보장한다. 이를 염두에 두고 코드를 작성한 상황에서 LinkedHashSet

            HashSet 으로 대체해버리면 연산 오류가 발생할 수 있다.


참조할 만한 인터페이스가 없는 경우

값 클래스 (Value Class)

• 한번 값이 할당 된 이후에 변경되지 않음을 보장해야 하는 경우

    ◦ 예) String, BigInteger 등의 불변 객체

• 값 클래스가 여러가지로 구현될 수 있음을 염두에 두고 설계하는 일이 거의 없다.

• final 인 경우가 많고 상응하는 인터페이스가 별도로 존재하는 경우가 드물다.

• 값 클래스는 매개변수 / 변수 / 필드 / 반환 타입으로 사용해도 무방하다.

 

클래스 기반으로 작성된 프레임워크가 제공하는 객체

• OutputStreamjava.io 패키지의 여러 클래스가 이 부류에 속한다.

 

인터페이스에는 없는 특별한 메소드를 제공하는 클래스

• PriorityQueue 클래스는 Queue 인터페이스에는 없는 comparator 메서드를 제공한다.

• 클래스 타입을 직접 사용하는 경우, 이런 특별 메서드의 사용은 꼭 필요한게 아니라면 최소화하는 것이 좋다.

    ◦ 코드의 유연성을 많이 떨어트리기 때문


정리

객체를 표현할 적절한 인터페이스가 있는지 먼저 찾아보자. 인터페이스로 객체를 참조하면 더 유연한 코드를 작성할 수 있기 때문이다.

만약 없다면, 필요한 기능을 제공해주는 범위 내에서 가장 상위 클래스의 타입을 사용하자.