2주차 코드에 대한 코드 리뷰
감사하게도 정말 많은 분들이 다양하게 코드 리뷰를 해주셨다. 이에 대해 일부 정리를 해보려한다.
public String showExecutionResults(ArrayList<String> carNames, ArrayList<Integer> carGoingCount) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < carNames.size(); i++) {
result.append(carNames.get(i)).append(" : ");
for (int j = 0; j < carGoingCount.get(i); j++) {
result.append("-");
}
result.append("\n");
}
return result.toString();
}
위와 같은 코드에서 이중 for문의 분리에 대한 의견을 여러 분들이 주셨다. 따라서 아래와 같이 안쪽 for문을 메서드로 따로 빼서 가독성을 올렸다.
public String showExecutionResults(ArrayList<String> carNames, ArrayList<Integer> carGoingCount) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < carNames.size(); i++) {
result.append(carNames.get(i)).append(" : ");
appendHyphens(result, carGoingCount.get(i));
}
return result.toString();
}
private void appendHyphens(StringBuilder result, int count) {
for (int i = 0; i < count; i++) {
result.append("-");
}
result.append("\n");
}
전체적으로 주석을 줄이고 메소드, 변수 이름에 좀더 힘을 기울여서 지으면 어떨까 생각하신다는 의견도 매우 감사하다. 주석을 최소화하면서 어떻게 하면 코드 자체만으로 보시는 분들이 이해할 수 있으실지 더 고민해봐야겠다.
public String[] inputCarNames() {
System.out.println(showInputCarNamesPrompt());
String inputList = UserInput();
String[] inputArr = inputList.split(",");
return inputArr;
}
1주차 공통 피드백에서 Java Colletion을 사용하라고 하셨는데, 신경 쓴다고 썼었는데 이렇게 떡하니 배열이 있었다. 코드 리뷰를 달아주셔서 처음 깨달았다. 따라서 아래와 같이 코드를 변경했다.
public static List<String> inputCarNames() {
System.out.println(showInputCarNamesPrompt());
String input = UserInput();
String[] inputs = input.split(",");
List<String> inputArr = new ArrayList<>();
for (String carName : inputs) {
inputArr.add(carName);
}
return inputArr;
}
public void runGame() {
InputView inputView = new InputView();
String[] inputArr = inputView.inputCarNames();
CarValidation carValidation = new CarValidation();
ArrayList<String> carNames = carValidation.carValidateAll(inputArr);
String inputNumber = inputView.inputTryNumber();
NumberValidation numberValidation = new NumberValidation();
int tryNumber = numberValidation.numbervalidateAll(inputNumber);
...
}
2주차 회고록에서도 언급했던 static 메서드에 대해서도 말씀해주셨다. 굳이 인스턴스를 생성할 필요가 없는 경우, 즉 인스턴스 변수를 사용하지 않는 경우에는 static 메서드로 접근하는게 맞다고 생각한다. 따라서 코드를 아래와 같이 변경했다.
public void runGame() {
String[] inputArr = InputView.inputCarNames();
ArrayList<String> carNames = CarValidation.carValidateAll(inputArr);
String inputNumber = InputView.inputTryNumber();
int tryNumber = NumberValidation.numbervalidateAll(inputNumber);
...
}
assertThat(output).isEqualTo("Tom : ---\nBob : -----\nLisa : --\n");
stringBuilder 를 사용하는 건 어떠냐는 의견을 받아들여 아래와 같이 수정했다. 실제 출력값 형태로 보여서 가독성이 더 좋아진 것 같다.
StringBuilder expectedOutput = new StringBuilder();
expectedOutput.append("Tom : ---\n");
expectedOutput.append("Bob : -----\n");
expectedOutput.append("Lisa : --\n");
assertThat(output).isEqualTo(expectedOutput.toString());
public class Constants {
public static final int MAX_CAR_NAME_LENGTH = 5;
public static final int MIN_TRY_NUMBER = 1;
public static final int MIN_RANDOM_NUMBER = 0;
public static final int MAX_RANDOM_NUMBER = 9;
public static final int FORWARD_CONDITION = 4;
public static final String NAME_CHARACTER_PATTERN = "^[a-zA-Z0-9]*$";
}
Constants 파일에 리턴 메세지들도 빼는게 어떻겠냐는 의견이 있었고 생각해보니 맞는 거 같아서 코드를 수정했다. 따라서 메세지를 메서드를 통해 분리하던 부분이 삭제됐고 Constants 파일에 메세지들이 추가됐다.
public class Constants {
public static final int MAX_CAR_NAME_LENGTH = 5;
public static final int MIN_TRY_NUMBER = 1;
public static final int MIN_RANDOM_NUMBER = 0;
public static final int MAX_RANDOM_NUMBER = 9;
public static final int FORWARD_CONDITION = 4;
public static final String NAME_CHARACTER_PATTERN = "^[a-zA-Z0-9]*$";
public static final String CAR_NAMES_PROMPT = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)";
public static final String TRY_NUMBER_PROMPT = "시도할 회수는 몇회인가요?";
public static final String EXECUTION_RESULTS_HEADER = "실행 결과";
public static final String WINNER_MESSAGE = "최종 우승자 : ";
}
public void runGame() {
List<String> inputArr = InputView.inputCarNames();
ArrayList<String> carNames = CarValidation.carValidateAll(inputArr);
String inputNumber = InputView.inputTryNumber();
int tryNumber = NumberValidation.numbervalidateAll(inputNumber);
System.out.println();
Race race = new Race();
race.initialize(carNames);
OutputView outputView = new OutputView();
System.out.println(Constants.EXECUTION_RESULTS_HEADER);
for (int i = 0; i < tryNumber; i++) {
race.runRace();
System.out.println(outputView.showExecutionResults(race.getCarNames(), race.getCarGoingCount()));
}
System.out.println(outputView.showWinner(race.winners()));
}
runGame() 의 핵심 로직들을 함수로 분리하면 좋겠다는 의견도 있었다. 너무 많은 기능을 수행한다는 뜻. 따라서 코드를 아래와 같이 변경했다.
public void runStart() {
List<String> inputArr = InputView.inputCarNames();
List<String> carNames = CarValidation.carValidateAll(inputArr);
String inputNumber = InputView.inputTryNumber();
int tryNumber = NumberValidation.numbervalidateAll(inputNumber);
System.out.println();
Race race = new Race();
race.initialize(carNames);
runRaceAndPrintResults(race, tryNumber);
}
private void runRaceAndPrintResults(Race race, int tryNumber) {
System.out.println(Constants.EXECUTION_RESULTS_HEADER.getValue());
for (int i = 0; i < tryNumber; i++) {
race.runRace();
System.out.println(OutputView.showExecutionResults(race.getCarNames(), race.getCarGoingCount()));
}
System.out.println(OutputView.showWinner(race.winners()));
}
public class Constants {
public static final int MAX_CAR_NAME_LENGTH = 5;
public static final int MIN_TRY_NUMBER = 1;
public static final int MIN_RANDOM_NUMBER = 0;
public static final int MAX_RANDOM_NUMBER = 9;
public static final int FORWARD_CONDITION = 4;
public static final String NAME_CHARACTER_PATTERN = "^[a-zA-Z0-9]*$";
public static final String CAR_NAMES_PROMPT = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)";
public static final String TRY_NUMBER_PROMPT = "시도할 회수는 몇회인가요?";
public static final String EXECUTION_RESULTS_HEADER = "실행 결과";
public static final String WINNER_MESSAGE = "최종 우승자 : ";
}
상수 파일을 enum 클래스로 관리하는건 어떻겠냐는 의견이 있었고 아래와 같이 코드를 변경했다.
public enum Constants {
MAX_CAR_NAME_LENGTH(5),
MIN_TRY_NUMBER(1),
MIN_RANDOM_NUMBER(0),
MAX_RANDOM_NUMBER(9),
FORWARD_CONDITION(4),
NAME_CHARACTER_PATTERN("^[a-zA-Z0-9]*$"),
CAR_NAMES_PROMPT("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"),
TRY_NUMBER_PROMPT("시도할 회수는 몇회인가요?"),
EXECUTION_RESULTS_HEADER("실행 결과"),
WINNER_MESSAGE("최종 우승자 : ");
private final Object value;
Constants(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
반복적인 public static final 을 없앨 수 있고 열거형이다보니 단순 나열보다는 한눈에 더 쉽게 들어온다. 또한 기본 생성자 타입이 private이기 때문에 인스턴스 생성과 상속을 방지하여 상수값의 타입안정성이 보장된다. 그리고 키워드 enum을 사용하기때문에 구현의 의도가 열거임을 분명하게 알 수 있는 장점이 있다.
public void runRace() {
List<Integer> randomNumbersList = new ArrayList<>();
for (int j = 0; j < carNames.size(); j++) {
int randomNumber = Randoms.pickNumberInRange((int) Constants.MIN_RANDOM_NUMBER.getValue(),
(int) Constants.MAX_RANDOM_NUMBER.getValue());
randomNumbersList.add(randomNumber);
}
for (int j = 0; j < randomNumbersList.size(); j++) {
if (randomNumbersList.get(j) >= (int) Constants.FORWARD_CONDITION.getValue()) {
carGoingCount.set(j, carGoingCount.get(j) + 1);
}
}
}
for문 보다는 stream을 사용하면 가독성이 더 좋아질 거 같다는 의견에 한 번 stream으로 코드를 다시 작성해보았다.
public void runRace() {
List<Integer> randomNumbersList = carNames.stream()
.map(name -> Randoms.pickNumberInRange((int) Constants.MIN_RANDOM_NUMBER.getValue(),
(int) Constants.MAX_RANDOM_NUMBER.getValue()))
.collect(Collectors.toList());
IntStream.range(0, randomNumbersList.size())
.filter(j -> randomNumbersList.get(j) >= (int) Constants.FORWARD_CONDITION.getValue())
.forEach(j -> carGoingCount.set(j, carGoingCount.get(j) + 1));
}
- List<Integer> randomNumbersList = carNames.stream()...
- carNames 리스트의 각 요소에 대한 스트림을 생성한다.
- map 연산을 사용하여 각 자동차의 랜덤 숫자를 계산한다.
- collect(Collectors.toList())를 사용하여 각 자동차의 랜덤 숫자를 수집하고 randomNumbersList 리스트에 저장한다. 이 리스트는 각 자동차에 대한 랜덤 숫자 목록을 나타낸다.
- IntStream.range(0, randomNumbersList.size())...
- 0부터 randomNumbersList의 크기 - 1 까지의 범위에 대한 IntStream을 생성합니다. 이 범위는 0부터 자동차의 개수까지의 정수를 생성한다.
- filter를 사용하여 랜덤 숫자가 전진 조건 (상수 FORWARD_CONDITION보다 크거나 같은지)을 충족하는 자동차를 필터링한다. j는 현재 자동차의 인덱스를 나타낸다.
- forEach를 사용하여 각 전진한 자동차에 대해 carGoingCount 리스트의 해당 인덱스를 증가시킨다. 이를 통해 각 자동차의 전진 횟수를 추적할 수 있다.
테스트 케이스들을 어노테이션으로 더 간편하게 만들 수 있다는 의견도 있었다. valueSource을 더 알아보라고 권유하셨다. valueSource는 아래와 같은 상황에서 유용했다.
@Test
@DisplayName("User 생성 name 2자 미만 예외처리")
void createUserException01() {
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new User(VALID_EMAIL, "q", password));
assertThat(e.getMessage()).isEqualTo(NAME_NOT_MATCH_MESSAGE);
}
@Test
@DisplayName("User 생성 name 10자 초과 예외처리")
void createUserException02() {
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new User(VALID_EMAIL, "qwerasdfzxcv", password));
assertThat(e.getMessage()).isEqualTo(NAME_NOT_MATCH_MESSAGE);
}
@Test
@DisplayName("User 생성 name 숫자 포함 예외처리")
void createUserException03() {
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new User(VALID_EMAIL, "qq23", password));
assertThat(e.getMessage()).isEqualTo(NAME_NOT_MATCH_MESSAGE);
}
위와 같이 같은 경우에 대해 테스트를 하는데 입력값만 다를 경우 아래와 같이 valueSource를 통해서 코드를 간단하게 만들 수 있다.
@ParameterizedTest
@ValueSource(strings = {"q", "qwerasdfzxcv", "qq23"})
void createUserException(String name){
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new User(VALID_EMAIL, name, password));
assertThat(e.getMessage()).isEqualTo(NAME_NOT_MATCH_MESSAGE);
}
하지만 내 코드의 경우 유효성 검사는 같은 메서드를 호출하지만, 그 메서드 안에서 각각의 유효성 검증 메서드가 있기 때문에 각 예외 처리에 대한 출력값 역시 다르다. 따라서 valueSource 만으로 입력값만 간편하게 묶어주는 방법은 각각의 다른 출력값을 표현하지 못한다. 좀 더 공부해보니 이에 대한 해결방안은 각 입력값에 대응하는 출력메세지를 함께 묶어주는 methodSource 라는 방법이다.
@Test
void validateAll_too_longCarNames() {
// given
List<String> validCarNames = Arrays.asList("Car123454", "Car5", "Car1");
// when & then
assertThatThrownBy(() -> carValidation.carValidateAll(validCarNames))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("자동차 이름이 5자 이하가 아닙니다: " + validCarNames.get(0));
}
@Test
void validateAll_invalidCarNames() {
// given
List<String> validCarNames = Arrays.asList("Car@4", "Car5$", "Car1");
// when & then
assertThatThrownBy(() -> carValidation.carValidateAll(validCarNames))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("특수문자가 포함된 문자열이 입력되었습니다: " + validCarNames.get(0));
}
@Test
void testValidateDuplication() {
// given
List<String> validCarNames = Arrays.asList("Car2", "Car2", "Car1");
// when & then
assertThatThrownBy(() -> carValidation.carValidateAll(validCarNames))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("중복된 이름이 입력되었습니다: " + validCarNames.get(1));
}
위와 같은 내 코드를 methodSource 방법을 통해 간단하게 만들 수 있다.
static Stream<Arguments> invalidParameters() {
return Stream.of(
Arguments.of("Car1", "Car123454", "Car3", "자동차 이름이 5자 이하가 아닙니다: Car123454"),
Arguments.of("Car1", "Car@4", "Car3", "특수문자가 포함된 문자열이 입력되었습니다: Car@4"),
Arguments.of("Car1", "Car1", "Car2", "중복된 이름이 입력되었습니다: Car1")
);
}
@ParameterizedTest
@MethodSource("invalidParameters")
void validateAll(String car1, String car2, String car3, String errorMessage) {
// given
List<String> validCarNames = Arrays.asList(car1, car2, car3);
// when & then
assertThatThrownBy(() -> CarValidation.carValidateAll(validCarNames))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(errorMessage);
}
에러메세지 역시 앞선 상수들처럼 enum 클래스로 관리해줬다.
public enum ErrorMessages {
CAR_NAME_LENGTH_EXCEED("자동차 이름이 5자 이하가 아닙니다: %s"),
NAME_CHARACTER_PATTERN_MISMATCH("특수문자가 포함된 문자열이 입력되었습니다: %s"),
DUPLICATE_NAME("중복된 이름이 입력되었습니다: %s"),
NOT_A_NUMBER("숫자가 아닌 값을 입력하셨습니다."),
MINIMUM_TRY_NUMBER("잘못된 시도 횟수입니다.");
private final String message;
ErrorMessages(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
public String winners() {
List<Integer> carGoingCountCopy = new ArrayList<>(carGoingCount);
Collections.sort(carGoingCountCopy, Collections.reverseOrder());
int maxCount = carGoingCountCopy.get(0);
StringJoiner joiner = new StringJoiner(", ");
for (int i = 0; i < carGoingCount.size(); i++) {
if (maxCount == carGoingCount.get(i)) {
joiner.add(carNames.get(i));
}
}
return joiner.toString();
}
메서드를 분리하면 좋겠다는 의견이 나왔고 따라서 maxCount를 추출하는 부분을 분리했다.
public String winners() {
int maxCount = maxCountGenerator();
StringJoiner joiner = new StringJoiner(", ");
for (int i = 0; i < carGoingCount.size(); i++) {
if (maxCount == carGoingCount.get(i)) {
joiner.add(carNames.get(i));
}
}
return joiner.toString();
}
private int maxCountGenerator() {
List<Integer> carGoingCountCopy = new ArrayList<>(carGoingCount);
Collections.sort(carGoingCountCopy, Collections.reverseOrder());
int maxCount = carGoingCountCopy.get(0);
return maxCount;
}
이외에도 Car 클래스를 따로 만들어서 runRace() 부분을 수행하거나, 변경에 용이하게 인터페이스 타입 지정에 대해서도 말씀해주셨다. 이 부분은 추후에 다시 해보기로 기약하며 여기서 글을 마치겠다.