지역변수의 유효 범위를 최소로 줄이면 코드의 가독성과 유지보수성이 높아지고, 오류가 발생할 가능성은 낮아진다.
지역변수의 범위를 줄이는 가장 강력한 기법은 ‘가장 처음 쓰일 때 선언하기’다. 거의 모든 지역변수는 선언과 동시에 초기화하는 것이 바람직하다.
반복문은 변수 범위를 자연스럽게 최소화해주는 대표적인 구조다.
반복 변수의 값을 반복문이 끝난 뒤에 사용할 필요가 없다면, while문보다는 for문을 사용하는 편이 낫다.
for문이 유리한 이유는 반복 변수의 유효 범위가 for문 블록 안으로 제한되기 때문이다.
이 덕분에 같은 이름의 변수를 여러 반복문에서 사용하더라도 서로 영향을 주지 않으며, 코드의 안정성이 높아진다.
반복자와 인덱스 변수는 모두 코드를 지저분하게 할 뿐 우리에게 진짜 필요한 건 원소들뿐이다. 혹시라도 잘못된 변수를 사용했을 때 컴파일러가 잡아준다는 보장도 없고... 등 문제가 많은데 이걸 for-each문을 사용하면 모두 해결이 된다!
for (Element e : elements) {
... // e로 무언가를 한다.
}
하지만 for-each문을 사용할 수 없는 상황 세 가지 존재한다!
1) 파괴적인 필터링 - 컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 반복자의 remove 메서드를 호출해야 한다. 자바 8부터는 Collection의 removeIf 메서드를 사용해 컬렉션을 명시적으로 순회하는 일을 피할 수 있다.
2) 변형 - 리스트나 배열을 순회하면서 그 원소의 값 일부 혹은 전체를 교체해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야한다.
3) 병렬 반복 - 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 한다.
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
문제점
1) n이 그리 크지 않은 2의 제곱수라면 얼마 지나지 않아 같은 수열이 반복된다.
2) n이 2의 제곱수가 아니라면 몇몇 숫자가 평균적으로 더 자주 반환된다.
3) 지정한 범위 '바깥'의 수가 종종 튀어나올 수 있다. rnd.nextInt()가 반환한 값을 Math.abs를 이용해 음수가 아닌 정수로 매핑하기 때문이다.
표준 라이브러리를 사용하면 좋은 점
java.util.concurrent를 알아두면.. 동시성 기능에 좋다?
하여튼간 자바 표준 라이브러리를 잘 이용해보자. 근데 원하는 기능이 거기 없다면 다음 선택지는 고품질의 서드파티 라이브러리에서 찾으면 되고.... (ex. 구글의 구아바 라이브러리) 그것도 없으면 직접 구현해야한다.
float와 double은 넓은 범위의 수를 빠르게 정밀한 '근사치'로 계산하도록 설계되어 정확한 결과가 필요할 때는 사용하면 안 된다. 특히 금융 관련 계산과는 맞지 않는다!!
그래서 금융 계산에는 BigDecimal, int, long을 사용해야한다.
하지만 BigDecimal에는 단점이 두가지가 있다. 기본 타입보다 쓰기가 훨씬 불편하고 훨씬 느리다.
그래서 대안으로 int/long을 쓸 수도 있다.
이 경우에는 다룰 수 있는 값의 크기가 제한되고 소수점을 직접 관리해야 한다.
자바의 데이터 타입은 크게 두 가지로 나눌 수 있다.
1) int, double, boolean 같은 기본 타입
2) String, List 같은 참조 타입
int, double, boolean에 대응하는 박싱된 기본 타입은 Integer, Double, Boolean이다.
기본 타입과 박싱된 기본 타입의 주된 차이
1. 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성이란 속성을 갖는다.
2. 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 유효하지 않은 값, 즉 null을 가질 수 있다.
3. 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.
박싱된 기본 타입 사용 시 발생할 수 있는 문제
해결 방법
기본 타입과 박싱된 기본 타입을 혼용한 연산에서는 대부분 자동 언박싱이 일어난다.
하지만 실수로 지역변수를 박싱된 타입으로 선언하면, 눈치채지 못한 채 박싱·언박싱이 반복되어 성능 저하가 발생할 수 있다.
박싱된 기본 타입을 사용해야 하는 경우
ThreadLocal<Integer> 등)문자열은 만능 타입처럼 보이지만, 다음과 같은 경우에는 적절하지 않다.
1. 다른 값 타입을 문자열로 대신하는 경우
수치 데이터는 숫자 타입으로, 예/아니오 값은 boolean이나 열거 타입으로 표현하는 것이 좋다.
2. 열거 타입을 문자열로 표현하는 경우
열거 타입을 사용하면 가독성과 타입 안정성이 크게 향상된다.
3. 혼합 타입을 문자열로 표현하는 경우
String compoundKey = className + "#" + i.next();
이 방식은 문자열 파싱이 필요해 번거롭고 오류 가능성이 크며,
equals, toString, compareTo 같은 메서드를 제대로 활용할 수 없다.
전용 클래스를 만드는 편이 훨씬 낫다.
문자열 연결 연산자 +는 간편하지만, 문자열을 많이 연결하는 경우 성능 문제가 심각해진다.
문자열 n개를 +로 연결하는 시간은 n²에 비례한다.
성능을 고려해야 한다면 StringBuilder를 사용하자.
StringBuilder는 문자열 개수에 따라 선형적으로 성능이 증가한다.
public String statement2() {
StringBuilder b = new StringBuilder(numItems() * LINEWIDTH);
for (int i = 0; i < numItems(); i++)
b.append(lineForItem(i));
return b.toString();
}
저자의 실험 기준으로 StringBuilder를 사용한 코드가 약 6.5배 더 빨랐다고 한다.
성능을 신경써야 한다면 많은 문자열을 연결할 때는 문자열 연결 연산(+)를 피하자.
대신 StringBuilder의 append 메서드를 사용하라.
문자 배열을 사용하거나, 문자열을 (연결하지 않고) 하나씩 처리하는 방법도 있다.
적합한 인터페이스만 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라.
인터페이스 사용 장점
인터페이스 대신 클래스 타입을 사용해도 되는 경우
적합한 인터페이스가 없다면 당연히 클래스로 참조해야 한다.
String과 BigInteger 같은 값 클래스
값 클래스를 여러 가지로 구현될 수 있다고 생각하고 설계하는 일은 거의 없다.
값 클래스는 매개변수, 변수, 필드, 반환 타입으로 사용해도 무방하다.
클래스 기반으로 작성된 프레임워크가 제공하는 객체들
이런 경우라도 특정 구현 클래스보다는 (보통은 추상 클래스인) 기반 클래스를 사용해 참조하는게 좋다. OutStream 등 java.io 패키지의 여러 클래스가 이 부류에 속한다.
인터페이스에는 없는 특별한 메서드를 제공하는 클래스
예시) PriorityQueue 클래스는 Queue 인터페이스에는 없는 comparator 메서드를 제공한다.
이러한 클래스 타입을 직접 사용하는 경우, 클래스 타입에서 제공하는 추가 메서드를 꼭 사용해야 하는 경우로 최소화해야 하며, 절대 남발하지 말아야 한다.
java.lang.reflect API
네이티브 메서드
네이티브 메서드는 주로 다음과 같은 용도로 사용된다.
그러나 네이티브 메서드를 성능을 개선할 목적으로 사용하는 것은 권장하지 않는다.
네이티브 메서드의 단점
1. 네이티브 언어가 안전하지 않으므로 네이티브 메서드를 사용하는 애플리케이션도 메모리 훼손 오류로부터 더 이상 안전하지 않다.
2. 네이티브 언어는 자바보다 플랫폼을 많이 타기 때문에 이식성이 낮다.
3. 디버깅이 더 어렵다.
4. 주의하지 않으면 속도가 오히려 느려질 수도 있다.
5. 가비지 컬렉터가 네이티브 메모리는 자동 회수하지 못한다. 심지어 추적조차 할 수 없다.
6. 네이티브 메서드와 자바 코드 사이의 접착 코드(glue code)를 작성해야 한다. 이는 작업하기 귀찮을 뿐만 아니라 가독성도 떨어진다.
모든 사람이 마음 깊이 새겨야 할 최적화 격언 세 개를 소개 한다.
(맹목적인 어리석음을 포함해) 그 어떤 핑계보다 효율성이라는 이름 아래 행해진 컴퓨팅 죄악이 더 많다. (심지어 효율을 높이지도 못하면서) - 윌리엄 울프
(전체의 97% 정도인) 자그마한 효율성은 모두 잊자. 섣부른 최적화가 만악의 근원이다. - 도널드 크누스
최적화를 할 때는 다음 두 규칙을 따르라. 첫 번째, 하지 마라. 두 번째, (전문가 한정) 아직 하지 마라. 다시 말해, 완전히 명백하고 최적화되지 않은 해법을 찾을 때까지는 하지 마라.
M. A 잭슨
최적화는 좋은 결과보다는 해로운 결과로 이어지기 쉽고, 섣불리 진행하면 특히 더 그렇다. 빠르지도 않고 제대로 동작하지도 않으면서 수정하기는 어려운 소프트웨어를 탄생시키는 것이다.
자바 플랫폼은 명명 규칙이 잘 정립되어 있으며, 그중 많은 것이 자바 언어 명세[JLS, 6.1]에 기술되어 있다. 자바의 명명 규칙은 크게 철자 규칙과 문법 규칙, 두 범주로 나뉜다.
| 대상 | 명명 규칙 | 예시 / 설명 |
|---|---|---|
| 패키지 / 모듈 | 점(.)으로 계층 구분, 모두 소문자 | com.google.common, org.eff |
| 조직 외부 공개용 패키지는 도메인 역순 | edu.cmu, com.google | |
표준 라이브러리는 java, javax로 시작 | java.util, javax.servlet | |
| 요소는 짧은 단어(보통 8글자 이하) | utilities → util, awt | |
| 클래스 / 인터페이스 | 각 단어의 첫 글자를 대문자로 | Thread, PriorityQueue |
| 불필요한 축약은 피함 | HttpUrl (권장), HTTPURL (비권장) | |
| 메서드 / 필드 | 첫 글자 소문자, 나머지는 클래스와 동일 | remove, ensureCapacity |
| 상수 필드 | 모두 대문자 + 밑줄 | VALUES, NEGATIVE_INFINITY |
static final + 불변 값 | 기본 타입 또는 불변 참조 | |
| 지역변수 | 메서드/필드와 유사, 약어 허용 | 문맥상 의미 유추 가능 |
| 매개변수 | 지역변수이지만 더 신중하게 작성 | 문서에 노출되기 때문 |
| 타입 매개변수 | 보통 한 글자 | T, E, K, V, R, X |
| 여러 개일 경우 순서 사용 | T, U, V, T1, T2 |
| 대상 | 규칙 | 예시 |
|---|---|---|
| 패키지 | 별도 문법 규칙 없음 | — |
| 클래스 (인스턴스 생성 가능) | 단수 명사 또는 명사구 | Thread, PriorityQueue |
| 클래스 (인스턴스 생성 불가) | 복수형 명사 | Collections, Collectors |
| 인터페이스 | 클래스와 동일하거나 -able, -ible 형용사 | Runnable, Iterable |
| 애너테이션 | 명확한 규칙 없음 | 명사, 동사, 형용사 등 |
| 메서드 (동작 수행) | 동사 또는 동사구 | add, remove, calculateTotal |
| 메서드 (boolean 반환) | is, 드물게 has로 시작 | isEmpty, isEnabled, hasNext |
| 메서드 (속성 반환) | 명사 / 명사구 또는 get | size, hashCode, getTime |
| 타입 변환 메서드 | toType 형태 | toString, toArray |
| 뷰 반환 메서드 | asType 형태 | asList |
| 기본 타입 값 반환 | typeValue 형태 | intValue |
| 정적 팩터리 | 관례적인 이름 사용 | from, of, valueOf, getInstance |
| 필드 (boolean) | 접근자에서 접두어 제거한 이름 | initialized, composite |
| 필드 (그 외) | 명사 또는 명사구 | size, count |
| 지역변수 | 필드와 유사하나 더 자유로움 | 짧고 명확하면 OK |
명명 규칙은 단순한 스타일 문제가 아니라, 가독성·일관성·API 품질을 좌우하는 설계 요소다.
자바의 관례를 따르는 것만으로도 코드의 신뢰도가 크게 올라간다.