[우아한테크코스] 다리 건너기 (최종 코테 준비)

YoonJuHo·2023년 12월 10일
1

우아한테크코스

목록 보기
4/5
post-thumbnail

다리 건너기 회고기록

고민 1 : 검증된 메서드를 활용하는 메서드에 대한 테스트?
고민 2 : 클래스를 작게 쪼개고, Controller는 비즈니스로직을 가지지 말자.
<고민 아닌 고민>
- @FunctionalInterface

GitHub Code

Convention

AngularJS commit conventions

docs(README): 기능목록 재정리

Google Java Style Guide

기존의 Google Java Style Guide와 비교했을 때 
크게 달라진 점은 블럭 들여쓰기 2 -> 4로 변경된 것 뿐이다.

Enable google-java-format을 설정 시 Google Java Style Guide로 설정.

DefaultWootecoStyle로 설정되어 있다.


Docs

이렇게 기능 요구사항이 주어진다면 처음 부터 아래와 같이 작성하기는 어렵다.

1. 기능 요구 사항을 읽어가면서 아래 사항들을 정리

  • 전체적인 게임 규칙
  • Domain(컴퓨터, 플레이어)
  • System 유의사항
  • Exception Handling
  • Input
  • Output
  • 사용해야 할 라이브러리

따라서 위의 1번을 수행하기전에 아래의 빨간 밑줄그어진 부분을 우선적으로 정리해 전체적인 흐름을 알아 내도록 하자

0. 전체적인 흐름 정리

  • 전체적인 게임 규칙
  • 전체적인 게임 흐름

위의 파란색 박스에 속해있는 부분들을 참고하여 아래 사항들을 정리해 나간다.
정리해 나가면서 입력출력에 관한 많은 부분은 입출력 요구 사항을 참고한다.

1. 기능 요구 사항을 읽어가면서 아래 사항들을 정리

  • 전체적인 게임 규칙
  • 전체적인 게임 흐름
  • Domain(다리, 플레이어)
  • System 유의사항
  • Exception Handling
  • Input
  • Output
  • 사용해야 할 라이브러리
# 게임 규칙
위아래 둘 중 하나의 칸만 건널 수 있는 다리를 끝까지 건너가는 게임이다.

# 게임 흐름
- 위아래 두 칸으로 이루어진 다리를 건너야 한다.
- 다리의 길이를 숫자로 입력받고 생성한다.
- 다리가 생성되면 플레이어가 이동할 칸을 선택한다.
- 다리를 끝까지 건너면 게임이 종료된다.
- 다리를 건너다 실패하면 게임을 재시작하거나 종료할 수 있다.

## 다리
- 다리의 길이는 3 이상 20 이하로 만들어져야 한다.
  - 올바른 값이 아니면 예외 처리한다.
- 다리를 생성할 때 위 칸과 아래 칸 중 건널 수 있는 칸은 0과 1 중 무작위 값을 이용해서 정한다. 
<br> 무작위 값이 0인 경우 아래 칸, 1인 경우 위 칸이 건널 수 있는 칸이 된다.

## 플레이어
- 다리는 왼쪽에서 오른쪽으로 건너야 한다.
- 위아래 둘 중 하나의 칸만 건널 수 있다. <br> 위 칸을 건널 수 있는 경우 U, 아래 칸을 건널 수 있는 경우 D값으로 나타낸다.

## 입력
- 자동으로 생성할 다리 길이를 입력 받는다. 3 이상 20 이하의 숫자를 입력할 수 있다
  - 올바른 값이 아니면 예외 처리한다.
- 이동할 때 위 칸은 대문자 U, 아래 칸은 대문자 D를 입력한다.
  - 올바른 값이 아니면 예외 처리한다.
- 게임 재시작/종료 여부를 입력 받는다. R(재시작)과 Q(종료) 중 하나의 문자를 입력할 수 있다
  - 올바른 값이 아니면 예외 처리한다.

## 출력
- 게임 시작 문구를 출력한다.
- 게임 종료 문구를 출력한다.
- 사용자가 이동할 때마다 다리 건너기 결과의 출력 (이동한 칸을 건널 수 있다면 O로 표시한다. 건널 수 없다면 X로 표시한다.) 

## System 유의사항
- 재시작해도 처음에 만든 다리로 재사용한다.
- 게임 결과의 총 시도한 횟수는 첫 시도를 포함해 게임을 종료할 때까지 시도한 횟수를 나타낸다.

## Exception Handling
- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.

## 사용해야 할 라이브러리
- Random 값 추출은 제공된 bridge.BridgeRandomNumberGenerator의 generate()를 활용한다.
- camp.nextstep.edu.missionutils에서 제공하는 Console API를 사용하여 구현해야 한다.

2. 도메인과 입력이 중복되는 부분이 반드시 존재한다. - 중복 제거 ❌

다리 : 다리의 길이는 3 이상 20 이하로 만들어져야 한다. | 입력 : 자동으로 생성할 다리 길이를 입력 받는다. 3 이상 20 이하의 숫자를 입력할 수 있다
이와 같은 부분은 도메인의 비즈니스 로직 예외 검증 부분과, 입력의 단순 입력 예외 검증 부분을 구분하는 용도로 중복을 제거하지 말고, 그대로 유지한다.

3. 중복되는 기능 요구사항들을 합치자. - 중복 제거 ⭕️

하나의 도메인(ex: 입력, 출력, 다리 등)에서 중복되는 요구사항들이 존재한다면 다른 도메인으로 전파 시키지 말고 그 도메인 내에서 중복을 제거하자

4. 단순 생성자만 존재해도 README에 추가하자

단순히 값을 표현하는 Enum객체에서 생성자Getter만 존재하더라도 기능 목록에 작성해 주고, 표현의 방식을 끝에 ~ 정의할 수 있다와 같이 표현해서 해당 도메인이 하는 역할을 특정해 주도록 하자.


Feat

구현 순서

Docs를 살아있는 문서로 만들면서 진행

  • Domain
  • Controller <-> View

<고민 1>

내부적으로 검증된 메서드들을 사용하더라도 메서드 시그니처검증된 메서드들의 시그니처와 다를 시 테스트 하자.
아래의 경우 finish()에 대한 테스트는 굳이 진행하지 않아도 되겠다.

// BridgeGame
public boolean finish() {
        return bridge.end();
}
// Bridge
public boolean end() {
        return unit.size() == index;
}

	/**
     * @param size 다리의 길이
     * @return 입력받은 길이에 해당하는 다리 모양. 위 칸이면 "U", 아래 칸이면 "D"로 표현해야 한다.
     */
    public List<String> makeBridge(int size) {
        validateSize(size);
        List<String> bridge = new ArrayList<>();
        for(int i = 0; i < size; i++){
            bridge.add(BridgeUnit.of(bridgeNumberGenerator.generate()).getSignatureLetter());
        }
        return bridge;
    }

위와 같이 이루어져 있는 경우에 makeBridge(int size)로 생성되는 List<String>형의 문자들은 정확히 어떤 문자들이 들어가는지 예측할 수가 없다.
(내부적으로 랜덤한 값을 생성해 문자로 변환하기 때문에)

하지만, 랜덤한 값을 생성하는 bridgeNumberGenerator가 정상적으로 동작하는지에 대해 검증했고, BridgeUnit.of 또한 정상작동하는 것을 검증했다면,
정확히 List<String>에 어떤 순서로 값이 들어가는 지는 별로 중요하지 않고,

특정 문자 UD만 정상적으로 들어갔는지 검사하는 것으로 테스트를 할 수 있겠다.

	@Test
    @DisplayName("무작위 값을 이용해 다리를 생성할 수 있다.")
    void makeBridge() {
        BridgeMaker bridgeMaker = new BridgeMaker(new BridgeRandomNumberGenerator());
        assertAll(
                () -> assertThat(bridgeMaker.makeBridge(3).size()).isEqualTo(3),
                () -> assertThat(bridgeMaker.makeBridge(3)).containsAnyElementsOf(List.of(CrossingDirection.TOP.getSignatureLetter(), CrossingDirection.BOTTOM.getSignatureLetter()))
        );
    }

<고민 2>

클래스를 잘게 쪼개야 한다,
또한 Controller가 비즈니스로직을 가지지 않게끔 구성해야 한다.

Bridge - 다리
BridgeMaker - 다리 생성
BridgeGame - 다리 게임

요구사항이 위처럼 주어졌는데 이 요구사항의 목적은 Bridge를 생성하는 클래스를 따로 두어 관리하라는 의미로 다가온다.

또한, BrdigeGame에 대한 요구사항이 위처럼 주어졌다는 것은 BridgeGame 클래스에서 InputView, OutputView를 사용하지 않는다 = BridgeGameController로 사용하지 말라는 뜻이다.

이걸 확장해서 생각해보면 아래와 같이 볼 수 있다.

"Controller비즈니스로직을 가지지 않게끔 Controller와 비슷한 성격을 가진 Domain 객체를 만들어 해당 Domain객체가 비즈니스로직을 가지도록 구성하라"


<고민 아닌 고민> - @FunctionalInterface

@FunctionalInterface
public interface BridgeNumberGenerator {

    int generate();
}
public class BridgeRandomNumberGenerator implements BridgeNumberGenerator {

    private static final int RANDOM_LOWER_INCLUSIVE = 0;
    private static final int RANDOM_UPPER_INCLUSIVE = 1;

    @Override
    public int generate() {
        return Randoms.pickNumberInRange(RANDOM_LOWER_INCLUSIVE, RANDOM_UPPER_INCLUSIVE);
    }
}

랜덤값을 생성하는 클래스인터페이스가 주어졌는데,

앞으로 해당하는 라이브러리(Random)를 활용해야 할 때 위와 같이 함수형 인터페이스를 생성해
@FunctionalInteface를 붙여 컴파일러에게 해당 인터페이스가 함수형 인터페이스임을 명시적으로 알려주자
이 어노테이션을 사용하면 컴파일러가 해당 인터페이스가 함수형 인터페이스의 규칙을 따르고 있는지를 검사하고, 그렇지 않은 경우 컴파일 오류를 발생시킨다.


Test

ChatGpt를 활용해 CodeReview를 받으려 했지만 OpenAI의 API를 활용하려면 일정 금액을 지불해야 했다.
금액을 지불해서 개선점이나 결함을 급히 찾아야 하는 프로젝트가 아니었기에 진행하지 않도록 했다.


Refactor

  • 값을 하드코딩하지 않고 상수화 하였는가
    (0을 ZERO로 표현하는 것은 안하느니 못하다)
  • 네이밍 규칙을 잘 따랐는가
  • 패키지 구분을 가독성있게 했는가
  • 접근제한자를 적절하게 사용했는가
  • 출력결과의 순서와 동일한가
  • 처리하지 않은 예외 검사가 있는가
  • 객체지향 생활 체조 원칙을 준수했는가

Code Coverage

Code Coverage에 연연하지 말고, 실제 비즈니스 로직에 대한 크리티컬한 테스트가 진행되지 않은 부분이 존재하는지만 판단하도록 하자.
"테스트는 결함 검출용으로 사용하고, 코드 커버리지에는 집착하지 마라"


참고 블로그

0개의 댓글