[우아한테크코스] 로또

2025. 5. 2. 14:21·Java

이번 스터디에서는 우테코 로또 미션을 진행했다.

자동차 경주 미션보다 기능도 많고 고려할 것들도 많았다. 기능 요구사항은 다음과 같다.


이번에는 리드미에 나름대로 구현목록을 작성하고 체크하면서 구현하였다.

 

자동차 경주 미션과 마찬가지로 객체지향을 지키며 mvc패턴으로 구현해보았다.

전체적인 설계는 다음과 같이 잡았다.

이제 구현해보자.

중요하지 않은 코드는 생략하도록 하겠다. 전체 코드는 아래의 링크에 첨부되어있다.

model 패키지

패키지 설계 중 모델 패키지에는 도메인의 핵심 모델을 담아야겠다고 생각했다. 

Lotto 클래스

  • 한 장의 로또를 나타내는 클래스 (6개의 숫자 포함)
  • 1부터 45중 6개의 숫자를 중복 없이 오름차순으로 멤버변수에 저장

Lottos 클래스

  • 여러 장의 로또(Lotto)를 하나의 객체로 묶어 다룬다.

일급컬렉션으로 구현하였고 정적 팩토리 메서드를 사용해보았다.

💡 정적 팩토리 메서드란?
 클래스를 생성하는 정적인(static) 메서드로, 객체 생성을 생성자 대신 담당하는 메서드이다.

장점은 어떤 게 있을까?

  • 생성자와 달리 의미 있는 이름을 가진다.
  • static이기 때문에 new 키워드 없이 클래스 이름으로 호출해야한다. 이렇게 하면 객체 생성 로직을 외부에 노출하지 않아서 캡슐화에도 유용하다.
  • 내부 상태를 변경하지 않는 객체(여기선 일급컬렉션) 생성에 적합하다.

코드로 알아보자.

private Lottos(List<Lotto> lottos) {
        this.lottos = lottos;
    }

public static Lottos from(int num){
    List<Lotto> lottoList = new ArrayList<>();
    for (int i = 0; i < num; i++) {
        lottoList.add(new Lotto());
    }
    return new Lottos(lottoList);
}

생성자를 private으로 막고 정적 팩토리 메서드를 통해 생성하도록 강제한다.

👉  생성자를 감추고 무엇을 기반으로 객체를 만드는지 명확하게 표현한다.

LottoRank 클래스

로또 번호 일치 개수와 보너스 번호 일치 여부에 따라 등수 를 구분하고, 각 등수에 상금을 매핑하는 역할을 수행한다.

enum을 사용해보았다.

💡 enum이란?
 자바에서 열거형 타입을 뜻하며 "미리 정해진 값들의 집합"을 표현하는 특별한 클래스이다.
 싱글턴처럼 동작하기 때문에 인스턴스가 고정된다.
 enum에 필드와 메서드를 추가하면 상수 객체 하나하나가 그 필드와 메서드를 가진 인스턴스처럼 동작할 수 있다.

장점은 어떤 게 있을까? 

  • 데이터와 의미를 함께 담아 응집도 향상
  • 정해진 값 외에는 컴파일 에러가 발생해 타입 안정성 증가
  • 새로운 값이 생겨도 기존 코드 수정이 거의 없음 → OCP (Open-Closed Principle) 준수
  • 각 상수는 한 번만 생성되고 재사용됨 (메모리 효율 + 스레드 안전)

또한 구현할 때 enum으로 바꾸면서 코드가 짧아지고 가독성이 증가하였다.

코드에서 예시를 보면 먼저 LottoRank enum클래스이다.

public enum LottoRank {
    THREE(3, 5_000),
    FOUR(4, 50_000),
    FIVE(5, 1_500_000),
    BONUS(5, 30_000_000),
    SIX(6, 2_000_000_000),
    NONE(0, 0);

    private final int matchCount;
    private final int prize;

    LottoRank(int matchCount, int prize) {
        this.matchCount = matchCount;
        this.prize = prize;
    }
    public static LottoRank of(int matchCount, boolean bonusMatch) {
        ...
    }
    public int getMatchCount() {
        ...
    }
    public int getPrize() {
       ...
    }
}

 

public static Map<LottoRank, Integer> getResult(Lottos lottos, List<Integer> winningNumbers, int bonusNumber) {
        Map<LottoRank, Integer> result = initResult();

        for (Lotto lotto : lottos.getLottos()) {
            int matchCount = lotto.getMatchCount(winningNumbers);
            boolean bonusMatch = lotto.getBonusCount(bonusNumber);
            LottoRank rank = LottoRank.of(matchCount, bonusMatch);
            if (rank != LottoRank.NONE) {
                result.put(rank, result.get(rank) + 1);
            }
        }
        return result;
    }

private static Map<LottoRank, Integer> initResult() {
        Map<LottoRank, Integer> result = new LinkedHashMap<>();
        for (LottoRank rank : LottoRank.values()) {
            if (rank != LottoRank.NONE) {
                result.put(rank, 0);
            }
        }
        return result;
    }

JVM이 자동으로 생성한 싱글턴 인스턴스이기 때문에 다른 코드에서 LottoRank를 new로 생성하지 않는다.

등수별 당첨 횟수 판단도 enum이 진행한다.

 

참고자료

https://techblog.woowahan.com/2527/

 

Java Enum 활용기 | 우아한형제들 기술블로그

안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다. 이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 Enum에 관해

techblog.woowahan.com

서비스 패키지

비즈니스 로직의 중심의 되는 부분이다. 

  • 로또 번호 생성
  • 당첨 번호와의 일치 여부 판별
  • 보너스 번호 체크
  • 등수별 결과 집계

이를 통해 핵심 규칙과 흐름을 한 곳에 모아두고,
컨트롤러나 모델, 뷰 계층과 명확하게 분리하여 응집도 높은 설계와 유지보수 용이성을 실현할 수 있다.

 

public class LottoService {
    public Lottos createLottos(int money){
        ...
    }

    public Map<LottoRank, Integer> matchLotto(Lottos lottos, List<Integer> winningNumbers, int bonusNumber) {
        ...
    }

    private Map<LottoRank, Integer> initResult() {
        ...
    }

    private int getMatchCount(List<Integer> lottoNumbers, List<Integer> winningNumbers){
        ...
    }

    public double checkProfitRate(int money, Map<LottoRank,Integer> result){
        ...
    }
}

컨트롤러 패키지

사용자의 입력을 받아 서비스로 전달하고, 그 결과를 다시 사용자에게 출력하는 역할을 담당하는 계층이다.

애플리케이션 전반의 처리 흐름을 통제한다.

 

코드는 생략하겠다.

 

뷰 패키지

view 패키지는 사용자와 상호작용하는 역할을 담당하며 출력(화면 표시)과 입력(사용자 입력 처리)을 담당하도록 구현했다.

MVC패턴에서 "View" 역할을 수행하며, 비즈니스 로직과는 분리되어 UI를 처리한다.

 

InputView, OutputView가 있고

에러 메세지를 enum으로 따로 만들어서 관리하였다.

InputView, OutputView 코드는 생략하겠다.

public enum ExceptionMessage {
    NOT_NUMBER_RANGE("[ERROR] 숫자는 1부터 45 사이의 숫자여야 합니다."),
    NOT_NUMBER_SIZE("[ERROR] 당첨 번호는 6개 입력 가능합니다."),
    NOT_NUMBER_OVERLAP("[ERROR] 중복된 숫자를 입력하셨습니다."),
    INPUT_TYPE_ERROR("[ERROR] 숫자만 입력해 주세요."),
    NOT_NUMBER_ERROR("[ERROR] 금액은 숫자만 등록 가능합니다."),
    NOT_MINIMUM_AMOUNT_ERROR("[ERROR] 금액은 1000원 이상이어야 합니다."),
    NOT_DIVISIBLE_NUMBER_ERROR("[ERROR] 금액은 1000단위여야 합니다.");

    private final String message;

    ExceptionMessage(String message) {
        this.message = message;
    }

    public void print() {
        System.out.println(message);
    }

    public String getMessage() {
        return message;
    }
}

validator 패키지

입력 값의 유효성을 검사하고, 도메인 로직으로 넘기기 전에 문제를 사전에 차단하는 계층이다.

정적 메서드로 구현하여 상태를 가지지 않고 어디서든 바로 호출이 가능하게 하였다.

에러 메시지를 일관성 있게 출력할 수 있도록 enum과 연결하여 구현하였다.

코드는 생략하겠다.

피드백

구현한 것을 토대로 피드백을 받았다.

 

1. Lotto가 직접 랜덤 숫자를 생성하고 있다.

  • 테스트 코드에서 직접 값을 지정을 못한다. 실제로 몇 장을 콘솔에 찍는 것으로 테스트하였다.

2. 서비스의 응집도가 낮다.

  • 현재 서비스가 하는 역할과 책임이 많다. 몇몇 로직은 model로 넘겨도 될 듯 하다.

3. 한 눈에 알아볼 수 있는 메서드명과, stream을 활용한 코드 간결화

  • 다른 사람이 나의 코드를 보는데에도 드는 비용이 존재한다. 명확한 메서드명과 알아보기 쉬운 코드로 비용을 줄이자.

피드백을 바탕으로 리팩토링을 진행해보았다.


리팩토링

먼저 리팩토링 목록을 정리해보았다.

리팩토링 하기 전 로또 각 숫자들도 객체처럼 다룰 수 있지 않을까 생각하였다.

각 숫자는 로또번호라는 의미를 가지고 있고 코드만 봐도 이 숫자가 로또 번호라는 걸 알 수 있게 도메인의 의도를 드러내도록 변경하는게 좋다고 생각했다. 그래서 LottoNumber 클래스를 따로 구현하였다.

LottoNumber 클래스

public class LottoNumber {
    public static final int MIN_LOTTO_NUM = 1;
    public static final int MAX_LOTTO_NUM = 45;
    private final int lottoNumber;

    public LottoNumber(int lottoNumber) {
        validate(lottoNumber);
        this.lottoNumber = lottoNumber;
    }

    public int getLottoNumber() {
        return lottoNumber;
    }

    @Override
    public String toString() {
        return String.valueOf(lottoNumber);
    }

    private void validate(int lottoNumber){
        if (lottoNumber < MIN_LOTTO_NUM || lottoNumber > MAX_LOTTO_NUM) {
            throw new IllegalArgumentException("[ERROR] 로또에 사용되는 번호는 1~45 사이의 숫자만 가능합니다.");
        }
    }
}

기존에는 번호 생성 이후, 로또 번호가 1~45 범위에 있는지 매번 수동으로 검사해야 했는데

LottoNumber 생성자에서 검사를 진행한다.

→ 데이터의 무결성 보장
→ 유효하지 않은 객체가 시스템에 퍼지는 걸 원천 차단

 

이제 리팩토링 목록을 보고 리팩토링 해보자.

1. 먼저 랜덤 번호 생성을 외부에서 주입해주는 것으로 리팩토링 하였다.

기존에는 Lotto에서 자체적으로 랜덤 번호를 생성하니 테스트코드에서 직접 번호 지정이 되지가 않았다.

  • 비즈니스 로직과 랜덤 구현이 강하게 결합이 되어있는 문제

generator 패키지를 따로 만들어서 의존성을 주입해주는 방식으로 리팩토링을 하였다.

 

별도의 NumberGenerator 인터페이스를 정의하고 구현한 RandomNumberGenerator를 Lotto는 생성자에서 NumberGenerator로 받아 내부 번호 리스트를 주입받도록 변경하는 방식이다.

테스트코드에서는 인터페이스를 구현한 FixNumberGenerator를 주입받는다.

설계 원칙 적용

  • DIP (Dependency Inversion Principle)
    → Lotto는 더 이상 구체적인 랜덤 구현체를 알지 않고, 인터페이스에만 의존함
  • OCP (Open-Closed Principle)
    → 새로운 번호 생성 로직이 추가되더라도 기존 코드를 변경하지 않고 확장 가능
  • SRP (Single Responsibility Principle)
    → Lotto는 "번호 보관 및 평가"만 책임지고, 생성은 외부에서 담당

코드로 구현해보자.

generator 패키지

NumberGenerator 인터페이스

public interface NumberGenerator {
    List<Integer> generate();
}

 

RandomNumberGenerator 클래스

public class RandomNumberGenerator implements NumberGenerator {
    private static final int MIN_RANGE = 1;
    private static final int MAX_RANGE = 45;
    private static final int PICK_NUMBER = 6;

    @Override
    public List<Integer> generate() {
        return Randoms.pickUniqueNumbersInRange(MIN_RANGE, MAX_RANGE, PICK_NUMBER);
    }
}

 

FixNumberGenerator 클래스

public class FixNumberGenerator implements NumberGenerator {
    private final List<Integer> fixNumbers;

    public FixNumberGenerator(List<Integer> fixNumbers) {
        this.fixNumbers = fixNumbers;
    }

    @Override
    public List<Integer> generate() {
        return fixNumbers;
    }
}

 

이런 구조는 전략패턴이라고도 한다.

전략 패턴(Strategy Pattern)이란?

행위를 캡슐화하여 동적으로 바꿀 수 있도록 설계하는 패턴
  • 어떤 객체의 행위를 객체 외부에서 주입받도록 한다.
  • 다양한 전략(행위)들을 인터페이스로 추상화한다.
  • 필요에 따라 전략을 교체할 수 있는 구조를 의미한다.

2. 서비스 역할 분리로 응집도를 향상

현재 LottoService가 맡은 책임이 많다. 

단일 책임 원칙(SRP)을 위반하고 관리하기 어려운 구조이다.

서비스의 역할을 나누고 당첨번호 판단은 Lotto 자체가 판단해도 될 듯 하다.

바뀐 Lotto 클래스

public class Lotto {
    private final List<LottoNumber> lotto;

    public Lotto(NumberGenerator generator) {
        ...
    }

    public List<LottoNumber> getNumbers() {
        return lotto;
    }

    public int getMatchCount(List<Integer> winningNumbers) {
        ...
    }


    public boolean getBonusCount(int bonus) {
        ...
    }

    private void validate(List<Integer> numbers) {
        ...
    }

}

위에서 리팩토링 한 것 처럼 generator인터페이스를 주입받고 있고

로또 번호가 당첨 번호와 얼마나 일치하는지 판단은 그 로또 객체(Lotto) 자신이 판단하는 것으로 구현하였다.

도메인 객체 중심 설계로 로또라는 도메인 개념이 “당첨 여부 판단”이라는 핵심 행위까지 포함하게 되어 도메인 모델이 더 풍부하게 되었다.

 

service 응집도 향상

기존의 LottoService를 통합서비스로 바꾸고 로또 생성 및 결과계산과 수익률 계산 서비스를 따로 구현하였다.

LottoResultService

public class LottoResultService {
    // 정적 메서드 사용
    ...
    }
    private static Map<LottoRank, Integer> initResult() {
       	...
    }
}

LottoProfitService

public class LottoProfitService {
    // 정적 메서드 사용
    public static double calculate(int money, Map<LottoRank, Integer> result) {
        ...
    }
}

LottoService를 작은 책임 단위로 나누고, LottoApplicationService로 통합 흐름을 관리하게 리팩토링하여
서비스 계층의 응집도를 높이고,
유지보수성, 테스트 용이성, 확장성에서 기존보다 뛰어난 구조로 개선하였다.

3. stream을 활용한 코드 간결화

Lotto 클래스 내부의 코드를 스트림을 사용하여 변경해보았다.

public class Lotto {
    private final List<LottoNumber> lotto;

    public Lotto(NumberGenerator generator) {
        List<Integer> tmpLotto = generator.generate();
        validate(tmpLotto);
        Collections.sort(tmpLotto); // 오름차순 정렬

        this.lotto = tmpLotto.stream()
                .map(LottoNumber::new)
                .collect(Collectors.toList());
    }

    public List<LottoNumber> getNumbers() {
        return lotto;
    }

    public int getMatchCount(List<Integer> winningNumbers) {
        return (int) lotto.stream()
                .map(LottoNumber::getLottoNumber)
                .filter(winningNumbers::contains)
                .count();
    }


    public boolean getBonusCount(int bonus) {
        return lotto.stream()
                .map(LottoNumber::getLottoNumber)
                .anyMatch(number -> number == bonus);
    }

    private void validate(List<Integer> numbers) {
        if (numbers.size() != 6) {
            throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다.");
        }

        Set<Integer> set = new HashSet<>(numbers);
        if (set.size() != 6) {
            throw new IllegalArgumentException("[ERROR] 로또 번호는 중복을 허용하지 않습니다.");
        }
    }

}

 

이 코드에서 stream()을 사용하는 이유는
간결하고 명확한 선언형 스타일로,
데이터 변환, 필터링, 계산을 안정적으로 수행하기 위해서이다.

객체지향 + 함수형 프로그래밍의 장점을 살렸다고 볼 수 있다.

스트림에 대한 자세한 내용은 아래의 링크에서 찾아볼 수 있다.

https://hstory0208.tistory.com/entry/Java%EC%9E%90%EB%B0%94-Stream%EC%8A%A4%ED%8A%B8%EB%A6%BC%EC%9D%B4%EB%9E%80

 

[Java/자바] Stream(스트림)이란? 사용법 총정리

자바에서는 많은 양의 데이터를 저장하기 위해서 배열이나 컬렉션을 이용합니다. 이렇게 저장된 데이터에 접근하기 위해서는 반복문이나 반복자(iterator)를 사용하여 매번 새로운 코드를 작성해

hstory0208.tistory.com


이번 미션에서 느낀점

이번 로또 프로젝트를 구현하고 리팩토링하면서, 단순히 동작하는 코드를 만드는 것을 넘어

“왜 이 책임이 여기에 있어야 하는가”
"무엇이 유지보수하기 좋은 구조인가"에 대해 깊이 고민할 수 있었다.

 

또한 설계 원칙(SOLID, OOP)의 진짜 가치는 리팩토링 과정에서 더 크게 체감되는 것을 느꼈다.

전략 패턴으로 리팩토링을 할 때 각 객체의 책임이 잘 나눠져 있어 리팩토링 시 큰 어려움이 없었다.

 

하필 시험기간과 겹쳐서 힘들었지만.. 성공적으로 끝낼 수 있었던 것 같다.


더 생각해볼 점

  • 서비스 패키지가 필요할까.. 라는 생각이 들었다. 해당 역할을 수행할 도메인을 만들어도 좋을 것 같다.
  • model에서 각 객체의 자체무결성을 위해 검증 로직을 추가했는데 validation패키지가 따로 있어서 책임이 중복되거나 혼란을 줄 수 있다.
  • 의존성을 어느 시점에 주입할지에 대해 고민해 볼 필요가 있다.

https://github.com/hwanh2/java-lotto

 

GitHub - hwanh2/java-lotto

Contribute to hwanh2/java-lotto development by creating an account on GitHub.

github.com

 

'Java' 카테고리의 다른 글

@Entity를 붙이면 일어나는 일 - JPA 내부 동작 원리  (0) 2026.02.01
JPA 영속성 컨텍스트  (0) 2026.01.31
[우아한테크코스] 출석부  (1) 2025.05.29
[우아한테크코스] 자동차 경주  (0) 2025.04.29
'Java' 카테고리의 다른 글
  • @Entity를 붙이면 일어나는 일 - JPA 내부 동작 원리
  • JPA 영속성 컨텍스트
  • [우아한테크코스] 출석부
  • [우아한테크코스] 자동차 경주
hwanheee
hwanheee
hwanheee 님의 블로그 입니다.
  • hwanheee
    hwanheee 님의 블로그
    hwanheee
  • 전체
    오늘
    어제
    • 분류 전체보기 (20)
      • 백엔드 (8)
      • Java (5)
      • cs (1)
      • 알고리즘 (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    소수 구하기
    분산락
    다이나믹 프로그래밍
    모의투자
    동시성 제어
    유니온파인드
    크루스칼
    영속성컨텍스트
    체결엔진
    배낭문제
    최소 스패닝 트리
    프림
    주식 모의투자
    우테코
    최소 신장 트리
    rabbitmq
    celery
    우테코 출석부
    알고리즘
    우테코 로또
    우테코 레이싱카
    최단경로 알고리즘
    스프링
    레디스
    우아한테크코스
    JPA
    비동기처리
    비관락
    동시성
    생성형AI
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
hwanheee
[우아한테크코스] 로또
상단으로

티스토리툴바