본문 바로가기

Better SW Development

[dW Review] 자바 컬렉션 중 enhanced for Loop와 성능에 대한 이야기

IBM developerWorks에는 개발자에게 도움이 되는 양질의 자료가 종종 올라오곤 합니다. 예전엔 영문사이트에 영문자료 뿐이었는데, 이제는 한글사이트에 한글로 번역된 자료들도 많아졌습니다. 다 읽지 못할 정도로 말입니다. 최근에 본 자료중에서 Java Collections API에 대해 모르고 있던 5가지 사항이라는 기사가 있었습니다. 자바 개발자라면 한번쯤 봐둘만한 내용이고, 초급개발자라면 빨리 보면 볼 수록 좋은 내용이라 생각합니다.

오늘은 그 내용 중에서  Enhanced for Loop(=for-each)구문과 성능에 대해서 좀 더 이야기를 해보려고 합니다.

:: About 컬렉션 ::

우선 아래와 같은 컬렉션이 있다고 가정하겠습니다.
    List<User> users = new ArrayList<User>();
    users.add( new User("cobb") );
    users.add( new User("Arthur") );
    users.add( new User("Eames") );
[User 엔티티를 원소로 갖는 List]

아래는 코드는 컬렉션을 객체지향적으로 꺼내는 방식에 대한 코드입니다.
    Iterator<Integer> iterator = null;

    for( iterator = users.iterator(); iterator.hasNext(); ) {
        User user = (User)iterator.next();
        System.out.println( user.getName() );
    }
[#1. Iterator 사용]

iterator 라는 표준 산출방식을 이용해서 객체 원소를 뽑아내는 방식입니다. 객체지향 패턴이 적용된 방식이고, 권장하는 방식인건 알지만 제 주변에서는 iterator를 사용하는 사람을 잘 보지 못했습니다. 오히려 아래와 같이 많이 쓰죠.

    for(int i = 0; i < users.size(); i++ ) {
        User user = (User)users.get(i);
        System.out.println( user.getName() );
    }
[#2. Hand-Writing index 사용]

만약 위 코드를 Java 5에서 부터 적용된 enhanced for loop문을 사용하면 다음과 같이 쓸 수 있습니다. 기본적으로 Iterable 인터페이스를 구현한 객체에는 다 적용가능한 코드입니다.

    for( User user : users  ) {
        System.out.println( user.getName() );
    }
[#3. enhanced for loop 사용]

간결함으로는 세번째 향상된 for loop 문이 가장 좋아보입니다. 그리고 디자인 패턴이나 리팩터링 강의때도 대부분의 경우 세번째를 권장합니다. 작성되는 코드가 간결할 수록 좀 더 복잡한 시스템을 좀 더 쉽게 구축가능하기 때문입니다.

그런데, 성능측면으로는 어떨까요? 셋 중 어느 것이 제일 나을까요?

사실 코드 조각을 놓고 성능을 논하는 건 그다지 좋은 접근법은 아닙니다. 일반적으로 성급한 성능 최적화 보다는 간결하고 의미적으로 명확한 코드를 작성하는 걸 (매우)권장합니다. 하지만 C시절부터 이어진, '아낄수 있을때, 최대한 아끼고 보자' 사상으로 볼때는 세 코드는 성능에 대한 차이가 있습니다. 위 코드에서 사용자의 이름(user.getName() )을 출력하는데에 있어 가장 비용이 적게 드는 건 인덱스를 직접 지정하는 #2 방식 입니다. #1이나 #3은 대동소이합니다. 왜냐하면 enhanced for loop는 알고보면 #1 방식을 랩핑한 것이거든요. 즉, iterator라는 객체를 새로 만들어 내기 때문입니다.

객체 생성은 전통적으로 다른 작업에 비해 비용이 많이 드는 작업입니다. (VM이 계속 발전하고 있다곤 하지만 말입니다). 특히  리소스가 한정된 상황, 즉 요즘 유행인 안드로이드같은 환경에서는 좀 더 민감해집니다. 단순히 CPU나 메모리만의 문제가 아니라, 그와 연관된 '배터리'의 문제까지 이어지기 때문입니다. 따라서, '최대한 아껴야 산다'는 표어가 다시 벽면에 표어로 붙게 되는거죠.



따라서 퍼포먼스를 따진다면 #2에서 조금 바뀐 형식으로 아래와 같이 사용할 것을 권장합니다.

    int size = users.size();
    for(int i = 0; i < size; i++ ) {
        User user = (User)users.get(i);
        System.out.println( user.getName() );
    }
[#4. 컬렉션 추출시 성능을 최대한 짜내기]

size() 메소드 호출하는것 조차 반복되지 않도록 아끼는 거죠. 안드로이드같은 한정된 디바이스에서라면 위와 같이 사용해야 합니다. 안드로이드 VM(=Dalvik VM) 기준으로 약 3배정도 빠르다고 합니다. (와우!)

하지만 앞에서도 말했듯이 일반적인 OO프로그래밍에서 좋은 습관이라고 부르긴 어렵습니다. 오로지 성능에 올인한 형태이기 때문입니다. 그래서 가독성이 좋아짐에도 컬렉션 추출에 enhanced for loop 문을 비추하는 상황이 벌어지는 아쉬운 상황이 발생합니다. enhanced for loop는 컬렉션 뿐 아니라 배열(array)에도 쓰이는데, 이 경우에는 또 어떨까요?



:: About 배열 ::

컬렉션과 비슷하게 흔히 사용되는 객체 리스트에는 배열(array)이 있습니다. 아래 코드는 안드로이드 개발자 사이트에서 소개된 코드입니다. 메소드가 zero(), one(), two() 세 개 있습니다.

    static class Foo {
        int mSplat;
    }
    Foo[] mArray = ...

    public void zero() {
        int sum = 0;
        for (int i = 0; i < mArray.length; ++i) {
            sum += mArray[i].mSplat;
        }
    }

    public void one() {
        int sum = 0;
        Foo[] localArray = mArray;
        int len = localArray.length;

        for (int i = 0; i < len; ++i) {
            sum += localArray[i].mSplat;
        }
    }

    public void two() {
        int sum = 0;
        for (Foo a : mArray) {
            sum += a.mSplat;
        }
    }

위 세 개의 메소드 중에서는 어떤 것이 제일 성능상 빠를까요?

조금 앞에서 이야기 한 바에 따르면 첫 번째에 해당하는 zero()나 두 번째에 해당하는 one()이어야 할 것 같습니다. 그런데 답은 for-each를 사용한 two()입니다. (으응?)
zero의 경우 매 loop마다 length를 사용하는 비용을 치뤄야 합니다. 언듯 생각하면 컴파일러가 inline으로 값을 치환해서 zero()나 one()이 같아야 할 것 같지만, JIT(Just In-Time)컴파일러의 경우에도 array.length는 아직 최적화가 이루어지지 않았다고 합니다.  반면에 for-each문은 최적화가 이루어진다네요. 따라서 one() 처럼 쓰면 좀 더 빨라지고, two()처럼 쓰면 가장 빨라진다고 합니다. 특히 zero()와 two()는 차이가 많이 난다고 합니다.


자, 이제 Enhanced for Loop문에 대한 결론을 내려봅시다.
1. 쓸수 있다면 최대한 쓴다.
2. 성능이 크리티컬한 영역에서 컬렉션을 대상으로 할때는 Hand writing index를 이용하는 걸 고려해 본다.
3. 파워가 충분히 공급되는 서버사이드 개발일 경우에는 고민할 필요도 없다. 쓰자.

네? 성능요? 이래서 미리미리 성능을 생각해서 코딩해야 한다는 오래된 규칙이 맞는거 아니냐고요?

:)

조슈아 블로쉐의 이펙티브 자바(Effective Java)에서 소개된 '신중한 최적화(Optimize Judiciously)'항목에는 다음과 같은 문구가 있습니다.

우리는 최적화 작업에 대해 다음과 같은 규칙을 따른다.
 * 규칙 1. 하지 않는다.
 * 규칙 2(전문가에 한해). 하지 않는다. 완벽하리만큼 깔끔하고 최적화가 필요없는 해법을 찾기 전까지.
- M.A. 잭슨

작은 효율증진에 대해서는 잊어라. 97%에 해당하는 시간에 대해 이야기 하자. 미숙한 최적화는 모든 악의 근원이다.
- 도널드 E. 크누스.

(맹목적인 어리석음을 포함한) 어떤 다른 이유보다도 '효율성'을 명목으로 이루어지는 컴퓨팅 죄악이 더 많다. 그럴필요가 없음에도 말이다.
- 윌리엄 A.울프


위 문구는 안드로이드 개발자 사이트, 성능을 고려한 설계(Designing for Performance)에도 기재되어 있답니다.
우선은 프로그램을 좋은 구조로 잘 만드세요. 그 다음에 성능을 고려하시는게 더 좋을 것 같습니다.

왜냐하면, 그 반대 순서로 작업하는 건 훨씬 어렵기 때문입니다. :)


[참조]
Java Collections API에 대해 모르고 있던 5가지 사항
http://www.ibm.com/developerworks/kr/library/j-5things2.html

Designing for Performance
http://developer.android.com/guide/practices/design/performance.html