Item06. 불필요한 객체 생성을 피라하!
똑같은 기능의 객체는 재사용하자!
1
2
3
4
5
// 아래 문장은 실행될 때마다 String 인스턴스를 생성한다. "bikini" 자체가 String 생성자로 만들어내려는 결과와 완전동일하다.
String s = new String("bikini");
// 위 코드의 개선. 하나의 String 인스턴스를 이용한다. 같은 VM 안에서 "bikini" 문자열 리터럴 사용은 이 객체를 재사용함이 보장된다.
String s = "bikini";
1
2
3
4
5
6
// 생성자 대신 정적 팩터리 메서드를 제공하는 불변 클래스에서
// 아래 코드는 불필요한 객체 생성을 계속한다.
Boolean(String);
// 팩터리 메서드 사용을 통해 불필요한 객체 생성을 피할 수 있다.
Boolean.valueOf(String);
- 불변 객체 뿐만 아니라 가변 객체더라도 사용 중 변경이 이뤄지지 않는다면 재사용이 가능하다.
1
2
3
4
//주어진 문자열이 유효한 로마 숫자인지 확인하는 메서드
static boolean isRomanNumeral(String s) {
return s.match("^(?=.)M*(C[MD]|D?C{0,3})"+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"));
}
- 위 경우 성능이 중요한 상황에서 적합한 방법이 아니다.
- 정규표현식에서 패턴으로 사용하는 인스턴스는 한번 사용하고 가비지 컬렉션의 대상이 된다. 즉 쓰레기가 된다.
- isRomanNumeral 함수를 실행할 때마다 유한 상태 머신을 만들기에 생성 비용이 높다.
1
2
3
4
5
6
7
// 미리 객체를 생성하여 캐싱해두고, 재사용하는 방식으로 개선
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();
}
- 성능 개선을 위해 패턴을 Pattern 인스턴스 클래스 초기화 시 직접 생성해 캐싱한다. 즉 메모리에 올려둔다.
- 미리 생성한 패턴 재사용을 통해 이전 방식과 비교해 상당한 성능 개선을 보인다.
불변객체 재사용을 조심하자!
- 불변 객체인 경우에 안정적으로 재사용하는 것이 매우 명확하지만 그렇지 않은 경우도 분명 존재한다.
- 그 예가 어댑터를 사용하는 경우다.
- 어댑터란? 실제 작업은 뒷단 객체에게 위임하고, 자신은 제2의 인터페이스 역할을 해주는 객체이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UsingKeySet {
public static void main(String[] args) {
Map<String, Integer> menu = new HashMap<>();
menu.put("Burger", 8);
menu.put("Pizza", 9);
Set<String> names1 = menu.keySet();
Set<String> names2 = menu.keySet();
names1.remove("Burger");
System.out.println(names2.size()); // 1
System.out.println(menu.size()); // 1
}
}
- Map 인터페이스가 제공하는 keySet의 경우, Map 인스턴스를 대변하는 Set 인터페이스 뷰를 반환한다. keySet 호출을 할 때마다 서로 다른 Set 인스턴스를 반환할 것 같지만 그렇지 않다. 반환하는 인스턴스는 같은 Map을 대변하는 Set이기에 모두 같다.
오토박싱
- 불필요한 객체를 만들어내는 또다른 예이다.
- 오토박싱이란? 프로그래머가 기본 타입과 박신된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.
- 오토박싱은 프리미티브 타입과 박스 타입의 경계를 흐리게 해준다. 하지만 완전히 없애는 것은 아니다.
1
2
3
4
5
6
7
private static long sum() {
Long sum = 0L;
for(long i=0; i<=Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
- 위 코드의 경우, sum을 정의할 때 long 프리미티브 타입이 아닌 Long 박스 타입으로 선언했다. 그렇기 때문에 sum이 계산되는 과정마다 새로운 Long 객체가 생성된다. 매우 비효율적이다.
- Long -> long : 6.3s -> 0.59s
주의 사항
- ‘객체 생성은 비용이 많이 드니 피해야만 한다’로 오해하지 말자! 프로그램의 명확성, 간결성, 기능을 위해 객체를 추가로 생성하는 것이라면 일반적으로 의미있는 일이다.
- 데이터베이스 연결 같이 생성비용이 높은 경우가 아니면 객체 생성을 피하고자 자신만의 객체 풀을 생성해 사용하는 것은 지양하자! 일반적으로 자체적으로 풀을 만들어 사용하면 유지보수성 낮아지고, 잘 최적화된 요즘의 가비지 컬렉터와 비교 시 성능 떨어진다.
- 객체를 재사용하라는 것은 ‘방어적 복사’ 개념과 대비되는 내용이다. 방어적 복사가 필요한 상황에서 객체를 재사용해 얻게 되는 피해가 객체 반복 생성을 통해 얻게 되는 피해보다 훨씬 크다. 방어적 복사 실패 시 버그, 보안적 이슈 등의 피해를 볼 수 있다.
Comments powered by Disqus.