BOOK 6 - 이펙티브 자바(9)
9장 일반적인 프로그래밍 원칙
지역변수의 범위를 최소화하라
- 지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고, 오류 가능성은 낮아진다.
지역변수 범위를 줄이는 방법
- 지역변수는 가장 처음 쓰일 때 선언한다.
- 지역변수의 범위는 선언된 지점부터 그 지점을 포함한 블록이 끝날 때까지이므로, 실제 사용하는 블록 바깥에서 선언된 변수는 그 블록이 끝난 뒤까지 살아 있게 된다.
- 실수로 의도한 범위 앞 또는 뒤에서 그 변수를 사용하면 의도치 않은 결과가 나올 수 있다.
- 거의 모든 지역변수는 선언과 동시에 초기화해야 한다.
- 초기화에 필요한 정보가 충분하지 않다면 충분해질 때까지 선언을 미뤄야 한다.
try-catch문은 예외이다.- 변수를 초기화하는 표현식에서 검사 예외를 던질 가능성이 있다면
try블록 안에서 초기화해야 한다. - 변수 값을
try블록 바깥에서도 사용해야 한다면try블록 앞에서 선언해야 한다.
- 변수를 초기화하는 표현식에서 검사 예외를 던질 가능성이 있다면
- 메서드를 작게 유지하고 한 가지 기능에 집중한다.
- 한 메서드에서 여러가지 기능을 처리한다면 그 중 한 기능과만 관련된 지역변수라도 다른 기능을 수행하는 코드에서 접근할 수 있을 것이다.
반복문에서의 지역변수
- 반복문은 독특한 방식으로 변수 범위를 최소화해준다.
- 반복문에서는 반복 변수의 범위가 반복문의 몸체, 그리고
for키워드와 몸체 사이의 괄호 안으로 제한된다. - 따라서 반복 변수의 값을 반복문이 종료된 후에도 써야 하는 상황이 아니라면
while문보다for문을 쓰는 편이 낫다.
for (Element e : c) {
...
}
- 반복자를 사용해야 하는 상황이면
for-each문 대신 전통적인for문을 쓰는 것이 낫다.
for (Iterator<Element> i = c.iterator(); i.hasNext();) {
Element e = i.next();
...
}
for문의 장점
- 반복 변수 유효 범위가
for문 범위와 일치하여 똑같은 이름의 변수를 여러 반복문에서 써도 서로 아무런 영향을 주지 않는다.- 복사 붙여넣기 오류를 줄여준다.
while문의 경우 반복 변수를while문 바깥에서 선언하면, 변수의 유효 범위가while문 밖에서도 끝나지 않아 복사 붙여넣기 오류가 발생할 수 있다.
while문보다 짧아서 가독성이 좋다.
for문에서의 비용 절감
for (int i = 0, n = expensiveComputation(); i < n; i++) {
...
}
-
반복 여부를 결정 짓는 변수
i의 한곗값을 변수n에 저장하여 반복 때마다 다시 계산해야 하는 비용을 없앴다. -
같은 값을 반환하는 메서드를 매번 호출한다면 이 관용구를 사용하는 것이 좋다.
전통적인 for문 보다는 for-each문을 사용하라
전통적인 for문의 단점
- 다음은 전통적인
for문으로 컬렉션과 배열을 순회하는 코드이다.
for (Iterator<Element> i = c.iterator(); i.hasNext();) {
Element e = i.next();
...
}
for (int i = 0; i < a.length; i++) {
...
}
- 이 관용구들은
while문보다는 낫지만 가장 좋은 방법은 아니다. - 반복자와 인덱스 변수는 코드를 지저분하게 할 뿐 진짜 필요한 건 원소들 뿐이다.
- 쓰이는 요소 종류가 늘어나면 오류가 생길 가능성이 높아진다.
- 혹시 잘못된 변수를 사용하더라도 컴파일러가 잡아주리라는 보장도 없다.
- 또한 컬렉션이냐 배열이냐에 따라 코드 형태가 달라지므로 주의해야 한다.
for-each문
- 위 문제들은 for-each문을 사용하면 모두 해결된다.
- 반복자와 인덱스 변수를 사용하지 않으니 코드가 깔끔해지고 오류가 날 일도 없다.
- 하나의 관용구로 컬렉션과 배열을 모두 처리할 수 있어 어떤 컨테이너를 다루는지 신경쓰지 않아도 된다.
for (Element e : elements) {
...
}
전통적인 for문에서의 중첩 순회
- 다음 코드에는 버그가 숨어있다.
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());
}
}
- 문제는 바깥 컬렉션의 반복자
i에서next메서드가 너무 많이 불린다는 것이다. i.next()는Suit하나당 한 번씩만 불려야 하는데, 안쪽 반복문에서 호출되는 바람에Rank하나당 한 번씩 불리고 있다.- 따라서
Suit가 바닥나면 반복문에서NoSuchElementException을 던진다.
- 따라서
해결책
- 문제를 해결하려면 바깥 반복문에 바깥 원소를 저장하는 변수를 하나 추가해야 한다.
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-each문의 이점이 더욱 커진다.
for (Suit suit : suits) {
for (Rank rank : ranks) {
deck.add(new Card(suit, rank));
}
}
for-each문을 사용할 수 없는 상황
- 파괴적인 필터링
- 컬렉션을 순회하면서 원소를 제거해야 한다면 반복자의
remove메서드를 호출해야 한다. - 자바 8부터는
Collection의removeIf메서드를 사용해 컬렉션을 명시적으로 순회하는 일을 피할 수 있다.
- 컬렉션을 순회하면서 원소를 제거해야 한다면 반복자의
- 변형
- 리스트나 배열을 순회하면서 그 원소의 값 일부 혹은 전체를 변경해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야 한다.
- 병렬 반복
- 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 한다.
Iterable 인터페이스
- for-each문은 컬렉션과 배열은 물론
Iterable인터페이스를 구현한 객체라면 무엇이든 순회할 수 있다. Iterable인터페이스는 다음과 같이 메서드가 단 하나 뿐이다.
public interface Iterable<E> {
Iterator<E> iterator();
}
Iterable을 처음부터 직접 구현하기는 까다롭지만, 원소들의 묶음을 표현하는 타입을 작성해야 한다면Iterable을 구현하는 쪽으로 고민하기 바란다.- 해당 타입에서
Collection인터페이스는 구현하지 않기로 했더라도 말이다. Iterable을 구현해두면 for-each문을 사용할 때 아주 유용할 것이다.
- 해당 타입에서
라이브러리를 익히고 사용하라
표준 라이브러리의 이점
- 표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 다른 프로그래머들의 경험을 활용할 수 있다.
- 핵심적인 일과 크게 관련없는 문제를 해결하느라 시간을 허비하지 않아도 된다.
- 따로 노력하지 않아도 성능이 지속해서 개선된다.
- 자바 플랫폼 라이브러리의 많은 부분이 수 년에 걸쳐 지속해서 다시 작성되며, 때론 성능이 극적으로 개선되기도 한다.
- 기능이 점점 많아진다.
- 라이브러리에 부족한 부분이 있다면 개발자 커뮤니티에서 이야기가 나오고 논의된 후, 다음 릴리즈에 해당 기능이 추가되곤 한다.
- 여러분이 작성한 코드가 많은 사람에게 낯익은 코드가 된다.
- 다른 개발자들이 더 읽기 좋고, 유지보수하기 좋고, 재활용하기 쉬운 코드가 된다.
Random 라이브러리
- 자바 7부터는
Random을 더이상 사용하지 않는 것이 좋다. ThreadLocalRandom으로 대체하면 대부분 잘 동작한다.Random보다 더 고품질의 무작위 수를 생성하고, 속도도 빠르다.
- 포크-조인 풀이나 병렬 스트림에서는
SplittableRandom을 사용하는 게 좋다.
기본적인 라이브러리
- 라이브러리가 방대하여 모든 API 문서를 알 수는 없지만, 기본적으로
java.lang,java.util,java.io와 그 하위 패키지들에는 익숙해져야 한다. - 또한 컬렉션 프레임워크와 스트림 라이브러리,
java.util.concurrent의 동시성 기능도 알아두면 큰 도움이 된다.
정리
- 아주 특별한 나만의 기능이 아니라면 누군가 이미 라이브러리 형태로 구현해놓았을 가능성이 크다.
- 라이브러리가 있다면 쓰면 된다.
- 일반적으로 라이브러리의 코드는 직접 작성한 것보다 품질이 좋고, 점차 개선될 가능성이 크다.
- 코드 품질에도 규모의 경제가 적용된다.
- 즉, 라이브러리 코드는 개발자 각자가 작성하는 것보다 주목을 훨씬 많이 받으므로 코드 품질도 그만큼 높아진다.
정확한 답이 필요하다면 float와 double은 피하라
float와double은 과학과 공학 계산용으로 설계되었다.- 이진 부동소수점 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 ‘근사치’로 계산하도록 설계되었다.
- 따라서 정확한 결과가 필요할 때는 사용하면 안된다.
float와double은 특히 금융 관련 계산과는 맞지 않는다.- 0.1 혹은 10의 음의 거듭제곱 수를 표현할 수 없기 때문이다.
잘못된 double 사용 예제
- 1달러가 있고, 10센트, 20센트, 30센트, … 1달러짜리의 사탕이 있다고 했을 때, 사탕을 몇 개나 살 수 있고, 잔돈은 얼마나 남을지 계산하는 예제를 보자.
public static void main(String[] args) {
double funds = 1.00;
int itemsBought = 0;
for (double price = 0.10; funds >= price; price += 0.10) {
funds -= price;
itemsBought++;
}
System.out.println(itmesBought + " 개 구입");
System.out.println("잔돈: " + funds);
}
- 프로그램 실행 시 사탕 3개 구입 후 잔돈은
0.39999999999999달러가 남았다고 출력한다. - 부동소수 타입인
double을 사용한 결과이다.
BigDecimal을 사용한 해결 방법
- 금융 계산에는
BigDecimal,int혹은long을 사용해야 한다.
public static void main(String[] args) {
final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS; funds.compareTo(price) >= 0; price = price.add(TEN_CENTS)) {
funds = funds.substract(price);
itemsBought++;
}
}
BigDecimal의 생성자 중 문자열을 받는 생성자를 사용했다.- 계산시 부정확한 값이 사용되는 걸 막기 위함이다.
BigDecimal은 8가지 반올림 모드를 이용하여 반올림을 완벽히 제어할 수 있다.- 법으로 정해진 반올림을 수행해야 하는 비즈니스 계산에서 아주 편리한 기능이다.
BigDecimal의 단점
- 기본 타입보다 쓰기가 훨씬 불편하고 훨씬 느리다.
BigDecimal의 대안으로int혹은long타입을 쓸 수도 있다.- 그럴 경우 다룰 수 있는 값의 크기가 제한되고, 소수점을 직접 관리해야 한다.
- 성능이 중요하고 소수점을 직접 추적할 수 있고, 숫자가 너무 크지 않다면
int나long을 사용하는 게 좋다. - 숫자를 9자리 십진수로 표현할 수 있다면
int를 사용하고, 18자리 십진수로 표현할 수 있다면long을 사용한다. - 18자리를 넘어간다면
BigDecimal을 사용해야 한다.
박싱된 기본 타입보다는 기본 타입을 사용하라
- 각각의 기본 타입에 대응하는 참조 타입이 하나씩 있으며, 이를 박싱된 기본 타입이라 한다.
기본 타입과 박싱된 기본 타입의 차이점
- 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성(identity)이라는 속성을 갖는다.
- 박싱된 기본 타입의 두 인스턴스는 값이 같아도 서로 다르다고 식별될 수 있다.
- 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 유효하지 않은 값(null)을 가질 수 있다.
- 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.
Integer 비교자 예제
Integer값을 오름차순으로 정렬하는 비교자 코드를 보자.
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
naturalOrder.compare(new Integer(42), new Integer(42))를 실행했을 때0을 출력해야 하지만,1을 출력하는 오류가 있다.- 첫번째 검사
i < j는 잘 작동한다.i,j가 참조하는 인스턴스는 기본 타입 값으로 변환된다.
- 두번째 검사인
i == j에서 두 객체 참조의 식별성을 검사하게 된다.- 즉,
i와j가 참조하는 인스턴스가 다르기 때문에 비교의 결과가false가 되고, 비교자는 1을 반환하는 것이다.
- 즉,
-
즉, 박싱된 기본 타입에
==연산자를 사용하면 오류가 일어난다. - 실무에서 기본 타입을 다루는 비교자가 필요하다면
Comparator.naturalOrder()를 사용하는게 좋다.
오토 언박싱
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
if (i == 42) {
System.out.println("unbelievable!");
}
}
}
- 위 프로그램을 실행했을 때
NullPointerException이 발생한다. i가 기본 타입이 아닌Integer이고, 다른 참조 타입 필드와 마찬가지로 초기값이null이기 때문이다.- 기본 타입과 박싱된 기본 타입을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 풀린다.
null참조를 언박싱하면NullPointerException이 발생한다.
성능 문제
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
- 이 프로그램은 성능이 매우 안좋다.
sum변수를 박싱된 기본 타입으로 선언하였기 때문에,sum += i연산 과정에서 박싱과 언박싱이 반복해서 일어나기 때문이다.
박싱된 기본 타입을 써야하는 경우
- 컬렉션의 원소, 키, 값으로 쓴다.
- 매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로는 박싱된 기본 타입을 써야 한다.
- 리플렉션을 통해 메서드를 호출할 때도 박싱된 기본 타입을 써야한다.
다른 타입이 적절하다면 문자열 사용을 피하라
- 문자열은 텍스트를 표현하도록 설계되었다.
- 그런데 문자열은 흔하고 자바가 잘 지원해주어 원래 의도하지 않은 용도로도 쓰이는 경향이 있다.
문자열을 쓰지 않아야 할 경우
다른 값 타입을 대신하는 경우
- 많은 경우에 파일, 네트워크, 키보드 입력으로부터 데이터를 받을 때 문자열을 사용한다.
- 입력받을 데이터가 진짜 문자열일 때만 그렇게 하는게 좋다.
- 받은 데이터가 수치형이라면
int,float,BigInteger등 적당한 수치 타입으로 변환해야 한다. - ‘예/아니오’ 형태라면 적절한 열거 타입이나
boolean으로 변환해야 한다. - 기본 타입이든 참조 타입이든 적절한 값 타입이 있다면 그것을 사용하고, 없다면 새로 하나 작성하는게 좋다.
열거 타입을 대신하는 경우
- 상수를 열거할 때는 문자열보다는 열거 타입이 월등히 낫다.
혼합 타입을 대신하는 경우
- 여러 요소가 혼합된 데이터를 하나의 문자열로 표현하는 것은 대체로 좋지 않다.
String compoundKey = className + "#" + i.next();
- 각 요소를 개별로 접근하려면 문자열을 파싱해야 해서 느리고, 오류 가능성도 커진다.
- 적절한
equals,toString,compareTo메서드를 제공할 수 없으며,String이 제공하는 기능에만 의존해야 한다. - 차라리 전용 클래스를 새로 만드는 편이 낫다.
- 이런 클래스는 보통 private 정적 멤버 클래스로 선언한다.
권한을 표현하는 경우
-
권한을 문자열로 표현하는 경우가 종종 있다.
-
다음 코드는 스레드별 지역변수를 클라이언트가 제공한 문자열 키로 식별하는 예제이다.
public class ThreadLocal {
private ThreadLocal() {}
public static void set(String key, Object value);
public static Object get(String key);
}
- 이 방식의 문제는 스레드 구분용 문자열 키가 전역 이름공간에서 공유된다는 점이다.
- 이 방식이 의도대로 동작하려면 각 클라이언트가 고유한 키를 제공해야 한다.
- 만약 두 클라이언트가 서로 소통하지 못해 같은 키를 쓰게된다면, 의도치 않게 같은 변수를 공유하게 된다.
- 결국 두 클라이언트 모두 제대로 기능하지 못할 것이다.
- 악의적인 클라이언트라면 의도적으로 같은 키를 사용하여 다른 클라이언트의 값을 가져올 수도 있다.
해결 방법
- 문자열 대신 위조할 수 없는 키를 사용하면 해결된다.
public class ThreadLocal() {
private ThreadLocal() {}
public static class Key {
Key(){}
}
public static Key getKey() {
return new Key();
}
public static void set(Key ket, Object value);
public static Object get(Key key);
}
set과get은 이제 정적 메서드일 이유가 없으니Key클래스의 인스턴스 메서드로 바꿀 수 있다.- 이렇게 하면
Key는 더이상 스레드 지역변수를 구분하기 위한 키가 아니라, 그 자체가 스레드 지역변수가 된다. - 결과적으로 톱레벨 클래스인
ThreadLocal은 별달리 하는 일이 없어지므로 중첩 클래스Key의 이름을ThreadLocal로 바꿀 수 있다.
public final class ThreadLocal {
public ThreadLocal();
public void set(Object value);
public void get();
}
- 이 API에서는
get으로 얻은Object를 실제 타입으로 형변환해야 하기 때문에 타입 안전하지 않다. - 매개변수화 타입으로 선언하면 문제가 해결된다.
public final class ThreadLocal<T> {
public ThreadLocal();
public void set(T value);
public T get();
}
- 처음의 문자열 기반 API는 타입안전하게 만들 수 없다.
문자열 연결은 느리니 주의하라
- 문자열 연결 연산자
+는 여러 문자열을 하나로 합쳐주는 편리한 수단이다. - 문자열 연결 연산자로 문자열 n개를 잇는 시간은 n^2에 비례한다.
- 문자열은 불변이라서 두 문자열을 연결할 경우 양쪽의 내용을 모두 복사해야 하므로 성능저하는 피할 수 없는 결과다.
public String statement() {
String result = "";
for (int i = 0; i < numItems(); i++) {
result += lineForItem(i);
}
return result;
}
- 아이템이 많을 경우 이 메서드는 심각하게 느려질 수 있다.
StringBuilder를 사용한 성능 개선
- 성능을 포기하고 싶지 않다면
String대신StringBuilder를 사용하자.
public String statement2() {
StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
for (int i = 0; i < numItems(); i++) {
b.append(lineForItem(i));
}
return b.toString();
}
StringBuilder를 전체 결과를 담기에 충분한 크기로 초기화했다.
객체는 인터페이스를 사용해 참조하라
- 적합한 인터페이스가 있다면 매개변수, 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하는게 좋다.
- 객체의 실제 클래스를 사용해야 할 상황은 오직 생성자로 생성할 때 뿐이다.
Set<Son> sonSet = new LinkedHashSet<>();
- 인터페이스 타입으로 사용하면 프로그램이 훨씬 유연해질 것이다.
- 나중에 구현 클래스를 교체하고자 한다면 그저 새 클래스의 생성자(혹은 정적 팩터리)를 호출해주기만 하면 된다.
Set<Son> sonSet = new HashSet<>();
- 주변 코드는 옛 클래스의 존재를 애초부터 몰랐으니 이러한 변화에 아무런 영향도 받지 않는다.
구현 타입 교체
주의점
- 원래의 클래스가 인터페이스의 일반 규약 이외의 특별한 기능을 제공하며, 주변 코드가 이 기능에 기대어 동작한다면 새로운 클래스도 반드시 같은 기능을 제공해야 한다.
- 예를 들어, 주변 코드가
LinkedHashSet이 따르는 순서 정책을 가정하고 동작하는 상황에서 이를HashSet으로 바꾸면 문제가 생길 수 있다.HashSet은 반복자의 순회 순서를 보장하지 않기 때문이다.
동기
- 원래 클래스보다 성능이 좋거나 멋진 신기능을 제공할 수 있다.
- 예를 들어,
HashMap을 참조하던 변수를EnumMap으로 바꾸면 속도가 빨라지고 순회 순서도 키의 순서와 같아진다.- 단,
EnumMap은 키가 열거 타입일 때만 사용 가능하다. - 키 타입과 상관없이 사용할 수 있는
LinkedHashMap으로 바꾼다면 성능은 비슷하게 유지하면서 순회 순서를 예측할 수 있다.
- 단,
적합한 인터페이스가 없는 경우
- 적합한 인터페이스가 없다면 당연히 클래스로 참조해야 한다.
- 적합한 인터페이스가 없다면 클래스의 계층구조 중 필요한 기능을 만족하는 가장 덜 구체적인 상위 클래스를 타입으로 사용하자.
값 클래스
-
String과BigInteger같은 값 클래스가 그렇다. - 값 클래스는
final인 경우가 많고, 상응하는 인터페이스가 별도로 존재하는 경우가 드물다. - 이런 값 클래스는 매개변수, 변수, 필드, 반환 타입으로 사용해도 무방하다.
클래스 기반으로 작성된 프레임워크가 제공하는 객체
- 이런 경우라도 특정 구현 클래스보다는 추상 클래스같은 기반 클래스를 사용해 참조하는게 좋다.
java.io패키지의OutputStream등 여러 클래스가 이 부류에 속한다.
인터페이스에 없는 특별한 메서드를 제공하는 클래스
- 예를 들어,
PriorityQueue클래스는Queue인터페이스에는 없는comparator메서드를 제공한다. - 클래스 타입을 직접 사용하는 경우는 이런 추가 메서드를 꼭 사용해야 하는 경우로 최소화해야 한다.
리플렉션보다는 인터페이스를 사용하라
리플렉션
java.lang.reflect의 리플렉션 기능을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다.Class객체가 주어지면 그 클래스의 생성자, 메서드, 필드에 해당하는Constructor,Method,Field인스턴스를 가져올 수 있다.- 또 그 인스턴스들로 그 클래스의 멤버 이름, 필드 타입, 메서드 시그니처 등을 가져올 수 있다.
- 인스턴스를 이용해 각각에 연결된 실제 생성자, 메서드, 필드를 조작할 수도 있다.
- 인스턴스를 통해 해당 클래스의 인스턴스를 생성하거나, 메서드를 호출하거나 필드에 접근할 수 있다.
- 리플렉션을 이용하면 컴파일 당시에 존재하지 않던 클래스도 이용할 수 있다.
리플렉션의 단점
- 컴파일타임 타입 검사가 주는 이점을 하나도 누릴 수 없다.
- 예외 검사도 마찬가지다.
- 리플렉션 기능을 써서 존재하지 않는 혹은 접근할 수 없는 메서드를 호출하려 시도하면 런타임 오류가 발생한다.
- 코드가 지저분하고 장황해진다.
- 성능이 떨어진다.
- 리플렉션을 통한 메서드 호출은 일반 메서드 호출보다 훨씬 느리다.
리플렉션 활용법
- 리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피하고 이점만 취할 수 있다.
- 컴파일타임에 이용할 수 없는 클래스를 사용해야만 하는 경우에, 비록 컴파일타임이라도 적절한 인터페이스나 상위 클래스를 이용할 수는 있을 것이다.
- 이런 경우라면 리플렉션은 인스턴스 생성에만 쓰고, 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.
Set 인스턴스 생성 예제
public static void main(String[] args) {
// 클래스 이름을 Class 객체로 변환
Class<? extends Set<String>> cl = null;
try {
cl = (Class<? extends Set<String>>)Class.forName(args[0]);
} catch (ClassNotFoundException e) {
fatalError("클래스를 찾을 수 없습니다.");
}
Constructor<? extends Set<String>> cons = null;
try {
cons = cl.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
fatalError("매개변수 없는 생성자를 찾을 수 없습니다.");
}
// Set 인스턴스 생성
Set<String> s = null;
try {
s = cons.newInstance();
} catch (IllegalAccessException e) {
fatalError("생성자에 접근할 수 없습니다.");
} catch (InstantiationException e) {
fatalError("클래스를 인스턴스화할 수 없습니다.");
} catch (InvocationTargetException e) {
fatalError("생성자가 예외를 던졌습니다.: " + e.getCause());
} catch (ClassCastException e) {
fatalError("Set을 구현하지 않은 클래스입니다.");
}
// 생성한 Set 사용
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
- 명령줄의 첫번째 인수로 클래스 이름을 받아 정확한 클래스를 확정한다.
- 생성한 Set에 두번째 이후의 인수들을 추가한 다음 출력한다.
- 인수들이 출력되는 순서는 첫번째 인수로 지정한 클래스가 무엇이냐에 따라 달라진다.
HashSet을 지정하면 무작위 순서가 될 것이고,TreeSet을 지정하면 알파벳 순서가 될 것이다.
예제에서 보이는 리플렉션 단점
- 런타임에 총 여섯가지나 되는 예외를 던질 수 있다.
- 그 모두가 인스턴스를 리플렉션없이 생성했다면 컴파일타임에 잡아낼 수 있었을 예외들이다.
- 클래스 이름만으로 인스턴스를 생성해내기 위해 코드가 매우 길어졌다.
- 참고로, 리플렉션 예외를 각각 잡는 대신
ReflectiveOperationException을 잡도록 하여 코드 길이를 줄일 수도 있다.
- 참고로, 리플렉션 예외를 각각 잡는 대신
리플렉션이 적합한 경우
- 런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드, 필드와의 의존성을 관리할 때 적합하다.
- 버전이 여러개 존재하는 외부 패키지를 다룰 때 유용하다.
- 가동할 수 있는 최소한의 환경, 즉 주로 가장 오래된 버전만을 지원하도록 컴파일한 후, 이후 버전의 클래스와 메서드 등은 리플렉션으로 접근하는 방식이다.
- 접근하려는 새로운 클래스나 메서드가 런타임에 존재하지 않을 수 있다는 사실을 반드시 감안해야 한다.
네이티브 메서드는 신중히 사용하라
Java Native Interface
- 자바 프로그램이 네이티브 메서드를 호출하는 기술이다.
- 네이티브 메서드란 C나 C++ 같은 네이티브 프로그래밍 언어로 작성한 메서드를 말한다.
네이티브 메서드 사용
- 레지스트리 같은 플랫폼 특화 기능을 사용한다.
- 네이티브 코드로 작성된 기존 라이브러리를 사용한다.
- 성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성한다.
네이티브 메서드 단점
- 네이티브 언어가 안전하지 않으므로 네이티브 메서드를 사용하는 어플리케이션도 메모리 훼손 오류로부터 안전하지 않다.
- 네이티브 언어는 자바보다 플랫폼을 많이 타서 이식성도 낮고 디버깅도 어렵다.
- 주의하지 않으면 속도가 오히려 느려질수도 있다.
- 성능 개선을 목적으로 네이티브 메서드를 사용하는 것은 거의 권장하지 않는다.
- 대부분의 작업에서 지금의 자바는 다른 플랫폼에 견줄만한 성능을 보인다.
- 가비지 컬렉터가 네이티브 메모리는 자동 회수하지 못하고, 심지어 추적조차 할 수 없다.
- 자바 코드와 네이티브 코드의 경계를 넘나들 때마다 비용도 추가된다.
- 네이티브 코드와 자바 코드 사이의 접착 코드를 작성해야 하는데, 귀찮고 가독성도 떨어진다.
최적화는 신중히 하라
- 최적화는 좋은 결과보다는 해로운 결과로 이어지기 쉽고, 섣불리 진행하면 특히 더 그렇다.
- 빠르지도 않고 제대로 동작하지도 않으면서 수정하기는 어려운 소프트웨어를 탄생시키는 것이다.
- 성능때문에 견고한 구조를 희생시키지 말자.
- 빠른 프로그램보다는 좋은 프로그램을 작성해야 한다.
- 좋은 프로그램은 정보 은닉 원칙을 따르므로 개별 구성요소의 내부를 독립적으로 설계할 수 있다.
- 따라서 시스템의 나머지에 영향을 주지 않고도 각 요소를 다시 설계할 수 있다.
설계 단계에서의 성능
- 성능을 제한하는 설계를 피해야 한다.
- 완성 후 변경하기가 가장 어려운 설계 요소는 컴포넌트끼리 혹은 외부 시스템과의 소통 방식이다.
- API, 네트워크 프로토콜 등이 대표적이다.
- 이런 설계 요소들은 완성 후에는 변경하기 어렵거나 불가능할 수 있으며, 시스템 성능을 심각하게 제한할 수 있다.
- API를 설계할 때 성능에 주는 영향을 고려해야 한다.
public타입을 가변으로 선언하여 내부 데이터를 변경할 수 있게 만들면, 불필요한 방어적 복사를 수없이 유발할 수 있다.- 컴포지션으로 해결할 수 있음에도 상속 방식으로 설계한
public클래스는 상위 클래스에 영원히 종속되며, 그 성능 제약까지도 물려받게 된다. - 인터페이스 대신 구현 타입을 사용하면, 특정 구현체에 종속되게 하여 나중에 더 빠른 구현체가 나오더라도 이용하지 못하게 된다.
성능 문제 예시 - java.awt.Component
Component클래스의getSize메서드는Dimension인스턴스를 반환한다.Dimension은 가변으로 설계했기 때문에,getSize를 호출하는 모든 곳에서Dimension인스턴스를 방어적 복사를 위해 새로 생성해야만 한다.
다른 설계 방법
-
Dimension을 불변으로 만든다. -
getSize를getHeight과getWidth로 나눈다.- 즉,
Dimension객체의 기본 타입 값들을 각각 반환하는 방식이다.
- 즉,
성능 측정
- 최적화 시도 전후로 성능을 측정해야 한다.
- 시도한 최적화 기법이 성능을 눈에 띄게 높이지 못하는 경우가 많고, 심지어 더 나빠지게 할 때도 있다.
- 자바는 프로그래머가 작성하는 코드와 CPU에서 수행하는 명령 사이의 추상화 격차가 크기 때문에, 최적화로 인한 성능 변화를 일정하게 예측하기가 더 어렵다.
- 자바의 성능 모델은 정교하지 않을 뿐더러, 구현시스템, 릴리스, 프로세서마다 차이가 있다.
- 프로그램을 여러가지 자바 플랫폼이나 여러 하드웨어 플랫폼에서 구동한다면, 최적화의 효과를 그 각각에서 측정해야 한다.
프로파일링 도구
- 프로파일링 도구는 최적화 노력을 어디에 집중해야 할지 찾는데 도움을 준다.
- 개별 메서드의 소비 시간과 호출 횟수 같은 런타임 정보를 제공하여, 집중할 곳은 물론 알고리즘을 변경해야 한다는 사실을 알려주기도 한다.
- 시간이 거듭제곱으로 증가하는 알고리즘이 숨어있다면 더 효율적인 것으로 교체해야 한다.
- 시스템 규모가 커질수록 프로파일러가 더 중요해진다.
정리
- 잘 설계된 API는 성능도 좋은게 보통이다.
- 성능을 위해 API를 왜곡하는 것은 매우 안좋은 생각이다.
- 신중하게 설계하여 깨끗하고 명확하고 멋진 구조를 갖춘 프로그램을 완성한 다음에야 최적화를 고려해볼 차례가 된다.
일반적으로 통용되는 명명 규칙을 따르라
- 자바의 명명 규칙은 철자와 문법 두 범주로 나뉜다.
철자 규칙
- 철자 규칙은 패키지, 클래스, 인터페이스, 메서드, 필드, 타입 변수의 이름을 다룬다.
- 특별한 이유가 없는 한 반드시 따라야 한다.
- 규칙을 어긴 API는 사용하기 어렵고, 유지보수하기도 어렵다.
패키지
- 패키지와 모듈 이름은 각 요소를
.으로 구분하여 계층적으로 짓는다. - 요소들은 모두 소문자 알파벳 혹은 숫자로 이루어진다.
- 조직 바깥에서도 사용될 패키지라면, 조직의 인터넷 도메인 이름을 역순으로 사용한다.
- 예를 들어,
com.google이런식이다.
- 예를 들어,
- 표준 라이브러리와 선택적 패키지들은 각각
java와javax로 시작한다. - 패키지 이름의 나머지는 해당 패키지를 설명하는 하나 이상의 요소로 이뤄진다.
- 일반적으로 8자 이하의 짧은 단어로 한다.
- 요소의 이름은 보통 한 단어 혹은 약어로 이루어진다.
util,awt같은 식이다.
- 많은 기능을 제공하는 경우엔 계층을 나눠 두 개 이상의 요소로 구성해도 좋다.
클래스와 인터페이스
- 클래스와 인터페이스의 이름은 하나 이상의 단어로 이뤄지며, 각 단어는 대문자로 시작한다.
- 약자의 경우 첫글자만 대문자로 할지 전체를 대문자로 할지는 논란이 있다.
- 첫글자만 대문자로 하는쪽이 훨씬 많다.
HttpUrl처럼 여러 약자가 혼합된 경우라도 각 약자의 시작과 끝을 명확히 알 수 있기 때문이다.
메서드와 필드
- 메서드와 필드 이름은 첫 글자를 소문자로 쓴다는 점만 빼면 클래스 명명 규칙과 같다.
- 상수 필드는 모두 대문자로 쓰며, 단어 사이는 밑줄로 구분한다.
- 예를 들어
NEGATIVE_INFINITY같은 식이다. - 상수 필드는 값이 불변인
static final필드를 말한다.
- 예를 들어
지역변수
- 지역변수에도 다른 멤버와 비슷한 명명규칙이 적용된다.
-
단, 약어를 써도 그 변수가 사용되는 문맥에서 의미를 쉽게 유추할 수 있기 때문에 써도 좋다.
- 입력 매개변수도 지역변수의 하나지만, 메서드 설명 문서에까지 등장하는 만큼 일반 지역변수보다는 신경을 써야한다.
타입 매개변수
- 타입 매개변수의 이름은 보통 한 문자로 표현한다.
- 대부분 다음 다섯가지 중 하나다.
- 임의의 타입 :
T,U,V - 컬렉션 원소의 타입 :
E - 맵의 키와 값 :
K,V - 예외 :
X - 메서드의 반환 타입 :
R
- 임의의 타입 :
문법 규칙
- 문법 규칙은 철자 규칙과 비교하면 더 유연하고 논란도 많다.
- 패키지에 대한 규칙은 따로 없다.
클래스와 인터페이스
- 객체를 생성할 수 있는 클래스의 이름은 보통 단수 명사나 명사구를 사용한다.
Thread,ChessPiece등
- 객체를 생성할 수 없는 클래스의 이름은 보통 복수형 명사로 짓는다.
Collectors,Collections등
- 인터페이스 이름은 클래스와 똑같이 짓거나
able혹은ible로 끝나는 형용사로 짓는다.Runnable,Iterable,Accessible등
메서드
- 어떤 동작을 수행하는 메서드의 이름은 동사나 동사구로 짓는다.
append,drawImage등
boolean값을 반환하는 메서드라면 보통is나has로 시작하고, 명사나 명사구, 혹은 형용사로 기능하는 단어나 구로 끝나도록 짓는다.isDigit,isEmpty,hasSiblings등
- 해당 인스턴스의 속성을 반환하는 메서드의 이름은 보통 명사, 명사구, 혹은
get으로 시작하는 동사구로 짓는다.size,hashCode,getTime등get으로 시작하는 형태는 주로 JavaBeans 명세에 뿌리를 두고 있다.- 자바빈즈는 재사용을 위한 컴포넌트 아키텍처의 초기버전 중 하나로, 최근의 도구 중에도 이 명명 규칙을 따르는 경우가 제법 많다.
- 클래스가 한 속성의 게터와 세터를 모두 제공할 때도 적합한 규칙이다.
특별한 형태의 메서드 이름
- 객체의 타입을 바꿔서 다른 타입의 또 다른 객체를 반환하는 인스턴스 메서드 :
toTypetoString,toArray등
- 객체의 내용을 다른 뷰로 보여주는 메서드 :
asTypeasList등
- 객체의 값을 기본 타입 값으로 반환하는 메서드 :
typeValueintValue등
- 정적 팩터리 :
from,of,valueOf,instance,getInstance,getType등
필드
- 필드 이름에 관한 문법 규칙은 클래스, 인터페이스, 메서드 이름에 비해 덜 명확하고 덜 중요하다.
- 필드가 직접 노출될 일은 거의 없기 때문이다.
boolean타입의 필드 이름은 보통boolean접근자 메서드에서 앞 단어를 뺀 형태다.initialized,composite등
- 다른 타입의 필드라면 명사나 명사구를 이용한다.
height,digits,bodyStyle등