[Java] 자바에서 Map 반복 시키는 방법들

    자바에서 Map 데이터를 loop를 돌리면서 가져오는 방법은 한가지만 있는 것이 아니다. 게다가 Stream이 지원이 되는 1.8부터는 더더욱 그 방법들이 늘어났는데 방법들을 정리해보고, 성능을 비교해보도록 한다.

     

    자바에서 map 반복 비교 정리

     

    고전적인 방법들

    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문을 통한 자유로움이 사라지지만, 위와 같이 코드를 매우 심플하게 만들 수 있다. 다만 이 방식은 장점보다 단점이 더 많은 문제가 있다.

     

    장점

    1. 코드라인이 줄어든다.
    2. 가독성이 올라간다.

    단점

    1. 가독성이 더 문제가 되는 경우도 많다.
    2. for문보다 속도가 느리다.
    3. 외부 변수들을 참고하기 힘들다
    4. 병렬 처리로 인하여 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

    댓글

    Designed by JB FACTORY