티스토리 뷰
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 |
- Total
- Today
- Yesterday
- spring cloud
- java8
- frontend개발환경
- JavaScript Core
- Git
- Spring
- OOP
- java
- JPA
- http
- mariadb
- backend개발환경
- EffectiveJava
- DesignPattern
- generics
- Jackson
- Kotlin
- TEST
- javascript
- programming
- 정규표현식
- toby
- Design Pattern
- go-core
- clean code
- servlet
- MySQL
- db
- frontcode
- code
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |