티스토리 뷰

728x90
반응형

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


  • 여기서 말하는 불필요한 객체란 무엇일까?
  • 아래 예제를 보면 String 타입의 변수 a, b, c는 모두 "Hello World"라는 문자열을 가지게 됩니다. 하지만 이 세 문자열이 참조하는 주소값은 모두 다릅니다. 동일한 문자열을 이처럼 여러개로 중복 생성하는 것은 메모리 낭비입니다.
  • String Constant Pool에 대하여
String a = new String("Hello World");
String b = new String("Hello World");
String c = new String("Hello World");

 

💡그럼 어떻게 사용해야 불필요한 객체 생성을 피할 수 있을까요?

  • 아래 예제를 보면 리터럴로 선언을 해 놓으면 컴파일 시점에서 상수풀에 해당 String 인스턴스를 저장하며, 동일한 리터럴이 있는 경우에는 같은 주소값을 가지게 됩니다. 그렇기 때문에 메모리 낭비가 발생하지 않게되고 같은 객체를 재사용할 수 있습니다.
String a = "Hello World";
String b = "Hello World";
String c = "Hello World";

 

💡Boolean(String) vs Boolean.valueOf(String)

  • 아래는 Boolean 클래스를 살펴본 것입니다. Boolean(String) 생성자 대신 Boolean.valueOf(String)을 사용하는게 더 좋습니다.
  • Boolean(String)은 호출시점에 항상 새로운 객체를 만들지만 팩토리 메서드는 그렇지 않습니다.
public final class Boolean implements java.io.Serializable, Comparable<Boolean> {
   
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

    /**
     * Allocates a {@code Boolean} object representing the value
     * {@code true} if the string argument is not {@code null}
     * and is equal, ignoring case, to the string {@code "true"}.
     * Otherwise, allocates a {@code Boolean} object representing the
     * value {@code false}.
     *
     * @param   s   the string to be converted to a {@code Boolean}.
     *
     * @deprecated
     * It is rarely appropriate to use this constructor.
     * Use {@link #parseBoolean(String)} to convert a string to a
     * {@code boolean} primitive, or use {@link #valueOf(String)}
     * to convert a string to a {@code Boolean} object.
     */
    @Deprecated(since="9")
    public Boolean(String s) {
        this(parseBoolean(s));
    }
    
    public static Boolean valueOf(String s) {
        return parseBoolean(s) ? TRUE : FALSE;
    }
    생략...
}

 

💡비싼 객체

  • 객체 생성 비용이 비싼 객체도 가끔 있습니다. 이런 비싼 객체가 반복되서 필요하다면 캐싱해서 재사용하는 것이 중요합니다. 하지만 자신이 만든 객체가 비싼 객체인지 판단하기는 어렵습니다.
  • 자바에서는 대표적으로 정규표현식용 클래스인 Pattern 클래스는 생성비용이 비싼 클래스입니다. String 클래스의 matches 메서드를 사용하게 되면 내부적으로 Pattern 인스턴스를 만들어 한번 사용하고 바로 버려져 GC의 대상이 됩니다.
  • 주의점 - 객체가 불변이라면 재사용해도 문제가 없습니다.
public class StringUtils {

    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 isRomanNumeralV1(String s) {
        return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    }
    
    // 좋은 방법
    static boolean isRomanNumeralV2(String s) {
        return ROMAN.matcher(s).matches();
    }
}

 

💡불필요한 객체를 만들어내는 또 다른 예 - 오토박싱

  • sumV1() 메서드를 보면 sum의 타입이 Long입니다. 반복문을 수행하면서 sum에 1씩 더하고 있으며 여기서 문제는 i의 타입에 있습니다. sum의 타입은 래퍼클래스인 Long이며, i는 기본 타입인 long입니다.
  • 이말은 long 타입인 i는 반복문이 돌면서 sum에 더해질때마다 새로운 Long 인스턴스를 만든다는 것입니다. 결과적으로 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의해야 합니다.
public class AutoBoxing {

    // bad
    public static long sumV1() {
        Long sum = 0L;

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

        return sum;
    }
    
    // good
    public static long sumV2() {
        long sum = 0L;

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

        return sum;
    }
}

 

💡결론

  • 불필요한 객체 생성을 피하라는 말이 "객체 생성은 비싸니 피해야한다"라고 오해하면 안됩니다. 
  • 요즘 GC는 상당히 잘 최적화되어 있어서 가벼운 객체를 생성 및 회수하는 일은 별로 부담이 되는 작업이 아닙니다.
  • 데이터베이스 연결 같은 경우 생성 비용이 워낙 비싸니 재사용하는 편이 좋습니다. 하지만 일반적으로는 자체 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘리고 성능을 떨어뜨립니다.
  • 무엇보다 이렇게 객체를 방어적으로 복사하는 방식은 피해가 발생했을 때 객체를 반복 생성했을 때보다 훨씬 큽니다. 반복 생성의 부작용은 코드 형태나 성능에만 영향을 주지만, 방어적 복사가 실패한 경우에는 버그와 보안 문제로 직결됩니다.

 

 

728x90
반응형