아래 문장은 실행될 때마다 String 인스턴스를 새로 만든다.
String s = new String("bikini");
아래 코드는 매번 인스턴스를 생성하는 대신 하나의 String 리터럴을 사용한다. 이 방식을 사용하면 같은 가상 머신 안에서 이와 같은 똑같은 문자열 리터럴이 같은 객체임을 보장 받을 수 있다.
String s = "bikini";
다음은 정규표현식을 활용한 검증 로직이다.
static boolean isRomanNumeral(String s) {
return s.matches("~~" + "~~");
}
이 방식의 문제는 String.matches 메서드를 사용한다는 것이다. String.matches는 정규표현식으로 문자열 형태를 확인하는 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기에 적합하지 않다. Pattern 인스턴스는 매번 생성되어 쓰이고, 곧바로 GC의 대상이 된다. Pattern은 입력받은 정규표현식에 해당한 유한 상태 머신을 만들기 때문에 인스턴스 생성 비용이 높다.
성능을 개선하기 위해 정규표현식을 표현하는 불변의 Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱해두고, 나중에 isRomanNumeral 메서드가 호출될 때마다 이를 재사용할 수 있다.
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"~~" + "~~"
);
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
이렇게 개선하면 isRomanNumeral()이 자주 호출되는 상황에서 성능을 꽤 끌어올릴수 있다. 또한 상수로 네이밍을 설정할 수 있어 가독성이 좋아졌다.
Lazy하게 isRomanNumeral()이 사용될 때 초기화할 수 있지만, 지연 초기화는 코드를 더 복잡하게 하고 성능상 엄청난 이점은 없다.
불필요한 객체를 만들어내는 또 다른 예는 오토박싱이다. 오토박싱은 Primitive 타입과 Wrapper 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다. 오토박싱은 Primitive 타입과 그에 대응하는 Wrapper 타입의 구분을 흐려주지만, 완전히 없애주는 것이 아니다. 의미상으로 비슷하지만, 성능은 그렇지 않다. Wrapper 타입보다는 Primitive 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.
아주 무거운 객체가 아니면 단순히 객체 생성을 피하고자 객체 풀(Pool)을 만들지 말자. 물론 데이터베이스 컨넥션과 같은 경우 생성 비용이 워낙 비싸기 때문에 재사용하는 편이 낫다. 하지만 일반적으로는 자체 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘리고 성능을 떨어트린다. 요즘 JVM의 GC는 상당히 잘 최적화되어서 가변운 객체용을 다룰 때는 직접 만든 객체 풀보다 훨씬 빠르다.
'객체 생성이 비싸니 피해야 된다'로 오해하면 안된다. 특히나 요즘 JVM에서는 작은 객체를 회수하는 일은 크게 부담되지 않는다. 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하되 불필요하게는 생성하지 말자.