본문 바로가기
Language/Java

이펙티브 자바 - 불필요한 객체 생성

by ocwokocw 2021. 9. 18.

- 이 글은 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 에 미리 할당해두어 재사용한다. 이런 객체 풀도 불필요한 객체를 생성하지 않는 원리를 이용한것이다.

댓글