Effective Java 3rd - Chapter09. 일반적인 프로그래밍 원칙

Dec 27, 2018


Item 57. 지역변수의 범위를 최소화하라

  1. ‘가장 처음 쓰일때 지역변수를 선언’해라.
  2. 반복변수의 값을 반복문이 종료된 뒤에도 써야 하는 상황이 아니라면 while문 보다 for문을 사용해라.
  3. 메서드를 작게 유지하고 한 가지 기능에 집중해라. 메서드를 기능별로 쪼개면 관련된 지역변수만 메서드별로 최소화할수 있다.

Item 58. 전통적인 for문보다는 for-each문을 사용하라

아래의 세가지 상황중 어느 상황에도 속하지 않는다면 for문보다 for-each문을 사용하자. for-each문은 for문보다 명료하고, 유연하고, 버그를 예방해준다.

  1. 파괴적인 필터링 - 컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 반복자의 remove 메서드를 호출해야 한다.
  2. 변형 - 리스트나 배열을 순회하면서 그 원소의 값 일부 혹은 전체를 교체해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야 한다.
  3. 병렬 반복 - 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자 또는 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 한다.

단, for-each문을 사용하기 위해서는 Iterable 인터페이스를 구현한 객체여야한다.

public interface Iterable<E> {
	// 이 객체의 원소들을 순회하는 반복자를 반환한다.
	Iterator<E> iterator();
}



Item 59. 라이브러리를 익히고 사용하라

자바 프로그래머라면 적어도 java.lang, java.util, java.io와 그 하위 패키지들에는 익숙해져야 한다. 위와 같은 자바 표준 라이브러리에서 원하는 기능을 찾지 못하면, 고품질의 서드파티 라이브러리를 찾아보자. 일반적으로 라이브러리의 코드는 여러분이 직접 작성한 것보다 품질이 좋고, 점차 개선될 가능성이 크다. 적합한 서드파티 라이브러리도 찾지 못했다면, 그때 직접 구현하라.

예를들어, 난수 생성시에 자바에서 제공하는 품질 좋은 ThreadLocalRandom 클래스를 사용하자.

Item 60. 정확한 답이 필요하다면 float와 double은 피하라

floatdouble 타입은 과학과 공학 계산용으로 설계되었다. 이진 부동소수점 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 근사치로 계산하도록 세심하게 설계되었다. 따라서 정확한 결과가 필요할 때는 사용하면 안 된다. (ex. 금융 관련 계산)

다음은 정확한 결과을 요하는 소수점 계산 코드 작성 팁이다.

  • 소수점 추적은 시스템에 맡기고, 코딩 시의 불편함이나 성능 저하를 신경 쓰지 않겠다면, BigDecimal을 사용하라.
  • 성능이 중요하고 소수점을 직접 추적할 수 있고, 숫자가 너무 크지 않다면 intlong을 사용하라.
    • 숫자를 아홉 자리 십진수로 표현할 수 있으면 int를 사용
    • 숫자를 열여덟 자리 십진수로 표현할 수 있으면 long을 사용
    • 열여덟 자리를 넘어가면 BigDecimal을 사용

Item 61. 박싱된 기본 타입보다는 기본 타입을 사용하라

기본 타입박싱된 기본 타입의 주된 차이는 크게 세가지다. 다음의 세가지를 통해 기본 타입을 주로 사용하는 이유를 설명하겠다.

1. 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성이란 속성을 갖는다.
다음은 Integer 값을 오름차순으로 정렬하는 비교자다.

Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

위의 상황에서 인자로 (new Integer(42), new integer(42))를 받는다고 가정해보자. 우리가 의도한 예상동작은 0이라는 리턴값을 얻는것이다. 하지만 직접 실행해보면 1을 리턴하는 것을 알 수 있다. (i < j)는 잘 동작하지만, (i == j) 구문에서 박싱된 기본 타입끼리 객체 참조의 식별성을 검사하기 때문에 위와 같은 현상이 발생한 것이다. 실무에서 이와 같이 기본 타입을 다루는 연산자가 필요하다면, Comparator.naturalOrder()를 사용하자.

2. 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 null을 가질 수 있다.
박싱된 기본 타입은 하나의 객체로 취급되므로, null을 가질 수 있다. 박싱된 기본 타입을 기본 타입과 비교할때, 박싱된 기본 타입의 박싱이 자동으로 풀린다. 이 과정에서 박싱된 기본타입이 null을 가지고 있었다면, 언박싱을 진행할때 예기치 못한 NullPointerException이 발생한다.

3. 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.

public static void main(String[] args) {
	Long sum = 0L;
	for (long i = 0; i <= Integer.MAX_VALUE; ++i) {
		sum += i;
	}
}

위 프로그램은 지역변수 sum을 박싱된 기본 타입으로 선언하였다. 그 결과 박싱과 언박싱이 반복해서 일어나, 성능이 체감될 정도로 느려진다.

박싱된 기본 타입을 써야할 때는 다음의 경우이다.

  • 매개변수화 타입이나 매개변수화 메서드의 타입 매개변수에 사용
    • ex) List, ThreadLocal
  • 리플렉션을 통해 메서드를 호출할 때 사용

Item 62. 다른 타입이 적절하다면 문자열 사용을 피하라

문자열은 다른 값 타입을 대신하기에 적합하지 않다. 예를 들어 예/아니오 질문의 답이라면 적절한 열거 타입이나, boolean으로 변환해야한다.

문자열은 열거 타입을 대신하기에 적합하지 않다. 문자열에 오타가 있다면 컴파일시에 문제의 원인을 찾기 힘들것이고, enum의 장점을 포기해야 한다.

문자열은 혼합 타입을 대신하기에 적합하지 않다. 예를 들어 아래의 코드를 보자.

String compoundKey = className + "#" + i.next();

각 요소를 개별로 접근하려면 문자열을 파싱해야 해서 느리고, 귀찮고, 오류 가능성도 커진다. 차라리 전용 클래스를 새로 만들어라.

문자열은 권한을 표현하기에 적합하지 않다. 아래의 코드를 보자.

public class ThreadLocal {
	private ThreadLocal() { }

	public static void set(String key, Object value);

	public static Object get(String key);
}

이 방식이 올바르게 동작하려면 각 클라이언트가 고유한 키를 제공해야 한다. 그런데 먄약 두 클라이언트가 실수로 같은 키를 쓰기로 결정한다면, 의도치 않게 같은 변수를 공유하게 된다. 이처럼 문자열 기반 API는 안전하게 만들기가 힘들다. 문자열 대신 고유한 키(예를들어 객체)를 사용하자.

Item 63. 문자열 연결은 느리니 주의하라

문자열 연결 연산자(+)로 문자열 n개를 잇는 시간은 n^2에 비례한다. 문자열은 불변이라서 두 문자열을 연결하는 경우 양쪽의 내용을 모두 복사해야 하므로 성능 저하는 피할 수 없다.

하지만 StringBuilder를 사용하면 문자열 n개를 잇는 시간은 n에 비례하게 된다. 성능에 신경을 써야한다면 문자열 연결시 StringBuilder를 사용하자.

Item 64. 객체는 인터페이스를 사용해 참조하라

적합한 인터페이스만 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라. 나중에 구현 클래스를 교체하고자 한다면 그저 새 클래스의 생성자 및 정적 팩토리를 호출해주기만 하면 된다.

// 인터페이스 타입으로 선언
Set<Son> sonSet = new HashSet<>();

// 순서를 보장하는 기능이 필요하다면 추후에 구현 클래스 변경
Set<Son> sonSet = new LinkedHashSet<>();

적합한 인터페이스가 없다면 클래스로 참조해야 한다.

예를들면,

  • String, BigInteger 같은 값 클래스
  • 프레임워크가 제공하는 클래스 기반으로 이미 작성된 클래스들
    • 이런 경우라도 특정 구현 클래스보다는 추상 클래스 및 기반 클래스를 사용해 참조하는게 좋음
  • 인터페이스에는 없는 특별한 메서드를 제공하는 클래스들
    • PriorityQueue 클래스는 Queue 인터페이스에는 없는 comparator 메서드를 제공

Item 65. 리플렉션보다는 인터페이스를 사용하라

리플렉션(java.lang.reflect)을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다. Constructor, Method, Field 인스턴스를 가져올 수 있고, 나아가 멤버 이름, 필드 타입, 메서드 시그니쳐 등을 가져올 수 있으며, 각각의 생성자, 메서드, 필드를 조작할 수도 있다. 즉 이 인스턴스들을 통해 해당 클래스의 인스턴스를 생성하거나, 메소드를 호출하거나, 필드에 접근할 수 있다.

하지만 다음과 같은 단점이 있다.

  • 컴파일 타입 검사가 주는 이점을 하나도 누릴 수 없다. 잘못 사용시 런타임 오류가 발생한다.
  • 리플렉션을 이용하면 코드가 지저분하고 장황해진다.
  • 성능이 떨어진다.

그러므로 리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 꾀하고 이점만 취할 수 있다. 즉 리플렉션은 인스턴스 생성 및 의존성 주입 목적으로만 사용하고, 만들어진 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자. 필자가 생각하는 가장 좋은 예시는 Spring Framework의 DI(의존성 주입)의 원리를 어노테이션 그리고 리플렉션과 연관지어 생각해보는 것이다.

Item 66. 네이티브 메서드는 신중히 사용하라

JNI(Java Native Interface)는 자바 프로그램이 C나 C++같은 네이티브 메서드를 호출하는 기술이다. 전통적으로 네이티브 메서드의 주요 쓰임은 다음 세 가지다.

  1. 레지스트리 같은 플랫폼 특화 기능을 사용한다.
  2. 네이티브 코드로 작성된 기존 라이브러리를 사용한다.
  3. 성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성한다. (지속적으로 JVM 및 자바 라이브러리가 발전해왔기 때문에 성능 개선을 목적으로 사용하는 것은 권장하지 않는다.)

필자가 몇 년전 이미지 처리를 위해 OpenCV를 사용했었을 때에는, 대부분의 기능이 C로 작성된 네이티브 메서드를 자바 메서드로 Wrapping만 해주는 라이브러리였다. 아마 2번과 같은 이유로 네이티브 메서드를 사용했었을 것이다.

하지만 다음과 같은 단점이 있으므로 신중하게 사용해야한다.

  1. 메모리 훼손 오류로부터 더 이상 안전하지 않다.
  2. 자바보다 플랫폼을 많이 타서 이식성이 낮고 디버깅도 어렵다. 주의하지 않으면 성능이 오히려 느려질 수 있다.
  3. 가비지 컬렉터의 이점을 누릴수 없다.
  4. 자바 코드와 네이티브 코드의 경계를 넘나들 때마다 비용이 추가된다.
  5. 네이티브 메서드와 자바 코드 사이의 접착 코드를 작성해야 한다.

네이티브 코드를 어쩔 수 없이 사용해야한다면, 최소한만 사용하고 철저하게 테스트하라.

Item 67. 최적화는 신중히 하라

성능 때문에 견고한 구조를 희생시키지 말자. 빠른 프로그램보다는 좋은 프로그램을 작성하라. 좋은 프로그램은 개별 구성요소의 내부를 독립적으로 설계할 수 있다. 따라서 시스템의 나머지에 영향을 주지 않고도 각 요소를 다시 설계할 수 있다. 다시 말하면, 구현상의 문제는 나중에 최적화해 해결할 수 있지만, 아키텍쳐의 결함이 성능을 제한하는 상황이라면 시스템 전체를 다시 작성하지 않고는 해결하지 못할 수 있다. 다음의 가이드라인을 따르자.

  • 성능을 제한하는 설계를 피하라.
    • API, 네트워크 프로토콜, 영구 저장용 데이터 포맷 등 컴포넌트 및 외부 시스템과의 소통 방식은 완성 후 변경하기가 어려운 설계 요소이다. 설계시 이 부분들은 신중히 고려하라.
  • API를 설계할 때 성능에 주는 영향을 고려하라.
    • ex) 구성으로 해결할 수 있음에도 불구하고 상속 방식으로 설계한 public 클래스는 상위 클래스의 성능 제약까지도 물려받게 된다.
  • 성능을 위해 API를 왜곡하는 건 매우 좋지않다. 잘 설계된 API는 성능도 좋은 게 보통이다.
  • 최적화 시도 전후로 성능을 측정하라.
    • 프로그램에서 시간을 잡아먹는 부분을 추측하기가 어려워, 최적화 시도 후에도 성능이 개선되지 않을 가능성이 있다.
  • 프로파일링 도구를 사용해 최적화 노력을 어디에 집중해야 할지 찾아라.

Item 68. 일반적으로 통용되는 명명 규칙을 따르라

  • 패키지와 모듈 이름
    • 조직의 인터넷 도메인 이름을 역순으로 사용
    • edu.cmu, com.google
    • 패키지 이름의 나머지는 해당 패키지를 설명하는 약어를 사용
      • utilities 보다는 util
  • 열거 타입, 어노테이션, 클래스, 인터페이스 이름
    • 하나 이상의 단어로 이루어지며, 대문자로 시작
      • List, FutureTask
    • max, min처럼 널리 통용되는 줄임말을 제외하고 단어를 줄여 쓰지 않기
    • 객체를 생성할 수 있는 클래스는 단수 명사나 명사구를 사용
      • Thread, ChessPiece
    • 객체를 생성할 수 없는 클래스는 복수형 명사로 사용
      • Collections, Collectors
    • 인터페이스 이름은 클래스와 똑같이 짓거나 able 혹은 ible로 끝나는 형용사로 사용
      • Comparator 또는 Runnable, Accessible
  • 메서드, 필드 이름
    • 첫 글자는 소문자로 쓴다는 점만 빼면 클래스 명명 규칙과 같음
      • remove, ensureCapacity
    • 단 상수 필드는 모두 대문자로 쓰며, 단어 사이는 _로 구별
      • VALUES, NEGATIVE_INFINITY
    • 어떤 동작을 수행하는 메서드의 이름은 동사나 목적어를 포함한 동사구로 지음
      • append 또는 isDigit, hasElements
    • 반환 타입이 boolean이 아니거나 해당 인스턴스의 속성을 반환하는 메서드의 일므은 보통 명사, 명사구 혹은 get으로 시작하는 동사구로 지음
      • size, hashCode, getTime
    • 객체의 타입을 바꿔서 다른 타입의 또 다른 객체를 반환하는 메서드 이름은 toType 형태로 지음
      • toString, toArray
    • 객체의 내용을 디른 뷰로 보여주는 메서드의 이름은 asType 형태로 지음
      • asList
    • 객체의 값을 기본 타입 값으로 반환하는 메서드의 이름은 typeValue 형태로 지음
      • intValue
    • 정적 팩토리 메서드는 from, of, instance, newType을 흔히 사용
    • boolean 타입의 필드 이름은 boolean 접근자 메서드에서 앞 단어를 뺀 형태
      • initialized, composite
    • 그 외 타입의 필드라면 명사나 명사구 사용
      • height, digits
  • 지역 변수
    • 다른 멤버와 비슷한 명명 규칙이 적용
    • 약어를 사용해도 좋음
      • i, houseNum
  • 타입 매개변수
    • 임의의 타입 : T
    • 컬렉션 원소의 타입 : E
    • 맵의 키와 값 : K, V
    • 예외 : X
    • 반환 타입 : R
    • 임의 타입의 시퀀스 : T, U, V 또는 T1, T2, T3