영호

우아한테크코스 5기 프리코스 3주차 후기 본문

우아한테크코스5기

우아한테크코스 5기 프리코스 3주차 후기

0h0 2022. 11. 16. 15:59

개요

3주 차 미션에서는 로또 게임이 나왔습니다. 미션을 진행하면서 있었던 일들에 대해 작성해보겠습니다.

 

신경 썼던 점

  • 자율적인 객체 만들기
  • 테스트하기 좋은 코드 작성
  • 연관관계와 의존관계 분리

자율적인 객체

캡슐화를 위해 객체가 자신이 가지고 있는 정보에 대한 책임을 가지게 함으로써 응집도를 높이고 결합도를 낮추고 싶었습니다. 왜냐하면 프리코스를 진행하면서 읽은 책, 구글링, 세미나 영상 등 모두 좋은 객체지향 설계를 위해서는 응집도를 높이고 결합도를 낮춰야 한다는 내용을 설명하며 캡슐화가 빠지지 않았습니다. 물론 2주 차 미션에서도 해당 내용을 지키기 위해서 노력했지만, 지속적인 노력을 통해 실력을 키우고 싶었습니다.

 

 이 과정에서 로또 당첨 순위를 판별하는 책임을 전체 로또 정보를 가지고 있는 EntireLotto에 부여해야 하는지, 당첨 번호 정보를 알고 있는 WinningNumbers에 부여해야 하는지 고민했습니다. 이 과정에서 현재 객체들이 어떤 식으로 협력 하는지 관계도를 그리고 이렇게 해보고 저렇게 해보고 계속 고민을 했습니다.

고민한 결과 당첨 순위를 판별하기 위해서는 각각의 Lotto에 접근해야 하는데, EntireLotto는 이미 Lotto에 접근하고 있기 때문에 EntireLotto에서 당첨 순위를 판별하는 책임을 수행하도록 접근했습니다.

 그 결과 아래와 같은 코드를 작성했습니다.

public void judgementEntireLottoWinning(WinningNumbers winningNumbers, RankingCount rankingCount) {
    for (Lotto lotto : entireLotto) {
        int correctLottoNumberCount = calculateContainsWinningNumbers(lotto, winningNumbers);
        boolean isBonus = calculateHasBonusNumber(lotto, winningNumbers);
        applyLottoRank(correctLottoNumberCount, isBonus, rankingCount);
    }
}

private int calculateContainsWinningNumbers(Lotto lotto, WinningNumbers winningNumbers) {
    int count = 0;
    for (int index = 0; index < Constant.CORRECT_LOTTO_SIZE; index++) {
        int lottoNumber = lotto.getIndexLottoNumber(index);
        if (winningNumbers.contains(lottoNumber)) {
            count += 1;
        }
    }
    return count;
}

private boolean calculateHasBonusNumber(Lotto lotto, WinningNumbers winningNumbers) {
    for (int index = 0; index < Constant.CORRECT_LOTTO_SIZE; index++) {
        int lottoNumber = lotto.getIndexLottoNumber(index);
        if (winningNumbers.hasBonusNumber(lottoNumber)) {
            return true;
        }
    }
    return false;
}

private void applyLottoRank(int correctLottoNumberCount, boolean isBonus, RankingCount rankingCount) {
    for (Ranking value : Ranking.values()) {
        if (value.getCorrectNumberCount() == correctLottoNumberCount && value.isBonus() == isBonus) {
            rankingCount.plusRankingCount(value.name());
        }
    }
}
  • 각각의 로또를 돌면서 winningNumber와 비교하며 당첨번호와 몇 개가 겹치는지, 보너스 번호가 맞는지 확인합니다.
  • 이후 당첨됐다면 <순위, count>로 구성된 rankingCount객체에게 해당 순위의 count를 증가시키는 메시지를 보냅니다.

 

 이렇게 데이터와 책임을 묶다 보니 자연스럽게 domain이라는 패키지에 분리할 수 있는 객체들이 생겼고, 전체적인 요청을 받고 적절한 domain객체에 요청을 보내는 controller도 만들게 됐습니다. 또한 요구사항에 있는 view기능도 분리하다 보니 자연스럽게 MVC패턴의 형태를 띄게 된 거 같습니다. 사실 MVC패턴에 대해 알고 있긴 했지만, 이를 의도적으로 적용하려 하진 않았습니다. 단지 객체들의 분리에 초점을 맞췄고, view분리라는 요구사항을 만족하기 위해 계속해서 고민하다 보니 MVC형태를 띄고 있는 것이 신기했습니다.


테스트하기 좋은 코드

지난 2주차 미션을 진행하면서 다음 미션에는 테스트하기 좋은 코드를 작성하고 싶었습니다. 그래서 어떻게 해야 테스트하기 좋은 코드를 짤 수 있고, 테스트하기 어려운 코드는 무엇인지에 대해 찾아봤습니다.

테스트하기 나쁜 코드

외부 상태에 의존하는 코드입니다. 예를 들어 A메서드는 현재 시간을 기준으로 어떤 값을 도출합니다. 하지만 시간을 계속 흐르기 때문에 A메서드는 실행되는 시간에 따라 항상 다른 결과가 도출될 것입니다. 그리고 사용자의 입력에 따라 처리하는 메서드도 마찬가지입니다.

테스트하기 나쁜 코드의 영향

A메서드가 테스트할 수 없는 구조로 구현됐다고 가정합시다. B메서드에서 A를 사용하고, C메서드에서 B메서드를 사용한다고 생각해보면, 테스트할 수 없는 C메서드로 인해 A,B메서드 모두 테스트 할 수 없게 됩니다.

나의 해결 방법

로또 게임에서 사용자의 입력을 받아 값을 생성하는 구매금액, 당첨번호의 생성자에서 값을 입력받는 것이 아닌 이를 parameter로 받게 변경했습니다. 이런 방법을 통해, 인스턴스 생성 시 해당 값들을 제가 임의로 설정해 테스트가 유용한 코드를 작성할 수 있었습니다.


연관관계 vs 의존관계

저는 그동안 연관관계와 의존관계에 대한 차이점을 모르고 모든 협력관계에 있는 객체를 인스턴스 변수로 선언했습니다. 그러나 객체 지향 설계에 대해 계속 찾아보고, 책을 읽고, 세미나 영상을 찾아보니 연관관계와 의존관계에 차이점이 있다는 것을 알았습니다.

 

 차이점을 알고 제 코드를 보니 모두 연관관계로 이루어져 있었고, 저는 해당 객체와의 협력이 하나의 public메서드에서만 쓰인다면 의존관계로 분리하자.’라는 기준을 세우고 리팩토링을 진행했습니다.

 

 그 결과 대부분 클래스의 인스턴스 변수를 1~3개 사이로 유지할 수 있었고, 이로 인해 클린 코드에 한 발짝 다가갔다고 생각해 신기했습니다.

 

개선할 점

EnumMap

이번에 Enum을 사용하면서 당첨 순위 별 상금, 맞춰야 하는 번호 개수, 보너스 번호 여부를 작성했습니다. 그리고 처음에는 여기에 count라는 값을 해당 순위에 당첨된 인원을 관리하고자 하는 의미로 추가했지만, 이는 상수의 개념이 아니기 때문에 별도의 map으로 관리하고자 했습니다.

 하지만, 저는 EnumMap개념을 모르고 아래 Ranking enum의 상수값을 map자료구조를 가지고 있는 RankingCount에 key값으로 설정했습니다. 

public class RankingCount {
    private Map<String, Integer> rankingCount = new LinkedHashMap<>();

    public RankingCount() {
        for (Ranking ranking : Ranking.values()) {
            rankingCount.put(ranking.name(), 0);
        }
    }
}
public enum Ranking {
    _5TH(3, false, 5_000, "3개 일치 (5,000원)"),
    _4TH(4, false, 50_000, "4개 일치 (50,000원)"),
    _3RD(5, false, 1_500_000, "5개 일치 (1,500,000원)"),
    _2ND(5, true, 30_000_000, "5개 일치, 보너스 볼 일치 (30,000,000원)"),
    _1ST(6, false, 2_000_000_000, "6개 일치 (2,000,000,000원)");

    private int correctNumberCount;
    private boolean isBonus;
    private int price;
    private String printFormat;

    Ranking(int correctNumberCount, boolean isBonus, int price, String printFormat) {
        this.correctNumberCount = correctNumberCount;
        this.isBonus = isBonus;
        this.price = price;
        this.printFormat = printFormat;
    }

    public int getCorrectNumberCount() {
        return correctNumberCount;
    }

    public boolean isBonus() {
        return isBonus;
    }

    public String getPrintFormat() {
        return printFormat;
    }

    public int getPrice() {
        return price;
    }
}

그러나 피어 리뷰를 진행하면서 받은 공통적인 피드백이 해당 부분을 EnumMap을 활용하는 것이 더 좋아 보인다는 것이었습니다. 그래서 EnumMap에 대해 찾아보니 제가 고민하면서 별도의 map으로 분리했던 문제에 대한 해결책이 바로 EnumMap이었습니다. 

 

그동안 코드 리뷰를 경험하고 싶었는데 이렇게 경험하자마자 새로운 지식을 얻을 수 있어서 너무 좋았고, 같이 성장하는 느낌을 받을 수 있었습니다.

Java기본기

이 부분 역시 피어 리뷰를 받으면서 아직 Java기본기가 부족하다고 느꼈습니다. 저는 map에서 value를 뽑을 때 key가 없어서 발생하는 에러를 방지하기 위해 생성자에서 모든 순위에 대해 value를 0으로 초기화하는 과정을 거쳤습니다. 하지만 이는 map.getOrDefault로 방지할 수 있는 부분이라는 피드백을 받았습니다.

 

 그리고 StringBuffer를 이용하면서 개행에 있어 append("\n")을 사용했습니다. 그러나 이 부분 역시 StringJoiner로 해결할 수 있는 부분임을 알게 됐습니다.

 

 또한, stream의 allMatch, anyMatch 등의 기능을 이용해도 for... if를 줄일 수 있다는 점을 알게 됐습니다.

 

이렇게 받은 피드백들 중 하나라도 다음 미션에서 제대로 보완하고 싶은 마음이 들었습니다

한 가지만 하기

기능 구현을 할 때는 리팩토링 요소가 보여도 참아야겠다는 생각을 했습니다. 자꾸 기능 구현하다가 리팩토링을 하니 한 번에 여러 개의 수정요소가 생겨서 기능별 커밋도 어려워졌습니다. 그리고 중간에 자꾸 코드가 변경되니 내가 지금 뭘 하고 있었지 라는 생각이 들면서 길을 잃곤 했습니다. 

 그래서 다음 미션에서는 아무리 리팩토링 요소가 보여도 기능 구현 시에는 잠시 접어두고 기능 구현 완료 후에 리팩토링을 진행을 해봐야겠습니다.

다양한 입력값으로 테스트해보기

 다양한 예외 입력이 생각이 났는데 안 했다기보다는, 생각을 못한 부분이긴 합니다. 그래도 계속해서 예외 사항에 대해 생각해보고 터무니없는 값을 넣어보면서 최대한 요구사항에 맞게 구현해야겠다는 생각을 했습니다.

 

 예를 들어, 수익률의 경우 저는 모든 총상금을 다 더한 뒤, 수익률을 계산했습니다. 아주 희박한 확률이지만 1등과 2등이 당첨되면 int범위를 벗어나기 때문에 overflow에러가 발생합니다. 그래서 저는 자료형을 int보다 큰 것으로 바꾸기보단 순위 별로 당첨이 될 때마다 해당 수익률을 누적시켜 가는 방식으로 다양한 상황에 대해 생각해봤습니다.

 만약 1등이 2번 당첨됐다고 가정을 하면 20억에 대한 수익률을 구하고, 다시 한번 20억에 대한 수익률을 구하는 방식으로 구현했습니다. 그러나 구매 금액이 int범위를 넘을 수도 있다는 생각은 제출기간이 지나고 나서야 생각났습니다.

 

다음 미션부터는 정말 다양한 예외에 대해 시간을 투자해 생각해봐야겠습니다.

4주차 구현 코드

https://github.com/youngh0/java-bridge/tree/youngh0

 

Comments