본문 바로가기
우테코 프리코스

[우테코] 7기 프리코스 1주차 회고 - 2탄

by ♡˖GYURI˖♡ 2024. 10. 23.
728x90

 

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주차 미션을 구현할 때는 피드백 받은 점들을 꼭 고려해서 코드를 짜도록 ㅎㅅㅎ...