1주차 추가 피드백
지난 1주차에 대한 공통 피드백 중 'Java에서 제공하는 API를 적극 활용한다' 라는 내용과 '배열 대신 Java Collection을 사용한다' 라는 내용이 있었습니다. 이 문구들과 다른 분들이 받은 피어리뷰들과 코드를 보니 저의 코드가 언어를 사용하는데 한정적이었다는 생각을 했습니다.
예를 들어 stream을 사용하여 가독성을 높이거나 Collection 자료구조를 정확한 이유로 사용해야 했지만 못했던 점이 아쉽고 많은 부족함이 느껴졌습니다.
프리코스 2주차
이번 주차부터 제대로 된 우아한테크코스의 미션 과정을 경험할 수 있다 했습니다.
목표
- 1주차 학습
- 함수의 분리
- 함수별 테스트 작성: 작은 단위의 기능 테스트 작성
미션 문제
요구 사항
1. 과제 요구 사항 분리
1주차에서는 문제의 요구 사항과 제한 사항을 .java 파일에 같이 작성했었습니다. 이번 주차 부터는 README.md파일에 기능 목록을 정리 후, git에 commit을 먼저 하라고 명시되어 있습니다.
2. 기능별 함수 분리
indent 깊이를 2까지만 허용했습니다. 최대한 하나의 함수가 하나의 기능을 가지게 메서드를 분리해야 했습니다.
3. 작은 단위의 기능 테스트 작성
이 부분에 대해서는 사실 많은 고민이 됐습니다. 작은 단위의 기능 테스트를 어떻게 해야할까 라는 생각을 많이 했던거 같습니다. JUnit5와 AssertJ를 이용하여 기능 목록의 정상 동작을 확인해야 합니다.
숫자 야구 미션
요구사항 정리: 필요 기능 목록을 분리하여 따로 README.md파일로 만들어서 미션의 요구사항을 정리 하였습니다. 또한 코드를 짜기 전 필요 기능 목록을 체크리스트로 만들었고 추가적으로 필요한 것이 생기면 추가하였습니다.
미션 설계: 미션을 설계할 때 전체적인 틀을 잡고 점점 뼈대를 잡아가는 방식으로 했습니다.
숫자 야구는 게임이 시작하면 자동으로 랜덤 값을 받고, 사용자의 입력 값을 통해 볼과 스트라이크를 카운트합니다.
그 후, 스트라이크가 3이면 게임이 끝나는 방식이므로 이 방식의 순서대로 기능별 함수를 분리하며 작성했습니다.
라이브러리
미션 도중 '랜덤한 값과 console값을 줘야하는데 테스트 코드로 어떻게 주는거지?' 라는 생각을 했습니다. 따라서 '함수만 가져와서 검사해야하나?' 라는 생각을 하며 일단 테스트 코드를 실행하고 디버깅을 진행는데 값이 테스트 코드에서 제공했던 값으로 고정되는걸 발견했습니다. 따라서 '왜 이렇게 될까' 생각하면서 내부 코드를 분석해봤습니다.
assertRandomNumberInRangeTest(
() -> {
run("246", "135", "1", "597", "589", "2");
assertThat(output()).contains("낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료");
},
1, 3, 5, 5, 8, 9
);
(위와 같이 진행하면 전체 main함수가 진행되며 랜덤값과 입력 값이 고정된다.)
NsTest (camp.nextstep.edu.missionutils.test)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
public abstract class NsTest {
private PrintStream standardOut;
private OutputStream captor;
@BeforeEach
protected final void init() {
standardOut = System.out;
captor = new ByteArrayOutputStream();
System.setOut(new PrintStream(captor));
}
@AfterEach
protected final void printOutput() {
System.setOut(standardOut);
System.out.println(output());
}
protected final String output() {
return captor.toString().trim();
}
protected final void run(final String... args) {
command(args);
runMain();
}
protected final void runException(final String... args) {
try {
run(args);
} catch (final NoSuchElementException ignore) {
}
}
private void command(final String... args) {
final byte[] buf = String.join("\n", args).getBytes();
System.setIn(new ByteArrayInputStream(buf));
}
protected abstract void runMain();
}
|
NsTest abstract 클래스에서 runMain을 추상화하여 사용되었습니다.
따라서 처음 run이라는 함수가 불릴때 사용자의 입력이 args에 담겨오며 해당 args를 inputstream에 미리 넣어두어 나중에 해당 값을 읽어 올때 inputstream에 있는 값을 가져오는 것으로 입력을 대신하였습니다.
@BeforeEach: 각 테스크가 시작하기 전에 먼저 시작하는 annotation입니다.
@AfterEach: 각 테스트가 끝나면 시작되는 annotation입니다.
output의 경우 ByteArrayOutputStream() 즉, outputstream에 대한 인스턴스를 미리 생성한 후, 콘솔에 출력하는 필드를 넣어주어 출력 결과값을 가져와서 비교한다.
Console ( camp.nextstep.edu.missionutils.Console)
public class Console {
private static Scanner scanner;
private Console() {
}
public static String readLine() {
return getInstance().nextLine();
}
private static Scanner getInstance() {
if (Objects.isNull(scanner) || isClosed()) {
scanner = new Scanner(System.in);
}
return scanner;
}
private static boolean isClosed() {
try {
final Field sourceClosedField = Scanner.class.getDeclaredField("sourceClosed");
sourceClosedField.setAccessible(true);
return sourceClosedField.getBoolean(scanner);
} catch (final Exception e) {
System.out.println("unable to determine if the scanner is closed.");
}
return true;
}
}
Console의 readLine()을 통해 입력 받았습니다.
getInstance 메소드에서 return값으로 scanner의 인스턴스를 받아 오기에 기본 입력은 Scanner클래스와 동일 했습니다. 다만 미리 inputstream에 넣어둔 값을 가져오면 되는 것이기 때문에 nextLine()을 통해 바로 값을 가져왔습니다.
Randoms (camp.nextstep.edu.missionutils.Randoms)
private static final Random defaultRandom = ThreadLocalRandom.current();
public static int pickNumberInRange(final int startInclusive, final int endInclusive) {
validateRange(startInclusive, endInclusive);
return startInclusive + defaultRandom.nextInt(endInclusive - startInclusive + 1);
}
private static void validateRange(final int startInclusive, final int endInclusive) {
if (startInclusive > endInclusive) {
throw new IllegalArgumentException("startInclusive cannot be greater than endInclusive.");
}
if (endInclusive == Integer.MAX_VALUE) {
throw new IllegalArgumentException("endInclusive cannot be greater than Integer.MAX_VALUE.");
}
if (endInclusive - startInclusive >= Integer.MAX_VALUE) {
throw new IllegalArgumentException("the input range is too large.");
}
}
시작 값과 끝 값에 대해 예외처리 후 random값을 가져와서 사용했습니다.
후기
함수 내부 함수
함수를 기능별로 분리하는 것에 대해 좀 더 의문이 들었던 미션인거 같습니다. 하나의 함수에 하나의 기능 뿐 아니라 코드를 짜면서 '함수 내부에 함수를 넣는 것이 과연 올바를까?'에 대한 의문이었습니다. 미션 도중 함수에 하나의 기능만을 담으려고 함수를 쓰다보니 함수 내부에 함수를 쓰게 되었고 이렇게 되면 하위 함수의 경우 상위 함수에 종속되는 것이 아닌가를 느끼게 되었습니다.
(이와 관련해서 비슷한 주제로 discussion(아고라)를 진행했던 글이 있어서 가져왔습니다. : 함수 내부 다른 함수 Call )
테스트 코드
기능별 테스트 코드를 어떻게 해야할지 처음에는 감이 잘 안왔습니다. 따라서 제공해준 테스트 코드에 디버깅을 하여 값을 확인했습니다. 그러다 함수를 최대한 많이 분리를 했다는 것을 생각하여 해당 함수 파라미터에 내가 생각하는 상수 값을 강제로 주고 return 값만 예측하여 같은지 확인하면 되지 않을까 라는 생각으로 시도를 해봤습니다. (마지막에 시도해서 마지막 commit들을 보면 전부 test함수 관련입니다 ㅎㅎ,,,)
이렇게 진행하니 여러 장점이 있었습니다.
1. 확실히 함수에 대한 결과 값이 바로 보여서 디버깅 전 함수가 정상 동작하는지 확인 가능했습니다.
2. 함수마다 각개로 확인했기 때문에 문제 해결을 빠르게 진행할 수 있었습니다.
매직 넘버에 대한 처리
매직 넘버 즉 특정 상수 값들을 처리할 때 static final변수대신 ENUM 클래스에 대해 처리를 하는 것도 코드가 깔끔해 져서 좋았던 거 같습니다. 다만 이런 ENUM 클래스를 클래스 내부 nested 클래스 처럼 사용하는 것이 좋은지 아님 외부 클래스로 사용할지에 대해 생각해 보았고, 해당 클래스에 포함된 내용인지 아닌지에 따라 다르게 생각하는 것이 좋다고 생각되었습니다.
추가
github discussions에서 아고라라는 카테고리가 있습니다. 해당 카테고리가 많이 유용하다고 생각했습니다. 제가 미션을 진행하면서 생각했던 내용을 다른 사람의 의견을 통해 스스로 피드백을 할 수 있었고, 제가 생각하지 못하거나 넘어갔던 의문 사항을 다시 한번 끌어내고 생각하게 한 점이 너무 좋았습니다.