로또 미션 중 발생한 참조 문제

SeokHwan An·2024년 6월 27일
0

초록스터디

목록 보기
3/6

로또 미션을 수행하던 중 참조값을 끊지 않아서 약 1시간 정도를 해맸습니다.

먼저 로또는 1과 45 사이의 숫자 6개로 구성됩니다. 로또를 구현하는 방법에는 여러가지가 있겠지만 저는 1부터 45 숫자를 캐싱해 두고 로또를 생성 때마 shuffle을 한 뒤에 앞에 6개의 숫자를 추출하는 방식으로 구현했습니다.

문제사항

public class LottoGenerator {

    private static final List<Integer> NUMBERS = Arrays.asList(
            1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
            11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
            21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
            31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
            41, 42, 43, 44, 45);

    public Lottos generateRandomLotto(final LottoPurchaseMoney lottoPurchaseMoney) {
        final List<Lotto> lottos = new ArrayList<>();
        for (int i = 0; i < lottoPurchaseMoney.getPurchaseQuantity(); i++) {
            Collections.shuffle(NUMBERS);
            lottos.add(new Lotto(NUMBERS.subList(0, 6)));
        }
        return new Lottos(lottos);
    }
}

위의 generateRandomLotto와 같이 여러개의 로또를 생성할 때는 로또를 생성할 때마다 shuffle()을 진행했습니다. 그래서 생성된 여러 로또가 서로 다르게 생성되었을 것이라고 예상했지만 결과는 다르게 나타났습니다.

문제 분석

여러 로또가 모두 같은 결과값을 반환하는 것이었습니다. 이 결과를 보고 너무 당황을 해서 shuffle() 메소드가 올바르게 동작하지 않는 것인가라고 생각해 shuffle()의 내용을 보았는데

public static void shuffle(List<?> list) {
    Random rnd = r;
    if (rnd == null)
        r = rnd = new Random(); // harmless race.
    shuffle(list, rnd);
}

public static void shuffle(List<?> list, Random rnd) {
    int size = list.size();
    if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
        for (int i=size; i>1; i--)
            swap(list, i-1, rnd.nextInt(i)); // 이부분이 실행됩니다.
    } else {
        Object[] arr = list.toArray();

        // Shuffle array
        for (int i=size; i>1; i--)
            swap(arr, i-1, rnd.nextInt(i));

        // Dump array back into list
        // instead of using a raw type here, it's possible to capture
        // the wildcard but it will require a call to a supplementary
        // private method
        ListIterator it = list.listIterator();
        for (Object e : arr) {
            it.next();
            it.set(e);
        }
    }
}

위의 코드로 shuffle이 진행될 때 마다 랜덤되게 섞이는 것을 디버깅하면서 알 수 있었습니다.

두개의 로또를 생성했을 때 각각 NUMBERS.subList(0, 6) 를 보면 다음과 같습니다.

좌우가 서로 다르게 나온다는 것을 알 수 있었습니다. 최종 결과는 아래와 같이 마지막 shuffle()이 수행된 것으로 결과가 나타났습니다. (로또가 생성될 때 정렬이 이루어집니다.)

이를 통해서 shuffle()은 로또 생성 때 마다 잘 동작하는 것을 확인했습니다. 그러면 어떤 부분이 잘못되었는지 파악하던 중 아래의 코드가 눈에 들어왔습니다.

lottos.add(new Lotto(NUMBERS.subList(0, 6)));

저는 지금껏 subList가 방어적 복사를 통해 새로운 리스트를 생성하는 것이라고 생각했습니다.(방어적 복사에 대해서 더 알아보려면 https://velog.io/@seokhwan-an/방어적-복사 를 참고해주세요) 하지만 코드를 보니 그렇지 않다는 것을 알 수 있었습니다.

subListRangCheck() 이 부분은 부분 리스트의 인덱스가 주어진 리스트의 길이를 벗어나는지 판단하는 부분입니다. 다음 줄을 보면 sublist를 새로 생성할 때 this를 root로 넣는 것을 볼 수 있습니다. 즉, 로또를 생성할 때 모든 로또는 NUMBERS를 root로 가지게 되는 것입니다. 따라서 아래 그림과 같이 생성된 모든 로또가 NUMBERS에 의존하는 상황이었습니다.

그래서 shuffle이 이루어질 때마다 이미 생성된 Lotto들이 영향을 받는 것이었습니다.

해결방안

해결 방안은 간단하게 Lotto를 생성할 때 subList에 방어적복사를 적용하면 됩니다. 변경된 코드는 아래와 같습니다.

public Lottos generateRandomLotto(final LottoPurchaseMoney lottoPurchaseMoney) {
    final List<Lotto> lottos = new ArrayList<>();
    for (int i = 0; i < lottoPurchaseMoney.getPurchaseQuantity(); i++) {
        Collections.shuffle(NUMBERS);
        lottos.add(new Lotto(new ArrayList<>(NUMBERS.subList(0, 6)))); //변경된 부분
    }
    return new Lottos(lottos);
}

new ArrayList<>() 통해 방어적 복사를 적용했습니다. 이제는 여러 로또를 생성할 때 각각 다른 로또가 생성되는 것을 확인할 수 있습니다.

배운점

방어적 복사라는 개념을 주로 외부에 컬랙션 속성을 내보낼 때 주로 이용했었는데 이번 경우처럼 외부에서 컬랙션을 받을 때에도 방어적 복사가 필요하다는 것을 배울 수 있었습니다.

또한 라이브러리를 이용하기에 앞서서 파악하는 과정이 선행되어야 한다는 것을 이번 디버깅을 통해서 배웠습니다.

0개의 댓글