우아한테크코스 첫 주차 서브미션이였던 문자열 계산기를 if문 없이 해결한 과정을 적어보겠습니다.

이번 미션은 단위 테스트 연습을 위한 서브미션이었습니다.
문제는 간단합니다. 3 + 2 \* 4 / 10 문자열을 space 기준으로 split하고 계산합니다.
(사칙 연산 우선순위는 무시합니다)

if문을 사용하면 쉽게 해결할 수 있습니다. 하지만, 'if문을 사용하지 말라'는 추가 미션을 받았고 해결한 과정을 적어보겠습니다.

코드의 구성

  • if문을 이용한 코드
  • 전략 패턴 적용
  • enum 활용
  • Java8 활용
  • 최종 완성본

먼저 if문을 사용한 기존 코드를 보겠습니다.

if문을 이용한 코드

public class TextCalculator {
    public static double calculate(String inputText) {
        String tokens[] = inputText.trim().split(" ");
        double result = toDouble(tokens[0]);

        for (int i = 1; i < tokens.length; i += 2) {
            String operator = tokens[i];
            double number = toDouble(tokens[i + 1]);
            result = calculate(operator, result, number);
        }
        return result;
    }

    private static double calculate(String operator, double result, double number) {
        if (operator.equals("+"))
            result += number;
        else if (operator.equals("-"))
            result -= number;
        else if (operator.equals("*"))
            result *= number;
        else if (operator.equals("/"))
            result /= number;

        return result;
    }

    private static double toDouble(String value) {
        return Double.parseDouble(value);
    }
}

TextCalculator.calculate()가 문자열을 받으면 스페이스를 기준으로 분리한 뒤에 사칙연산을 합니다.

처음에는 if문 없이 연산자를 어떻게 구분하지? 과연 이게 가능할까? 고민한 결과 전략패턴을 이용했습니다.
Calculator interface를 만들어서 각각의 연산을 구현클래스로 만들어서 Map에 담았습니다.

전략패턴 적용한 코드

public interface Calculator {
    double calculate(double num1, double num2);
}

class DivideCalculator implements Calculator {
    @Override
    public double calculate(double num1, double num2) {
        return num1 / num2;
    }
}

class MinusCalculator implements Calculator {
    @Override
    public double calculate(double num1, double num2) {
        return num1 - num2;
    }
}


[...]


public class CalculatorMapping {
    private static Map<String, Calculator> calculatorMap = new HashMap<>();

    static{
        calculatorMap.put("+",new PlusCalculator());
        calculatorMap.put("-",new MinusCalculator());
        calculatorMap.put("*",new MultiplyCalculator());
        calculatorMap.put("/",new DivideCalculator());
    }

    public static Calculator getCalculator(String operator){
        return calculatorMap.get(operator);
    }
}


public class TextCalculator {

        [...]

    private static double calculate(String operator, double result, double number) {
        return CalculatorMapping.getCalculator(operator).calculate(result, number);
    }

}

Map<String, Calculator> Key - 연산자, Value - Calulator를 담아서
map.get(연산자)를 호출하면 해당 연산자에 맞는 Calculator를 받아서 계산해주는 방식으로 했습니다.
해결하고 보니 Servlet의 HandlerMapping이랑 비슷하더군요.
얼마 전에 봤던 자바 웹 프로그래밍 Next Step에서 공부한 부분인데 무의식 속에 남아 있어서 사용한거 같습니다.

해결은 했지만, 코드가 만족스럽지 않았습니다.
'조금 더 가독성이 좋은 방법은 없을까?', '코드를 줄일 수 있지 않을까?' 라는 생각을 거듭하다.

Enum을 사용해봤습니다.

Enum 적용한 코드

public enum Calculator {
    PLUS {
        @Override
        public double calculate(double num1, double num2) {
            return num1 + num2;
        }
    },
    MINUS {
        @Override
        public double calculate(double num1, double num2) {
            return num1 - num2;
        }
    },
    DIVIDE {
        @Override
        public double calculate(double num1, double num2) {
            return num1 / num2;
        }
    },
    MULTIPLY {
        @Override
        public double calculate(double num1, double num2) {
            return num1 * num2;
        }
    };

    public abstract double calculate(double num1, double num2);
}


public class CalculatorMapping {
    private static Map<String, Calculator> calculatorMap = new HashMap<>();

    static {
        calculatorMap.put("+", Calculator.PLUS);
        calculatorMap.put("-", Calculator.MINUS);
        calculatorMap.put("*", Calculator.MULTIPLY);
        calculatorMap.put("/", Calculator.DIVDE);
    }

    public static Calculator getCalculator(String operator) {
        return calculatorMap.get(operator);
    }

enum을 사용하니 조금은 더 간결하고 명시적입니다. 하지만 여전히 코드가 만족스럽지 않았아서 더 좋은 방법을 찾다가 예전에 읽었던 Java Enum 활용기에서 힌트를 얻었습니다.
그 방법은 Java8부터 지원하는 BiFunction과 Lambda를 사용하는 것이었는데요.
평소에는 Lambda를 잘 사용하지 않아서 미처 생각을 못 했는데 사용하니 만족스러운 코드가 나왔습니다.

Java8 api를 사용한 코드

public class Calculator {
    enum Operator {
        PLUS("+", (num1, num2) -> num1 + num2),
        MINUS("-", (num1, num2) -> num1 - num2),
        MULTIPLY("*", (num1, num2) -> num1 * num2),
        DIVIDE("/", (num1, num2) -> num1 / num2);

        private String operator;
        private BiFunction<Double, Double, Double> expression;

        Operator(String operator, BiFunction<Double, Double, Double> expression) {
            this.operator = operator;
            this.expression = expression;
        }

        public double calculate(double num1, double num2) {
            return this.expression.apply(num1, num2);
        }
    }

    private static Map<String, Operator> operators = new HashMap<>();

    static {
        for (Operator value : Operator.values())
            operators.put(value.operator, value);
    }

    public static double calculate(String operator, double num1, double num2) {
        return operators.get(operator).calculate(num1, num2);
    }
}


public class TextCalculator {

        [...]

    private static double calculate(String operator, double result, double number) {
        return Calculator.calculate(operator, result, number);
    }

}

Enum 속성에 연산자와 연산 메소드를 넣어줍니다.
Calculator(Mapping) 클래스는 모든 Operator를 Map에 저장한 다음에 외부에서 calculate 요청이 오면 계산해서 리턴해줍니다.

확실히 더 간결해지지 않았나요?
추가로 Operator enum을 Calculator에 넣어줌으로써 둘이 연관이 되어있다고 명시되어 더 보기가 좋습니다.

첫 번째 버전을 해냈을 때는 해결 했구나 하면서 조금 흐뭇했지만, 조금 더 아름다운 코드를 짜기 위해서 계속 생각해서 드디어 만족하는 마지막 코드가 나왔을 때는 처음으로 내 코드에 만족하는 여태 느껴보지 못한 기쁨을 느꼈습니다.

작년에 Java Enum 활용기를 읽고 '어떻게 이런 멋진 생각을 했을까? 언젠가 한 번 실전에 사용해 보자' 하고 북마크에 저장해둔 것을 이번 기회에 잘 활용할 수 있어서 한 번 더 만족!

*추가 최종 완성본

이 과정에 오기까지 같이 스터디하는 형하고 토론을 하면서 나온 결과물인데요.
형이 우리가 너무 enum에 집착한거 같다. 굳이 enum을 써야할까? 하면서 새로운 버전이 또 나왔습니다.

public class Calculator {
        private static Map<String, BiFunction<Double, Double, Double>> operators = new HashMap<>();

        static {
            operators.put("+", (num1, num2) -> num1 + num2);
            operators.put("-", (num1, num2) -> num1 - num2);
            operators.put("*", (num1, num2) -> num1 * num2);
            operators.put("/", (num1, num2) -> num1 / num2);
        }

        public static double calculate(String operator, double num1, double num2) {
            return operators.get(operator).apply(num1, num2);
        }
}

Map의 value에 enum이 아닌 메소드를 직접 담아줬습니다.

역시 같이 공부하면 더 재미있고 좋은 결과를 보내요. 제가 새로운 해결책을 제시하면 다른 사람이 또 새로운 해결책을 제시하면서 서로 계속 더 나은 방법을 발견하는 과정이 재미있었습니다.

진짜 끝!!

최종적으로 느낀점

  • if문을 사용하지 말라고 한 이유는 다형성 연습이었다.
  • 공부는 같이해야 재밌다.
  • 단위테스트를 작성하면 리팩토링이 쉽다.
  • 아름다운 코드를 짜는 것은 즐겁다.

해결에 도움된 자료들