동글이기가 레포

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

 

<자바의 데이터 타입은 두 가지로 나눌 수 있음>

 

1. 기본 타입

  - int, double, boolean과 같은 타입

 

2. 참조 타입(박싱된 기본 타입)

  - Integer, Double, Boolean과 같이 기본 타입에 대응하는 참조 타입

 

※ 앞 전에 배웠던 레퍼런스 타입(Wrapper Class)와 동일

기본 타입 박싱된 기본 타입
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Char
boolean Boolean

※ 동일하게 앞 전에 배웠던 오토박싱과 오토언박싱을 사용하면 두 타입의 경계를 왔다갔다 하면서 사용할 수 있지만 두 개의 타입에는 분명한 차이가 존재

 

 1) 같은 값이여도 다르게 식별될 수 있음

   - 기본 타입은 값만 가지고 있음

   - 참조 타입(박싱된 기본 타입)은 값과 해당 값을 식별할 수 있는 identity 속성을 가지고 있음

     > 따라서 참조 타입은 같은 값이라도 서로 다르게 식별될 수 있음

     > 기본 타입에서는 값을 비교하는 == 연산자가 참조 타입에서는 주소를 비교하기 때문에 사용할 수 없고, 인스턴스가 가지고 있는 값을 비교하는 equals() 메소드를 사용해야 함

 

 2) null의 유무

   - 기본 타입은 언제나 값이 유효해야 함

   - 참조 타입은 유호하지 않은 값인 null을 가질 수 있음

 

 3) 시간과 메모리 효율성

   - 기본 타입이 참조 타입보다 시간과 메모리 사용면에서 더 효율적임

     > 참조 타입은 메모리의 번지 수를 타고 들어가야하는 시간이 생기기 때문

 

* 즉, 차이를 생각하며 적절한 방식으로 각 타입을 사용해야 함

 

 

 

◎ 참조 타입 비교 예시

 

// Comparator : Comparable과 같은 기본 정렬 기준과는 다른 방식으로 정렬할 때 사용하는 클래스
// Comparable : 클래스의 기본 정렬 기준을 설정하는 인터페이스
public static void main(String[] args) {
        Comparator<Integer> naturalOrder =
                (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

        int output = naturalOrder.compare(new Integer(42), new Integer(42));
        System.out.println(output);
    }
// 출력 : 1

 

위의 예시는 첫 번째 원소 i가 j보다 작으면 -1, 같으면 0, 크면 1을 반환하는 코드

하지만, i와 j둘 다 Integer 42로 초기화 하였지만 결과는 1이 나왔기 때문에 i와 j는 서로 다르다는 결론에 도달

 

이유가 뭘까?

 

 

우선 (i < j)를 바꾸어 보았다.

 

// (i < j) -> (i.equals(j))
public static void main(String[] args) {
        Comparator<Integer> naturalOrder =
                (i, j) -> (i.equals(j)) ? -1 : (i == j ? 0 : 1);

        int output = naturalOrder.compare(new Integer(42), new Integer(42));
        System.out.println(output);
    }
// 출력 : -1

 

비교 연산자 '<' 대신 equals() 메소드를 사용하니 -1로 정상적인 결과를 출력하였다.

 

그렇다면, 해당 부분에서는 문제가 발생하지 않은 것이다.

i와 j는 42이기 때문에 (i < j)에서 false가 되어 우측으로 이동되는 것이 맞기 때문

 

 

그렇다면 역시나 우측 '=='연산자가 잘못된 것을 느낄 수 있다.

  > '==' 연산자는 참조 타입의 경우 주소를 비교하기 때문

 

 

'=='연산자를 equals() 메소드로 변경해보았다.

 

// 'i == j' -> i.equals(j)
public static void main(String[] args) {
        Comparator<Integer> naturalOrder =
                (i, j) -> (i < j) ? -1 : (i.equals(j) ? 0 : 1);

        int output = naturalOrder.compare(new Integer(42), new Integer(42));
        System.out.println(output);
    }
// 결과 : 0

 

0이 출력되었기 때문에 정상적인 결과가 출력되었다.

 

따라서, 참조 타입에는 '=='연산자를 사용할 수 없다.

 

 

추가적으로, 오토박싱을 이용하여 해결이 가능하다.

 

// 오토박싱을 이용한 해결법
public static void main(String[] args) {
        Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
            int i = iBoxed, j = jBoxed; // 오토박싱
            return i < j ? -1 : (i == j ? 0 : 1);
        };

        int output = naturalOrder.compare(new Integer(42), new Integer(42));
        System.out.println(output);
    }
// 결과 : 0

 

해당 경우에 정상적으로 출력되지만 기본 타입으로 변환하였기 때문에 식별성 검사는 제대로 이루어지지 않음

 

 

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

  > 직접 만들 시, 비교자 생성 메서드나 기본 타입을 받는 정적 compare 메서드를 사용해야 함

 

 

또 하나의 예시가 있다.

 

// 결과를 맞춰보자
public class Main {
    static Integer i;
    public static void main(String[] args) {
        if (i == 42)
            System.out.println("믿을 수 없군!");
    }
}

 

당연히 i를 초기화 하지 않았으니 42가 아니고, 에러가 발생할 수 밖에 없지만 특이한 에러가 발생한다.

 

그것은 NullPointerException이다.

 

Integer로 초기화한 i는 현재 초기값이 null이고 그것을 int인 42와 비교하려고 한 것이다.

* 이때 기본 타입과 참조 타입(박싱된 기본 타입)을 혼용한 연산에서는 참조 타입이 자동으로 언방식이 되는데, null을 언방식하기 때문에 NullPointerException이 발생하는 것이다.

 

해결법은 Integer로 선언한 i를 int로 선언해주는 것이다.

 

 

다음 예시는 끔찍하게 느린 성능을 보여주는 코드이다.

 

// 느린 이유를 생각해보자
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은 참조 타입인 Long으로 선언이 되어 있지만 반복문 for안의 i는 기본 타입인 long으로 선언이 되어 있다.

 

그리고 sum += i; 연산에서 반복적인 오토박싱과 오토언박싱이 발생하게 되므로 느려지게 되는 것이다.

  > sum = {sum(오토언박싱) + i}(오토박싱)

 

* 따라서 기본 타입과 참조 타입(박싱된 기본 타입)의 차이를 잘 생각하고 사용을 해야 성능 문제에서 벗어날 수 있음

 

 

※ 참조 타입(박싱된 기본 타입)의 쓰임새

 1) 컬렉션의 원소, 키, 값

   > 컬렉션은 기본 타입을 담을 수 없기 때문

 2) 타입 매개변수

   ex) ThreadLocal<Integer>

 3) 리플렉션을 통해 메서드를 호출할 때

 

 


정리

- 기본 타입과 참조 타입중에서 하나를 사용한다면 기본 타입을 권장

- 참조 타입을 사용할 때에는 각별한 주의를 기울여서 부작용을 겪지 않도록 하기

 

 

 

 

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

 

문자열은 흔한만큼 의도하지 않은 용도로 쓰이는 경향도 종종 보인다.

 

 

이번 item 62에서는 문자열을 쓰지 말아야 할 사례에 대해 알아보자.

 

1. 문자열은 다른 값 타입을 대신하기에 적합하지 않다.

 

 - 많은 개발자들이 데이터를 입력받을 때 문자열을 사용하지만, 받은 데이터가 수치형이라면 int, float 등 범위에 맞는 수치 타입, 예/아니요라면 boolean으로 변환해야 한다.

    > 즉, 기본 타입이나 참조 타입 중 적절한 값 타입을 사용해야 함

 

 

2. 문자열은 열거 타입을 대신하기에 적합하지 않다.

 

 - 상수의 의미를 출력할 수 있다는 점은 좋지만, 문자열 상수 이름 대신 문자열 값을 그대로 하드코딩하게 만들기 때문

 - 문자열에 오타가 있어도 컴파일러에서 확인할 길이 없기 때문에 런타임 버그

 - 문자열 비교에 따른 성능저하

 

// 문자열 상수 예시
public static final String RAINBOW_FIRST = "RED"
public static final String RAINBOW_SECOND = "ORANGE"

// Enum
public enum Rainbow {
    RED, ORANGE, YELLOW, GREEN, BLUE, NAVY, PURPLE
}

public static void main(String arg[]) {
    Rainbow rain = Rainbow.YELLOW;
    
    if (color == Rainbow.YELLOW){
        System.out.println("Hi!");
    }
    else {
            System.out.println("Bye!");
    }
}

// 결과 : Hi!

 

 

3. 문자열은 혼합 타입을 대신하기에 적합하지 않다.

 

 - 여러 요소가 혼합된 데이터를 하나의 문자열로 표현하는 것은 지양한다.

 

// 예시
String compoundKey = className + "#" + i.next()

 

위의 예시는 여러 단점을 가지고 있다.

두 요소를 구분해주는 문자 #이 단점 중의 하나인데, 만약 앞이나 뒤의 요소에 같은 문자인 #이 쓰였다면 혼란스러운 결과를 초래할 수 있다.

 

각 요소를 개별적으로 파싱하기 위해서는 파싱 작업이 필요하기 때문에 귀찮고 오류 가능성도 커지기도 한다.

 

추가적으로 equals, toString,compareTo 메서드를 사용할 수 없으며, String이 제공하는 기능에만 의존해야 하기 때문에 전용 클래스를 새로 만드는 편이 낫다.

 

 

4. 문자열은 권한을 표현하기에 적합하지 않다.

 

 - 권한을 문자열로 표현하는 경우가 종종 있다.

 

// 문자열을 사용해서 권한을 구분하는 잘못된 예
public class ThreadLocal {
    private ThreadLocal() {} // 객체 생성 불가
    
    // 현 스레드의 값을 키로 구분해 저장
    public static void set(String key, Object value);
    
    // (키가 가리키는) 현 스레드의 값을 반환한다.
    public static Object get(String key);
}

 

위 예제의 방식처럼 설계를 한다면 스레드 구분용 문자열 키가 전역 공간에서 공유된다는 것이 문제이다.

 > 의도한 방향은 각 클라이언트가 고유한 키를 제공하는 것

 

그런데, 만약 여러 클라이언트가 같은 키를 쓰는 상황이 발생한다면 기능상의 문제가 발생할 위험이 있음

  + 보안상의 문제도 발생할 수 있음

 

이러한 문제는 문자열 대신 위조할 수 없는 키를 사용하여 해결할 수 있다.

key는 권한이라고 한다.

 

// 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(Key key);
}

 

위의 예시는 앞서 언급한 문제들을 해결해주시만, 개선의 여지는 존재한다.

 

 1)  set과 get은 정적 메서드일 이유가 없으니 Key클래스의 인스턴스 메서드로 변경

   > 결과적으로 Key는 스레드 지역변수를 구분하는 키에서 그 자체가 스레드 지역변수가 됨

 

 2) 1번에 이어서 현재 ThreadLocal의 역할이 없기 때문에 클래스 Key의 이름을 ThreadLocal로 변경

 

 

개선 후 결과는 아래와 같다.

 

public final class ThreadLocal {
    public ThreadLocal();
    public void set(Object value);
    public Object get();
}

 

여기서는 get으로 얻은 Object를 실제 타입으로 형변환하고 써야하기 때문에 안전하지 않다.

 

처음의 문자열 기반, Key를 사용한 방식도 타입안전하게 만들기가 어려운데, 이 문제는 ThreadLocal을 매개변수화 타입으로 선언하면 해결된다.

 

public final class ThreadLocal<T> {
    public ThreadLocal();
    public void set(T value);
    public T get();
}

 

위의 코드는 현재의 java.lang.ThreadLocal과 흡사하다.

 

이는 문자열 기반과 Key기반의 문제를 해결해주며 성능이 좋다.

 

 


정리

- 문자열을 사용하기 전에 적합한 데이터 타입이 있거나 새로 만들 수 있는지 확인

- 문자열은 잘못 사용하면 성능의 저하와 오류 발생 가능성이 존재

- 문자열을 잘못 사용하는 흔한 예 : 기본 타입, 열거 타입, 혼합 타입

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading