[프로그래머스] 주차 요금 계산 : ConcurrentModificationException?

김건우·2024년 8월 14일

트러블슈팅

목록 보기
5/5

요즘 코드 문제 풀이에 대해서 블로그를 작성하는게 불필요하다고 느껴서 딱히 문제 해결 이후에 블로그는 작성 안하고 있었다.

근데, 처음보는 오류가 떠서 이유와 해결방법을 기록하려 한다.


[프로그래머스] 주차 요금 계산 : 92341
해당 문제로 자세한 문제 설명은 생략한다.

import java.util.*;

class Solution {


public static Map<Integer, String> map = new HashMap<>();
public static Map<Integer, Integer> sum = new HashMap<>();

public int[] solution(int[] fees, String[] records) {        
    for (String record : records) {
        StringTokenizer st = new StringTokenizer(record, " ");
        String time = st.nextToken();
        int carNumber = Integer.parseInt(st.nextToken());
        String status = st.nextToken();
        
        if (map.containsKey(carNumber)) { // 입차 내역이 존재한다면 - 출차 처리
            String inTime = map.get(carNumber);
            timeProcessing(inTime, time, carNumber);
        } else { // 입차 내역이 없다면 - 입차 처리
            map.put(carNumber, time);
        }
    }
    
    for (int number : map.keySet()) { // 출차된 내역이 없는 경우
        if (map.containsKey(number)) {
            String inTime = map.get(number);
            timeProcessing(inTime, "23:59", number);
        }
    }
    
    Map<Integer, Integer> result = new TreeMap<>();
    for (int number : sum.keySet()) { // 최종 요금 계산
        int time = sum.get(number);
        int fee = time > fees[0] ? fees[1] + (int)Math.ceil((float)(time - fees[0]) / fees[2]) * fees[3] : fees[1];
        result.put(number, fee);
    }
    
    return result.values().stream().mapToInt(i->i).toArray();
}

// 입차와 출차의 누적 주차 시간을 sum Map에 저장하고, 기존 Map에서 제거하는 메서드
public void timeProcessing(String inTime, String time, int carNumber) {
    String[] inTimes = inTime.split(":");
    String[] times = time.split(":");

    int hour = Integer.parseInt(times[0]) - Integer.parseInt(inTimes[0]);
    int min = Integer.parseInt(times[1]) - Integer.parseInt(inTimes[1]);
    int parkTime = (60 * hour) + min;
    
    sum.put(carNumber, sum.getOrDefault(carNumber, 0) + parkTime);
    map.remove(carNumber);
}

처음 작성 코드는 다음과 같다.

문제 자체가 어려운 계산을 원하는게 아니라 살짝 구현쪽에 가깝다고 느꼈다.
그렇기에 여러 블로그를 찾아봤었는데, 해결 방법이 각각 달라서 내가 닥친 문제가 뭔지 몰랐다.

이처럼 제공된 3개의 테스트 케이스에 대해서는 문제없이 통과했기에 당연히 문제없이 통과될 줄 알았지만

이처럼 1, 3~12 케이스에 대해서 런타임 에러로 실패한 것을 확인했다.

아무리봐도 뭔 문제인지 모르겠어서 질문 탭을 뒤져보다가 한 글을 발견했다.

이 테스트케이스를 통해 돌려보다가 다음과 같은 문제를 확인했다.

제목에서 설명한 ConcurrentModificationException 오류인데, Concurrent 라고 하니 동시성 처리에 문제가 있는건가? 이런 간단한 문제를 돌려보는 것에서도 문제가 발생할 수 있나? 라고 생각했었다.

    for (int number : map.keySet()) { // 출차된 내역이 없는 경우
        if (map.containsKey(number)) {
            String inTime = map.get(number);
            timeProcessing(inTime, "23:59", number);
        }
    }
    public void timeProcessing(String inTime, String time, int carNumber) {
        String[] inTimes = inTime.split(":");
        String[] times = time.split(":");

        int hour = Integer.parseInt(times[0]) - Integer.parseInt(inTimes[0]);
        int min = Integer.parseInt(times[1]) - Integer.parseInt(inTimes[1]);
        int parkTime = (60 * hour) + min;
        
        sum.put(carNumber, sum.getOrDefault(carNumber, 0) + parkTime);
        map.remove(carNumber);
    }

알고보니 이부분.. 다음 flow를 보면, map.keySet() 을 통해 map 안에 모든 key들을 가지고와서 루프를 돈다. 남아있다면 입차는 되었지만 출차가 되지않은 경우로 출차 시간을 23:59로 설정해서 시간계산을 하는 로직인데, timeProcessing 메서드를 확인해보면 시간 처리가 끝난 key에 대해서는 삭제 처리를 해주고 있다.

이 부분이 문제였던 것이다.

ConcurrentModificationException은 Java의 컬렉션을 순회하는 중에 해당 컬렉션을 변경하려고 할 때 발생하는 오류, HashMap을 순회하고 있는 동안에 map.remove(carNumber)가 호출되어 map이 수정되었기 때문에 이 오류가 발생한 것이다.

이제보니 주어진 테스트케이스 안에서는 입차 처리 후, 출차 처리가 되지 않은 경우가 2개 이상인 경우가 존재하지 않았다. 그렇기에 더 찾기 힘든 오류가 아니였을까 싶다..

중요한 점은 for-each 문은 Iterator 를 사용해서 컬레션을 순회하는데, 이때 Iterator 는 Read-Only 이기 때문에 순회 중 요소를 변경하는 작업이 일어나면 ConcurrentModificationException를 발생시켜 컬렉션을 안전하게 보호하는 것이다.

해결 방안은 간단하다.

    for (int number : new ArrayList<>(map.keySet())) { // 출차된 내역이 없는 경우
        if (map.containsKey(number)) {
            String inTime = map.get(number);             
            timeProcessing(inTime, "23:59", number);
        }
     }

이처럼 실제 map 을 사용하는게 아니라 복사본을 사용하면 된다.
Iterator 를 직접 사용하거나, for 문을 사용해도 되지만 이 방법이 가장 간단한 해결방법 인 것 같다.

기존에 컬렉션을 사용하면서 만나보지 못했던 오류였는데, 상당히 많은 시간을 잡아먹었 던 것 같다.
그래도 이제는 한 번 만난 오류이기 때문에 다음번엔 두렵지 않을 것이다!

해당 코드를 수정하니 쉽게 통과했다~~

profile
공부 정리용

0개의 댓글