- 이 글은 Effective Java 를 기반으로 작성되었습니다.
- 불필요한 객체 생성
기능이 동일한 객체가 있다면 새로 생성하지 않고 재사용을 하는 편이 좋다. 특히 변경 불가능한 immutable 객체의 경우 값을 변경하지 않기 때문에 언제나 재사용이 가능하다.
String test1 = "test1";
String test2 = "test1";
String test3 = new String("test1");
String test4 = new String("test1");
위의 코드에서 모두 같은 test1 이라는 문자열을 생성하지만 test1 과 test2 는 리터럴 형식으로, test3 과 test4 는 new 를 이용해서 String 객체를 생성했다.
test1 과 test2 는 JVM 내에서 같은 String 객체를 생성 및 참조하게 된다. String Pool 에 저장하고 사용할 때 이미 기존에 저장된 같은 값이 있다면 이를 재사용하고 없으면 신규로 할당한다. 반면 test3 과 test4 의 경우에는 new 를 이용하여 생성했기 때문에 Heap 영역에 저장된다.
Java6 이전에는 String Pool 이 PermGen 영역에 존재하였는데 너무 많은 문자열을 사용하면 OOM 이 발생한다. JVM 옵션 인자값으로 조절이 가능하지만 동적이지 않다는 점에서 문제가 되었다. Java7 에서는 이런 문제점을 인식했는지 String Pool 이 Heap 영역으로 옮겨갔으며 Java8 에서는 PermGen 영역이 고정크기인것 자체가 문제라는것을 인식하여 아예 없애 버리고 Metaspace 로 이름을 변경하고 크기도 동적으로 변경되었다.
public class StringTest {
public static void main(String[] args) {
String test1 = "test1";
String test2 = "test1";
String test3 = new String("test1");
String test4 = new String("test1");
System.out.println("test1 == test2: " + (test1 == test2));
System.out.println("test2 == test3: " + (test2 == test3));
System.out.println("test3 == test4: " + (test3 == test4));
}
}
.....
test1 == test2: true
test2 == test3: false
test3 == test4: false
test1 과 test2 가 true 인 이유는 String Pool 에서 같은 곳을 참조하기 때문이며, test3 과 test4 가 다른 이유는 new 로 생성할때마다 Heap 영역에 객체가 새롭게 생성되기 때문이다.
String 은 불변객체이기 때문에 new 를 이용해서 생성할 필요가 없다.
- 생성자와 정적 팩토리 메소드
생성자와 정적 팩토리 메소드를 동시에 제공한다면 정적 팩토리 메소드를 이용하는 편이 좋다. 생성자를 이용하면 무조건 객체가 새로 생성되지만 정적 팩토리 메소드를 이용하면 상황에 따라 객체를 재사용할 수 있기 때문이다.
public class BooleanTest {
public static void main(String[] args) {
Boolean test1 = new Boolean("true");
Boolean test2 = new Boolean("true");
Boolean test3 = Boolean.valueOf("true");
Boolean test4 = Boolean.valueOf("true");
System.out.println("test1 == test2: " + (test1 == test2));
System.out.println("test3 == test4: " + (test3 == test4));
}
}
.........
test1 == test2: false
test3 == test4: true
"true" 를 생성자 인자로 하여 생성한 test1 과 test2 의 경우 신규 객체를 반환하지만 정적 팩토리 메소드인 Boolean.valueOf 의 경우 "true" 의 인자로 반환된 Boolean 객체는 재사용되었다. Boolean 도 불변객체이기 때문에 굳이 새롭게 new 생성자를 이용해서 생성할 필요가 없다.
- 재사용을 위한 초기화
적절하게 초기화를 하지 않아서 재사용을 할 수 있음에도 불구하고 매번 신규로 객체 생성을 하는 경우도 있다.
public class StaticInitializeTest {
private final Date birthDate;
public StaticInitializeTest(Date birthDate) {
this.birthDate = birthDate;
}
public boolean isBabyBoomer() {
Calendar gmtCal = Calendar.getInstance(TimeZone.getDefault());
gmtCal.set(1946, Calendar.JANUARY, 1);
Date babyBoomerFrom = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1);
Date babyBoomerTo = gmtCal.getTime();
return birthDate.compareTo(babyBoomerFrom) >= 0 &&
birthDate.compareTo(babyBoomerTo) < 0;
}
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
for(int i = 0; i < 10000000; i += 1) {
StaticInitializeTest test = new StaticInitializeTest(new Date());
test.isBabyBoomer();
}
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
}
위의 예제 Code 는 isBabyBoomer 메소드로 BabyBoomer 세대인지 판단하는 코드이다. 해당 메소드 안에서 babyBoomerFrom 과 babyBoomerTo 에 값을 할당할 때 Calendar 를 이용하는데 한번 할당 후 변하지 않는 값이므로 실질적으로 상수로 보아야 한다. 하지만 위의 코드는 메소드 안에 있어서 호출마다 객체의 생성이 일어난다.
public class StaticInitializeTest2 {
private static final Date babyBoomerFrom;
private static final Date babyBoomerTo;
static {
Calendar gmtCal = Calendar.getInstance(TimeZone.getDefault());
gmtCal.set(1946, Calendar.JANUARY, 1);
babyBoomerFrom = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1);
babyBoomerTo = gmtCal.getTime();
}
private final Date birthDate;
public StaticInitializeTest2(Date birthDate) {
this.birthDate = birthDate;
}
public boolean isBabyBoomer() {
return birthDate.compareTo(babyBoomerFrom) >= 0 &&
birthDate.compareTo(babyBoomerTo) < 0;
}
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
for(int i = 0; i < 10000000; i += 1) {
StaticInitializeTest2 test = new StaticInitializeTest2(new Date());
test.isBabyBoomer();
}
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
}
위의 코드는 babyBoomerFrom 과 babyBoomerTo 를 static 초기화 블록을 이용하여 재사용 할 수 있도록 변경한 코드이다. 재사용으로 인하여 성능상의 이점이 있어서 수행시간의 차이가 꽤 난다. 한 가지 추가적인 장점도 얻을 수 있는데 final 예약어로 인하여 한번 설정하면 값이 변하지 않는 상수라는 개념이 명확해졌다.
- AutoBoxing
Java 에는 int, long 과 같은 primitive type 과 이를 감싼 Integer, Long 과 같은 클래스들을 지원한다. 코드 작성시에 int 를 Integer 형으로 변환해야할 때 특별한 형변환 없이 그대로 할당해도 형변환을 하라고 하지 않는데 이는 AutoBoxing 때문이다.
public class AutoBoxingTest {
public static void main(String[] args) {
Long sum = 0L;
for(int i = 0; i < 100000; i += 1) {
sum += i;
}
System.out.println("sum: " + sum);
}
}
예제 코드에서 Long 형의 sum 을 이용하여 int 형을 계속 더하고 있는데, 기존 sum 값에 i 를 더하는 부분에서 AutoBoxing 된 객체들이 생성된다. 얼핏보면 성능에 크게 영향을 줄 것 같진 않지만 의외로 숫자가 커지면 차이가 많이 난다.
- 객체 풀(Object pool)
웹 프로그래밍시 Tomcat 같은 서블릿 컨테이너나 WAS 를 사용하는 경우에 DB 에 접속할때마다 Connection 을 새로 얻지 않고 일정 수를 Connection Pool 에 미리 할당해두어 재사용한다. 이런 객체 풀도 불필요한 객체를 생성하지 않는 원리를 이용한것이다.
'Language > Java' 카테고리의 다른 글
이펙티브 자바 - 종료자 사용을 피하라 (0) | 2021.09.22 |
---|---|
이펙티브 자바 - 유효기간이 지난 객체 참조 (0) | 2021.09.19 |
이펙티브 자바 - 싱글턴 패턴 (0) | 2021.09.18 |
이펙티브 자바 - Builder 패턴 (0) | 2021.09.16 |
이펙티브 자바 - 정적 팩토리 메소드 (0) | 2021.09.05 |
댓글