티스토리 뷰

Java5 에서부터 for-each 문이 추가됐다. 특별히 새로운 문법이 추가된게 아니라 기존 for문을 활용하는거라 for-each라고하면 못알아듣는 사람도 있고, 향상된 for문이라고 말하는사람도 있고.. 특히 요즘엔 stream API에 forEach() 메서드까지 추가되면서 의사소통에 약간 혼란스럼이 있긴하지만 보면 다들 알것이다.

 

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

for(Integer i : list){
    System.out.println(i);
}

 

for-each 문은 인덱스를 명시할 필요없이 알아서 리스트 사이즈만큼 반복되기때문에 에러여지도 없어지고 코드도 간결해지는 매우 유용한 문법이지만 한가지 치명적인 문제점이 있다. 그것은 인덱스를 사용할 수 없다는 것이다. 얼마전 현업에서 이 문제에 부딪힌적이 있었는데 아무생각없이 인덱스를 사용하기 위한 반복문을 돌렸다.

static <T> void method01(List<T> list){
    for(int i = 0; i < list.size(); i++){
        T t = list.get(i);
        // 인덱스를 사용하는 추가코드...
    }
}

이 코드는 코드리뷰에서 탈탈 털렸는데 리스트를 일반 for문으로 돌렸기때문이다. for-each에 비해 인덱스를 계산하는 코드가 좀 더 추가되는것 외에 어떤 문제가 있기에 일반 for문이 털린걸까?

 

작은 SI를 다니던시절부터 다른건 몰라도 컬렉션 구현체들만큼은 인터페이스를 이용해 참조했다, 습관처럼.

List<String> list1 = new ArrayList<>();
List<String> list2 = new LinkedList<>();
Set<String> set1 = new HashSet<>();
Set<String> set2 = new LinkedHashSet<>();
Map<String, Object> map1 = new HashMap<>();
Map<String, Object> map2 = new LinkedHashMap<>();

왜 얘네만큼은 인터페이스로 선언하는지 궁금한 맘에 물어봤었지만 당시엔 답을 얻지 못했었다. 뭐 이번주제는 이게아니니까 간략히 말하면 인터페이스를 사용해서 다른 구현체를 사용할때도 유연하게 교체하기위함인데 문제는 이것 때문이다. 다시한번 위 소스를 보면 메서드의 인자는 List<T> 타입으로 받고있다. List<T>의 구현체는 대표적으로 ArrayList<T>, LinkedList<T>가 있을텐데 이 구현체에 따라서 get() 메서드의 시간복잡도가 극명하게 달라질 수 있다는 것이다.

LinkedList의 경우 인자로 전달된 인덱스의 요소를 가져오기위해서는 항상 첫 노드에서부터 찾아 들어가야하기때문에 get()의 시간복잡도는 O(n)이다. for문과 결합되면 2중 반복문으로 돌아서 O(n^2)이 되는것이다. 

static <T> void method01(List<T> list){
    for(int i = 0; i < list.size(); i++){
        T t = list.get(i);
        // 인덱스를 사용하는 추가코드...
    }
}

LinkedList는 get()메서드 내부에 이미 반복문이 있기때문에 의도치않게 2중반복이 되는것이다. 이걸 이해하는건 어렵지않았는데 여기서 한가지 개념없는 말대꾸를 했다.

"List로 받고는 있지만 해당 메서드에 전달되는 구현체는 ArrayList이고, 그럼 O(n^2)이 아니라 O(n)인거 아니에요?"

"ArrayList를 확정짓고 코딩할거면 왜 List로 받아요?"

여기서 들려온 대답은 내 머리를 쿵 치는것 같았다. 왜 List로 선언하는지 머리로는 다 알고있었지만 이유를 알고있으면서도 습관처럼 List로 선언하고있었던거였다. 구현체는 언제든 바뀔 수 있고, 설사 내가 LinkedList를 쓰지않더라도 List를 반환하는 외부 라이브러리에서 LinkedList 인스턴스를 반환할 수도 있는 법이다. 다형성을 이용해 구현체에 상관없게끔 코드를 짜고있었다면 머리속에서도 구현체를 지워야하는 것이었다.

 

자, 그럼 List를 반복할땐 무조건 for-each문을 사용해야하는건 알았다. 그런데 그럼 인덱스는 어떻게해야하나?

static <T> void method01(List<T> list){
    int i = 0;
    for(T t : list){
        // 인덱스를 사용하는 추가코드...
        i++;
    }
}

뭐 특별한 해결법은 없다. 다만 개인적으로 인덱스가 필요한 경우는 메서드 분리나 구조변경을 통해 리팩토링이 가능한 경우가 많았다.

 

List를 반복할때는 꼭 for-each나 Iterator를 사용하고, 인덱스가 필요한 경우는 명시적으로 인덱스를 사용하지않을 수 있게 리팩토링할 수는 없는지 고민해보자.

'Java' 카테고리의 다른 글

토비의봄#01. Double Dispatch  (4) 2016.11.07
Java8#05. Optional  (0) 2016.10.31
Java8#04. 스트림(Stream)  (3) 2016.10.19
Java8#03. 메서드 레퍼런스(Method Reference)  (2) 2016.10.17
Java8#02. 함수형 인터페이스(Functional Interface)  (5) 2016.10.17
공유하기 링크
TAG
댓글
댓글쓰기 폼