다리 건너기 회고기록
고민 1
: 검증된 메서드를 활용하는 메서드에 대한 테스트?
고민 2
: 클래스를 작게 쪼개고,Controller
는 비즈니스로직을 가지지 말자.
<고민 아닌 고민>
-@FunctionalInterface
docs(README): 기능목록 재정리
기존의 Google Java Style Guide와 비교했을 때
크게 달라진 점은 블럭 들여쓰기 2 -> 4로 변경된 것 뿐이다.
Enable google-java-format
을 설정 시Google Java Style Guide
로 설정.
Default
는WootecoStyle
로 설정되어 있다.
이렇게 기능 요구사항이 주어진다면 처음 부터 아래와 같이 작성하기는 어렵다.
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
만 존재하더라도 기능 목록에 작성해 주고, 표현의 방식을 끝에~ 정의할 수 있다
와 같이 표현해서 해당 도메인이 하는 역할을 특정해 주도록 하자.
Docs
를 살아있는 문서로 만들면서 진행
Domain
Controller
<->View
내부적으로 검증된 메서드들을 사용하더라도
메서드 시그니처
가검증된 메서드들의 시그니처
와 다를 시 테스트 하자.
아래의 경우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>
에 어떤 순서로 값이 들어가는 지는 별로 중요하지 않고,
특정 문자
U
와D
만 정상적으로 들어갔는지 검사하는 것으로 테스트를 할 수 있겠다.@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())) ); }
클래스를 잘게 쪼개야 한다,
또한 Controller가 비즈니스로직을 가지지 않게끔 구성해야 한다.
Bridge
- 다리
BridgeMaker
- 다리 생성
BridgeGame
- 다리 게임
요구사항이 위처럼 주어졌는데 이 요구사항의 목적은 Bridge를 생성하는 클래스를 따로 두어 관리
하라는 의미로 다가온다.
또한, BrdigeGame
에 대한 요구사항이 위처럼 주어졌다는 것은 BridgeGame 클래스에서 InputView, OutputView를 사용하지 않는다
= BridgeGame
을 Controller
로 사용하지 말라는 뜻이다.
이걸 확장해서 생각해보면 아래와 같이 볼 수 있다.
"
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
를 붙여 컴파일러에게 해당 인터페이스가 함수형 인터페이스임을 명시적으로 알려주자
이 어노테이션을 사용하면 컴파일러가 해당 인터페이스가 함수형 인터페이스의 규칙을 따르고 있는지를 검사하고, 그렇지 않은 경우 컴파일 오류를 발생시킨다.
ChatGpt
를 활용해CodeReview
를 받으려 했지만 OpenAI의 API를 활용하려면 일정 금액을 지불해야 했다.
금액을 지불해서 개선점이나 결함을 급히 찾아야 하는 프로젝트가 아니었기에 진행하지 않도록 했다.
(0을 ZERO로 표현하는 것은 안하느니 못하다)
Code Coverage
에 연연하지 말고, 실제비즈니스 로직
에 대한 크리티컬한 테스트가 진행되지 않은 부분이 존재하는지만 판단하도록 하자.
"테스트는 결함 검출용으로 사용하고, 코드 커버리지에는 집착하지 마라"