2017년 4월 18일 화요일

effective java 규칙 5. 불필요한 객체는 만들지 말라

String 클래스는 잘못된 이해로 인해 불필요한 객체를 만들 가능성이 크다.
많이 알고들 있듯이 String 은 참조 자료형이다. 즉 값의 주소를 가지고 있는 변수이다.
따라서 개발자들이 String 변수를 생성할 때
String abc = new String(“abc”) 와  String abc = “abc” 의 객체 생성의 부담이 같다고 생각할 수 있다.
(물론 느낌상 new String 으로 생성하는게 더 자원을 많이 소비할 것 같긴하다.)

하지만 두 생성 방식은 전혀 다르게 동작한다.
new 로 생성하면 다른 인스턴스를 만들때 처럼 heap 메모리에 객체가 생성된다.
= “abc”  처럼 큰따옴표 방식으로 객체를 생성할 때도 heap 메모리에 객체가 생성된다.
heap 에 저장되는 것 자체는 두 방식 모두 동일하지만 큰 따옴표 방식은 heap 메모리 내에서도
특별한 영역에 값이 저장된다. 그 영역은 String Constant Pool 이라는 곳이고 
이곳에는 기존에 만들어진 문자열 값이 저장되어 있다.
따라서 “abc” 라는 문자열이 큰따옴표 방식으로 생성된 적이 있으면 
이 값은 String Constant Pool 영역에 저장되고 또다시 같은 값을 “abc” 라는 형식으로 생성한다면
기존에 생성된  “abc” 와 같은 레퍼런스를 가지게된다.

그러나 new String(“abc”) 로 String 객체를 생성할 때마다 heap 영역의 어딘가에 “abc” 값이 매번 새롭게 저장되어
새로운 레퍼런스를 반환하게된다.

즉 String 객체를 생성할 때 큰 따옴표 방식으로 생성해야만 동일한 문자열을 다시 String 변수에 할당할때
추가적인 객체 생성작업이 일어나지 않는다. 


※ new String 으로 생성한 객체를 String Constant Pool 영역에 넣는 방법도 있다.
String newStr = new String(“abc”);
newStr.intern();

intern 메소드는 heap 메모리에 있는 String 값을 String constant pool 영역으로 복사시키고 stirng constant pool 에 복사된 주소를 반환한다.

public static void main(String[] args) {
String a = new String("a"); // heap 에 매번 새롭게 생성
String b = "a"; // string constant pool 에 생성
System.out.println(a==b); // false
System.out.println(a.intern() == b); // true
System.out.println(a == b); // false
String c = a.intern();
System.out.println( a == c); // false
System.out.println( b == c); // true

System.out.println("----------------");

}

결론은 쓸데 없이 new String 하지 말자.
(모두 다른 문자열 생성하는 것이면 new 로 생성하나 큰 따옴표로 생성하나 성능상의 차이점은 없지 않을까?)


생성자와 정적 팩터리 메서드를 함께 제공하는 변경 불가능 클래스의 경우 정적 팩터리 메서드를 이용하면 불필요한 객체 생성을 피할 수 있을 때가 많다.

예로 Boolean 클래스는 new Boolean(String) 보다는 Boolean.valueOf(String) 으로 생성하면 동일한 (클래스 로드시 미리 생성된 객체) 를 반환 받을수 있어 객체 생성의 부담을 피할수 있다.

그리고 어떠한 메소드 내부에서 동일한 객체를 항상 생성하는 로직이 있다면 정적 초기화 블록으로 생성 로직을 이동시켜 성능을 개선하는 것이 좋다.

public class PersonClient {

public static void main(String[] args) {
Person p = new Person();

// 이렇게 하면 Calendar 객체가 1000 번 생성된다
for(int i=0; i< 1000; i++)
p.someMethod();
}

}
class Person {

public void someMethod() {
// getInstance 라는 메소드명 때문에 singleton 으로 오해할 수 있으나 호출시마다 매번 새로운 객체를 생성해 반환한다.
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());

System.out.println(cal.get(Calendar.SECOND));
}
}

위 코드를 정적 초기화 블록으로 개선한다면

private static Calendar cal;

static {
cal = Calendar.getInstance();
}

/**
* 만들고 보니 date 객체도 계속 생성되긴 하지만
* cal 객체는 class 초기화시 한번만 생성된다.
* 다만 improveMethod 가 호출되지 않는다면 불필요한 cal 객체가 생긴샘이다.
* 이런 점을 보완하기 위해 improveMethod가 처음으로 호출될때만 객체를 생성시키는
* 초기화 지연 (lazy initialization) 기법을 사용할 수 있지만 구현이 복잡해지고 성능이 크게 향상되지도 않는다.
*/
public void improveMethod() {
cal.setTime(new Date());
System.out.println(cal.get(Calendar.SECOND));
}

위와 같이 구현 할 수 있다.



또다른 불필요한 객체가 생성되는 경우는
jdk 1.5 부터의 autoboxing (자동 객체화)로 인해 발생하기 쉽다.
기본자료형과 객체 표현형을 섞어 쓸때 둘 간의 변환이 자동으로 이뤄진다.
 
Long sum = 0L;
for(long i = 0; i < Integer.MAX_VALUE; i++)
    sum += i;

이와 같은 코드가 있을때 for 문의 += 연산이 일어날때마다 long 기본형인 i 변수는 Long 타입의 객체로 변환되어 Long 객체 sum에 저장된다. 즉 Integer.MAX_VALUE 만큼 객체가 생성되는 것이다.



참고로 객체풀을 만들어 객체를 재사용 하는 방법은 객체 생성 비용이 극단적으로 높지 않다면 사용하지 않는 것이 좋다.
DB 연결 같은 경우는 접속비용이 충분히 높으므로 풀을 만들어 재사용하는 것이 타당하나
일반적인 경우 코드가 복잡해지고 성능도 오히려 떨어질 수 있다.

즉 가벼운 객체라면 최신 jvm 에서는 고도화된 gc 가 있어 풀을 만들어 재사용하는 대신 새로 생성하는 편이 월등한 성능을 보여준다.

댓글 없음:

댓글 쓰기

Intelij 설정 및 plugin

1. preferences... (settings...) Appearance & Behavior > Appearance - Window Options        ✓   Show memory indicator Editor &...