Rule 70 - 스레드 안정성에 대해 문서로 남겨라

스레드 안정성 수준

  1. 변경불가능(immutable) : 불변식 String, Integer
  2. 무조건적 스레드 안정성(uncoditionally thread-safe) : ConcurrentHashMap
  3. 조건부 스레드 안정성(conditionally thread-safe) : iterator
  4. 스레드 안정성 없음 : ArrayList, HashMap
  5. 다중 스레드에 적대적(thread-hostile)

스레드 안정성 어노테이션

Immutable, ThreadSafe, NotThreadSafe

example 콜렉션 뷰 순회 숙어

Map<K, V> m = Collections.synchronizedMap(new HashMap<K, V>());
...
Set<K> s = m.ketSet();  // 동기화 블록 안에 있을 필요가 없음
...
synchronized(m) {   // s가 아니라 m에 동기화
    for (K key : s)
        key.f();
}

example lock 객체 숙어

// DoS 공격을 피하기 위한 private lock 객체 숙어
private final Object lock = new Object();

public void foo() {
    synchronized(lock) {
        ...
    }
}

결론

어떤 방식(말로 풀건, 스레드 안정성 어노테이션등)을 이용해서 모든 클래스는 자신의 스레드 안정성을 분명히 밝혀야한다. - synchronized 키워드는 문서적으로 전혀 의미없다.

무조건적 스레드 안정성을 가진다면 private lock 객체 숙어 를 사용을 고려하라.

Rule 71 - 초기화 지연은 신중하게 하라

초기화 지연(lazy initialization) : 필드 초기호를 실제로 값이 쓰일 때까지 미루는 것

대부분의 경우 초기화 지연을 하는 것 보단 일반 초기화를 하는 것이 낫다.

초기화 숙어들

일반적인 초기화

private final FieldType field = computeFieldValue();

일반적인 방법

지연 초기화 - synchronized

// 동기화된 접근자를 사용한 객체 필드 초기화 지연 방법
private FieldType field;

synchronized FieldType getField() {
    if (feild == null)
        filed = computeFieldValue();
    return field;
}

안전하긴 하지만 항상 동기화가 걸리기 때문에 성능이 나쁨

지연초기화 - 초기화 지연 담당클래스 숙어

// 정적 필드에 대한 초기화 지연 담당 클래스 숙어
private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}
static FieldType getField() { return FieldHolder.field; }

안전하면서 성능도 챙기는 좋은 숙어

단점으로는 한번 재초기화를 하기 힘들다.

지연초기화 - Double check

// 이중 검사 패턴을 통해 객체 필드 초기화를 지연시키는 숙어
private volatile FieldType field;

FieldType getField() {
    FieldType result = field;
    if (result == null) {   // 첫 번째 검사 (락 없음)
        synchronized(this) {
            result = field;
            if (result == null) // 두 번째 검사 (락 있음)
                field = result = computeFieldValue();
        }
    }
    return result;
}

부득이하게 담당클래스를 이용할 수 없거나 재초기화가 필요한 경우 사용

한 번은 동기화 전, 한 번은 동기화 후에 검사해(그래서 double)서 단 한 번만 락이 걸리게 한다.

결론

대부분의 초기화 지연을 시키지 말아야한다.

  • 객체 필드는 Double check 숙어
  • 정적 필드는 초기화 지연 담당클래스 숙어

Rule 72 - 스레드 스케줄러에 의존하지 마라

정확성을 보장하거나 성능을 높이기 위해 스레드 스케줄러 에 의존하는 프로그램은 이식성이 떨어진다. 즉 플랫폼(OS) 환경에 따라서 스레드 우선순위가 달라질 수 있기 때문에 실행 결과가 달라질 수 있다.

멀티 스레드 관리 방안

  • 스레드 풀을 적절하게 : 실행 가능 스레드의 평균적인 수가 프로세서 수보다 너무 많아지지 않도록 하는 것
  • 태스크를 적당히 작게 : 너무 작으면 context switching 이슈가 생김
  • 서로 독립적으로
  • 바쁘게 대기하지 않는다 : 공유객체에 너무 자주 접근하지 않는다.

스레드 우선순위에 대한 이슈를 해결하기 위해서 Thread.yield()스레드 우선순위를 바꾸는 기법 을 사용하지 말자. 별 효능이 없다.

프로그램의 구조를 바꿔서 실행 가능한 스레드의 수를 줄이는 것이 좋은 해결책이다.

만약 병행성 테스트가 목적이라면 차라리 Thread.sleep(1)를 사용하라. - Thread.sleep(0)은 바로 return 하므로 효과가 없다

결론

플랫폼이 제공하는 스레드 스케줄러에 의존하지 말고 적절한 멀티스레드 관리 방안을 마련하라.

마찬가지로 우선순위를 조작하는 메서드에도 의존하지 마라 - 그저 힌트 수준이지 강제성이 없다.

Rule 73 - 스레드 그룹은 피하라

스레드 그룹은 거의 폐기수순이다. - 스레드 안정성에도 취약하다

쓸모 있는 기능이 무점검 예외를 던졌을 때 가져오는 유일한 수단인 ThreadGroup.uncaughtException 이 있었는데 이 마저도 Thread.setUncaughtExceptionHandler가 제공되었으니 이것을 사용 하라.

결론

가능한 절대로 사용하지 말고 대신 ThreadPoolExecutor를 사용 하라.

Rule 66 - 변경 가능 공유 데이터에 대한 접근은 동기화하라

상호 배제성뿐 아니라 스레드 간의 안정적 통신을 위해서도 동기화는 반드시 필요하다.

Thread.stop는 폐기된 API이므로 절대로 사용하지 마라.

읽기 연산과 쓰기 연산에 전부 적용하지 않으면 동기화는 아무런 효과도 없다.

동기화를 구현하는 방법

1. synchronized

// 적절히 동기화한 스레드 종료 예제
public class StopThread {
    private static boolean stopRequested;
    private static synchronized void requestStop() {
        stopRequested = true;
    }
    private static synchronized boolean stopRequested() {
        return stopRequested;
    }

    public static void main(String[] args)
            throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while(!stopRequested())
                    i++;
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

2. volatile

// 적절히 동기화한 스레드 종료 예제
public class StopThread {
    private static volatile boolean stopRequested;
    public static void main(String[] args)
            throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while(!stopRequested)
                    i++;
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

volatile은 상호배제를 구현하지 않지만 가장 최근에 기록된 값을 조회하도록 보장한다.

example 잘못된 volatile

// 잘못된 예제 - 동기화가 필요하다!
private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
    return nextSerialNumber++;
}

문제는 ++연산자가 원자적이지 않다는데 있다. 이 연산자는 두가지 연산을 순서대로 시행한다.

  1. 값을 읽는다
  2. 새로운 값(+1을 더한 값)을 필드에 쓴다.

첫번째 해결방법은 synchronized

두번째 해결방법은 아래 3번째 동기화 구현방법을 참고!!

3. Atomic 클래스

example

private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
    return nextSerialNum.getAndIncrement();
}

동기화의 솔루션의 역발상

변경 가능 데이터를 공유하지 않는 것이다 - 동시성 이슈가 발생하지 않음

  1. 변경 불가능 데이터(불변객체)를 공유
  2. 변경 가능한 데이터는 한 스레드만 이용하도록 한다 (공유하지 않음)

실질적으로 변경 불가능한 객체(effectively immutable)

특정 스레드만이 데이터 객체를 변경할 수 있도록 하고, 변경이 끝난 뒤에야 다른 스레드와 공유하도록 할 때는 객체 참조를 공유하는 부분에만 동기화를 적용하는 객체

결론

변경 가능한 데이터를 공유할 때는 해당 데이터를 읽거나 쓰는 모든 스레드는 동기화를 수행해야 한다

Rule 67 - 과도한 동기화는 피하라

동기화 시 발생할 수 있는 문제

  1. 교착상태(deadlock)
  2. 비결정적 동작(nondeterministic behavior)

동기화 메서드나 블록 안에서 클라이언트에게 프로그램 제어 흐름을 넘기지 마라

동기화가 적용된 영역(synchronized) 안에서는 재정의 가능 메서드나 클라이언트가 제공한 함수 객체 메서드를 호출하지 말라는 것 - 불가해(alien) 메서드이기 때문 - 제어권이 다른 객체로 위임되는데 위임되는 메서드 안에서는 동기화 제어를 할 수 없기 때문이다.

불가해 메서드 오류와 해결

example 불가해 메서드 동기화 오류

private void notifyElementAdded(E element) {
    synchronized(observers) {
        for (SetObserver<E> observer : observers) {
            observer.added(this, element);
        }
    }
}

example 솔루션 1 : 방어복사 + open call

private void notifyElementAdded(E element) {
    List<SetObserver<E>> snapshot = null;
    synchronized(observers) {
        snapshot = new ArrayList<SetObserver<E>>(observers);
    }
    for (SetObserver<E> observer : snapshot) {
        observer.added(this, element);
    }
}

example 솔루션 2 : Concurrent Collection

// 다중 스레드에 안전한 구독자 집합 : CopyOnWriteArrayList 이용
private final List<SetObserver<E>> observers =
        new CopyOnWriteArrayList<SetObserver<E>>();

public void addObserver(SetObserver<E> observer) {
    observers.add(observer);
}
public boolean removeObserver(SetObserver<E> observer) {
    return observers.remove(observer);
}
private void notifyElementAdded(E element) {
    for (SetObserver<E> observer : observers) {
        observer.added(this, element);
    }
}

핵심

동기화 영역 안에서 수행되는 작업의 양을 가능한 줄여야 한다.

동기화의 진짜 비용은 병렬성을 활용할 기회를 잃는다는 것 이다.

static 필드(또는 단일객체 내 필드)를 변경하는 메서드가 있을 때는 해당 필드에 대한 접근을 반드시 동기화 해야 한다.

결론

  • 동기화 영역 안에서 불가해(alien) 메서드를 호출하지 않는다.
  • 동기화 영역 안에서 작업을 최소화 한다.
  • 가변 클래스 내 동기화가 필요한지 검토하고 필요하다면 동기화 한다.

Rule 68 - 스레드보다는 실행자와 태스크를 이용하라

실행자 프레임워크(Executor Framework)

Task 실행 프레임워크

example

// 실행자 생성
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(runnable); // 실행
executor.shutdown();    // 셧다운

추가기능

  1. 특정 태스크가 종료되길 대기
  2. 임의의 태스크들이 종료되기를 대기 : invokeAny, invokeAll
  3. 실행자 서비스가 자연스럽게 종료될 수 있도록 대기 : awaitTermination
  4. 태스크가 끝날 때 마다 결과 조회 : ExecutorCompletionService

Thread pool

멀티쓰레드를 사용하기 위한 풀

java.util.concurrent.Executors.* ThreadPoolExecutor

  • 일반적인 상황 : Executors.newCachedThreadPool
  • 부하가 큰 상황 : Executors.newFixedThreadPool

결론

중요한 것은 Thread가 아니다. 작업과 실행 메커니즘이 분리된 것이다.

작업

중요한 것은 작업이다.

종류

  • Runnable
  • Callable : Runnable과 다르게 반환값이 존재한다.

실행 메커니즘

실행 메커니즘은 실행자 서비스 이다. 그리고 우리는 이것을 자바API가 제공해주는 실행자 프레임워크를 통해서 사용하면된다.

추가 tip

java.util.Timer 대신 ScheduleThreadPoolExecutor을 추천

  • 추상화 수준이 높아서 사용하기 편리하고 유연성이 높음
  • 타이밍이 정확함
  • 멀티스레드 이용 가능
  • 무점검 예외 복구 : 예외를 핸들링 할 필요가 없음

Rule 69 - wait나 notify 대신 병행성 유틸리티를 이용하라

과거처럼 waitnotify를 직접 구현하지 말고, 자바 플랫폼(1.5이상)이 제공하는 고수준 병행성 유틸리티(high-level concurrency utility)를 이용하라.

병행성 유틸리티 분류

  • 실행자 프레임워크 : Rule 67
  • 병행 컬렉션(concurrent collection) : 이번 규칙
  • 동기자(synchronizer) : 이번 규칙

병행 컬랙션

표준 Collection 인터페이스(ex:List, Queue, Map)에 고성능 병행 컬렉션 구현을 제공하며, 병행성을 높이기 위해 동기화를 내부적으로 처리

그래서 컬렉션 외부에서 병행성을 처리하는 것이 불가능. 락을 걸어봐야 락 중복으로 인해 성능만 나빠짐

위 문제를 해결하기 위해서 상태 종속 변경 연산을 제공. : 몇가지 기본 연산들을 하나의 원자적 연산으로 묶은 것

예를들면 ConcurrentMap.putIfAbsent(K, V) : 키에 해당하는 키에 해당하는 값이 없을 때문 주어진 값을 집어 넣고, 해당 키에 대응하여 저장되어 있었던 기존 값을 반환. 값이 없으면 null 반환

example

// ConcurrentMap으로 구현한 병행 정규화 맵 : 최적이 아님
private static final ConcurrentMap<String, Stirng> map =
        new ConcurrentHashMap<String, String>();

public static String intern(String s) {
    String previousValue = map.putIfAbsent(s, s);
    return previousValue = null ? s : previousValue;
}

example 개선

// ConcurrentMap으로 구현한 병행 정규화 맵 - 더 빠르다!
public static String intern(String s) {
    String result = map.get(s);
    if (result == null) {
        result = map.putInAbsent(s, s);
        if (result == null)
            result = s;
    }
    return result;
}

추천 클래스

ConcurrentHashMap

CopyOnWriteArrayList

BlockingQueue : 봉쇄 연상(blocking operaton : task) 가능

task는 큐의 맨 앞(head) 원소를 제거한 다음 반환하는데, 큐가 비어있는 경우에는 대기(wait)

생산자-소비자큐 라고 불리기도 함

생산자 스레드가 큐에 작업들일 집어 넣고, 소비자 스레드가 꺼내어 처리

ThreadPoolExecutor을 비롯한 대부분의 ExecutorService 구현은 BlockingQueue 를 사용

동기자

쓰레드들의 병행성을 제어해서 서로 상호협력이 가능하게 함

CountDownLatch

일회성 배리어(barrier)로서 하나 이상의 스레드가 작업을 마칠 때까지 다른 여러 스레드가 대기하게 한다. 대기 중인 스레드가 진행할 수 있으려면 그 횟수만큼 countdown 메서드가 호출되어야 한다.

example

// 작업의 병령 수행 시간을 재는 간단한 프레임워크
public static long time(Executor executor, int concurrency,
        final Runnable action) throws InterruptedException {
    // 작업 준비
    final CountDownLatch ready = new CountDownLatch(concurrency);
    // 작업 시작(타이머)
    final CountDownLatch start = new CountDownLatch(1);
    // 작업 완료
    final CountDownLatch done = new CountDownLatch(concurrency);
    for (int i = 0; i < concurrency; i++) {
        executor.execute(new Runnable() {
            public void run() {
                ready.countDown();  // 타이머에게 준비됨을 알림
                try {
                    start.await();  // 다른 작업스레드가 준비될 때까지 대기
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    done.countDown();   // 타이머에게 끝났음을 알림
                }
            }
        })
    }
    ready.await();      // 모든 작업 스레드가 준비될 때까지 대기
    long startNanos = System.nanoTime();
    start.countDown();  // 출발
    done.await();       // 모든 작업 스레드가 끝날 때까지 대기
    return System.nanoTime() - startNanos;
}

작업(Runnable 인자)을 병렬로 실행할 횟수(concurrency) 만큼 돌려서 실행되는 시간을 구하는 메서드

  1. 작업을 쓰레드에 등록을 해서
  2. 전체 작업 쓰레드가 준비를 대기한 다음
  3. 시작시간을 재고
  4. 전체 작업이 시작되고
  5. 전체 작업이 마감이 된 후
  6. 진행된 시간을 계산해서 반환

주의사항

  • 실행자(Executor인자)가 주어진 병행수(concurrency) 보다 같거나 많은 수의 스레드를 동시에 생성할 수 있어야함
  • 시간을 잴 때는 System.currentTimeMillis 대신 System.nanoTime를 사용한다.
    • 더 정밀하고, 시스템의 실시간 클락의 변동에도 영향을 받지 않는다.

Samaphore

기타

CycleBarrier, Exchanger

레거시코드 (wait, notify)

example 표준숙어

// wait 메서드를 사용하는 표준숙어
synchronized (obj) {
    while (실패조건)
        obj.wait(); // 락을 해제하고 대기풀(wating pool) 에서 락 획득 대기
    // 락을 획득(스레드를 깨움) 후 해야할 실제작업
}

while 문으로 실패조건을 확인하는 이유?

if로 체크할 경우 wait 이후에 다시 락을 획득 했을 시 실패 조건 재검증이 안되서 일관성이 깨질 수 있음

락을 다시 획득하는(스레드를 다시 깨울) 경우 가능하면 notify 대신 notifyAll을 사용하라

  • JVM이 알아서 락을 획득할 스레드의 우선순위를 적절하게 정해줘서 영원히 잠드는(대기상태인) 스레드가 없을 것이다.
  • 악의적으로 또는 객체의 API 노출 등의 실수로 notify가 호출되는 경우 무한 대기가 걸릴 수 있음.

결론

자바API가 신규로 제공하는 고수준의 병행성 유틸리티를 사용하라. notify, wait를 사용할 이유가 없다.

만약 기존 코드를 유지보수 해야하는 상황이라면 위의 표준숙어를 이용해서 안전하게 처리하고, 일반적으로 notify 대신 notifyAll을 사용하도록 하라

더 자세한 내용은 Java Concurrency in Practice 책을 잠조하라.

Rule 61 - 추상화 수준에 맞는 예외를 던져라

상위 계층 추상화에 맞는 예외로 변환해서 던져야 한다.

example

// 예외 변환 (exception translation)
try {
    // 낮은 수준의 추상화 계층
    ...
} catch(LowerLevelException e) {
    throw new HighLevelException(...);
}

Exception chaining(예외 연결)

// 예외 연결
try {
    // 낮은 수준의 추상화 계층
    ...
} catch(LowerLevelException cause) {
    throw new HighLevelException(cause);
}

예외 스택을 통해서 하위 예외를 추적할 수 있다.

주의사항

남용하면 안 된다.

가능하면 하위계층에서 예외가 생기지 않도록 하는 것이다.

  • 하위계층 호출 시 인자 유효성 강화
  • 치명적인 상황(경고수준)이면 상위 계층에서 하위계층 예외를 격리시킨다. 이 때 로깅을 꼭 남기도록 한다.
    • 추후 해당 오류를 분석해야함

Rule 62 - 메서드에서 던져지는 모든 예외에 대해 문서를 남겨라

모든 예외를 javadoc @throws를 통해서 명확하게 기술한다.

일반적인 예외를 던지는 것은 금한다.

  • ex) throws Exception, throws Throwable

무점검 예외를 통해서 메서드를 성공적으로 호출하기 위한 선행 조건을 알 수 있다.

throws를 이용할 시에는 점검예외만 사용하자. 무점검 예외는 javadoc 문서만으로도 충분하다.

  • 점검지정예외와 무점검지정예외를 구분하는 일반적인 규약이다.

동일한 예외(ex:NullPointerException)를 던지는 메서드가 많다면 메서드 마다문서를 만들지 않고, 햬당 예외 클래스에 문서화 해도 된다.

Rule 63 - 어떤 오류인지를 드러내는 정보를 상세한 메시지에 담으라

오류 정보를 포착해 내기 위해서는, 오류의 상세 메시지에 예외에 관계된 모든 인자와 필드의 값을 포함 시켜야한다.

  • 예외 상세메세지 : 예외 원인을 파악하는 메세지
  • 사용자 레벨 오류 메시지 : 어플리케이션을 이용하는 최종사용자(end user)를 위한 메시지

Rule 64 - 실패 원자성 달성을 위해 노력하라

실패 원자성

메소드의 호출이 정상적으로 처리되지 못한 객체의 상태는, 메서드 호출 전 상태와 동일해야 한다. 이 속성을 만족하는 메서드는 실패 원자성(failure atomicity) 을 갖추었다고 한다.

실패원자성을 만족하는 방법

1. 불변 객체

불변 객체를 만들면 생성자를 기반하기 때문에 실패원자성은 자동으로 만족한다. 객체 생성 or 실패

2. pre check validation

실패할 가능성이 있는 코드를 전부 객체 상태를 바꾸는 코드 앞에 배치

3. Roleback

오류가 발생하면 복구코드가 연산 전 상태로 객체를 되돌린다

4. Temporary copy?

객체의 복사본 상에서 필요한 연산을 수행하고, 연산이 정상적으로 완료되면 임시 복사본의 내용을 객체 상태로 변경

Collection 정렬 시 권장 - 만약 정렬이 실패해도 원본은 그대로 유지

실패원자성을 달성할 수 없는 경우

  1. 멀티쓰레드를 통해 안전하지 않은 상태 객체 상태 제어 시 : 애초에 객체 일관성이 깨지고 복구를 어떤 쓰레드를 기준으로 해야할지 모호해짐
  2. 복구 비용이 심각하게 커지는 경우

만약 실패원자성을 만족하지 못한다면 *API문서에 어떻게 변하는지** 서술해야 한다.

Rule 65 - 예외를 무시하지 마라

무조건 예외는 catch 해야한다.

적어도 catch 블록 안에는 로그라도 남기고 그것마저 필요없다면 예외를 무시해도 괜찮은 이유라도 주석을 남겨두어야한다.

Rule 57 - 예외는 예외적 상황에만 사용하라

예외를 남용하면?

  1. 성능 저하
  2. 가독성 저하

예외는 예외적인 상황에서 사용해야 한다. 제어 흐름에 이용하면 안 된다.

example hasNext가 메서드가 없었다면

// 이렇게 하면 곤란
try {
    Iterator<Foo> i = collection.iterator();
    while(true) {
        Foo foo = i.next();
        ...
    }
} catch (NoSuchElementExcepton e) {
}

위와 같인 제어흐름 일부를 일부러 예외를 사용하도록 강요하면 안된다.

정상적인 제어를 위함 처리 방안

  1. 상태 검사 메서드 : Iterator#hasNext()
    1. 대부분의 상황에서 추천
    2. 가독성이 좋으며, 디버깅에 유리
  2. 특이값(distinguished value) 제공 : null, -1 또는 enum상수
    1. 멀티쓰레드 환경에서 강추 - 상태 메서드가 가변적이므로

Rule 58 - 복구 가능 상태에는 점검지정 예외를 사용하고, 프로그래밍 오류에는 실행지점 예외를 이용하라

Throwable

점검지정 예외(checked exception)

client가 복구할 것으로 여겨지는 상황에 던지는 예외. 즉 복구한 권한을 준다는 것

실행시점 예외(runtime exception)

프로그래밍 오류를 표현할 때는 실행시점 예외를 사용한다.

일반적으로 선행조건 위반(precondition violation)을 나타낸다. 예를들면 인자유효성 검증 오류

오류(error)

프로그램을 더 이상 실행할 수 없는 상황 자원(메모리)이 부족한 상황에 발생할 수 있음.

JVM에 기반하는 프로세스 자체가 죽어버림. Exception들은 해당 Thread 가 죽을 뿐 프로세스는 살아있음. 즉 프로그램은 계속 유지가능

예외도 객체다

필요에 따라서 catch구문에서 적절하게 상태나 메서드를 구현해서 처리할 수 있도록 한다.

example

통장 잔고가 부족해서 이쳬가 실패할 경우(NotEnoughBalanceException) 잔고가 얼마인지 알려주는 getter(getBalance()) 제공

Rule 59 - 불필요한 점검지정 예외 사용은 피하라

너무 남발하면 사용하기 불편한 API가 될 것이다.

Rule 60 - 표준 예외를 사용하라

코드 재사용성은 예외에도 적용되어야 한다.

표준예외 장점

  1. 쉬우며, 친숙함
  2. 가독성이 좋음
  3. 클래스 로딩 수가 줄어듬 (성능향상)

자주 사용하는 표준 예외

예외 용례
IllegalArgumentException null이 아닌 인자의 값이 잘못되었을 때
IllegalStateException 객체 상태가 메서드 호출을 처리하기에 적절치 않을 때
NullPointerExceptoin null 값을 받으면 안되는 인자에 null이 전달되었을 때
IndexOutOfBoundsException 인자로 주어진 첨자가 허용범위를 벗어났을 때
ConcurrentModificationException 병렬적으로 사용이 금지된 객체에 대한 병렬 접근이 탐지되었을 때
UnsupportedOperationException 객체가 해당 메서드를 지원하지 않을 때

그 외의 예외

  • ArthmeticException : 수학적계산오류
  • NumberFormatException : 숫자형식오류

Rule 51 - 문자열 연결 시 성능에 주의하라

n개의 문자열에 연결 연산자(+ or concat)를 반복 적용해서 연결하는데 드는 시간은 n^2에 비례한다.

문자열은 불변객체이기 때문이다

반복적으로 문자열을 연결해야한다면 StringBuilder을 사용해야한다.

example

public String statement() {
    StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
    for (int i = 0; i < numItems(); i++)
        b.append(lineForItem(i));
    return b.toString();
}

StringBuffer의 경우 동기화가 되기 때문에 성능이 저하된다. 쓰레드 안정성이 필요하지 않다면 (예를 들면 지역변수라면) StringBuilder를 쓰도록하자.

Rule 52 - 객체를 참조할 때는 그 인터페이스를 사용하라

인터페이스를 자료형으로 쓰는 습관을 들이면 프로그램은 더욱 유연해진다.

example

// 좋은 예제
List<Subscriber> subscribers = new Vector<Subscriber>();
// 나쁜 예제
Vector<Subscriber> subscribers = new Vector<Subscriber>();

인터페이스를 자료형으로 사용하면 구현체를 변경할 수 있으므로 훨씬 유연하다. 하지만 실제 구상클래스가 특별한 기능(예를 들면 동기화)을 제공한다면 해당 기능을 유지하도록 해야한다.

적당한 인터페이스가 없는 경우에는 객체를 클래스로 참조하는 것이 당연하다.

  • 값객체
  • final 클래스
  • 유틸성 클래스(Random)

인터페이스가 없고 기반클래스가 있다면 그것을 사용하는 것도 한 방법이다.

결론

  • 인터페이스를 통한 참조를 하면 유연해진다.
  • 적절한 인터페이스가 없다면 가장 일반적인 클래스(super class)를 사용하도록 하라

Rule 53 - 리플렉션 대신 인터페이스를 이용하라.

리플렉션

클래스의 정보를 가져오는 프로그램을 작성할 수 있게 하는 기능. 클래스의 맴버 이름이나 필드 자료형, 메서드 시그너처 등의 정보를 얻어낼 수 있다.

  • Class, Constructor, Method, Field

그리고 위 객체들을 이용하면 실제 행위나 상태들을 반영적으로(reflectively) 조작할 수 있다.

리플렉션의 단점

  1. 실행시점 오류 가능성이 큼
  2. 가독성이 매우 떨어짐
  3. 성능이 낮음

결론

컴파일 시점에 알수 없는(실행시점에 알 수 있는) 클래스를 이용하고 싶다면, 리플렉션을 사용하되 가능하면 객체를 만들 때만 이용하고, 객체를 참조할 때는 컴파일 시에 알고 있는 인터페이스나 상위 클래스를 이용하라

Rule 54 - 네이티브 메서드는 신중하게 사용하라

JNI : Java Native interface**

C나 C++ 등의 네이티브 프로그래밍 언어로 작성된 네이티브 메서드를 호출하는데 이용되는 기능

용도

  1. OS(플랫폼) 고유 기능 이용 가능
  2. 네이티브 라이브러리 재사용
  3. 성능 향상

그러나 네이티브 메서드를 통한 성능개선은 추천하지 않는다.

  • 과거에 비해 JVM의 성능 향상
  • Java 만으로 필적한 성능 가능

네이티브 메서드 직접 사용 시 단점

  1. 안전하지 않음
  2. 메모리 참조 이슈
  3. 플랫폼 종속정 (이식성이 낮음)
  4. 디버깅 어려움
  5. 잘못 사용할 경우 성능하락
  6. 가독성이 떨어짐

결론

가능하면 네이티브 메서드는 사용하지 않는다. 만약 사용한다면 최소한의 용도로만 이용하고, 검증을 확실히 한다.

Rule 55 - 신중하게 최적화하라

최적화의 3가지 격언

맹목적인 어리석음(blind stupidity)을 비롯한 다른 어떤 이유보다도, 효율성이라는 이름으로 저질러지는 죄악이 더 많다(효율성을 반드시 성취하는 것도 아니면서 말이다)

작은 효율성(small efficiency)에 대해서는, 말하자면 97% 정도에 대해서는, 잊어버려라. 섣부른 최적화(premature optimization)는 모든 악의 근원이다.

최적화를 할 때는 아래의 두 규칙을 따르라 규칙 1 : 하지마라 규칙 2 : (전문가들만 따를 것) 아직은 하지마라 - 완벽히 명료한, 최적화되지 않은 해답을 얻을 때 까지는.

즉 성능 최적화는 신중하게 생각하고 적용하라

원칙

성능 때문에 구조적인 원칙(architectural principle)을 희생하지 마라.

구조가 좋은데, 성능이 안 나오면 내부 모듈을 수정을 통해서 유연하게 성능을 향상하면서 구조를 유지할 수 있다. 구조가 안 좋은데, 성능이 안 나오면 구조 자체를 변경하거나 나쁜 구조가 더 나빠질 수 있다.

설계를 할 때는 성능을 제약할 가능성이 있는 결정들을 피하라.

API를 설계할 때 내리는 결정들이 성능에 어떤 영향을 끼칠지를 생각하라.

잘 설계된 API는 일반적으로 좋은 성능을 보인다. 좋은 성능을 내기 위해 API를 급진적으로 바꾸면 호환성 때문에 더 큰 문제가 발생할 수 있다.

최적화는 객관적인 수치를 바탕으로 진행하라.

결론

빠른 프로그램을 만들고자 애쓰지 말라는 것이다. 대신 좋은 프로그램을 짜기 위해 노력하라. 성능은 따라올 것이다.

Rule 56 - 일반적으로 통용되는 작명 관습을 따르라

자바는 작명 관습(naming convention)이 잘 정립되어 있음

철자 작명 관습

식별자 자료형 예제 비고
패키지 com.edu, com.sun 소문자와 .으로 구분
타입 Timer, FutureTask  
메서드, 필드 remove, ensureCapacity  
상수 VALUES, NEGATIVE_INFINITY  
지역변수 i, xref, houseNumber 약어가능
자료형인자(제네릭타입) <E>, <T>, <IN> 대문자

문법 작명 관습

클래스

  • enum : 단수형 명사
  • 클래스, 인터페이스 : 명사, 형용사격(~able, ~ible) 어미
  • 어노테이션 : 이것저것 다

메서드

  • 동사
    • getter, setter
  • boolean 일 시 : is~, has~
  • 객체 변환 : toType, asType, typeValue