[내일배움캠프 Spring_3기] CH2 계산기과제 Step3

jiiim_ni·2026년 1월 15일

오늘은 Step3(도전과제)를 진행해보았다.

OperatorType.java 구현 과정

Step2까지의 계산기에서는 연산자를 char 타입( + , - , * , /)으로 입력받아 switch 문으로 분기 처리했다.

switch (operator) {
    case '+': ...
    case '-': ...
}

이 방식은 동작에는 문제가 없지만
연산자 종류가 늘어나면 관리가 어려워지고, 잘못된 연산자 입력에 대한 처리가 분산되며 하나의 개념으로 관리한다. 는 느낌이 부족하다.

그래서 Step3에서는 Enum을 활용해 연산자 타입을 관리하도록 요구하고 있다.

OperatorType enum 설계

사칙연산은 고정된 값을 가지므로 Enum을 사용하기에 좋다

package com.example.calculator.step3;

public enum OperatorType {
    ADD('+'),
    SUB('-'),
    MUL('*'),
    DIV('/');

    private final char symbol;

    OperatorType(char symbol) {
        this.symbol = symbol;
    }

    public char getSymbol() {
        return symbol;
    }

    public static OperatorType fromSymbol(char symbol) {
        for (OperatorType type : values()) {
            if (type.symbol == symbol) return type;
        }
        throw new IllegalArgumentException("지원하지 않는 연산자입니다.");
    }
}

enum 상수 정의

ADD('+'),
SUB('-'),
MUL('*'),
DIV('/');
  • 사칙연산을 영어 이름으로 정의를 했다.
  • 연산자는 실제 입력되는 기호를 함께 가진다.

필드와 생성자

private final char symbol;

OperatorType(char symbol) {
    this.symbol = symbol;
}
  • enum도 클래스이기 때문에 필드와 생성자를 가질 수 있다.
  • 각 연산 타입은 자신이 어떤 기호와 연결되어 있는지 알 수 있다.

fromSymbol 메서드

public static OperatorType fromSymbol(char symbol) {
    for (OperatorType type : values()) {
        if (type.symbol == symbol) return type;
    }
    throw new IllegalArgumentException("지원하지 않는 연산자입니다.");
}
  • 이 메서드의 역할은 사용자가 입력한 연산자를 그에 대응하는 OperatorType으로 변환시켜주는 것이다.

입력은 char지만, 내부 로직은 enum 기반으로 동작하게 만든다.

이 구조 덕분에 잘못된 연산자 입력은 한 곳에서 예외 처리가 가능하고 계산 로직에서는 enum만 신경 쓰면 된다.

기존에 Step2에서 계산 메서드는

calculate(int num1, int num2, char operator)

이런 형태였다.

Step3에서는
1. 연산자를 char로 입력받고
2. OperatorType.fromSymbol()로 enum으로 변환한 뒤
3. 계산기 클래스에 enum을 전달한다.

Enum 적용의 장점

  • 연산자 타입을 하나의 개념으로 묶어 관리할 수 있다.
  • switch 분기의 기준이 명확해진다.
  • 잘못된 연산자 예외 처리가 깔끔해진다.
  • 이후 연산이 추가되더라도 확장성이 좋아진다.

트러블슈팅

OperatorType.java를 커밋한 뒤
git push origin main을 실행했지만 오류가 발생했다.

해결을 시도하려고 git pull origin main을 실행했지만 또 오류가 발생하였다.

해결방법: rebase 사용

rebase 과정에서 충돌은 발생하지 않았고, 정상적으로 push 까지 완료할 수 있었다.


calculator.java

calculator.java를 수정하여
Enum 기반 연산, 제네릭으로 다양한 숫자 타입 처리, Stream/Lambda로 조회
까지 구현했다.

Step2에서는

  • calculate(int, int, car)
  • 결과저장 - List< Integer >
    이렇게 했지만

Step3에서는

  • 연산자는 OperatorType(Enum)으로 관리
  • 숫자 타입은 double도 받을 수 있도록 확장 -> 제네릭 < T extends Number >로 확장
  • 저장된 결과 중에서 기준값보다 큰 값만 조회 가능(Stream, Lambda 사용)
    이렇게 진행했다.

결과 저장

private final List<Double> results = new ArrayList<>();
  • Step2에서는 List< Integer >이었지만 Step3에서는 double로 변경했다.
  • final을 사용해 리스트 자체를 바꾸지 못하게 하였다.(내부 값만 관리)

calculate 메서드 - 제너릭

public <T extends Number> double calculate(T num1, T num2, OperatorType operator)
  • T extends Number로 설정하면 다양한 숫자 타입을 받을 수 있다.

doubleValue()

double a = num1.doubleValue();
double b = num2.doubleValue();
  • Number가 제공하는 doubleValue()를 통해 계산 가능한 상태로 변환했다.(제네릭 타입 T는 그대로 연산이 안됨)

Enum 기반 연산

switch (operator) {
    case ADD: ...
}
  • Step2에서는 char로 했다면, Step3에서는 OperatorType(enum)으로 했다.
  • 이렇게 하면 연산자 관리가 한 곳에서 이루어져 유지보수에 편하다.

예외 처리

if (b == 0) {
    throw new IllegalArgumentException("0으로 나눌 수 없습니다.");
}
  • 0으로 나누는 상황을 막음
  • 예외를 던지면 App에서 잡아서 메시지를 출력하도록 구성

결과 저장

results.add(result);
  • Step2와 동일하게 결과는 리스트에 저장한다.

Stream, Lambda 조회

return results.stream()
        .filter(r -> r > value)
        .collect(Collectors.toList());
  • Stream()으로 리스트를 흐름으로 만들고
    filter(r -> r > value)로 조건에 맞는 값만 걸러냄.
  • collect(...)로 다시 리스트로 모아 반환

Calculator.java 전체 코드

package com.example.calculator.step3;

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

public class Calculator {

    // 실수도 저장해야 하므로 Double로 변경
    private final List<Double> results = new ArrayList<>();

    // 제네릭, Enum 적용
    public <T extends Number> double calculate(T num1, T num2, OperatorType operator) {

        double result = 0;

        // 제네릭(T)을 실제 계산 가능한 double로 바꿈
        double a = num1.doubleValue();
        double b = num2.doubleValue();

        switch (operator) {
            case ADD:
                result = a + b;
                break;
            case SUB:
                result = a - b;
                break;
            case MUL:
                result = a * b;
                break;
            case DIV:
                if (b == 0) {
                    throw new IllegalArgumentException("0으로 나눌 수 없습니다.");
                }
                result = a / b;
                break;
            default:
                throw new IllegalArgumentException("지원하지 않는 연산자입니다.");
        }

        results.add(result);
        return result;
    }

    // Getter
    public List<Double> getResults() {
        return new ArrayList<>(results);
    }

    // Setter
    public void setResults(List<Double> newResults) {
        results.clear();
        results.addAll(newResults);
    }

    // 가장 먼저 저장된 값 삭제
    public void removeResult() {
        if (!results.isEmpty()) {
            results.remove(0);
        }
    }

    // 기준값보다 큰 결과만 조회 (Stream + Lambda)
    public List<Double> getResultsGreaterThan(double value) {
        return results.stream()
                .filter(r -> r > value)
                .collect(Collectors.toList());
    }
}

App.java 구현 과정

calculator 인스턴스 생성

Calculator calculator = new Calculator();
  • App은 입력/출력만 담당(연산을 담당하는 클래스)

반복 실행 구조

while (true) { ... }

exit 입력 전까지 계산이 계속 반복되게 함

OperatorType 변환

OperatorType opType = OperatorType.fromSymbol(operator);
  • 사용자가 입력한 char 연산자를 내부에서는 enum 기반으로 처리하도록 변환

calculate 호출(제네릭 사용)

double result = calculator.calculate(num1, num2, opType);
  • int 뿐만 아니라 double도 처리할 수 있음

예외 처리(트러블슈팅)

실행해보니까 오류(0으로 나누기, 잘못된 연산자)에서도 프로그램이 종료되지 않고 오류 메시지 출력 후 다시 입력으로 돌아가는 상황이 발생했다.(오류 발생했음에도 기준값을 계속 물어보는 문제)

catch (IllegalArgumentException e) {
    System.out.println("오류: " + e.getMessage());
    continue;
}
  • catch에서 continue를 사용하여 오류가 났을 때 기준값을 묻는 흐름이 실행되지 않음(오류가 발생하면 바로 다음 반복으로 넘어가도록 수정)

기준값 조회(람다, 스트림 사용)

System.out.println(calculator.getResultsGreaterThan(value));
  • calculator 내부에서 stream, lambda로 필터링한 결과 출력

App.java 전체코드

package com.example.calculator.step3;

import java.util.Scanner;

public class App {
    public static void main(String[] args) {

        //Calculator 인스턴스 생성
        Calculator calculator = new Calculator();

        Scanner sc = new Scanner(System.in);

        // 반복문 시작
        while (true) {
            System.out.print("첫 번째 숫자를 입력하세요:");
            int num1 = sc.nextInt();
            System.out.print("두 번째 숫자를 입력하세요:");
            int num2 = sc.nextInt();

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


            try {
                OperatorType opType = OperatorType.fromSymbol(operator);  // enum 변환
                double result = calculator.calculate(num1, num2, opType);  // calculate 호출
                System.out.println("결과: " + result);
            } catch (IllegalArgumentException e) {
                System.out.println("오류: " + e.getMessage());
                continue; // 오류났으면 기준값/삭제/exit 안 묻고 다음 반복으로
            }

            // getter
            System.out.println("저장된 결과 목록: " + calculator.getResults());

            System.out.print("기준값을 입력하세요: ");
            double value = sc.nextDouble();

            System.out.println("기준값보다 큰 결과: " + calculator.getResultsGreaterThan(value));

            // removeResult 활용해보기
            System.out.println("가장 먼저 저장된 결과를 삭제하시겠습니까? (remove 입력 시 삭제)");
            String cmd = sc.next();
            if (cmd.equalsIgnoreCase("remove")) {
                calculator.removeResult();
                System.out.println("삭제 후 결과 목록: " + calculator.getResults());
            }

            System.out.println("더 계산하시겠습니까? (exit 입력 시 종료)");
            String answer = sc.next();

            if (answer.equals("exit")) break;
        }
        // 반복문 종료

        sc.close();
    }
}


플로우차트

며칠 전에 플로우차트를 배우면서 간단한 실습을 진행했는데 이번 계산기 과제를 정리하는 데도 플로우차트를 활용해보았다.


과제 총평(회고)

Step1, 2까지는 비교적 수월하게 진행할 수 있었지만
Step3 하면서는 난이도가 조금 올라갔다고 느꼈다.

Enum, 제네릭, 스트림, 람다 모두 분명 강의에서 들었는데 막상 적용하려고 하니 어디에, 어떤 방식으로 써야하는지 계속 고민을 하였다.

특히 제네릭 적용하는 과정에서
int -> double로 바꾸는 게 아니라 < T extends Number > 로 바꾸는 부분이 가장 어려웠다.

이번 과제를 시작할 때는 괄호 우선순위, 제곱, 제곱근 같은 기능까지 확장해보려는 목표도 있었지만
수식 문자열을 숫자와 연산자로 나누는 과정(토큰화)과 연산 우선순위를 처리하는 로직이 생각보다 난이도가 높아 이번 과제에서 해당 기능까지 구현하지는 못했다.

대신 Step3를 진행하는 데 집중했고 다음에 시간이 날 때 위 기능들을 구현해보며 GUI 기반으로도 계산기를 만들어보고 싶은 목표도 생겼다.

이번 과제를 진행하면서 처음에는 ai를 사용하지 않고 혼자 해결해보려고 했다. 하지만 막히는 부분이 계속 생기면서 결국 ai의 도움을 받게 되었다.

하지만 코드를 전부 작성해달라고 요청하는 것이 아닌 문제가 되는 개념이나 해결 방향에 대한 힌트를 얻는 용도로 사용했고 그 이후에는 코드를 직접 작성하고 수정하며 이해하려고 노력했다.

이번 계산기 과제를 하면서 자바 문법이 아직 완전히 익숙하다고는 못하겠지만, 적어도 왜 이렇게 작성했는지 설명은 할 수 있는 상태가 된 거 같아 의미가 있는 과제였다고 생각한다.
문법을 아는 것과 실제로 사용하는 것의 차이를 확실히 느끼게 되었다.

0개의 댓글