ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [과제] 계산기 Lv3
    내일배움캠프/과제 2025. 3. 6. 12:33

     

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

     

    1️⃣ 과제 조건

    • 현재 사칙연산 계산기는 (➕,➖,✖️,➗) 이렇게 총 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 강의에서 람다 & 스트림을 학습 및 복습 하시고 적용

     

    2️⃣ 주요 개선 및 구현 포인트

     

    1. 제네릭을 통한 유연한 타입 지원

    • 제네릭
      • 계산에 사용되는 피연산자와 결과값의 타입을 제네릭으로 처리하여, Double뿐만 아니라 Integer, Float 등 다양한 Number 타입에 대응
      • ArithmeticCalculator<T extends Number & Comparable<T>>와 같은 형태로 제네릭 타입 T에 대한 제한을 걸어, 숫자와 비교 기능을 모두 지원

    2. Enum을 통한 연산 기호 관리 및 연산 처리

    • OperatorType Enum
      • 사칙연산 기호(+, -, *, /)를 Enum으로 관리하며, 입력된 기호에 대해 올바른 연산 타입을 매핑
      • 각 Enum 상수마다 자신의 기호를 가지고 있으며, findByOperator() 메서드로 문자열 입력을 Enum 타입으로 변환하고, apply() 제네릭 메서드를 통해 NumberOperator 인터페이스를 활용하여 연산을 수행

    3. 람다 표현식 및 스트림 API 활용

    • 람다 표현식
      • CalculatorController 생성자에서 Double 타입에 대해 Double::parseDouble을 전달하는 등, 람다 표현식을 활용해 코드를 간결하게 작성
    • 스트림 API
      • ArithmeticCalculator의 getSearchList() 메서드에서는 스트림을 이용해 사용자가 입력한 값보다 큰 결과들만 필터링하여 반환

    1-4. 모듈화 및 책임 분리

    • 계산 로직 분리
      • Lv2와 마찬가지로 계산 관련 로직은 ArithmeticCalculator 클래스에서 담당하며, 사용자 입력 처리 및 프로그램 흐름은 CalculatorController에서 처리
    • 입력 처리 모듈
      • InputHandler 클래스를 도입해 사용자 입력을 읽고 검증하는 로직을 분리하여, 코드의 재사용성과 유지보수성을 높임

     

    3️⃣ 최종 코드

    1. App 클래스

    프로그램의 시작점으로, Double 타입을 사용해 계산기를 실행
    컨트롤러 생성 시, Double 파싱 함수와 Double 연산 구현체(DoubleOperator)를 전달

    public class App {
        public static void main(String[] args) {
            // Double로 구현 – 상황에 따라 다양한 타입으로 사용 가능
            CalculatorController<Double> controller = new CalculatorController<>(Double::parseDouble, new DoubleOperator());
            controller.run();
        }
    }

     

    2. ArithmeticCalculator 클래스

    제네릭을 사용해 계산 결과들을 Result 객체로 저장하는 리스트를 관리

    • calculate() 메서드
      • 두 피연산자와 OperatorType Enum을 받아 연산을 수행하고 결과를 저장
    • getSearchList() 메서드
      • 스트림을 사용해 입력된 값보다 큰 결과들을 필터링해 반환
    import java.util.ArrayList;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class ArithmeticCalculator<T extends Number & Comparable<T>> {
    
        // 계산 결과들을 Result 객체로 저장하는 리스트
        private final List<Result<T>> resultList = new ArrayList<>();
        private final NumberOperator<T> numberOperator;
    
        public ArithmeticCalculator(NumberOperator<T> numberOperator) {
            this.numberOperator = numberOperator;
        }
    
        /**
         * 두 피연산자와 연산자 기호를 받아 계산 후, 결과를 Result 객체로 저장.
         */
        public void calculate(T num1, T num2, OperatorType opType) {
            if (opType == null) {
                throw new IllegalArgumentException("\n! 올바른 사칙연산 기호를 입력해주세요 !\n");
            }
            T value = opType.apply(num1, num2, numberOperator);
            Result<T> result = new Result<>(num1, num2, value, opType);
            resultList.add(result);
        }
    
        public Result<T> getResult() {
            if (!resultList.isEmpty()) {
                return resultList.get(resultList.size() - 1);
            }
            return null;
        }
    
        public List<Result<T>> getResultList() {
            return resultList;
        }
    
        public int getResultListSize() {
            return resultList.size();
        }
    
        public boolean removeResult(int index) {
            int resultListSize = this.getResultListSize();
            if (index <= 0 || index > resultListSize) {
                return false;
            }
            resultList.remove(index - 1);
            return true;
        }
    
        public List<Result<T>> getSearchList(T searchValue) {
            return resultList.stream()
                    .filter(res -> res.getResult().compareTo(searchValue) > 0)
                    .collect(Collectors.toList());
        }
    }

     

    3. CalculatorController 클래스

    사용자와의 입출력을 담당하는 컨트롤러로, InputHandler를 이용해 입력받은 데이터를 처리하고, ArithmeticCalculator를 통해 연산을 수행

    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.IOException;
    import java.util.List;
    import java.util.function.Function;
    
    public class CalculatorController<T extends Number & Comparable<T>> {
    
        private final ArithmeticCalculator<T> calculator;
        private final InputHandler<T> inputHandler;
    
        // 생성자 주입
        public CalculatorController(Function<String, T> converter, NumberOperator<T> operator) {
            calculator = new ArithmeticCalculator<>(operator);
            inputHandler = new InputHandler<>(new BufferedReader(new InputStreamReader(System.in)), converter);
        }
    
        public void run() {
            System.out.println("계산기를 시작합니다.");
            System.out.println("계산기는 exit 입력 시 종료됩니다.");
    
            while (true) {
                try {
                    T num1 = inputHandler.getValidNumber("첫번째 숫자를 입력해주세요 : ");
                    T num2 = inputHandler.getValidNumber("두번째 숫자를 입력해주세요 : ");
                    OperatorType operatorType = inputHandler.getValidOperator("사칙연산 기호를 입력하세요(+, -, *, /) : ");
    
                    calculator.calculate(num1, num2, operatorType);
                    Result<T> result = calculator.getResult();
                    System.out.println(result.toString());
    
                    if (inputHandler.isYes("기존 저장된 결과 값들을 보시겠습니까? (Y/N) : ")) {
                        int i = 1;
                        for (Result<T> res : calculator.getResultList()) {
                            System.out.println(i++ + ". " + res.toString());
                        }
                    }
    
                    int resultListSize = calculator.getResultListSize();
                    if (resultListSize > 0) {
                        if (inputHandler.isYes("기존 저장된 결과 값을 삭제하시겠습니까? (Y/N) : ")) {
                            while (true) {
                                System.out.print("삭제할 결과 번호를 입력하세요 : ");
                                int index = Integer.valueOf(inputHandler.readInput());
                                if (!calculator.removeResult(index)) {
                                    System.out.println("\n! 올바른 결과 번호를 입력해주세요 !\n");
                                    continue;
                                }
                                System.out.println("결과 값 삭제가 완료되었습니다.");
                                break;
                            }
                        }
                    }
    
                    if (!inputHandler.isYes("계속 계산하시겠습니까? (Y/N) : ")) {
                        if (inputHandler.isYes("\n계산 결과 중 입력 받은 값 보다 큰 결과를 검색하시겠습니까? (Y/N) : ")) {
                            T searchValue = inputHandler.getValidNumber("검색할 숫자를 입력해주세요 : ");
                            List<Result<T>> resultList = calculator.getSearchList(searchValue);
                            if (resultList.isEmpty()) {
                                System.out.println("검색 결과가 없습니다.");
                                break;
                            }
                            int i = 1;
                            for (Result<T> res : resultList) {
                                System.out.println(i++ + ". " + res.toString());
                            }
                        }
                        break;
                    }
                    System.out.println();
                } catch (NumberFormatException e) {
                    System.out.println("\n! 숫자 형식이 올바르지 않습니다 !\n");
                } catch (IllegalArgumentException e) {
                    System.out.println("\n! 올바른 사칙연산 기호를 입력해주세요 !\n");
                } catch (ArithmeticException e) {
                    System.out.println("\n! 나눗셈 연산에서 분모(두번째 숫자)에 0이 입력될 수 없습니다 !\n");
                } catch (IOException e) {
                    System.out.println("입력 처리 중 오류가 발생했습니다.");
                    break;
                }
            }
            System.out.println("계산기가 종료되었습니다.");
        }
    }

     

    4. DoubleOperator 클래스

    Double 타입에 대해 NumberOperator 인터페이스를 구현하여 사칙연산을 수행
    예외 처리도 포함해 0으로 나누는 경우를 처리

    public class DoubleOperator implements NumberOperator<Double> {
    
        @Override
        public Double add(Double num1, Double num2) {
            return num1 + num2;
        }
    
        @Override
        public Double subtract(Double num1, Double num2) {
            return num1 - num2;
        }
    
        @Override
        public Double multiply(Double num1, Double num2) {
            return num1 * num2;
        }
    
        @Override
        public Double divide(Double num1, Double num2) {
            if (num2 == 0) {
                throw new ArithmeticException("\n! 나눗셈 연산에서 분모(두번째 정수)에 0이 입력될 수 없습니다 !\n");
            }
            return num1 / num2;
        }
    }

     

    5. InputHandler 클래스

    BufferedReader와 제네릭 컨버터를 활용하여, 사용자 입력을 받고 검증하는 로직을 담당

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.util.function.Function;
    
    public class InputHandler<T extends Number> {
    
        private static final String EXIT_COMMAND = "exit";
        private final BufferedReader br;
        private final Function<String, T> converter;
    
        public InputHandler(BufferedReader br, Function<String, T> converter) {
            this.br = br;
            this.converter = converter;
        }
    
        public String readInput() throws IOException {
            String input = br.readLine().trim();
            if (input.equalsIgnoreCase(EXIT_COMMAND)) {
                System.out.println("계산기가 종료되었습니다.");
                System.exit(0);
            }
            return input;
        }
    
        public T getValidNumber(String prompt) throws IOException {
            T number;
            while (true) {
                System.out.print(prompt);
                try {
                    number = converter.apply(this.readInput());
                    break;
                } catch (NumberFormatException e) {
                    System.out.println("\n! 숫자를 입력해주세요 !\n");
                }
            }
            return number;
        }
    
        public OperatorType getValidOperator(String prompt) throws IOException {
            OperatorType op = null;
            while(op == null) {
                System.out.print(prompt);
                op = OperatorType.findByOperator(this.readInput());
            }
            return op;
        }
    
        public boolean isYes(String prompt) throws IOException {
            String input = "";
            while(!"Y".equalsIgnoreCase(input) && !"N".equalsIgnoreCase(input)) {
                System.out.print(prompt);
                input = this.readInput();
            }
            return "Y".equalsIgnoreCase(input);
        }
    }

     

    6. NumberOperator 인터페이스 및 OperatorType Enum

    NumberOperator 인터페이스는 사칙연산에 대한 추상 메서드를 정의하고,

    OperatorType Enum은 각 연산 기호에 맞는 연산 함수를 apply() 제네릭 메서드로 실행

    public interface NumberOperator<T extends Number> {
        T add(T num1, T num2);
        T subtract(T num1, T num2);
        T multiply(T num1, T num2);
        T divide(T num1, T num2);
    }
    public enum OperatorType {
        ADD("+"),
        SUBTRACT("-"),
        MULTIPLY("*"),
        DIVIDE("/");
    
        private final String operator;
    
        OperatorType(String operator) {
            this.operator = operator;
        }
    
        public String getOperator() {
            return operator;
        }
    
        public static OperatorType findByOperator(String operator) {
            for (OperatorType op : OperatorType.values()) {
                if (op.getOperator().equalsIgnoreCase(operator)) {
                    return op;
                }
            }
            return null;
        }
    
        public <T extends Number> T apply(T num1, T num2, NumberOperator<T> op) {
            return switch (this) {
                case ADD -> op.add(num1, num2);
                case SUBTRACT -> op.subtract(num1, num2);
                case MULTIPLY -> op.multiply(num1, num2);
                case DIVIDE -> op.divide(num1, num2);
            };
        }
    }

     

    7. Result 클래스

    계산 결과를 담을 객체로, 두 숫자, 연산 기호, 그리고 최종 결과를 저장하며 toString()을 Overide하여 보기 쉽게 출력

    public class Result<T extends Number & Comparable<T>> {
        private final T num1;
        private final T num2;
        private final T result;
        private final OperatorType operatorType;
    
        public Result(T num1, T num2, T value, OperatorType operatorType) {
            this.num1 = num1;
            this.num2 = num2;
            this.result = value;
            this.operatorType = operatorType;
        }
    
        public T getNum1() {
            return num1;
        }
    
        public T getNum2() {
            return num2;
        }
    
        public T getResult() {
            return result;
        }
    
        @Override
        public String toString() {
            return num1 + " " + operatorType.getOperator() + " " + num2 + " = " + result;
        }
    }

     

    '내일배움캠프 > 과제' 카테고리의 다른 글

    [과제] 키오스크 필수 Lv3  (0) 2025.03.13
    [과제] 키오스크 필수 Lv2  (1) 2025.03.13
    [과제] 키오스크 필수 Lv1  (0) 2025.03.13
    [과제] 계산기 Lv2  (0) 2025.03.06
    [과제] 계산기 Lv1  (1) 2025.03.05
Designed by Tistory.