불필요한 객체 생성을 피하라
객체 생성의 비용
- 객체를 매번 생성하고 지우는 것은 반복적으로 발생했을 때 큰 비용이 될 수 있다.
- 물론 현대 컴퓨터의 성능이 많이 좋아서 작은 객체는 큰 부담이 되지 않을 수도 있다.
- 계속 같은 내용의 객체를 사용할 것이라면 불변 객체를 만들어놓고 재사용하는 것이 좋다.
불필요한 객체 생성의 예
String 객체의 예
String s = new String("bikini");
위 코드는 안티패턴이다. 결국 bikini라는 문자열을 사용하고 싶은 건데, 굳이 JVM 문자열 풀에서 가져오지 않을 이유가 없다.
String s = "bikini";
더 간결하면서도 더 옳은 코드이다.
Boolean 객체의 예
Boolean boolean = new Boolean(true);
과연 위 객체는 정말 필요한 걸까? 결국 Boolean 객체란 true와 false 2가지 상태밖에 존재하지 않는다.
@Deprecated(since="9", forRemoval = true)
public Boolean(boolean value) {
this.value = value;
}
결국 위와 같이 Java 9 버전부터는 @Deprecated 된 것을 볼 수 있다. 객체 생성 대신 true, false primitive 타입을 쓰거나 Boolean.TRUE 혹은 Boolean.FALSE를 이용하면 미리 생성되어 있는 싱글턴 객체를 불러와 사용할 수 있다.
불필요한 객체 생성의 특징은 매번 같은 내용의 객체를 반복해서 사용하면서, 매번 새로 생성하는 경우이다.
정규표현식에서의 불필요한 객체 생성
기존 코드
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.matchs()메서드는 정규표현식으로 해당 문자열이 내가 원하는 형태인지 확인하는 가장 쉬운 방법이다.
matches() 메서드 사용 중 놓치기 쉬운 부분
public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}
String.matches()메서드의 구현을 따라가보면, 위처럼Pattern인스턴스를 생성하는 부분이 있다.- 위 과정에서 생성되는
Pattern인스턴스는 한번 쓰고 버려져 가비지컬렉션된다. - 정규표현식에 해당되는 유한상태머신을 만들기 때문에 인스턴스 생성 비용이 높다.
- 내부적으로
Pattern을 생성하는 로직을 밖으로 빼서 재활용하면 객체 생성을 1번만 하고 성능을 높일 수 있다.
개선된 코드로 성능 테스트해보기
package item6;
import org.junit.jupiter.api.Test;
import java.util.regex.Pattern;
public class Item6Test {
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}))");
}
static class RomanNumerals {
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();
}
}
@Test
public void regexTest() {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
boolean iiv = isRomanNumeral("IV");
}
long end = System.currentTimeMillis();
System.out.println("매번 새로 생성 : " + (end - start) + "ms");
}
@Test
public void regexTest2() {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
boolean iiv = RomanNumerals.isRomanNumeral("IV");
}
long end = System.currentTimeMillis();
System.out.println("객체 재활용 : " + (end - start) + "ms");
}
}
regexTest메서드에서는 매번 객체를 새로 생성해본다.regexTest2메서드에서는 정적 메서드를 이용하여 생성된 객체를 매번 재활용한다.- 반복 횟수는 동일하게
10000번이다.
결과
객체 재활용 : 6ms
매번 새로 생성 : 46ms비록 6ms와 46ms로 현실세계에서는 별 것 아닌 시간이지만, 단순 비례로 계산해보면 거의 7.x배 차이가 난다. 동시접속자가 많은 서버라면, 이정도면 꽤나 유의미한 차이일 수 있다.
사실
Pattern객체를 더 최적화하면, 사용하지 않을 때는 아예 생성되지 않도록 지연초기화를 할 수 있으나 지연 초기화로 코드가 복잡해지는 반면 성능상 이득이 크게 없을 수 있으므로 이는 적용하지 않는다.
언제 객체를 재생성할 필요 없이 재사용해도 될까?
재사용해도 될 때
- 객체가 불변이라면 안심하고 재사용할 수 있다.
재사용을 조심해야 할 때
- 간단히 말하면, 객체 내부의 내용이 변할 수 있을 때다.
- 다른 스레드에 의해 해당 객체가 변해서 동시성 문제가 발생하게 되면 개발자가 원했던 결과와 다른 결과가 나오게 될 것이다.
어댑터라는 개념에서는 특히 주의해야 한다.어댑터는 실제 작업을 뒷단 객체에 위임하고 자신은 인터페이스의 역할만 하는 것이다.어댑터는 사실상View의 역할을 하기 때문에 실제 객체의 내용이 바뀌면어댑터가 바라보는 내용도 바뀐다.
간단한 예제
Map인터페이스의keySet()은Map안에 들어있는key를Set타입으로 반환한다.- 이 반환받은
Set을 저장해놓고 재활용해도 될까?- 안된다.
keySet()이라는 메서드가 매번Set을 생성한다고 보장할 수 없다.
- 안된다.
keySet()메서드의 구현을 살펴보자.
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new AbstractSet<K>() {
public Iterator<K> iterator() {
return new Iterator<K>() {
private Iterator<Entry<K,V>> i = entrySet().iterator();
public boolean hasNext() {
return i.hasNext();
}
public K next() {
return i.next().getKey();
}
public void remove() {
i.remove();
}
};
}
public int size() {
return AbstractMap.this.size();
}
public boolean isEmpty() {
return AbstractMap.this.isEmpty();
}
public void clear() {
AbstractMap.this.clear();
}
public boolean contains(Object k) {
return AbstractMap.this.containsKey(k);
}
};
keySet = ks;
}
return ks;
}
ks라는 변수에 해당 클래스가 가진keySet필드의 값을 할당한다.ks가null이라면AbstractSet클래스를 이용해 새로운 객체를 넣어 할당한다.- 만들어둔 객체는 재활용할 수 있도록
keySet필드에 할당해둔다.
ks가null이 아니라면keySet필드의 값을 재활용한다.
Map.keySet()으로 할당받은 Set은 어댑터의 대표적인 예이다. 원본 Map이 바뀌면 keySet도 바뀌므로, 항상 인지하고 있어야 한다. 또한, 매번 Map.keySet()을 하더라도 같은 객체를 반환할 것이다. Map 객체가 이를 재사용하고 있음을 인지하고 코딩하는 것이 좋다.
primitive 오토 박싱의 함정
primitive 오토 박싱은 primitive 타입을 객체화 시켜준다. 이로 인해, 일반 객체처럼 null 값을 할당해두는 것도 가능해지고, 제너릭스에 타입으로 줄 수도 있게 된다. 그러나, 이렇게 의도한 박싱도 있는 반면 나도 모르게 박싱이 되는 경우도 있다.
파라미터 타입에 의한 오토박싱
public static void unintendedBoxing1(Integer number1, Integer number2) {
System.out.println(number1 + number2);
}
위와 같은 메서드가 있을 때, 일반 int 타입을 넘기더라도 파라미터 타입에 의해 오토 박싱이 일어난다. 이는 작은 작업에서는 별다른 두각을 나타내지 않지만, 이 메서드를 이용해 대량의 작업을 하면 분명히 그냥 int 타입을 활용하는 것보다 훨씬 느리게 될 것이다.
변수 타입에 의한 오토박싱
private static long unintendedBoxing2() {
Long sum = 0L;
for(long i=0; i<=Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
위는 왜 오토 박싱이 발생하는 것일까? 그 이유는 모든 합계를 더해줄 sum을 Long으로 선언하였기 때문이다.
/**
* The value of the {@code Long}.
*
* @serial
*/
private final long value;
Long 클래스 내부에는 저렇게 final로 불변 value를 가지고 있다. 그렇기 때문에 값을 바꾸는 행위는 사실 새로운 Long 객체를 한번 더 생성 해야만 가능한 것이다.
이렇게 의도치 않게, 불필요한 객체를 재생성해 낭비가 발생할 수 있다.
그렇다면 어떻게 코딩을 해야 할까?
primitive 타입을 사용해도 동작에 별다른 지장이 없는 경우엔 무조건 primitive타입을 사용하자.
객체 생성 자체를 최대한 피해야 할까?
정답은 아니다. JVM에서 작은 객체를 생성하고 회수하는 게 그렇게 큰 일은 아니다.
그러나 몇가지 확실히 객체 생성이 안좋을 때가 있다.
- 첫째, 불필요한 객체 생성일 때
- ex1) 같은 정규표현식
Pattern객체를 재사용해도 무관할 때 - ex2)
primitive타입만 사용해도 되는 경우인데, 불필요한오토 박싱이 일어나 낭비를 줄 때
- ex1) 같은 정규표현식
- 둘째, 부하가 많은 서버 환경일 때
- 이 경우엔 작은 낭비도 사용자들에 의해 큰 낭비가 되어버릴 수 있으니 내가 객체 생성을 새로 하고 있다는 것을 인지하고 있는 것이 좋다.
이후에 나오는 내용 중,
새로운 객체를 만들어야 한다면, 기존 객체를 재사용하지 마라라는 내용도 있다. 무조건 xx해라. 라는 것은 없고 결국 상황에 맞는 코드를 사용하는 것이 가장 중요하다.
방어적 복사가 필요한 때 기존 객체를 재사용하는 것이 필요없는 객체를 반복 생성하는 것보다 훨씬 피해가 크다는 점을 분명히 인지해두어야 한다.
그렇다면 객체 풀을 만드는 것은 어떨까?
- 요즘 JVM의 가비지 컬렉터는 상당히 최적화가 잘되어서 사용자가 어설프게 만든 객체 풀보다는 훨씬 빠르다.
- 되려 코드의 복잡성만 증가할 수 있다.
'Java > 이펙티브 자바' 카테고리의 다른 글
| 이펙티브 자바, 쉽게 정리하기 - item8. finalizer와 cleaner 사용을 피하라 (0) | 2021.12.27 |
|---|---|
| 이펙티브 자바, 쉽게 정리하기 - item7. 다 쓴 객체 참조를 해제하라 (0) | 2021.12.27 |
| 이펙티브 자바, 쉽게 정리하기 - item5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2021.12.25 |
| 이펙티브 자바, 쉽게 정리하기 - item4. 인스턴스화를 막으려면 private 생성자를 사용하라 (0) | 2021.12.25 |
| 이펙티브 자바, 쉽게 정리하기 - item3. private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2021.12.24 |