1주차 회고 1탄에서는 미션을 진행하면서 고민한 점, 배운 점, 느낀 점 등을 정리했다면, 2탄에서는 코드 리뷰를 받고 리팩토링한 부분들에 대해 정리해보려 한다!
Controller가 '다른 객체를 생성하는 책임'을 가져야 할까?
AppConfig의 역할에 대해 정확히 알지 못했기 때문에 놓친 부분이다.
기존코드
public class CalculatorController {
private final DelimiterCalculator delimiterCalculator;
public CalculatorController() {
this.delimiterCalculator = new DelimiterCalculator();
}
...
}
public class Application {
public static void main(String[] args) {
CalculatorController calculatorController = new CalculatorController();
calculatorController.run();
}
}
보다시피 CalculatorController의 기본 생성자에서 DelimiterCalculator의 객체를 생성하고 있다.
수정코드
package calculator.config;
import calculator.controller.CalculatorController;
import calculator.model.DelimiterCalculator;
public class AppConfig {
public DelimiterCalculator delimiterCalculator() {
return new DelimiterCalculator();
}
public CalculatorController calculatorController() {
return new CalculatorController(delimiterCalculator());
}
}
package calculator;
import calculator.config.AppConfig;
import calculator.controller.CalculatorController;
public class Application {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
CalculatorController calculatorController = appConfig.calculatorController();
calculatorController.run();
}
}
AppConfig를 따로 만들어 생성자 주입의 역할을 하도록 수정하였다!
재사용되지 않는 변수 선언
기존코드
public void run() {
try {
OutputView.printInput();
String input = InputView.requestInput();
int result = delimiterCalculator.calculate(input);
OutputView.printResult(result);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e);
}
}
input과 result 변수는 각각 한 번만 사용될 뿐, 재사용되지 않는다.
이 부분에 대해 피드백을 받아 아래와 같이 수정하였다.
수정코드
public void run() {
try {
OutputView.printResult(delimiterCalculator.calculate(InputView.requestInput()));
} catch (IllegalArgumentException e) {
throw e;
}
}
다만 가독성 면에서 '수정코드의 가독성이 오히려 안 좋은게 아닌가?'라는 의문이 남는다.
IllegalArgumentException을 잡아 새로운 IllegalArgumentException을 던진다?
기존코드
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e);
}
ㅎㅎ... 완전 실수한 부분
수정코드
} catch (IllegalArgumentException e) {
throw e;
}
DelimiterCalculator가 가지는 여러 역할의 분리
기존코드
package calculator.model;
import static calculator.exception.ErrorMessage.CONSECUTIVE_DELIMITERS;
import static calculator.exception.ErrorMessage.INVALID_INPUT;
import static calculator.exception.ErrorMessage.NEGATIVE_NUMBER;
import static calculator.exception.ErrorMessage.NULL_OR_EMPTY_INPUT;
import java.util.Arrays;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class DelimiterCalculator {
private static final String DELIMITER_REGEX = "[,:]";
private static final Pattern INVALID_DEFAULT_INPUT_PATTERN = Pattern.compile("[^0-9,:]");
private static final Pattern CONSECUTIVE_DELIMITER_PATTERN = Pattern.compile("(,|:){2,}");
/**
* 사용자의 입력 값을 기반으로 계산
*
* @param input 사용자의 입력 값
* @return 계산된 결과 값
* @throws IllegalArgumentException : NULL_OR_EMPTY_INPUT
*/
public int calculate(String input) {
if (input == null || input.isEmpty()) {
throw new IllegalArgumentException(NULL_OR_EMPTY_INPUT);
}
input = input.replace(" ", "");
if (isUsingDefaultDelimiter(input)) {
validateDefaultInput(input);
} else {
validateCustomInput(input);
}
String[] parts = splitInput(input);
return Arrays.stream(parts)
.mapToInt(this::parseNumber)
.sum();
}
/**
* 기본 구분자 사용 여부 판단
*
* @param input 사용자의 입력 값
* @return 기본 구분자 사용 여부
*/
private boolean isUsingDefaultDelimiter(String input) {
return !input.startsWith("//");
}
/**
* 기본 구분자를 사용하는 입력 값 검증 : 숫자와 기본 구분자로만 이루어진 문자열인지, 구분자를 연속으로 사용하지 않는지 판단
*
* @param input 사용자의 입력 값
* @throws IllegalArgumentException : INVALID_INPUT, CONSECUTIVE_DELIMITERS
*/
private void validateDefaultInput(String input) {
if (INVALID_DEFAULT_INPUT_PATTERN.matcher(input).find()) {
throw new IllegalArgumentException(INVALID_INPUT);
}
if (CONSECUTIVE_DELIMITER_PATTERN.matcher(input).find()) {
throw new IllegalArgumentException(CONSECUTIVE_DELIMITERS);
}
}
/**
* 커스텀 구분자를 사용하는 입력 값 검증 : 구분자를 연속으로 사용하지 않는지 판단
*
* @param input 사용자의 입력 값
*/
private void validateCustomInput(String input) {
String delimiter = input.split("\\\\n")[0].substring(2);
input = input.split("\\\\n")[1];
String regex =
"(" + Arrays.stream(delimiter.split("")).map(Pattern::quote).collect(Collectors.joining("|")) + "){2,}";
if (delimiter.isEmpty()) {
throw new IllegalArgumentException(INVALID_INPUT);
}
if (Pattern.compile(regex).matcher(input).find()) {
throw new IllegalArgumentException(CONSECUTIVE_DELIMITERS);
}
}
/**
* 커스텀 구분자 존재 여부 판단 -> 구분자에 따라 split
*
* @param input 사용자의 입력 값
* @return String 배열 : 숫자
*/
private String[] splitInput(String input) {
if (input.startsWith("//")) {
String[] parts = input.split("\\\\n");
String delimiter = parts[0].substring(2);
if (delimiter.length() > 1) {
delimiter = "[" + delimiter + "]";
}
return parts[1].split(delimiter);
}
return input.split(DELIMITER_REGEX);
}
/**
* String -> int 변환
*
* @param part String 값
* @return 변환된 int 값
* @throws IllegalArgumentException : NEGATIVE_NUMBER, INVALID_INPUT
*/
private int parseNumber(String part) {
try {
int num = Integer.parseInt(part);
if (num < 0) {
throw new IllegalArgumentException(NEGATIVE_NUMBER);
}
return num;
} catch (NumberFormatException e) {
throw new IllegalArgumentException(INVALID_INPUT);
}
}
}
계산, 검증, 파싱 등 여러 역할을 DelimiterCalculator 하나에서 처리하고 있었다.
수정코드
package calculator.model;
import static calculator.constants.DelimiterConstants.CUSTOM_START_STRING;
import static calculator.constants.DelimiterConstants.DELIMITER_REGEX;
import java.util.Arrays;
public class DelimiterCalculator {
private final InputValidator validator = new InputValidator();
private final DelimiterParser delimiterParser = new DelimiterParser();
private final NumberParser numberParser = new NumberParser();
public int calculate(String input) {
validator.validateInput(input);
if (input.trim().isEmpty()) {
return 0;
}
input = preprocessInput(input);
String[] parts = splitInput(input);
return sumResult(parts);
}
private boolean isUsingDefaultDelimiter(String input) {
return !input.startsWith(CUSTOM_START_STRING);
}
private String preprocessInput(String input) {
return input.replace(" ", "");
}
private String[] splitInput(String input) {
String[] parts;
if (isUsingDefaultDelimiter(input)) {
validator.validateDefaultInput(input);
parts = input.split(DELIMITER_REGEX);
validator.validateParts(parts);
return parts;
}
String delimiter = delimiterParser.extractDelimiter(input);
String values = delimiterParser.extractValues(input);
validator.validateCustomInput(delimiter, values);
parts = delimiterParser.splitByCustomDelimiter(values, delimiter);
validator.validateParts(parts);
return parts;
}
private int sumResult(String[] parts) {
return Arrays.stream(parts)
.mapToInt(numberParser::parseNumber)
.sum();
}
}
package calculator.model;
import static calculator.constants.DelimiterConstants.DELIMITER_INDEX;
import static calculator.constants.DelimiterConstants.DELIMITER_PREFIX_LENGTH;
import static calculator.constants.DelimiterConstants.SPLIT_REGEX;
import static calculator.constants.DelimiterConstants.VALUE_INDEX;
import java.util.Arrays;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class DelimiterParser {
public String extractDelimiter(String input) {
return input.split(SPLIT_REGEX)[DELIMITER_INDEX].substring(DELIMITER_PREFIX_LENGTH);
}
public String extractValues(String input) {
return input.split(SPLIT_REGEX)[VALUE_INDEX];
}
public String[] splitByCustomDelimiter(String values, String delimiter) {
String regexDelimiter = delimiter.replaceAll(".", "\\\\$0");
String regex = "[" + regexDelimiter + "]";
return values.split(regex);
}
public String createConsecutiveDelimiterRegex(String delimiter) {
return "(" + Arrays.stream(delimiter.split(""))
.map(Pattern::quote)
.collect(Collectors.joining("|")) + "){2,}";
}
}
package calculator.model;
import static calculator.constants.DelimiterConstants.MINUS_CRITERIA;
import static calculator.constants.ErrorMessage.INVALID_INPUT;
import static calculator.constants.ErrorMessage.NEGATIVE_NUMBER;
public class NumberParser {
public int parseNumber(String part) {
try {
int num = Integer.parseInt(part);
if (num < MINUS_CRITERIA) {
throw new IllegalArgumentException(NEGATIVE_NUMBER);
}
return num;
} catch (NumberFormatException e) {
throw new IllegalArgumentException(INVALID_INPUT);
}
}
}
package calculator.model;
import static calculator.constants.DelimiterConstants.CONSECUTIVE_DELIMITER_PATTERN;
import static calculator.constants.DelimiterConstants.INVALID_DEFAULT_INPUT_PATTERN;
import static calculator.constants.ErrorMessage.CONSECUTIVE_DELIMITERS;
import static calculator.constants.ErrorMessage.INVALID_INPUT;
import static calculator.constants.ErrorMessage.NULL_INPUT;
import java.util.regex.Pattern;
public class InputValidator {
private final DelimiterParser delimiterParser = new DelimiterParser();
public void validateInput(String input) {
if (input == null) {
throw new IllegalArgumentException(NULL_INPUT);
}
}
public void validateParts(String[] parts) {
if (parts == null || parts.length == 0) {
throw new IllegalArgumentException(INVALID_INPUT);
}
}
public void validateDefaultInput(String input) {
validateDefaultInputPattern(input);
validateConsecutiveDelimiters(input);
}
public void validateCustomInput(String delimiter, String values) {
validateDelimiter(delimiter);
validateCustomDelimiterConsecutiveness(values, delimiter);
}
private void validateDefaultInputPattern(String input) {
if (INVALID_DEFAULT_INPUT_PATTERN.matcher(input).find()) {
throw new IllegalArgumentException(INVALID_INPUT);
}
}
private void validateConsecutiveDelimiters(String input) {
if (CONSECUTIVE_DELIMITER_PATTERN.matcher(input).find()) {
throw new IllegalArgumentException(CONSECUTIVE_DELIMITERS);
}
}
private void validateDelimiter(String delimiter) {
if (delimiter == null || delimiter.isEmpty()) {
throw new IllegalArgumentException(INVALID_INPUT);
}
}
private void validateCustomDelimiterConsecutiveness(String input, String delimiter) {
String regex = delimiterParser.createConsecutiveDelimiterRegex(delimiter);
if (Pattern.compile(regex).matcher(input).find()) {
throw new IllegalArgumentException(CONSECUTIVE_DELIMITERS);
}
}
}
DelimiterCalculator를 DelimiterCalculator(계산), DelimiterParser(구분자 파싱), NumberParser(숫자 파싱), InputValidator(검증)의 4가지 역할로 구분하였다!
놓친 요구사항
""를 입력하면 0을 반환하는 요구사항을 놓치고 말았다...
기존코드
if (input == null || input.isEmpty()) {
throw new IllegalArgumentException(NULL_OR_EMPTY_INPUT);
}
에러를 던지도록 처리해버렸다 ㅜ.ㅜ
수정코드
if (input.trim().isEmpty()) {
return 0;
}
input을 trim()한 후, 공백("")이라면 0을 반환하도록 하였다.
else의 사용 지양
기존코드
if (isUsingDefaultDelimiter(input)) {
validateDefaultInput(input);
} else {
validateCustomInput(input);
}
수정코드
private String[] splitInput(String input) {
String[] parts;
if (isUsingDefaultDelimiter(input)) {
validator.validateDefaultInput(input);
parts = input.split(DELIMITER_REGEX);
validator.validateParts(parts);
return parts;
}
String delimiter = delimiterParser.extractDelimiter(input);
String values = delimiterParser.extractValues(input);
validator.validateCustomInput(delimiter, values);
parts = delimiterParser.splitByCustomDelimiter(values, delimiter);
validator.validateParts(parts);
return parts;
}
코드를 수정하며 많이 바뀌었지만, if문 내부에서 return을 사용하여 조기반환하도록 수정하였다!
자주 사용하는 String값들의 상수화
따로 DelimiterConstatns라는 클래스에 모아보았다.
package calculator.constants;
import java.util.regex.Pattern;
public class DelimiterConstants {
public static final String DELIMITER_REGEX = "[,:]";
public static final String CUSTOM_START_STRING = "//";
public static final int DELIMITER_PREFIX_LENGTH = 2;
public static final int VALUE_INDEX = 1;
public static final int DELIMITER_INDEX = 0;
public static final String SPLIT_REGEX = "\\\\n";
public static final int MINUS_CRITERIA = 0;
public static final Pattern INVALID_DEFAULT_INPUT_PATTERN = Pattern.compile("[^0-9,:]");
public static final Pattern CONSECUTIVE_DELIMITER_PATTERN = Pattern.compile("(,|:){2,}");
}
InputView 수정
기존코드
package calculator.view;
import camp.nextstep.edu.missionutils.Console;
public class InputView {
public static String requestInput() {
return Console.readLine();
}
}
단순히 Console.readLine()을 한 번 더 감싼 것에 불과했다.
수정코드
package calculator.view;
import camp.nextstep.edu.missionutils.Console;
public class InputView {
private static final String INPUT_PROMPT = "덧셈할 문자열을 입력해 주세요.";
public static String requestInput() {
System.out.println(INPUT_PROMPT);
return Console.readLine();
}
}
OutputView에 있던 입력 안내 문구와 출력 기능을 InputView로 옮겼다!
에러 테스트 시에는 assertEquals() 대신 assertThrownBy() 사용
기존코드
//when & then
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
delimiterCalculator.calculate(input);
});
assertEquals(ErrorMessage.NULL_OR_EMPTY_INPUT, exception.getMessage());
던져진 에러의 메세지와 ErrorMessage를 assertEquals()를 사용하여 비교하였다.
수정코드
//when & then
assertThatThrownBy(() -> delimiterCalculator.calculate(input))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(ErrorMessage.INVALID_INPUT);
assertThatThrownBy()를 사용하여 한 번에 처리하도록 수정하였다!
혼자 구현할 때보다 받은 피드백들에 따라 리팩토링하면서 많은 것을 배운 것 같다!
2주차 미션을 구현할 때는 피드백 받은 점들을 꼭 고려해서 코드를 짜도록 ㅎㅅㅎ...
'우테코 프리코스' 카테고리의 다른 글
[우테코] 7기 프리코스 3주차 회고 (0) | 2024.11.06 |
---|---|
[우테코] 7기 프리코스 2주차 회고 (1) | 2024.10.31 |
[우테코] 7기 프리코스 1주차 회고- 1탄 (2) | 2024.10.22 |
[우테코] 4주차 회고 (0) | 2023.11.23 |
[우테코] 3주차 회고 (1) | 2023.11.23 |