← 학습기록 (Java · 이펙티브 자바 3E)

9장 - 일반적인 프로그래밍 원칙

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

이번 아이템은 기본적으로 “클래스와 멤버의 접근 권한을 최소화하라”고 한 아이템 15의 취지와 비슷하다.

지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지 보수성이 높아지고 오류 가능성은 낮아진다.

지역변수의 범위를 줄이는 가장 강력한 기법은 역시 ‘가장 처음 쓰일 때 선언하기’다.

사용하려면 멀었는데, 미리 선언부터 해두면 코드가 어수선해져 가독성이 떨어진다.

변수를 실제로 사용하는 시점엔 타입과 초깃값이 기억나지 않을 수도 있다.

지역변수를 생각 없이 선언하다 보면 변수가 쓰이는 범위보다 너무 앞서 선언하거나, 다 쓴 뒤에도 여전히 살아 있게 되기 쉽다.

지역변수의 범위는 선언된 지점부터 그 지점을 포함한 블록이 끝날 때까지이므로, 실제 사용하는 블록 바깥에 선언된 변수는 그 블록이 끝난 뒤까지 살아 있게 된다.

또한 거의 모든 지역변수는 선언과 동시에 초기화해야 한다.

초기화에 필요한 정보가 충분하지 않다면 충분해질 때까지 선언을 미뤄야 한다.

try-catch 문은 이 규칙에서 예외다. 변수를 초기화하는 표현식에서 검사 예외를 던질 가능성이 있다면 try 블록 안에서 초기화해야 한다. (그렇지 않으면 예외가 블록을 넘어 메서드에까지 전파된다)

지역변수 범위를 최소화하는 마지막 방법은 메서드를 작게 유기하고 한 가지 기능에 집중하는 것이다.

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

아이템 45에서 이야기했듯, 스트림이 제격인 작업이 있고 반복이 제격인 작업이 있다.

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

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

그리고 다음은 전통적인 for 문으로 배열을 순회하는 코드다

for(int i = 0; i < a.length; i++) {
	... // a[i] 무언가 한다
}

이 관용구들은 while 문보다는 낫지만 가장 좋은 방법은 아니다.

반복자와 인덱스 변수는 모두 코드를 지저분하게 할 뿐 우리에게 진짜 필요한건 원소들뿐이다.

더군다나 이처럼 쓰이는 요소 종류가 늘어나면 오류가 생길 가능성이 높아진다.

1회 반복에서 반복자는 세 번 등장하며, 인덱스는 네 번이나 등장하여 변수를 잘못 사용할 틈새가 넓어진다.

혹시라도 잘못된 변수를 사용했을 때 컴파일러가 잡아주리라는 보장도 없다.

마지막으로, 컬렉션이나 배열이냐에 따라 코드 형태가 상당히 달라지므로 주의해야 한다.

이상의 문제는 for-each문을 사용하면 모두 해결된다.

참고로 for-each문의 정식 이름은 향상된 for문 이다.

반복자와 인덱스 변수를 사용하지 않으니 코드가 깔끔해지고 오류가 날 일도 없다.

for( Element e : elements) {
	... // e로 무언가 한다
}

하지만 for-each 문을 사용할 수 없는 상황이 세 가지 존재한다.

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

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

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

표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 여러분보다 앞서 사용한 다른 프로그램들의 경험을 활용할 수 있다.

표준 라이브러리를 쓰는 두 번째 이점은 핵심적인 일과 크게 관련 없는 문제를 해결하느라 시간을 허비하지 않아도 된다는 것이다.

세 번째 이점은 따로 노력하지 않아도 성능이 지속해서 개선된다는 점이다.

네 번째 이점은 기능이 점점 많아진다는 것이다.

마지막 이점은 여러분이 작성한 코드가 많은 사람에게 낯익은 코드가 된다는 것이다.

다른 개발자들이 더 읽기 좋고, 유지보수하기 좋고, 재활용하기 쉬운 코드가 된다.

라이브러리가 너무 방대하여 모든 API 문서를 공부하기는 벅차겠지만 자바 프로그래머라면 적어도 java.lang. java.util, java.io와 그 하위 패키지들에는 익숙해져야 한다.

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

float과 double 타입은 과학과 공학 계산용으로 설계되었다. 이진 부동소수점 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 ‘근사치’로 계산하도록 세심하게 설계되었다.

따라서 정확한 결과가 필요할 때는 사용하면 안 된다.

float과 double 타입은 특히 금융 관련 계산과는 맞지 않는다.

예를 들어 주머니에 1.03 달러가 있었는데 그중 42센트를 썼다고 해보자.

남은 돈은 얼마인가? 다음은 이 문제의 답을 구하기 위해 ‘어설프게’ 작성한 코드다

System.out.println(1.03 - 0.42);

안타깝게도 이 코드는 0.61000000…01을 출력한다.

이는 특수한 사례도 아니다. 이번엔 주머니에 1달러가 있었는데 10센트짜리 사탕 9개를 샀다고 해보자. 얼마가 남았을까?

System.out.println(1.00 - 9 * 0.10);

이 코드는 0.09999999…98을 출력한다.

결괏값을 출력하기 전에 반올림하면 해결되리라 생각할지 모르지만, 반올림을 해도 틀린 값이 나올 수 있다.

이 문제를 올바로 해결하려면 어떻게 해야 할까?

금융 계산에는 BigDecimal, int 혹은 long을 사용해야 한다.

BigDecimal에는 단점이 두 가지 있다. 기본 타입보다 훨씬 불편하고, 훨씬 느리다.

단발성 계산이라면 느리다는 문제는 무시할 수 있지만, 쓰기 불편하다는 점은 못내 아쉬울 것이다.

BigDecimal의 대안으로 int, long 타입을 쓸 수도 있다. 그럴 경우 다룰 수 있는 값의 크기가 제한되고, 소수점을 직접 관리해야 한다.

위 예에서는 모든 계산을 달러 대신 센트로 수행하면 이 문제가 해결된다.

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

자바의 데이터 타입은 크게 두 가지로 나눌 수 있다.

바로 int, boolean 같은 기본 타입과, String, List와 같은 찹조 타입이다.

그리고 각각의 기본 타입에는 대응하는 참조 타입이 하나씩 있으며, 이를 박싱된 기본 타입이라고 한다.

예컨대 Integer, Boolean 같은 타입이다.

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

첫 번째, 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성이란 속성을 갖는다.

달리 말하면 박싱된 기본 타입의 두 인스턴스는 값이 같아도 서로 다르다고 식별될 수 있다.

두 번째, 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 null을 가질 수 있다.

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

이상의 세 가지 차이 때문에 주의하지 않고 사용하면 진짜로 문제가 발생할 수 있다.

박싱된 기본 타입에 == 연산자를 사용하면 오류가 일어난다.

기본 타입과 박싱된 기본 타입을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 풀린다.

그리고 null 참조를 언박싱하면 NPE가 발생한다.

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

문자열은 텍스트를 표현하도록 설계되었고, 그 일을 아주 멋지게 해낸다.

그런데 문자열은 워낙 흔하고 자바가 또 잘 지원해주어 원래 의도하지 않은 용도로도 쓰이는 경향이 있다.

이번 아이템에서는 문자열을 쓰지 않아야 할 사례를 다룬다.

문자열은 다른 값 타입을 대신하기에 적합하지 않다

입력받을 데이터가 진짜 문자열일때만 그렇게 하는 게 좋다.

만약 수치형 데이터라면 int, float 등을 사용하고 예.아니요 질문의 답이라면 적당한 열거 타입이나 boolean을 사용하는게 좋다.

문자열은 열거 타입을 대신하기에 적합하지 않다.

문자열은 혼합 타입을 대신하기에 적합하지 않다.

문자열은 권한을 표현하기에 적합하지 않다.

권한(capacity)를 문자열로 표현하는 경우가 종종 있다. 문자열을 키로 사용하게 되면, 만약 같은 문자열을 키로 사용하게 될 때 구분할 수 있는 방법이 없다.

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

문자열을 “+”로 연결하는 기능이 있다.

한 줄짜리 출력값 혹은 작고 크기가 고정된 객체의 문자열 표현을 만들 때라면 괜찮지만, 본격적으로 사용하기 시작하면 성능 저하를 감내하기 어렵다.

문자열 연결 연산자로 문자열 n개를 잇는 시간은 n^2에 비례한다.

문자열은 불변이라서 두 문자열을 연결할 경우 양쪽의 내용을 모두 복사해야 하므로 성능 저하는 피할 수 없는 결과다.

대신 StringBuilder의 append 메서드를 사용하라

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

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

구현 타입을 유연하게 바꿀 수 있다.

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

리플렉션 기능을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다.

Class 객체가 주어지면 그 클래스의 생성자, 메서드, 필드에 해당하는 Constructor, Method, Field 인스턴스를 가져올 수 있고, 이어서 이 인스턴스들로는 그 클래스의 멤버 이름, 필드 타입, 메서드 시그니처 등을 가져올 수 있다.

나아가 Constructor, Method, Field 인스턴스를 이용해 각각에 연결된 실제 생성자, 메서드, 필드를 조작할 수도 있다.

리플렉션을 이용하면 컴파일 당시에 존재하지 않던 클래스도 이용할 수 있는데, 물론 단점이 있다

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

리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피하고 이점만 취할 수 있다.

컴파일타임에 이용할 수 없는 클래스를 사용해야만 하는 프로그램은 비록 컴파일타임이라도 적절한 인터페이스나 상위 클래스를 이용할 수는 있을 것이다.

다행히 이런 경우라면 리플렉션은 인스턴스 생성에만 쓰고, 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.

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

자바 네이티브 인터페이스는 자바 프로그램이 네이티브 메서드를 호출하는 기능이다.

여기서 네이티브 메서드란 C나 C++같은 네이티브 프로그래밍 언어로 작성한 메서드를 말한다.

전통적으로 네이티브 메서드의 주요 쓰임은 다음 세 가지다.

첫 번째, 레지스트리같은 플랫폼 특화 기능을 사용한다.

두 번째, 네이티브 코드로 작성된 기존 라이브러리를 사용한다.

세 번째, 성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성한다.

성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 거의 권장하지 않는다.

자바 초기 시절 (자바 3 전)이라면 이야기가 다르지만, JVM은 그동안 엄청난 속도로 발전해왔다.

대부분 작업에서 지금의 자바는 다른 플랫폼에 견줄만한 성능을 보인다.

네이티브 메서드에는 심각한 단점이 있다.

네이티브 언어가 안전하지 않으므로 네이티브 메서드를 사용하는 애플리케이션도 메모리 훼손 오류로부터 더 이상 안전하지 않다.

네이티브 언어는 자바보다 플랫폼을 많이 타서 이식성도 낮다.

디버깅도 어렵다.

주의하지 않으면 속도가 오히려 느려질 수도 있다.

가비지 컬렉터가 네이티브 메모리는 자동 회수하지 못하고, 심지어 추적조차 할 수 없다.

자바 코드와 네이티브 코드의 경계를 넘나들 때마다 비용도 추가된다.

마지막으로 자바 코드와 네이티브 코드 사이에 접착 코드를 작성해야 하는데, 귀찮고 가독성도 떨어진다.

67. 최적화는 신중히 하라

모든 사람이 마음 깊이 새겨야 할 최적화 격언 세 개를 소개한다.

그 어떤 핑계보다 효율성이라는 이름 아래 행해진 컴퓨팅 죄악이 더 많다.(심지어 효율을 높이지도 못하면서). - 윌리엄 울프

자그마한 효율성은 모두 잊자. 섣부른 최적화가 만악의 근원이다. - 도널드 크루스

최적화를 할 때는 다음 두 규칙을 따르라 첫 번째, 하지 마라. 두 번째, (전문가 한정) 아직 하지 마라. 다시 말해 완전히 명백하고 최적화되지 않은 해법을 찾을 때까지는 하지 마라 - M.A 잭슨

이 격언들은 자바가 탄생하기 20년 전에 나온 것으로, 최적화의 어두운 진실을 이야기해준다.

최적화는 좋은 결과보다는 해로운 결과로 이어지기 쉽고, 섣불리 진행하면 특히 더 그렇다.

빠르지도 않고 제대로 동작하지도 않으면서 수정하기는 어려운 소프트웨어를 탄생시키는 것이다.

성능 때문에 견고한 구조를 희생하지 말자. 빠른 프로그램보다는 좋은 프로그램을 작성하라.

좋은 프로그램이지만 원하는 성능이 나오지 않는다면 그 아키텍쳐 자체가 최적화할 수 있는 길을 안내해줄 것이다.

성능을 제한하는 설계를 피하라.

API를 설계할 때 성능에 주는 영향을 고려하라

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

자바 플랫폼은 명명 규칙이 잘 정립되어 있으며, 그중 많은 것이 자바 언어 명세에 기술되어 있다.

자바의 명명 규칙은 크게 철자와 문법, 두 범주로 나뉜다.

패키지와 모듈 이름은 각 요소를 점으로 구분하여 계층적으로 짓는다.

조직 밖에서도 사용될 패키지라면 조직의 인터넷 도메인 이름을 역순으로 사용한다.

예컨대 edu.cmu, com.google, org.eff 식이다.

각 요소는 일반적으로 8자 이하의 짧은 단어로 한다. utilities 보다는 util처럼 의미가 통하는 약어를 사용한다.

클래스와 인터페이스의 이름은 하나 이상의 단어로 이뤄지며, 각 단어는 대문자로 시작한다.

널리 통용되는 줄임말을 제외하고는 단어를 줄여 쓰지 않도록 한다.

메서드와 필드 이름은 첫 글자를 소문자로 쓴다는 점만 배면 클래스와 규칙이 같다

단 상수 필드는 예외다. 모두 대문자로 쓰이며 각 단어 사이는 밑줄로 구분한다. (static final인 변수를 말함)