본문 바로가기
이펙티브 자바

[아이템 6] 불필요한 객체 생성을 피하라

by 에드박 2021. 4. 22.

똑같은 기능의 객체를 매번 생성하는것 보다 객체 하나를 재사용하는 편이 나을 때가 많습니다.

나중에 배울 불변 객체(아이템 17)은 언제든 재사용할 수 있습니다.

 

다음 코드는 사용하지 말아야할 극단적인 예시 입니다.

 

public class AntiPatternEx1 {

    public static void main(String[] args) 

        // 아래 s1 과 s2 는 각각 새로운 인스턴스를 생성한다.
        // 다른 객체로 활용하고 싶은게 아니라면 아래의 행위는 매우 쓸모없는 행위이다.
        String s1 = new String("charlie");
        String s2 = new String("charlie");

        System.out.println(s1 == s2);
    }
}

 

new String(String) 으로 생성하면 매번 새로운 주소를 할당한 인스턴스를 생성합니다.

하지만 두 객체는 기능적으로 완전히 똑같습니다.

 

만약 같은 문자열을 가진 String 인스턴스를 생성하려면 아래와 같이 하는것이 좋습니다.

 

public class GoodPatternEx1 {

    public static void main(String[] args) {
        // 아래 s3 와 s4 는 하나의 같은 인스턴스로 초기화한다.
        // String 은 내부적으로 문자열 풀을 사용해서 같은 문자열이라면 같은 주소값을 가지도록 합니다.
        String s3 = "charlie";
        String s4 = "charlie";

        System.out.println(s3 == s4);
    }
}

이 방식으로 생성하는 String 인스턴스는 같은 문자열 리터럴을 사용한다면 같은 주소값을 사용하도록 합니다.

즉, 문자열 리터럴이 같다면 같은 객체를 가지게 됩니다.

 


정적 팩터리 메서드를 통한 불필요한 객체 생성 피하기

생성자 대신 정적 팩터리 메서드(아이템1)를 제공하는 불변클래스에서는 불필요한 객체 생성을 피할 수 있습니다.

 

Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 더 좋습니다.

Boolean(String) 생성자는 매번 새로운 객체를 반환해주지만

Boolean.valueOf(String)은 미리 생성된 객체를 반환해줍니다.

 

java의 Boolean 생성자 부분

 

Boolean 생성자 부분은 매번 새로운 객체를 생성해줍니다.

 

java Boolean.valueOf 팩터리 메서드

 

Boolean.valueOf 팩터리 메서드는 이미 생성된 TRUE, FALSE 객체를 리턴해줍니다.

 

Boolean 에서 미리 생선된 객체인 TRUE, FALSE

 

Java9 부터는 Boolean 생성자는 Deprecated 되었습니다.

즉, 사용을 자제하고 다른것을 사용하길 권장한다는 의미입니다.

생성자는 매번 새로운 객체를 반환해야하지만(필수) 팩터리 메서드는 전혀 그렇지 않습니다.

 


생산 비용이 비싼경우 재사용을 고려하자

생산 비용이 아주 비싼 객체도 가끔 있습니다.

이런 '비싼 객체'가 반복해서 필요하다면 캐싱하여 재사용하는것을 권장합니다.

 

이에 해당하는것이 정규표현식을 활용한 예제입니다.

기본적으로 Pattern 클래스의 인스턴스를 생성하는것은 비용이 비쌉니다.

 

public class AntiPatternEx3 {


    // String.matches() 는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만,
    // 성능이 중요한 상황에서 반복해 사용하기엔 적합하지 않다.
    // 내부에서 만드는 정규표현식용 Pattern 인스턴스는,
    // 한 번 쓰고 버려져서 곧바로 가비지 컬렉터 대상이 된다.
    // Pattern 은 입력받은 정규표현식에 해당하는
    // 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다.
    static boolean isRomanNumeral(String input) {
        return input.matches("^(?=.)M*(C[MD]|D?C{0,3})"
                + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    }
}

 

 

위 예제의 문제는 String 클래스의 matches 메서드를 사용하는데 있습니다.

String.matches는 정규 표현식으로 문자열 형태를 확인하는 가장 쉬운 방법 이지만, 성능이 중요한 상황에서 반복해서 사용하기엔 적합하지 않습니다.

 

matches 메서드 내부에서 Pattern 인스턴스가 생성되고 한번 사용한 뒤 버려져서 가비지 컬렉션 대상이 됩니다.

입력받은 정규표현식은 유한 상태머신 (finite state machine)을 만들기 때문에 인스턴스 생성 비용이 비쌉니다.

 

따라서 아래와 같이 Pattern 인스턴스를 불변 정적 객체로 만들어 캐싱해두고 해당 객체를 재사용하는것이 좋습니다.

public class GoodPatternEx3 {

    // 캐시해두고 재사용
    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 isRomanNumeral(String input) {
        return ROMAN.matcher(input).matches();
    }
}

 

이런 방식으로 하면 정적 Pattern 타입 필드에 어떤 정규표현식인지 이름을 붙여줄 수도 있습니다.

 


불필요하게 오토박싱이 일어나는 구현을 주의하라

오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술입니다.

오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아닙니다.

의미상으로는 다를것이 없지만 성능에는 차이가 발생합니다.

 

public class AntiPatternEx4 {
    public static void main(String[] args) {
        // 오토 박싱 비용으로 인해 불필요한 작업이 추가로 생김
        long start = System.currentTimeMillis();
        Long sum = 0L;

        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }

        System.out.println(sum);
        System.out.println(System.currentTimeMillis() - start);
    }
}

 

위의 예제는 0부터 int타입의 최대값까지 전부 더해주는 예제입니다.

정확한 결과는 나오지만 잘못된 부분이 있습니다.

바로 sum 변수의 타입이 Long 이라서 sum += i 부분에서 계속 오토박싱이 일어나고 있습니다.

 

여기서 단순히 sum 변수의 타입을 long으로 바꾸기만해도 약 10배가량 빨라지는것을 확인할 수 있습니다.

 

public class GoodPatternEx4 {

    public static void main(String[] args) {
        // 오토 박싱 비용이 없으므로 위보다 약 10배 내외로 빠름
        long start = currentTimeMillis();
        long sum = 0L;

        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }

        System.out.println(sum);
        System.out.println(currentTimeMillis() - start);
    }
}

 


이번 아이템에서 오해하지 말아야 할것

"객체 생성은 비싸니 피해야한다" 로 오해하면 안됩니다.

요즘의 JVM에서 작은 객체를 생성하고 회수하는 일은 크게 부담되는 일이 아닙니다.

프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일입니다.

 

만약 아주 무거운 객체가 아니라면 단순히 객체 생성을 피하기 위해서 우리만의 객체 풀(pool)을 생성하는것은 피해야 합니다.

무거운 객체로 인해 객체 풀을 사용하는 대표적인 예로 데이터베이스 연결 객체를 예로 들 수 있습니다.

하지만 일반적으로 자체 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘려서 성능을 떨어뜨립니다.

 

이번 아이템은 방어적 복사(defensive copy)를 다루는 아이템 50과 대조적입니다.

 

이번 아이템 : 기존 객체를 재사용해야 한다면 새로운 객체를 만들지 마라

방어적 복사 : 새로운 객체를 만들어야 한다면 기본 객체를 재사용하지 마라

 

방어적 복사가 필요한 상황에서

객체를 재사용했을 때의 피해는 필요없는 객체를 생성했을 때의 피해보다 훨씬 큽니다.

 

방어적 복사에 실패하면 언제 터져 나올지 모르는 버그와 보안 구멍으로 이어지지만, 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 줍니다.

댓글