[Java] Level2 계산기 리팩토링

thezz9·2025년 2월 28일
1

개인 과제가 하나 주어졌다.

요구사항은 아래와 같다.

README 보러가기


요구 사항

Lv.1

  • 양의 정수(0 포함)를 입력받아야 합니다.
  • 사칙연산 기호(+, -, *, /)를 입력받습니다.
  • 특정 문자열을 입력하기 전까지 무한히 계산을 진행합니다.
  • Scanner 클래스를 사용하여 사용자 입력을 처리합니다.

Lv.2

  • 결과값 반환 메서드 구현 & 연산 결과를 저장하는 컬렉션 타입 필드를 가진 Calculator 클래스 생성
  • App 클래스의 main 메서드에서 Calculator 클래스의 연산 결과를 저장하고 있는 컬렉션 필드에
    직접 접근하지 못하도록 수정 (캡슐화)
  • Calculator 클래스에 저장된 연산 결과들 중 가장 먼저 저장된 데이터를 삭제하는 기능을 가진 메서드를
    구현한 후 App 클래스의 main 메서드에 삭제 메서드가 활용될 수 있도록 수정

Lv.3

  • Enum 타입을 활용하여 연산자 타입에 대한 정보를 관리하고, 사칙연산 계산기 ArithmeticCalculator 클래스에 활용
  • 실수, 즉 double 타입의 값을 전달 받아도 연산이 수행되도록 만들기
  • 저장된 연산 결과들 중 Scanner로 입력받은 값보다 큰 결과값들을 출력
  • Lambda & Stream을 활용하여 구현

오늘은 Level2를 구현하며 코드를 점진적으로 클린 코드로 개선한 부분에 대해 작성해볼 예정이다.

먼저 정상적인 기능 작동에만 포커싱을 두고 만든 기존 코드는 아래와 같았다.

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

        Calculator calc = new Calculator();
        Scanner sc = new Scanner(System.in);

        while (true) {
            int num1, num2;
            char operator;

            // 필터링 구간
            while (true) {
                System.out.print("첫 번째 숫자를 입력하세요: ");
                if (sc.hasNextInt()) {
                    num1 = sc.nextInt();
                    if (num1 >= 0) {
                        break;
                    }
                    System.out.println("양수(0 포함)를 입력하세요.");
                } else {
                    System.out.println("숫자만 입력할 수 있습니다.");
                    sc.next();
                }
            }

            while (true) {
                System.out.print("사칙연산 기호를 입력하세요: ");
                operator = sc.next().charAt(0);
                if (operator == '+' || operator == '-' || operator == '*' || operator == '/') {
                    break;
                }
                System.out.println("사칙연산 기호가 아닙니다. 다시 입력해주세요.");
            }

            while (true) {
                System.out.print("두 번째 숫자를 입력하세요: ");
                if (sc.hasNextInt()) {
                    num2 = sc.nextInt();
                    if (num2 >= 0) {
                        break;
                    }
                    System.out.println("양수(0 포함)를 입력하세요.");
                } else {
                    System.out.println("숫자만 입력할 수 있습니다.");
                    sc.next();
                }
            }
            
			// 계산 구간
            int result = calc.calculate(num1, num2, operator);

            System.out.println("결과: " + result);

            // 종료 구간
            System.out.println("더 계산 하시겠습니까? (exit 입력시 종료)");
            if (sc.next().equals("exit")) {
                System.out.println("저장된 연산 결과: " + calc.getResults());
                break;
            }
        }

        sc.close();

    }
}
public class Calculator {

    private List<Integer> results = new ArrayList<Integer>();

    public int calculate(int num1, int num2, char operator) {

        int result = 0;

        // 계산 구간
        if (operator == '+') {
            result = num1 + num2;
        } else if (operator == '-') {
            result = num1 - num2;
        } else if (operator == '*') {
            result = num1 * num2;
        } else if (operator == '/') {
            if (num2 == 0) {
                System.out.println("나눗셈 연산에서 분모(두 번째 정수)에 0이 입력될 수 없습니다.");
            } else {
                result = num1 / num2;
            }
        }

    results.add(result);
    return result;
    }

    public List<Integer> getResults() {
        return results;
    }

    public void setResults(List<Integer> results) {
        this.results = results;
    }

    public void removeResult() {
        results.remove(0);
    }

}

코드를 개선하고 나니 이 코드의 문제점이 너무 많다는 게 실감된다. 😅
제일 크게 개선한 부분 몇 가지만 소개하자면 다음과 같다.

  • 같은 기능을 하는 중복되는 코드가 많았다.
  • 가독성과 성능 면에서 불리할 수 있는 if문을 switch문으로 대체
    (switch문의 장점은 더 많다.)
  • 무한 루프 가능성은 크지 않았지만, 좋은 습관을 위해 조건식의 true를 대체하기

1. 중복 코드 분리하기

1-1 숫자 필터링 메서드 분리

public class Filter {

    Scanner sc = new Scanner(System.in);

    public int checkedNum() {
        while (true) {
            if (sc.hasNextInt()) {
                return sc.nextInt();
            } else {
                System.out.println("숫자만 입력할 수 있습니다.");
                sc.next();
            }
        }
    }

    public int checkedPositiveNum() {
        while (true) {
            int num = checkedNum();
            if (num >= 0) {
                return num;
            }
            System.out.println("양수(0 포함)를 입력하세요.");
        }
    }

}

1-2 필터링 메서드를 사용 & 입력 메서드 오버로딩 적용

    public int addNum() {
        // 필터링 구간
        System.out.print("첫 번째 숫자를 입력하세요: ");
        return filter.checkedPositiveNum();

    }

    public int addNum(char operator) {
        System.out.print("두 번째 숫자를 입력하세요: ");
        int num = filter.checkedPositiveNum();

        if (operator == '/' && num == 0) {
            System.out.println("나눗셈 연산에서 분모(두 번째 정수)에 0이 입력될 수 없습니다.");
            return addNum(operator);
        }
        return num;

    }

2. if문을 switch문으로 대체

2-1 계산 구간 (+ 나눗셈 연산 필터링 분리)

		// 계산 구간
        switch (operator) {
            case '+':
                result = firstNum + secondNum;
                break;
            case '-':
                result = firstNum - secondNum;
                break;
            case '*':
                result = firstNum * secondNum;
                break;
            case '/':
                result = firstNum / secondNum;
                break;
        }

2-2 종료 구간 (System.exit(0) 메서드를 사용하여 main 메서드 종료)

		switch (sc.next()) {
        	case "1":
            	System.out.println("저장된 연산 결과: " + calc.getResults());
                continue;
            case "2":
                calc.removeResult();
                System.out.println("저장된 연산 결과: " + calc.getResults());
                continue;
            case "3":
                flag = true;
                subFlag = false;
                break;
            case "4":
                System.out.println("종료합니다.");
                System.exit(0);
            default:
                System.out.println("잘못된 입력입니다. 다시 시도해주세요.");
            }

(⭐+if문보다 switch문을 권장하는 이유)
1. 가독성 및 코드 구조
→ 값에 따라 어떤 분기가 발생하는지 한눈에 파악하기 쉽게 해준다.
2. 성능
→ 컴파일러가 여러 번의 조건 검사를 순차적으로 수행해야하는 if-else에 비해
switch문은 해시 테이블이나 검색 트리로 변환하여 성능을 높일 수 있다.
3. 새로운 케이스 추가 용이
if-else 체인은 각 조건이 별도로 작성되므로 추가할 때마다 비교 연산이 추가되어야 한다.
4. 에러 방지
default 케이스를 사용하여 데드 코드를 방지할 수 있으며, 특정 변수의 값에 따라 정확하게 분기하므로 논리적인 오류를 줄이는 데 도움이 된다.


3. 조건식을 flag로 수정 & do-while문으로 변경

package level2;

import java.util.Scanner;

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

        Scanner sc = new Scanner(System.in);
        Input input = new Input();
        Calculator calc = new Calculator();
        Menu menu = new Menu(calc);

        int firstNum, secondNum;
        char operator;
        boolean flag = false;
        boolean subFlag = true;

        do {
            // 입력 구간
            firstNum = input.addNum();
            operator = input.addOperator();
            secondNum = input.addNum(operator);

            // 출력 구간 (계산)
            int result = calc.calculate(firstNum, secondNum, operator);
            System.out.println("결과: " + result);

            // 종료 구간 (메뉴 출력)
            flag = menu.display(subFlag);
        } while (flag);
        sc.close();

    }
}

정리가 끝난 main 메서드 부분이다. 훨씬 깔끔하다.
모든 기능을 분리한 전체 구조는 아래와 같다.

App - main method
Calculate - calculate method
Filter - filter method
Input - input method
Menu - display method

트러블슈팅

문제

main 메서드에서 계산을 위해 생성한 calc 객체와 display 메서드에서 저장된 연산 결과를 보기 위해 생성한 calc 객체가 서로 다른 객체여서, Menu 클래스에서 results 리스트의 값이 불러와지지 않았다.

해결 과정

고민을 하다 처음엔 static으로 해결을 했으나, 튜터님께 여쭤본 결과 다른 방법을 찾아보라고 하셨다.
객체를 전달하는 방법을 고민한 끝에, main 메서드에서 생성한 calc 객체를 menu 객체의 생성자로 넘겨 해결할 수 있었다.

느낀점

같은 클래스의 객체를 두 개 생성하면 서로 다른 객체라는 사실을 알고 있었음에도 구현하는 과정에서 놓쳤다는 점에서, 더 깊이 공부할 필요성을 느꼈다. 또한 튜터님께 코드리뷰를 받으며 클린 코드가 무엇인지 조금은 이해할 수 있었고, 매우 유익한 경험이었다.

profile
개발 취준생

0개의 댓글