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

신민철·2023년 4월 11일
1

Effective Java

목록 보기
6/23
post-thumbnail

객체는 항상 생성하는 것보다 있던 걸 재사용하는 것이 좋을 때가 많다.

다음은 하지 말아야 할 예시다.

String s = new String("mincshin");

이 문장은 실행될 때마다 새로운 String 인스턴스를 만든다. 생성자에 넘겨진 “mincshin”자체가 이 생성자로 만들어내려하는 String과 완전히 똑같은데, 반복적으로 호출되게 되면 쓸데없는 인스턴스가 엄청나게 많이 만들어질 수도 있다.

String s = "mincshin";

새로운 인스턴스를 항상 만들어내지 말고 하나의 String 인스턴스를 사용한다. 이는 같은 문자열 리터럴이면 같은 객체임을 보장한다.

public final class Boolean implements java.io.Serializable,
                                      Comparable<Boolean>, Constable
{
	public static final Boolean TRUE = new Boolean(true);
  public static final Boolean FALSE = new Boolean(false);

	@Deprecated(since="9", forRemoval = true)
	public Boolean(boolean value) {
		this.value = value;
  }

	@IntrinsicCandidate
  public static Boolean valueOf(boolean b) {
	  return (b ? TRUE : FALSE);
  }
}

이 코드는 Boolean 클래스이다. Boolean의 생성자는 deprecated되었고 대신 valueOf 팩토리 메소드를 통해 불필요한 TRUE, FALSE 객체의 재생성을 막고 같은 객체를 재사용한다.

Boolean 처럼 불변 객체가 아니더라도 사용중에 변경이 되지 않음을 안다면 재사용할 수 있는 것이다.

생성 비용이 비싼 객체도 있을텐데 반복해서 필요하다면 캐싱을 해서 사용하는 것이 좋다.

아쉽게도 생성 비용이 비싼지 잘 파악을 못하는 경우가 많다. 그래서 다음의 예를 살펴보자.

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 인스턴스로 만들어져서 matches를 사용한 후에 인스턴스가 버려진다. Pattern은 유한 상태 머신(FSM, finite state machine)을 만들기 때문에 생성 비용이 높다.

그래서 이와 같은 경우에는 직접 생성해 캐싱해두고 메소드가 호출될 때마다 재사용하는 것이 효율적일 것이다.

static boolean isRomanNumeral(String s) {
	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 isRomanNumeral(String s) {
		return ROMAN.matcher(s).matches();
	}
}

이렇게 작성하면 성능이 개선될 뿐 아니라 Pattern 인스턴스를 끄집어내어 코드의 의미가 잘 드러나게 된다.

이렇게 개선을 시켜놔도 isRomanNumeral 메소드가 호출되지 않으면 무용지물일 것이다. 그래서 지연 초기화(lazy initialization)로 불필요한 초기화를 없앨 수 있긴 한데 코드를 복잡하게 만들고 성능이 크게 개선되지 않기 때문에 권하지는 않는다고 한다.

final이 붙으면 재사용하기 좋을 것 같다. 하지만 모든 객체의 인스턴스는 final이 아니기 때문에 애매할 것이다… 디자인 패턴 중에 어댑터 패턴을 생각해보자. 서로 다른 인터페이스를 이어주기 위해 만든 객체이기 때문에 많을 필요가 없다. 이런 경우엔 딱 하나만 쓰는게 의미 있다는 것이 보장된다.

여기서 어댑터(Adapter)란?

우리가 일상생활에서 사용하는 어댑터랑 원리가 똑같다. 우리가 충전기로 기기를 충전하려면 어댑터를 전기 콘센트에 꽂아야 한다. 어댑터 패턴도 마찬가지다. 한 객체의 인터페이스를 다른 객체가 이해할 수 있도록 변환하는 특별한 객체이다.

책에서 불필요한 객체를 만들어내는 다음 예시로 든 것은 오토박싱(auto boxing)이다.

이것은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 변환해주는 기술이다.


오토박싱을 이해하려면 래퍼 클래스(Wrapper Class)에 대해서 이해해야 한다. 우리는 int, float, double, boolean.. 같은 기본 타입(Primitive Type)에 대해 생각해볼 것이다. 사실 primitive type은 객체가 아니지만 객체처럼 사용해야 할 때가 있다. 그럴 때 Integer.parseInt()처럼 문법을 일상적으로 쓰지는 않는다. 이럴 때 래퍼 클래스를 사용하는 것이다. (Integer, Float, Double, Boolean..)


래퍼 클래스의 문제는 산술적 연산을 위해 만들어진 클래스가 아니라 값이 저장되면 바뀌지 않는다. 기본 타입의 데이터가 래퍼 클래스로 바뀌고 또 반대도 이루어지고 한다. 각각을 박싱, 언박싱이라고 부른다.

다음 코드를 살펴보자.

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

	return sum;
}

여기서는 래퍼클래스인 Integer와 primitive type인 i가 비교가 되기 때문에 자동으로 박싱을 해주게 된다. 문제는 의도치 않게 오토박싱이 발생할 수 있다는 것이다.

sum이 Long으로 선언되었기 때문에

  1. sum이 오토 언박싱이 이루어져 long으로 변환됨
  2. sum과 i가 더해짐
  3. 다시 Long으로 오토 박싱

이런 식이면 효율성이 매우 떨어지게 된다. 그래서 타입 체킹을 신경써서 해야되는 것이다.

그래도 최근 JVM은 작은 객체의 생성과 소멸에는 성능 저하가 잘 발생하지 않는다. 그래도 큰 규모로 갈 때는 신경써서 체크해주는 것이 좋겠다.

핵심 정리
객체가 반복적으로 사용될 때는 인스턴스를 먼저 생성해서 캐싱을 하여 사용하는 것이 좋다. 그리고 기타 상황에서 오토 박싱의 비효율이 발생하는 것을 피해야 한다.

0개의 댓글