본문 바로가기
생각정리

[우아한테크코스 5기] 프리코스 2주차 야구게임 미션 학습 내용 및 회고

by greeng00se 2022. 11. 9.

2주차 회고

PR -> https://github.com/woowacourse-precourse/java-baseball/pull/709

그동안 배운 객체지향에 관한 것 + 새로운 것을 적용해 보려고 매우 몰입했던 한 주입니다.

2주 차에는 요구사항을 만족하는 것 이외에 다음 목표를 기준으로 삼았습니다.

  • 역할, 책임, 협력의 관점에서 객체들을 지속적으로 생각하기
  • 응집도, 결합도, 의존성 관점에서도 생각해 보기
  • red → green → refactor의 TDD 사이클 적용하여 코드 작성하기
  • 객체지향 생활체조에 언급된 원칙들을 최대한 만족하려고 노력하기
  • 1개의 들여 쓰기, 메서드 라인 수 최대 10라인 적용하기

초반에는 요구사항을 정리한 뒤 해당 책임은 누가 가지는 것이 좋을까?에 대한 생각이 너무 많아서 쉽게 미션을 시작하지 못했던 부분이 아쉬웠습니다. 간단한 야구 게임인 것 같아도 객체지향적으로 잘 작성해야겠다고 생각하니 어려웠습니다.


어느 정도 구조가 머릿속에서 정리되고 난 이후로는 탄력이 붙어서 진행속도가 조금 빨라진 것 같았습니다. 특히 객체지향 생활체조에 있는 내용들이 도움이 많이 된 것 같습니다.


이번 주 미션에서는 TDD 사이클을 자연스럽게 적용하지 못한 점이 많이 아쉬웠습니다.

쉬운 부분에서는 TDD 사이클을 지키면서 작성하도록 노력을 했지만 코드가 복잡해지면서 점점 적용하기 어려운 부분이 있었습니다. 그렇기 때문에 아직 TDD의 연습이 덜 되었다고 생각했습니다. 그리고 내가 하고 있는 것이 잘 하고 있는 것인지 혼자 판단해야 했기에 알기 어려운 부분이 있었습니다. 하지만 덕분에 혼자 생각을 하는 시간이 많아진 것 같기도 합니다.


기다리고 기다리던 2주 차부터 커뮤니티에서 다른 사람들의 코드를 리뷰할 수 있었습니다.

나 자신이 소프트 스킬이 많이 부족하다고 생각하여 다른 사람들이 나의 코드 리뷰를 받았을 때 속상해하지 않을까?에 대한 생각이 많이 들었고, 좋은 코드 리뷰를 하는 법 같은 것을 찾아보면서 최대한 좋은 리뷰를 작성하려고 노력했습니다. 정말 좋은 경험이었던 것 같습니다.

얼른 다음 주가 되어서 2주 차의 코드도 리뷰하고 또한 리뷰 받아 성장하고 싶은 마음이 굴뚝같습니다. 또한 다음주에는 이번 주에 부족했다고 느꼈던 TDD를 더욱 더 제대로 해보고 싶습니다!


야구 게임

2주차는 야구게임에 관한 미션이었습니다.

추가된 요구사항은 다음과 같습니다.

  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
  • 3항 연산자를 쓰지 않고 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
  • JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.

저는 극단적으로 1개의 들여 쓰기와 메서드 라인 수를 최대 10라인으로 적용하기로 했습니다.

책임 분리하기

시작하기에 앞서 요구사항을 정리하면서 객체의 책임에 대해 곰곰히 생각을 많이 했습니다.

콘솔 입출력이 있기 때문에 MVC 패턴을 생각했고 아래와 같은 모양이었습니다.

image

리팩토링을 거쳐나가며 다음과 같이 처음과 약간 다른 모양을 가지게 되었습니다.

image

위 구성도를 토대로 하나씩 생각한 것들을 정리해보겠습니다.

View 분리하기

처음에 입력과 출력을 처리하는 책임을 각각 나눠서 분리를 해야겠다고 생각했습니다.

하지만 입력에는 잘못 입력된 값에 대해 예외를 던져야 하는 부분이 필요했습니다.

사용자의 입력에 대한 검증을 하는 책임은 누구에게 할당해야 할까? 라는 생각이 들었고 처음에는 InputView가 입력과 입력에 대한 검증까지 수행하도록 생각을 했다가 코드를 작성해나가며 클래스의 크기가 커지는 걸 보면서 입력을 검증하는 책임을 갖도록 할 InputValidator 클래스를 하나 새로 만들어서 InputView가 검증에 대한 책임을 위임하도록 했습니다.

class InputValidator {
    private static final int VALID_BASEBALL_LENGTH = 3;
    private static final char START_RANGE = '1';
    private static final char END_RANGE = '9';
    private static final String VALID_GAME_STATUS_PLAY = "1";
    private static final String VALID_GAME_STATUS_STOP = "2";

    private InputValidator() {
    }

    public static void validateBaseballNumber(String baseballNumber) {
        if (baseballNumber.length() != VALID_BASEBALL_LENGTH
                || getDistinctNumberCount(baseballNumber) != VALID_BASEBALL_LENGTH) {
            throw new IllegalArgumentException();
        }
    }
    ...
}

InputValidator의 경우 package-private 접근 제어자를 사용하여 view 패키지의 InputView만 접근이 가능하도록 했습니다.

일급 컬렉션과 원시 값 포장

객체지향 생활체조에는 원시 값 포장과, 일급 컬렉션을 사용하라는 내용이 있습니다.

지금까지 일급 컬렉션을 프로젝트에서 사용한 기억이 없는 것 같아 비즈니스 로직을 처리하는 일급 컬렉션을 만들기 위해 향로님의 아티클을 많이 참고 했습니다.

야구공에 대한 부분을 일급 컬렉션으로 만들 수 있겠다 생각을 했고, List<Integer>를 가지고 있는 일급 컬렉션보다 원시 값을 포장하여 BaseballNumber 리스트를 가지고 있는 BaseballNumbers를 만들게 되었습니다.

BaseballNumbers 클래스는 야구 게임이 진행 될 때 스트라이크와 볼을 계산하는 책임을 가지도록 했습니다.

BaseballNumber 같은 경우 처음에는 단순히 Integer를 포장한 클래스였지만 테코블의 반복적으로 사용되는 인스턴스 캐싱하기 게시물을 참고하여 범위 내의 인스턴스를 재사용하도록 했습니다.

참고한 아티클은 다음과 같습니다.

일급 컬렉션 → 향로님의 일급 컬렉션 (First Class Collection)의 소개와 써야할 이유

인스턴스 캐싱 → 우테코 2기의 스티치님의 반복적으로 사용되는 인스턴스 캐싱하기

그래서 BaseballNumbers는 누가 생성합니까?

BaseballNumbers 클래스에 static 메서드나 생성자를 이용하려고 하다가 클래스가 너무 커질 것 같아서 해당 책임을 BaseballNumbersFactory에 몰아주기로 했습니다.

private BaseballNumbers toBaseballNumbers(List<Integer> baseballs) {
    return baseballs.stream()
            .map(BaseballNumber::valueOf)
            .collect(collectingAndThen(toList(), BaseballNumbers::new));
}

public BaseballNumbers generate(String inputBaseballNumbers) {
    return inputBaseballNumbers.chars()
            .map(Character::getNumericValue)
            .mapToObj(BaseballNumber::valueOf)
            .collect(collectingAndThen(toList(), BaseballNumbers::new));
}

컴퓨터와 사용자 BaseballNumbers 는 모두 해당 객체가 생성하도록 책임을 할당하였습니다.

랜덤으로 생성할 때 범위에 대한 값과 같이 상수에 대한 부분을 고민을 많이 했는데 BaseballNumber, BaseballNumbers 가 가지고 있는 범위 값을 가져와 사용하도록 했습니다.

이펙티브 자바 아이템 15에는 다음과 같이 상수에 대한 내용을 언급합니다.

public 클래스에서 상수에 한해 public static final 필드를 사용해도 된다.

원래는 package-private을 사용할까 생각도 했는데 야구공 공장의 생산 기준은 야구공이 가지고 있는 값에 따라 달라져야 한다고 단순하게 생각하여 야구공이 가지고 있는 상수를 사용하도록 했습니다.

BaseballMachine

BaseballGameMachine 에는 야구 게임을 진행하는 책임을 할당했습니다.

처음에는 BaseballFactory를 의존하는 형태를 생각했었는데 추후에 개발할 때 컴퓨터의 야구공 정보를 내부에서 랜덤으로 생성하기 때문에 테스트 하기 어려워져서 해당 클래스는 컴퓨터의 야구공 정보와 게임의 상태만 가지고 있도록 했습니다.

야구공에 대한 비교는 BaseballNumbers가 하기 때문에 단순하게 위임하도록 했습니다.

야구 게임의 결과

처음에는 BaseballNumbers에서 결과 반환을 위한 DTO를 생성하도록 코드를 작성했습니다.

야구공의 결과를 OutputView에서 하나하나 다 작성하기에는 무리가 있는 것 같고, 출력 결과도 비즈니스 로직 중 하나라고 생각하여 Enum을 사용하기로 했습니다.

초기에 작성한 코드는 다음과 같은 형태를 띄었습니다.

public enum BaseballGameResult {
    _3_STRIKE(3, 0, "3스트라이크"),
    _2_STRIKE(2, 0, "2스트라이크"),
    _1_STRIKE(1, 0, "1스트라이크"),
    _1_STRIKE_2_BALL(1, 2, "2볼 1스트라이크"),
    _1_STRIKE_1_BALL(1, 1, "1볼 1스트라이크"),
    _0_STRIKE_2_BALL(0, 2, "2볼"),
    _0_STRIKE_1_BALL(0, 1, "1볼"),
    _NOTHING(0, 0, "낫싱");

    private final int strike;
    private final int ball;
    private final String message;

    BaseballGameResult(int strike, int ball, String message) {
        this.strike = strike;
        this.ball = ball;
        this.message = message;
    }

    @Override
    public String toString() {
        return message;
    }
}

하지만 객체지향 생활체조 원칙 중 3개 이상의 인스턴스 변수를 가진 클래스를 사용하지 않는다 라는 부분이 신경쓰였고 추후에 스트라이크와 볼 원시 값도 포장하여 다음과 같은 형태로 리팩토링 했습니다.

public enum BaseballGameResult {
    _3_STRIKE(Strike.valueOf(3), Ball.valueOf(0)),
    _2_STRIKE(Strike.valueOf(2), Ball.valueOf(0)),
    _1_STRIKE(Strike.valueOf(1), Ball.valueOf(0)),
    _1_STRIKE_2_BALL(Strike.valueOf(1), Ball.valueOf(2)),
    _1_STRIKE_1_BALL(Strike.valueOf(1), Ball.valueOf(1)),
    _3_BALL(Strike.valueOf(0), Ball.valueOf(3)),
    _2_BALL(Strike.valueOf(0), Ball.valueOf(2)),
    _1_BALL(Strike.valueOf(0), Ball.valueOf(1)),
    _NOTHING(Strike.valueOf(0), Ball.valueOf(0));

    private static final String NOTHING_MESSAGE = "낫싱";
    private static final String DEFAULT_MESSAGE_FORMAT = "{0}{1}";
    private static final String DELIMITER_MESSAGE_FORMAT = "{0} {1}";

    private final Strike strike;
    private final Ball ball;

    BaseballGameResult(Strike strike, Ball ball) {
        this.strike = strike;
        this.ball = ball;
    }

    @Override
    public String toString() {
        if (isNothing()) {
            return NOTHING_MESSAGE;
        }
        if (isNeedDelimiter()) {
            return MessageFormat.format(DELIMITER_MESSAGE_FORMAT, ball, strike);
        }
        return MessageFormat.format(DEFAULT_MESSAGE_FORMAT, ball, strike);
    }

    private boolean isNothing() {
        return this.equals(_NOTHING);
    }

    private boolean isNeedDelimiter() {
        return this.equals(_1_STRIKE_1_BALL) || this.equals(_1_STRIKE_2_BALL);
    }

    public static BaseballGameResult toEnum(Strike strike, Ball ball) {
        return Arrays.stream(values())
                .filter(baseballGameResult -> isStrikeEqual(baseballGameResult, strike))
                .filter(baseballGameResult -> isBallEqual(baseballGameResult, ball))
                .findFirst()
                .orElseThrow(BaseballGameException::new);
    }

    private static boolean isStrikeEqual(BaseballGameResult baseballGameResult, Strike strike) {
        return baseballGameResult.strike.equals(strike);
    }

    private static boolean isBallEqual(BaseballGameResult baseballGameResult, Ball ball) {
        return baseballGameResult.ball.equals(ball);
    }

    public boolean isStrikeOut() {
        return this.strike.isStrikeOut();
    }
}

원시값에서 toString 메서드를 오버라이드하여 3볼과 같은 출력값을 반환하도록 하고 BaseballGameResult 에서 최종적으로 어떤 값을 출력할 지 결정하도록 했습니다.

하지만 이부분에서 의존성 관련해서 아쉬운 부분이 있었는데요!

View가 Domain에 있는 코드를 의존하게 된다는 것이었습니다.

OutputView에서 해당 BaseballGameResult를 사용하여 그대로 출력을 하고 있었는데 Domain 코드의 의존성을 dto를 이용하여 제거했으면 더 좋지 않을까?라고 생각을 하고 있습니다.

그래서 다음주는 도메인에 의존하지 않는 View를 작성하도록 노력하기로 마음먹었습니다.

추가로 도전해본 것

TDD

TDD를 제대로 하지 못해서 이번 주 미션에서는 TDD 사이클을 자연스럽게 적용해보려다가 실패를 경험한 것 같습니다.

앞서 말한대로 쉬운 부분에서는 TDD 사이클을 지키기 쉬웠지만 막상 복잡해지면 제대로 적용을 못해서 아직 TDD의 연습이 덜 되었다고 생각했습니다.

그래서 다음주에는 꼭 처음부터 끝까지 TDD 사이클을 지키면서 적용해보고 싶습니다.

계층 구조로 테스트하기

예전에 John Grib님의 아티클인 JUnit5로 계층 구조의 테스트 코드 작성하기를 읽은게 기억나서 이를 우테코 프리코스 미션에 적용을 해보고 싶어 직접 적용을 했었습니다.

따라서 다음과 같은 예쁜 테스트 출력물을 만들 수 있었는데요!

image

하지만 지원 홈페이지에서 테스트 할 때 예기치 못한 실행 오류가 계속 발생하여 계층형 코드를 전부 제거하고 다시 테스트 코드를 작성했습니다.

계층형 테스트 코드는 추후에 개인 프로젝트 때 적용해보기로 했습니다.

한 주를 마무리 하며

다음주에는 이번주에 했던 셀프 피드백 + 생각을 좀 덜 하고 빠르게 코드를 작성하는 쪽으로 진행해보려고 합니다.

TDD도 무조건 사이클을 지켜가며 하도록 노력해보려고 합니다.

3주차는 더 좋은 코드를 작성하고 성장할 수 있는 한 주가 되길 기대하면서 2주차를 마치겠습니다.