-
똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다.
- 재사용은 빠르고 세련됨.
- 특히 불변 객체는 언제든 재사용할 수 있다.
String s = new String("bikini"); // 따라 하지 말 것
String s = "bikini";
-
첫번째 코드는 String 인스턴스를 새로 만들게 되어 생성자에 넘겨진 "bikini" 자체가 이 생성자로 만들어내려는 String과 기능적으로 완전히 똑같다.
- 반복문이나 빈번히 호출되는 메서드 안에 있다면 쓸데없는 String 인스턴스가 수백만 개 만들어질 수도 있다.
-
두번째 코드는 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용한다.
- 같은 가상 머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장됨. (상수 풀 사용)
-
생성자 대신 정적 팩터리 메서드를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다.
- Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다. (그래서 이 생성자는 자바 9에서 deprecated 되었다)
-
생성 비용이 아주 비싼 객체도 있다.
- 이런 객체는 반복해서 필요하다면 캐싱하여 재사용하길 권함.
-
대표적인 예로 String의 matches 메서드가 있다.
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})$"); }
- String.matches는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기엔 적합하지 않음.
-
메서드 내부에서 정규표현식용 Pattern 인스턴스를 만드는데 Patten은 입력받은 정규표현식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높음.
- 유한 상태 머신이란 한번에 하나의 상태를 가지며 특정 이벤트에 의해 한 상태에서 다른 상태로 전이할 수 있는 기계라고 하는데 정규표현식은 이러한 유한 상태 머신을 이용해서 구현된다고 한다.
- 모든 상태와 전이를 찾아놓고 매칭을 하기에 생성 비용이 높다고 한다.
- 성능 개선을 위해 정규표현식을 표현하는 (불변인) Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고, 나중에 isRomanNumeral 메서드가 호출될 때마다 이 인스턴스를 재사용함.
public class RomanNumerals { 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(); } }
-
ROMAN 필드를 메서드가 처음 호출될 때 필드를 초기화할 수 있게 지연 초기화를 사용할 순 있지만 권하지는 않는다.
- 코드가 복잡해지고 성능은 크게 개선되지 않을 때가 많기 때문.
-
객체가 불변이라면 재사용해도 안전함이 명백하다.
- 하지만 훨씬 덜 명확하거나, 심지어 직관에 반대되는 상황도 있음.
-
대표적인 예로 어댑터 패턴(Adapter Pattern)이 있음.
- 어댑터는 실제 작업은 뒷단 객체에 위임하고, 자신은 제2의 인터페이스 역할을 해주는 객체.
- 어댑터는 뒷단 객체만 관리하면 됨.
- 즉, 뒷단 객체 외에는 관리할 상태가 없으므로 뒷단 객체 하나당 어댑터 하나씩만 만들어지면 충분함.
-
예컨대 Map 인터페이스의 keySet 메서드는 Map 객체 안의 키 전부를 담은 Set 뷰를 반환함.
- keySet을 호출할 때마다 새로운 Set 인스턴스가 만들어지는게 아닌 매번 같은 Set 인스턴스를 반환함.
- 반환한 객체 중 하나를 수정하면 다른 모든 객체가 따라서 바뀜.
- keySet이 뷰 객체를 여러 개 만들어도 상관없지만, 그럴 필요도 없고 이득도 없음.
-
불필요한 객체를 만들어내는 또 다른 예로 오토박싱(auto boxing)을 들 수 있음.
- 오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술.
- 오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아님.
- 의미상으로는 별다를 것 없지만 성능에는 그렇지 않음.
private static long sum() { Long sum = 0L; for (long i = 0; i <= Integer.MAX_VALUE; i++) sum += i; return sum; }
- sum 변수를 Long으로 선언해서 불필요한 Long 인스턴스가 약 2의 31승개나 만들어진 것.
- 이 경우에 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.
정리
-
"객체 생성은 비싸니 피해야 한다"로 오해하면 안 됨.
- 요즘 JVM은 작은 객체 생성하고 회수하는 일이 크게 부담되지 않음.
- 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일.
-
아주 무거운 객체가 아닌 이상 단순히 객체 생성을 피하고자 객체 풀(pool)을 만들지는 말자.
- DB Connection과 같이 생성 비용이 비싼 경우가 아니고서야 자체 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘리고 성능을 떨어뜨림.
- 요즘 JVM GC는 상당히 잘 최적화되어서 가벼운 객체용을 다룰 때는 직접 만든 객체 풀보다 훨씬 빠르다.
-
방어적 복사가 필요한 상황에서 객체를 재사용했을 때의 피해가 필요 없는 객체를 반복 생성했을 때의 피해보다 훨씬 크다는 사실을 기억하자.
- 방어적 복사에 실패하면 버그와 보안 구멍으로 이어지지만, 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 줌.