미션 간단 설명

사용자에게 금액을 입력 받고, 입력받은 금액만큼 무작위 숫자 6개를 가지는 숫자 배열을 생성하고, 사용자에서 우승 번호와 보너스 번호를 요청하여 총 몇 퍼센트 이득인지 반환하는 미션이다.

프로젝트 구조 변경

변경 이유

2주차 레이싱카 미션에서는 MVC 패턴을 사용했지만, 이번 로또 미션에서는 DDD(Domain-Driven Design) 관점으로 재구성했습니다.

기존에 사용하던 MVC 패턴의 경우 흐름제어의 역할을 하는 controller, 데이터와 비지니스가 혼재한 model, 입출력의 view를 사용합니다. 이는 model에 계산 로직과 검증 로직이 섞이며, 비지니스 규칙이 명확히 드러나지 않습니다.

그러한 이유로 순수히 비지니스 로직을 담당하는 domain, 도메인과 조합하는 응용 서비스인 service, 흐름 제어인 controller, 입출력인 view로 만들었습니다.

계층별 책임

계층책임예시
Domain비즈니스 규칙, 검증”구매 금액은 1,000원 단위”
Service도메인 조합, 외부 의존성랜덤 로또 생성, 통계 계산
Controller흐름 제어, 예외 처리입력 → 생성 → 계산 → 출력
View입출력Console 읽기/쓰기

프로젝트 구조

src/main/java/lotto/
├── Application.java
├── domain/
│   ├── Lotto.java              # 로또 번호 (6개 숫자)
│   ├── PurchaseAmount.java     # 구매 금액 (검증 포함)
│   ├── WinningNumbers.java     # 당첨 번호 (검증 포함)
│   ├── BonusNumber.java        # 보너스 번호 (검증 포함)
│   └── Rank.java               # 당첨 등수 (열거형)
├── service/
│   ├── LottoGenerator.java     # 로또 생성 (랜덤)
│   └── LottoStatistics.java    # 통계 계산
├── view/
│   ├── InputView.java          # 입력
│   ├── OutputView.java         # 출력
│   └── ErrorMessage.java       # 에러 메시지
│   └── Message.java            # 메시지
└── controller/
    └── LottoController.java    # 게임 흐름 제어

Rank 열거형으로 상수 관리

로또 등수는 1~5등으로 고정된 상수의 집합이기에 열거형을 통하여 관리하였습니다.

장점설명
타입 안정성컴파일 타임에 잘못된 값 방지
가독성Rank.FIRST > 1
응집도등수별 속성(일치수, 상금)을 한 곳에 관리
확장성새 등수 추가 시 한 곳만 수정

정적 팩토리 메서드 (of)

Rank 열거형을 만들며, 생성자 대신 정적 팩토리 메서드를 사용했습니다.

public static Rank of(int matchCount, boolean matchBonus)
{
    
}

사용한 이유로는 생성자는 반환 타입 고정이기에 제가 원하는 방식으로 사용할 수 없었고, 찾아보니 정적 팩토리 메서드란 것이 있어 사용해 보았습니다.

생성자정적 팩토리 메서드 (of)
이름이 없음이름으로 의도 표현
매번 새 객체 생성캐싱 가능
반환 타입 고정하위 타입 반환 가능

핵심 기능

  1. 구매 금액 검증
  • 1,000원 단위 검증
  • 양수 검증
  • 숫자 형식 검증
  1. 로또 번호 검증
  • 6개 숫자 검증
  • 1~45 범위 검증
  • 중복 검증
  1. 보너스 번호 검증
  • 1~45 범위 검증
  • 당첨 번호와 중복 검증
  1. 당첨 통계
  • 일치 개수 계산
  • 보너스 번호 확인
  • 등수 판별 (Rank.of)
  1. 수익률 계산
  • 총 당첨금 / 구매 금액 × 100
  • 소수점 둘째 자리에서 반올림

아키텍처 다이어그램

플로우 차트

flowchart TD
    Start([시작]) --> InputAmount[로또 구입 금액 입력]
    
    InputAmount --> ValidateAmount{금액 유효성 검사}
    ValidateAmount -->|숫자 아님| ErrorNotNumber["[ERROR] 숫자여야 합니다"]
    ValidateAmount -->|음수| ErrorNegative["[ERROR] 양수여야 합니다"]
    ValidateAmount -->|1000원 나머지 존재| ErrorDivisible["[ERROR] 1000원 단위"]
    ErrorNotNumber --> InputAmount
    ErrorNegative --> InputAmount
    ErrorDivisible --> InputAmount
    
    ValidateAmount -->|유효| CalcQuantity[구매 수량 계산<br/>금액 ÷ 1000]
    CalcQuantity --> PrintQuantity[구매 수량 출력]
    
    PrintQuantity --> GenerateLotto[랜덤 로또 번호 생성<br/>Randoms.pickUniqueNumbersInRange]
    GenerateLotto --> SortNumbers[번호 오름차순 정렬]
    SortNumbers --> PrintLotto[로또 번호 출력]
    
    PrintLotto --> MoreLotto{더 생성할<br/>로또가 있는가?}
    MoreLotto -->|예| GenerateLotto
    MoreLotto -->|아니오| PrintWinning[로또 번호 출력]
    PrintWinning --> InputWinning[당첨 번호 입력]
    
    InputWinning --> ValidateWinning{당첨 번호<br/>유효성 검사}
    ValidateWinning -->|6개 아님| ErrorCount["[ERROR] 6개 아닙니다"]
    ValidateWinning -->|문자 포함| ErrorChar["[ERROR] 문자 입력됨"]
    ValidateWinning -->|중복 존재| ErrorDup["[ERROR] 중복됨"]
    ValidateWinning -->|범위 초과| ErrorRange["[ERROR] 1~45 범위"]
    ErrorCount --> InputWinning
    ErrorChar --> InputWinning
    ErrorDup --> InputWinning
    ErrorRange --> InputWinning
    
    ValidateWinning -->|유효| InputBonus[보너스 번호 입력]
    
    InputBonus --> ValidateBonus{보너스 번호<br/>유효성 검사}
    ValidateBonus -->|당첨번호와 중복| ErrorBonusDup["[ERROR] 당첨번호와 중복"]
    ValidateBonus -->|범위 초과| ErrorBonusRange["[ERROR] 1~45 범위"]
    ErrorBonusDup --> InputBonus
    ErrorBonusRange --> InputBonus
    
    ValidateBonus -->|유효| InitCount[당첨 집계 초기화<br/>1등~5등 count = 0]
    InitCount --> CheckLoop{모든 로또<br/>확인 완료?}
    
    CheckLoop -->|아니오| CompareNumbers[당첨 번호와 비교]
    CompareNumbers --> MatchCount{일치 개수 확인}
    
    MatchCount -->|6개| Rank1[1등 count++<br/>2,000,000,000원]
    MatchCount -->|5개 + 보너스| Rank2[2등 count++<br/>30,000,000원]
    MatchCount -->|5개| Rank3[3등 count++<br/>1,500,000원]
    MatchCount -->|4개| Rank4[4등 count++<br/>50,000원]
    MatchCount -->|3개| Rank5[5등 count++<br/>5,000원]
    MatchCount -->|2개 이하| NoWin[당첨 없음]
    
    Rank1 --> AddPrize[총 당첨금 누적]
    Rank2 --> AddPrize
    Rank3 --> AddPrize
    Rank4 --> AddPrize
    Rank5 --> AddPrize
    NoWin --> CheckLoop
    AddPrize --> CheckLoop
    
    CheckLoop -->|예| PrintStats[당첨 내역 출력<br/>3개 일치 - X개<br/>4개 일치 - X개<br/>5개 일치 - X개<br/>5개+보너스 - X개<br/>6개 일치 - X개]
    
    PrintStats --> CalcReturn[수익률 계산<br/>총당첨금 ÷ 구매금액 × 100]
    CalcReturn --> RoundReturn[소수점 둘째자리 반올림]
    RoundReturn --> PrintReturn[수익률 출력<br/>총 수익률은 X%입니다]
    
    PrintReturn --> End([종료])
    
    style Start fill:#e1f5e1
    style End fill:#ffe1e1
    style ErrorNotNumber fill:#ffcccc
    style ErrorNegative fill:#ffcccc
    style ErrorDivisible fill:#ffcccc
    style ErrorCount fill:#ffcccc
    style ErrorChar fill:#ffcccc
    style ErrorDup fill:#ffcccc
    style ErrorRange fill:#ffcccc
    style ErrorBonusDup fill:#ffcccc
    style ErrorBonusRange fill:#ffcccc
    style Rank1 fill:#fff4cc
    style Rank2 fill:#fff4cc
    style Rank3 fill:#fff4cc
    style Rank4 fill:#fff4cc
    style Rank5 fill:#fff4cc

패키지 다이어그램

graph LR
    subgraph lotto
        App[Application.java]

        subgraph controller
            Ctrl[LottoController]
        end

        subgraph view
            IV[InputView]
            OV[OutputView]
            EM[ErrorMessage]
        end

        subgraph service
            LG[LottoGenerator]
            LS[LottoStatistics]
        end

        subgraph domain
            PA[PurchaseAmount]
            Lotto[Lotto]
            WN[WinningNumbers]
            BN[BonusNumber]
            Rank[Rank]
        end
    end

    App --> Ctrl
    Ctrl --> view
    Ctrl --> service
    Ctrl --> domain
    service --> domain

    style App fill:#ffeb3b
    style controller fill:#e1f5ff
    style view fill:#fff4e1
    style service fill:#e8f5e9
    style domain fill:#f3e5f5

시퀀스 다이어그램

sequenceDiagram
    actor User
    participant App as Application
    participant Ctrl as LottoController
    participant IV as InputView
    participant OV as OutputView
    participant PA as PurchaseAmount
    participant LG as LottoGenerator
    participant Lotto
    participant WN as WinningNumbers
    participant BN as BonusNumber
    participant LS as LottoStatistics
    participant Rank

    User->>App: 프로그램 실행
    App->>Ctrl: run()
    
     로또 생성
    Ctrl->>LG: generate(8)
    loop 8번 반복
        LG->>Lotto: new Lotto(랜덤번호)
        Lotto-->>LG: Lotto 객체
    end
    LG-->>Ctrl: List<Lotto>
    
    Ctrl->>OV: printPurchaseCount(8)
    OV->>User: "8개를 구매했습니다."
    Ctrl->>OV: printLottos(lottos)
    OV->>User: 로또 번호 출력
    
     보너스 번호 입력
    Ctrl->>OV: printBonusNumberPrompt()
    OV->>User: "보너스 번호를 입력해 주세요."
    Ctrl->>IV: readLine()
    User->>IV: "7"
    Ctrl->>BN: new BonusNumber("7", winningNumbers)
    BN-->>Ctrl: BonusNumber 객체
    
     결과 출력
    Ctrl->>OV: printStatisticsHeader()
    OV->>User: "당첨 통계"
    Ctrl->>OV: printWinningStatistics(statistics)
    OV->>User: 등수별 당첨 내역
    Ctrl->>OV: printProfitRate(profitRate)
    OV->>User: "총 수익률은 62.5%입니다."

테스트 전략

테스트 종류대상개수
Domain 단위 테스트Lotto, PurchaseAmount, etc.60+
통합 테스트ApplicationTest14

요구사함

기능 요구사항

  • 입력

지역 변수로 입력받은 번호를 가지고 있다.

  • 로또 구입 금액을 입력 받는다.

만일 1000원으로 나누었을시 나머지가 있다면, 에러 처리한다.

  • 당첨 번호를 입력 받는다.

번호는 ’,‘를 기준으로 구분한다. 번호는 6자리이다. 번호는 중복되어서는 안된다. 번호는 1~45 사이의 숫자이다.

  • 보너스 번호를 입력 받는다.

당첨 번호와 중복되어서는 않된다. 번호는 1~45 사이의 숫자이다.

  • 출력

    • 발행한 로또 수량을 출력한다.

    {갯수}개 구매했습니다.

    • 로또 번호를 출력한다.

    [{{갯수}}, {갯수}, {갯수}, {갯수}, {갯수}, {갯수}]

    • 당첨 내역을 출력한다.
      3개 일치 (5,000원) - {갯수}개
      4개 일치 (50,000원) - {갯수}개
      5개 일치 (1,500,000원) - {갯수}개
      5개 일치, 보너스 볼 일치 (30,000,000원) - {갯수}개
      6개 일치 (2,000,000,000원) - {갯수}개 
    • 수익률을 출력한다.

    총 수익률은 {퍼센트}%입니다.

    • 예외 상황 시 에러 문구를 출력해야 한다. “[ERROR]“로 시작해야 한다.
  • 랜덤 함수

    • Randoms.pickUniqueNumbersInRange(1, 45, 6)을 통해 중복되지 않는 6개의 숫자 생성
  • 에러 처리

    • 입력이 빈 문자열인 경우

    [ERROR] 빈 문자열이 입력 되었습니다. 다시 입력해 주십시오

    • 입력이 천원 단위가 아닐 경우

    [ERROR] 입력은 천원 단위여야 합니다. 다시 입력해 주십시오

    • 로또 구입 금액이 숫자가 아닐 시

    [ERROR] 입력한 금액은 숫자여야 합니다. 다시 입력해 주십시오

    • 로또 구입 금액이 음수일 경우

    [ERROR] 입력한 금액은 양수여야 합니다. 다시 입력해 주십시오

    • 구분자로 분리한 숫자가 6개가 아닌 경우

    [ERROR] 입력 받은 숫자가 6개가 아닙니다. 다시 입력해 주십시오

    • 구분자로 분리한 숫자가 문자일 경우

    [ERROR] 숫자 입력이 아닌 문자가 입력되었습니다. 다시 입력해 주십시오

    • 입력한 번호가 중복되는 경우

    [ERROR] 입력받은 숫자가 중복되었습니다. 다시 입력해 주십시오 [ERROR] 입력한 보너스 번호는 중복되었습니다. 다시 입력해 주십시오

    • 번호가 1~45의 범위를 초과하는 경우

    [ERROR] 로또 번호는 1에서 45사이의 숫자를 입력해야 합니다. 다시 입력해 주십시오

사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 “[ERROR]“로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다. Exception이 아닌 IllegalArgumentException, IllegalStateException 등과 같은 명확한 유형을 처리한다.

  • 로또 함수

    • 숫자열을 오름차순으로 정렬한다.
    • 지역 변수 numbers가 6개의 숫자를 가진지 검증한다.
    • 입력받은 로또 객체와 비교하여 등수를 반환한다.
  • 가챠 함수

지역 변수로는 구매한 금액, 당첨 count, 총 당첨금을 가지고 있다.

  • 전달 받은 값을 1000으로 나눈 몫을 반환한다.
  • 랜덤함수로 받은 값을 로또 클래스의 전달인자로 사용하여 객체를 생성한다.
  • 당첨 기준을 토대로 당첨 count를 증가 시킨다.
      1등: 6개 번호 일치 / 2,000,000,000원
      2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
      3등: 5개 번호 일치 / 1,500,000원
      4등: 4개 번호 일치 / 50,000원
      5등: 3개 번호 일치 / 5,000원
  • 수익률을 소수점 둘째 자리부터 반올림한다.

입출력 요구사항

입력
  • 로또 구입 금액을 입력 받는다. 구입 금액은 1,000원 단위로 입력 받으며 1,000원으로 나누어 떨어지지 않는 경우 예외 처리한다.

14000

  • 당첨 번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다.

1,2,3,4,5,6

  • 보너스 번호를 입력 받는다.

7

출력
  • 발행한 로또 수량 및 번호를 출력한다. 로또 번호는 오름차순으로 정렬하여 보여준다.
8개를 구매했습니다.
[8, 21, 23, 41, 42, 43] 
[3, 5, 11, 16, 32, 38] 
[7, 11, 16, 35, 36, 44] 
[1, 8, 11, 31, 41, 42] 
[13, 14, 16, 38, 42, 45] 
[7, 11, 30, 40, 42, 43] 
[2, 13, 22, 32, 38, 45] 
[1, 3, 5, 14, 22, 45]
  • 당첨 내역을 출력한다.
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
  • 수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, 1,000,000.0%)
총 수익률은 62.5%입니다.
  • 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 “[ERROR]“로 시작해야 한다.

[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.

실행 결과 예시

구입금액을 입력해 주세요.
8000
 
8개를 구매했습니다.
[8, 21, 23, 41, 42, 43] 
[3, 5, 11, 16, 32, 38] 
[7, 11, 16, 35, 36, 44] 
[1, 8, 11, 31, 41, 42] 
[13, 14, 16, 38, 42, 45] 
[7, 11, 30, 40, 42, 43] 
[2, 13, 22, 32, 38, 45] 
[1, 3, 5, 14, 22, 45]
 
당첨 번호를 입력해 주세요.
1,2,3,4,5,6
 
보너스 번호를 입력해 주세요.
7
 
당첨 통계
---
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
총 수익률은 62.5%입니다.