본문 바로가기
독서이야기/이펙티브 자바 3 edition

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

by 사랑꾼이야 2022. 9. 3.
반응형

이 내용은 이펙티브 자바 Effective Java 3/E 를 읽으면서 정리한 내용을 포함하고 있습니다.

똑같은 기능의 객체를 매번 생성하는 것 보다는 처음 생성된 객체를 통해서 재사용하는 편이 좋을때가 많습니다. 특히 불변객체는 언제든지 재사용할 수 있습니다.

문자열 객체 생성

극단적인 예를 들어보겠습니다.

String str = new String("extreme")
  • 위 문장은 실행될때마다 String 인스턴스를 새롭게 만듭니다.
  • 만약 위 문장을 반복문 또는 빈번히 호출하게 된다면 String 인스턴스는 계속 생겨나게 됩니다.

그래서 다음과 같이 사용해야 합니다.

String str = "extreme";
  • 위 문장은 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용합니다.
  • 처음 생성된 객체를 재사용합니다.

다른 예를 들어보도록 하겠습니다. 아래 Boolean 생성자를 호출해보도록 하겠습니다.

Boolean result = Boolean("true");

호출된 생성자는 아래 parseBoolean 을 호출하게 됩니다.

public Boolean(String s) {
    this(parseBoolean(s));
}

문자열의 값을 확인하여 boolean 을 반환합니다.

public static boolean parseBoolean(String s) {
    return "true".equalsIgnoreCase(s);
}
  • 위 생성자는 호출시 매번 새로운 인스턴스를 생성하게 됩니다.

static 팩토리 메소드 사용

같은 역할을 하는 정적 팩토리 메소드를 호출해보도록 하겠습니다.

public static Boolean valueOf(String s) {
        return parseBoolean(s) ? TRUE : FALSE;
}

호출된 valueOf 메소드는 parseBoolean 결과값의 따라 미리 생성된 인스턴스 변수를 반환합니다.

public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
  • 위 정적 팩토리 메소드는 호출시 마다 새로운 인스턴스를 만드는 것이 아닌 처음 생성된 객체를 재사용합니다.

비싼 생성 객체

위에서 봐온 객체들의 인스턴스가 많아진다 하여도 사실 크게 느끼지 못할 수 있습니다. 하지만 객체들 중에는 생성 비용이 아주 비싼 객체도 있습니다. 이런 비싼 객체가 반복해서 필요한 경우라면 앞에서 본 정적 팩토리 메소드처럼 미리 생성하여 캐싱을 통해 재사용하길 추천합니다.

예를 들어, 문자열에 , 또는 : 문자가 있으면 true 를 반환하는 메소드가 있습니다. 해당 문자를 찾기 위해 정규표현식을 활용하였습니다.

public class CheckSeparatorAndDelimiterBefore {

    public static boolean checkSeparatorAndDelimiter(String str) {

        final Pattern PATTERN_DELIMITERS = Pattern.compile("[,:]");
        final Matcher matcher = PATTERN_DELIMITERS.matcher(str);
        return matcher.find();
    }
}
  • String.matches 가 가장 쉽게 정규 표현식에 매치가 되는지 확인하는 방법이긴 하지만 성능이 중요한 상황에서 반복적으로 사용하기에 적절하지 않습니다.
  • String.matches는 내부적으로 Pattern 객체를 만들어 쓰는데 그 객체를 만들려면 정규 표현식으로 유한 상태 기계로 컴파일 하는 과정이 필요하다. 즉 비싼 객체입니다.

이러한 상황에서 성능을 개선하기 위해서는 어떻게 해야 할까요?

Pattern 인스턴스는 불변하기 때문에 클래스 초기화 과정에서 직접 생성하여 캐싱해두고 실제 메소드가 호출될때마다 이를 재사용하면 좋습니다.

public class CheckSeparatorAndDelimiterAfter {

    private static final Pattern PATTERN_DELIMITERS = Pattern.compile("[,:]");

    public static boolean checkSeparatorAndDelimiter(String str) {

        final Matcher matcher = PATTERN_DELIMITERS.matcher(str);
        return matcher.find();
    }
}
  • Pattern 객체를 초기화 과정에서 생성하였습니다.
  • checkSeparatorAndDelimiter 메소드가 자주 호출되는 상황이라면 이전보다는 성능이 좋아졌습니다.
  • 추가로 Pattern 객체를 의미있는 이름으로 상수화하여 코드도 훨씬 명확해졌습니다.

성능 비교에 대해서 궁금하시다면 정리된 블로그를 읽어보시길 추천드립니다. - [Item6] 불필요한 객체를 생성하지 마라

객체가 불변이라면 재사용해도 안전합니다.

오토박싱

불필요한 객체를 만들어내는 또 다른 예로 오토박싱을 들 수 있습니다. 오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 상호 변환해주는 기술로 구분을 없애주지만, 완전히 없애주는 것은 아닙니다.

아래 모든 양의 정수를 총합을 구하는 메서드가 있고 long 을 통해서 계산하고 있습니다.

private static long totalSum() {

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

  return sum;
}
  • 로직의 결과는 문제가 없습니다.
  • sum 변수를 long 이 아닌 Long 으로 선언하여서 long 타입인 i가 Long 타입인 sum에 더해질 때마다 불필요한 인스턴스가 약 231개나 만들어지게 됩니다.

불필요한 오토박싱을 피하려면 박스 타입 보다는 프리미티브 타입을 사용해야 한다.

박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의를 해야합니다.

주의해야할 점

무조건적인 객체 생성을 피해야 하는 것이 아닙니다.

  • 하드웨어의 성능이 좋아진 요즘 JVM에서 작은 객체를 생성하고 회수하는 것은 크게 부담이 되지 않습니다. 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것이라면 괜찮다고 생각합니다.

반대로, 아주 무거운 객체가 아닌 객체를 생성을 피하고자 객체 풀을 만드는 것은 지양해야 합니다.

  • JVM의 가비지 컬렉터는 상당히 최적화가 잘 되어 있어서 가벼운 객체용을 다룰 때는 직접 만든 객체 풀보다 훨씬 빠릅니다. 단, 데이터베이스 연결처럼 생성 비용이 워낙 비싼 객체는 재사용하는 것이 맞습니다.

정리

  • 객체들 중 생성 비용이 비싼 객체의 경우 객체 풀 또는 캐싱을 이용하여 이를 재사용하는 것이 좋습니다.
  • 박싱된 기본 타입은 사용을 지양하고, 로직에서 의도치 않은 오토박싱이 숨어들지 않도록 조심해야 합니다.

참고

반응형

댓글