최근 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;
}
}