1 Mar 2019

Effective Java 09 - 일반적인 프로그래밍 원칙

일반적인 프로그래밍 원칙


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

이 원칙은 클래스와 멤버의 접근 권한을 최소화하라는 것과 비슷하다. 지역변수의 유효범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고 오류 가능성은 낮아진다.

지역변수의 범위를 줄이는 가장 강력한 기법은 “가장 처음 쓰일 때 선언하기”이다. 사용하려면 멀었는데 미리 변수를 선언부터 해두면, 코드가 어수선해져 가독성이 떨어진다. 지역변수를 생각없이 선언하다보면 변수가 실제로 쓰이는 범위보다 너무 앞서 선언하거나, 다 쓴 뒤에도 여전히 살아 있게 되기 쉽다.

거의 모든 지역변수는 선언과 동시에 초기화해야 한다. 초기화에 필요한 정보가 충분치 않다면 충분해질 때까지 선언을 미루어야 한다.

지역변수 범위를 최소화하는 마지막 방법은 메서드를 작게 유지하고 한 가지 기능에 집중하는 것이다. 한 메서드에서 여러 기능을 처리한다면 그중 한 기능과 연관된 지역변수라도, 다른 기능을 수행하는 코드에서 접근할 수 있다.


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

다음은 전통적인 for 문으로 컬렉션을 순회하는 코드이다.

for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
    Element e = i.next();
    ...
}

for (int i = 0; i < a.length; i++) {
    ...
}

반복자와 인덱스 변수는 모두 코드를 지저분하게 할 뿐이고, 이 코드에서 정작 필요한 것은 원소들 뿐이다. 이처럼 쓰이는 요소의 종류가 늘어나면 오류가 생길 가능성이 커지며 변수를 잘못 사용할 틈새가 넓어진다. 또한 컬렉션이냐 배열이냐에 따라 코드 형태가 상당히 달라진다.

for-each 문을 사용하면 모두 해결된다. 반복자와 인덱스 변수를 사용하지 않으니 코드가 깔끔해지고 오류가 날 일도 없다. 하나의 코드 형태로 컬렉션과 배열을 모두 처리할 수 있다.

for (Element e : elements) {
    ...
}

컬렉션을 중첩해서 순회해야 한다면 for-each 문의 이점이 더 커진다.

enum Suit { CLUB, DIAMOND, HEART, SPADE }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
    NINE, TEN, JACK, QUEEN, KING }

static Collection<Suit> suits = Arrays.asList(Suit.values());
static Collection<Rank> ranks = Arrays.asList(Rank.values());

List<Card> deck = new ArrayList<>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
    for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
        deck.add(new Card(i.next(), j.next())); // 버그

위의 코드에서 NoSuchElementException 예외가 발생하는데, 이는 바깥 컬렉션 (suits)의 반복자에 대해 next 메서드가 너무 많이 불리기 때문이다. 안쪽 반복문에서 ranks 컬렉션의 수만큼 suits의 반복자 next 메서드가 호출된다.

이를 회피하기 위해서는 다음과 같이 suits 컬렉션의 현재 반복자를 중간에 저장해두어야 한다.

for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) {
    Suit suit = i.next();
    for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
            deck.add(new Card(suit, j.next())); 
}

하지만 코드가 깔끔하지 않다. 대신 for-each 문을 사용함으로써 이 문제는 간단히 해결된다.

for (Suit suit : suits)
    for (Rank rank : ranks)
        deck.add(new Card(suit, rank));

코드가 놀랄만큼 간결해지지만, 모든 상황에서 for-each 문을 사용할 수 있는 것은 아니다.

  • 파괴적인 필터링: 컬렉션을 순회하면서 원소를 제거해야 될 경우 반복자의 remove 메서드를 호출해야 한다.
  • 변형: 리스트나 배열을 순회하면서 원소의 값을 변경해야 한다면 반복자나 인덱스를 사용해야 한다.
  • 병렬 반복: 여러 컬렉션을 병렬적으로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용하여 명시적으로 제어해야 한다.

for-each 문은 컬렉션과 배열은 물론 Iterable 인터페이스를 구현한 객체라면 무엇이든 순회 가능하다.

public interface Iterable<E> {
    Iterable<E> iterator();
}

Iterable을 처음부터 구현하기는 힘들지만 원소들의 묶음을 표현하는 타입을 정의해야 한다면 Iterable을 구현하는 것도 고려할만 하다.


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

표준 라이브러리를 적극적으로 활용하면 그 코드를 작성한 전문가의 지식과 앞서 사용한 프로그래머들의 경험을 활용할 수 있다. 또한 핵심적인 일과 크게 관련없는 문제를 해결하느라 시간을 허비하지 않아도 된다. 그리고 표준 라이브러리는 성능이 지속적으로 개선되고, 기능도 점점 많아진다는 이점이 있다.

이렇게 표준 라이브러리를 활용한 코드는 많은 사람들에게 읽히기 쉬운 낯익은 코드가 될 수 있다는 점이다. 자연스럽게 유지보수하기 좋고 재활용하기 쉬운 코드가 된다.

자바 프로그래머라면 적어도 java.lang, java.util, java.io와 그 하위 패키지들에는 익숙해져야 한다. 아주 특별한 기능이 아니라면 누군가가 이미 라이브러리 형태로 구현해놓았을 가능성이 크다. 그런 라이브러리가 있다면, 쓰면 된다.


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

float와 double 타입은 과학과 공학 계산용으로 설계되었다. 이진 부동소수점 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 ‘근사치’로 계산하도록 설계되었다. 따라서 정확한 결과가 필요할 경우 사용하면 안된다. 대신에 BitDecimal이나 int 혹은 long을 사용해 결과를 구해야 한다.


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

자바의 데이터 타입은 크게 두 가지로 나뉠 수 있는데, int 및 double, boolean과 같은 기본 타입과 String, List와 같은 참조 타입으로 나눌 수 있다. 그리고 각 기본 타입에 대해 대응하는 참조 타입이 있는데 이를 박싱된 기본 타입이라고 한다.

기본 타입과 박싱된 기본 타입의 주된 차이는 크게 세 가지이다.

  1. 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성이라는 속성을 지닌다.
    • 박싱된 기본 타입은 값이 같아도 서로 다르다고 식별될 수 있다.
  2. 언제나 기본 타입의 값은 유효한 반면, 박싱된 기본 타입은 null을 가질 수도 있다.
  3. 기본 타입이 박싱된 기본 타입에 비해 시간과 메모리 사용면에서 더 효율적이다.

다음 예를 살펴보자.

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

위 코드는 Integer 값을 오름차순으로 정렬하는 비교자인데, 심각한 결함이 존재한다. naturalOrder.compare(new Integer(42), new Integer(42))의 값을 출력해보면 값이 같으므로 0이 리턴해야 될 것 같지만, 실제로는 1을 리턴한다.

이의 원인은 (i == j) 비교문에 있다. 이 비교문에서 두 객체 참조의 식별성을 검사하게 된다. 같은 값이지만 서로 다른 인스턴스이므로 이 비교의 결과는 false이며, 따라서 1을 반환하는 것이다. 이 처럼 박싱된 기본 타입에 == 연산자를 사용하면 오류가 발생할 수도 있다.

다음 코드도 살펴보자.

Integer i;

if (i == 42) {
    System.out.println("equals");
}

이 코드의 결과로 NullPointerException 예외가 발생한다. 원인은 이 i 변수가 초기화되지 않은 Integer이며 null이라는 데 있다. 기본 타입과 박싱된 타입을 혼용한 연산에서는 박싱된 타입의 박싱이 자동으로 풀리는데, null 참조를 언박싱하면 NullPointerException 예외가 발생한다. 또한 언박싱이 자동으로 이루어지므로 성능이 느려질 수도 있다. 따라서 박싱된 기본 타입 대신 일반 기본 타입을 사용할 수 있다면 일반 기본 타입을 사용해야 한다.

박싱된 기본 타입을 사용해야 하는 경우는 다음과 같다.

  1. 컬렉션의 원소나 키, 값으로 사용할 경우
    • 컬렉션은 기본 타입을 담을 수 없다.
  2. 매개변수화 타입이나 제네릭 메서드의 타입 매개변수로 사용할 경우
    • 자바 언어는 타입 매개변수로 일반 기본 타입은 지원하지 않는다.
  3. 리플렉션을 통해 메서드를 호출할 경우


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

문자열 연결 연산자 (+)는 여러 문자열을 하나로 합쳐주는 편리한 수단이다. 한 줄짜리 출력값 또는 크기가 고정된 객체의 문자열 표현을 만들 때는 괜찮지만, 남용시 성능 저하를 감내하기 어렵다. 문자열 연결 연산자로 n개의 문자열을 잇는 시간은 n^2에 비례한다. 문자열은 불변이라서 두 문자열 연결시 양쪽의 내용을 모두 복사해야 하기 때문이다.

따라서 성능을 위해서는 문자열 연결시 String 대신 StringBuilder를 사용해야 한다.


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

메서드의 매개변수 타입으로 클래스가 아니라 인터페이스를 사용해야 된다고 했는데, 이 뿐만 아니라 객체 또한 클래스가 아닌 인터페이스로 참조하는 것이 좋다. 적합한 인터페이스가 있다면 매개변수 뿐만 아니라 반환 값, 변수, 필드를 전부 인터페이스로 선언하는 것이다. 객체의 실제 클래스를 사용해야 할 상황은 오직 생성자로 생성할 때 뿐이다.

인터페이스를 타입으로 사용하는 습관을 길러두면 프로그램이 훨씬 유연해진다. 나중에 구현 클래스를 교체하고자 할 경우, 그저 새 클래스의 생성자를 호출해주기만 하면 된다.

만약 사용할만한 인터페이스 타입이 없다면 클래스의 계층구조 중 필요한 기능을 만족하는 가장 덜 구체적인 (상위의) 클래스를 타입으로 사용하도록 한다.


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

리플레션 기능을 사용하면 프로그램에서 임의의 클래스에 접근이 가능하다. 생성자 및 메서드, 필드를 조작할 수도 있다.

그러나 리플렉션은 다음 단점이 존재한다.

  1. 컴파일타임의 타입 검사가 주는 이점을 하나도 누릴 수 없다.
  2. 리플렉션을 이용하면 코드가 장황하고 지저분해진다.
  3. 성능이 느리다.
    • 리플렉션을 통한 메서드 호출은 일반적인 메서드 호출보다 훨씬 느리다.

리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피하고 이점을 취할 수 있다. 컴파일 타임에 이용할 수 없는 클래스를 사용해야 하는 프로그램은 적절한 인터페이스나 상위 클래스를 이용하면 된다.


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

자바 네이티브 인터페이스 (JNI)는 자바 프로그램이 네이티브 메서드를 호출하는 기술이다. 여기서 네이티브 메서드란 C, C++과 같은 네이티브 프로그래밍 언어로 작성한 메서드를 말한다.

네이티브 메서드는 플랫폼 특화된 기능을 사용하거나, 네이티브 코드로 작성된 라이브러리를 사용할 경우 또는 성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성하는 경우이다.

성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 거의 권장하지 않는다. JVM은 그동안 엄청난 속도로 발전해 일반 자바 코드로도 충분한 성능을 보여주며, 네이티브 메서드는 안전하지도 않다. 이식성이 낮고 디버깅도 어렵다. 또한 가비지 컬렉터가 네이티브 메모리를 자동 회수하지도 못하며 추적도 못한다. 마지막으로 네이티브 메서드와 자바 코드 사이에 접착 코드(glue code)를 작성해야 하는데 귀찮은 작업이기도 하고 가독성도 떨어진다.


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

대부분의 최적화는 좋은 결과보다는 해로운 결과로 이어지기 쉽다. 빠르지도 않고 제대로 동작하지도 않으면서 유지보수하기는 어려운 코드를 생산하기 쉽다.

성능 때문에 견고한 구조를 희생하면 안된다. 빠른 프로그램보다는 좋은 프로그램을 작성해야 한다. 신중하게 설계하여 깨끗하고 명확한 멋진 구조를 갖춘 프로그램을 완성한 다음에야 최적화를 고려해볼 차례가 된다.


Tags:
Stats:
0 comments