**주의사항** 모든 코드는 내 생각을 바탕으로 작성하였으며, 완벽하지 않을 수 있음. 참고해주시면 감사하겠습니다~~
문자열 계산기 기능 요구사항은 다음과 같다.
먼저, MVC 패턴으로 구현하는 것을 목표로 하였고 의존성을 최대한 만들지 않고자 하였습니다.
파일 형식은 이런 식으로 만들었습니다. 이제 각 파일을 하나 하나 뜯어봅시다!!
* InputView
package view;
import java.util.Scanner;
public class InputView {
private static final Scanner input = new Scanner(System.in);
public String getString() {
return input.next();
}
}
InputView 파일은 따로 설명할 부분은 없습니다. 오직 문자열을 입력 받을 수 있는 기능만을 구현하도록 하였습니다.
* OutputView
package view;
public class OutputView {
public void printNumber(int sum) {
System.out.println(sum);
}
}
OutputView 파일 또한 오직 숫자의 총 합을 출력하도록 구현하였.
* StringCalculator
StringCalculator의 경우, 메서드가 많기 때문에 먼저 모든 코드를 보여주고 설명을 시작하겠습니다.
package domain;
import java.util.ArrayList;
import java.util.List;
public class StringCalculator {
private final String STANDARD_SPLIT_STRINGS = "[,|:]";
private final int INITIAL_NUMBER = 0;
private final int FIRST_NUMBER = 1;
private final int SECOND_NUMBER = 2;
private final int THIRD_NUMBER = 3;
private final int CUSTOM_STRING_SPLIT_NUMBER = 5;
private final String EXCEPTION_MINUS = "음수 입력 불가.";
private final String EXCEPTION_INCORRECT = "제대로 입력하세요.";
private final String CHECK_CUSTOM_OR_STANDARD = "/";
private final String inputStrings;
public StringCalculator(final String inputStrings) {
this.inputStrings = inputStrings;
}
public List<String> parseStrings() {
String checkCustomOrStandard = inputStrings.substring(INITIAL_NUMBER, FIRST_NUMBER);
if (!checkCustomOrStandard.equals(CHECK_CUSTOM_OR_STANDARD)) {
String[] splitStrings = inputStrings.split(STANDARD_SPLIT_STRINGS);
return List.of(splitStrings);
}
String delimiter = inputStrings.substring(SECOND_NUMBER, THIRD_NUMBER);
String divideStrings = inputStrings.substring(CUSTOM_STRING_SPLIT_NUMBER);
String[] splitStrings = divideStrings.split(delimiter);
return List.of(splitStrings);
}
public List<Integer> changeStringsToNumbers(List<String> splitStrings) {
List<Integer> numberList = new ArrayList<>();
try {
for (String stringNumber : splitStrings) {
Integer number = Integer.parseInt(stringNumber);
numberList.add(number);
}
} catch (RuntimeException runtimeException) {
throw new RuntimeException(EXCEPTION_INCORRECT);
}
validateMinusNumber(numberList);
return numberList;
}
private void validateMinusNumber(List<Integer> numberList) {
for (Integer realNumber : numberList) {
if (realNumber < 0) {
throw new RuntimeException(EXCEPTION_MINUS);
}
}
}
public int addNumbers(List<Integer> numberList) {
int sum = INITIAL_NUMBER;
for (int number : numberList) {
sum += number;
}
return sum;
}
}
- 생성자
public StringCalculator(final String inputStrings) {
this.inputStrings = inputStrings;
}
InputView에서 입력받은 문자열을 StringCalculator에서 가공할 수 있도록 위 코드와 같이 생성자를 만들었습니다.
- 문자열 나누기 메서드 : parseString
public List<String> parseStrings() {
String checkCustomOrStandard = inputStrings.substring(INITIAL_NUMBER, FIRST_NUMBER);
if (!checkCustomOrStandard.equals(CHECK_CUSTOM_OR_STANDARD)) {
String[] splitStrings = inputStrings.split(STANDARD_SPLIT_STRINGS);
return List.of(splitStrings);
}
String delimiter = inputStrings.substring(SECOND_NUMBER, THIRD_NUMBER);
String divideStrings = inputStrings.substring(CUSTOM_STRING_SPLIT_NUMBER);
String[] splitStrings = divideStrings.split(delimiter);
return List.of(splitStrings);
}
가장 먼저, 커스텀 구분자 문자열인지 기본 구분자 문자열인지 구분하기 위해 문자열의 첫 부분이 "/" 인지를 기준으로 판단하여 나눌 수 있도록 checkCustomOrStandard 변수를 선언하였습니다.
if 문이 실행되는 조건은 기본 구분자 문자열일 때고, 상수 선언 한 STANDARD_SPLIT_STRINGS를 기준으로 문자열을 나누고 리스트 형식으로 반환할 수 있도록 구성하였습니다.
if 문이 실행되지 않는 조건은 커스텀 구분자 문자열일 때고, 이 때는 커스텀 구분자(delimiter)를 substring 메서드를 통해 알아낸 뒤 커스텀 문자열을 기준으로 문자열을 나눠서 리스트 형식으로 반환되도록 구현하였습니다.
- 문자형식 숫자를 integer 형태로 변환해주는 함수 : changeStringsToNumbers
public List<Integer> changeStringsToNumbers(List<String> splitStrings) {
List<Integer> numberList = new ArrayList<>();
try {
for (String stringNumber : splitStrings) {
Integer number = Integer.parseInt(stringNumber);
numberList.add(number);
}
} catch (RuntimeException runtimeException) {
throw new RuntimeException(EXCEPTION_INCORRECT);
}
validateMinusNumber(numberList);
return numberList;
}
private void validateMinusNumber(List<Integer> numberList) {
for (Integer realNumber : numberList) {
if (realNumber < 0) {
throw new RuntimeException(EXCEPTION_MINUS);
}
}
}
parseStrings 메서드의 반환 값을 매개변수로 받을 수 있도록 하는 형식으로 메서드를 구현하였습니다. 해당 메서드에서 음수일 때와 잘못 입력된 경우 예외처리가 발생될 수 있도록 하였고, 이 메서드 또한 리스트 형식으로 반환될 수 있도록 하였습니다. 예외처리 하는 것에 있어서 뭔가 코드가 어색하게 느껴지는데 , 이 부분은 코드 리뷰를 통해 리펙터링 할 것입니다.(예외처리 메서드는 구현을 많이 못 해봐서 ㅠㅠ)
- 숫자 합을 구하는 메서드 : addNumbers
public int addNumbers(List<Integer> numberList) {
int sum = INITIAL_NUMBER;
for (int number : numberList) {
sum += number;
}
return sum;
}
changeStringsToNumbers 메서드의 반환 값을 매개변수로 받아 sum 값을 도출할 수 있도록 작성한 메서드입니다.
* CalculatorController
package controller;
import domain.StringCalculator;
import view.InputView;
import view.OutputView;
import java.util.List;
public class CalculatorController {
private final InputView inputView;
private final OutputView outputView;
public CalculatorController(InputView inputView, OutputView outputView) {
this.inputView = inputView;
this.outputView = outputView;
}
public void startCalculator(){
String inputStrings = inputView.getString();
StringCalculator stringCalculator = new StringCalculator(inputStrings);
List<String> paresStrings = stringCalculator.parseStrings();
List<Integer> numberList = stringCalculator.changeStringsToNumbers(paresStrings);
outputView.printNumber(stringCalculator.addNumbers(numberList));
}
}
- 생성자 : 생성자 부분에서 InputView와 OutputView 객체를 받을 수 있도록 구현하였는데, 객체의 안정성과 밑에 startCalculator 메서드에서 인풋과 아웃풋 메서드를 자유롭게 사용하고자 이러한 형식으로 작성하였습니다.
- startCalculator : 이 메서드를 통해 컨트롤러에서 인풋 값과 아웃풋 값을 도메인 객체로 넘겨줄 수 있게 하였고, 이를 통해 각 객체의 의존성을 없앴습니다.
*Application
import controller.CalculatorController;
import view.InputView;
import view.OutputView;
public class Application {
public static void main(String[] args) {
InputView inputView = new InputView();
OutputView outputView = new OutputView();
CalculatorController calculatorController = new CalculatorController(inputView, outputView);
calculatorController.startCalculator();
}
}
최종 문자열 계산기 구현은 Application에서 구현하였습니다.
*Test
@Test
void standardStrings_parseStrings() {
//given
final String standardStrings = "1:2:3";
final StringCalculator stringcalculator = new StringCalculator(standardStrings);
final List<String> expected = new ArrayList<>(Arrays.asList("1", "2", "3"));
//when
final List<String> actual = stringcalculator.parseStrings();
//then
assertEquals(expected, actual);
}
@Test
void customStrings_parseStrings() {
//given
final String customStrings = "//;\\n1;2;3";
final StringCalculator stringcalculator = new StringCalculator(customStrings);
final List<String> expected = new ArrayList<>(Arrays.asList("1", "2", "3"));
//when
final List<String> actual = stringcalculator.parseStrings();
//then
assertEquals(expected, actual);
}
parseStrings 메서드의 경우, 1. 기본 구분자 문자열이 들어오는 경우 2. 커스텀 구분자 문자열이 들어오는 경우, 이렇게 총 2가지 경우로 나누어 테스트를 진행하였습니다.
@Test
void standardStrings_changeStringsToNumbers() {
//given
final String standardStrings = "1:2:3";
final StringCalculator stringcalculator = new StringCalculator(standardStrings);
final List<String> splitStrings = stringcalculator.parseStrings();
final List<Integer> expected = new ArrayList<>(Arrays.asList(1, 2, 3));
//when
final List<Integer> actual = stringcalculator.changeStringsToNumbers(splitStrings);
//then
assertEquals(expected, actual);
}
@Test
void customStrings_changeStringsToNumbers() {
//given
final String standardStrings = "//;\\n1;2;3";
final StringCalculator stringcalculator = new StringCalculator(standardStrings);
final List<String> splitStrings = stringcalculator.parseStrings();
final List<Integer> expected = new ArrayList<>(Arrays.asList(1, 2, 3));
//when
final List<Integer> actual = stringcalculator.changeStringsToNumbers(splitStrings);
//then
assertEquals(expected, actual);
}
@Test
void IncorrectInputException_changeStringsToNumbers() {
//given
final String standardStrings = "//;\\nt;2;3";
final StringCalculator stringcalculator = new StringCalculator(standardStrings);
final List<String> splitStrings = stringcalculator.parseStrings();
var expected = "제대로 입력하세요.";
//when
final RuntimeException actual = assertThrows(RuntimeException.class, () -> stringcalculator.changeStringsToNumbers(splitStrings));
//then
assertEquals(expected, actual.getMessage());
}
@Test
void minusInputException_changeStringsToNumbers() {
//given
final String standardStrings = "//;\\n-1;2;3";
final StringCalculator stringcalculator = new StringCalculator(standardStrings);
final List<String> splitStrings = stringcalculator.parseStrings();
var expected = "음수 입력 불가.";
//when
final RuntimeException actual = assertThrows(RuntimeException.class, () -> stringcalculator.changeStringsToNumbers(splitStrings));
//then
assertEquals(expected, actual.getMessage());
}
changeStringsToNumbers 메서드의 경우, 1. 기본 구분자 문자열이 들어오는 경우 2. 커스텀 구분자 문자열이 들어오는 경우 3. 숫자가 아닌 다른 문자열을 입력하여 예외를 발생시키는 경우 4. 문자열에 음수가 포함되어 예외를 발생시키는 경우, 총 4가지 경우로 나누어 테스트를 진행하였습니다.
@Test
void addNumbers() {
//given
final String standardStrings = "//;\\n1;2;3";
final StringCalculator stringcalculator = new StringCalculator(standardStrings);
final List<Integer> numberList = new ArrayList<>(Arrays.asList(1, 2, 3));
final int expected = 6;
//when
final int actual = stringcalculator.addNumbers(numberList);
//then
assertEquals(expected, actual);
}
addNumbers 메서드의 경우, 위에 보이는 코드와 같이 테스트를 진행하였습니다. numberList 변수를 따로 선언하여 바로 메서드에 넣어줬습니다.
테스트하며 느낀점.
사실 단위테스트는 정말 그 메서드만 테스트를 진행해야 하는데, 제가 구현한 코드는 특정 메서드를 테스트 하기 위해 다른 메서드를 사용하는 것 같다는 사실을 알게 되었습니다. 이렇게 하는 것이 허락되는 방법인지 혹은 좋은 방법인지는 잘 모르겠지만 코드 리뷰를 통해 테스트 코드를 수정해보겠습니다.
정리
이 글을 쓰며 코드 분석을 하면서 빈 문자열이 들어왔을 때 0값이 나와야하는 기능을 제대로 구현하지 못하였다는 것을 깨달았습니다.... 그리고 테스트 코드도 부족한 부분이 많이 보이는데, 역시 처음이라 어려운 것 같습니다..이 글을 참고하시어 자바 연습해 볼 분들은 해보셨으면 좋겠고, 코드 리뷰를 통해 리펙터링 하여서 최대한 깔끔한 코드로 다시 돌아오겠습니다. 제 글을 보면서 틀린 부분이 있다면 어떤 형식으로든지 알려주셨으면 좋겠습니다. 자바러들 화이팅~~
'자바 > 초록스터디' 카테고리의 다른 글
(6)초록스터디 로또 1,2 단계 구현 (0) | 2024.05.23 |
---|---|
(5)초록스터디 : 계산기 구현 PR 후기 (0) | 2024.05.22 |
(3)초록스터디 step1 - 초간단 계산기 구현: 계산기 MVC 패턴 구현, JUnit5 테스트 적용 (0) | 2024.05.06 |
(2)초록스터디 step1 - 초간단 계산기 구현 : 계산기 구현과 JUnit5 (0) | 2024.05.04 |
(1)초록스터디 시작! step1 - 초간단 계산기 구현 (0) | 2024.05.04 |