티스토리 뷰
단위 테스트 실습 - 문자열 계산기
- 다음 요구사항을 JUnit을 활용해 단위 테스트 코드를 추가해 구현한다.
요구사항
- 사용자가 입력한 문자열 값에 따라 사칙연산을 수행할 수 있는 계산기를 구현해야 한다.
- 문자열 계산기는 사칙연산의 계산 우선순위가 아닌 입력 값에 따라 계산 순서가 결정된다. 즉, 수학에서는 곱셈, 나눗셈이 덧셈, 뺄셈 보다 먼저 계산해야 하지만 이를 무시한다.
- 예를 들어 "2 + 3 * 4 / 2"와 같은 문자열을 입력할 경우 2 + 3 * 4 / 2 실행 결과인 10을 출력해야 한다.
힌트
- 문자열을 입력 받은 후(scanner의 nextLine() 메소드 활용) 빈 공백 문자열을 기준으로 문자들을 분리해야 한다.
String value = scanner.nextLine();
String[] values = value.split(" ");
- 문자열을 숫자로 변경하는 방법
int number = Integer.parseInt("문자열");
들어가기전에...
객체 지향에 대해서 잘 모르는 개발자가 처음에 아주 이상하게 짠 코드가 많습니다.....
마지막 시도에 그나마 조금은 괜찮아지니 감안해주시길..
1차시도
연산자를 미리 정의해놓고
매칭되는 연산자가 있으면 operate 클래스를 따로 만들어서
→ 각 연산하는 함수 plus, minus, multifly, division 을 호출하려고 했다.
public class Calculator {
public static int result;
public String[] operations = {"+", "-", "*", "/"};
public int calculator(String input) {
// 전체 문자열을 입력받는다.
Scanner scanner = new Scanner(input);
String inputValue = scanner.nextLine();
// 공백으로 분리한다.
String[] values = inputValue.split(" ");
// [1,+,2,*,10,=]
result = Integer.parseInt(values[0]);
// 이중 포문이 이상해
for (String value : values) {
for (String operation : operations) {
// 문자열이면 다음 숫자까지 같이 전달한다?????
// 반복문인데 다음 숫자를 어케 전달하니?
// 연산자 + 다음 숫자 같이 넘겨야함!
if (value.equals(operation))
operate(operation, value);
}
}
return result;
}
public int plus(String input) {
return result += Integer.parseInt(input); // 이전 결과 + 새로 입력받은 숫자
}
}
2차시도
@2023-03-19
- 각 함수를 호출하는게 의미가 없는거 같아서 제거하고, operation 클래스에서 직접 계산한 결과 리턴하도록 했다.
- 연산자 하나 있는 식
“1+2”
까지는 제대로 계산되는데 늘어나면 틀린다. “1+2*3”
불가능애초에 이렇게 배열을 이중 포문 돌려서 연산하는 방법 자체가 틀린거 같다…- 으악아아가!! 어떻게 뜯어고쳐야할까 ?
- 받아온 숫자 배열에서 이중 포문을 돌려서 이전 배열 위치값 (예를들어 3은 세번째 숫자 배열[2]번지)을 알아와야 하는데 그건 너무 비효율적인 코드가 될거 같은 느낌
public class Calculator {
public static int result;
public int calculate(String input) {
// 입력받은 문자열을 정규식으로 특수문자를 거른다. 숫자 배열 / 연산식 배열
String[] nums = input.split("\\D+"); // 숫자
String[] operations = input.split("[\\d]+");
if (operations[0].isEmpty()) {
operations = Arrays.copyOfRange(operations, 1, operations.length);
}
for (String operation : operations) {
// 이전 배열 몇번째 까지 연산했는지 위치 정보가 필요하다.
for (String num : nums) {
// 처음 숫자
if (result == 0) {
result = Integer.parseInt(num);
continue;
}
operate(operation, num);
break;
}
}
return result;
}
private int operate(String operation, String input) {
switch (operation){
case "+":
return result += Integer.parseInt(input); // 이전 결과 + 새로 입력받은 숫자
case "-":
return result -= Integer.parseInt(input);
case "*":
return result *= Integer.parseInt(input);
case "/":
return result /= Integer.parseInt(input);
}
return result;
}
}
3차시도 - 연산식 Enum 클래스 적용
enum 클래스를 사용하면 열거형으로 상수 집합으로 사용할 수도 있고, 여기에 따로 연산까지 할 수 있는걸 발견했다!
가장 깔끔하고 객체지향적인 코드를 만들 수 있을거 같아 enum 을 사용하는게 정답인듯 하다.
effective java를 사랑합시다...
@2023-03-20
// enum 클래스를 사용해서 그 일에 대한 책임을 갖고 있는 객체가 상태(값)와 행위(로직)를 가질 수 있다.
enum Operator {
PLUS(left, right -> left + right),
MINUS(left, right -> left - right),
MULTIFLY(left, right -> left * right),
DIVISION(left, right -> left / right);
private Function<Integer, Integer> expression;
Operator(Function<Integer, Integer> expression) {
this.expression = expression;
}
public int calculator(int left, int right) {
return expression.apply(left, right);
}
}
public class CalculatorConsumer {
public static void main(String[] args) {
int left = 1;
int right = 2;
int plus = Operator.PLUS.calculator(left, right); // NEP 발생 !!
}
}
* 함수형 인터페이스 Function 을 사용하면서 NullPointerException가 발생했다.
PLUS, MINUS, MULTIFLY, DIVISION 에 각각 두 개의 매개변수를 가지는 람다 표현식을 사용했다.
그런데 Operator 클래스의 생성자는 두 개의 매개변수가 필요하지 않아서 NullPointerException이 발생한다.
→ Operator 생성자에 매개변수를 추가하여 람다 표현식에서 필요한 매개변수를 받도록 변경해야 한다.
* BiFunction : 두 개의 입력 매개변수와 하나의 출력 값을 가지는 함수형 인터페이스
BiFunction 로 Operator 생성자와 calculator 메소드에서 두 개의 매개변수를 받도록 한다.
4차시도(1) - 함수형 인터페이스
@2023년 3월 21일
Comparator
인터페이스는 주어진 두 개의 인수를 비교하는 함수를 정의하는 데 사용되지만, 이 경우 비교하는 것이 아니라 연산을 수행하므로 Comparator
대신 BinaryOperator<Integer>
를 사용해야 한다.
또, symbol
을 생성자에 추가했으므로 각 열거형 상수에서 해당 심볼도 전달해야 한다.
4차시도(2)-입력받은 식 split 하는 클래스 생성
@2023년 3월 21일
public class Expression {
private final String expression;
public Expression(String expression) {
this.expression = expression;
}
public String[] splitByBlank() {
return expression.split(" ");
}
}
public static void main(String[] args) {
Expression expression = new Expression("3 + 5 - 2");
String[] result = expression.splitByBlank();
for (String element : result) {
System.out.println(element);
}
}
splitByBlank
메서드는 인자를 받지 않고 메서드 내에서 인스턴스 변수this.expression
을 사용하여 split 한다.- 객체를 생성할 때 한 번만
expression
을 설정하고, 여러 번splitByBlank()
메서드를 호출할 수 있다.
public class Expression_old {
private final String expression;
Expression(String expression) {
this.expression = expression;
}
public String[] splitByBlank(String expression) {
return expression.split(" ");
}
}
Expression, Expression_old 차이점
- Expression_old :
splitByBlank
메서드를 호출할 때마다 새로운 문자열을 전달해야 한다.. 또한 인스턴스 변수expression
의 값을 사용하지 않는다. - Expression : 객체 생성 시
expression
값을 설정하고, 이후splitByBlank
메서드를 호출할 때 인자를 전달하지 않고, 인스턴스 변수expression
의 값이 메서드 내에서 사용됩니다.
→ Expression에서 처럼 Expression
객체의 상태를 사용하는게 좀 더 객체 지향적이다!!
4차시도(3) - 연산식 호출해서 계산하는 Calculator 클래스
@2023년 3월 21일
"1 + 2 * 3"
[0] 1
[1] +
[2] 2
[3] *
[4] 3
- [0][1][2] 가장 처음 계산식 = result
- [0][1][2] 의 result [3][4] = result2
그럼 다음과 같은 플로우로 가져오게 된다.
- 배열에서 0번지를 가져온다. -> 초기값 left
- 1번지 -> 연산자
- 2번지 -> right
- 의 결과를 구한다 -> 다시 left
- 3번지 -> 연산자
- 4번지 -> right
int left = 0;
String operator = "";
left = arr[0];
operator = arr[1];
right = arr[2];
left = Calculator.calculate(operator, left, right);
operator = arr[3];
right = arr[4];
int result2 = Calculator.calculate(operator, left, right);
이제 반복문으로 돌리자!
import java.util.ArrayList;
import java.util.List;
public class RefactoredCalculator {
public static void main(String[] args) {
Object[] arr = {1, "+", 2, "-", 3}; // 예시 입력값입니다.
int result = calculateWithCollection(arr);
System.out.println("Result: " + result);
}
public static int calculateWithCollection(Object[] arr) {
List<Integer> numbers = new ArrayList<>();
List<String> operators = new ArrayList<>();
for (int i = 0; i < arr.length; i++) {
if (i % 2 == 0) {
numbers.add((Integer) arr[i]);
} else {
operators.add((String) arr[i]);
}
}
int result = numbers.get(0);
for (int i = 0; i < operators.size(); i++) {
result = Calculator.calculate(operators.get(i), result, numbers.get(i + 1));
}
return result;
}
}
public class Calculator {
public static int calculate(String operator, int left, int right) {
switch (operator) {
case "+":
return left + right;
case "-":
return left - right;
case "*":
return left * right;
case "/":
return left / right;
default:
throw new IllegalArgumentException("Invalid operator: " + operator);
}
}
}
최종 정리 - Expression, Operation, Calculator 세가지 클래스로 구분
1. return 타입 List<Object> 로 변환
public class Expression {
private final String expression;
public Expression(String expression) {
this.expression = expression;
}
public List<Object> splitByBlank() {
return Arrays.stream(expression.split(" "))
.map(String::new)
.collect(Collectors.toList());
}
}
2. static Operator getOperator 로 연산자를 가져온다.
public enum Operator {
// enum 클래스를 사용해서 그 일에 대한 책임을 갖고 있는 객체가 상태(값)와 행위(로직)를 가질 수 있다.
PLUS("+", (left, right) -> left + right),
MINUS("-", (left, right) -> left - right),
MULTIPLY("*", (left, right) -> left * right),
DIVISION("/", (left, right) -> left / right);
private final BinaryOperator<Double> expression;
private final String symbol;
Operator(String symbol, BinaryOperator<Double> expression) {
this.symbol = symbol;
this.expression = expression;
}
public Double calculate(double left, double right) {
return expression.apply(left, right);
}
public static Operator getOperator(String symbol) {
return Stream.of(Operator.values())
.filter(op -> op.symbol.equals(symbol))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Invalid operator symbol: " + symbol));
}
}
3. left → symbol → right 반복하면서 연산
class Calculator {
public static Double calculate(List<Object> input) {
double result = Double.parseDouble((String) input.get(0));
for (int i = 1; i < input.size(); i += 2) {
String operatorSymbol = input.get(i).toString();
Operator operator = Operator.getOperator(operatorSymbol);
double right = Double.parseDouble((String) input.get(i + 1));
result = operator.calculate(result, right);
}
return result;
}
}
[구현 후기]
TDD 단위 테스트 실습문제고 첫번째 문제라서 만만하게 생각했는데 정말 큰코 다쳤다.
문제 자체가 그냥 객체 지향적으로 생각하게 하는 훈련이었음을... 처음에 무작정 만들려고 할 때는 미처 몰랐다.
테스트 코드 작성을 훈련하고 싶었는데 오히려 자바 객체지향에서 막히는거 보고 정말 자괴감이 들었지만 🥲 기초 공사부터 부족한 것을 알게 되었다.
앞으로 남은 훈련들이 많은데 첫단계부터 이렇게 쩔쩔매면 뒤엔 얼마나 어려울지 걱정되지만
얼마나 더 나를 클린하게 만들어줄지도 기대된다!
나중에 이 문제를 리팩토링하는 눈이 커져있으면 좋겠다.
- Total
- Today
- Yesterday
- 자바 어플리케이션 실행 과정
- n+1
- MultipleBagFetchException
- 스프링오류
- port
- spring boot 3
- 추상클래스
- redisson 분산락
- bucket4j
- addFilterBefore
- ChatGPT
- 오블완
- dto 클래스 생성자
- 티스토리챌린지
- array
- JPA
- junit5
- MongoDB
- FetchJoin
- jvm warm-up 전략
- Java
- 스프링 스케줄링
- 배열
- Kotlin
- QueryDSL
- checkout
- Cannot construct instance of
- Linux
- Spring Security
- Git
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |