본문 바로가기
Java/Error

List 두개의 중복값 찾기, 그리고 ConcurrentModificationException

by seaweed_one 2022. 12. 15.
728x90

최근 TDD 스터디를 시작했습니다.

구현 과제는 로또 프로그램입니다.

해당 과제를 위해 로또 번호를 생성하고 당첨 번호를 입력하여 매칭되는 숫자의 개수를 찾아야하는 로직이 존재했는데요.

기능 구현 중 새로운 Exception을 만나 기록해봅니다.

바로 ConcurrentModificationException 입니다.

 

구현과 예외 발생 

기능 구현을 위하여 리스트 두개의 값을 비교, 중복되는 항목의 개수를 구해야 합니다.

for 문을 사용할 수도 있지만 이번 과제에는 indent 가 1을 초과해서는 안된다는 조건이 존재합니다.

for 문 내에서 if 문 사용 시 indent == 2 로 조건을 어기게 되는 것이죠.

 

해서 제가 사용한 방법은  ArrayList.retainAll() 메소드 입니다.

retainAll() 메소드는 두 리스트의 중복 값 ( 즉 교집합 ) 만 남기고 나머지 값들을 제거합니다.

해당 메소드를 사용해 구현하면 포문을 사용하지 않고 교집합 값을 찾아낼 수 있습니다.

 

초기 구현 함수는 아래와 같습니다. ( 전체 코드는 하단에 첨부하겠습니다. )

저는 먼저 로또생성을 위해 Lotto 라는 객체를 만들었습니다.

해당 객체에서 getNumbers() 메소드를 통해  List<Integer> 타입의 로또 번호를 가져와 변수에 담은 후 중복요소만 남기고 제거합니다.

리스트에 남은 중복값의 개수를 가지고 와 등수를 판별하는 로직입니다.

  private int compareValue(Lotto lotto) {
    List<Integer> lottoNumber = lotto.getNumbers();
    lottoNumber.retainAll(winningNumbers);
    return lottoNumber.size();
  }

위와 같은 로직 구현 시 발생한 예외가 바로 ConcurrentModificationException 입니다.

ConcurrentModificationException는 Collection 객체에 remove 메소드 호출 시 발생하는 예외로 객체 내부의 요소가 실시간으로 변하기 때문에 발생하는 이벤트인데요. List , Map 등 객체를 돌며 요소를 삭제 / 변경 시 발생됩니다.

저의 경우에는 (확실치는 않지만) getNumbers() 함수로 가지고온 값을 새로운 리스트에 넣었지만 각각의 변수가 서로 다른 곳을 바라보기 때문에 발생한 것 같습니다.

자바의 주소값과 실제값에 대해서는  다른 포스팅에서 자세히 알아보도록 하겠습니다.

 

해결

해당 문제 해결을 위해 사용한 것이 바로 CopyOnWriteArrayList 객체입니다.

 

CopyOnWriteArrayList 는 ArrayList를 구현한 클래스입니다.

두 객채의 차이점은 바로 CopyOnWriteArrayList 는 ThreadSafe 하다는 것입니다.

ArrayList 의 경우 스레드에 안전하게 설계되지 않았기 때문에 synchroized를 적절하게 사용해야 합니다.

 

CopyOnWriteArrayList는 내부를 변경하는 작업은 사본을 만들어 수행하고 객체 전달 시 해당 상태를 스냅샷으로 가지고 있습니다.

따라서 ArrayList 와 synchroized 를 함께 사용하는 것보다 속도가 매우 빠릅니다.

또한 인덱스 정보를 저장해두고 복사하는 방식이기 때문에 remove와 같은 동작에서도 안전합니다. 

데이터 수정이나 삭제 등의 용도로 쓰일 경우에는 속도가 느려질 수 있기 때문에 적절한 사용이 필요합니다.

 

버그를 수정하여 최종적으로 구현한 클래스는 다음과 같습니다.

ublic class ScoreManager {
  List<Integer> winningNumbers;
  List<Lotto> lottos;

  public ScoreManager(List<Lotto> lottos, List<Integer> winningNumbers) {
    this.winningNumbers = winningNumbers;
    this.lottos = lottos;
  }

  public ScoreInformation getScore() {
    ScoreInformation scoreInformation = new ScoreInformation();
    for (Lotto lotto : lottos) {
      int matchingCnt = compareValue(lotto);
      scoreInformation.addScore(getMatchingScore(matchingCnt));
    }
    return scoreInformation;
  }

  private int compareValue(Lotto lotto) {
    List<Integer> lottoNumber = new CopyOnWriteArrayList(lotto.getNumbers());
    lottoNumber.retainAll(winningNumbers);
    return lottoNumber.size();
  }

  private MatchingScore getMatchingScore(int matchingNum) {
    if (matchingNum == 3) {
      return MatchingScore.MATCH_3;
    }
    if (matchingNum == 4) {
      return MatchingScore.MATCH_4;
    }
    if (matchingNum == 5) {
      return MatchingScore.MATCH_5;
    }
    if (matchingNum == 6) {
      return MatchingScore.MATCH_6;
    }
    return MatchingScore.NONE;
  }
}

 

 

728x90