57. 지역변수의 범위를 최소화하자.
지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고 오류 가능성은 낮아진다.
- 지역변수 범위를 줄이는 가장 강력한 기법은
가장 처음 쓰일 때 선언하기
이다. - 거의 모든 지역 변수는 선언과 동시에 초기화해야 한다.
- 메서드를 작게 유지하고 한 가지 기능에 집중하는 것
58. 전통적인 for문보다는 for-each문을 사용하자.
for(Iterator<Element> i = c.iterator(); i.hasNext();)
for(int i = 0; i < a.length; ++i)
위 코드는 while문보다는 낫지만 가장 좋은 방법은 아니다.
반복자, 인덱스 변수는 코드를 지저분하게 할 뿐 진짜 필요한 건 원소들 뿐이다.
for-each문을 사용하면 문제점들을 해결할 수 있다.
for(Element e : elements)
for-each를 사용할 수 없는 경우
- 파괴적인 필터링(destructive filtering)
- 컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 반복자의 remove 메서드를 호출해야 한다.
- Java 8 부터 Collection의 removeIf 메서드를 사용해 컬렉션을 명시적으로 순회하지 않을 수 있다.
- 변형(transforming)
- 리스트나 배열을 순회하면서 원소 값 일부 혹은 전체를 교체하는 경우에는 인덱스를 사용해야 한다.
- 병렬 반복(parallel iteration)
- 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 한다.
61. 박싱된 기본 타입보다는 기본 타입을 사용하자.
각각 기본 타입에는 대응하는 참조 타입이 하나씩 있으며, 이를 박싱된 기본 타입이라고 한다.
int, double, boolean
→ Integer, Double, Boolean
오토박싱과 오토언박싱 덕분에 크게 구분하지 않고 사용할 수 있지만 그렇다고 차이가 없는 것은 아니다.
분명한 차이가 있으니 주의해서 선택해야 한다.
Auto Boxing vs Auto UnBoxing
Integer i = new Integer(5);
int j = i; // Auto UnBoxing
int k = 10;
Integer l = k; // Auto Boxing
기본타입 vs 참조타입
- 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성(identity)란 속성을 갖는다.
- 기본 타입은 흔히 말하는 리터럴(Literal)이다.
리터럴(Literal)이란?
소스 코드의 고정된 값을 의미하는 용어이다.
상수(Constants) 또는 변수(Variable)에 할당 할 수 있는 값 자체를 일컫는 용어이다. - 기본 타입의 값은 JVM내의 Stack 메모리에 저장된다.
- 참조 타입의 값은 객체 내의 상수에 저장된다. 따라서 JVM 내의 Heap 메모리에 저장된다.
- 따라서 박싱된 타입의 객체는 같은 값이라 하더라도 다른 객체일 경우에는 다르다고 식별이 가능하다.
- 기본 타입은 흔히 말하는 리터럴(Literal)이다.
- 기본 타입의 값은 언제나 유효한 값을 가지고 있으나 박싱된 기본타입은 유효하지 않을 수 있다.
- 기본타입의 값은 Java의 경우 초기화 되지 않으면 0으로 초기화 된다.
- 박싱된 기본 타입의 경우에는 초기화 되지 않으면 null이 될 수 있다.
- 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.
- 박싱된 타입은 heap에 객체를 생성하기 때문에 메모리 사용면에서 더 안좋다.
- 기본타입은 변수에 값이 있는 반면, 박싱된 기본 타입은 변수의 객체참조 정보를 바탕으로 heap에서 찾으므로
시간적인 측면에서 기본타입보다 값에 접근하는 시간이 더 들게 된다.
박싱된 기본타입에 ==
연산자를 이용하여 비교하면 예상과는 다른 결과가 나올 수 있다.
- 잘못된 경우
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
- 올바른 구현 방법
Comparator<Integer> naturalOrder = (i, j) -> {
int unBoxi = i;
int unBoxj = j;
return (unBoxi < unBoxj) ? -1 : (unBoxi == unBoxj ? 0 : 1);
}
갑자기 발생하는 NullPointerException
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
if (i == 42) {
System.out.println("믿을 수 없군");
}
}
}
i == 42
를 검사하는 과정에서 NullPointerException을 던진다.
i는 Auto UnBoxing을 수행한다.
하지만 i는 null이기 때문에 Auto UnBoxing을 수행하는 과정에서 NullPointerException을 발생시키게 된다.
의도하지 않은 Auto Boxing으로 인한 성능저하
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0L; i < Integer.MAX_VALUE; ++i) {
sum += i; // sum이 UnBoxing되어 i와 연산되고 연산 후에 AutoBoxing되어 Long타입으로 변환된다.
}
System.out.println(sum);
}
위 코드는 sum을 Long으로 선언하였기 때문에 엄청난 성능상 안좋은 코드이다.
sum += i;
를 수행하는 과정에서 sum이 long타입으로 UnBoxing되고
sum + i
연산이 이루어진다음 Long타입으로 AutoBoxing되기 때문이다.
박싱된 기본타입은 언제 사용해야 하는가?
- 컬렉션의 원소, 키, 값으로 쓴다.
- 컬렉션은 기본타입을 담을 수 없으므로 어쩔 수 없이 박싱된 기본타입을 사용해야 한다.
- 제네릭(Generics) 타입을 이용하는 경우에도 박싱된 기본타입을 사용한다.
- 제네릭 타입에서는 int, double과 같은 기본타입을 지원하지 않기 때문이다.
- 리플렉션(Reflection)을 통해 메서드를 호출할 때에도 박싱된 기본타입을 사용한다.
64. 객체는 인터페이스를 사용해 참조하자.
예전에 매개변수 타입으로 클래스가 아니라 인터페이스를 사용하라고 했었다.
이는 객체는 클래스가 아닌 인터페이스로 참조하라 까지 확장할 수 있다.
적합한 인터페이스만 있다면 매개변수뿐만 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하면 된다.
객체의 실제 클래스를 사용해야 할 상황은 오직 생성자로 생성할 때뿐이다.
// 좋은 예 - 인터페이스를 타입으로 사용
Set<Son> sonSet = new LinkedHashSet<>();
// 나쁜 예 - 클래스를 타입으로 사용
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
인터페이스 타입 장점
- 인터페이스 타입을 사용하면 클라이언트 코드를 수정하지 않고 참조 객체를 변경할 수 있다.
- 다른 타입의 객체를 사용하더라도 컴파일 에러 / 런타임 에러에 대한 걱정을 하지 않아도 된다.
인터페이스 타입의 단점
- 인터페이스 타입에 선언된 메서드를 구현한 메서드만 사용이 가능하다.
- 특정 구현체의 내부 메서드를 사용할 수 없다.
클래스를 참조해야 하는 경우
- 값 타입에는 클래스를 참조하자.
- String, Integer, Long과 같은 값 타입에 대해서는 인터페이스를 사용할 수 없으니 클래스를 참조해야 한다.
- Integer, Long과 같은 타입을 사용할 때는 Number와 같은 상위 타입을 사용하면 안 된다.
형변환이 발생할 때 특정 데이터가 절삭되어 다른 결과가 발생할 수 있기 때문이다. - 이런 경우 인터페이스나 상위 타입 보다는 본래의 클래스로 참조하는 것이 좋다.
- 인터페이스에는 없는 메서드를 사용할 때 클래스를 참조하자.
- PriorityQueue 클래스에는 Queue 인터페이스에는 없는 comparator 메서드를 제공한다.
- 클래스 타입을 직접 사용하는 경우에는 추가 메서드를 사용해야 하는 경우로 최소화 하는 것이 좋다.
리플렉션보다는 인터페이스를 사용하자.
리플렉션 기능(java.lang.reflect)을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다.
Class 객체가 주어지면 클래스 정보를 통해 아래와 같은 인스턴스를 가져올 수 있다.
- Constructor
- 생성자 시그니처를 가져올 수 있다.
- 생성자 인스턴스를 통해 객체를 생성할 수 있다.
- Method
- Method 시그니처를 가져올 수 있다.
- Method 인스턴스를 통해 Method를 실행시킬 수 있다. (Method.invoke)
- Field
- 필드 타입, 멤버 필드 이름 등을 가져올 수 있다.
리플렉션 단점
-
리플렉션을 이용하면 컴파일 당시에 존재하지 않던 클래스도 이용할 수 있다.
예) 외부 라이브러리의 클래스를 리플렉션으로 인스턴스를 생성한다. -
컴파일 타입 검사가 주는 이점을 누릴 수 없다.
-
리플렉션을 이용하면 코드가 지저분하고 장황해진다.
- 성능이 떨어진다.
- 리플렉션을 통한 메서드 호출은 일반 메서드 호출보다 훨씬 느리다.
- 리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피할 수 있다.
- 컴파일 타임에 이용할 수 없는 클래스를 사용해야만 하는 프로그램은 컴파일 타임이라도 인터페이스나 상위 클래스를 이용할 수는 있을 것이다.
- 리플렉션은 인스턴스 생성에만 쓰고 이렇게 만든 인터페이스나 상위 클래스로 참조해 사용해야 한다.
리플렉션은 무조건 쓰지 말아야 하는 걸까?
-
Spring MVC, Serialize/Deserialize, BeanUtils.copyProperties등 실무에서 사용하는 코드에 리플렉션이 적용된 예는 굉장히 많다.
-
단점이 많다고는 하지만 공통적인 기능을 설계하거나, 재사용 가능한 코드를 설계할 경우에는 오히려 리플렉션이 적합할 수 있다.
-
그렇기 때문에 Java 1.3 이후부터 리플렉션에 대한 성능향상을 발전시켜왔다고 한다.
-
이러한 발전으로 리플렉션은 우려할 만큼 성능이 떨어지지는 않는다고 한다.
-
리플렉션을 남발하는 것이 아닌 필요한 상황에 적시적소에 사용한다면 오히려 서비스 개발을 더 단순화 시킬수 있다.
66. 네이티브 메서드는 신중히 사용하자.
자바 네이티브 인터페이스는 자바 프로그램이 네이티브 메서드를 호출하는 기술이다.
여기서 네이티브 메서드란 C나 C++ 같은 네이티브 프로그래밍 언어로 작성한 메서드를 말한다.
네이티브 메서드 주요 쓰임
- 레지스트리 같은 플랫폼 특화 기능을 사용한다.
- 네이티브 코드로 작성된 기존 라이브러리를 사용한다. (레거시 라이브러리)
- 성능 개선을 위해 사용한다.
성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 거의 권장하지 않는다.
자바 초기 시절이라면 이야기가 다르지만 JVM은 그동안 엄청난 속도로 발전 했다.
지금의 자바는 다른 플랫폼에 견줄만한 성능을 보인다.
네이티브 메서드의 단점
네이티브 메서드는 심각한 단점이 있다.
네이티브 언어가 안전하지 않아 사용하는 애플리케이션도 메머리 훼손 오류로부터 더 이상 안전하지 않다는 것이다.
디버깅도 어렵고 주의하지 않으면 속도가 오히려 느려질 수도 있다.
가비지 컬렉터가 네이티브 메모리는 자동 회수하지 못하며 추적조차 할 수 없다.
67. 최적화는 신중히 하자.
최적화는 좋은 결과보다는 해로운 결과로 이어지기 쉽고, 섣불리 진행하면 특히 더 그렇다.
성능때문에 견고한 구조를 희생시키지 말자.
빠른 프로그램보다는 좋은 프로그램을 작성하라 좋은 프로그램이지만 원하는 성능이 나오지 않는다면 그 아키텍쳐 자체가 최적화할 수 있는 길을 안내해줄 것이다.
프로그램을 완성할 때까지 성능 문제를 무시하라는 뜻이 아니다.
구현상의 문제는 나중에 최적화해 해결할 수 있지만, 아키텍쳐의 결함이 성능을 제한하는 상황이라면 시스템 전체를 다시 작성하지 않고는 해결하기 불가능할 수 있다. 따라서 설계단계에서 성능을 반드시 염두에 두어야 한다.
성능을 제한하는 설계를 피하라.
완성 후 변경하기가 가장 어려운 설계 요소는 바로 컴포넌트끼리, 혹은 외부 시스템과의 소통 방식이다.
API를 설계할 때 성능에 주는 영향을 고려하라.
public 타입을 가변으로 만들면, 불필요한 방어적 복사를 유발할 수도 있다.
합성으로 해결할 수 있는 문제를 상속 방식으로 해결하면 상위클래스에 영원히 종속되어 그 성능 제약까지 물려받게 된다.
다행히 잘 설계된 api는 성능도 좋은 게 보통이다. 그러니 성능을 위해 API를 애곡하는 건 매우 안 좋은 생각이다.