Effective Java 3rd - Ch09
포스트
취소

Effective Java 3rd - Ch09

Java 기초를 다지기 위해 효과적인 자바란 책을 공부 중입니다.

제9장 일반적인 프로그래밍 원칙

  • 자바 언어의 핵심 요소에 집중한다
  • 지역변수, 제어구조, 라이브러리, 데이터 타입, 최적화와 명명규칙
  • 언어의 경계를 넘나드는 리플렉션과 네이티브 메서드

57) 지역변수의 범위를 최소화하라

기본적으로 항목 15 ‘클래스와 멤버의 접근 권한을 최소화하라’와 취지가 비슷하다.

지역변수의 범위를 줄이는 가장 강력한 기법은 역시 가장 처음 쓰일 때 선언하기이다.

  • 사용하려면 멀었는데 미리 선언부터 해두는 경우
  • 다 쓴 뒤에도 여전히 살아 있게 되거나

거의 모든 지역변수는 선언과 동시에 초기화해야 한다.

  • 초기화에 필요한 정보가 충분하지 않다면 충분해질 때까지 선언을 미뤄야 한다
  • try-catch 문은 이 규칙에서 예외

반복문은 독특한 방식으로 변수 범위를 최소화해준다.

  • while 문 보다는 for 문을 쓰는 편이 낫다
  • for-each 문 대신 전통적인 for 문을 쓰는 것이 낫다

코드 57-1 컬렉션이나 배열을 순회하는 권장 관용구

1
2
3
for( Element e : c ){
    ... // e로 무언가를 한다.
}

코드 57-2 반복자가 필요할 때의 관용구

1
2
3
4
for( Iterator<Element> i = c.iterator(); i.hasNext(); ){
    Element e = i.next();
    ... // e와 i로 무언가를 한다.
}

while 문보다 for 문이 더 나은 이유

1
2
3
4
5
6
7
8
9
10
11
Iterator<Element> i = c.iterator();
while( i.hasNext() ){
    doSomething( i.next() );
}
...

// 프로그램 오류가 겉으로 드러나지 않아 오래 발견 안될 수 있다
Iterator<Element> i2 = c2.iterator();
while( i.hasNext() ){           // 버그!! : 붙여넣기 오류
    doSomething( i2.next() );
}

전통적인 for 문에서의 상황

1
2
3
4
5
6
7
8
9
10
for( Iterator<Element> i = c.iterator(); i.hasNext(); ){
    Element e = i.next();
    ... // e와 i로 무언가를 한다.
}

// 다음 코드는 "i를 찾을 수 없다"는 컴파일 오류를 낸다
for( Iterator<Element> i2 = c2.iterator(); i.hasNext(); ){
    Element e2 = i2.next();
    ... // e2와 i2로 무언가를 한다.
}

for 문의 장점

  • 붙여넣기 오류를 줄여준다
  • 변수 유효범위가 있어 똑같은 이름의 변수를 여러 반복문에 써도 서로 영향을 주지 않는다
  • while 문보다 짧아서 가독성이 좋다
1
2
3
4
// 변수의 i의 한계값을 변수 n에 저장하여, 반복 때마다 다시 계산해야 하는 비용을 없앴다
for( int i=0, n=expensiveComputation(); i<n; i++ ){
    ... // i로 무언가를 한다
}

메서드를 작게 유지하고 한가지 기능에 집중하는 것이다.

  • 한 메서드에서 여러 가지 기능을 처리한다면 한 기능에만 관련된 변수라도 다른 기능을 수행하는 코드에서 접근할 수 있다
  • 해결책은 메서드를 기능별로 쪼개는 것이다

58) 전통적인 for문 보다는 for-each문을 사용하라

스트림이 제격인 작업이 있고 반복이 제격인 작업이 있다.

코드 58-1 컬렉션 순회하기 - 더 나은 방법이 있다

1
2
3
4
for( Iterator<Element> i = c.iterator(); i.hasNext(); ){
    Element e = i.next();
    ... // e로 무언가를 한다
}

코드 58-2 배열 순회하기 - 더 나은 방법이 있다

1
2
3
for( int i=0; i<a.length; i++ ){
    ... // a[i]로 무언가를 한다
}

반복자와 인덱스 변수는 모두 코드를 지저분하게 할 뿐, 우리에게 진짜 필요한 건 원소들 뿐이다.

:arrow_right: 이상의 문제는 ‘for-each 문’을 사용하면 모두 해결된다.
:arrow_right: for-each 의 정식 명칭은 ‘향상된 for 문(enhanced for statement)’이다

  • 코드가 깔끔해지고 오류가 날 일도 없다.
  • 어떤 컨테이너를 다루는지 신경쓰지 않아도 된다.

코드 58-3 컬렉션과 배열을 순회하는 올바른 관용구

1
2
3
for( Element e : elements ){
    ... // e로 무언가를 한다
}

코드 58-4 버그를 찾아보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Card {
    private final Suit suit;
    private final Rank rank;

    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());

    Card(Suit suit, Rank rank ) {
        this.suit = suit;
        this.rank = rank;
    }

    public static void main(String[] args) {
        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()가 왜 두개나 한문장에?
    }
}

코드 58-6 문제는 고쳤지만 보기 좋진 않다. 더 나은 방법이 있다!

1
2
3
4
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()));

코드 58-7 컬렉션이나 배열의 중첩 반복을 위한 권장 관용구

1
2
3
for (Suit suit : suits)
    for (Rank rank : ranks)
        deck.add(new Card(suit, rank));

다음은 같은 버그에 대한 또다른 증상이다.

코드 58-5 같은 버그, 다른 증상!

1
2
3
4
5
6
7
8
9
10
11
12
public class DiceRolls {
    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());
    }
}

코드 58-8 컬렉션이나 배열의 중첩 반복을 위한 권장 관용구

1
2
3
for (Face f1 : faces)
    for (Face f2 : faces)
        System.out.println(f1 + " " + f2);

for-each 문을 사용할 수 없는 상황 세가지

  • 파괴적인 필터링(destructive filtering)
    • 컬렉션을 순회하면서 선택된 원소를 제거해야 한는데, 반복자의 remove 메서드를 호출해야 한다
    • 자바 8부터는 Collection 의 removeIf 메서드를 사용
  • 변형(transforming)
    • 리스트나 배열을 순회하면서 그 원소의 값 일부 혹은 전체를 교체해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야 한다
  • 병렬 반복(parallel iteration)
    • 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 한다

:hand: 참고 : foreach를 사용하여 Java List의 element를 제거할 때 발생하는 문제

1
2
3
4
5
6
7
8
9
10
11
12
List<Integer> list = new ArrayList<Integer>();
// ... Add Integer elements in list

// 파괴적인 필터링
for (Integer i : list) {
    if(i==0) {              // i == 0 인 원소를 제거하고 싶다
        list.remove(i);     // ConcurrentModificationException 발생
    }
}

// 자바 8부터는 Collection 의 removeIf 메서드를 사용하여 명시적인 순회가 필요없다
list.removeIf( i -> i == 0 );

for-each 문은 컬렉션과 배열은 물론 Iterable 인터페이스를 구현한 객체라면 무엇이든 순회할 수 있다.

1
2
3
4
public interface Iterable<E> {
    // 이 객체의 원소들을 순회하는 반복자를 반환한다
    Iterator<E> iterator();
}

:hand: 참고1 : 자바 디자인 패턴 1 - Iterator

:hand: 참고2 : How to create a custom Iterator in Java?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Foo implements Iterable<Foo> {
    private final int value;

    public Foo(final int value) {
        this.value = value;
    }

    @Override
    public Iterator<Foo> iterator() {
        return new Iterator<Foo>() {
            private Foo foo = new Foo(0);

            @Override
            public boolean hasNext() {
                return foo.value < Foo.this.value;
            }

            @Override
            public Foo next() {
                if (!hasNext()) throw new NoSuchElementException();

                Foo cur = foo;
                foo = new Foo(cur.value+1);
                return cur;
            }
        };
    }

    public static void main(String[] args) {
        Foo foo = new Foo(10);
        for( Foo f: foo ){
            System.out.println(f.value);
        }
    }
}

:hand: 참고3 : Can we write our own iterator in Java?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// custom Iterator class
public class FactorialIteartor implements Iterator<Integer> {

    private final Integer mNumber;
    private Integer mPosition;
    private Integer mFactorial;

    public FactorialIteartor(Integer number) {
        this.mNumber = number;
        this.mPosition = 1;
        this.mFactorial = 1;
    }

    @Override public boolean hasNext() {
        return mPosition <= mNumber;
    }

    @Override public Integer next() {
        if (!hasNext())
            return 0;

        mFactorial = mFactorial * mPosition;
        mPosition++;

        return  mFactorial;
    }
}

// custom Iterable class
public class FactorialIterable implements Iterable<Integer> {

    private final FactorialIteartor factorialIteartor;

    public FactorialIterable(Integer value) {
        factorialIteartor = new FactorialIteartor(value);
    }

    @Override
    public Iterator<Integer> iterator() {
        return factorialIteartor;
    }

    @Override
    public void forEach(Consumer<? super Integer> action) {
        Objects.requireNonNull(action);
        Integer last = 0;
        for( Integer t : this ){
            last = t;
        }
        action.accept(last);
    }
}

///////////////////////////////

FactorialIterable fi = new FactorialIterable(10);
Iterator<Integer> iterator = fi.iterator();
while( iterator.hasNext() ){
     System.out.println(iterator.next());
}

// shortly code for Java 1.8
new FactorialIterable(5).forEach(System.out::println);

:hand: 참고4 : what’s the point of having both Iterator.forEachRemaining() and Iterable.forEach()?

59) 라이브러리를 익히고 사용하라

바퀴를 다시 발명하지 말자.
아주 특별한 기능이 아니라면 누군가 이미 라이브러리 형태로 구현해 놓았을 것이다.

코드 59-1 흔하지만 문제가 심각한 코드 - 무작위 정수 생성

1
2
3
4
5
static Random rnd = new Random();

static int random(int n){
    return Math.abs(rnd.nextInt()) % n;
}

괜찮은듯 보여도 문제를 세가지나 내포하고 있다.

  • n 이 그리 크지 않은 2의 제곱수라면 얼마 지나지 않아 같은 수열이 반복된다
  • n 이 2의 제곱수가 아니라면 몇몇 숫자가 평균적으로 더 자주 반복된다 (n 값이 크면 이 현상은 더 두드러진다)
  • random 메서드의 결함으로 지정한 범위 ‘바깥’의 수가 종종 튀어나올 수 있다

랜덤 생성에 대한 또다른 예제를 보자.
예상으로는 약 50만개가 출력돼야 하지만, 실제로 돌려보면 66만개에 가까운 값을 얻는다.

1
2
3
4
5
6
7
8
9
10
// 선택한 범위에서 무작위 수를 백만개 생성한 다음
// 그중 중간 값보다 작은게 몇개인지 출력
public static void main(String[] args){
    int n = 2 * (Integer.MAX_VALUE / 3);        // 선택범위
    int low = 0;
    for(int i=0; i<1000000; i++)                // 100만개 임의의 수 생성
        if( random(n) < n/2 )                   // 중간값보다 작은지 비교
            low++;                              // 카운팅
    System.out.println( low );
}

자바 7부터는 ThreadLocalRandom 을 사용하라.

  • Random 보다 더 고품질의 무작위 수를 생성하고 속도도 더 빠르다
  • 한편 포크-조인 풀이나 병렬 스트림에서는 SplittableRandom 을 사용하라

:hand: 표준 라이브러리 사용시 이점

  • 그 코드를 작성한 전문가의 지식과 여러분보다 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있다.
  • 핵심적인 일과 크게 관련 없는 문제를 해결하느라 시간을 허비하지 않아도 된다는 것이다.
  • 따로 노력하지 않아도 성능이 지속해서 개선된다 (벤치마킹하며 다시 작성됨)
  • 기능이 점점 많아진다 (커뮤니티에서 논의되고 다음 릴리즈에 추가됨)
  • 여러분이 작성한 코드가 많은 사람에게 낯익은 코드가 된다 (읽기 좋고, 유지보수 좋고, 재활용 좋고)

그럼에도 실상은 많은 프로그래머가 직접 구현해 쓰고 있다. 그런 기능이 있는지 모르기 때문이다.

자바는 메이저 릴리즈마다 주목할 만한 수많은 기능이 라이브러리에 추가된다.
ex) 지정한 URL 의 내용을 가져오는 기능 : 자바 9에 추가된 InputStream.transferTo 메서드

코드 59-2 transferTo 메서드를 이용해 URL의 내용 가져오기 - 자바 9부터 가능하다

1
2
3
4
5
6
7
public class Curl {
    public static void main(String[] args) throws IOException {
        try (InputStream in = new URL(args[0]).openStream()) {
            in.transferTo(System.out);
        }
    }
}

자바 프로그래머라면 적어도 java.lang, java.util, java.io 와 그 하위 패키지들에는 익숙해져야 한다.

그 외 언급해둘 만한 라이브러리가 몇개 있다.

  • java.util.concurrent 의 동시성 기능
    • 멀티스레드 프로그래밍 작업을 단순화해주는 고수준의 편의 기능 제공

때로는 라이브러리가 여러분에게 필요한 기능을 충분히 제공하지 못할 수 있다.

  • 우선은 라이브러리를 사용하려 시도해보자
  • 원하는 기능이 아니라 판단되면 고품질의 서드파티 라이브러리를 사용하자
    • 구글의 Guava 라이브러리가 대표적

60) 정확한 답이 필요하다면 float 와 double은 피하라

float 와 double 타입은 정확한 경과가 필요할 때는 사용하면 안된다.

  • 과학과 공학 계산용으로 이진 부동소수점 연산에 쓰이며
  • 넓은 범위의 수를 빠르게 정밀한 ‘근사치’로 계산하도록 설계되었다
  • 0.1 혹은 10의 음의 거듭제곱 수(10^-1, 10^-2 등)를 표현할 수 없다
1
2
3
4
5
System.out.println( 1.03 - 0.42 );
// ==> 0.6100000000000001 을 출력

System.out.println( 1.00 - 9*0.10 );
// ==> 0.0999999999999998 을 출력

반올림을 하면 해결되리라 생각할지 모르지만, 그래도 틀린 답이 나올 수 있다.

코드 60-1 오류 발생! 금융 계산에 부동소수 타입을 사용했다.

1
2
3
4
5
6
7
8
9
10
11
// 0.1 달러짜리 10개를 살 수 있을 줄 알았다
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(itemsBought + "개 구입");
    System.out.println("잔돈(달러): " + funds);
}

실행해보면 사탕 3개를 구입한 후 잔돈은 0.3999999999999999999 달러가 남았다.

금융 계산에는 BigDecimal, int 혹은 long 을 사용해야 한다.

  • BigDecimal 생성자 중 문자열을 받는 생성자를 사용했음에 주목하자
    • 계산시 부정확한 값이 사용되는 걸 막기 위해 필요한 조치이다
  • BigDecimal 에는 단점이 두가지 있다
    • 기본 타입보다 쓰기가 훨씬 불편하고, 훨씬 느리다.
  • BigDemical 대안으로 int 혹은 long 타입을 쓸 수도 있다
    • 그럴 경우 다룰 수 있는 값의 크기가 제한되고 소수점을 직접 관리해야 한다

코드 60-2 BigDecimal을 사용한 해법. 속도가 느리고 쓰기 불편하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
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.subtract(price);
        itemsBought++;
    }
    System.out.println(itemsBought + "개 구입");
    System.out.println("잔돈(달러): " + funds);
}

코드 60-3 정수 타입을 사용한 해법 - 달러 대신 센트 단위로 계산

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
    int itemsBought = 0;
    int funds = 100;
    for (int price = 10; funds >= price; price += 10) {
        funds -= price;
        itemsBought++;
    }
    System.out.println(itemsBought + "개 구입");
    System.out.println("잔돈(센트): " + funds);
}

성능이 중요하고 소수점을 직접 추적할 수 있고 숫자가 너무 크지 않다면 int 나 long 을 사용하라.

  • int 사용 범위 : 숫자를 아홉자리 십진수로 표현할 수 있다면
  • long 사용 범위 : 숫자를 열여덟 자리 십진수로 표현할 수 있다면
  • 그 이상이면 BigDecimal 을 사용해야 한다

61) 박싱된 기본 타입보다는 기본 타입을 사용하라

자바의 데이터 타입은 크게 두가지로 나눌 수 있다.

  • int, double, boolean 같은 기본 타입
    • 각각의 기본타입에 대응하는 참조 타입이 있다 :arrow_right: 박싱된 기본 타입
      • Integer, Double, Boolean 등
  • String, List 같은 참조 타입

기본 타입과 박싱된 기본 타입의 주된 차이는 크게 세가지

  • 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성이란 속성을 갖는다
    • 값이 같아도 서로 다르다고 식별될 수 있다
  • 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 유효하지 않은 값, 즉 null 을 가질 수 있다
  • 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다

코드 61-1 잘못 구현된 비교자 - 문제를 찾아보자!

1
2
3
4
5
6
Comparator<Integer> naturalOrder =
       (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
       // 두번째 i == j 에서 두 객체 참조의 식별성을 검사하고 있다

int result = naturalOrder.compare(new Integer(42), new Integer(42));
System.out.println(result);     // 실행하면 1을 출력한다 (왜지??)

박싱된 기본 타입에 == 연산자를 사용하면 오류가 일어난다.

실무에서 이와 같이 기본 타입을 다루는 비교자가 필요하다면
Comparator.naturalOrder() 를 사용하자.

코드 61-2 문제를 수정한 비교자 (359쪽)

1
2
3
4
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
    int i = iBoxed, j = jBoxed; // 오토 언박싱 (기본타입으로)
    return i < j ? -1 : (i == j ? 0 : 1);
};

코드 61-3 기이하게 동작하는 프로그램 - 결과를 맞혀보자!

1
2
3
4
5
6
7
8
public class Unbelievable {
    static Integer i;       // null

    public static void main(String[] args) {
        if (i == 42)        // NullPointerException 발생
            System.out.println("믿을 수 없군!");
    }
}

기본 타입과 박싱된 기본 타입을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 풀린다.
:arrow_right: null 참조를 언박싱하면 NullPointerException 이 발생

코드 61-4 끔찍이 느리다! 객체가 만들어지는 위치를 찾았는가? - 심각한 성능 문제

1
2
3
4
5
6
7
public static void main(String[] args) {
    Long sum = 0L;                                  // Long
    for( long i=0; i<= Integer.MAX_VALUE; i++ ){    // Integer --> long
        sum += i;                                   // long --> Long
    }
    System.out.println( sum );
}

박싱된 기본 타입은 언제 써야 하는가?

  • 컬렉션의 원소, 키, 값으로 쓴다
    • 컬렉션은 기본 타입을 담을 수 없으므로 어쩔 수 없이 박싱된 기본 타입을 써야만 한다
    • 자바 언어가 타입 매개변수로 기본 타입을 지원하지 않기 때문이다
    • ex) ThreadLocal<int> 불가능, 대신 ThreadLocal<Integer> 를 써야함
  • 리플렉션을 통해 메서드를 호출할 때도 박싱된 기본 타입을 사용해야 한다 (항목65)

62) 다른 타입이 적절하다면 문자열 사용을 피하라

문자열을 쓰지 않아야 할 사례를 다룬다. (원래 텍스트 표현을 벗어난)

  • 문자열은 다른 값 타입을 대신하기에 적합하지 않다
    • 입력 받을 데이터가 수치형 데이터라면 수치 타입으로 변환해야 한다
  • 문자열은 열거 타입을 대신하기에 적합하지 않다
    • 상수를 열거할 때는 열거 타입이 월등히 낫다 (성능 문제)
  • 문자열은 혼합 타입을 대신하기에 적합하지 않다
    • 여러 요소가 혼합된 데이터를 하나의 문자열로 표현하는 것은 좋지 않은 생각이다
  • 문자열은 권한을 표현하기에 적합하지 않다
    • 자바 1 시절에는 스레드 지역변수(ThreadLocal) 설계시 자신만의 변수를 문자열로 설정했다
      • 우연히 다른 스레드가 같은 키를 쓰게 되면 두 스레드 모두 제대로 기능하지 못한다

코드 62-1 혼합 타입을 문자열로 처리한 부적절한 예

1
String compoundKey = className + "#" + i.next();

코드 62-2 잘못된 예 - 문자열을 사용해 권한을 구분하였다

1
2
3
4
5
6
7
8
9
public class ThreadLocal {
    private ThreadLocal() {}    // 객체 생성 불가

    // 현 스레드의 값을 문자열 키로 구분해 저장한다 (변수명: key, 값: value)
    public static void set(String key, Object value);

    // (키가 가리키는) 현 스레드의 값을 반환한다
    public static Object get(String key);
}

코드 62-3 (설계1) Key 클래스로 권한을 구분했다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ThreadLocal {
    private ThreadLocal() {}    // 객체 생성 불가

    // 문자열 대신 위조할 수 없는 Key 를 생성해 이것을 key로 저장
    public static class Key {   // (권한)
        key() {}
    }

    // 위조 불가능한 고유 키를 생성한다
    public static Key getKey() {
        return new Key();
    }

    // 위조할 수 없는 key로 저장
    public static void set(Key key, Object value);
    public static Object get(String key);
}

코드 62-4 (설계2) 리팩토링하여 Key를 ThreadLocal 로 변경

1
2
3
4
5
6
7
8
9
10
11
// ThreadLocal 은 별달리 하는 일이 없어지므로 치워버리고
// Key 클래스를 ThreadLocal 로 삼아 바꾼 형태
public final class ThreadLocal {
    public ThreadLocal();
    // Key 는 더 이상 스레드 지역변수를 구분하기 위한 키가 아니라
    // 그 자체가 스레드 지역변수가 된다

    // set 과 get 은 더이상 정적 메서드일 이유가 없어서 인스턴스 메서드로 변경
    public void set(Object value);
    public Object get();
}

코드 62-5 (설계3) 매개변수화하여 타입안정성 확보

1
2
3
4
5
6
7
// 타입안전하게 만들기 위해 매개변수화 타입을 선언
// 이렇게 해서 현재 java.lang.ThreadLocal 과 흡사해졌다
public final class ThreadLocal<T> {
    public ThreadLocal();
    public void set(T value);
    public T get();
}

:hand: 참고 : An Introduction to ThreadLocal in Java

ThreadLocal 사용법

1
2
3
4
5
6
7
8
ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

threadLocalValue.set(1);
Integer result = threadLocalValue.get();

ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

threadLocal.remove();

Storing User Data in a Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Context {
    private String userName;

    public Context(String userName) {
        this.userName = userName;
    }
}

public class SharedMapWithUserContext implements Runnable {

    public static Map<Integer, Context> userContextPerUserId
      = new ConcurrentHashMap<>();
    private Integer userId;
    private UserRepository userRepository = new UserRepository();

    @Override
    public void run() {
        String userName = userRepository.getUserNameForUserId(userId);
        userContextPerUserId.put(userId, new Context(userName));
    }

    // standard constructor
}

////////////////////////////

SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1);
SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();

// ConcurrentHashMap 을 이용해 thread 별 userId 를 저장하고 사용
assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);

Storing User Data in ThreadLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ThreadLocalWithUserContext implements Runnable {

    // ThreadLocal 로 Context 를 감싸 스레드별로 사용토록 함
    private static ThreadLocal<Context> userContext = new ThreadLocal<>();
    private Integer userId;
    private UserRepository userRepository = new UserRepository();

    @Override
    public void run() {
        String userName = userRepository.getUserNameForUserId(userId);
        userContext.set(new Context(userName));     // 스레드별로 저장
        System.out.println("thread context for given userId: "
            + userId + " is: " + userContext.get());
    }

    // standard constructor
}

///////////////////////////

ThreadLocalWithUserContext firstUser  = new ThreadLocalWithUserContext(1);
ThreadLocalWithUserContext secondUser = new ThreadLocalWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();

// ==>
// thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'}
// thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}

Do not use ThreadLocal with ExecutorService

ExecutorService 는 ThreadPool 를 이용해 실행되므로, Runnable action 이 항상 같은 스레드에 의해 실행된다고 보장할 수 없기 때문이다.

63) 문자열 연결은 느리니 주의하라

성능에 신경 써야 한다면 많은 문자열을 연결할 때는 문자열 연결 연산자(+)를 피하자.

  • 문자열 연결 연산자로 문자열 n개를 잇는 시간은 n^2 에 비례한다
  • 문자열은 불변이라서 두 문자열을 연결할 경우 양쪽의 내용을 모두 복사해야 한다

해결책

  • 많은 문자열을 연결할 때는 StringBuilder 의 append 메서드를 사용하라
  • 문자 배열을 이용하거나, 문자열을 연결하지 않고 하나씩 처리하는 방법도 있다

코드 63-1 문자열 연결을 잘못 사용한 예 - 느리다!

1
2
3
4
5
6
public String statement() {
    String result = "";
    for( int i=0; i<numItems(); i++ )
        result += lineForItem(i);       // 문자열 연결
    return result;
}

코드 63-2 StringBuilder 를 사용하면 문자열 연결 성능이 크게 개선된다

1
2
3
4
5
6
7
// 품목 100개와 LineForItem 길이 80인 문자열 기준으로 최대 6.5배 빨랐다
public String statement2() {
    StringBuilder b = new StringBuilder(numItems()*LINE_WIDTH);
    for( int i=0; i<numItems(); i++)
        b.append( lineForItem(i) );
    return b.toString();
}

64) 객체는 인터페이스를 사용해 참조하라

적합한 인터페이스만 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라.
객체의 실제 클래스를 사용해야 할 상황은 ‘오직’ 생성자로 생성할 때 뿐이다

1
2
3
4
5
6
7
// 좋은 예: 인터페이스를 타입으로 사용했다
Set<Son> sonSet = new LinkedHashSet<>();
// 혹 구현부 변경시 생성자만 바꾸면 된다
Set<Son> sonSet = new HashSet<>();

// 나쁜 예: 클래스를 타입으로 사용했다
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();

인터페이스를 타입으로 사용하는 습관을 길러두면 프로그램이 훨씬 유연해질 것이다.

  • 나중에 구현 클래스를 교체하고자 한다면 그저 (호환 타입인) 새 클래스의 생성자를 호출해주기만 하면 된다
  • 자칫하면 새로운 코드에서 컴파일되지 않을 수 있다

적합한 인터페이스가 없는 부류는 무엇일까?

  • 적합한 인터페이스가 없다면 당연히 클래스로 참조해야 한다
  • 클래스 기반으로 작성된 프레임워크가 제공하는 객체들이다 (OutputStream 등)
  • 인터페이스에는 없는 특별한 메서드를 제공하는 클래스들이다 (PriorityQueue 클래스 등)

적합한 인터페이스가 없다면 클래스의 계층구조 중 필요한 기능을 만족하는
가장 덜 구체적인(상위의) 클래스를 타입으로 사용하자.

65) 리플렉션보다는 인터페이스를 사용하라

:hand: 참고 : 자바 리플렉션(Reflection) 개념

리플렉션 기능(java.lang.reflect)을 이용하면 프로그램에서 임의의 클래스에 접근할 수 있다.

  • 실제로 로드 되기 전에는 어떤 클래스가 올라올지 알 수 없다
    • 예) 자바에서 JDBC 처리시 : Class.forName(“oracle.jdbc.driver.OracleDriver”)
  • Class 객체에서 Constructor, Method, Field 인스턴스를 가져올 수 있고
  • 이 인스턴스들로부터 그 클래스의 멤버 이름, 필드 타입, 메서드 시그니처 등을 가져올 수 있다

리플렉션 이용의 단점

  • 컴파일타임 타입 검사가 주는 이점을 하나도 누릴 수 없다
  • 리플렉션을 이용하면 코드가 지저분하고 장황해진다
  • 성능이 떨어진다 (단순한 int 반환 함수도 11배나 느렸다)

리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피하고 이점만 취할 수 있다.

  • 리플렉션은 인스턴스 생성에만 쓰고
  • 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자

코드 65-1 리플렉션으로 생성하고 인터페이스로 참조해 활용한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 리플렉션으로 활용한 인스턴스화 데모
// ==> 대부분의 경우 리플렉션 기능은 이정도만 사용해도 충분하다
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 (InstantiationException e) {
        fatalError("클래스를 인스턴스화할 수 없습니다.");
    } catch (InvocationTargetException e) {
        fatalError("생성자가 예외를 던졌습니다: " + e.getCause());
    } catch (ClassCastException e) {
        fatalError("Set을 구현하지 않은 클래스입니다.");
    }

    // 생성한 집합을 사용한다.
    s.addAll(Arrays.asList(args).subList(1, args.length));
    System.out.println(s);
}

private static void fatalError(String msg) {
    System.err.println(msg);
    System.exit(1);
}

이 예제는 리플렉션의 단점 두가지를 보여준다.

  • 런타임에 총 여섯 가지나 되는 예외를 던질 수 있다
    • 컴파일타임에 잡아낼 수 있었을 예외들
  • 클래스 이름만으로 인스턴스를 생성해내기 위해 무려 25줄이나 되는 코드를 작성했다
    • 리플렉션이 아니라면 생성자 호출 한 줄로 끝났을 일이다
    • 자바 7부터 지원되는 ReflectiveOperationException 으로 잡도록 하여 코드 길이를 줄일 수도 있다

드물긴 하지만, 리플렉션은 런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드, 필드와의 의존성을 관리할 때 적합하다. 이 기법은 버전이 여러개 존재하는 외부 패키지를 다룰 때 유용하다.

66) 네이티브 메서드는 신중히 사용하라

자바 네이티브 인터페이스(JNI)는 자바 프로그램이 네이티브 메서드를 호출하는 기술이다.
:hand: 네이티브 메서드란 C 나 C++ 같은 네이티브 프로그래밍 언어로 작성한 메서드를 말한다.

주요 쓰임새

  • 레지스트리 같은 플랫폼 특화 기능을 사용한다
  • 네이티브 코드로 작성된 기존 라이브러리를 사용한다
  • 성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성한다

성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 거의 권장하지 않는다

  • 자바 3 이전의 초기시절이라면 몰라도 지금은 다른 플랫폼에 견줄만한 성능을 보인다
    • 자바 1.1 시절 BigInteger 는 C 로 작성한 고성능 라이브러리에 의존했지만 자바 3 때는 네이티브 구현보다 빨라졌다
  • 정말로 고성능의 다중 정밀 연산이 필요한 자바 프로그래머라면 GMP 사용을 고려해도 좋다
    • 자바쪽은 자바 8 까지 더 이상의 큰 변화가 없었는데,
    • 네이티브 라이브러리 쪽은 GNU 다중 정밀 연산 라이브러리(GMP)를 필두로 개선작업이 계속되어 왔다

네이티브 메서드에는 심각한 단점이 있다.

  • 네이티브 언어가 안전하지 않으므로, 네이티브 메서드를 사용하는 애플리케이션도 메모리 훼손 오류로부터 더 이상 안전하지 않다.
  • 네이티브 언어는 자바보다 플랫폼을 많이 타 이식성도 낮다. 디버깅도 더 어렵다.
  • 주의하지 않으면 속도가 오히려 느려질 수도 있다.
  • 가비지 컬렉터가 네이티브 메모리는 자동 회수하지 못하고, 심지어 추적조차 할 수 없다
  • 자바 코드와 네이티브 코드의 경계를 넘나들 때마다 비용도 추가된다
  • 네이티브 메서드와 자바 코드 사이의 접착 코드(glue code)를 작성해야 하는데, 귀찮고 가독성도 떨어진다.

67) 최적화는 신중히 하라

모든 사람이 마음 깊이 새겨야 할 최적화 격언 세 개를 소개한다. (자바가 탄생하기 20년 전에 나온)

  • (맹목적인 어리석음을 포함해) 그 어떤 핑계보다 효율성이라는 이름 아래 행해진 컴퓨팅 최악이 더 많다 (심지어 효율을 높이지도 못하면서)
    • 윌리엄 울프
  • (전체의 97% 정도인) 자그마한 효율성은 모두 잊자. 섣부른 최적화가 만악의 근원이다
    • 도널드 크누스
  • 최적화를 할 때는 다음 두 규칙을 따르라. 첫번째, 하지마라. 두번째, (전문가 한정) 아직 하지 마라.
    • 다시 말해, 완전히 명백하고 최적화되지 않은 해법을 찾을 때까지는 하지 마라.
    • M.A. 잭슨

:bangbang:   핵심 정리

  • 빠른 프로그램보다는 좋은 프로그램을 작성하라. (빠른 프로그램을 작성하려 안달하지 말자)
    • 성능 때문에 견고한 구조를 희생하지 말자
    • 좋은 프로그램이지만 성능이 나오지 않는다면 아키텍처 자체를 최적화할 길을 찾아라.
      • 좋은 프로그램은 각 요소가 독립적이라 영향을 주지않고 다시 설계할 수 있다
  • 시스템 구현을 완료했다면 이제 성능을 측정해보라. 충분히 빠르면 그것으로 끝이다.
    • 그렇지 않다면 프로파일러를 사용해 문제의 원인이 되는 지점을 찾아 최적화를 수행하라
  • 가장 먼저 어떤 알고리즘을 사용했는지를 살펴보자
    • 알고리즘을 잘못 골랐다면 다른 저수준 최적화는 아무리 해봐야 소용이 없다
  • 성능을 제한하는 설계를 피하라
    • API 를 설계할 때 성능에 주는 영향을 고려하라
    • 성능을 위해 API 를 왜곡하는 건 매우 안 좋은 생각이다
  • 각각의 최적화 시도 전후로 성능을 측정하라

68) 일반적으로 통용되는 명명 규칙을 따르라

자바의 명명 규칙은 크게 ‘철자’와 ‘문법’으로 나뉜다.

철자 규칙 : 패키지, 클래스, 인터페이스, 메서드, 필드, 타입 변수의 이름을 다룬다.

  • 패키지와 모듈 이름은 각 요소를 점(.)으로 구분하여 계층적으로 짓는다
    • 패키지 이름의 나머지 요소는 일반적으로 8자 이하의 짧은 단어로 한다 (utiltities 보다 util)
  • 클래스와 인터페이스의 이름은 하나 이상의 단어로 이뤄지며, 각각의 단어는 대문자로 시작한다
  • 메서드와 필드 이름은 첫글자를 소문자로 쓴다
    • 단, 상수 필드는 모두 대문자로 쓰며 밑줄로 구분한다 (static final)
  • 지역변수에도 멤버와 비슷한 명명규칙이 적용된다
  • 타입 매개변수 이름은 보통 한 문자로 표현한다
    • 임의의 타입엔 T 를, 컬렉션 원소의 타입은 E 를, 맵에는 K 와 V 를, 예외에는 X 를, 메서드 반환타입에는 R 을 사용한다
식별자 타입
패키지와 모듈 org.junit.jupiter.api, com.google.common.collect
클래스와 인터페이스 Stream, FutureTask, LinkedHashMap, HttpClient
메서드와 필드 remove, groupingBy, getCrc
상수 필드 MIN_VALUE, NEGATIVE_INFINITY
지역 변수 i, denom, houseNum
타입 매개변수 T, E, K, V, X, R, U, V, T1, T2

문법 규칙

  • 객체를 생성할 수 있는 클래스의 이름은 보통 단수 명사나 명사구를 사용
  • 인터페이스의 이름은 클래스와 똑같이 짓거나, able 혹은 ible 로 끝나는 형용사로 짓는다
  • 메서드의 이름은 동사나 동사구로 짓는다
    • boolean 값을 반환하는 메서드라면 보통 is 나 has 로 시작
    • 인스턴스의 속성을 반환하는 메서드는 get 으로 시작하는 동사구로 짓는다
    • 객체의 타입을 바꿔서 또는 다른 객체를 반환하는 메서드 이름은 보통 toType 형태로 짓는다
    • 정적 패토리의 이름은 from, of, valueOf, instance, getInstance, getType 을 흔히 사용한다
  • 필드 이름은 덜 명확하고 덜 중요하다. 주로 명사구 사용
  • 지역변수 이름도 필드와 비슷하거나 조금 더 느슨하다

 
 

끝!   읽어주셔서 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.