(우테코 7기 프리코스) 3주차

2024-10-17-215628.png

3주차 회고

이번 로또 과제를 진행하면서 저번 과제 보다는 많은 요구사항들이 있었고 그만큼 흐름잡기나 각 클래스의 책임을 부여하는 과정들이 오래 걸렸고 요구사항들을 꼼꼼히 분석하지 못하여 예외를 잡는 부분등을 고려하지 않고 개발을 했다가 큰코를 다쳐가며 ㅋㅋㅋㅋ 열심히 구현을 해봤다.

코드의 흐름도 2024-11-04-231040.png

Controller

프로그램의 전반적인 진행 흐름을 제어하는 역할을 한다.

  • LottoMachinController
    • 전체 게임 흐름을 제어한다.
1
2
3
4
5
6
public void run() {  
    CustomerLotto customerLotto = createCustomerLotto();  
    printCustomerLotto(customerLotto);  
    WinningLotto winningLotto = createWinningLotto();  
    displayLottoResults(customerLotto, winningLotto);  
}

View

컨트롤러로부터 데이터를 넘겨받아 출력하는 OutputView, 사용자로 부터 데이터를 입력 받아 컨트롤러로 넘기는 InputView

Model

상태를 변경 관리 조회를 담당하는 역할을 한다.

  • Lotto
    • Lotto 번호를 가지고 있고, 로또와 관련된 예외 검증 로직과, 파라미터로 들어오는 번호와 몇개 일치하는지 반환하는 로직 포함
  • BonusNumber
    • int value를 가지고 있는 값 객체, 보너스 번호와 관려된 예외 검증 로직검증 포함
  • WinningLotto
    • Lotto와 BonusNumber를 가지고 있고, 로또 번호와 당첨 번호를 비교하여 결과를 반환하는 역할을 맞고 있다.
  • CustomerLotto
    • 고객의 로또들을 가지고 있는 일급 컬렉션으로 당첨 로또를 고객의 로또 티켓들과 비교하는 역할을 한다. 내부에서 stream을 돌면서 WinningLotto:checkLotto를 수행하여 LottoResults를 반환한다.

Constance

  • Rank
    • 각 랭크에 맞는 일치 개수(ex 1등은 6개 모두 일치), 보너스 번호 일치 여부, 상금, 해당 랭크이기 위한 조건을 가지는 enum 클래스

공부 한 내용

1. BiFunction<> 사용

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
public enum Rank {  
    FIRST(6, false, 2_000_000_000, (count, bonus) -> count == 6),  
    SECOND(5, true, 30_000_000, (count, bonus) -> count == 5 && bonus),  
    THIRD(5, false, 1_500_000, (count, bonus) -> count == 5 && !bonus),  
    FOURTH(4, false, 50_000, (count, bonus) -> count == 4),  
    FIFTH(3, false, 5_000, (count, bonus) -> count == 3),  
    NONE(0, false, 0, (count, bonus) -> count <= 2);  
  
    private final int matchingNumber;  
    private final boolean isMatchingBonusNumber;  
    private final long prizeMoney;  
    private final BiFunction<Integer, Boolean, Boolean> criteria;  
  
    Rank(int matchingNumber, boolean isMatchingBonusNumber, long prizeMoney,  
         BiFunction<Integer, Boolean, Boolean> criteria) {  
        this.matchingNumber = matchingNumber;  
        this.isMatchingBonusNumber = isMatchingBonusNumber;  
        this.prizeMoney = prizeMoney;  
        this.criteria = criteria;  
    }  
  
    /**  
     * 현재 랭크가 주어진 조건(일치한 번호 개수와 보너스 번호 일치 여부)에 부합하는지 체크  
     *  
     * @param matchedCount 일치한 번호 개수  
     * @param bonus        보너스 번호일치 여부  
     * @return 기준에 부합하다면 True, 부합하지 않다면 False  
     */    public boolean matchesCriteria(int matchedCount, boolean bonus) {  
        return criteria.apply(matchedCount, bonus);  
    }

이번 과제 구현시 Rank enum 클래스에서 BiFuntion이라는 함수형 인터페이스를 사용했다.

2개의 인자를 받아 BiFuntion에 구현된 내용이 수행된다.

각 랭크별로 조건들을 지정해두고 해당 조건이 만족하면 True를 반환하고 아니라면 False를 반환한다.

1
2
3
4
SECOND(5, true, 30_000_000, (count, bonus) -> count == 5 && bonus),

boolean result1 = SECOND.matchesCriteria(5, ture); // true
boolean result2 = SECOND.mathcesCriteria(5, false); //false
  • SECOND는 (count, bonus) -> count == 5 && bonus일 때 true를 반환한다.

2. @ParameterizedTest -> @MethodSource 사용

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
46
47
48
49
50
@ParameterizedTest(name = "{0}")  
@MethodSource("provideLottoRankTestCases")  
@DisplayName("로또 결과를 검증시")  
void rankingTest(String description, List<Integer> customerLottoNumbers, int bonusNumber, Rank expectedRank) {  
    // given  
    BonusNumber bonusNum = BonusNumber.of(bonusNumber, winningLottoNumbers);  
    WinningLotto winningLotto = WinningLotto.of(ticket, bonusNum);  
    Lotto newTicket = Lotto.of(customerLottoNumbers);  
  
    // when  
    LottoResult result = winningLotto.checkLotto(newTicket);  
  
    // then  
    assertThat(result.getRank()).isEqualTo(expectedRank);  
}

private static Stream<Arguments> provideLottoRankTestCases() {  
    return Stream.of(  
            // NONE 랭크  
            Arguments.of("로또 번호 0개 맞고 보너스도 맞지 않을때 None 랭크를 반환한다"  
                    , java.util.List.of(7, 8, 9, 10, 11, 12), 13, Rank.NONE),  
            Arguments.of("로또 번호 1개 맞고 보너스도 맞지 않을때 None 랭크를 반환한다"  
                    , java.util.List.of(6, 7, 8, 9, 10, 11), 12, Rank.NONE),  
            Arguments.of("로또 번호 1개 맞고 보너스 번호 맞을때 None 랭크를 반환한다"  
                    , List.of(6, 7, 8, 9, 10, 11), 7, Rank.NONE),  
            Arguments.of("로또 번호 2개 맞고 보너스도 맞지 않을때 None 랭크를 반환한다"  
                    , List.of(5, 6, 7, 8, 9, 10), 11, Rank.NONE),  
            Arguments.of("로또 번호 2개 맞고 보너스 번호 맞을때 None 랭크를 반환한다"  
                    , List.of(5, 6, 7, 8, 9, 10), 7, Rank.NONE),  
            // FIFTH 랭크  
            Arguments.of("로또 번호 3개 맞고 보너스도 맞지 않을때 FIFTH 랭크를 반환한다"  
                    , List.of(4, 5, 6, 7, 8, 9), 10, Rank.FIFTH),  
            Arguments.of("로또 번호 3개 맞고 보너스 번호 맞을때 FIFTH 랭크를 반환한다"  
                    , List.of(4, 5, 6, 7, 8, 9), 7, Rank.FIFTH),  
            // FOURTH 랭크  
            Arguments.of("로또 번호 4개 맞고 보너스도 맞지 않을때 FOURTH 랭크를 반환한다"  
                    , List.of(3, 4, 5, 6, 7, 8), 9, Rank.FOURTH),  
            Arguments.of("로또 번호 4개 맞고 보너스 번호 맞을때 FOURTH 랭크를 반환한다"  
                    , List.of(3, 4, 5, 6, 7, 8), 8, Rank.FOURTH),  
            // THIRD 랭크  
            Arguments.of("로또 번호 5개 맞고 보너스도 맞지 않을때 THIRD 랭크를 반환한다"  
                    , List.of(2, 3, 4, 5, 6, 7), 8, Rank.THIRD),  
            // SECOND 랭크  
            Arguments.of("로또 번호 5개 맞고 보너스 번호 맞을때 SECOND 랭크를 반환한다"  
                    , List.of(2, 3, 4, 5, 6, 7), 7, Rank.SECOND),  
            // FIRST 랭크  
            Arguments.of("로또 번호 6개 맞을때 FIRST 랭크를 반환한다"  
                    , List.of(1, 2, 3, 4, 5, 6), 7, Rank.FIRST)  
    );  
}

@MethodSource(“함수명”) private static Stream<Arguments> provideLottoRankTestCases(){} 사전에 지정해둔 함수안에 Stream.of(Argument.of())를 사용하여 테스트의 파라미터로 매핑이 되어 테스트를 진행한다.

많은 중복되는 내용을 테스트 할 때 각각을 따로 만드는 방식보다 효율적이다. 2024-11-05-175307.png

3. 의도적인 분리가 아닌 분리해야 할 때 클래스 분리

개인적으로 저는 클래스로 분리할 때는 한 클래스가 너무 많은 역할을 가지고 있거나, 테스트 코드를 작성할 때 테스트 하고 싶은 기능이 있을 때 해당 기능이 private 메서드 일때 클래스를 분리하는게 어떨까?라고 생각을 하는 편이다

기존 WinningLotto는

1
2
3
4
5
6
7
8
9
private final Lotto ticket;  
private final int bonusNumber;

private WinningLotto(Lotto ticket, int bonusNumber) {  
    validateRange(bonusNumber);  
    this.ticket = ticket;  
    validateDuplicate(bonusNumber);  
    this.bonusNumber = bonusNumber;  
}

이렇게 구현이 되어있었다. 하지만 검증하는 부분을 보면 뭔가 이상하다… 분명 WinningLotto 클래스 만드는 부분인데 bonusNumber에 대해서만 검증을 진행하고 있다. Lotto는 이미 검증이 끝난 상태이다. WinningLotto에서 bonusNumber에 대해 검증을 하고 있다? 아 분리해야겠다

1
2
3
4
5
6
7
private final Lotto ticket;  
private final BonusNumber bonusNumber;  
  
private WinningLotto(Lotto ticket, BonusNumber bonusNumber) {  
    this.ticket = ticket;  
    this.bonusNumber = bonusNumber;  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BonusNumber {  
    public static final int MIN_RANGE = 1;  
    public static final int MAX_RANGE = 45;  
    public static final String BONUS_NUMBER_RANGE_ERROR_MESSAGE = "[ERROR] 보너스 번호는 %d와 %d사이 숫자여야합니다.";  
    public static final String BONUS_NUMBER_DUPLICATE_MESSAGE = "[ERROR] 보너스 번호는 로또 번호와 중복될 수 없습니다.";  
  
    private final int value;  
  
    private BonusNumber(int value, List<Integer> ticket) {  
        validate(value, ticket);  
        this.value = value;  
    }  
  
    public static BonusNumber of(int value, List<Integer> ticket) {  
        return new BonusNumber(value, ticket);  
    }  
  
    private void validate(int value, List<Integer> ticket) {  
        validateRange(value);  
        validateDuplicate(value, ticket);  
    }

이렇게 분리하니 BonusNumber 클래스에서 검증을 진행할 수 있게 되었고 예외 사항 테스트 하기에도 유리하게 바뀌었다.

3주차 소감문

1
2
3
2주차때 많은 분들에 피드백을 받을 수 있었다. 그래서 이번 3주차 과제를 진행할때 공통 피드백과 더불어 2주차때 받은 피드백을 최대한 반영하려고 노력을 했다. 의식적으로 신경 써서 구현을 하려고 하니 생각보다 많은 시간이 걸렸다. 특히 서로 코드리뷰를 묻고 답하는 과정에서 느꼈던 객체의 책임을 정하는 기준과 메서드가 한가지 기능을 할 수 있도록 추상화의 깊이를 신경써야겠다는 생각을 하게 되었고, 이번 과제에서 객체의 책임 메서드의 책임을 어떻게 더 효과적으로 분리 해낼수 있을까를 고민하며 여러번 기능목록을 수정하였고, 프로젝트의 구조도 많이 바꿨다. 이렇게 고민하고 수정하고 다시 고민하고를 반복하니 어떻게 설계를 하는것이 효율적이고 읽기좋은 코드가 되는지를 조금은 알게 되었다. 1주차의 나보다 성장을 했구나라는 생각을 하게 되었고 기분이 좋았다. 이렇게 코드리뷰, 피드백을 받으며 이를 적용해보면서 얻는 장점들을 이번주차에 느꼈기에 얼른 이번 주차 코드도 많은 피드백을 받아보고 발전하고 싶다는 생각이 들었다.
이번 과제를 수행하면서 저번 과제보다 다소 복잡한 요구사항들로 어떻게 구현을 해야할지 흐름을 파악하기위해 흐름도를 작성을 하였고 이를 통해 필요한 클래스들을 알 수 있었고, 자연스럽게 어디서 예외 처리를 해야하는지 어디서 어떤 기능을 구현해야하는지 감이 잡혔다. 하지만 로또를 해본적이 없기에 도메인에 대한 지식이 없는 상태에서 구현을 하다보니 몇번 구현을 다시 한 적이 있었다. 가령 보너스 번호가 일치하는 부분에서 4개 번호가 일치한 상태에서 보너스 번호까지 일치하면 5개 번호가 일치하여 3등으로 출력되도록 구현을 했었다. 도메인에 대한 이해가 중요하다는 것을 알 수 있었다.
이번 과제를 진행하면서 디스코드 함께 나누기에도 공유한 블로그 내용이 있었는데 오브젝트의 저자이신 조영호님의 블로그중 설계 트레이드오프라는 제목의 글을 보았다. 설계를 선택할때 얻는 것이 있으면 잃는 것이 있고, 이를 받아들이는 것이 설계 트레이드오프를 이해하는 첫걸음이다. 많은 설꼐 원칙들은 좋은 설계를 만들 확률을 높일 수 있는 가이드일 뿐이지 반드시 따라야 하는 법칙이 아니다라는 문구가 마음에 와닿았다. 기존에 나는 어떻게든 남들의 구조를 따라하기에 바빴고, 그 이유에 대해서 깊은 고민을 하지 않고 적용하려고 했던것 같다. 주어진 환경에서 너무 과도한 추상화나 유연성을 높여 얻는 이익에만 집중하지 않아야겠다라는 생각을 하게 되었고 그만큼 설계에 대해 그 이유를 찾으려 노력을 했다.