[이펙티브 자바 3판] 9장. 일반적인 프로그래밍 원칙

[Effective Java 3th Edition] Chapter9: General Programming

#java #effectivejava


목차


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

Minimize the scope of local variables

지역변수는 사용할 때 선언하고 초기화해야 한다.

옛날 방식의 습관으로 코드 블록의 첫 부분에 변수를 선언하는 경우가 많았다. 하지만 자바는 어디에서 선언해도 된다. 그렇기 때문에 처음 사용할 때 선언하면 지역변수의 범위를 줄일 수 있다. 그리고 모든 지역변수는 선언과 함께 초기화해야 초깃값을 헷갈리는 경우가 없다.

아직 지역변수를 초기화할 수 없다면 초기화할 수 있을 때 선언하면 된다. 다만 try-catch 문장에서는 예외다. try 블록 밖에서도 변수를 사용해야 한다면 지역변수의 선언은 try 문장 밖에서 진행하고 초기화는 try 문장 안에서 해야 한다.

반복문은 while 보다 for 문을 권장한다.

while 문을 사용하면 반복문 밖으로 불필요한 변수가 선언된다.

Iterator<Element> i = c.iterator(); // 불필요하다.
while (i.hasNext()) {
    doSomething(i.next());
}

for 문을 사용하면 반복 변수(loop variable)의 범위가 반복문 내부로 제한된다. 따라서 똑같은 이름의 변수를 여러 반복문에서 사용해도 어떠한 영향이 없다.

for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
    Element e = i.next();
    // e와 i로 무언가 한다.
}

지역변수의 범위를 줄일 수 있는 또 다른 방법은 메서드를 작게 유지하고 한 가지 기능에만 집중하면 된다. 여러 가지 기능을 처리하게 되면 다른 기능을 수행하는 코드에서 접근할 가능성이 있다. 메서드를 기능별로 나누면 간단해진다.

지역변수의 범위를 최소화해야 잠재적인 오류를 줄일 수 있다.



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

Prefer for-each loops to traditional for loops

배열과 컬렉션의 요소를 탐색할 때 보통 for 문을 사용했다. 특히 반복자(iterator)나 인덱스 탐색을 위한 루프 변수는 실제로 필요한 원소를 얻기 위한 코드일 뿐이다. 따라서 불필요하며 오히려 잘못 사용한 경우 오류가 발생할 가능성이 높다.

그래서 향상된 for 문(enhanced for statement)인 for-each 문장을 권장한다.

for-each 문은 명료하고 유연하며 버그를 예방해주며 성능 저하도 없다.



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

Know and use the libraries

표준 라이브러리를 사용하면 좋은 점

  • 그 코드를 작성한 전문가의 지식과 경험을 활용할 수 있다.
  • 핵심적인 일과 관련없는 시간 소비가 줄어든다.
  • 따로 노력하지 않아도 성능이 지속해서 개선된다.
  • 기능이 점점 많아진다. 커뮤니티에서의 요구, 논의가 대부분 다음 릴리즈에 기능이 추가된다.
  • 많은 사람들에게 익숙한 코드가 되기 때문에 읽기 좋고, 유지보수하기 좋고, 재활용하기 좋다.

서드 파티 라이브러리

대부분의 표준 라이브러리는 메이저 릴리즈마다 주목할 만한 많은 기능이 추가된다. 자바 개발자라면 java.lang, java.util, java.io와 그 하위 패키지들에는 익숙해지자. 원하는 기능이 없다면 서드 파티(third-party) 라이브러리를 찾아보자.

직접 작성하는 것보다 라이브러리를 쓰는 것이 좋은 경우가 많다.



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

60: Avoid float and double if exact answers are required

floatdouble은 과학과 공학 계산용도로 설계되었다. 넓은 범위의 수를 빠르고 정밀한 ‘근사치’로 계산하도록 설계되었다. 따라서 0.1 또는 10의 음의 거듭 제곱 등을 표현할 수 없기 때문에 금융 관련 계산에는 적합하지 않다.

System.out.println(1.03 - 0.42);
// 예상: 0.61
// 실제: 0.6100000000000001

정확한 계산이 필요할 땐 BigDecimal, int 또는 long을 사용하면 된다.

하지만 BigDecimal에는 primitive 타입보다 사용하기 불편하고 성능적으로 훨씬 느리다. 이때는 int 또는 long 타입을 사용해야 하는데, 값의 크기가 제한되고 소수점을 직접 관리해야 하는 점이 있다.

성능 저하를 크게 신경 쓰지 않는다면 BigDecimal을 사용하고 숫자가 너무 크지 않다면 intlong 타입을 사용하자. 9자리 십진수로 표현할 수 있다면 int 타입, 18자리 십진수로 표현할 수 있다면 long 타입, 18자리가 넘어가면 BigDecimal을 사용하면 된다.

정확한 계산이 필요할 때는 float와 double은 피하자.



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

Prefer primitive types to boxed primitives

자바의 데이터 타입은 int, double, boolean 같은 기본형과 String, List 같은 참조형으로 분류할 수 있다. 그리고 각각의 기본형에 대응되는 참조 타입이 하나씩 있다. 예를 들면 Integer, Double, Boolean 등이다.

자바에는 오토박싱과 오토언박싱이 지원되기 때문에 기본 타입과 박싱된 기본 타입을 크게 구분하지 않고 사용할 수 있지만 차이가 있다.

첫 번째로 기본 타입은 값만 같지만 박싱된 기본 타입은 값과 식별성(identity) 속성을 갖는다. 즉, 두 인스턴스가 값이 같더라도 다르다고 식별된다. 두 번째로 기본 타입은 항상 유효한 값을 갖지만 박싱된 기본 타입은 null을 가질 수 있다. 마지막으로 기본 타입이 박싱된 기본 타입보다 상대적으로 시간, 메모리 사용면에서 더 효율적이다.

문제 사례 1 - 잘못 사용된 비교

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

// 반환되는 값은?
naturalOrder.compare(new Integer(42), new Integer(42));

위의 비교자 코드를 보면 박싱된 기본 타입에 == 연산자를 사용한다. 즉, 객체 참조의 식별성을 검사하기 때문에 결과는 1이 반환된다.

문제 사례 2 - 오류

public class Unbelievable {
    static Integer i;

    public static void main(String[] args) {
      if (i == 42) {
        System.out.println("Hello!");
      }
    }
}

위 코드를 실행하면 “Hello!”를 출력하지 않지만 NullPointerException을 발생시킨다. 다른 참조 타입과 마찬가지로 초기값이 null인 것이 이유다. 기본 타입과 박싱된 기본 타입을 혼용하는 연산에서는 박싱된 기본 타입의 박싱이 해제된다. 해결 방법은 i를 int로 선언해주면 된다.

문제 사례 3 - 성능 저하

private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++>) {
      sum += i;
    }
    return sum;
}

위 코드에서는 박싱과 언박싱이 반복해서 일어나 성능이 느려진다.

그렇다면 박싱된 기본 타입은 언제 사용할까?

컬렉션의 원소, 키, 값에 사용한다. 컬렉션에서는 기본 타입을 담을 수 없기 때문에 박싱된 기본 타입을 사용해야 한다. 또한 매개변수화 타입이나 메서드의 타입 매개변수로는 박싱된 기본 타입을 사용해야 한다. 그리고 리플렉션을 통한 메서드 호출을 할 때도 박싱된 기본 타입을 사용한다.

박싱된 기본 타입을 사용해야 한다면 주의를 기울이자.



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

Avoid strings where other types are more appropriate

문자열은 사용하기 쉽고 편리하다. 그래서 의도한 것과 사용되기도 한다.

문자열은 잘못 사용하면 번거롭고, 느리고, 오류 가능성도 크다.



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

Beware the performance of string concatenation

문자열 연결 연산자로 문자열 n개를 잇는 시간은 에 비례한다. 문자열은 불변이기 때문에 두 문자열을 연결하는 경우에 양쪽의 내용을 모두 복사해야 하기 때문에 성능 저하가 발생한다.

많은 문자열을 연결할 때는 StringBuilder를 사용하자.



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

Refer to objects by their interfaces

적합한 인터페이스만 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하는 것이 좋다.

// 좋은 예시. 인터페이스 타입으로 사용했다.
Set<Fruit> fruitSet = new LinkedHashSet<>();

// 나쁜 예시. 클래스를 타입으로 사용했다.
LinkedHashSet<Fruit> fruitSet = new LinkedHashSet<>();

이렇게 인터페이스 타입을 사용하면 차후에 구현 클래스를 교체할 때 유연하다. 또한 손쉬운 교체로 성능 향상 또는 새로운 기능을 기대할 수 있다. 예를 들어 HashMapEnumMap으로 바꾸면 속도가 향상되고 순회 순서도 키의 순서와 같아지는 장점을 얻을 수 있다. 단, EnumMap은 키가 열거(enum) 타입일 때만 사용 가능한데, 이럴 때는 키 타입과 상관없이 사용할 수 있는 LinkedHashMap을 사용하면 된다.

하지만 구현체가 인터페이스의 일반 규약 외에 특별한 기능을 제공한다면 주의해야 한다. 기존 코드가 기대한 것과 다르게 동작할 수 있다. 예를 들면 LinkedHashSet을 이용하여 순서 정책을 가정하고 사용했다면, HashSet으로 바꾸는 순간 문제가 발생할 것이다.

한편 적절한 인터페이스가 없다면 클래스로 참조해야 한다. StringBigInteger와 같은 값 클래스가 대표적이다. 그리고 클래스 기반으로 작성된 프레임워크가 제공된 객체도 클래스를 참조해야 한다. 예로는 java.io 패키지의 여러 클래스가 있다. 마지막으로 PriorityQueue와 같이 인터페이스에는 없는 특별 메서드를 제공하는 클래스들이 그렇다.

가능하면 인터페이스 타입으로 선언해 사용하자.



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

Prefer interfaces to reflection

자바에서 제공하는 리플렉션을 이용하면 실행 중에 임의의 클래스에 접근할 수 있다.

리플렉션은 자바에서 제공하는 강력한 기능이지만 단점이 있다. 첫 번째로 컴파일타임의 타입 검사가 주는 이점이 없어져 각종 런타임 오류가 발생할 수 있다. 두 번째로 코드가 지저분하고 장황해져 읽기 어려울 수도 있다. 마지막으로 성능이 떨어진다. 필자의 실험에서는 약 11배가 느려졌다고 한다.

리플렉션을 사용할지 말지 고민된다면 대부분 필요 없는 경우이다. 아주 제한된 형태로만 사용해야 리플렉션의 단점을 피할 수 있다. 인스턴스 생성에만 사용하고 만든 인스턴스는 인터페이스나 상위 클래스로 참조하여 사용하는 것이 좋다.

리플렉션은 런타임에 존재하지 않을 수 있는 다른 클래스, 메서드, 필드를 다룰 때 적합하다. 이는 버전이 여러 개 존재하는 외부 패키지를 다룰 때 유용하다. 최소한의 환경은 가장 오래된 버전만을 지원하도록 컴파일하고 이후 버전의 클래스, 메서드는 리플렉션으로 접근하는 방식이다. 이렇게 하려면 접근하려는 새로운 클래스나 메서드가 런타임에 존재하지 않을 수 있다는 것을 감안해야 한다.

리플렉션은 강력하지만 단점도 많다.



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

Use native methods judiciously

자바 네이티브 인터페이스(Java Native Interface)는 자바 프로그램이 네이티브 메서드를 호출하는 기술이다. 여기서 네이티브 메서드란 네이티브 프로그래밍 언어로 작성된 메서드를 말한다.

반드시 네이티브 메서드를 사용해야 한다면 최소한만 사용하자.



아이템 67. 최적화는 신중히 하라

Optimize judiciously

빠른 프로그램보다는 좋은 프로그램을 작성해야 한다.

성능 때문에 견고한 구조를 희생해서는 안된다. 빠른 프로그램보다는 좋은 프로그램이 더 좋다. 설계 단계에서부터 성능을 염두해야 한다. 아키텍처의 결함이 성능을 제한하는 상황이라면 시스템 전체를 다시 작성하지 않고서는 성능 이슈를 해결하기 불가능할 수 있다.

성능을 제한하는 설계를 피해야 한다.

개발이 완료된 후에 변경하기가 가장 어려운 설계 요소는 바로 컴포넌트끼리 혹은 외부 시스템과의 소통 방식이다. 이러한 설계 요소들은 변경이 어렵거나 불가능할 수 있으며 동시에 성능을 제한할 수 있어 염두에 두어야 한다.

API를 설계할 때 성능에 주는 영향을 고려해야 한다.

public 메서드에서 내부 데이터를 변경할 수 있게 한다면 불필요한 방어적 복사를 유발한다. 또 컴포지션으로 해결할 것을 상속 방식으로 설계하게 되면 영원히 상위 클래스에 종속되고 성능까지 물려받는다. 즉, 더 빠른 구현 클래스가 나오더라도 이용하기 어려울 수 있다.

각각의 최적화 시도 전후로 성능을 측정하라

프로파일링 도구는 최적화 노력을 어디에 집중할지 도와준다. 가장 먼저 어떤 알고리즘을 사용했는지 살펴보는 것이 도움이 된다.

최적화는 신중히 해야 한다. 왠만해서는 하지 말자.



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

Adhere to generally accepted naming conventions

표준 명명 규칙을 습관화해야 한다. 자바는 명명 규칙이 잘 정립되어 있다. 다만 오랫동안 따라온 규칙과 충돌한다면 그 규칙을 무조건 따라서는 안된다. 상식이 이끄는 대로 따르자.

표준 명명 규칙을 적절히 따르자.