관리 메뉴

개발공부기록

자바로 계산기 만들기 - 리펙토링, LV1 - equals() 비교시 Null 방지, LV2 - 비슷한 로직의 메서드 통합, LV3 - 의미있는 제네릭 사용 본문

프로젝트/토이프로젝트

자바로 계산기 만들기 - 리펙토링, LV1 - equals() 비교시 Null 방지, LV2 - 비슷한 로직의 메서드 통합, LV3 - 의미있는 제네릭 사용

소소한나구리 2025. 3. 14. 19:45
728x90

계산기 과제 개발 회고


 LV1 - 피드백 반영

equals() 비교 순서

원래의 코드

더보기
if (!(operator.equals("+") || operator.equals("-") || operator.equals("*") || operator.equals("/"))) {
    System.out.println("**** 연산 부호를 잘못 입력 하셨습니다. 다시 시작합니다 ****");
    System.out.println();
    continue;
}

 

 

입력받은 연산자를 검증하기 위해서 검증된 입력받은 연산자인 operator변수를 equals()를 사용하여 각 연산자가 맞는지 검증하는 로직을 위에처럼 작성하였다.

 

아무래도 아무리 여러 검증 로직을 거친다고해도, 다양한 방법으로 악의적인 요청이나 실수로 다양한 입력값이 올 수 있기때문에 애초에 예외가 발생하지 않는 코드를 적용하지 않을 이유는 없다고 생각이 들었다.

피드백 적용

if (!("+".equals(operator) || "-".equals(operator) || "*".equals(operator) || "/".equals(operator))) {
    System.out.println("**** 연산 부호를 잘못 입력 하셨습니다. 다시 시작합니다 ****");
    System.out.println();
    continue;
}

피드백을 수용하여 코드에서 절대 NullPointerException이 발생할 수 없도록 코드에 작성한 문자열.equals(입력된 문자열)로 변경을 적용하였다.


LV2 - 피드백 반영

비슷한 기능을 하는 메서드 수정

원래의 코드

계산을 위한 두 입력값을 받았을 때 연산자가 "/"일 경우에 두 번째 계산 값을 입력할 때는 동작하는 로직이 달라야 하고 안내 문구를 각각 다르게 하기 위해 첫 번째 값을 입력받는 메서드와, 두 번째 입력받는 메서드 각각 작성했었다.

 

그러나 튜터님의 피드백 내용으로 보면 비슷한 기능을 하나의 메서드로 묶고, 연산 기호를 검증하는 validOperator를 추가하는 것이 더 좋을 것 같다고 하여 코드를 수정해보았다.

 

main()

더보기
// 기타 로직 존재

int firstValue = validFirstValue(scanner);                // 첫 번째 값 검증
int secondValue = validSecondValue(operator, scanner);    // 두 번째 값 검증

// 기타 로직 존재

 

validFirstValue() - 첫 번째 값 검증

더보기
private static int validFirstValue(Scanner scanner) {
    int firstValue;
    while (true) {
        try {
            System.out.print("첫번째 숫자를 입력해주세요. 양의 정수(0포함)만 가능합니다: ");
            firstValue = scanner.nextInt();

            if (firstValue < 0) {
                System.out.println("음수가 입력되었습니다. 양의 정수(0포함)만 입력 가능합니다.");
                System.out.println();
                scanner.nextLine();
                continue;
            }

            break;
        } catch (InputMismatchException e) {
            System.out.println("**** 타입이 맞지 않습니다. 양의 정수(0포함)정수만 입력해 주세요.****");
            System.out.println();
            scanner.nextLine(); // 버퍼지우기
        }
    }
    return firstValue;
}

 

validSecondValue() - 두 번째 값 검증

더보기
private static int validSecondValue(String operator, Scanner scanner) {
    int secondValue;
    while (true) {
        try {
            if (operator.equals("/")) {
                System.out.print("두번째 숫자(양의 정수, 0포함)를 입력해주세요. 나눗셈 연산은 0을 입력할 수 없습니다: ");
                secondValue = scanner.nextInt();

                if (secondValue < 0) {
                    System.out.println("음수가 입력되었습니다. 양의 정수(0포함)만 입력 가능합니다.");
                    System.out.println();
                    scanner.nextLine();
                    continue;
                }

                if (secondValue == 0) {
                    System.out.println("**** 나눗셈 연산에서는 0을 입력할 수 없습니다. 다른 수를 입력해 주세요 ****");
                    System.out.println();
                    continue;
                }
                break;
            } else {
                System.out.print("두번째 숫자를 입력해주세요. 양의 정수(0포함)만 가능합니다: ");
                secondValue = scanner.nextInt();

                if (secondValue < 0) {
                    System.out.println("음수가 입력되었습니다. 양의 정수(0포함)만 입력 가능합니다.");
                    System.out.println();
                    scanner.nextLine();
                    continue;
                }
                break;
            }

        } catch (InputMismatchException e) {
            System.out.println("**** 타입이 맞지 않습니다. 양의 정수(0포함)만 입력해 주세요.****");
            System.out.println();
            scanner.nextLine(); // 버퍼지우기
        }
    }
    return secondValue;
}

피드백 적용

우선 가장 먼저 비슷한 로직인 입력 값을 검증 받는 메서드를 하나로 묶는 시도를 하였고, 출력 메서드를 공통으로 사용하기 위해서 일부 수정했다.

 

문제는 이 메서드를 통해 두번 입력을 받아서 사용하는 것은 상관 없지만 두 번째 입력을 받을 때 연산자를 검증해야 하는 문제가 남아있다.

이를 어떻게 효과적으로 피드백을 반영하는 것이 좋을지 잘 생각이 나질 않았다.

 

처음에는 작성해 주신 validOperator() 메서드를 만들어서 적용해보기도 했지만 잘 되지 않았고 다음 방법으로 메서드 오버로딩으로 해결해보려고 접근해보고 시도해 보았는데, 이러면 첫 번째 방법과 별로 다를게 없어보였다.

 

그래서 더 고민하지 말고 LV3 피드백도 반영해야 하기 때문에 튜터님께 조언을 구하여 말씀하신 의도를 명확하게 해주셨다.

아래는 피드백 중 일부 내용이다

  • 오버로딩은 X
  • 동일 로직을 메서드화해서 재사용
  • 연산자 검증 로직을 입력값을 둘 다 받은 다음 이 값을 가지고 검증하고 예외에 대한 흐름을 별도로 가져가도 된다.

위 내용해서 힌트를 얻고 바로 코드를 수정하여 먼저 두 값을 입력 받고 그 아래에 예외에 대한 처리를 진행하도록 코드를 작성했다

// main 메서드
System.out.println("== 첫 번째 숫자 ==");
int firstValue = validNumber(scanner);

System.out.println("== 두 번째 숫자 ==");
int secondValue = validNumber(scanner);

if ("/".equals(operator) && secondValue == 0) {
    System.out.println("**** 나눗셈 연산에서는 0을 입력할 수 없습니다. 다른 수를 입력해 주세요 ****");
    System.out.println();
    continue;
}

이렇게 코드를 수정하니 입력값을 받는 메서드 하나로 두 값을 입력 받을 수 있게 되었고, 나눗셈 연산일 경우 두 번째 값이 0이면 추가적인 예외로직이 발생하여 다시 입력값을 받도록 적용이 완료 되었다.

 

그러나 여기서 한가지 내가 의도한 로직과 프로그램 흐름이 달라졌는데, 기존의 입력값을 받는 메서드를 보면 의도한 흐름이 아닐 경우 프로그램의 처음으로 돌아가는게 아니라 해당 입력값을 다시 입력 받아야 하는 것이 원래의 의도였다

 

그러나 지금과 같은 로직에서는 두 번째 값을 입력했을 때 예외가 발생하면 즉, 두 번째 입력값에 0을 입력해 버리게 되면 다시 프로그램의 처음으로 돌아가는 문제가 발생했다.

 

그래서 이 / 연산 로직을 validNumber()메서드 안에 넣어서 이 안에서 돌고있는 반복문에서 검증을 해야 될 것 같다고 생각해 매개변수에 operator를 추가로 받고, 몇 번째 입력 값인지 확인할 수 있는 값을 추가로 입력 받도록 수정했다.

 

main() - LV2 피드백 반영 최종 코드

// main 메서드
// 기타 나머지 코드

Integer firstValue = null;
System.out.println("== 첫 번째 숫자 ==");
firstValue = validNumber(scanner, operator, firstValue);

System.out.println("== 두 번째 숫자 ==");
int secondValue = validNumber(scanner, operator, firstValue);

입력값을 검증하고 반환하는 validNember() 메서드에 연산자와 첫 번째 입력값인 firstValue를 추가로 입력 받는다

 

여기서 firstValue를 Integer로 선언하였는데, validNumber에서 검증할 때 넘어간 firstValue가 null이 아니면 두 번째 입력 값이라고 보고 검증 로직을 작성이 필요하여 null을 가질 수 있는 Integer로 선언했다

 

validNumber() - LV2 피드백 반영 최종 코드

// after - 메서드 하나로 변경
private static int validNumber(Scanner scanner, String operator, Integer firstValue) {
    int validNumber;

    while (true) {
        try {
            System.out.print("계산할 숫자를 입력해주세요. 양의 정수만 가능합니다: ");    // 공통으로 사용할 수 있도록 문구 변경
            validNumber = scanner.nextInt();

            if (validNumber < 0) {
                System.out.println("음수가 입력되었습니다. 양의 정수(0포함)만 입력 가능합니다.");
                System.out.println();
                scanner.nextLine();
                continue;
            }

            if (firstValue != null && "/".equals(operator) && validNumber == 0) {
                System.out.println("**** 나눗셈 연산에서는 0을 입력할 수 없습니다. 다른 수를 입력해 주세요 ****");
                System.out.println();
                continue;
            }

            break;
        } catch (InputMismatchException e) {
            System.out.println("**** 타입이 맞지 않습니다. 양의 정수(0포함)정수만 입력해 주세요.****");
            System.out.println();
            scanner.nextLine(); // 버퍼지우기
        }

    }
    return validNumber;
}

두 개의 메서드에서 비슷한 기능을 하는 것을 하나의 메서드로 합친 메서드이다

연산자가 "/" 일 때 밖에서 검증하던 로직을 메서드 내부로 가져와서 두 번째 입력값이 검증될 때 오류가 발생해도 다시 프로그램이 처음으로 가지 않고 두 번째 입력값을 다시 받을 수 있다.

 

여기서 firstValue != null 조건이 들어가 있는데, 만약 validNumber()가 처음 호출 된다면 fisrtValue는 null인 상태로 메서드가 동작하게 되어 두 번째 if검증은 무시 된다.

 

두 번째 validNumber()가 호출되면 첫 번째 validNumber()의 반환 값이 fisrtValue에 들어가고 그 값이 다시 매개변수로 들어오기 때문에 여기에서는 firstValue가 null이 될 수 없으므로 firstValue가 null이 아니면 두 번째 입력값이라고 생각하고 로직을 구상할 수 있었다.

실행 결과

그 결과 첫 번째 로직에서는 0도 들어올 수 있고, 두 번째 로직에서 예외가 발생해도 두 번째 값만 입력되며 피드백을 반영하여 중복 로직을 하나로 합칠 수 있게 되었다


LV3 - 피드백 반영

의미있는 제네릭 사용

원래의 코드

String으로 입력 받은 계산할 두 값을 inputTypeConverter로 검증하여 "."이 있으면 Double로 .없으면 Integer로 형변환을 하고 반환타입을 Number로 반환 한다

 

그 다음 두 값을 instanceof로 검사하여 타입이 더블이면 double로 변환하여 doubleCalculate()로 인자를 넘겨서 계산을 진행했고, 타입이 Integer이면 int로 변환하여 intCalculate()로 넘겨서 계산을 진행했는데 피드백을 보고 이 코드를 보니 확실히 비효율을 넘어서 제네릭을 활용한다는 느낌은 들지 않았다.

 

그리고 추가적으로 처음에 제네릭을 도입하려고 할 때 이력을 저장하는 DB의 역할을하는 List 자료구조를 튜터링을 통해 Number로 활용하도록 코드를 작성했지만, 아무래도 제네릭을 제대로 활용하는 코드는 아니다보니 이부분도 변경을 진행했다.

 

App 클래스의 코드

더보기
// main 메서드
public static void main(String[] args) 

        ArithmeticCalculator<Number> calculator = new ArithmeticCalculator<>();

   // 기타 다른 코드들

        Number firstValue = inputTypeConverter(validFirstValueStr);
        Number secondValue = inputTypeConverter(validSecondValueStr);

        if (firstValue instanceof Double || secondValue instanceof Double) {
            calculator.doubleCalculate(validOperator, firstValue.doubleValue(), secondValue.doubleValue());
        } else {
            calculator.intCalculate(validOperator, firstValue.intValue(), secondValue.intValue());
        }
        
   // 기타 다른 코드들
}

// Number로 형변환하는 메서드
private static Number inputTypeConverter(String inputValue) {
    if (inputValue.contains(".")) {
        return Double.valueOf(inputValue);
    } else {
        return Integer.valueOf(inputValue);
    }
}

 

 

ArithmeticCalculator - intCalculate, doubleCalculate

더보기
public void intCalculate(Operator operator, Integer firstValue, Integer secondValue) {
    Integer result = 0;

    switch (operator) {
        case PLUS:
            result = firstValue + secondValue;
            break;
        case MINUS:
            result = firstValue - secondValue;
            break;
        case MULTIPLY:
            result = firstValue * secondValue;
            break;
        case DIVIDE:
            result = firstValue / secondValue;
            break;
    }
    System.out.println("***************** 계산 결과 출력 *****************");
    System.out.println("계산 결과: " + firstValue + " " + operator.getSymbol() + " " + secondValue + " = " + result);
    System.out.println();
    add((T) result);
}

public void doubleCalculate(Operator operator, Double firstValue, Double secondValue) {
    Double result = 0.0;

    switch (operator) {
        case PLUS:
            result = firstValue + secondValue;
            break;
        case MINUS:
            result = firstValue - secondValue;
            break;
        case MULTIPLY:
            result = firstValue * secondValue;
            break;
        case DIVIDE:
            result = firstValue / secondValue;
            break;
    }

    String printFirstValue = printFormat(firstValue);
    String printSecondValue = printFormat(secondValue);
    String printResultValue = printFormat(result);

    System.out.println("***************** 계산 결과 출력 *****************");
    System.out.println("계산 결과: " + printFirstValue + " " + operator.getSymbol() + " " + printSecondValue+ " = " + printResultValue);
    System.out.println();

    if (result % 1 == 0) {
        add((T) Integer.valueOf(result.intValue()));
    } else {
        add((T) result);
    }
}

피드백 적용

LV2 피드백 적용된 메서드를 LV3에 맞게 적용

// 입력값 검증
String validFirstValueStr = null;
validFirstValueStr = validNumber(scanner, INPUT_VALID_REGEXP, validOperator, validFirstValueStr);
String validSecondValueStr = validNumber(scanner, INPUT_VALID_REGEXP, validOperator, validFirstValueStr);

// after
private static String validNumber(Scanner scanner, String regExp, Operator operator, String firstValue) {
    String validNumber;

    while (true) {
        System.out.print("숫자를 입력해 주세요(소수점 입력 가능): ");
        validNumber = scanner.nextLine();
        boolean checkResult = Pattern.matches(regExp, validNumber);  // 정규식과 비교
        if (!checkResult) {
            System.out.println("**** 입력값이 올바르지 않습니다. 숫자(소수점포함)만 입력해 주세요.****");
            System.out.println();
            continue;
        }

        if (firstValue != null && Operator.DIVIDE.equals(operator) && Double.parseDouble(validNumber) == 0.0) {
            System.out.println("**** 나눗셈 연산에서는 0을 입력할 수 없습니다. 다시 입력해 주세요 ****");
            System.out.println();
            continue;
        }
        break;
    }
    return validNumber;
}

 

LV2에서 입력값을 2개의 메서드로 각각 나눠서 검증했던 코드도 LV3에 그대로 남아있어서 LV2와 동일하게 하나의 메서드로 사용할 수 있도록 변경했다.

 

LV3에서는 소수점 때문에 문자열로 계산 값을 입력받아서 정규식으로 검증을 하기 때문에 검증로직이 더 적어서 하나의 메서드로 통합하기가 더 쉬웠다.

 

App 클래스 - main()

// 연산
public static void main(String[] args) {

    ArithmeticCalculator<Double> calculator = new ArithmeticCalculator<>();

    // 기타 코드들
    
    double firstValue = Double.parseDouble(validFirstValueStr);
    double secondValue = Double.parseDouble(validSecondValueStr);

    Double result = calculator.calculate(validOperator, firstValue, secondValue);
    calculator.calculateResultPrinter(validOperator, firstValue, secondValue, result);
    
    // 기타 코드들
}

 

제네릭의 의미를 더 살려보기 위해서 ArithmeticCalculator를 생성할 때 제네릭을 <Double>로 생성했다.

ArithmeticCalculator는 T extends Number이기 때문에 Integer로도, Double로도 생성할 수 있다.

 

그리고 입력값 검증을 통해 String 타입으로 반환 받은 두 값을 Double 타입으로 형변환하고, 타입에 따라 계산을 하는 메서드가 따로 있는 것이 아니라 calculate()메서드 하나로 계산하고 그 결과를 Double로 반환한다.

 

ArithmeticCalculator - calculate()

public Double calculate(Operator operator, T firstValue, T secondValue) {
    Double result = 0.0;
    Double num1 = firstValue.doubleValue();
    Double num2 = secondValue.doubleValue();
    
    switch (operator) {
        case PLUS:
            result = num1 + num2;
            break;
        case MINUS:
            result = num1 - num2;
            break;
        case MULTIPLY:
            result = num1 * num2;
            break;
        case DIVIDE:
            result = num1 / num2;
            break;
    }

    if (result % 1 == 0) {
        add((T) Integer.valueOf(result.intValue()));
    } else {
        add((T) result);
    }

    return result;
}
  • 계산을 담당하는 계산기이다. 계산을 위한 두 값을 T타입으로 받을 수 있어 Number의 하위 타입으로 모두 입력 받을 수 있으며 연산 결과는 Double 타입으로 반환한다.
  • 다만 현재 로직에서는 Double 타입의 계산기만 사용하기도하고, 사실 어떤 타입으로 들어와도 더블 타입으로 계산하면 모든 연산 결과를 얻을 수 있기 때문에 더블로 형변환하여 계산을 진행하고 그 결과를 Double 타입으로 반환한다.
  • 여기서 저장결과를 저장하는 List의 타입이 T 타입이므로 결과를 1로 나눠서 0이면 Integer로 형변환하고 그게 아니라면 그대로 더블타입으로 저장하는 로직은 그대로 살려둔다.

ArithmeticCalculator - calculateResultPrinter()

public void calculateResultPrinter(Operator operator, Double firstValue, Double secondValue, Double result) {

    String printFirstValue = printFormat(firstValue);
    String printSecondValue = printFormat(secondValue);
    String printResultValue = printFormat(result);

    System.out.println("***************** 계산 결과 출력 *****************");
    System.out.println("계산 결과: " + printFirstValue + " " + operator.getSymbol() + " " + printSecondValue+ " = " + printResultValue);
    System.out.println();
}
  • 연산 결과를 출력하는 메서드를 분리했다.
  • 기존에는 int, double 타입에 따라 연산을 하고 출력하는 로직이 함께 있었는데 기능을 명확하게 하기위해서 분리했다.
  • 여기서는 연산자와 입력받은 계산할 값들, 그리고 연산 결과를 받아서 printFormat()메서드를 통해 더블 타입이면 정수형태로 변환한 다음 String으로 반환하고, 더블 타입이면 그대로 String으로 변환한 후 반환하여 1.0, 2.0 처럼 소수점이 0으로 끝나는 값들을 정수 형태로 변경해서 출력한다.

최종 회고

자바로 진행하는 첫 계산기 만들기 였지만 나름대로 처리하고자 하는 예외처리도 많이 적용해보고, 피드백도 반영하여 수정하는 과정에서 코드를 리팩토링 하는 방법에 대해서도 배울점이 있었다.

 

물론 Double을 사용하기 때문에 4.1 - 2 와 같은 연산은 제대로 값이 안와서 이를 예외 처리해야 하는 부분도 존재하고, 조금 더 객체지향적으로 기능을 분리해서 설계할 수 있는 부분은 여전히 남아있는 것은 맞다.

 

하지만 요구사항보다 더 예외처리를 많이 진행했고, 피드백을 통해 기존 코드를 개선하면서 불필요한 로직도 변경하고 코드의 중복도 제거하고 자바가 제공하는 기능을 더 정확하게 쓰는 연습을 하면서 다음 과제나 프로젝트를 진행할 때 여기서 배운 부분을 활용할 수 있을 것 같다.

 

계산기 만들기는 여기서 마무리 한다.

728x90