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

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

아래 코드는 하지 말아야 할 극단적인 예시라고 볼 수 있다.

// 아래처럼 사용 X
String s = new String("바보");

위의 문장은 실행될 때 마다 String 인스턴스를 새로 만든다.

완전히 쓸대없는 행위다. 생성자에 넘겨진 "바보" 자체가 이 생성자로 만들어내려는 String과 동일하다. 

이를 개선한 버전을 보자.

String s = "바보";

이 코드는 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용한다.

이 방식을 사용한다면 같은 가상 머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용하는 것이 보장된다.

 

생성자 대신 정적 팩터리 메서드를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다. 예를 들면 Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.

생성자는 호출할 때 마다 새로운 객체를 만들지만, 팩터리 메서드는 그렇지 않다.

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

생성 비용이 비싼 경우도 더러있는데, 이런 비싼 객체가 반복해서 필요하다면 캐싱해서 재사용하도록 하자.

우리는 우리가 만드는 객체가 비싼 객체인지 매번 명확하게 알 수 없다.

 

예를 들어 주어진 문자열이 유효한 로마 숫자인지를 확인하는 메서드를 작성한다고 해보자.

다음은 정규표현식을 활용한 가장 쉬운 해법이다.

 

static boolean isRomanNumeral(String s){
	return s.matches("^(?=.)M*(C[MD])샬라샬라ㅏ);
}

이 방식의 문제는 String.matches 메서드를 재사용 하는 것이다.

String.matches는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기에는 적합하지 않다.

이 메서드가 내부에서 만드는 정규표현식용 Pattern 인스턴스는 한 번 쓰고 버려져서 곧바로 가비지 컬렉터의 대상이 된다. Pattern은 입력받은 정규표현식에 해당하는 유한상태머신..?을 만들기 때문에 인스턴스 생성비용이 높다고 한다.

해당 코드의 성능을 개선하려면 필요한 정규식을 표현하는 Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱해두고, 나중에 isRomanNumeral 메서드가 호출될 때 마다 재 사용하게 한다.

 

//개선된 코드
public class RomanNumerals{
	private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD])샬라샬라ㅏ");
   
   static boolean isRomanNumeral(String s){
		return ROMAN.matcher(s).matches();
	}
}

이렇게 개선하면 isRomanNumeral이 빈번히 호출되는 상황에서 성능을 상당히 끌어올릴 수 있다.

 

개선된 isRomanNumeral 방식의 클래스가 초기화된 후 이 메서드를 한번도 호출하지 않는다면 ROMAN은 쓸대없이 초기화된 것으로 볼 수도 있다.

isRomanNumeral 가 호출될 때 필드를 초기화 하는 지연 초기화는 불필요한 초기화를 없앨수는 있지만 권장되지 않는 방식이다. 성능은 크게 개선되지 않으면서도 코드만 복잡해 질 수 있기 때문이다.

 

객체가 불변이라면 재사용해도 안전함이 명백하다. 하지만, 훨씬 덜 명확하거나 심지어 직관에 반대되는 상황도 있다.

불필요한 객체를 만들어내는 또 다른 예로 오토박싱을 들 수 있다.

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

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

의미상으론 다를것 없어 보이지만, 성능에서는 그렇지 않다.

 

다음 메서드를 보자.

모든 양의 정수의 총합을 구하는 메서드로 int는 충분히 크지 않으니 long을 사용해서 계산하고 있다.

//끔찍하게 느린 코드..ㅎ 
private static long sum(){
    Long sum = 0L;
    for(long i=0; i<Integer.MAX_VALUE; i++){
    	sum += i;
    }
    return sum;
}

프로그램 자체는 정확한 답을 내기는 하지만 제대로 구현했을 때보다 훨씬 느리다.

sum 변수를 Long으로 선언해서 불필요한 Long 인스턴스가 231개나 만들어 진 것이다.

단순히 sum 타입을 long으로 변환해주기만 해도 6초에서 0.6초로 10배 이상 빨라졌다.

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

 

객체 생성은 비싸니 피해야 한다고 오해하면 안된다.

특히나 요즘 JVM 에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일은 크게 부담되지 않는 정도로 발전했다. 프로그램의 명확성 간결성 기능을 위해서 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일이다.

 

반대로! 아주 무거운 객체가 아닌 경우 객체 생성을 피하고자 우리만의 객체 풀을 만들지는 말자.

객체 풀을 만드는 것이 나을 때도 있지만, 일반적으로 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘려 성능을 떨어뜨린다.

 

참고 : 책, 이펙티브 자바 3/E

 

+ Recent posts