본문 바로가기

Language & Framework/Java

이펙티브 자바 (5) 불필요한 객체 생성을 피하라 (feat. StringPool, Autoboxing)

 

 

 

오늘은 이펙티브 자바의 "Item 6 : 불필요한 객체 생성을 피하라" 파트를 다루겠다.

자바 기초를 공부하며 어느 정도는 상식선에서 알고 있을만한 내용들이다.

 

우선 첫번째 예시는 저자의 표현상 아주 극단적인 사례인 new String()이다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class EffectiveString {
    public static void main(String[] args) {
        /*
       * string은 상수풀에 저장된다
        * string2는 무조건 강제적으로 새로운 문자열을 생성한다
        * string3은 string이 저장한 문자열을 재사용한다
        * */
 
        String string = "String";
        String string2 = new String(("String"));
        String string3 = "String";
 
        System.out.println(string==string2); // false
        System.out.println(string.equals(string2)); // true
        System.out.println(string==string3); // true
        System.out.println(string.equals(string3)); // true
        // => but, 항상 같은 인스턴스일 것을 확신할 수 없기 때문에 문자열 비교는 반드시 equals를 사용할 것을 권장
    }
cs

 

 

다들 지겹게 들었던 말이겠지만, 모든 리터럴은 Heap Area의 문자열 풀(String pool)에 저장되며 이후 해당 리터럴을 재사용할 경우 기존에 생성된 문자열 풀에서 참조를 가져온다.

String str = "str"로 리터럴을 저장하는 정확한 과정은 디버깅으로도 볼 수가 없기 때문에 작동 방식이 거의 유사하다고 느껴지는 Intern() 메서드의 주석이라도 가져왔다.

(실제 코드에서 리터럴은 컴파일 시 String Intering을 통해 문자열 풀에 등록하고 재사용한다, 그리고 intern 메서드를 사용하면 이 때 등록된 문자열을 재사용하거나 새롭게 등록한다.)

 

 

어노테이션이 많은데 저걸 뭐라고 읽어야하는지 잘 모르겠어서 읽기 힘들었다..

요약하자면 intern() 메서드는 문자열 풀에서 equals()를 사용하여 같은 문자열이 저장되어 있는지 확인한 뒤, 만약 문자열 풀에 해당 문자열이 존재한다면 참조를 바로 가져오고, 존재하지 않는다면 문자열을 문자열 풀에 저장한 뒤 그에 대한 참조를 가져온다고 한다.

또한 이렇게 가져온 같은 같은 문자열의 == 비교는 true를 반환하며, 당연히 equlas 또한 true를 반환한다.

 

그러나 new String("str")을 사용해서 문자열을 생성하는 경우 디버깅으로 추적 가능하며, 아래와 같이 생성된다.

 

 

 

설명을 보면 인수로 제공된 문자열과 동일한 시퀀스를 나타내도록 새롭게 String 객체를 만든다고 한다.

또한, String은 불변이기 때문에 특별히 오리지널에 대한 명시적인 복사가 필요한 것이 아니라면 굳이 해당 생성자를 이용해서 문자열을 생성할 필요가 없다고 적혀있다.

 

 

 

그래서 결과적으로 이런 형태로 저장된다.

쓸데없이 너무 길어진 감이 있는데, 아무튼간에 어떤 특별한 의도를 가진 것이 아니고서야 new String()을 사용할 필요는 없다는 것이다.

 

* String은 Java6까지는 PermGen영역에 저장되었었으나, 현재는 Heap 메모리에 저장되어 문자열 풀에도 GC가 동작할 수 있다.

** 또한 Java8부터 PermGen 영역은 Metaspace로 대체되었다.

 

 

 

 

 

다음 예시는 AutoBoxing과 Unboxing에 대한 부분이다. 

Wrapper type과 Primitive type을 불필요하게 섞어서 사용할 경우 성능 저하의 원인이 될 수 있다.

아래의 사례를 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class EffectiveAutoboxing {
    private static long sum() {
//        Long sum = 0L;
        long sum = 0L;
        /*
        * 내 실행 환경에서 6배 차이난다.
        * */
        for (long i=0; i<= Integer.MAX_VALUE; i++) {
            sum += i;
        }
        return sum;
    }
 
    public static void main(String[] args) {
        long start = System.nanoTime();
        long x = sum();
        long end = System.nanoTime();
        System.out.println((end-start) / 1000000. + " ms.");
        System.out.println(x);
    }
}
cs

 

어디가 문제인지 바로 보일 것이다.

위 코드를 내 실행 환경에서 테스트 시 6배의 속도 차이가 났다.

 

자바 공식 문서에서도 오토 박싱, 언박싱의 성능에 대한 언급을 찾아볼 수 있다.

 

https://docs.oracle.com/javase/8/docs/technotes/guides/language/autoboxing.html

 

Autoboxing

Autoboxing As any Java programmer knows, you can't put an int (or other primitive value) into a collection. Collections can only hold object references, so you have to box primitive values into the appropriate wrapper class (which is Integer in the case of

docs.oracle.com

 

So when should you use autoboxing and unboxing? Use them only when there is an "impedance mismatch" between reference types and primitives, for example, when you have to put numerical values into a collection. It is not appropriate to use autoboxing and unboxing for scientific computing, or other performance-sensitive numerical code. An Integer is not a substitute for an int
;autoboxing and unboxing blur the distinction between primitive types and reference types, but they do not eliminate it.

--

언제 오토박싱과 언박싱을 사용해야하는가? 그들 (참조 유형과 기본형 간에) "Impedance 불일치"가 있는 경우에만 사용해라.
예를 들어, 네가 숫자 값들을 컬렉션에 넣을 때말이다. (Collection에 값을 넣는 것 같은 어쩔 수 없는 경우를 말하는 듯하다)
오토 박싱과 언박싱을 사용하는 것은 적합하지 않다 과학적 컴퓨팅, 혹은 성능에 민감한 계산 코드에 사용하는 것은.
Integer는 int를 대체하지 않는다.
언박싱과 오토박싱이 둘의 차이를 흐리게 보이게는 하나, 그들의 차이가 제거되지는 않는다.

 

 

솔직히 나도 지금까지 그냥 사용하기 편하다는 이유로 Integer, Long을 도배해왔는데, 기존 코드를 열어보고 정말 쓸데없이 wrapper class를 사용한 곳은 없는지 확인해봐야할 것 같다..

 

아무튼 오토박싱과 언박싱을 불필요하게, 특히나 위에서처럼 섞어쓰는 것은 정말 최악의 예시이니 무조건 피해야 한다.

 

*절대 WrapperClass가 나쁘다는 취지에서 저자가 이를 다룬 것이 아니다. 적절하게 사용하라는 것이 맹점이다. 이 글을 읽고 갑자기 프로젝트의 모든 타입을 Primitive로 바꾼다던가 하는 이상한 행동을 하지 말자. 특히나 우리가 리얼 월드에서 사용하는 자료구조가 거의 항상 Collection이기 때문에 일반적인 경우에서는 WrapperClass를 사용하는 것이 정상적인 일이다.*

 

 

 

마지막 예시인 정규식이다.

정규식으로 생성되는 Pattern 클래스의 경우 생성 비용이 비싼데, GC의  "더이상 해당 객체에 대한 참조가 없으면 정리한다"의 원칙에 따라 매번 삭제되고 매번 새롭게 생성된다.

 

따라서 해당 정규식이 자주 사용될 여지가 있다면 static으로 생성해서 재사용하는 것이 좋다고 한다.

예시는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class EffectiveRegex {
    static boolean isValid(String email) {
        return email.matches("^[_a-z0-9-]+(.[_a-z0-9-]+)*@(?:\\w+\\.)+\\w+$");
    }
 
    private static final Pattern EMAIL_PATTERN = Pattern.compile(
            "^[_a-z0-9-]+(.[_a-z0-9-]+)*@(?:\\w+\\.)+\\w+$"
    );
 
    static boolean isValidFastVer(String email) {
        return EMAIL_PATTERN.matcher(email).matches();
    }
 
    public static void main(String[] args) {
        /*
        *
        * */
 
        boolean result = false;
        long start = System.nanoTime();
        String email = "asdasd@asdasd.com";
        for (int i=0; i<100; i++) {
            result = EffectiveRegex.isValid(email);
//            result = EffectiveRegex.isValidFastVer(email);
            /*
            * 1.2배 ~ 1.5배가량 차이남.
            * */
        }
        long end = System.nanoTime();
        System.out.println(end-start);
        System.out.println(result);
    }
}
 
cs

 

음.. 크게 설명할 부분은 없지만 저자가 언급하길 필요할 때 사용하도록 지연 초기화(Lazy Initialization)하는 방법도 있으나 대부분의 경우 코드만 더 복잡하게 할 뿐 의미가 없다고 한다.

 

지난 글 (https://7357.tistory.com/263)에서 이와 관련된 내용을 이미 다뤘었지만 추가적으로 조금만 덧붙이자면, 지연 초기화 시 해당 객체의 멤버의 값을 제외한 클래스만 로드되며, 초기화 시점(호출 시점)에 멤버까지 모두 로딩된다.

 

따라서 객체 자체가 엄청나게 무겁고 생성 비용이 많이 드는 경우가 아니면 지연 초기화가 의미가 없다는 취지에서 나온 말인 것 같다.(검증 필요)

 

 

아무튼 오늘도 여기서 끝이다. 이펙티브 자바.. 재미있긴 하지만 이러다가 1년동안 이것만 보게 생겼다..