-
불필요한 객체를 만들지 말자JAVA/Effective java 2021. 1. 1. 22:37
- 기능적으로 동일한 객체를 새로 만드는 대신 객체 하나를 재사용하는 것이 대부분 적절하다.
- 재사용하면 새로운 객체를 만들지 않기 때문에 성능적으로 더 빠르다. 그리고 불변 객체들은 항상 재사용할 수 있다.
문자열 객체 생성
- 자바의 문자열, String을 new로 생성하면 항상 새로운 객체를 만들게 된다.
- == 로 주소값을 비교한 name과 name2의 값은 false가 나온다. name에서 힙에 메모리가 할당되고
- new로 또 다시 생성했기 때문에 name2에서 새로운 메모리가 할당되어 힙에 생성된다.
- 따라서 다음과 같이 String 객체를 생성하는 것이 올바르다.
String s = "bikini";
- 문자열 리터럴을 재사용하기 때문에 해당 자바 가상 머신에 동일한 문자열 리터럴이 존재한다면
- 그 리터럴을 재사용한다.
- JVM 내부에서 캐싱하고 있는 동일한 String은 동일한 레퍼런스를 참조하게 해 주기 때문이다.
- 스트링 리터럴 재사용에 관한 자세한 내용은 다음의 링크를 참조하자
static 팩토리 메소드 사용하기
- static 팩토리 메소드에 생성자에 비교되는 장점 중 하나는 매번 새로운 객체를 만들지 않고 캐싱해둔 객체를 리턴해도 된다라는 장점이 있었다.
- 자바 9에서 deprecated 된Boolean(String)대신Boolean.valueOf(String)같은 static 팩토리 메서드(아이템 1)를 사용할 수 있다.
- 사실 Boolean은 true 아니면 false 두 개밖에 없지만 생성자를 받는 팩토리 메서드가 있다.
- 이 valueOf라는 팩토리 메서드는 동일한 객체를 반환하고 tree1 == tree2는 true가 나오는 것을 확인할 수 있다.
- 두 값 모두 Boolean.TRUE라는 동일한 상수값이 나오게 된다.
- 새로운 객체를 반환하는 new 는 deprecated 된 것을 확인할 수 있다.
- 생성자는 반드시 새로운 객체를 만들어야 하지만 팩토리 메서드는 그렇지 않다.
무거운 객체
- 만드는데 메모리나 시간이 오래 걸리는 객체 즉 "비싼 객체"를 반복적으로 만들어야 한다면 캐시 해두고 재사용할 수 있는지 고려하는 것이 좋다.
- 정규 표현식으로 예제로 살펴보자. 문자열이 로마 숫자를 표현하는지 확인하는 코드는 다음과 같다.
123static 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})$");}cs - String.matches가 가장 쉽게 정규 표현식에 매치가 되는지 확인하는 방법이긴 하지만 성능이 중요한 상황에서 반복적으로 사용하기에 적절하지 않다.
- String.matches는 내부적으로Pattern객체를 만들어 쓰는데 그 객체를 만들려면 정규 표현식으로 유한 상태 기계로 컴파일하는 과정이 필요하다. 즉 비싼 객체다.
- 성능을 개선하려면 Pattern객체를 만들어 재사용하는 것이 좋다.
123456789public class RomanNumber {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();}}cs - 다음과 같이 정규식에 해당하는 Pattern을 컴파일 해놓고 그다음 메서드 안에서 해당 객체를 재사용하는 것이다.
- 이렇게 하면 반복해서 사용하는 경우 성능적으로 이득을 볼 수 있다.
- 하지만 이 코드도 문제가 있는데,isRomanNumeral메소드가 호출되지 않는다면, ROAM이라는 필요 없이 만든 셈이 된다. 게으른 초기화(lazily initializing)를 사용해서 최적화할 수 있지만 추천하진 않는다.
- 보통 지연 초기화는 측정 가능한 성능 개선 없이 구현을 복잡하게 만든다.
어댑터
- 불변 객체인 경우에 안정하게 재사용하는 것이 매우 명확하다
- 하지만 몇몇 경우에 분명하지 않은 경우가 있다. 오히려 반대로 보이기도 한다.
- 어댑터는 구현체를 다른 인터페이스에 맞게끔 감싸주는 중간 객체이다.
- 이러한 중간 객체는 여러개 만들 필요 없이 하나만 재사용하면 된다.
- Map인터페이스가 제공하는 keySet은 Map이 뒤에 있는 Set인터페이스의 뷰를 제공한다.
- keySet을 호출할 때마다 새로운 객체가 나올 거 같지만 사실 같은 객체를 리턴하기 때문에
- 리턴 받은Set타입의 객체를 변경하면, 결국에 그 뒤에 있는 Map객체를 변경하게 된다.
- name1과 name2는 주소가 똑같다.
- 따라서 names1을 제거하면 name s2도 동일한 주소를 참조하기 때문에 제거된 1을 반환한다. menu도 마찬가지이다.
오토 박싱
불필요한 객체를 생성하는 또 다른 방법으로 오토 박싱이 있다.
오토 박싱은 프로그래머가 프리미티브 타입과 박스 타입을 섞어 쓸 수 있게 해주고 박싱과 언박싱을 자동으로 해준다.
오토박싱은 프리미티브 타입과 박스 타입의 경계가 안 보이게 해 주지만 그렇다고 그 경계를 없애주진 않는다.
- 위 코드에서 sum변수의 타입을 Long으로 만들었기 때문에 불필요한 Long 객체를 2의 31 제곱 개만큼 만들게 되고 대략 6초 조금 걸린다.
- 타입을 프리미티브 타입으로 바꾸면 600 밀리 초로 약 10배 이상의 차이가 난다.
- 불필요한 오토 박싱을 피하려면 박스 타입보다는 프리미티브 타입을 사용해야 한다.
주의할 점
- 이번 아이템으로 인해 객체를 만드는 것은 비싸며 가급적이면 피해야 한다는 오해를 해서는 안된다.
- 특히 방어적인 복사(Depensive copying)를 해야 하는 경우에도 객체를 재사용하면 심각한 버그와 보안성에 문제가 생긴다.
- 객체를 생성하면 그저 스타일과 성능에 영향을 줄 뿐인데 맒이다.
참고 자료
'JAVA > Effective java' 카테고리의 다른 글
Finalizer와 Cleaner는 피하라 (0) 2021.01.03 더이상 쓰지 않는 객체 레퍼런스는 없애자 + Weak Reference (0) 2021.01.02 리소스를 엮을 때는 의존성 주입을 선호하라 (0) 2020.12.31 private 생성자로 noninstantiability를 강제할 것 (0) 2020.12.30 private 생성자 또는 enum 타입을 사용해서 싱글톤으로 만들 것 (0) 2020.12.29