- 자동차 경주 게임 요구사항을 파악한다.
- 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다.
- 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다.
- 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다.
- "1,2"을 ,로 split 했을 때 1과 2로 잘 분리되는지 확인하는 학습 테스트를 구현한다.
- "1"을 ,로 split 했을 때 1만을 포함하는 배열이 반환되는지에 대한 학습 테스트를 구현한다.
- 배열로 반환하는 값의 경우 assertj의 contains()를 활용해 반환 값이 맞는지 검증한다.
- 배열로 반환하는 값의 경우 assertj의 containsExactly()를 활용해 반환 값이 맞는지 검증한다.
- "(1,2)" 값이 주어졌을 때 String의 substring() 메소드를 활용해 ()을 제거하고 "1,2"를 반환하도록 구현한다.
- "abc" 값이 주어졌을 때 String의 charAt() 메소드를 활용해 특정 위치의 문자를 가져오는 학습 테스트를 구현한다.
- String의 charAt() 메소드를 활용해 특정 위치의 문자를 가져올 때 위치 값을 벗어나면 StringIndexOutOfBoundsException이 발생하는 부분에 대한 학습 테스트를 구현한다.
- JUnit의 @DisplayName을 활용해 테스트 메소드의 의도를 드러낸다.
- AssertJ Exception Assertions 문서 참고
import static org.assertj.core.api.Assertions.*;
assertThatThrownBy(() -> {
// ...
}).isInstanceOf(IndexOutOfBoundsException.class)
.hasMessageContaining("Index: 2, Size: 2");
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
assertThatExceptionOfType(IndexOutOfBoundsException.class)
.isThrownBy(() -> {
// ...
}).hasMessageMatching("Index: \\d+, Size: \\d+");
자주 발생하는 Exception의 경우 Exception별 메서드 제공하고 있음
- assertThatIllegalArgumentException()
- assertThatIllegalStateException()
- assertThatIOException()
- assertThatNullPointerException()
- 다음과 같은 Set 데이터가 주어졌을 때 요구사항을 만족해야 한다.
public class SetTest {
private Set numbers;
@BeforeEach
void setUp() {
numbers = new HashSet<>();
numbers.add(1);
numbers.add(1);
numbers.add(2);
numbers.add(3);
}
// Test Case 구현
}
- Set의 size() 메소드를 활용해 Set의 크기를 확인하는 학습테스트를 구현한다.
- Set의 contains() 메소드를 활용해 1, 2, 3의 값이 존재하는지를 확인하는 학습테스트를 구현하려한다.
- 구현하고 보니 다음과 같이 중복 코드가 계속해서 발생한다.
- JUnit의 ParameterizedTest를 활용해 중복 코드를 제거해 본다.
@Test
void contains() {
assertThat(numbers.contains(1)).isTrue();
assertThat(numbers.contains(2)).isTrue();
assertThat(numbers.contains(3)).isTrue();
}
- Guide to JUnit 5 Parameterized Tests
@ParameterizedTest
@ValueSource(strings = {"", " "})
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
요구사항 2는 contains 메소드 결과 값이 true인 경우만 테스트 가능하다. 입력 값에 따라 결과 값이 다른 경우에 대한 테스트도 가능하도록 구현한다. 예를 들어 1, 2, 3 값은 contains 메소드 실행결과 true, 4, 5 값을 넣으면 false 가 반환되는 테스트를 하나의 Test Case로 구현한다.
- Guide to JUnit 5 Parameterized Tests 문서에서 @CsvSource를 활용한다.
@ParameterizedTest
@CsvSource(value = {"test:test", "tEst:test", "Java:java"}, delimiter = ':')
void toLowerCase_ShouldGenerateTheExpectedLowercaseValue(String input, String expected) {
String actualValue = input.toLowerCase();
assertEquals(expected, actualValue);
}
- assertj 활용
- Introduction to AssertJ 문서 참고해 assertj의 다양한 활용법 익힌다.
- [fix01][SetCollectionTest.java] hasSize를 활용할 수 있습니다
- 사용자가 입력한 문자열 값에 따라 사칙연산을 수행할 수 있는 계산기를 구현해야 한다.
- 입력 문자열의 숫자와 사칙 연산 사이에는 반드시 빈 공백 문자열이 있다고 가정한다.
- 나눗셈의 경우 결과 값을 정수로 떨어지는 값으로 한정한다.
- 문자열 계산기는 사칙연산의 계산 우선순위가 아닌 입력 값에 따라 계산 순서가 결정된다. 즉, 수학에서는 곱셈, 나눗셈이 덧셈, 뺄셈 보다 먼저 계산해야 하지만 이를 무시한다.
- 예를 들어 2 + 3 * 4 / 2와 같은 문자열을 입력할 경우 2 + 3 * 4 / 2 실행 결과인 10을 출력해야 한다.
- 메소드가 너무 많은 일을 하지 않도록 분리하기 위해 노력해 본다.
- 기능 분리 힌트
- 테스트할 수 있는 단위로 나누어 구현 목록을 만든다.
- 덧셈
- 뺄셈
- 곱셈
- 나눗셈
- 입력 값이 null이거나 빈 공백 문자일 경우 IllegalArgumentException throw
- 사칙연산 기호가 아닌 경우 IllegalArgumentException throw
- 사칙 연산을 모두 포함하는 기능 구현
- 공백 문자열을 빈 공백 문자로 분리하려면 String 클래스의 split(" ") 메소드를 활용한다.
- 반복적인 패턴을 찾아 반복문으로 구현한다.
- [fix01][전체파일] if 다음에 공백, for 다음에 공백 컨벤션 준수하기
- [fix02][CalculationOperator.java] operation 이름을 의미있게 변경하기
- [fix03][Calculator.java] validate 조건문 간소화 하기
- [fix04][Calculator.java] reduce를 활용하여 lamda식 변경해보기
private static int makeResult(List numbers, List operators) {
int[] index = { 0 };
int result = numbers.stream()
.reduce((preOperand, postOperand) -> {
int sum = operators.get(index[0]).operation(preOperand, postOperand);
index[0] += 1;
return sum;
}).get();
return result;
}
- 초간단 자동차 경주 게임을 구현한다.
- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
- 사용자는 몇 대의 자동차로 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
- 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4이상일 경우이다.
- 자동차의 상태를 화면에 출력한다. 어느 시점에 출력할 것인지에 대한 제약은 없다.
- 위 요구사항에 따라 3대의 자동차가 5번 움직였을 경우 프로그램을 실행한 결과는 다음과 같다.
자동차 대수는 몇 대 인가요?
3
시도할 회수는 몇 회 인가요?
5
실행 결과
-
-
-
--
-
--
---
--
---
----
---
----
----
----
-----
- 값을 입력 받는 API는 Scanner를 이용한다.
Scanner scanner = new Scanner(System.in);
String value = scanner.nextLine();
int number = scanner.nextInt();
- 랜덤 값은 자바 java.util.Random 클래스의 nextInt(10) 메소드를 활용한다.
- 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외
- 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
- UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다.
- 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- else 예약어를 쓰지 않는다.
- 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
- else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
- [fix01][Entry.java] Domain과 View를 분리할 것
- 만약 View의 변화로 인해 도메인 수정이 일어난다면 코드의 의존성을 넘어 아키텍처 강한 의존성이 생기게 됨
- 비지니스 로직과 출력 로직을 도메인이 책임지고 있기 때문에 단일 책임의 원칙에도 위배
- View 같은 세부 정보를 참조하여 복잡성을 더 추가한다면 복잡도가 더 증가하고 테스트 하기가 어려움
- 그렇기에 출력에 관련된 로직은 출력 객체에게 위임 하는게 맞는 방법
- 하지만 출력 객체에게 로직을 위임하면 아키텍처 의존성 문제는 해결되지만 View가 도메인을 알게되는 문제점이 발생
- 해당 경우는 여러 방법을 통해 데이터를 View에 전달할 수 있는 구조로 변경
- 도메인을 그대로 노출하는 방법
- DTO를 통해 필요한 데이터를 전달하는 방법
- 읽기 전용 인터페이스를 만드는 방법
- 각 자동차에 이름을 부여할 수 있다. 자동차 이름은 5자를 초과할 수 없다.
- 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
- 자동차 이름은 쉼표(,)를 기준으로 구분한다.
- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한명 이상일 수 있다.
- 위 요구사항에 따라 3대의 자동차가 5번 움직였을 경우 프로그램을 실행한 결과는 다음과 같다.
- 경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).
pobi,crong,honux
시도할 회수는 몇회인가요?
5
실행 결과
pobi : -
crong : -
honux : -
pobi : --
crong : -
honux : --
pobi : ---
crong : --
honux : ---
pobi : ----
crong : ---
honux : ----
pobi : -----
crong : ----
honux : -----
pobi : -----
crong : ----
honux : -----
pobi, honux가 최종 우승했습니다.
- 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
- indent가 2이상인 메소드의 경우 메소드를 분리하면 indent를 줄일 수 있다.
- else를 사용하지 않아 indent를 줄일 수 있다.
- 자동차 이름을 쉼표(,)를 기준으로 분리하려면 String 클래스의 split(",") 메소드를 활용한다.
String[] names = inputName.split(",");
- 사용자가 입력한 이름의 숫자 만큼 자동차 대수를 생성한다.
- 자동차는 자동차 이름과 위치 정보를 가지는 Car 객체를 추가해 구현한다.
- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다.
- 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다.
- 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다.
- 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외
- 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
- UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다.
- 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- else 예약어를 쓰지 않는다.
- 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
- else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
- [fix01][SnapShotEntry.java] cars를 가져올 때, 불변 객체로 만들어 보기
- [fix02][RacingCar.java] Object의 clone 보다 복사 객체를 이용하여 개선해보기
- clone을 지양하는 이유
- Object의 clone 사용시 복제하려는 객체 내에 변경 가능한 인스턴스 객체 필드가 있을 경우 얕은 복사가 되며,
- Object의 clone 메소드는 Checked Exception인 CloneNotSupportedException을 처리하도록 선언되어있지만 복사하다 예외가 발생하는 시점은 런타임 시점이기에 선언된 예외 타입의 예외 시점과 발생하는 예외 시점이 맞지 않는 보기 불편한(?) 문제
- clone 메서드 재 정의시 clone시 규약을 지키지 않으면 더욱 큰 문제를 야기 할 수 있는 문제
- clone을 지양하는 이유
- [fix03][SnapShotEntry.java] 최대 거리를 한번만 가져오도록 수정
- 핵심 비지니스 로직을 가지는 객체를 domain 패키지, UI 관련한 객체를 view 패키지에 구현한다.
- MVC 패턴 기반으로 리팩토링해 view 패키지의 객체가 domain 패키지 객체에 의존할 수 있지만, domain 패키지의 객체는 view 패키지 객체에 의존하지 않도록 구현한다.
- 테스트 가능한 부분과 테스트하기 힘든 부분을 분리해 테스트 가능한 부분에 대해서만 단위 테스트를 진행한다.