[Java] 자바에서 Map 반복 시키는 방법들
- 프로그램언어/자바(Java)
- 2021. 1. 2.
자바에서 Map 데이터를 loop를 돌리면서 가져오는 방법은 한가지만 있는 것이 아니다. 게다가 Stream이 지원이 되는 1.8부터는 더더욱 그 방법들이 늘어났는데 방법들을 정리해보고, 성능을 비교해보도록 한다.
고전적인 방법들
Iterator 방식
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String> ();
for(int i = 0; i < 1000000; i++) {
map.put("key" + i, "value" + i);
}
long startTime = System.currentTimeMillis();
iteratorloop(map);
long endTime = System.currentTimeMillis();
System.out.println("elapsed " + (endTime-startTime) + "(ms)");
}
public static void iteratorloop(Map<String, String> map) {
Iterator<String> keys = map.keySet().iterator();
int loopCnt = 0;
while(keys.hasNext()) {
loopCnt++;
String key = keys.next();
if(loopCnt <= 5)
System.out.println(key + "=>" + map.get(key));
}
System.out.println("loop cnt =>" + loopCnt);
}
Iterator 결과
key346006=>value346006
key649815=>value649815
key346007=>value346007
key649816=>value649816
key346008=>value346008
loop cnt =>1000000
elapsed 53(ms)
iterator로 키 값을 미리 받아놓고 while문 반복을 통하는 방식이다. 이 방식은 불필요한 작업이 많기 때문에 사실상 활용하기 어려운 방식이다.
entrySet 방식
public static void entrySetloop(Map<String, String> map) {
int loopCnt = 0;
for(Map.Entry<String, String> entry : map.entrySet()) {
loopCnt++;
if(loopCnt <= 5)
System.out.println(entry.getKey() + "=>" + entry.getValue());
}
System.out.println("loop cnt =>" + loopCnt);
}
entrySet 결과
key346006=>value346006
key649815=>value649815
key346007=>value346007
key649816=>value649816
key346008=>value346008
loop cnt =>1000000
elapsed 51(ms)
map의 key와 value set을 전달하여 처리를 하는 방식이다. for 부분이 지저분 해보이지만, getKey와 getValue라는 명확한 값을 전달하며 iterator에 비해서 key를 별도로 세팅을 할 필요가 없는 방식이라 가독성도 뛰어나다. 라인이 줄었기 때문에 더 빠르지 않을까 생각할 수 있지만 둘의 속도차이는 없다고 보면 된다.
KeySet 방식
public static void keySetloop(Map<String, String> map) {
int loopCnt = 0;
for(String key : map.keySet()) {
loopCnt++;
if(loopCnt <= 5)
System.out.println(key + "=>" + map.get(key));
}
System.out.println("loop cnt =>" + loopCnt);
}
keySet 결과
key346006=>value346006
key649815=>value649815
key346007=>value346007
key649816=>value649816
key346008=>value346008
loop cnt =>1000000
elapsed 46(ms)
고전적인 방식중에 필자가 가장 선호하는 방식으로 map에서 키를 뽑아서 for로 반복하는 방식이다. 3개의 방식 중 가장 심플하고 key값의 선언을 해당 map의 키 이름으로 할 수 있기 때문에 (ex: boardId와 같은 키) 유지보수하기에도 용이하다.
최근 방법들
forEach 방식 (람다방식)
map.forEach((key, value) -> System.out.println(key + "=>" + map.get(key)));
forEach를 사용할 경우 람다식을 지원하며 for문을 통한 자유로움이 사라지지만, 위와 같이 코드를 매우 심플하게 만들 수 있다. 다만 이 방식은 장점보다 단점이 더 많은 문제가 있다.
장점
- 코드라인이 줄어든다.
- 가독성이 올라간다.
단점
- 가독성이 더 문제가 되는 경우도 많다.
- for문보다 속도가 느리다.
- 외부 변수들을 참고하기 힘들다
- 병렬 처리로 인하여 CPU의 점유율이 올라갈 수 있다.
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String> ();
for(int i = 0; i < 5; i++) {
map.put("key" + i, "value" + i);
}
long startTime = System.currentTimeMillis();
forEachloop(map);
long endTime = System.currentTimeMillis();
System.out.println("elapsed " + (endTime-startTime) + "(ms)");
}
public static void forEachloop(Map<String, String> map) {
map.forEach((key, value) -> System.out.println(key + "=>" + map.get(key)));
}
key1=>value1
key2=>value2
key0=>value0
key3=>value3
key4=>value4
elapsed 82(ms)
위 소스 코드를 보면 알겠지만, 기존에 100만건의 루프를 도는 것을 5건으로 변환하였다. 단지 5개의 데이터를 loop 도는 것인데 위와 같이 82ms나 걸려버린것이다 이것이 얼마나 치명적이냐하면
keySet으로 하는 방식은
key1=>value1
key2=>value2
key0=>value0
key3=>value3
key4=>value4
loop cnt =>5
elapsed 1(ms)
5건만 처리할 경우 1ms밖에 걸리지 않았다는 것이다. 82(ms)가 얼마나 중요한지 모르는 사람들이 많겠지만, 이 차이는 빅데이터를 처리해야 하는 경우 혹은 대규모 서비스를 해야 할 때 엄청난 리스크를 안긴다. 그러니, Lambda 방식이나 Stream 방식등을 사용할 때에는 꼭 성능 테스트를 해보고 사용해야 한다.
결론적으로...
SI하던 사람들이 빅데이터 혹은 인공지능쪽으로 왔을 때 데이터를 처리하거나 대규모 Back-end API를 만들 경우 Stream과 람다방식을 너무 많이 써서 API 속도가 튀거나 기존보다 훨씬 느린 퍼포먼스를 보여주는 경우가 많다. 문제는 이들이 코딩을 할 때 저 부분에서 속도가 느릴거라 1도 생각을 하지 못한다는 것에 있다.
왠지 람다와 스트림을 마구 활용해야 더 있어보이고 코딩을 잘할거라 생각하겠지만, 그건 어디까지나 그쪽 수준(초급~중급)의 사람들끼리의 이야기이고, 분당 몇백만건을 처리해야 하는 빅데이터, 인공지능 관련 개발자라면 가독성보다는 코드 하나하나 최적화를 시켜야 된다는 것이다.
게다가 어쩔 땐 forEach를 쓰고 어쩔 땐 for문을 쓴다면 오히여 가독성에 더 큰 문제가 발생한다. Query를 짤 때 데이터가 없다면 Full Scan을 돌아도 문제가 발생하지 않는다. 그러다보니 초급~중급들로 이루어진 개발자들이 서비스를 런칭할 때 실 데이터로 런칭하지 못할 경우 몇달 후 갑자기 엄청나게 느려지는 경우가 종종 있다. 프로그램도 마찬가지이다. 쿼리를 최적화 하는 것처럼 코드 역시 최적화를 해야 한다.
마지막으로 lambda, stream에 관련된 내용은 하단 참고자료를 보면, 퍼포먼스에 대해서 잘 설명하고 있으니 참고하면 좋을 것 같다.
참고자료
https://jaxenter.com/java-performance-tutorial-how-fast-are-the-java-8-streams-118830.html
'프로그램언어 > 자바(Java)' 카테고리의 다른 글
[Java] 네이버(Naver) 검색 사용 및 JSON 파싱(Parsing)하기 (2) | 2021.02.26 |
---|---|
[Java] 네이버 검색 API 등록 및 호출하기 (0) | 2021.02.24 |
[Java] 랜덤함수 사용 및 속도 비교 (0) | 2020.12.08 |
포조(Plain Old Java Object, POJO) 이해하기 (0) | 2020.09.28 |
[Java] 소수점 반올림하는 3가지 방법 (0) | 2020.05.26 |