공부함
프리코스 2주차 미션 본문
유틸리티 클래스에 대한 고민
지금 2주차 미션을 구현하고 있는데 ....... 유틸리티 클래스를 사용해야 할지 너무 고민이다.
1주차 미션에서는 Printer와 Writer 유틸리티 클래스를 사용했다.
사용한 이유는 정리하자면 이렇다.
- SRP : 단일책임의 원칙 : 하나의 클래스는 하나의 책임만 가져야 한다
- 입,출력은 각각 하나의 기능이다
- 클래스로 분리하면 수정사항이 발생했을 때 하나의 클래스만 관리하면 된다
이러한 이유에서였다.
그런데 1주차 pr 리뷰에 이러한 리뷰가 달렸다.
이러한 리뷰도 달리고 해서 다시 생각해 보니까 굳이 필요한것인가..? 하는 생각도 든다.
그래서 추가로 생각해 보니 이러한 장점도 있을 것 같다.
- System.out.println(START_MENT); 보다 Printer.startMent(); 가 더 가독성이 좋다
- Console.readLine(); 보다 Reader.userInput(); 이 가독성이 좋고, 입력 형태에 대한 검증도 할 수 있다
입력 형태에 대한 검증이라 하면, 입력이 숫자 형태인지 아닌지 등이다. (도메인 로직과 상관없는)
그래서 일단은 2주차도 Printer와 Writer 유틸리티 클래스를 만들어서 진행해보고...
디스코드 채널에 토론하기에도 한번 올려봐야겠다.
상수 클래스 파일 분리
내가 리뷰한 분은 1주차 미션에서 상수 파일을 별도로 분리했다.
이런 식으로 별도의 클래스 파일을 선언해 상수들을 관리했다.
이러한 방식이 훨씬 깔끔해보여서 나도 2주차부터는 이렇게 클래스를 만들어 따로 상수를 관리해야 겠다고 생각했다.
하지만 생각해 보니 나는 Writer 유틸리티 클래스를 만들었고, 출력 상수들은 Writer 클래스에서만 사용된다.
그래서 따로 상수를 분리하기보다는 Writer 클래스 안에 넣는 것이 맞다고 생각한다.
반면 여러 곳에서 공통적으로 쓰이는 상수라면 위 예시 처럼 클래스를 분리하는게 맞다고 생각한다.
String 생성 방법 변경
1주차 미션에서는 String 객체를 +로 이어붙이는 방법으로 모든 문자열을 생성했다.
pr리뷰에서 String.format이나 StringBuilder, StringJoiner 사용을 권유받아 이번주에 사용해 보았다.
그리고 car1 : ---- 와 같이 진행 경과를 출력하는 것을 StringBuilder를 활용했었는데 HYPEN.repeat(position) 형태로 사용하도록 바꿨다.
public final class Cars {
private final List<Car> cars = new ArrayList<>();
public Cars(String[] carNames) {
for (String carName : carNames) {
cars.add(new Car(carName));
}
}
public void attempt() {
for (Car car : cars) {
car.attempt();
}
}
@Override
public String toString() {
StringJoiner stringJoiner = new StringJoiner("\n");
for (Car car : cars) {
stringJoiner.add(car.toString());
}
return stringJoiner.toString();
}
public String winners() {
StringJoiner stringJoiner = new StringJoiner(", ");
Integer winnerPosition = calculateWinnerPosition();
for (Car car : cars) {
Integer carPosition = car.getPosition();
if (carPosition >= winnerPosition) {
stringJoiner.add(car.getName());
}
}
return stringJoiner.toString();
}
private Integer calculateWinnerPosition() {
Integer winnerPosition = 0;
for (Car car : cars) {
Integer carPosition = car.getPosition();
if (carPosition > winnerPosition) {
winnerPosition = carPosition;
}
}
return winnerPosition;
}
}
일급컬렉션 사용
다른 참가자가 공유한 내용 중 객체지향 생활체조에서 일급컬렉션을 사용하라고 해서 사용해 보았다.
https://jojoldu.tistory.com/412
내가 일급컬렉션에 대해 참고한 글은 위 글이다.
위 글에서는 일급컬렉션의 장점으로
1. 비즈니스에 종속적인 자료구조
2. 불변
3. 상태와 행위를 한곳에서 관리
4. 이름이 있는 컬렉션
을 꼽는다.
이번주차 미션에서는 3번 상태와 행위를 한곳에서 관리한다는 것이 도드라진 것 같다.
일급컬렉션을 사용하지 않았다면
for (Car car : carList){
car.attempt();
}
위와 같은 식으로 코드를 짰어야 할 것이다.
public void start() {
Writer.results();
for (int currentAttempt = 1; currentAttempt <= attempts; currentAttempt++) {
cars.attempt();
Writer.cars(cars);
Writer.nextLine();
}
Writer.winner(cars);
}
하지만 일급컬렉션을 사용하면 위처럼 코드를 짤 수 있다.
cars.attempt()만 호출하면 attempt 내에서 필요한 로직을 알아서 호출한다 (cars 대상으로 각각 attempt 호출)
더 깔끔하고 직접 Car에 접근하지 않아서 안전하다.
테스트
테스트코드 짜는데 어려움이 있었다.
첫째로 랜덤 숫자를 얻는 pickNumberInRange를 호출하는 메서드에서 어떻게 랜덤값을 내가 원하는 대로 지정할 지 고민했다.
외부 라이브러리를 사용하면 안되기 때문에 Mockito를 사용할 수는 없었다.
@Test
void 전진_정지() {
assertRandomNumberInRangeTest(
() -> {
run("pobi,woni", "1");
assertThat(output()).contains("pobi : -", "woni : ", "최종 우승자 : pobi");
},
MOVING_FORWARD, STOP
);
}
ApplicationTest에 기본 제공되는 코드다.
private static <T> void assertRandomTest(
final Verification verification,
final Executable executable,
final T value,
final T... values
) {
assertTimeoutPreemptively(RANDOM_TEST_TIMEOUT, () -> {
try (final MockedStatic<Randoms> mock = mockStatic(Randoms.class)) {
mock.when(verification).thenReturn(value, Arrays.stream(values).toArray());
executable.execute();
}
});
}
타고 들어가보면 최종적으로 assertRandomTest를 호출한다
정확히는 모르겠지만 Randoms가 verification에 해당하는 코드를 호출했을 때 value, values를 리턴하게 mock되어 있는 것을 알 수 있다.
@Test
void 지정한_값을_넘는_랜덤_값이_주어지면_포지션_증가() {
Car car = 자동차_생성(NAME_UNDER_LENGTH_LIMIT);
assertRandomNumberInRangeTest(
() -> {
car.attempt();
assertThat(car.getPosition()).isEqualTo(1);
},
MOVING_FORWARD
);
}
따라서 나도 assertRandomNumberInRangeTest를 활용해서 테스트코드를 짰다.
문제는 ApplicationTest에서 어떤 식으로 활용하는 지 보고 사용한 것이지 내가 완전히 이해한 것은 아니라는 것이다..
이와 관련해서 추가적인 공부가 필요할 것 같다
관련해서 다른 참가자 분이 분석해 놓은 글이다.
https://velog.io/@oyoungsun/interface-%ED%99%9C%EC%9A%A9%EA%B8%B0
또 인터페이스를 활용해서 테스트 하는 글이다.
다만 나는 이해가 잘 안가는 부분이, 실제 동작하는 인터페이스랑 테스트 용 인터페이스랑 따로 만들어서 사용하는데, 이러면 제대로 테스트되지 않은게 아닌가? 라는 생각이 든다.
두번째로 테스트상황에서 콘솔 입, 출력을 만들고 싶은 데 방법을 몰랐다.
역시 구글링을 통해 방법을 찾아냈다.
private OutputStream 출력_스트림_생성() {
OutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
return out;
}
출력은 OutputStream을 생성해주고 무언가를 출력하는 메서드 호출 후 OutputStream.toString을 해서 출력 결과를 assertThat으로 비교할 수 있다.
@AfterEach
private void 콘솔_닫기() {
Console.close();
}
private void 입력(String input) {
System.setIn(new ByteArrayInputStream(input.getBytes()));
}
입력은 이렇게 입력할 값들을 input으로 넣어주고 나서 읽는 메서드를 호출하면 된다.
사실 이것도 완벽하게 이해하지는 못했다. 사용 방법을 어떻게 익혀서 사용했을 뿐이다 ..
입력 테스트의 경우 @AfterEach로 Console.close()를 호출해 콘솔을 닫아줘야 했다.
이게 없으면 처음 실행되는 테스트 말고 나머지는 입력값을 입력받지 못했다.
이번 주차 미션을 진행하면서 내가 기본기가 턱없이 부족하다는 것을 느꼈다.
Java를 쓰긴 쓰지만 완벽히 이해하고 쓰는 게 아닌 것 같다. 시간내서 Java 공부를 한번 해야겠다..
'우테코 6기 프리코스' 카테고리의 다른 글
MVC 패턴 (0) | 2023.11.12 |
---|---|
1주차 숫자야구 피드백 (0) | 2023.11.06 |
프리코스 1주차 미션 (2) | 2023.10.29 |