-
Notifications
You must be signed in to change notification settings - Fork 0
Description
section: 9장 일반적인 프로그래밍 원칙
- Item 57 지역변수 범위를 최소화하라
- Item 58 전통적인 for문보다는 for-each문을 사용하라
- Item 59 라이브러리를 익히고 사용하라
- Item 60 정확한 답이 필요하다면 float와 double은 피하라
🍵 서론
본론으로 바로 가자
🌒 본론
57. 지역변수 범위를 최소화하라
💡 기본적으로 “클래스와 멤버의 접근 권한을 최소화하라”고 한 아이템 15와 취지가 비슷하다.
지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고, 오류 가능성은 낮아진다.
Java는 C와 다르게 문장을 선언할 수 있는 곳이면 어디서든 변수를 선언할 수 있다.
가장 처음 쓰일 때 선언하기
사용하려면 멀었는데, 미리 선언부터 해두면 코드가 어수선해져 가독성이 떨어진다.
또한 변수가 쓰이는 범위보다 너무 앞서 선언하면, 다 쓴 뒤에도 여전히 살아있게 되기 쉽다.
거의 모든 지역변수는 선언과 동시에 초기화해야 한다.
초기화에 필요한 정보가 충분하지 않다면 충분해질 때까지 선언을 미뤄야 한다.
하지만 try-catch문은 이 규칙에서 예외다.
변수를 초기화하는 표현식에서 검사 예외를 던질 가능성이 있다면 try 블록 안에서 초기화해야 한다.
한편, 변수 값을 try 블록 바깥에서도 사용해야 한다면, 비록 정확히 초기화하진 못하더라도 try 블록 앞에서 선언해야한다.
반복 변수의 값을 반복문이 종료된 뒤에도 써야하는 상황이 아니라면 while문보다 for문을 사용하라.
반복자를 사용해야하는 상황이면 for-each문 대신 전통적인 for문을 사용하라.
for (Iterator<Element> i = c.iterator(); i.hasNext();) {
Element e = i.next();
}다음 두 while문을 보면, 앞서 for문이 더 나은 이유를 알 수 있다.
Iterator<Element> i1 = c1.iterator();
while (i1.hasNext()) {
doSomething(i1.next());
}
Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) { // 버그!
doSomething(i2.next());
}
// 두번째 while문에는 복사 붙여넣으면서 발생한 오류가 있다.이렇게 프로그램 오류가 겉으로 드러나지 않으니 오랜 기간 발견되지 않을 수도 있다.
for문을 사용하면 이런 복사 붙여넣기 오류를 컴파일 타임에 잡을 수 있다.
첫번째 반복문이 사용한 원소와 반복자의 유효 범위가 반복문 종료와 함께 끝나기 때문이다.
또한, 변수 유효 범위가 for문 범위와 일치하여 똑같은 이름의 변수를 여러 반복문에서 써도 서로 아무런 영향을 주지 않는다.
또한, while문보다 짧아서 가독성이 좋다는 점이다.
마지막으로, 반복 때마다 다시 계산해야하는 비용을 없앤다.
for (int i = 0; n = expensiveComputation(); i < n; i++) {
...
}메서드를 작게 유지하고 한 가지 기능에 집중하는 것이다.
한 메서드에서 여러 가지 기능을 처리한다면, 그 중 한 기능과만 관련된 지역변수라도 다른 기능을 수행하는 코드에서 접근해버릴 수 있다.
해결책은 간단하다.
메서드를 기능별로 쪼개면 된다.
58. 전통적인 for문보다는 for-each문을 사용하라
아이템 45에서 이야기했듯, 스트림이 제격인 작업이 있고, 반복이 제격인 작업이 있다.
for (Iterator<Element> i = c.iterator(); i.hasNext();) {
Element e = i.next();
}for (int i = 0; i < a.length(); i++) {
...
}이 관용구들은 while문보다는 낫지만, 가장 좋은 방법은 아니다.
1회 반복에서 반복자는 세 번 등장하며, 인덱스는 네 번이나 등장하여 변수를 잘못 사용할 틈새가 넓어진다.
혹시라도 잘못된 변수를 사용했을 때, 컴파일러가 잡아주리라는 보장도 없다.
마지막으로, 컬렉션이냐 배열이냐에 따라 코드 형태가 상당히 달라지므로 주의해야한다.
우리에게 진짜 필요한건 원소들 뿐이다.
이상의 문제는 for-each문을 사용하면 모두 해결된다.
참고로 정식 이름은 향상된 for문(Enhanced for statement)이다.
반복자와 인덱스 변수를 사용하지 않으니 코드가 깔끔해지고 오류가 날 일도 없다.
하나의 관용구로 컬렉션과 배열을 모두 처리할 수 있어서 어떤 컨테이너를 다루는지 신경쓰지 않아도 된다.
for (Element e : elements) {
...
}여기서 콜론(:)은 안의(in)라고 읽으면 된다.
따라서 이 반복문은 elements 안의 각 원소 e에 대해라고 읽는다.
컬렉션을 중첩해 순회해야한다면, for-each문의 이점이 더욱 커진다.
버그를 찾아보자.
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()` 정말 운이 나빠서 바깥 컬렉션의 크기가 안쪽 컬렉션 크기의 배수라면, 이 반복문은 예외를 던지지 않고 종료한다.for-each문
for (Suit suit : suits)
for (Rank rank : ranks)
deck.add(new Card(suit, rank));for-each문을 사용할 수 없는 상황
-
파괴적인 필터링(Destructive Filtering)
컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 반복자 remove 메서드를 호출해야한다.
Java8부터는 Collection의 removeIf 메서드를 사용해 컬렉션을 명시적으로 순회하는 일을 피할 수 있다. -
변형(Transforming)
리스트나 배열을 순회하면서 그 원소의 값 일부, 혹은 전체를 교체해야 한다면, 리스트의 반복자나 배열의 인덱스를 사용해야한다. -
병렬 반복(Parallel Iteration)
여러 컬렉션을 병렬로 순회해야한다면, 각각의 반복자나 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야한다.
세가지 상황 중 하나에 속할 때는 일반적인 for문을 사용하되, 이번 아이템에서 언급한 문제들을 경계하기 바란다.
for-each문은 컬렉션과 배열은 물론 Iterable 인터페이스를 구현한 객체라면 무엇이든 순회할 수 있다.
public interface Iterable<E> {
Iterator<E> iterator();
}정리
전통적인 for문과 비교했을 때 for-each문은 명료하고, 유연하고, 버그를 예방해준다.
성능 저하도 없다.
가능한 모든 곳에서 for문이 아닌 for-each문을 사용하자.
59. 라이브러리를 익히고 사용하라
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}괜찮은 듯 보여도 여러가지 문제를 내포하고 있다.
- n이 그리 크지 않은 2의 제곱수라면 얼마 지나지 않아 같은 수열이 반복된다.
- n이 2의 제곱수가 아니라면 몇몇 숫자가 평균적으로 더 자주 반환된다. n 값이 크면 이 현상은 두드러진다.
- 지정한 범위 바깥의 수가 종종 튀어나올 수 있다.
random 메서드가 이상적으로 동작한다면 약 50만개가 출력돼야 하지만, 실제로 돌려보면 666,666에 가까운 값을 얻는다. 무작위로 생성된 수중에서 2/3 가량이 중간값보다 낮은 쪽에 쏠린 것이다.
이 결함을 해결하려면 의사난수 생성기, 정수론, 2의 보수 계산 등에 조예가 깊어야 한다.
하지만 다행히 여러분이 직접 해결할 필요는 없다.
Random.nextInt(int)가 이미 해결해놨으니 말이다.
이 메서드의 자세한 동작 방식은 몰라도 된다. 알고리즘에 능통한 개발자가 설계, 구현과 검증에 시간을 들여 개발했고, 이 분야의 여러 전문가가 잘 동작함을 검증해줬다.
💡 이렇게 표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 여러분보다 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있다.
Java7부터는 Random을 더 이상 사용하지 않는게 좋다.
ThreadLocalRandom으로 대체하면 대부분 잘 작동한다. Random보다 더 고품질의 무작위 수를 생성할 뿐 아니라 속도도 더 빠르다.
한편, 포크-조인 풀이나 병렬 스트림에서는 SplittableRandom을 사용하라.
표준 라이브러리를 쓰는 두번째 이점은
💡 핵심적인 일과 크게 관련 없는 문제를 해결하느라 시간을 허비하지 않아도 된다는 것이다.
프로그래머들은 하부 공사를 하기보다는 애플리케이션 기능 개발에 집중하고 싶어한다.
여러분도 마찬가지일 것이다.
세번째 이점은
💡 따로 노력하지 않아도 성능이 지속해서 개선된다는 점이다.
사용자가 많고, 업계 표준 벤치마크를 사용해 성능을 확인하기 때문에 표준 라이브러리 제작자들은 더 나은 방법을 꾸준히 모색할 수 밖에 없다.
Java 플랫폼 라이브러리의 많은 부분이 수년에 걸쳐 지속해서 다시 작성되며, 때론 성능이 극적으로 개선되기도 한다.
네번째 이점은
💡 기능이 점점 많아진다는 것이다.
라이브러리에 부족한 부분이 있다면 개발자 커뮤니티에서 이야기가 나오고 논의된 후 다음 릴리스에 해당 기능이 추가되곤 한다.
마지막 이점은
💡 여러분이 작성한 코드가 많은 사람에게 낯익은 코드가 된다는 것이다.
자연스럽게 다른 개발자들이 더 읽기 좋고, 유지보수하기 좋고, 재활용하기 쉬운 코드가 된다.
실상은?
이상의 이점들에 비춰볼 때 표준 라이브러리의 기능을 사용하는 것이 좋아 보이지만, 실상은 많은 프로그래머가 직접 구현해 쓰고 있다. 왜 그럴까?
아마도 라이브러리에 그런 기능이 있는지 모르기 때문일 것이다.
메이저 릴리스마다 주목할 만한 수많은 기능이 라이브러리에 추가된다.
Java는 메이저 릴리스마다 새로운 기능을 설명하는 웹페이지를 공시하는데, 한번쯤 읽어볼만 하다.
예시 [Java8-feat, Java9-feat]
지정한 URL의 내용을 가져오는 명령줄 애플리케이션을 작성해보겠다.(리눅스의 curl 명령을 생각하면 된다.)
예전에는 작성하기가 까다로운 기능이었지만, Java9에서 InputStream에 추가된 transferTo 메서드를 사용하면 쉽게 구현할 수 있다.
public static void main(String[] args) throws IOException {
try (InputStream in = new URL(args[0]).openStream()) {
in.transferTo(System.out);
}
}라이브러리가 너무 방대하여 모든 API 문서를 공부하기는 벅차겠지만, Java 프로그래머라면 적어도 java.lang, java.util, java.io와 그 하위 패키지들에는 익숙해져야 한다.
언급해둘 만한 라이브러리 몇 개
컬렉션 프레임워크와 스트림 라이브러리(아이템 45-48)다.
java.util.concurrent의 동시성 기능도 마찬가지로 알아두면 큰 도움이 된다.
java.util.concurrent
- Atomic
- 스레드로부터 안전한 가변 변수를 제공
- Lock
- Condition
- 스레드가 특정 조건에서 대기하고 있다가 특정 조건이 참이 되면 깨어날 수 있는 기능을 제공
기본 스레드 에서 wait()와 notify()는 스레드를 구분하여 처리하는 것이 불가능 하였다면 Condition은 wait()와 signal() 이라는 함수를 제공하여 스레드에 따른 처리를 할 수 있게 만들어 줍니다.
- 스레드가 특정 조건에서 대기하고 있다가 특정 조건이 참이 되면 깨어날 수 있는 기능을 제공
- Condition
- ConcurrentHashMap
- 표준 HashMap의 동시 버전
- BlockingQueue
- ThreadLocal
정리
💡 바퀴를 다시 발명하지 말자.
아주 특별한 나만의 기능이 아니라면 누군가 이미 라이브러리 형태로 구현해놓았을 가능성이 크다.
코드 품질에도 규모의 경제가 적용된다.
60. 정확한 답이 필요하다면 float와 double은 피하라
float와 double 타입은 과학과 공학 계산용으로 설계되었다. 이진 부동소수점 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 ‘근사치’로 계산하도록 세심하게 설계되었다. 따라서 정확한 결과가 필요할 때는 사용하면 안된다.
💡 float와 double 타입은 특히 금융 관련 계산과는 맞지 않는다.
0.1 혹은 10의 음의 거듭 제곱 수(10^-1 등)를 표현할 수 없기 때문이다.
System.out.println(1.03 - 0.42); // 0.61000000000001
System.out.println(1.00 - 9 * 0.10); // 0.099999999998금융 계산에는 BigDecimal, int 혹은 long을 사용해야 한다.
하지만 BigDecimal에는 단점이 두가지 있다. 기본 타입보다 쓰기가 훨씬 불편하고, 훨씬 느리다.
int 혹은 long 타입은 다룰 수 있는 값의 크기가 제한되고, 소수점을 직접 관리해야 한다.
소수점 추적은 시스템에 맡기고, 코딩 시의 불편함이나 성능 저하를 신경쓰지 않겠다면…
BigDecimal을 사용하라.
BigDecimal이 제공하는 여덟가지 반올림 모드를 이용하여 반올림을 완벽히 제어할 수 있다.
반면, 성능이 중요하고, 소수점을 직접 추적할 수 있고, 숫자가 너무 크지 않다면, int나 long을 사용하라.
9자리 십진수 → int
18자리 십진수 → long
18자리 초과 → BigDecimal
