본문 바로가기
책읽고 정리/Effective Java

[Effective Java 3/E] 아이템6. 불필요한 객체 생성을 피하라

by jeonghaemin 2021. 6. 10.
728x90

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

불변 객체는 언제든 재사용할 수 있다.

String s = new String("bikini"); //따라 하지 말 것!

생성자로 문자열을 만들면 매번 새로운 String 인스턴스를 생성(Heap 영역에 할당).

String s = "bikini";

리터럴로 문자열을 만들면 String Constatnc Pool에 할당되어 같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용하는 것이 보장된다.

https://madplay.github.io/post/java-string-literal-vs-string-object

정적 팩터리 메서드

생성자 대신 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다.

생성자는 매번 새로운 객체를 만들지만 팩터리 메서드는 그렇지 않다.

불변 객체가 아닌 가변 객체라도 사용 중에 변경되지 않는다면 재사용할 수 있다.

생성 비용이 비싼 객체는 캐싱하여 재사용

// 주어진 문자열이 유효한 로마 숫자인지를 확인하는 메서드
// 코드 6-1 성능을 훨씬 더 끌어올릴 수 있다!
static boolean isRomanNumeral(String s) {
    return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
           + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}

String.matches : 메서드 내부에서 만드는 Pattern 인스턴스는 한번 쓰고, 버려 저서 가비지 컬렉션의 대상이 된다 -> 성능이 중요한 상황에서 반복해서 사용하기 적합하지 않음.

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

정규표현식을 표현하는 (불변인) Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성하여 캐싱해두고 재사용하여 성능을 개선한다.

public class RomanNumerals {
    // 코드 6-2 값비싼 객체를 재사용해 성능을 개선한다.
    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();
    }    
}

어댑터 패턴

어댑터(뷰(view) 라고도함)는 실제 작업은 뒷단 객체에 위임하고, 자신은 제2의 인터페이스 역할만 해주는 객체이다.

뒷단 객체 외에는 관리할 상태가 없으므로 뒷단 객체 하나당 어댑터 하나씩만 만들어지면 충분한다.

예시

Map인터페이스의 keySet 메서드는 키 전부를 담은 Set뷰를 반환한다.

keySet메서드는 호출 시마다 새로운 Set인스턴스가 만들어지는 것이 아닌, 같은 인스턴스를 반환한다.

Map<Integer, String> map = new HashMap<>();
map.put(1, "hello");

Set<Integer> set1 = map.keySet();

map.put(2, "안녕하세요");

Set<Integer> set2 = map.keySet();

System.out.println(set1 == set2); //true

오토박싱(auto boxing)

오토박싱은 기본 타입과 레퍼 클래스를 섞어서 사용할 때 자동으로 상호 변환해주는 기술이다.

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

성능에 영향을 준다.

//모든 양의 정수의 총합을 구하는 메서드.
//sum변수를 long이 아닌 Long으로 선언하여 불필요한 Long 인스턴스가 매번 생성된다.
private static long sum() {
    Long sum = 0L;

    for(long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i //매번 Long 인스턴스 생성
    }

    return sum;
}

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

오해 - 객체 생성은 비싸니 피해야한다

JVM은 별일 없는 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않는다.

명확성, 간결성, 기능을 위해 객체를 추가로 생성하는 것은 일반적으로 좋은 일이다.

단순히 객체 생성을 피하기 위해 나만의 객체 풀(pool)을 만들지 말자.

  • 객체 풀 : 재사용 가능한 객체들을 모아놓은 클래스
  • DB 연결 같이 생성 비용이 비싼 경우는 재사용하는 것이 낫다.
  • 일반적으로 자체 객체 풀은 코드를 헷갈리게 하고 메모리 사용량을 늘리고, 성능을 떨어트린다.
  • -> 요즘 JVM 가비지 컬렉터는 최적화가 잘 돼있어서 가벼운 객체를 다룰 때는 직접 만든 객체 풀보다 훨씬 빠르다.

방어적 복사(아이템 50)의 "새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라"와 대조적이지만, 방어적 복사 실패 시 피해가 훨씬 크다.

방어적 복사 필요시에 객체 재사용 -> 버그와 보안 구멍으로 이어짐

불필요한 객체 생성 -> 코드 형태와 성능에만 영향을 줌

댓글