본문 바로가기
교내|외 활동/멋사 13기

멋사 13기 지원 | 2단계 과제_lotto

by 0/0 2025. 2. 6.

목차

1. lotto 과제 코드(GitHub)

2. 구현

3. Issues

 

1. lotto 과제 코드(GitHub)

 

https://github.com/lye5615/13th-backend-lotto

 

2. 구현

 

관련 있는 클래스들을 패키지 별로 나눠놓았다.

 

lotto: 사용자가 입력한 번호(당첨 번호, 보너스 번호)검증, 로또 발행 / 등수 열거형 / 입력받은 번호와 로또 간의 비교 결과

service: 입력받은 번호와 로또 간의 비교 수행 / 복권 구매와 발행 / 등수 결과 / 수익률 계산

ui: 프로그램 실행(구입 금액 입력, 당첨 및 보너스 번호 입력) / 결과 출력

 

이번 과제에서 아래와 같은 요구 사항이 있었다.

  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
    • 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
  • 3항 연산자를 쓰지 않는다.
  • 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
  • JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
  • 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.
  • else 예약어를 쓰지 않는다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
    • else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
  • Java Enum을 적용한다.
  • 도메인 로직에 단위 테스트를 구현해야 한다. 단, ui(System.out, System.in, Scanner) 로직은 제외한다.
    • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 분리해 구현한다.
    • 단위 테스트 작성이 익숙하지 않다면 test/java/lotto/LottoTest를 참고하여 학습한 후 테스트를 구현한다.

- 들여쓰기 + else 예약어 쓰지 않기: 기존에 if-else 문을 자주 활용하는 습관이 있었어서 의식적으로 코딩해야했다.

 

- 메서드가 한 가지 일만 하도록 작게 만들기 + 메서드 길이 15라인 넘어가지 않도록 구현: 읽고 있는 책 [객체지향의 사실과 오해]에도 나온 내용인데, 객체의 책임과 역할을 명확히 하는 게 중요하다고 한다. 또 요구인 메시지를 전달해서 이를 처리하는 게 메소드라고 하는데, 메소드가 여러 역할을 하고 있으면 책임과 역할이 모호해지거나 너무 많아질 것 같다. 이 요구사항도 이 일환인 것 같은데, 이를 인지하고 의식적으로 코딩하려고 노력했다.

 

- Enum 적용: Enum을 배울 때 '이런 건 왜 있는 거지?' 생각했던 클래스인데, 이번에 등수와 관련한 상수가 많았기 때문에 Enum에 내용을 작성함으로써 유용하게 활용할 수 있었다.

 

-  핵심 로직과 UI 담당 로직을 분리: 이 요구 사항을 보고 관련 클래스들끼리 패키지를 나눠서 클래스를 관리해야겠다고 생각했다.

 

- 단위 테스트를 직접 작성하는 것은 처음이라서 따로 공부를 해야 했다. 공부라고 했지만 chatGPT로 알아본 것이 다라서 다음에 따로 내용을 찾아보고 추가해야 겠다.

 

 

3. Issues

#Issue_1 : 여러 개의 복권을 발행

 

복권 한 장: 1~45 범위의 정수 6개로 이루어진 List 객체

복권 여러 장 발행: List

 

*처음에는 HashMap으로 구현했다. (key(Integer): 복권의 번호, value(ArrayList): 복권 구성 번호 리스트)

그러나 이 경우, 복권을 for-each문으로 순회하고 그 복권의 구성 번호 리스트를 순회하면서 조건문을 작성해야하므로 Depth가 3이 되었다.

또한 어떤 복권인지는 중요하지 않고 '복권에 따라 번호 일치 개수'가 중요하기에 Set으로 구현키로 했다.(25.2.10)

최종적으로는 List로 구현했다.(25.2.12)

 

*HashMap: 입력된 데이터의 순서가 유지되지 않음.

당첨 번호와 발행된 복권 번호와의 비교를 통한 일치 개수에 따른 등수가 중요한 것이지,

특정 복권에 따른 결과를 내는 것이 아니기 때문에 복권 순서대로 비교하는 게 중요한 게 아니라서 HashMap을 사용했다.

최종적으로 List로 구현했는데, 개별 복권에 번호를 부여할 이유가 없었기 때문이다.

 

  Issue_1.1 : 아래 코드에서 Map에 put할 때 issuedLottoNumbers에서 오류 발생

    아래의 경우, issuedLottoNumbers는 계속해서 같은 객체로 사용되기 때문에 매번 새로 복사된 리스트를 put하는 방식으로 해결

for(int i=0; i<numbersOFIssuedTickets; i++) {
            while (issuedLottoNumbers.size() < LOTTO_SET_SIZE) { //6개의 정수 add
                Random random = new Random();
                int randomNumber = random.nextInt(MAX_LOTTO_NUMBER) + 1; //1~45까지의 임의의 정수 생성
                if (!issuedLottoNumbers.contains(randomNumber)) {
                    issuedLottoNumbers.add(randomNumber); //중복되지 않은 임의의 한 정수 add
                }
            }
            Collections.sort(issuedLottoNumbers);
            issuedTickets.put(i, issuedLottoNumbers); //issuedLottoNumbers에 오류 발생
        }

 

-> 매 복권 발행은 복권 객체 생성을 의미,

따라서 issuedLottoNumbers = new ArrayList<>();로 생성

 

-> Map에 put할 때 깊은 복사(Deep Copy) 사용

더보기

깊은 복사(Deep Copy)

 

새로운 객체를 생성하여 원본 리스트의 값을 복사하는 방법 즉, 객체 복사라고 생각하면 쉽다.

이렇게 하면 기존 리스트와 완전히 독립적인 새로운 리스트가 만들어지기 때문에 이후에 리스트가 변경되어도 put된 다른 리스트에는 영향을 미치지 않는다.

 

issuedTickets.put(i, new ArrayList<>(issuedLottoNumbers));  // 깊은 복사

 

new ArrayList<>(issuedLottoNumbers) issuedLottoNumbers 새로운 복사본을 만들어서 issuedTickets put하는 방식,

각 티켓마다 독립적인 리스트가 저장됨

cf, 얕은 복사?

 

참조 복사

 

원래 리스트를 그대로 참조하는 방법으로 put할 때 같은 객체를 저장하게 됨 

issuedTickets.put(i, issuedLottoNumbers);  // 참조 복사​

 

issuedLotoNumbers를 put 할 때 같은 리스트를 저장하게 되기 때문에, 이후 issuedLottoNumbers에 추가되는 값이 모든 티켓에 영향을 미침

 

#Issue_2: PR 후 피드백 반영