[우아한테크코스 6기] 프리코스 3주차 회고 && 최종 리팩토링 후기

조경찬 (Jo Gyungchan)·2023년 12월 9일
0

우아한테크코스

목록 보기
3/4
post-thumbnail

이 글은 프리코스 3주차 회고 글, 그리고 프리코스가 끝난 뒤 진행한 리팩토링 결과까지 포함하고 있다는 것을 미리 말씀드립니다!! 😀
리팩토링 후 달라진 점도 궁금하신 분들을 끝까지 다 봐주세요!~🧐🧐


프리코스 2주 차 공통 피드백

3주 차 미션에 대한 글을 작성하기 이전에 먼저 2주 차 공통 피드백 내용들은 다음과 같습니다.

  • README.md를 상세히 작성한다
  • 기능 목록을 재검토한다
  • 기능 목록을 업데이트한다
  • 값을 하드 코딩하지 않는다
  • 구현 순서도 코딩 컨벤션이다
  • 변수 이름에 자료형은 사용하지 않는다
  • 한 함수가 한 가지 기능만 담당하게 한다
  • 함수가 한 가지 기능을 하는지 확인하는 기준을 세운다
  • 테스트를 작성하는 이유에 대해 본인의 경험을 토대로 정리해본다
  • 처음부터 큰 단위의 테스트를 만들지 않는다

2주 차 공통 피드백에 대해 자세히 보고 싶은 분은 2주 차 공통 피드백에서 볼 수 있습니다.

3주 차 미션을 구현하기 이전 2주 차 공통 피드백 내용들을 준수할 수 있도록 노력했습니다.

3주차


로또

3주차 미션은 로또를 구현하는 것이었습니다.
미션을 진행하는 방식은 다음과 같이 나와있었습니다.

🔍 진행 방식

  • 미션은 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항 세 가지로 구성되어 있다.
  • 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
  • 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.

또한, 이전 미션과 다르게 이번 미션에선 제공된 Lotto 클래스를 활용해 구현하도록 되어있었습니다.

// 제공된 Lotto 클래스
public class Lotto {
	private final List<Integer> numbers;
    
    public Lotto(List<Integer> numbers) {
    	validate(numbers);
        this.numbers = numbers;
    }
    
    private void validate(List<Integer> numbers) {
    	if (numbers.size != 6) {
        	throw new IllegalArgumentException();
        }
    }
    
    // TODO: 추가 기능 구현
}

요구 사항 분석


이전 주차와 마찬가지로, 요구 사항을 정확히 준수하기 위해 노력했습니다.

미션을 진행하기 전에 요구 사항을 꼼꼼히 분석하고 이를 토대로 구현할 기능 목록을 작성하였습니다.

# 💸 미션 - 로또

## 📝 로또 게임 규칙

### 로또
- 로또 번호의 숫자 범위는 1~45까지이다.
- 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
- 당첨은 1등부터 5등까지 있다.
- 당첨 기준
  - 1등: 6개 번호 일치
  - 2등: 5개 번호 일치 + 보너스 번호 일치
  - 3등: 5개 번호 일치
  - 4등: 4개 번호 일치
  - 5등: 3개 번호 일치
- 당첨 순위별 금액
  - 1등: 2,000,000,000원
  - 2등: 30,000,000원
  - 3등: 1,500,000원
  - 4등: 50,000원
  - 5등: 5,000원

### 입력
- 로또 구입 금액에 맞게 로또를 발행할 수 있다.
  - 로또 1장의 가격은 1,000원이다.
- 당첨 번호를 쉼표(,)를 기준으로 입력받는다
- 보너스 번호를 입력받는다.
- 사용자가 잘못된 값을 입력하면 `IllegalArgumentException`을 발생시키고 다시 입력받는다.

### 출력
- 발행한 로또 수량 및 번호를 출력한다.
  - 발행한 로또 번호는 오름차순으로 정렬하여 보여준다.
- 사용자의 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력한다.
  - 수익률은 소수점 둘째 자리에서 반올림한다.
- 예외가 발생할 경우 `[ERROR]`로 시작하는 에러 메시지를 출력한다.

---

## 🕹구현할 기능 목록

### 로또 기능 
- [x] 로또 번호는 1에서 45사이의 중복되지 않는 6개의 숫자를 가진다.
  - [x] **[예외 처리]** 로또 번호의 개수가 6개가 아니면 예외가 발생한다.
  - [x] **[예외 처리]** 로또 번호의 숫자들 중 1보다 작거나 45보다 큰 숫자가 있다면 예외가 발생한다.
  - [x] **[예외 처리]** 6개의 숫자 중 중복되는 숫자가 있다면 예외가 발생한다.

### 로또 구입 기능
- [x] 로또 구입 금액에 해당하는 만큼 로또를 발행할 수 있다.
  - [x] 로또 1장의 가격은 1,000원이다.
  - [x] **[예외 처리]** 로또 구입 금액이 1,000원보다 작다면 예외가 발생한다.
  - [x] **[예외 처리]** 로또 구입 금액이 1,000원으로 나누어 떨어지지 않으면 예외가 발생한다.

### 로또 생성 기능
- [x] 입력된 크기만큼 로또를 만들 수 있다.

### 보너스 번호 기능
- [x] 보너스 번호는 1부터 45사이의 숫자이다.
  - [x] **[예외 처리]** 보너스 번호가 1보다 작거나 45보다 크다면 예외가 발생한다.

### 당첨 로또 기능
- [x] 당첨 로또는 중복되지 않는 6개의 숫자와 보너스 번호 1개를 가진다.
  - [x] **[예외 처리]** 6개의 숫자 중 보너스 번호와 일치하는 숫자가 있으면 예외가 발생한다.
- [x] 1장의 로또와 당첨 번호를 비교하여 결과를 알 수 있다.
  - [x] 1등: 6개 번호 일치
  - [x] 2등: 5개 번호 일치 + 보너스 번호 일치
  - [x] 3등: 5개 번호 일치
  - [x] 4등: 4개 번호 일치
  - [x] 5등: 3개 번호 일치

### 당첨 상금 기능
- [x] 당첨 순위별 상금이 얼마인지 알 수 있다.
  - [x] 1등: 2,000,000,000원
  - [x] 2등: 30,000,000원
  - [x] 3등: 1,500,000원
  - [x] 4등: 50,000원
  - [x] 5등: 5,000원

### 당첨 통계 기능
- [x] 사용자가 구매한 총 로또와 당첨 번호를 비교하여 전체 당첨 내역을 알 수 있다.
- [x] 사용자가 구매한 로또 번호의 결과를 통해 수익률을 계산할 수 있다.

### 변환 기능
- [x] 문자열에서 숫자 리스트로 변환할 수 있다.
  - [x] 쉼표(,)를 기준으로 숫자를 분리한다.

### 입력 기능
- [x] **[공통 예외 처리]** 입력이 공백이면 예외가 발생한다.
- [x] 로또 구입 금액을 입력 받는다.
  - [x] **[예외 처리]** 로또 구입 금액에 대한 입력이 숫자가 아니면 예외가 발생한다.
- [x] 당첨 번호를 입력 받는다.
  - [x] **[예외 처리]** 당첨 번호에 대한 입력 형식이 올바르지 않으면 예외가 발생한다.
- [x] 보너스 번호를 입력 받는다.
  - [x] **[예외 처리]** 보너스 번호에 대한 입력이 숫자가 아니면 예외가 발생한다.
- [x] 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, 그 부분부터 입력을 다시 받는다.

### 출력 기능
- [x] 발행한 로또 수량 및 번호를 출력한다.
  - [x] 로또 번호는 오름차순으로 정렬하여 출력한다.
- [x] 당첨 내역을 출력한다.
- [x] 로또 결과에 대한 총 수익률을 출력한다.
  - [x] 수익률은 소수점 둘째 자리에서 반올림하여 출력한다.
- [x] 예외 상황 시 에러 문구를 출력한다.
  - [x] 에러 문구는 "[ERROR]"로 시작해야 한다.

---

## 🚨 과제 제출 전 체크 리스트
- [x] 요구 사항에 명시된 출력값 형식을 지켰는지 확인
  - [x] 예외 발생시 `[ERROR]`로 시작하는지 확인
  - [x] 수익률은 소수점 둘째 자리에서 반올림하는지 확인
  - [x] 로또 번호가 오름차순으로 정렬하여 출력하는지 확인
- [x] 모든 테스트가 성공하는지 확인
  - [x] `./gradlew clean test`가 정상 통과하는지 확인
  - [x] `ApplicationTest`가 정상 통과하는지 확인
  - [x] 도메인 로직에 단위 테스트를 구현했는지 확인
- [x] 자바 17버전으로 정상 작동되는지 확인
- [x] 프로그램 실행의 시작점이 Application의 main()인지 확인
- [x] indent depth가 3이 넘지 않는지 확인
- [x] 3항 연산자를 쓰지 않았는지 확인 
- [x] 함수의 길이가 15라인을 넘어가지 않도록 확인
- [x] else 예약어를 쓰지 않았는지 확인
  - [x] switch/case를 쓰지 않았는지 확인
- [x] Java Enum을 적용했는지 확인
- [x] `Randoms` 및 `Console` API를 사용했는지 확인
  - [x] Random 값 추출은 `camp.nextstep.edu.missionutils.Randoms` 사용했는지 확인
  - [x] 입력은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 사용했는지 확인
- [x] 로또 클래스 확인
  - [x] 로또 클래스의 numbers의 접근 제어자가 private인지 확인
  - [x] 로또 클래스의 인스턴스 변수를 추가했는지 확인(추가하는 것을 허용하지 않음)

이건 제가 3주 차 미션을 진행하기 이전 로또 게임 규칙구현할 기능 목록을 작성한 내용들입니다.

이전 주차와 달라진 것은, 이전 주차에는 기능 목록에서 클래스명까지 생각하고 작성하였습니다.

하지만 2주 차 공통 피드백에 아래와 같이 글이 작성되어있었습니다.

기능 목록을 클래스 설계와 구현, 함수(메서드) 설계와 구현과 같이 너무 상세하게 작성하지 않는다. 클래스 이름, 함수(메서드) 시그니처와 반환값은 언제든지 변경될 수 있기 때문이다...

마치 저의 기능 목록을 보고 공통 피드백을 작성하셨나 느꼈을만큼, 이전까지 기능 목록을 작성하는 방법이 잘못되었다는 것을 알게 되었습니다.

그래서 변경 가능한 클래스명이나 메서드명 등을 최대한 작성하지 않을 수 있도록 신경쓰며 작성해보았습니다.


핵심 기능

원시값 포장

보너스 번호에 대한 값을 검증하기 위해 원시값을 포장하여 미션을 진행하였습니다.

이번 미션에서 보너스 번호숫자 범위1~45까지였습니다.
이러한 보너스 번호에 대한 숫자 범위를 쉽게 검증하기 위해 BonusNumber라는 객체를 구현하여 이 안에 검증 관련 코드를 위치시켰습니다.

public class BonusNumber {
    private static final String OUT_OF_RANGE_NUMBER_EXCEPTION_FORMAT = "보너스 번호는 %d부터 %d 사이의 숫자여야 합니다.";
    private static final int MINIMUM_LOTTO_NUMBER = 1;
    private static final int MAXIMUM_LOTTO_NUMBER = 45;

    private final int bonusNumber;

    public BonusNumber(int bonusNumber) {
        validateBonusNumber(bonusNumber);
        this.bonusNumber = bonusNumber;
    }

    private void validateBonusNumber(int bonusNumber) {
        if (isOutOfRange(bonusNumber)) {
            throw new IllegalArgumentException(
                    String.format(OUT_OF_RANGE_NUMBER_EXCEPTION_FORMAT, MINIMUM_LOTTO_NUMBER, MAXIMUM_LOTTO_NUMBER));
        }
    }

    private boolean isOutOfRange(int bonusNumber) {
        return bonusNumber < MINIMUM_LOTTO_NUMBER || bonusNumber > MAXIMUM_LOTTO_NUMBER;
    }
}

물론, 기본 로또 번호 또한 숫자 범위1~45까지였지만 위에서 말씀드린대로 주어진 Lotto 클래스를 이용하여 구현해야되었기 때문에 로또 번호에 대한 원시값 포장은 따로 진행하지 않고, 로또 번호는 단순히 List<Integer> 타입을 이용하여 구현하였습니다.

이처럼 우아한테크코스 프리코스를 진행하며 느꼈던 것 중 하나는, 주어진 요구 사항을 읽다 보면 이 부분은 원시값을 포장하여 미션을 진행하면 좋겠다라는 생각을 자연스레 느낄 수 있었습니다!! 👍


검증

이번 미션에서 예외적인 부분을 계속해서 생각해보고, 작성해보았습니다.

이를 통해, 추려낼 수 있었던 검증 부분들은 다음과 같습니다.

1. 핵심 도메인 검증

  • 로또 검증 기능
    • [예외 처리] 로또 번호의 개수가 6개가 아니면 예외가 발생한다.
    • [예외 처리] 로또 번호의 숫자들 중 1보다 작거나 45보다 큰 숫자가 있다면 예외가 발생한다.
    • [예외 처리] 6개의 숫자 중 중복되는 숫자가 있다면 예외가 발생한다.
  • 로또 구입 금액 검증 기능
    • [예외 처리] 로또 구입 금액이 1,000원보다 작다면 예외가 발생한다.
    • [예외 처리] 로또 구입 금액이 1,000원으로 나누어 떨어지지 않으면 예외가 발생한다.
  • 당첨 로또 검증 기능
    • [예외 처리] 6개의 숫자 중 보너스 번호와 일치하는 숫자가 있으면 예외가 발생한다.

2. 올바른 입력 검증

  • [공통 예외 처리] 입력이 공백이면 예외가 발생한다.
  • 로또 구입 금액을 입력 받는다.
    • [예외 처리] 로또 구입 금액에 대한 입력이 숫자가 아니면 예외가 발생한다.
  • 당첨 번호를 입력 받는다.
    • [예외 처리] 당첨 번호에 대한 입력 형식이 올바르지 않으면 예외가 발생한다.
  • 보너스 번호를 입력 받는다.
    • [예외 처리] 보너스 번호에 대한 입력이 숫자가 아니면 예외가 발생한다.

다음과 같이 검증 처리를 핵심 도메인 부분입력 부분으로 나눈 이유는, 제 개인적인 생각으로 검증 처리는 view 영역domain 영역 모든 부분에서 하는 것이 옳다고 느껴졌기 때문입니다. view 영역에서는 입력 값이 올바른지 판단하기 위해 단순한 검증만 진행하고, domain 영역에서 핵심적인 검증 기능을 수행할 수 있도록 구현하였습니다.


Enum 활용

이번 미션에서 추가된 요구 사항Java Enum을 적용한다.라는 요구 사항이 있었습니다.

Enum 객체를 활용하기 위해 로또 등수에 대한 값을 Enum을 활용하여 구현해볼 수 있었습니다.

public enum LottoRanking {
    FIRST(6, false, 2000000000),
    SECOND(5, true, 30000000),
    THIRD(5, false, 1500000),
    FOURTH(4, false, 50000),
    FIFTH(3, false, 5000),
    NOTHING(0, false, 0);

    private static final int VALUE_TO_DETERMINE_SECOND_OR_THIRD = 5;

    private final int numberOfMatches;
    private final boolean hasBonusNumber;
    private final int prizeMoney;

    LottoRanking(int numberOfMatches, boolean hasBonusNumber, int prizeMoney) {
        this.numberOfMatches = numberOfMatches;
        this.hasBonusNumber = hasBonusNumber;
        this.prizeMoney = prizeMoney;
    }

    public static LottoRanking of(int numberOfMatches, boolean hasBonusNumber) {
        if (numberOfMatches != VALUE_TO_DETERMINE_SECOND_OR_THIRD) {
            return findRanking(numberOfMatches, false);
        }
        return findRanking(numberOfMatches, hasBonusNumber);
    }

    private static LottoRanking findRanking(int numberOfMatches, boolean hasBonusNumber) {
        return Arrays.stream(values())
                .filter(lottoRanking -> lottoRanking.isMatch(numberOfMatches, hasBonusNumber))
                .findFirst()
                .orElse(NOTHING);
    }

    private boolean isMatch(int numberOfMatches, boolean hasBonusNumber) {
        return this.numberOfMatches == numberOfMatches && this.hasBonusNumber == hasBonusNumber;
    }
}

다음과 같이 로또와 일치하는 숫자 개수와 보너스 번호 일치 여부를 이용하여 로또 등수를 매길 수 있도록 구현하였습니다.

여기서 주의한 점은, 보너스 번호 일치 여부는 2등과 3등일 경우만 이를 구분하기 위해 필요하고, 다른 경우는 보너스 번호 일치 여부는 중요하지 않습니다.

이를 위해, 다음과 같이 구현해볼 수 있었습니다.

public static LottoRanking of(int numberOfMatches, boolean hasBonusNumber) {
	if (numberOfMatches != VALUE_TO_DETERMINE_SECOND_OR_THIRD) {
		return findRanking(numberOfMatches, false);
	}
	return findRanking(numberOfMatches, hasBonusNumber);
}

로또와 일치하는 숫자 개수가 5가 아닐 경우는 findRanking() 메서드에 false 값을 전달하고, 맞을 경우는 전달된 hasBonusNumber 값을 그대로 사용하여 로또 등수를 구하도록 구현하였습니다.


상수 활용

2주 차 공통 피드백에서 다음과 같은 말이 명시되어 있었다.

문자열, 숫자 등의 값을 하드 코딩하지 마라. 상수(static final)를 만들고 이름을 부여해 이 변수의 역할이 무엇인지 의도를 드러내라...

이를 통해 이전까지는 예외 메시지 등을 상수로 처리하지 않았더라면, 이번 미션에서는 모든 예외 메시지까지 상수로 처리할 수 있도록 구현하였습니다.

// 로또 클래스 예외 메시지 예시
private static final String INVALID_NUMBERS_SIZE_EXCEPTION_FORMAT = "로또는 총 %d개의 번호로 이루어져야 합니다.";
private static final String OUT_OF_RANGE_NUMBER_EXCEPTION_FORMAT = "로또 번호는 %d부터 %d 사이의 숫자여야 합니다.";
private static final String DUPLICATED_NUMBER_EXCEPTION_MESSAGE = "로또 번호들 중 중복된 숫자가 존재합니다.";

테스트 코드

모든 도메인 로직에 대한 테스트 코드를 작성하기 위해 노력했습니다.

이전 미션에서 테스트 코드를 통해 오류를 쉽게 찾을 수 있었다는 장점을 느꼈다면, 이번 미션에서는 리팩토링 과정에서 여러 구현 방법을 적용하면서, 놓친 부분들을 테스트 코드의 오류를 통해 쉽게 감지할 수 있다는 장점을 느낄 수 있었습니다.


마치며

벌써 절반 이상을 진행하며 벌써 아쉽기도 하며, 한편으로 목표를 가지고 성장할 수 있는 기회를 준 우테코에게 감사의 말을 전하고 싶다!! 😀

저의 자세한 코드가 궁금하신 분은 아래 링크에서 확인할 수 있습니다.
https://github.com/jcoding-play/java-lotto-6/tree/gyungchan


최종 리팩토링 후 달라진 부분

이 부분은 프리코스가 끝나고 난 뒤 리팩토링을 진행하고 이전과 크게 달라진 점을 알려드리기 위해 작성해보았습니다!!

원시값 포장

이번 미션에서는 제공된 Lotto 클래스를 활용하도록 명시되어 있었기 때문에, 로또 번호에 대한 원시값 포장을 진행하지 못하였었다.

하지만 지금은 혼자 리팩토링을 진행하는 것이기에 로또 번호에 대한 값도 LottoNumber라는 객체를 이용하여 원시값을 포장하도록 구현하였다.

public class Lotto {
	// 바뀐 부분
    private final List<LottoNumber> numbers;

    public Lotto(List<Integer> numbers) {
        validate(numbers);
        this.numbers = mapLottoNumber(numbers);
    }

    private List<LottoNumber> mapLottoNumber(List<Integer> numbers) {
        return numbers.stream()
                .map(LottoNumber::new)
                .toList();
    }
    
    ...
}
public class LottoNumber {
	private static final int MINIMUM_LOTTO_NUMBER = 1;
    private static final int MAXIMUM_LOTTO_NUMBER = 45;

    private final int lottoNumber;

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

    private void validateLottoNumber(int lottoNumber) {
        if (lottoNumber < MINIMUM_LOTTO_NUMBER || lottoNumber > MAXIMUM_LOTTO_NUMBER) {
            throw new IllegalArgumentException(
                    String.format("로또 번호는 %d부터 %d 사이의 숫자여야 합니다.", MINIMUM_LOTTO_NUMBER, MAXIMUM_LOTTO_NUMBER));
        }
    }
}

또한 로또 구입 금액에 대한 값도 원시값을 포장하여 구현할 수 있도록 리팩토링하였다.

이전에는 로또 구입 금액에 대한 값을 검증하기 위해 LottoStore 클래스에 buyLotto 메서드 내부에서 검증을 진행하였다.

// 리팩토링 전 LottoStore 클래스
public class LottoStore {
    private static final String LESS_THAN_MINIMUM_PRICE_EXCEPTION_FORMAT = "로또 구입 금액은 최소 %d원 이상이어야 합니다.";
    private static final String INVALID_PURCHASE_AMOUNT_EXCEPTION_FORMAT = "로또 구입 금액은 %d원 단위어야 합니다.";

    private static final int VALID_REMAINING_AMOUNT = 0;

    private final LottoGenerator lottoGenerator;

    public LottoStore(LottoGenerator lottoGenerator) {
        this.lottoGenerator = lottoGenerator;
    }

    public List<Lotto> buyLotto(int purchaseAmount) {
        validatePurchaseAmount(purchaseAmount);

        return lottoGenerator.createLottos(purchaseAmount / LOTTO_PRICE);
    }

    private void validatePurchaseAmount(int purchaseAmount) {
        if (purchaseAmount < LOTTO_PRICE) {
            throw new IllegalArgumentException(
                    String.format(LESS_THAN_MINIMUM_PRICE_EXCEPTION_FORMAT, LOTTO_PRICE));
        }
        if (isInvalidPurchaseAmount(purchaseAmount)) {
            throw new IllegalArgumentException(
                    String.format(INVALID_PURCHASE_AMOUNT_EXCEPTION_FORMAT, LOTTO_PRICE));
        }
    }

    private boolean isInvalidPurchaseAmount(int purchaseAmount) {
        return purchaseAmount % LOTTO_PRICE != VALID_REMAINING_AMOUNT;
    }
}

하지만 이를 다음과 같이 PurchaseAmount라는 객체를 이용하여 구입 금액에 대한 검증 기능을 위치시키고, LottoStore 클래스 내부에서는 이와 관련된 기능만을 수행하도록 리팩토링하였다.

public class PurchaseAmount {
    private final int purchaseAmount;

    public PurchaseAmount(int purchaseAmount) {
        validatePurchaseAmount(purchaseAmount);
        this.purchaseAmount = purchaseAmount;
    }

    private void validatePurchaseAmount(int purchaseAmount) {
        if (purchaseAmount < Constants.LOTTO_PRICE) {
            throw new IllegalArgumentException(
                    String.format("구입 금액은 최소 %,d원 이상이어야 합니다.", Constants.LOTTO_PRICE));
        }
        if (isInvalidPurchaseAmount(purchaseAmount)) {
            throw new IllegalArgumentException(
                    String.format("구입 금액은 %,d원 단위어야 합니다.", Constants.LOTTO_PRICE));
        }
    }
}
// 리팩토링 후 LottoStore 클래스
public class LottoStore {

    public Lottos buyLotto(PurchaseAmount purchaseAmount) {
        int result = purchaseAmount.divideByLottoPrice();
        return generateLottos(result);
    }

    private Lottos generateLottos(int size) {
        return Stream.generate(this::generateLotto)
                .limit(size)
                .collect(Collectors.collectingAndThen(Collectors.toList(), Lottos::new));
    }

    private Lotto generateLotto() {
        return new Lotto(pickUniqueNumbers());
    }

    private List<Integer> pickUniqueNumbers() {
        return Randoms.pickUniqueNumbersInRange(MINIMUM_LOTTO_NUMBER, MAXIMUM_LOTTO_NUMBER, LOTTO_NUMBERS_SIZE);
    }
}

함수형 인터페이스 활용

이전에 로또 등수를 구하기 위해 if 문을 이용하여 구할 수 있었다.

이를 깔끔하게 변경하기 위한 방법을 찾아보다가 BiPredicate라는 함수형 인터페이스를 적용하게 되었습니다.

@FunctionalInterface
public interface BiPredicate<T, U> {
    boolean test(T t, U u);
}    

이는 2개의 인자를 통해 참, 거짓을 반환해주는 함수형 인터페이스입니다.

BiPredicate을 적용함으로 if문을 이용하여 등수를 구했던 이전과 달리 간단히 로또 등수를 구할 수 있었습니다.

// 변경 전 LottoRanking
public enum LottoRanking {
    FIRST(6, false, 2000000000),
    SECOND(5, true, 30000000),
    THIRD(5, false, 1500000),
    FOURTH(4, false, 50000),
    FIFTH(3, false, 5000),
    NOTHING(0, false, 0);

    private static final int VALUE_TO_DETERMINE_SECOND_OR_THIRD = 5;

    private final int numberOfMatches;
    private final boolean hasBonusNumber;
    private final int prizeMoney;

    LottoRanking(int numberOfMatches, boolean hasBonusNumber, int prizeMoney) {
        this.numberOfMatches = numberOfMatches;
        this.hasBonusNumber = hasBonusNumber;
        this.prizeMoney = prizeMoney;
    }

    public static LottoRanking of(int numberOfMatches, boolean hasBonusNumber) {
        if (numberOfMatches != VALUE_TO_DETERMINE_SECOND_OR_THIRD) {
            return findRanking(numberOfMatches, false);
        }
        return findRanking(numberOfMatches, hasBonusNumber);
    }

    private static LottoRanking findRanking(int numberOfMatches, boolean hasBonusNumber) {
        return Arrays.stream(values())
                .filter(lottoRanking -> lottoRanking.isMatch(numberOfMatches, hasBonusNumber))
                .findFirst()
                .orElse(NOTHING);
    }

    private boolean isMatch(int numberOfMatches, boolean hasBonusNumber) {
        return this.numberOfMatches == numberOfMatches && this.hasBonusNumber == hasBonusNumber;
    }
}
// 변경 후 LottoRanking
public enum LottoRanking {
    FIRST((numberOfMatches, hasBonusNumber) -> numberOfMatches == 6, 2_000_000_000),
    SECOND((numberOfMatches, hasBonusNumber) -> numberOfMatches == 5 && hasBonusNumber, 30_000_000),
    THIRD((numberOfMatches, hasBonusNumber) -> numberOfMatches == 5, 1_500_000),
    FOURTH((numberOfMatches, hasBonusNumber) -> numberOfMatches == 4, 50_000),
    FIFTH((numberOfMatches, hasBonusNumber) -> numberOfMatches == 3, 5_000),
    NOTHING((numberOfMatches, hasBonusNumber) -> numberOfMatches == 0, 0);

    private final BiPredicate<Integer, Boolean> predicate;
    private final int prizeMoney;

    LottoRanking(BiPredicate<Integer, Boolean> predicate, int prizeMoney) {
        this.predicate = predicate;
        this.prizeMoney = prizeMoney;
    }

    public static LottoRanking of(int numberOfMatches, boolean hasBonusNumber) {
        return Arrays.stream(values())
                .filter(lottoRanking -> lottoRanking.isMatch(numberOfMatches, hasBonusNumber))
                .findFirst()
                .orElse(NOTHING);
    }

    private boolean isMatch(int numberOfMatches, boolean hasBonusNumber) {
        return predicate.test(numberOfMatches, hasBonusNumber);
    }
}
profile
한걸음씩 성장하는 개발자

0개의 댓글