럿고의 개발 노트
아이템 6. 불필요한 객체 생성을 피하라. 본문
이펙티브 자바 - 2장. 객체 생성과 파괴
아이템 6. 불필요한 객체 생성을 피하라.
- 똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 훨씬 나을때가 있다. 특히 불변 객체는 언제든 재사용할 수 있다.
Item1
에서 팩터리 메서드를 제공하는 불변클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다.- 예를들어
Boolean(String)
생성자 대신Boolean.valueOf(String)
팩터리 메서드를 사용하는 것이 좋다. - 생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리 메서드는 전혀 그렇지 않다.
- 불변 객체만이 아니라 가변 객체라 해도 사용 중에 변경 되지 않을 것임을 안다면 재사용할 수 있다.
- 생성 비용이 아주 비싼 객체는 반복해서 필요하다면 캐싱하여 재사용하길 권한다.
String 예제
String str = new String("woowa");
- 위의 코드는 실행될 때마다
String
인스턴스를 새로 만든다. 빈번히 호출된다면 쓸데없는String
인스턴스가 수없이 만들어 질 것이다.
String str = "woowa";
- 위의 코드는 매번 인스턴스를 생성하는게 아니라 하나의
String
인스턴스를 사용하게 된다. - 즉, 같은 가상 머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.
정규표현식 예제
- 대표적인 비싼 객체로
String.matches
가 있다. - 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기엔 적합하지 않다.
public class RomanNumerals {
static boolean isRomanNumeralSlow(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
}
- 이 메서드 내부에서
Pattern
인스턴스는, 한 번 쓰고 버러져서 곧바로 GC 대상이 된다.Pattern
은 입력받은 정규표현식에 해당하는 유한 상태 머신을 만들기 때문에 인스턴스 생성 비용이 높다. - 성능을 개선하려면 필요한 정규표현식을 표현하는 (불변인)
Pattern
인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고, 나중에isRomanNumeralSlow
메서드가 호출될 때마다 이 인스턴스를 재사용 한다.
import java.util.regex.Pattern;
public class RomanNumerals {
static boolean isRomanNumeralSlow(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
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 isRomanNumeralFast(String s) {
return ROMAN.matcher(s).matches();
}
public static void main(String[] args) {
int numSets = 1;
int numReps = 1;
boolean b = false;
for (int i = 0; i < numSets; i++) {
long start = System.nanoTime();
for (int j = 0; j < numReps; j++) {
b ^= isRomanNumeralSlow("MCMLXXVI");
}
long end = System.nanoTime();
System.out.println(((end - start) / (1_000 * numReps)) + " us.");
}
if(!b){
System.out.println();
}
for (int i = 0; i < numSets; i++) {
long start = System.nanoTime();
for (int j = 0; j < numReps; j++) {
b ^= isRomanNumeralFast("MCMLXXVI");
}
long end = System.nanoTime();
System.out.println(((end - start) / (1_000 * numReps)) + " us.");
}
if(!b){
System.out.println();
}
}
}
- 개선된 isRomanNumeral 방식의 클래스가 초기화된 후 이 메서드를 한 번도 호출하지 않는다면 ROMAN 필드는 슬데없이 초기화된 꼴이다.
isRomanNumberal
가 처음 호출될 때 필드를 초기화하는 지연 초기화로 불필요한 초기화를 없앨 수는 있지만, 권하지는 않는다. 지연 초기화는 코드를 복잡하게 만드는데, 성능은 크게 개선되지 않을 때가 많기 때문이다.
불변객체의 재사용
- 객체가 불변이라면 재사용해도 안전함이 명백하다. 하지만 훨씬 덜 명확하거나, 심지어 직관에 반대되는 상황도 있다.
- 어댑터(=뷰)는 실제 작업은 뒷단 객체에 위임하고, 자신은 제2의 인터페이스 역할을 해주는 객체다. 어댑터는 뒷단 객체만 관리하면 된다. 즉, 뒷단 객체 외에는 관리할 상태가 없으므로 뒷단 객체 하나당 어댑터 하나씩만 만들어지면 충분하다.
- 예를들어
Map
인터페이스의keySet
은Map
객체 안에 키 전부를 담은Set
뷰를 반환한다. keySet
을 호출할 때 마다 새로운Set
인스턴스가 만들어지라고 순진하게 생각할 수 있지만, 사실은 매번 같은Set
인스턴스를 반환할지도 모른다.- 반환된
Set
인스턴스가 일반적으로 가변이더라도 반환된 인스턴스들은 기능적으로 모두 똑같다. 즉, 반환한 객체 중 하나를 수정하면 다른 모든 객체가 따라서 바뀐다. 모두가 똑같은Map
인스턴스를 대변하기 때문이다. - 따라서
keySet
이 뷰 객체를 여러 개 만들어도 상관은 없지만, 그럴 필요도 없고 이득도 없다.
오토박싱 예제
- 오토박싱이란 기본 타입과 박상된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.
- 오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다. 의미상으로는 별다를 것 없지만 성능에서는 그렇지 않다.
public class Sum {
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++){
sum += i;
}
return sum;
}
public static void main(String[] args) {
int numSets = 1;
long x = 0;
for (int i = 0; i < numSets; i++){
long start = System.nanoTime();
x += sum();
long end = System.nanoTime();
System.out.println((end - start) / 1_000_000. + " ms.");
}
if(x == 42){
System.out.println();
}
}
}
- 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.
오해
- "객체 생성은 비싸니 피해야 한다"라고 오해하면 안된다.
- 요즘의 JVM에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않는다.
- 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일이다.
- 아주 무거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 여러분만의 객체 풀을 만들지는 말자.
- 일반적으로는 자체 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘리고 성능을 떨어뜨린다.
- 요즘 JVM의 가비지 컬렉터는 상당히 잘 최적화되어서 가벼운 객체용을 다룰 때는 직접 만든 객체 풀보다 훨씬 빠르다.
- 데이터베이스 연결 같은 경우 생성 비용이 비싸니 재사용하는 편이 낫다.
- 방어적 복사를 다루는 아이템 50인 "새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라"와는 대조적이다.
- 방어적 복사가 필요한 상황에서 객체를 재사용했을 때의 피해가, 필요 없는 객체를 반복 생성했을 때의 피해보다 훨씬 크다는 사실을 기억하자.
- 방어적 복사에 실패하면 언제 터져 나올지 모르는 버그와 부안 구멍으로 이어지지만, 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 준다.
'Java Note > 이펙티브 자바 3판(EFFECTIVE JAVA 3E)' 카테고리의 다른 글
아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라. (0) | 2020.03.23 |
---|---|
아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라. (0) | 2020.03.17 |
아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라. (0) | 2020.03.16 |
아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라. (0) | 2020.03.15 |
아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라. (0) | 2020.03.14 |
Comments