티스토리 뷰

SMALL

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를 사용하고, 인덱스가 필요한 경우는 명시적으로 인덱스를 사용하지않을 수 있게 리팩토링할 수는 없는지 고민해보자.

LIST
공유하기 링크
TAG
댓글
  • 프로필사진 티라노 okky에서 토비님 강의 정리하신거 보다가 다른글 보는데 너무 정리 잘해주셨네요
    자주올께요~~ 좋은글 많이부탁드려요
    2016.12.20 11:21
  • 프로필사진 LichKing 안녕하세요~ 좋은 말씀감사합니다~! 2016.12.22 21:30 신고
  • 프로필사진 eastglow for문을 사용해서 list.get(i)를 해주면 index용 int i가 담긴 for문 안의 list.get()할 때 for문이 한번 더 돌게 되어 2번의 for문이 돌게 되고
    for-each문을 사용하면 for(T t : list) 에서 list를 그냥 가져다 쓰기만 하기 때문에 for문 한번만 돌게 되는군요. 인덱스를 사용할 수 없다는 것 때문에 for문 안에 list.get(i) 식으로 많이 썼는데 for-each로 쓰고 인덱스용 변수는 바깥에 선언하여 i++; 하는 식으로 써야겠군요-_- 좋은 지식 하나 배워갑니다.
    2018.08.31 18:01
  • 프로필사진 LichKing 정확히는 LinkedList가 그렇게 작동하고, ArrayList는 상관없습니다. ArrayList는 List 인터페이스를 구현하고있지만 RandomAccess 인터페이스도 마커인터페이스로 구현하고있는데요. 말그대로 랜덤 엑세스(인덱스 접근)를 해도 상관없다는 의미입니다. ArrayList는 내부에 배열로 데이터를 관리하기때문에 get() 으로 꺼내도 성능적으로는 별상관없습니다. 2018.09.01 17:01 신고
  • 프로필사진 김용환 무조건 foreach가 정답은 아니라고 생각합니다.
    ArrayList에서는 iterator next하는 비용보다 get(i)보다 더 컷던걸로.. 기억합니다.
    2019.01.30 18:32
  • 프로필사진 LichKing 본문에 그에 대한 내용이있는데요.
    ArrayList가 넘어오는게 확실해서 RandomAccess를 할거라면 List가 아니라 ArrayList로 받아서 하면 됩니다.
    List로 받을거면 ArrayList라는 가정을 하면 안되지요.
    2019.03.18 10:26 신고
  • 프로필사진 jazzDev index가 필요하다면 java.util.concurrent.atomic에 있는 AtomicInteger를 사용하면 좋습니다.
    thread-safe하기도 하고
    2019.03.17 15:40
  • 프로필사진 LichKing 굳이 int 놔두고 걔를 사용해야하는 이유가 있을까요? 2019.03.18 10:29 신고
  • 프로필사진 Ryan LinkedList는 foreach가 빠르지만 ArrayList는 전통적인 for문이 훨씬 더 성능이 좋다고 책에서 봤거든요.
    무조건 foreach가 옳다는게 좋은 생각인지는 모르겠습니다..
    글 잘 보고갑니다 ^^
    2020.10.06 17:27
  • 프로필사진 xeropise stream ForEach 를 쓰는 경우에는 외부 변수 값을 변경 할 수 없어

    final 로 사용하거나, 윗분 언급처럼 AtomicInteger 를 사용해야 하는 것으로 알고 있습니다.
    2021.05.14 12:57
댓글쓰기 폼