티스토리 뷰

단위 테스트 실습 - 문자열 계산기

  • 다음 요구사항을 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

  1. [0][1][2] 가장 처음 계산식 = result
  2. [0][1][2] 의 result [3][4] = result2

 

그럼 다음과 같은 플로우로 가져오게 된다.

  1. 배열에서 0번지를 가져온다. -> 초기값 left
  2. 1번지 -> 연산자
  3. 2번지 -> right
  4. 의 결과를 구한다 -> 다시 left
  5. 3번지 -> 연산자
  6. 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
링크
«   2024/05   »
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
글 보관함