티스토리 뷰
제목에서 느낄 수 있듯이 객체를 생성하는데도 컴퓨터 자원이 필요하기 때문에 이를 최소화하기 위한 아이템이다. 제목 그대로 불필요한 객체를 매번 생성하는 것이 아니라 하나의 객체를 재사용 함으로써 더 빠르고 세련된 코드를 짤 수 있다. (불변 객체는 언제든 재사용할 수 있다.)
String s = new String("ch4njun");
String s = "ch4njun";
위 코드에서 첫 번째 코드는 절대 하지 말아야할 극단적인 예시이다. 생성자를 통해 만들려는 객체는 인자로 넘긴 "ch4njun"과 기능적으로 완벽하게 동일한 객체이기 때문이다.
첫 번째 코드를 사용할 때마다 새로운 String 객체를 만드는데 모두 "ch4njun"과 동일한 기능을 가지는 객체가 된다. 즉 첫 번째 코드가 호출됨으로써 "ch4njun"과 동일한 기능을하는 객체가 수백개 생성될 수 있다는 것이다.
첫 번째 코드 대신 두 번째 코드를 사용하면 모든 String 객체는 리터럴 "ch4njun"을 재사용한다.
이와 굉장히 비슷한 맥락은 객체 생성시 생성자 대신 팩토리 메서드를 사용하는 것이다. 정적 팩토리 메서드에 대한 설명은 아래 링크를 통해 확인할 수 있다.https://ch4njun.tistory.com/233
생성자는 호출시마다 완전히 새로운 객체를 생성한다. (반복적이더라도 무조건 새로운걸 만든다!!)
하지만, 정적 팩토리 메서드를 사용하면 불변 객체 뿐만아니라 가변 객체까지도 사용중에 변경되지 않음만 보장한다면 재사용할 수 있다.
생성비용이 비싼 객체
생성비용이 비싼 객체가 반복해서 필요하다면 캐싱해서 재사용 하는 것이 좋다. 하지만 생성비용이 비싼지 여부를 파악하는 것이 쉽지 않다.
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() 를 이용해 정규표현식을 활용한 가장 간단한 해법이다.
하지만 String.matches는 성능이 중요한 상황에서 사용하기엔 적합하지 않다. 이 메서드가 내부에서 만드는 정규표현식용 Pattern 인스턴스는 한 번 쓰고 버려지기 때문에 곧바로 GC의 대상이 된다. Pattern은 입력받은 정규표현식에 해당하는 유한 상태 머신을 만들기 때문에 인스턴스 생성 비용이 높다고 할 수 있다.
(이 내용이 무슨말인지 정확히 이해를 못했다. 그만큼 생성비용이 비싼지 여부를 정확하게 파악하는 것이 쉽지 않다.)
따라서 성능을 개선하기 위해서는 정규표현식을 표현하는 Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱해두고 나중에 isRomanNumeral() 메서드에서 해당 인스턴스를 재사용하는 것이 좋다.
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.complie("^?=.)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).mathces();
}
}
이를 통해 isRomanNumeral() 메서드가 빈번히 호출되는 상황에서 성능을 향상시킬 수 있다.
오토박싱
오토박싱은 프로그래머가 기본 타입(int)과 박싱된 기본 타입(Integer)를 섞어 쓸 때 자동으로 상호 변환해주는 기술이다. 오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만 완전히 없애주는 것은 아니다.
예를 들어, 아래 코드를 살펴보자.
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; ++i)
sum += i;
return sum;
}
sum 변수를 Long으로 선언했기 때문에 sum += i 연산을 수행할 때마다 새로운 Long 객체가 생성된다. 그 결과 원래 코드에서 0.59초 걸렸던 것이 6.3초로 느려지게 되었다.
따라서 박싱된 기본 타입의 사용을 최대한 피해야하고 정확한 의도를 가지고 성능에 영향이 가지않게 사용해야 한다.
정리
이번 포스팅을 객체 생성은 비용이 많이드니 최소화해야한다고 착각하면 안된다. 최근에는 JVM에서 하는일이 많이 없기 때문에 객체를 생성하고 회수하는 일이 큰 부담이 되지 않는다고 한다. 따라서 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것은 일반적으로 좋은 일이다.
하지만 데이터베이스 연결 객체나 위에서 살펴본 Pattern 객체, 오토박싱 객체와 같이 객체생성에 비용이 많이 드는 것이 명확한 경우에만 해당 객체에 대해서 재사용을 구현하는 것이 좋다.
추후 다룰 내용중에 "새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라" 라는 아이템이 있다고 한다. 이번 포스팅과 완벽하게 반대되는 내용이다. 이런 것을 방어적 복사라고 하는데 방어적 복사에 실패했을 때의 피해가 불필요한 객체를 반복 생성했을 때보다 크다는 것을 명심하자.
방어적 복사를 실패한 경우 버그와 보안의 구멍으로 이어질 수 있기 때문이다. 반대로 불필요한 객체를 반복 생성한 것은 단지 코드 형태와 성능에만 영향을 준다.
아직 어쩌란건지 잘 모르겠다. 지금 할 수 있는건 객체 생성 비용이 비싸다는 확신이 든 상황에 맞게 잘 사용해야 한다는 것 정도인 것 같다.
'개발 > Effective JAVA' 카테고리의 다른 글
[아이템9] try-finally보다는 try-with-resources를 사용하라 (0) | 2021.06.22 |
---|---|
[아이템7] 다 쓴 객체 참조를 해제하라 (0) | 2021.06.22 |
[아이템4] 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2021.06.16 |
[아이템2] 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2021.06.16 |
[아이템1] 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2021.06.15 |