2 분 소요

Item 6. 불필요한 객체 생성을 피하라

   말 그대로 ‘불필요한’ 객체 생성을 피해야 한다. 필요한 객체는 생성해서 사용하면 된다.

   그렇다면 불필요한 객체 생성에는 어떤 경우가 있을까?

메모리가 낭비되는 경우

   첫 번째로 나오는 예시는 String 사용 방법이다. String 클래스는 자바에서 기본적으로 사용하는 문자열 클래스이며 불변으로 설계되어 있다. 문자열은 가장 많이 쓰는 데이터 중 하나이기 때문에 자바에서도 특별하게 취급되는데 그 중 하나가 바로 문자열 리터럴이다. String 클래스도 생성자가 있지만 생성자를 사용하게 되면 당연히 객체가 새로 생성된다. 문자열 리터럴을 사용할 경우 JVM이 내부적으로 String 객체를 재사용한다. 즉, 다음과 같은 코드는 절대 사용해서는 안 되는 코드다.

String s = new String("java");

   위 코드를 개선한 코드를 보자.

String s = "java";

   비슷한 예시로 Boolean 클래스가 있다. Boolean(String) 생성자 대신에 Boolean.valueOf(String) 메서드를 사용하게 되면 객체를 재사용하는 이점을 누릴 수 있다.

객체 생성 비용이 비싸 시간이 오래 걸리는 경우

   객체 생성 비용이 비싼 경우에는 해당 객체를 재사용하는 것을 고려해야 한다. 무심코 지나갈 수 있는 코드를 살펴보자.

public class RomanNumerals {

    static boolean isRomanNumeralSlow(String s) {
        return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    }
}

   위 메서드는 문자열이 유효한 로마 숫자인지 확인하는 메서드이다. 너무 간단하게 작성해서 성능을 생각할 겨를이 있나 싶지만 메서드 내부를 따라가 보면 이런 코드를 마주하게 된다.

public static Pattern compile(String regex) {
    return new Pattern(regex, 0);
}

   문자열 검증을 하는 데 있어 Pattern 객체를 생성함을 볼 수 있다. 이 객체는 RomanNumerals.isRomanNumeralSlow() 메서드가 실행될 때마다 새로 생성되고 곧바로 버려진다. 가볍게 생성할 수 있는 객체라면 문제 없다고 생각할 수 있겠으나 Pattern 객체의 생성 비용은 꽤 크기 때문에 문제가 발생한다. 만약에 위 메서드가 자주 사용된다고 하면 시간적인 비용이 아주 많이 들 것이다.

   대안은 Pattern 객체를 재사용하는 것! 어차피 로마 숫자인지 확인할 때의 Pattern 객체는 늘 하는 일이 같은 객체가 생성되기 때문에 충분히 재사용할 여지가 있다.

   다음 개선된 코드는 메서드 반복 시 걸리는 시간을 획기적으로 줄여준다.

public class RomanNumerals {

    private static final Pattern ROMAN = Pattern.compile(
            "^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

    static boolean isRomanNumeralFast(String s) {
        return ROMAN.matcher(s).matches();
    }
}

   Pattern 객체를 상수 필드로 초기화하여 재사용한다. RomanNumerals.isRomanNumeralFast() 메서드는 반복 실행되더라도 계속해서 Pattern 객체를 만들지 않고 최초 초기화 된 객체를 가져다가 사용한다.

오토박싱으로 인해 불필요한 객체를 생성하는 경우

   오토박싱은 자칫 넘어갈 수 있는 문제 중 하나이다. 오토박싱은 기본 타입을 자동으로 참조 타입으로 바꿔주어 개발자들을 도와주지만 이러한 점이 무심코 성능을 떨어뜨리는 원인이 될 수 있다.

public class Sum {
    private static long sum() {
        Long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; i++)
            sum += i;
        return sum;
    }
}

   성능이 떨어지는 지점이 보이는가? 오토박싱은 직관적으로 느껴지지 않아 세심히 살펴봐야 한다. 문제의 코드는 바로 여기다.

sum += i;

   long 타입의 변수 iLong 타입의 변수 sum에 더해질 때, i는 오토박싱 되어 Long 객체가 생성된다. 이 작업을 반복이 끝날 때까지 반복한다. 즉, 객체 생성이 2^31번 이루어진다.

   sum의 타입을 long으로 바꾼다면 시간이 획기적으로 단축되는 것을 쉽게 확인할 수 있다. 이것은 오토언박싱에도 동일하게 적용되며 직접 확인해 본 결과 sum의 타입을 long, i의 타입을 Long으로 지정해도 비슷한 문제가 발생했다.

   되도록이면 기본 타입을 사용하자. 기본 타입도 형변환이 발생하긴 하지만 (int 타입을 long 타입으로 변환하는 등) 그것은 큰 비용이 발생하지는 않는다. 실제로 sum의 타입을 long, i의 타입을 int로 지정해 보았지만 시간 차이는 크게 발생하지 않았다.


[참고]
백기선, inflearn 강의 이펙티브 자바 완벽 공략 1부
Joshua Bloch, 이펙티브 자바

<== Prev                                                Next ==>