[Java] 사칙연산 계산기 만들기 (3)

조민경·2025년 1월 10일

Java

목록 보기
7/8

🌐 github 주소
https://github.com/JoeMinKyung/java_Calculator

3. Enum, 제네릭, 람다 & 스트림을 이해한 계산기 만들기

✅ 현재 사칙연산 계산기는 (➕,➖,✖️,➗) 이렇게 총 4가지 연산 타입으로 구성되어 있습니다.

  • Enum 타입을 활용하여 연산자 타입에 대한 정보를 관리하고 이를 사칙연산 계산기 ArithmeticCalculator 클래스에 활용 해봅니다.

  • 예시 코드

public enum OperatorType {
    /* 구현 */
}

public class ArithmeticCalculator /* Hint */ {
		/* 수정 */
}

✅ 실수, 즉 double 타입의 값을 전달 받아도 연산이 수행하도록 만들기

  • 키워드 : 제네릭

    • 단순히, 기존의 Int 타입을 double 타입으로 바꾸는 게 아닌 점에 주의하세요!
  • 지금까지는 ArithmeticCalculator, 즉 사칙연산 계산기는 양의 정수(0 포함)를 매개변수로 전달받아 연산을 수행

  • 피연산자를 여러 타입으로 받을 수 있도록 기능을 확장

    • ArithmeticCalculator 클래스의 연산 메서드(calculate)
  • 위 요구사항을 만족할 수 있도록 ArithmeticCalculator 클래스를 수정합니다. (제네릭)

    • 추가적으로 수정이 필요한 다른 클래스나 메서드가 있다면 같이 수정 해주세요.
  • 예시 코드

public class ArithmeticCalculator /* Hint */ {
		/* 수정 */
}

✅ 저장된 연산 결과들 중 Scanner로 입력받은 값보다 큰 결과값 들을 출력

  • ArithmeticCalculator 클래스에 위 요구사항을 만족하는 조회 메서드를 구현합니다.

  • 단, 해당 메서드를 구현할 때 Lambda & Stream을 활용하여 구현합니다.

    • Java 강의에서 람다 & 스트림을 학습 및 복습 하시고 적용 해보세요!
  • 추가) 람다 & 스트림 학습을 위해 여러 가지 조회 조건들을 추가하여 구현 해보시면 학습에 많은 도움이 되실 수 있습니다.



💻 정답 코드

ArithmeticCalculator.java

package com.example.calculator3;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class ArithmeticCalculator {
    private final List<Double> results = new ArrayList<>();

    // 연산 수행
    public <T extends Number> double calculate(T a, T b, Calculator3.OperatorType operatorType) {
        double result = operatorType.apply(a, b).doubleValue();
        addResult(result);
        return result;
    }

    // 연산 결과 추가
    private void addResult(double result) {
        results.add(result);
    }

    // 연산 결과를 가져오기
    public List<Double> getResults() {
        return new ArrayList<>(results);
    }

    // 연산 결과 기록 출력
    public void printResults() {
        System.out.println("=========== 연산 기록 ===========");
        for (int i = 0; i < results.size(); i++) {
            System.out.println((i + 1) + ". " + results.get(i));
        }
    }

    // 가장 먼저 저장된 연산 결과 삭제
    public void removeResult() {
        if (!results.isEmpty()) {
            results.remove(0);
        } else {
            System.out.println("삭제할 연산 결과가 없습니다.");
        }
    }

    // 주어진 값보다 큰 연산 결과들 출력 (람다 & 스트림 사용)
    public void printResultsGreaterThan(double value) {
        List<Double> greaterResults = results.stream()
                .filter(result -> result > value)
                .collect(Collectors.toList());

        if (greaterResults.isEmpty()) {
            System.out.println("입력한 값보다 큰 결과는 없습니다.");
        } else {
            System.out.println("=== 입력한 값보다 큰 연산 결과들 ===");
            IntStream.range(0, results.size())
                    .forEach(i -> System.out.println((i + 1) + ". " + results.get(i)));

        }
    }
}

Calculator3.java

package com.example.calculator3;

public class Calculator3 {
    // 사칙연산을 표현하기 위한 Enum 클래스
    public enum OperatorType {
        // 덧셈 연산
        ADD('+') {
            @Override
            public <T extends Number> T apply(T a, T b) {
                return (T) Double.valueOf(a.doubleValue() + b.doubleValue());
            }
        },
        // 뺄셈 연산
        SUBTRACT('-') {
            @Override
            public <T extends Number> T apply(T a, T b) {
                return (T) Double.valueOf(a.doubleValue() - b.doubleValue());
            }
        },
        // 곱셈 연산
        MULTIPLY('*') {
            @Override
            public <T extends Number> T apply(T a, T b) {
                return (T) Double.valueOf(a.doubleValue() * b.doubleValue());
            }
        },
        // 나눗셈 연산
        DIVIDE('/') {
            @Override
            public <T extends Number> T apply(T a, T b) {
                if (b.doubleValue() == 0) { // 0으로 나누려는 경우 예외 발생
                    throw new ArithmeticException("나눗셈 연산에서 분모가 0입니다.");
                }
                return (T) Double.valueOf(a.doubleValue() / b.doubleValue());
            }
        };

        private final char symbol;

        // Enum 생성자: 각 연산자에 해당하는 기호를 초기화
        OperatorType(char symbol) {
            this.symbol = symbol;
        }

        public char getSymbol() {
            return symbol;
        }

        // 추상 메서드: 각 연산자가 고유의 로직으로 구현해야 함
        public abstract <T extends Number> T apply(T a, T b);

        // 입력된 기호에 해당하는 OperatorType 반환
        public static OperatorType fromSymbol(char symbol) {
            for (OperatorType type : OperatorType.values()) {
                if (type.getSymbol() == symbol) {
                    return type;
                }
            }
            throw new IllegalArgumentException("유효하지 않은 연산자입니다: " + symbol);
        }
    }
}

App.java

package com.example.calculator3;

import java.util.Scanner;

public class App {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        ArithmeticCalculator calc = new ArithmeticCalculator();

        while (true) {
            // 첫번째 숫자 입력받기
            double firstNum = getPositiveNumber(sc, "첫 번째 숫자를 입력하세요: ");
            // 두번째 숫자 입력받기
            double secondNum = getPositiveNumber(sc, "두 번째 숫자를 입력하세요: ");
            // 사칙연산 기호 입력받기
            char operatorSymbol = getOperator(sc);

            // 연산 수행 및 출력하기
            try {
                Calculator3.OperatorType operatorType = Calculator3.OperatorType.fromSymbol(operatorSymbol);
                double result = calc.calculate(firstNum, secondNum, operatorType);
                System.out.println("결과: " + result);
            } catch (ArithmeticException | IllegalArgumentException e) {
                System.out.println("오류: " + e.getMessage());
            }

            if (!continueOrExit(sc, calc)) {
                break;
            }
        }
    }

    private static double getPositiveNumber(Scanner sc, String prompt) {
        double number;
        while (true) {
            System.out.print(prompt);
            String input = sc.next();

            try {
                number = Double.parseDouble(input);
                if (number >= 0) {
                    return number;
                } else {
                    System.out.println("0 이상의 숫자를 입력해주세요.");
                }
            } catch (NumberFormatException e) {
                System.out.println("유효한 숫자를 입력해주세요.");
            }
        }
    }

    private static char getOperator(Scanner sc) {
        while (true) {
            System.out.print("사칙연산 기호를 입력하세요: ");
            char operator = sc.next().charAt(0);

            if ("+-*/".indexOf(operator) != -1) {
                return operator;
            }
            System.out.println("잘못된 사칙연산 기호입니다. 다시 입력해주세요.");
        }
    }

    private static boolean continueOrExit(Scanner sc, ArithmeticCalculator calc) {
        while (true) {
            System.out.println("연산 결과 조회: 1, 연산 결과 삭제: 2, 입력한 값보다 큰 결과 출력: 3, 더 계산하려면 아무 키나 입력해주세요. (exit 입력 시 종료)");

            String input = sc.next();

            if (input.equals("exit")) {
                return false; // 메서드 종료
            } else if (input.equals("1")) {
                // 연산 결과 조회
                calc.printResults();
            } else if (input.equals("2")) { // 연산 결과 삭제
                System.out.println("가장 먼저 저장된 데이터를 삭제합니다. 삭제하시겠습니까? (y, n)");
                String response = sc.next();
                if (response.equalsIgnoreCase("y")) {
                    calc.removeResult();
                } else if (response.equalsIgnoreCase("n")) {
                    continue;
                } else {
                    // "y"나 "n"이 아닌 다른 입력이 들어왔을 때 메시지 출력하고 다시 입력받기
                    System.out.println("y 또는 n을 눌러주세요.");
                    continue; // 다시 "y" 또는 "n" 입력을 기다림
                }
            } else if (input.equals("3")) {
                System.out.print("입력한 값보다 큰 연산 결과를 조회합니다. 기준 값을 입력하세요: ");
                double value = sc.nextDouble();
                calc.printResultsGreaterThan(value);
            } else {
                return true;
            }
        }
    }
}



💡 코드 해설

App.java
App 클래스는 프로그램의 시작점으로, 사용자가 입력한 숫자와 연산자를 바탕으로 계산을 수행하고 결과를 출력하는 역할을 한다.

1. 반복문을 통한 계산기 실행:

  • 사용자에게 첫 번째와 두 번째 숫자를 입력받고, 연산자(+, -, *, /)를 입력받는다.

  • 사용자가 입력한 값에 따라 계산을 수행하고, ArithmeticCalculator 클래스의 calculate 메서드를 호출한다.

  • calculate 메서드는 OperatorType Enum을 사용하여 연산을 수행하며, 결과를 results 리스트에 저장한다.

2. 연산 결과 조회 및 삭제:

  • 사용자가 1을 입력하면 기존에 수행된 계산 기록을 조회하고, 2를 입력하면 가장 먼저 저장된 연산 결과를 삭제한다.

  • 3을 입력하면 입력한 값보다 큰 연산 결과를 조회한다. 이때, Stream API를 사용하여 조건에 맞는 값을 필터링한다.

3. 중단 조건:

  • 사용자가 exit를 입력하면 계산기를 종료한다.

ArithmeticCalculator.java
이 클래스는 실제로 계산을 수행하는 로직을 담당한다.

1. 제네릭 메서드 calculate:

  • calculate 메서드는 두 숫자(T 타입)를 받아서 연산을 수행한다. 제네릭을 사용하여 Integer, Double 등 여러 타입에 대해 동작할 수 있게 확장되었다.

  • 연산 결과는 double로 반환되며, 연산 결과는 results 리스트에 저장됩니다.

2. 결과 출력:

  • printResults() 메서드는 저장된 모든 연산 결과를 출력한다.

  • printResultsGreaterThan() 메서드는 사용자가 입력한 값보다 큰 결과만 출력한다. 이때, Stream API를 사용하여 결과를 필터링하고, IntStream.range()를 사용하여 결과를 번호와 함께 출력한다.

3. 연산 결과 삭제:

  • removeResult() 메서드는 가장 먼저 저장된 연산 결과를 삭제한다.

Calculator3.java
OperatorType Enum 클래스는 사칙연산을 정의하고, 각 연산에 해당하는 기호(+, -, *, /)를 처리하는 로직을 구현한다.

1. Enum과 연산 로직:

  • ADD, SUBTRACT, MULTIPLY, DIVIDE 각각의 연산에 대해 apply() 메서드를 구현하여 연산을 처리한다.

  • DIVIDE에서는 분모가 0일 경우 ArithmeticException을 발생시킨다.

2. fromSymbol() 메서드:

  • 사용자가 입력한 연산 기호(+, -, *, /)에 맞는 OperatorType Enum을 반환한다.


💥 트러블 슈팅

1. Enum의 메서드 구현

  • 문제: Enum에서 연산을 정의하기 위해 apply()와 같은 메서드를 구현해야 했다. 각 연산에 대한 로직을 Enum 안에 구현하는 것이 처음에는 직관적이지 않아서 어려웠다.

  • 해결: Enum은 자체적으로 메서드를 가질 수 있기 때문에 연산 로직을 각 Enum 상수 내에 정의하는 방식이 매우 유용하다는 것을 깨달았다. 따라서 각 연산에 대해 별도의 클래스를 만들지 않고, Enum 내에서 한 곳에서 관리하도록 수정하였다.


2. 제네릭과 Enum의 결합

  • 문제: calculate() 메서드에서 제네릭을 사용하여 다양한 숫자 타입(Integer, Double)에 대해 동작하도록 해야 했다. 하지만 Enum에서 제네릭을 처리하는 방식이 다소 복잡하여 어려움을 겪었다.

  • 해결: Enum 상수에서 각 연산을 수행할 때, Number 타입을 doubleValue() 메서드를 사용하여 처리할 수 있었다. 이는 Integer, Double, Long 등 다양한 숫자 타입에 대해 안전하게 동작하게 해주었다.


3. 기호에 맞는 Enum 찾기

  • 문제: 사용자가 입력한 기호에 맞는 OperatorType을 찾는 과정에서 fromSymbol() 메서드를 구현할 때, 해당 기호에 대응하는 Enum 값을 찾는 과정이 직관적이지 않아 어려움을 겪었다.

  • 해결: Enumvalues() 메서드를 활용하여 모든 Enum 상수를 순회하면서 사용자가 입력한 기호와 일치하는 Enum을 찾는 방법을 사용했다. 예외가 발생할 수 있으므로, 이를 처리하는 로직을 추가했다.


4. 예외 처리

  • 문제: 나눗셈에서 0으로 나누는 예외 처리나 잘못된 연산자가 들어오는 경우에 대한 예외 처리도 Enum 내부에서 수행해야 했다. 예외 처리 로직을 구현하는 과정에서 코드가 길어지고 복잡해지는 문제가 있었다.

  • 해결: ArithmeticExceptionIllegalArgumentException을 적절히 사용하여 예외를 처리하고, 각 연산에 대한 예외 처리를 Enum에서 처리하도록 했다.



✍ 고민 및 어려웠던 부분

😮 제네릭을 사용하는 것에 대한 고민

Calculator3 클래스에서 OperatorTypeapply() 메서드에서 제네릭을 사용하고 있다. 제네릭을 사용해서 T extends Number로 피연산자를 처리하도록 구현되어 있는데, 실제로 이 메서드는 반환 값이 항상 double 타입이고, 내부에서 Double.valueOf()를 사용하여 double로 형변환을 수행하고 있다. 따라서 제네릭을 사용하는 것이 의미가 없다는 고민이 생겼다. 일단 문제 조건에서 제너릭을 사용하라해서 코드를 짰지만, 내 코드가 억지로 제너릭을 사용한 느낌이었다.

제네릭은 보통 다양한 타입을 처리할 수 있도록 설계된 방법이다. 예를 들어, T extends Number를 사용하면 Integer, Double, Float 등 다양한 숫자 타입을 처리할 수 있도록 할 수 있다. 하지만 apply() 메서드에서 결국 Double.valueOf()를 사용하여 반환 타입을 double로 고정시키고 있기 때문에, 제네릭을 사용하는 것이 불필요해 보인다. return할 때 오류가 나서 반환 타입을 double로 고정한 것이 문제인지 뭐가 문제인지 고민이다.

형변환과 제네릭: 제네릭을 사용하여 T를 처리하고 있지만, 반환 시 double로 형변환하고 있기 때문에 사실상 제네릭을 사용할 이유가 없어 보인다. apply() 메서드는 사실상 double 연산을 위한 메서드이기 때문이다.

피드백을 받기엔 시간이 부족해서 일단 튜터님의 과제 해설 강의를 보고 수정할 예정이다..!

0개의 댓글