일반적인 프로그래밍 원칙
지역변수, 제어규ㅜ조, 라이브러리, 데이터 타입, 그리고 리플렉션과 네이티브 메서드를 다루고 최적화와 명명 규칙을 논한다.
아이템 57 : 지역변수의 범위를 최소화하라
이번 아이템은 기본적으로 "클래스와 멤버의 접근 권한을 최소화하라"라고 했던 아이템15와 취지가 비슷하다.
지역 변수의 범위를 최소화 하는 방법
- 지역 변수의 범위를 줄이는 가장 강력한 기법은 '가장 처음 쓰일 때 선언하기'다.
- 미리 선언해두면 코드가 어수선해지고 가독성이 떨어지고 사용 시점에는 초기 값이 기억나지 않을 수도 있다.
- 거의 모든 지역변수는 선언과 동시에 초기화해야 한다.
- 초기화에 필요한 정보가 충분하지 않다면 충분해질 때까지 선언을 미뤄야 한다.
- 검사 예외를 던질 가능성이 있으면 try 안에서 초기화 해야 하고 try 바깥에서도 사용해야 한다면 try 블록 앞에서 선언해야 한다.
- 반복 변수가 계속 필요하지 않다면 while보다는 for문을 쓰는게 낫다.
- 반복자를 사영해야 하는 상황에는 for-each문 보다 전통적인 for문을 쓰는게 낫다.
- while문에서는 복사해 붙여넣기 오류가 발생 할 수 있다. int i를 첫 번째 while에서 쓰고 두 번째 while문에서 다시 사용하는 실수를 할 수도 있다.
- for문은 while문보다 짧아서 가독성이 좋다.
- 메서드를 작게 유지하고 한 가지 기능에 집중하는것이다.
- 한 메서드에서 여러 가지 기능을 처리한다면 그 중 한 기능과만 관련된 지역변수라도 다른 기능을 수행하는 코드에서 접근할 수 있게되는 문제가 발생한다.
- 지역 변수는 가장 처음 쓰일 때 작성한다.
- 선언과 동시에 초기화 해야 한다.
- while보다는 for문을 사용하자.
- 메서드를 작게 유지하고 한 가지 기능에 집중한다.
아이템 58 : 전통적인 for문보다는 for-each문을 사용하라
스트림이 제격인 작업이 있고, 반복이 제격인 작업이 있다.
전통적인 for문의 문제점
public static void main(String[] args) {
List<Element> list = new ArrayList<>();
// for문으로 컬렉션을 순회하는 코드
for (Iterator<Element> i = list.iterator(); i.hasNext(); ) {
Element e = i.next();
}
// 전통적인 for문으로 배열을 순회하는 코드
for (int i = 0; i < list.size(); i++) {
}
}
- while문 보다는 낫지만 가장 좋은 방법은 아니고 반복자와 인덱스 변수는 코드를 지저분하게 할 뿐 우리에게 필요한건 원소들뿐기 때문이다.
- 혹시라도 잘못된 변수를 사용했을 때 컴파일러가 잡아주지 않고 컬렉션인지 배열인지에 따라 코드 형태가 달라진다.
향상된 for-each이라는 대안
- 반복자와 인덱스를 사용하지 않아 코드가 깔끔하고 오류가 날 일도 없다.
- 하나의 관용구로 컬렉션과 배열을 모두 처리할 수 있어서 어떤 컨테이너를 다루는지는 신경쓰지 않아도 된다.
for (Element e : // ':' 은 in 이라고 읽으면 된다
list) {
System.out.println(e.toString());
}
- for-each문이 만들어 내는 코드는 사람이 손으로 최적화한 것과 같아 속도는 그대로이다.
for-each의 잘못된 사용의 경우
enum Suit {
CLUB("1"), DIAMOND("2"), HEART("3"), SPADE("4");
private String idx;
Suit(String idx) {
this.idx = idx;
}
}
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX }
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())); // next는 숫자 하나당 한 번씩만 불러야 한다
}
}
- 위 소스는 바깥 컬렉션(suits)의 반복자에서 next메서드가 너무 많이 불러내는 문제로 i.next()를 부르다가 가지고 있는 원소가 바닥나면 NoSuchElementException을 던지게 된다.
enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX }
public static void main(String[] args) {
Collection<Face> faces = EnumSet.allOf(Face.class);
for (Iterator<Face> i = faces.iterator(); i.hasNext(); )
for (Iterator<Face> j = faces.iterator(); j.hasNext(); )
System.out.println(i.next() + " " + j.next());
}
- 위 코드는 6가지 수를 중첩해서 36개의 쌍을 출력하려고 했으나 6개만 출력된다.
잘못 사용한 경우를 해결하는 방법
for (Iterator<Face> i = faces.iterator(); i.hasNext(); ) {
Face f = i.next();
for (Iterator<Face> j = faces.iterator(); j.hasNext(); ) {
System.out.println(f + " " + j.next());
}
}
- 바깥 반복문 값을 보관하면 간단히 해결 된다.
for-each문을 사용할 수 없는 상황 세 가지
아래 세 가지 경우에 해당하면 일반적인 for문을 사용하되 이번 아이템에 언급 된 문제들을 경계해야 한다.
- 파괴적인 필터링(clestructive : 컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 반복자의 remove메서드를 호출해야 한다. 자바 8부터는 Collection의 removeIf 메서드를 사용해 컬렉션을 명시적으로 순회하는 일을 피할 수 있다.
- 변형(transforming) : 리스트나 배열을 순회하면서 그 원소와 값 일부 혹은 전체를 교체해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야 한다.
- 병렬 반복(parallel iteration) : 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 한다.
for-each문은 컬렉션과 배열은 물론 Iterable 인터페이스를 구현한 객체라면 무엇이든 순회할 수 있다. Iterable 인터페이스는 다음과 같은 메서드가 단 하나뿐이다.
public interface Iterable<E> {
// 이 객체의 원소들을 순회하는 반복자를 반환한다.
Iterator<E> iterator();
}
- Iterator을 처음부터 구현하기에는 까다롭지만, 원소들의 묶음을 표현하는 타입을 작성한다면 Iterable을 구현하는 쪽으로 고민해보기 바란다.
⇒ while문보다는 낫지만(아이템57)
전통적인 for 문과 비교했을 때 for-each문은 명료하고, 유연하고, 버그를 예방해준다. 성능 저하도 없다. 가능한 모든 곳에서 for-each문을 사용하자.
아이템 59 : 라이브러리를 익히고 사용하라
예를 들어 무작위 정수 하나를 생성한다면 값의 범위는 0부터 명시한 수 사이이의 소스이고 세 가지의 문제를 내포하고 있다.
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
- n이 그리 크지 않은 2의 제곱수라면 얼마 지나지 않아 같은 수열이 반복 된다.
- n이 2의 제곱수가 아니라면 몇몇 숫자가 평균적으로 더 자주 반환한다.
- n값이 크면 이 현상은 더 두드러진다.
표준 라이브러리를 사용의 이점
- 표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 여러분보다 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있다.
- 자바 7부터는 Random을 사용하지 않는게 좋고 ThreadLocalRandom으로 대체하면 대부분 잘 동작하고 포크-조인 풀이나 병렬 스트림에서는 SplittableRandom을 사용하자
- 핵심적인 일과 크게 관련 없는 문제를 해결하느라 시간을 허비하지 않아도 된다.
- 따로 노력하지 않아도 성능이 지속해서 개선된다.
- 기능이 점점 많아지고 있다.
- 작성한 코드가 많은 사람에게 낯익은 코드가 되어 자연스럽게 다른 개발자들이 더 읽기 좋고, 유지보수 하기 좋고, 재활용하기 쉬운 코드가 된다.
메이저 릴리스마다 주목할 만한 수많은 기능이 라이브러리에 추가 된다.
- 직접 구현해 사용하기 보다는 라이브러리에 그런 기능이 있는지 찾아보자.
public static void main(String[] args) throws IOException {
try(InputStream in = new URL(args[0]).openStream()){
in.transferTo(System.out);
}
}
- 라이브러리가 너무 방대하여 모든 API 문서를 공부하기에는 벅차지만 자바 프로그래머라면 적어도 java.lang, java.utul, java.io와 그 하위 패키지들에는 익숙해져야 한다.
- 어떤 영역의 기능을 제공하는지 살펴보고, 여러분이 원하는 기능이 아니라 판단되면 대안을 사용하자.
- 어떤 라이브러리든 제공하는 기능은 유한하므로 항상 빈 구멍이 있기 마련이고 라이브러리에서 원하는 기능을 찾지 못하면 고품질의 서드파티 라이브러리가 될 것이며 대표적으로 구글의 구아바 라이브러리가 있다.
⇒ 컬렉션 프레임워크와 스트림 라이브러리(아이템45~48), java.util.concurrent의 고수준 개념(아이템80,81)
바퀴를 다시 발명하지 말자. 아주 특별한 나만의 기능이 아니라면 누군가 이미 라이브러리 형태로 구현해놓았을 가능성이 크다.
아이템 60 : 정확한 답이 필요하다면 float와 double은 피하라
float와 double 타입은 과학과 공학 게산용으로 설계되어 이진 부동소수점 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 '근사치'로 계산하도록 세심하게 설계되었기 때문에 정확한 결과가 필요할 때는 사용하면 안 된다.
float와 double 타입은 특히 금융 관련 계산과는 맞지 않는다.
- 0, 1 혹은 10의 음의 거듭 제곱수(10^-1, 10^-2 등)를 표현할 수 없다.
- 1.03달러에서 42센트를 쓴 코드를 작성하면 System.out.println(1.03 - 0.42)가 되고 결과는 0.6100000001을 출력한다.
- 반올림을 한다고 해서 해결되지 않고 틀린 답이 나올 수 있다.
금융 계산에는 BigDecimal, int 혹은 long을 사용해야 한다.
- double 타입을 BigDecimal로 교체하면 된다.
public static void main(String[] args) {
final BigDecimal TEN_DENTS = new BigDecimal(".10");
int itemBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_DENTS;
funds.compareTo(price) >= 0;
price = price.add(TEN_DENTS)) {
funds = funds.subtract(price);
itemBought++;
}
System.out.println(itemBought + "개 구입");
System.out.println("잔돈(달러) : " + funds);
}
- BigDecimal로 올바른 답을 낼 수 있지만 기본 타입보다 쓰기 불편하훨씬 느리다.
int itemBought2 = 0;
int funds2 = 100;
for (int price = 10; funds2 >= price; price += 10) {
funds2 -= price;
itemBought2++
}
System.out.println(itemBought + "개 구입");
System.out.println("잔돈(센트): " + funds);
- 기본 타입 계산방식으로 다룰 수 있는 값의 크기가 제한되고 소수점을 직접 관리해야 한다.
정확한 답이 필요한 계산에는 float나 double을 피하라.
소수점 추적은 시스템에 맡기고, 코딩 시에 불편함이나 성능 저하를 신경쓰지 않겠다면 BigDecimal을 사용하라.
반면 성능이 중요하고 소수점을 직접 추적할 수 있고 숫자가 너무 크지 않다면 int나 log을 사용하라. 숫자를 아홉자리 십진수로 표현할 수 있다면 int를 열여덟 자리 십진수로 표현할 수 있다면 log을 사용하라.
열여덟 자리를 넘어가면 BigDecimal을 사용해야 한다.
아이템 61 : 박싱된 기본 타입보다는 기본 타입을 사용하라
자바의 데이터 타입은 크게 int, double, boolean같은 기본 타입과 String, List같은 타입으로 나눌 수 있다.
기본 타입과 박싱된 기본 타입의 주된 차이는 크게 세 가지다.
아래 세 가지를 유의해서 사용해야 한다.
- 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성이란 속성을 갖기에 서로 값이 같아도 서로 다르다고 식별될 수 있다.
- 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 유효하지 않은 값인 null을 가질 수 있다.
- 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
- 정상적으로 동작하는 코드 같지만 결함이 있는데 naturalOrder.compare(new Integer(42), new Integer(42))의 값을 출력하면 0이 나오지 않고 1을 출력하여 첫 번째 Integer가 두 번째보다 크다고 주장한다.
- (같은 객체를 비교하는게 아니라면) 박싱된 기본 타입에 == 연산자를 사용하면 오류가 일어난다.
- 위와 같이 기본 타입을
- 비교자를 직접 만들면 비교자 생성 메서드나 기본 타입을 받는 정적 compare메서드를 사용해야 한다.
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
int i = iBoxed, j = jBoxed; // 오토박싱
return i < j ? -1 : (i == j ? 0 : 1);
};
- 문제를 수정한 비교자
public class Unbelieable {
static Integer i; // i를 int로 선언하면 해결 된다.
public static void main(String[] args) {
if (i == 42)
System.out.println("믿을 수 없군!");
}
}
- 기이하게 동작하는 프로그램
- 원인은 i가 int가 아닌 Integer이며, 다른 참조 타입 필드와 마찬가지로 i의 초깃값도 null이라는데 있다.
- 기본 타입과 박싱된 기본 타입을 혼용한 연산에서는 박싱된 기본 타입 박싱으로 자동으로 풀린다.
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
- 실수로 지역 변수 sum을 박싱된 기본 타입으로 선언하여 느려진 코드이고 박싱 언박싱이 반복해서 일어나면 체감할 수 있을 정도로 성능이 느려진다.
박싱된 기본 타입은 언제 사용해야 하는가
- 컬렉션의 원소, 키, 값으로 쓴다. 컬렉션은 기본 타입을 담을 수 ㅇ벗으므로 어쩔 수 없이 박싱된 기본 타입을 써야 한다.
- 매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로는 박싱된 기본 타입을 써야 한다.
- 리플렉션을 통해 메서들를 호출할 때도 박싱된 기본 타입을 사용해야 한다.
⇒ 오토박싱과 오토언박싱(아이템6), 정적 compare 메서드를 사용해야 한다(아이템14), 리플렉션(아이템65)
기본 타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 가능하다면 기본 타입을 사용하라.
기본 타입은 간단하고 빠르며 박싱된 타입을 써야 한다면 주의를 기울이자.
오토박싱이 박싱된 기본 타입을 사용할 때의 번거로움을 줄여주지만, 그 위험까지 없애주지는 않는다.
언박싱 과정에서 NullPointerException을 던질 수 있다.
아이템 62 : 다른 타입이 적절하다면 문자열 사용을 피하라
문자열은 다른 값 타입을 대신하기에 적합하지 않다.
- 텍스트로 표현하도록 설계되었지만 흔하고 자바가 잘지원해주어원래 의도하지 않은 용도로 쓰이는 경향이 있다.
- 받은 데이터가 수치라면 int, float, BigInteger 등 적당한 수치 타입으로 변환해야 한다.
- 기본 타입이든 참조 타입이든 적절한 값 타입이 있다면 그것을 사용하고, 없다면 새로 하나 작성하라
문자열은 열거 타입을 대신하기에 적합하지 않다.
- 상수를 열거할 때는 문자열보다는 열거 타입이 월등히 낫다
문자열은 흔한 타입을 대신하기에 적합하지 않다.
- 여러 요소가 혼합된 데이터를 하나의 문자열로 표현하는 것은 대체로 좋지 않은 생각이다.
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 key, Object value);
// (키가 가리키는) 현 스레드의 값을 반환한다.
public static Object get(String key);
}
- 문자열 대신 위조할 수 없는 키를 사용하면 해결할 수 있고 이 키를 권한이라고도 한다.
public final class ThreadLocal {
public THreadLocal();
public void set(Object value);
public Object get();
}
- set과 get은 이제 정적 메서드일 이유가 없으니 Key 클래스의 인스턴스 메서드로 바꾸면 Key는 더 이상 스레드 지역변수를 구부누하기 위한 키가 아니라, 그 자체가 스레드 지역변수가 된다.
public final class ThreaddLocal<T> {
public ThreadLocal();
public void set(T value);
public T get();
}
- Object를 실제 탕비으로 형변환해서 써야 해서 타입 안전하지 않아 ThreadLocal을 매개변수화 타입으로 선언해준다.
⇒ 정적 멤버 클래스(아이템 24), 매개변수화 타입(아이템29)
더 적합한 데이터 타입이 있거나 새로 저장할 수 있다면, 문자열을 쓰고 싶은 유혹을 뿌리쳐라. 문자열은 잘못 사용하면 번거롭고, 덜 유연하고, 느리고, 오류 가능성도 크다. 문자열을 잘못 사용하는 흔한 예로는 기본타입, 열거 타입, 혼합 타입이 있다.
아이템 63 : 문자열 연결은 느리니 주의하라
문자열 연결 연산자로 문자열 n개를 잇는 시간은 n^2에 비례한다.
- 문자열 연결 연산자(+)는 여러 문자열을 하나로 합쳐주는 편리한 수단으로 한 줄짜리 출력 값 혹은 작고 크기가 고정된 객체의 문자열 표현을 만들때라면 괜찮지만, 본격적으로 사용하기 시작하면 성능 저하를 감내하기 어렵다.
- 문자열은 불변이라서 두 문자열을 연결할 경우 양쪽의 내용을 모두 복사해야 하므로 성능 저하는 피할 수 없다.
public String statement() {
String result = "";
for (int i = 0; i < numItems(); i++)
result += lineForItem(i); // 문자열 연결을 잘못 사용한 예 -- 느리다!
return result;
}
성능을 포기하고 싶지 않다면 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();
}
- 자바 6 이후 문자열 연결 성능을 다방면으로 개선했지만 차이는 여전히 크다
⇒ 문자열 불변(아이템 17)
원칙은 간단하다. 성능에 신경 써야 한다면 많은 문자열을 연결할 때는 문자열 연결 연산자(+)를 피하자. 대신 StringBuilder의 append 메서드를 사용하라. 문자 배열을 사용하거나, 문자열을(연결하지 않고) 하나씩 처리하는 방법도 있다.
아이템 64 : 객체는 인터페이스를 사용해 참조하라
적합한 인터페이스만 있다면 매개변수뿐 아니라 반환 값, 변수, 필드를 전부 인터페이스 타입으로 선언하라.
- 객체는 클래스가 아닌 인터페이스로 참조하라는 의미이기도 하다.
- 객체의 실제 클래스를 사용해야 할 상황은 '오직' 생성자로 생성할 때뿐이다.
// 좋은 예. 인터페이스를 타입으로 사용했다.
Set<Son> sonSet = new LinkedHashSet<>();
- 좋은 예의 코드이다.
// 나쁜 예. 클래스를 타입으로 사용했다.
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
- 안 좋은 예의 코드이다.
인터페이스를 타입으로 사용하는 습관을 길러두면 프로그램이 훨씬 유연해질것이다.
Set<Son> sonSet = new HashSet<>(); // LinkedHashSet()에서 HashSet()으로 쉽게 변환했다.
- 교체가 유연하게 이루진다.
- 단점으로는 원래의 클래스가 인터페이스의 일반 규약 이외의 특별한 기능을 제공하며, 주변 코드가 이 기능에 기대어 동작한다면 새로운 클래스도 반드시 같은 기능을 제공해야 한다.
- 첫 번째 선언의 주변 코드가 LinkedHashSet이 따르는 순서 정책을 가정하고 동작하는 상황에서 이를 HashSet으로 바꾸면 문제가 될 수 있는데 HashSet은 반복자의 순회 순서를 보장하지 않기 때문이다.
- 구현 타입을 바꾸는 경우는 HashMap을 참조하던 변수를 EunmMap으로 바꾸면 속도가 빨라지고 순회 순서도 키의 순서와 같아지지만 키가 열거 타입일 때만 사용할 수 있다. 반면 LinkedHashMap으로 바꾸면 성능은 비슷하게 유지하면서 순회 순서를 예측할 수 있다.
- 변수를 구현 타입으로 바꾸면 기존 타입에서만 제공하는 메서드를 사용하거나 기존 타입을 사용해야 한느 다른 메서들에 인스턴스를 넘기게 되면 컴파일이 되지 않을 수 있지만 인터페이스 타입으로 선언하면 이런 문제가 발생하지 않는다.
적합한 인터페이스가 없다면 당연히 클래스로 참조해야 한다.
- String과 BigInteger 같은 값 클래스는 구현 객체로 만들어야 하며 값 클래스를 여러 가지로 구현될 수 있다고 생각하고 설계하는 일은 거의 없다.
- final인 경우가 많고 상응하는 인터페이스가 별도로 존재하는 경우가 드물어 매개변수, 변수, 필드, 반환 타입으로 사용해도 무방하다.
- 클래스 기반으로 작성된 프레임워크가 제공하는 개체들이며 특정 구현 클래스보다는(보통은 추상 클래스인) 기반 클래스를 사용해 참조하는 게 좋다. OutputStream 등 java.io 패키지의 여러 클래스가 이 부류에 속한다.
- 인터페이스에는 없는 특별한 메서드를 제공하는 클래스들인데 PriorityQueue 클래스는 Queue 인터페이스에는 없는 comparator 메서드를 제공한다. 클래스 타입을 직접 사용하는 경우는 이런 추가 메서드를 꼭 사용해야 하는 경우로 최소화 해야 하며, 절대 남발하지 말아야 한다.
적합한 인터페이스가 없다면 클래스의 계층구조 중 필요한 기능을 만족하는 가장 덜 구체적인(상위의) 클래스를 타입으로 사용하자.
인터페이스 타입으로 사용하는걸 습관화 하고 적합한 인터페이스가 없다면 클래스를 참조해야 한다.
아이템 65 : 리플렉션보다는 인터페이스를 사용하라
리플렉션의 단점
- 컴파일타임 타입 검사가 주는 이점을 하나도 누릴 수 없다.
- 예외 검사의 이점을 누릴 수 없고 프로그램이 리플렉션 기능을 써서 존재하지 않는 혹은 접근할 수 없는 메서드를 호출하려 시도하면(주의해서 대비 코드를 작성해두지 않았다면) 런타임 오류가 발생한다.
- 리플렉션을 이용하면 코드가 지저분하고 장황해진다.
- 성능이 떨어진다.
- 일반 메서드 호출보다 훨씬 느리고 고려해야 하는 요소가 많아 정확한 차이는 이야기하기 어렵지만 굉장히 느리다.
리플렉션을 사용해야 하는 경우
- 리플렉션은 인스턴스 생성에만 쓰고, 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.
- 리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피하고 이점만 취할 수 있다.
- 컴파일타임에 이용할 수 없는 클래스를 사용해야만 하는 프로그램은 비록 컴파일타임이라도 적절한 인터페이스나 상위 클래스를 이용할 수는 있을 것이다.
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<String> s = null;
try {
s = cons.newInstance();
} catch (IllegalAccessException e){
fatalError("생성자에 접근할 수 없습니다.");
} catch (InvocationTargetException e) {
fatalError("생성자가 예외를 던졌습니다." + e.getCause());
} catch (InstantiationException e) {
fatalError("클래스를 인스턴스화할 수 없습니다.");
}catch (ClassCastException e ) {
fatalError("Set을 구현하지 않은 클래스입니다.");
}
// 생성한 집합을 사용한다.
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
- 위 예시의 단점 두 가지가 있다.
- 런타임에 총 여섯 가지지나 되는 예외를 던질 수 있는데 모두 인스턴스를 리플렉션 없이 생성했다면 컴파일타임에 잡아낼 수 있었을 예외들이다.(명령줄 인수를 일부러 잘못입력해보면 여섯 가지 예외를 모두 발생시킬 수 있다.)
- 클래스 이름만으로 인스턴스를 생성해내기 위해 무려 25줄이나 되는 코드를 작성하는데 생성자 호출 한줄로 끝났을 일이다.
리플렉션은 런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드, 필드와의 의존성을 관리할 때 적합하다.
- 버전이 여러 개 존재하는 외부 패키지를 다룰 때 유용하다.
- 가동할 수 있는 최소한의 환경으로 가장 오래된 버전만을 지원하도록 컴파일한 후, 이후 버전의 클래스와 메서드 등은 리플렉션으로 접근하는 방식이다.
- 위와 같이 하려면 접근하려는 새로운 클래스나 메서드가 런타임에 존재하지 않을 수 있다는 사실을 반드시 감안해야 한다.
- 같은 목적을 이룰 수 있는 대체 수단을 이용하거나 기능을 줄여 동작하는 등의 적절한 조치를 취해야 한다.
⇒ 적절한 인터페이스나 상위 클래스를 이용할 수 있을 것이다.(아이템 64)
리플렉션은 복잡한 특수 시스템을 개발할 때 필요한 강력한 기능이지만, 단점도 많다. 컴파일타임에는 알 수 없는 클래스를 사용하는 프로그램을 작성한다면 리플렉션을 사용할 것이다. 단, 되도록 객체 생성에만 사용하고, 생성한 객체를 이용할 때는 적절한 인터페이스나 컴파일타임에 알 수 있는 상위 클래스로 형변환해서 사용해야 한다.
아이템 66 : 네이티브 메서드는 신중히 사용하라
- 자바 네이티브 인터페이스[JNI(Java Native Interface)]는 자바 프로그램이 네이티브 메서드를 호출하는 기술로써 C나 C++같은 네이티브 프로그래밍 언어로 작성한 메서드를 호출함을 말한다.
네이티브 메서드의 세 가지 쓰임
- 레지스트리 같은 플랫폼 특화 기능을 사용한다.
- 네이티브 코드로 작성된 기존 라이브러리를 사용한다.
- 레거시 데이터를 사용하는 레거시 라이브러리가 예이다.
- 성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성한다.
성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 거의 권장하지 않는다.
- 자바가 성숙해지면서(OS 같은) 하부 플랫폼의 기능들을 점차 흡수하고 있고 자바 9는 새로 process API를 추가해 OS 프로세스에 접근하는 길을 열어주었다.
- 대체할 만한 자바 라이브러리가 없는 네이티브 라이브러리를 사용해야 할 때도 네이티브 메서드를 써야 한다.
GNU 다중 정밀 연산 라이브러리(GMP)를 필두로 개선 작업을 계속해왔기 때문에 고성능의 다중 정밀 연산이 필요한 자바 프로그래머는 네이티브 메서드를 통해 GMP를 사용하는걸 고려해도 좋다.
네이티브 메서드의 단점
- 네이티브 메서드는 안전하지 않으므로 네이티브 메서드를 사용하는 애플리케이션도 메모리 훼손 오류로부터 더 이상 안전하지 않다.
- 자바보다 플랫폼을 많이 타서 이식성도 낮다.
- 디버깅도 어렵고 주의하지 않으면 속도가 오히려 느려질 수도 있다.
- 가비지 컬렉터가 네이티브 메모리는 자동 회수하지 못하고, 심지어 추적조차 할 수 없다.
- 자바 코드와 네이티브 코드의 경계를 넘나들 때마다 비용도 추가 된다.
- 네이티브 메서드와 자바 코드 사이의 '접착 코드'를 작성해야 하는데 귀찮은 작업이기도 하고 가독성도 떨어진다.
⇒ 안전하지 않으므로(아이템50), 가비지컬렉터(아이템8)
네이티브 메서드를 사용하려거든 한번 더 생각하라. 네이티브 메서드가 성능을 개선해주는 일은 많지 않다. 저수준 자원이나 네이티브 라이브러리를 사용해야만 해서 어쩔 수 없더라도 네이티브 코드는 최소한만 사용하고 철저히 테스트 하라.
아이템 67 : 최적화는 신중히 하라
모든 사람이 마음 깊이 새겨야 할 최적화 격언 세 가지를 소개한다.
- (맹목적인 어리석음을 포함해) 그 어떤 핑계보다 효율성이라는 이름 아래 행해진 컴퓨팅 최악은 더 많다(심지어 효율을 높이지도 못하면서). -윌리엄 울프)
- (전체의 97%정도인) 자그마한 효율성은 모두 잊자. 섣부른 최적화가 만악의 근원이다. -도널드 크누스-
- 최적화를 할 때는 다음 두 규칙을 따르라.
- 첫 번째 하지 마라.
- (전문가 한정) 아직 하지 마라. 다시 말해, 완전히 명백하고 최적화되지 않은 해법을 찾을 때까지는 하지 마라. -M. A. 잭슨
빠른 프로그램보다는 좋은 프로그램을 작성하라.
- 성능 때문에 견고한 구조를 희생하지 말자.
- 좋은 프로그램이지만 원하는 성능이 나오지 않는다면 그 아키텍처 자체가 최적화할 수 있는 길을 안내해줄 것이다.
- 좋은 프로그램은 정보 은닉 원칙을 따르므로 개별 구성요소의 내부를 독립적으로 설계할 수 있어서 시스템의 영향을 주지 않고도 각 요소를 다시 설계할 수 있다.
- 프로그램을 완성할 때까지 성능 문제를 무시하라는 의미는 아니며 구현상의 문제는 나중에 최적화해 해결할 수 있지만, 아키텍처의 결함이 성능을 제한하는 상황이라면 시스템 전첼ㄹ 다시 작성하지 않고는 해결하기 불가능할 수 있다.
- 완성된 설계의 기본 틀을 변경하려다 보면 유지보수하거나 개선하기 어려운 꼬인 구조의 시스템이 만들어지기 쉽기 때문에 설계 단계에서 성능을 반드시 염두에 두어야 한다.
성능을 제한하는 설계를 피하라.
- 완성 후 변경하기가 가장 어려운 설계 요소는 바로 컴포넌트나 외부 시스템과의 소통 방식이다. API, 네트워크 프로토콜, 영구 저장용 데이터 포맷 등이 대표적이다.
- 완성 후에는 변경하기 어렵거나 불가능할 수 있으며, 동시에 시스템 성능을 심각하게 제한할 수 있다.
API를 설계할 때 성능에 주는 영향을 고려하라.
- public 타입을 가변으로 만들면 내부 데이터를 변경할 수 있게 만들어 불필요한 방어적 복사를 수 없이 유발할 수 있다.
- 컴포지션으로 해결할 수 있음에도 상속 방식으로 설계한 public 클래스는 상위 클래스에 영원히 종속되며 그 성능까지도 물려받게 된다.
- 인터페이스도 있는데 굳이 구현 타입을 사용하는 것 역시 좋지 않으며 특정 구현체에 종속되어 더 빠른 구현체가 나오더라도 이용하지 못하게 된다.
성능을 위해 API를 왜곡하는건 매우 안 좋은 생각이다.
- 다행히 잘 설계된 API는 성능도 좋은 게 보통이다.
- API를 왜곡하도록 만든 성능 문제는 해당 플랫폼이나 아랫단 소프트웨어의 다음 버전에서 사라질 수도 있지만 왜곡된 API와 이를 지원하는데 따르는 고통은 영원히 계속될 것이다.
- 신중하게 설계하여 깨끗하고 명확하고 멋진 구조를 갖춘 프로그램을 완성한 다음에야 최적화를 고려해볼 차례가 되고 성능에 만족하지 못할 경우에는 한정된다.
잭슨이 소개한 최적화 규칙 두 가지에 하나를 더 추가한다면 '각각의 최적화 시도 전후로 성능을 측정하라'
- 첫 번째 "하지 마라", 두 번째 "(전문가 한정) 아직 하지 마라" 세 번째 "각각의 최적화 시도 전후로 성능을 측정하라"가 되겠다.
- 시도한 최적화 기법이 성능을 눈에 띄게 높이지 못하는 경우가 많고 심지어 더 나빠지게 할 때도 있는데 주요 원인은 프로그램에서 시간을 잡아먹는 부분을 추측하기 어려움에 있다.
- 일반적으로 90%의 시간을 단 10%의 코등레서 사용한다는 사실을 기억해두자.
- 프로파일링 도구(profiling tool)는 최적화 노력을 어디에 집중해야 할지 찾는데 도움을 준다.
- 개별 메서드의 소비 시간과 호출 횟수 같은 런타임 정보를 제공하여, 집중할 곳은 물론 알고리즘을 변경해야 한다는 사실을 알려주기도 한다.
- 그 외에 jmh도 언급할만한 도구로 자바 코드의 상세한 성능을 알기 수비게 보여주는 벤치마킹 프레임워크이다.
프로그래머가 작성한 코드와 CPU에서 수행하는 명령 사이의 '추상화 격차'가 커서 최적화로 인한 성능 변화를 일정하게 예측하기가 그만큼 어렵다.
- 자바의 성능 모델은 정교하지 않을뿐더러 구현 시스템, 릴리스, 프로세서마다 차이가 있다.
- 여러분의 프로그램을 여러 가지 자바 플랫폼이나 여러 하드웨어 플랫폼에서 구동한다면 최적화의 효과를 그 각각에서 측정하다보면 다른 구현 혹은 하드웨어 플랫폼 사이에서 성능을 타협해야 하는 상황도 마주할 것이다.
⇒ 시스템의 나머지에 영향을 주지 않고도 각 요소를 다시 설계할 수 있다(아이템15), 방어적 복사(아이템50), 상속 방식(아이템18), 인터페이스를 사용하라(아이템 64), 불변(아이템17)
빠른 프로그램을 작성하려 안달하지 말자. 좋은 프로그램을 작성하다 보면 성능은 따라오기 마련이다. 하지만 시스템을 설계할 때, 특히 API, 네트워크 프로토콜, 영구 저장용 데이터 포맷을 설계할 때는 성능을 염두에 두어야 한다.
시스템 구현을 완료했다면 이제 성능을 측정해보라. 충분히 빠르면 그것으로 끝이고 그렇지 않다면 프로파일러를 사용해 문제의 원인이 되는 지점을 찾아 최적화를 수행하라.
아이템 68 : 일반적으로 통용되는 명명 규칙을 따르라
- 철자 규칙은 패키지, 클래스, 인터페이스, 메서드, 필드, 타입 변수의 이름을 다루고 특별한 이유가 없는 한 반드시 따라야 한다.
- 규칙을 어긴 API는 사용하기 어렵고, 유지보수 하기 어렵다
- 철자 규칙이나 문법 규칙을 어기면 다른 프로그래머들이 코드를 읽기 번거로울 뿐 아니라 다른 뜻으로 오해할 수도 있고 그로 인해 오류까지 발생할 수 있다.
규칙
- 패키지와 모듈 이름은 각 요소를 점(.)으로 구분하여 계층적으로 짓는다.
- 요소들은 모두 소문자 알파벳 혹은 (드물게) 숫자로 이뤄진다.
- 조직 바깥에서도 사용될 패키지라면 조직의 인터넷 도메인 이름을 역순으로 사용하고 예외적으로 표준 라이브러리와 선택적 패키지들은 각각 java와 javax로 시작한다.
- 패키지 이름의 나머지는 해당 패키지를 설명하는 하나 이상의 요소로 이뤄진다.
- 각 요소는 일반적으로 8자 이하의 짧은 단어로 한다.
- utilities보다는 util처럼 의미가 통하는 약어를 추천한다.
- 요소의 이름은 보통 한 단어 혹은 약어로 이뤄진다.
- 인터넷 도메인 이름 뒤에 요소 하나만 붙인 패키지가 많지만, 많은 기능을 제공하는 경우엔 계층을 나눠 더 많은 요소로 구성해도 좋다.
- (열거 타입과 애너테이션을 포함해) 클래스와 인터페이스의 이름은 하나 이상의 단어로 이뤄지며, 각 단어는 대문자로 시작한다.(List, FutherTask 등)
- 여러 단어의 첫 글자만 딴 약자는 max, min처럼 널리 통용되는 줄임말을 제외하고는 단어를 줄여 쓰지 않도록 한다.
- 약자의 경우 첫 글자만 대문자로 할지 전체를 대문자로 할지는 살짝 논란이 있다. 전체를 대문자로 쓰는 프로그래머도 있지만, 첫 글자만 대문자로 하는 쪽이 훨씬 많다.(HttpUrl)
- 메서드와 필드 이름은 첫 글자를 소문자로 쓴다는 점만 빼면 클래스 명명 규칙과 같다.(remove, ensureCapacity) 첫 단어가 약자라면 단어 전체가 소문자여야 한다.
- 상수 필드를 구성하는 단어는 모두 대문자로 쓰며 단어 사이는 밑줄로 구분한다.(VALUES, NEGATIVE_INFINITY 등) 이름에 밑줄은 상수 필드가 유일하다.
- 지역 변수에도 다른 멤버와 비슷한 명명 규칙이 적용된다. 약어를 써도 좋은데 변수가 사용되는 문맥에서 의미를 쉽게 유추할 수 있기 때문이다.
- 타입 매개변수 이름은 보통 한 문자로 표현되며 아래 다섯 가지 중 하나이다.
- 임의의 타입은 T (Type)
- 컬렉션 원소의 타입은 E (Element)
- 맵의 키와 값에는 K와 V (Key 와 Value)
- 예외에는 X (eXception)
- 메서드의 반환 타입에는 R (Return)
- 문법 규칙은 철자 규칙과 비교하면 더 유연하고 논란도 많은데 패키지에 대한 규칙은 따로 없다
- 객체를 생성할 수 있는 클래스(열거 타입 포함)의 이름은 보통 단수 명사나 명사구를 사용한다.(Thread, PriorityQueue, ChessPiece 등)
- 객체를 생성할 수 없는 클래스의 이름은 보통 복수행 명사로 짓는다.(Collectors, Collections 등)
- 인터페이스 이름은 클래스와 똑같이 짓거나(Collection, Comparator 등), able 혹은 ible로 끝나는 형용사로 짓는다.(Runnable, Iterable, Accessible)
- 애너테이션은 워낙 다양하게 활용되어 지배적인 규칙이 없이 명사, 동사, 전치사, 형용사가 두루 쓰인다.(BindingAnnotation, Inject, ImplementedBy, Singleton 등)
- 어떤 동작을 수행하는 메서드의 이름은 동사나(목적어를 포함한) 동사구로 짓는다.(append, drawImage)
- boolean 값을 반환하는 메서드라면 보통 is나 (드물게) has로 ㅅ작하고 명사나 명사구, 혹은 형용사로 기능하는 아무 단어나 구로 끝나도록 짓는다.(isDigit, isProbablePrime, isEmpty, isEnabled, hassSibling 등)
- 반환 타입이 boolean이 아니거나 해당 인스턴스의 속성을 반환하는 메서드의 이름은 보통 명사, 명사구, 혹은 get으로 시작하는 동사구로 짓는다.(size, hashCode, getTime 등)
- get으로 시작하는 형태만 써야 한다는 주장도 있지만, 근거가 빈약하다. 다음 코드에서 보듯 보통은 처음 두 형태를 사용한 코드의 가독성이 더 좋기 떄문이다.
if (car.speed() > * SPEED_LIMIT) generateAudibleAlert("경찰 조심하세요!");
- 한편 클래스가 한 속성의 게터와 세터를 모두 제공할 때도 적합한 규칙으로 이런 경우라면 보통 getAttribute와 setAttribute 형태의 이름을 갖게 될 것이다.
식별자 타입 | 예 |
패키지와 모듈 | org.junit.jupiter.api, com.google.common.collect |
클래스와 인터페이스 | Stream, FutureTask, LinkedHashMap, HttpClient |
메서드와 필드 | remove, groupingBy, getCrc |
상수 필드 | MIN_VALUE, NEGATIBE_INFINITY |
지역변수 | i, denmon, houseNum |
타입 매개변수 | T, E, K, V, X, R, U, V, T1, T2 |
꼭 언급해야 하는 특별한 메서드 이름 몇가지
- 객체의 타입을 바꿔서 다른 타입의 또 다른 객체를 반환하는 인스턴스 메서드의 이름은 보통 toType 형태로 짓는다.(toString, toArray 등)
- 객체의 내용을 다른 뷰로 보여주는 메서드의 이름은 asType형태로 짓는다(asList 등)
- 객체의 값을 기본 타입 값으로 반환하는 메서드의 이름은 보통 typeValue 형태로 짓는다.(intValue 등)
- 정적 팩터리의 이름은 다양하지만 from, of, valueOf, instance, getInstance, newInstance, getType, newType을 흔히 사용한다.
필드 이름에 관한 문법 규칙은 클래스, 인터페이스, 메서드 이름에 비해 덜 명확하고 덜 중요하다.
- API 설계를 잘 했다면 필드가 직접 노출될 일이 거의 없기 때문이다.
- boolean 타입의 필드 이름은 보통 boolean 접근자 메서드에서 앞 단어를 뺀 형태다(initialized, composite 등)
- 다른 타입의 필드라면 명사나 명사구를 사용한다.(height, digits, bodyStyle 등)
- 지역변수 이름도 필드와 비슷하게 지으면 되나, 조금 더 느슨하다.
⇒ 불변 참조 타입(아이템17), 객체를 생성할 수 없는 클래스(아이템4), 다른 뷰로 보여주는 메서드(아이템6), 정적팩터리(아이템1)
표준 명명 규칙을 체화하여 자연스럽게 베어 나오도록 하자.
철자 규칙은 직관적이라 모호한 부분이 적은 데 반해, 문법 규칙은 더 복잡하고 느슨하다.
자바 언어 명세의 말을 인용하면 "오랫동안 따라온 규칙과 충돌한다면 그 규칙을 맹종해서는 안 된다." 상식이 이끄는 대로 따르자.
'나(다) > 책' 카테고리의 다른 글
이펙티브 자바 - 11장 : 동시성 (0) | 2021.02.06 |
---|---|
이펙티브 자바 - 10장 : 예외 (5) | 2021.01.26 |
이펙티브 자바 - 8장 : 메서드 (0) | 2021.01.18 |
읽기 좋은 코드가 좋은 코드다 (5) | 2021.01.15 |
내 삶의 주인으로 산다는 것 (4) | 2021.01.14 |