본문 바로가기

Dev Book Review/Effective Java 3판

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

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

특히 불변객체는 언제든 재사용할 수 있다!

1.  String constant pool 사용

String s1 = "hello"; // String constant pool에 존재 (Heap 영역 내 위치) 
String s2 = "hello"; // s1을 캐싱하여 재사용
String s3 = new String("hello"); // Heap 영역에 존재 (새로운 객체)

 

같은 JVM 안에서 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.

참고1: https://starkying.tistory.com/entry/what-is-java-string-pool
참고2: https://medium.com/@joongwon/string-%EC%9D%98-%EB%A9%94%EB%AA%A8%EB%A6%AC%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0-57af94cbb6bc

2. 정적 팩터리 메서드를 제공하는 불변 클래스

또한 생성자 대신 정적 팩터리 메서드를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 막을 수 있다.

Boolean(String); //deprecated
Boolean.valueOf(String); // 권장

Integer i1 = new Integer(10); // 새로운 객체
Integer i2 = Integer.valueOf(10); // 캐싱
Integer i3 = Integer.valueOf(10); // 캐싱

이처럼 Wrapper 클래스는 캐싱을 지원하는 valueOf() 메서드가 존재한다.

불변 객체만이 아니라 가변 객체라 해도 사용 중에 변경되지 않을 것임을 안다면 재사용할 수 있다.

3. 생성 비용이 아주 비싼 객체는 캐싱하여 재사용

1) 크기가 아주 큰 Array

2) DB connection

3) I/O 작업을 필요로 하는 객체

4) Pattern

 

다음과 같은 경우는 생성 비용이 비싸다. 따라서 이러한 객체를 반복해서 필요하다면 캐싱하여 재사용하길 권한다.

// 여기서 성능을 훨씬 더 끌어올릴 수 있다.
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)
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();
}

String.matches()에서 사용하는 정규표현식용 Pattern 인스턴스는, 한 번 쓰고 버려져서 GC 대상이 된다.

따라서 항상 같은 Pattern이 필요함이 보장되고 재사용 빈도가 높다면, 클래스 초기화 과정에서 상수로 캐싱하여 재사용할 수 있다.

4. 어댑터 패턴

어댑터: 실제 작업은 뒷단 객체에 위임하고, 자신은 제2의 인터페이스 역할을 하며 뒷단 객체만 관리한다. 뒷단 객체 당 어댑터 하나씩.
Map<String, Object> map = new HashMap<>();
map.put("hello", "world");

Set<String> s1 = javabom.keySet(); // s1과 s2는 같은 Map을 바라본다.
Set<String> s2 = javabom.keySet();

이때 Map 인터페이스의 keySet() 메서드는 Map 객체 안의 키 전부를 담은 Set 뷰(어댑터)를 반환한다.

 

반환된 Set 인스턴스가 일반적으로 가변이더라도, 반환된 인스턴스들은 기능적으로 모두 똑같다. (같은 Map을 바라본다)

결론적으로 keySet() 메서드는 매번 같은 인스턴스를 반환한다. (굳이 새로운 인스턴스를 생성할 이유가 없다..)

5. 오토박싱 (auto boxing)

오토박싱이란 기본 타입-박싱된 기본 타입을 상호 변환해주는 기술이다.

따라서 두 타입 간의 구분을 흐려주지만, 완전히 같게 만들지는 않는다.

private static long sum(){
  Long sum = 0L; // 여기가 문제!	
  for(long i =0; i <= Integer.MAX_VALUE; i++){
    sum += i;	// 불필요한 Long 인스턴스가 만들어진다.
  }
  return sum;
}

물론 해당 코드는 정확한 답을 낸다. 하지만 제대로 구현했을 때보다 훨씬 느리다.

sum을 Long 타입으로 선언했기 때문에 sum += i 을 실행할 때 i를 Long 타입으로 계속 변환해주어야 한다.

 

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