럿고의 개발 노트

아이템 6. 불필요한 객체 생성을 피하라. 본문

Java Note/이펙티브 자바 3판(EFFECTIVE JAVA 3E)

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

KimSeYun 2020. 3. 23. 17:34

이펙티브 자바 - 2장. 객체 생성과 파괴

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

  • 똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 훨씬 나을때가 있다. 특히 불변 객체는 언제든 재사용할 수 있다.
  • Item1에서 팩터리 메서드를 제공하는 불변클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다.
  • 예를들어 Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.
  • 생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리 메서드는 전혀 그렇지 않다.
  • 불변 객체만이 아니라 가변 객체라 해도 사용 중에 변경 되지 않을 것임을 안다면 재사용할 수 있다.
  • 생성 비용이 아주 비싼 객체는 반복해서 필요하다면 캐싱하여 재사용하길 권한다.

String 예제

String str = new String("woowa");

  • 위의 코드는 실행될 때마다 String 인스턴스를 새로 만든다. 빈번히 호출된다면 쓸데없는 String 인스턴스가 수없이 만들어 질 것이다.

String str = "woowa";

  • 위의 코드는 매번 인스턴스를 생성하는게 아니라 하나의 String 인스턴스를 사용하게 된다.
  • 즉, 같은 가상 머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.

정규표현식 예제

  • 대표적인 비싼 객체로 String.matches가 있다.
  • 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기엔 적합하지 않다.
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})$");
    }
}
  • 이 메서드 내부에서 Pattern 인스턴스는, 한 번 쓰고 버러져서 곧바로 GC 대상이 된다. Pattern은 입력받은 정규표현식에 해당하는 유한 상태 머신을 만들기 때문에 인스턴스 생성 비용이 높다.
  • 성능을 개선하려면 필요한 정규표현식을 표현하는 (불변인) Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고, 나중에 isRomanNumeralSlow 메서드가 호출될 때마다 이 인스턴스를 재사용 한다.
import java.util.regex.Pattern;

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})$");
    }

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

    public static void main(String[] args) {
        int numSets = 1;
        int numReps = 1;
        boolean b = false;

        for (int i = 0; i < numSets; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < numReps; j++) {
                b ^= isRomanNumeralSlow("MCMLXXVI");
            }
            long end = System.nanoTime();
            System.out.println(((end - start) / (1_000 * numReps)) + " us.");
        }

        if(!b){
            System.out.println();
        }

        for (int i = 0; i < numSets; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < numReps; j++) {
                b ^= isRomanNumeralFast("MCMLXXVI");
            }
            long end = System.nanoTime();
            System.out.println(((end - start) / (1_000 * numReps)) + " us.");
        }

        if(!b){
            System.out.println();
        }
    }
}
  • 개선된 isRomanNumeral 방식의 클래스가 초기화된 후 이 메서드를 한 번도 호출하지 않는다면 ROMAN 필드는 슬데없이 초기화된 꼴이다.
  • isRomanNumberal가 처음 호출될 때 필드를 초기화하는 지연 초기화로 불필요한 초기화를 없앨 수는 있지만, 권하지는 않는다. 지연 초기화는 코드를 복잡하게 만드는데, 성능은 크게 개선되지 않을 때가 많기 때문이다.

불변객체의 재사용

  • 객체가 불변이라면 재사용해도 안전함이 명백하다. 하지만 훨씬 덜 명확하거나, 심지어 직관에 반대되는 상황도 있다.
  • 어댑터(=뷰)는 실제 작업은 뒷단 객체에 위임하고, 자신은 제2의 인터페이스 역할을 해주는 객체다. 어댑터는 뒷단 객체만 관리하면 된다. 즉, 뒷단 객체 외에는 관리할 상태가 없으므로 뒷단 객체 하나당 어댑터 하나씩만 만들어지면 충분하다.
  • 예를들어 Map 인터페이스의 keySetMap 객체 안에 키 전부를 담은 Set 뷰를 반환한다.
  • keySet을 호출할 때 마다 새로운 Set 인스턴스가 만들어지라고 순진하게 생각할 수 있지만, 사실은 매번 같은 Set 인스턴스를 반환할지도 모른다.
  • 반환된 Set 인스턴스가 일반적으로 가변이더라도 반환된 인스턴스들은 기능적으로 모두 똑같다. 즉, 반환한 객체 중 하나를 수정하면 다른 모든 객체가 따라서 바뀐다. 모두가 똑같은 Map 인스턴스를 대변하기 때문이다.
  • 따라서 keySet이 뷰 객체를 여러 개 만들어도 상관은 없지만, 그럴 필요도 없고 이득도 없다.

오토박싱 예제

  • 오토박싱이란 기본 타입과 박상된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.
  • 오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다. 의미상으로는 별다를 것 없지만 성능에서는 그렇지 않다.
public class Sum {
    private static long sum() {
        Long sum = 0L;
        for (long i = 0; i <= Integer.MAX_VALUE; i++){
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) {
        int numSets = 1;
        long x = 0;

        for (int i = 0; i < numSets; i++){
            long start = System.nanoTime();
            x += sum();
            long end = System.nanoTime();
            System.out.println((end - start) / 1_000_000. + " ms.");
        }

        if(x == 42){
            System.out.println();
        }
    }
}
  • 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.

오해

  1. "객체 생성은 비싸니 피해야 한다"라고 오해하면 안된다.
    • 요즘의 JVM에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않는다.
    • 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일이다.
  2. 아주 무거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 여러분만의 객체 풀을 만들지는 말자.
    • 일반적으로는 자체 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘리고 성능을 떨어뜨린다.
    • 요즘 JVM의 가비지 컬렉터는 상당히 잘 최적화되어서 가벼운 객체용을 다룰 때는 직접 만든 객체 풀보다 훨씬 빠르다.
    • 데이터베이스 연결 같은 경우 생성 비용이 비싸니 재사용하는 편이 낫다.
  3. 방어적 복사를 다루는 아이템 50인 "새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라"와는 대조적이다.
    • 방어적 복사가 필요한 상황에서 객체를 재사용했을 때의 피해가, 필요 없는 객체를 반복 생성했을 때의 피해보다 훨씬 크다는 사실을 기억하자.
    • 방어적 복사에 실패하면 언제 터져 나올지 모르는 버그와 부안 구멍으로 이어지지만, 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 준다.
Comments