[Effective-Java] Item6 - 불필요한 객체 생성을 피하라

imcool2551·2022년 2월 7일
0

Effective-Java

목록 보기
6/8
post-thumbnail

1. 불변 객체 재사용


String s = new String("bikini") // BAD
String s = "bikini" // GOOD

똑같은 객체를 매번 생성하는 것보다 객체 하나를 재사용하는 것이 나을 경우가 많다. 위의 코드에서 new 키워드로 생성자를 호출하면 새로운 메모리가 할당된다. 해당 문장이 반복문을 통해 수백만번 호출되면 쓸데 없는 String 인스턴스 수백만 개가 메모리를 할당 받게된다. 반면 문자열 리터럴로 문자열을 만들면 같은 문자열은 같은 객체를 재사용 하는 것을 JVM에서 보장해준다.

2. 정적 팩터리 메서드


생성자를 사용하면 객체 생성이 된다고 했다. 그래서 문자열은 new 키워드로 만들지 말고 리터럴로 만드는 것이 낫다.

비슷한 이유로 생성자 대신 정적 팩터리를 사용하면 불필요한 객체 생성을 피할 수 있다.

Boolean b = new Boolean("true") // BAD
Boolean b = Boolean.valueOf("true") // GOOD

Boolean(String) 생성자는 호출할 때마다 객체를 새로 만들지만 정적 팩터리 메서드는 그렇지 않다. Boolean(String)은 자바9 부터 deprecated API 로 지정되었으니 되도록 사용하지 말자.

3. 생성 비용이 비싼 객체


생성 비용이 비싼 객체는 캐싱해서 재사용 하는 것이 성능상 유리한 경우가 있다. 예를 살펴보자.

// 값비싼 객체를 재사용해 성능을 개선한다. (32쪽)
public class RomanNumerals {
    // BAD
    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})$");
    }

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

isRomanNumeralSlow 메서드는 내부적으로 Pattern 인스턴스를 생성한다. Pattern 인스턴스는 인스턴스 생성 비용이 높은데 한 번 쓰고 버려져서 곧장 가비지 컬렉션의 대상이 된다.

반면, Pattern 인스턴스를 캐싱해두고 재사용하는 isRomanNumeralFast 메서드는 매번 객체를 생성하는 isRomanNumeralSlow 메서드보다 성능이 좋다. 정규표현식을 필드로 뽑아내서 이름을 붙여줬기에 코드 가독성이 좋아진 것은 덤이다.

위의 코드에서 Pattern 객체가 사용되지 않는다면 불필요하게 초기화 한 것이 된다. 성능을 좀더 높이기 위해 isRomanNumeralFast 함수가 처음 호출될 때 객체를 초기화하는 이른 바 지연 초기화(lazy initialization)를 통해 불필요한 초기화를 없앨 수 있긴 하지만 권하지는 않는다. 복잡해지는 코드에 비해 얻을 수 있는 성능 이득이 작은 경우가 많다.

4. 어댑터 객체


어댑터(뷰)란 실제 작업은 뒷단 객체에 위임하고, 자신은 제2의 인터페이스 역할을 해주는 객체다. 어댑터는 뒷단 객체만 관리하면 된다. 뒷단 객체 하나당 어댑터(뷰)가 하나씩 만들어진다.

예로 Map 인터페이스의 keySet 메서드가 Map 객체의 키를 모두 담은 Set 뷰(어댑터)를 반환한다. keySet을 호출할 때마다 새로운 Set 인스턴스가 만들어진다고 생각할 수 있지만 사실 매번 같은 Set 인스턴스가 반환되는 것이다.

5. 오토 박싱


오토 박싱이란 기본 타입과 래퍼 타입간에 변환을 자동으로 수행해주는 것을 말한다. 매우 편리한 기능이지만 박싱 과정에서 객체를 생성하기 때문에 성능면에서 문제가 될 수 있다.

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

Long 타입 인스턴스가 하나만 생성됐다고 생각하면 안 된다. sum += i 부분에서 Long 타입과 long 타입 즉, 기본 타입과 래퍼 타입간에 연산을 수행하기 위해 오토박싱이 일어난다(객체 생성). 불필요한 객체가 2^31 개나 생성된 것이다. 의도치 않은 오토박싱이 숨어들지 않도록 주의해야한다.

6. 정리


이번 아이템을 객체 생성이 비싸니 최대한 피하자는 것으로 오해하지 말자. 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것은 일반적으로 좋은 일이다.

최근엔 JVM 성능이 좋아져서 작은 객체를 생성하고 가비지 콜렉트하는 것은 성능에 크게 영향을 주지 않는다. 아주 무거운 객체가 아니고서야 성능을 조금 개선시켜 보겠다고 직접 만든 객체 풀(pool)을 사용하는 것은 코드를 헷갈리게 만들고 오히려 성능을 떨어뜨릴 수 있다. 데이터베이스 커넥션과 같이 생성비용이 상당히 비싼경우는 객체 재사용의 아주 좋은 예라고 볼 수 있다.

이번 아이템은 새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라 라는 아이템50과 대조적이다. 방어적 복사가 필요한 시점에 객체를 재사용하면 피해가 매우 크다. 버그와 보안 구멍으로 이어지는 경우가 많다. 그러나 불필요한 객체 생성은 코드형태와 성능에만 영향을 준다.

profile
아임쿨

0개의 댓글