오늘은 Step3(도전과제)를 진행해보았다.
Step2까지의 계산기에서는 연산자를 char 타입( + , - , * , /)으로 입력받아 switch 문으로 분기 처리했다.
switch (operator) {
case '+': ...
case '-': ...
}
이 방식은 동작에는 문제가 없지만
연산자 종류가 늘어나면 관리가 어려워지고, 잘못된 연산자 입력에 대한 처리가 분산되며 하나의 개념으로 관리한다. 는 느낌이 부족하다.
그래서 Step3에서는 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("지원하지 않는 연산자입니다.");
}
}
ADD('+'),
SUB('-'),
MUL('*'),
DIV('/');
private final char symbol;
OperatorType(char symbol) {
this.symbol = symbol;
}
public static OperatorType fromSymbol(char symbol) {
for (OperatorType type : values()) {
if (type.symbol == symbol) return type;
}
throw new IllegalArgumentException("지원하지 않는 연산자입니다.");
}
입력은 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를 수정하여
Enum 기반 연산, 제네릭으로 다양한 숫자 타입 처리, Stream/Lambda로 조회
까지 구현했다.
Step2에서는
Step3에서는
private final List<Double> results = new ArrayList<>();
public <T extends Number> double calculate(T num1, T num2, OperatorType operator)
double a = num1.doubleValue();
double b = num2.doubleValue();
switch (operator) {
case ADD: ...
}
if (b == 0) {
throw new IllegalArgumentException("0으로 나눌 수 없습니다.");
}
results.add(result);
return results.stream()
.filter(r -> r > value)
.collect(Collectors.toList());
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());
}
}
Calculator calculator = new Calculator();
while (true) { ... }
exit 입력 전까지 계산이 계속 반복되게 함
OperatorType opType = OperatorType.fromSymbol(operator);
double result = calculator.calculate(num1, num2, opType);

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

System.out.println(calculator.getResultsGreaterThan(value));
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의 도움을 받게 되었다.
하지만 코드를 전부 작성해달라고 요청하는 것이 아닌 문제가 되는 개념이나 해결 방향에 대한 힌트를 얻는 용도로 사용했고 그 이후에는 코드를 직접 작성하고 수정하며 이해하려고 노력했다.
이번 계산기 과제를 하면서 자바 문법이 아직 완전히 익숙하다고는 못하겠지만, 적어도 왜 이렇게 작성했는지 설명은 할 수 있는 상태가 된 거 같아 의미가 있는 과제였다고 생각한다.
문법을 아는 것과 실제로 사용하는 것의 차이를 확실히 느끼게 되었다.