본문 바로가기

JAVA

객체지향적인 계산기 만들기 : 멘토님 코드와 나의 코드간의 개선점 비교

입출력 

멘토님 코드

  • 음수 입력시에 재입력을 받도록 처리가 되어 있음
if (firstNumber < 0) {
                System.out.println("음수는 입력할 수 없습니다. 프로그램을 재시작합니다.");
                continue;
            }

 

 세이프티 코딩 (Safety Coding) 적용

  • 문자열 비교 시 변수에 담지 않고, 해당 문자열 자체로 equals를 사용하여 비교함 -> 세이프티 코딩 (Safety Coding)
  • 널은 메서드를 사용하지 못하기 때문에 스트링으로 비교하면 프로그램이 종료됨
  • NullPointerException 방지
if ("exit".equals(sc.next())) {
                break;
            }

 

기존에 내가 작성한 코드

  • 변수명에 널이 들어갈 수 있기 때문에 위험함
String exit = sc.nextLine();
            exit = exit.toLowerCase();
            if (exit.equals("exit")){
                break;
            }

 

입력방식

  • sc.next() : 공백을 포함하지 않는 단일 토큰 입력시 사용, 공백 기준으로 공백 앞에 있는 문자열만 받아옴
  • sc.nextLine() : 엔터를 기준으로 한 줄 전체를 입력받음

List return시에 toString으로 변환하지 않고, 그냥 return 해도 잘 반환 -> 편의를 위해서 toString 내부적으로 구현되어 있음

 

객체지향 활용

클래스 내부에 필드는 final로 선언하기

  • 컬랙션에 대해서 여러 동작을 수행해도 스택에 저장되어 있는 인스턴스의 주소값을 변경하는 것이 아니기 때문에 변경해도 됨
private final List<Double> resultList = new ArrayList<>();

 

기존에 내가 작성한 코드

  • final을 붙이지 않음
private List<Integer> results = new ArrayList<Integer>();

 

컬랙션 선언시, 타입은 반드시 인터페이스 타입으로 선언하기

  • List 인터페이스를 구현한 것이 ArrayList이기 때문에, 타입은 상위 인터페이스 타입으로 지정하기
  • 나중에 ArrayList -> LinkedList로 변경 할 수 있음 즉, 구현체를 나중에 쉽게 변경할 수 있으므로 재사용성과 유지보수성 용이(다형성)
private final List<Double> resultList = new ArrayList<>();

 

Enum class의 활용

  • Calculator클래스의 계산 메서드를 실행할 때 연산자 타입을 선언해둔 enum 클래스를 활용하여 전달
  • enum클래스에서 입력받은 Sting타입의 연산자와 비교하여 변환하는 메서드는 static으로 선언
public enum Operator {
    ADD("+"),
    SUBTRACT("-"),
    MULTIPLY("*"),
    DIVIDE("/");

    private final String symbol;

    Operator(String symbol) {
        this.symbol = symbol;
    }

    public String getSymbol() {
        return symbol;
    }

    // 입력받은 문자열과 매칭되는 클래스인지 확인하는 정적 메서드
    public static Operator fromSymbol(String symbol) {
        for (Operator operator : values()) {
            // 매칭되는 해당 문자열에 해당하는 클래스 반환
            if (operator.getSymbol().equals(symbol)) {
                return operator;
            }
        }
        throw new IllegalArgumentException("유효하지 않은 연산 기호입니다. (+, -, *, / 중 하나여야 합니다)");
    }
}
String symbol = sc.next();
// 입력받은 String을 이넘 타입에 넣어서 매칭되는 타입으로 반환
Operator operator = Operator.fromSymbol(symbol);

//이넘 타입으로 넘겨줌
System.out.println("결과: " + calculator.calculate(firstNumber, secondNumber, operator));

 

 

기존에 내가 작성한 코드

  • 연산 메서드로 전달할 때 타입은 그냥 String으로 넘기고, 메서드만 오버라이딩 해서 사용함
  • 결국 연산자 자체를 String으로 사용해서 비교했기에 enum 클래스를 연산자 이름 측면에서는 잘 활용 못함
public enum OperatorType {
    PLUS("+") {
        @Override
        public <T extends Number> double calculate(T firstInput, T secondInput) {
            return firstInput.doubleValue() + secondInput.doubleValue();
        }
    },
    MINUS("-") {
        @Override
        public <T extends Number> double calculate(T firstInput, T secondInput) {
            return firstInput.doubleValue() - secondInput.doubleValue();
        }
    },
    MULTIPLY("*") {
        @Override
        public <T extends Number> double calculate(T firstInput, T secondInput) {
            return firstInput.doubleValue() * secondInput.doubleValue();
        }
    },
    DIVIDE("/") {
        @Override
        public <T extends Number> double calculate(T firstInput, T secondInput) throws DivideByZeroException {
            if (secondInput.doubleValue() == 0) {
                throw new DivideByZeroException();
            }
            return firstInput.doubleValue() / secondInput.doubleValue();
        }
    };

    private final String operator;

    OperatorType(String operator) {
        this.operator = operator;
    }

    //계산을 수행하는 메서드를 추상메서드로 선언하여 각각의 연산자 객체가 오버라이딩하여 구현
    public abstract <T extends Number> double calculate(T firstInput, T secondInput) throws DivideByZeroException;
}
public <T extends Number> Double calculate(T firstInput, T secondInput, String operator) throws DivideByZeroException {
        double answer = switch (operator) {
            case "+" -> OperatorType.PLUS.calculate(firstInput, secondInput);
            case "-" -> OperatorType.MINUS.calculate(firstInput, secondInput);
            case "*" -> OperatorType.MULTIPLY.calculate(firstInput, secondInput);
            case "/" -> OperatorType.DIVIDE.calculate(firstInput, secondInput);
            default -> throw new IllegalArgumentException("Invalid operator: " + operator);
        };

        results.add(answer);
        return answer;
    }

 

제네릭 활용

  • 계산기 클래스 범위에서 Number 하위 객체만 가능하도록 타입 제한
  • Class<T> type; 필드를 생성자 주입이용하여 기록함 -> 런타임에 처음 생성했던 제네릭 타입은 사라지기 때문
  • List<T> resultList에 값을 넣어줄 때 T자체로 넣어줌
public class ArithmeticCalculator<T extends Number> {

    // 필드들의 타입들은 Number의 하위 타입
    private final List<T> resultList = new ArrayList<>();
    // 생성자 주입시 넣어준 타입
    private final Class<T> type;

    public ArithmeticCalculator(Class<T> type) {
        this.type = type;
    }

    public T calculate(T firstNumber, T secondNumber, Operator operator) {
        double num1 = firstNumber.doubleValue();
        double num2 = secondNumber.doubleValue();
        double result;

		// 계산로직
        T typedResult = castToType(result, type);
        addResult(typedResult);
        return typedResult;
    }
  • 처음에 생성한 타입에 맞게 계산 결과값을 변환시켜주는 메서드를 만들어 변환해줌
public static <T extends Number> T castToType(double result, Class<T> type) {
        //type.getSimpleName() -> 클래스 명을 조회하는 메서드
        return switch (type.getSimpleName()) {
            case "Integer" -> type.cast((int) result);
            case "Double" -> type.cast(result);
            case "Long" -> type.cast((long) result);
            default -> throw new IllegalArgumentException("지원하지 않는 타입입니다.");
        };
    }

 

 

기존에 내가 작성한 코드

  • 입력받는 피연산자의 값이 어떤 숫자타입이든 상관없이 입력받는 부분에 초점을 맞춤
  • 계산 결과값을 저장하는 컬렉션 타입을 Double로 지정하고 시작했기에 계산 결과값 저장부분에서 사용했다고 하기엔 아쉬움이 있음
  • Double 지정 리스트 => double로 넣음
public Double executeCalculator() throws DivideByZeroException {
        // 여기서 String 타입의 피연산자들을 실수형이나 정수형으로 변환하여 자유롭게 넣어 줄 수 있음, Integer도 가능
        Double firstNumber = Double.parseDouble(firstInput);
        Double secondNumber = Double.parseDouble(secondInput);
        return arithmeticCalculator.calculate(firstNumber, secondNumber, operator);
    }
public class ArithmeticCalculator {

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

    public <T extends Number> Double calculate(T firstInput, T secondInput, String operator) throws DivideByZeroException {
        double answer = switch (operator) {
            case "+" -> OperatorType.PLUS.calculate(firstInput, secondInput);
            case "-" -> OperatorType.MINUS.calculate(firstInput, secondInput);
            case "*" -> OperatorType.MULTIPLY.calculate(firstInput, secondInput);
            case "/" -> OperatorType.DIVIDE.calculate(firstInput, secondInput);
            default -> throw new IllegalArgumentException("Invalid operator: " + operator);
        };

        results.add(answer);
        return answer;
    }
    }
public enum OperatorType {
    PLUS("+") {
        @Override
        public <T extends Number> double calculate(T firstInput, T secondInput) {
            return firstInput.doubleValue() + secondInput.doubleValue();
        }
    },
    }

 

 

 

멘토님 코드를 보면서 이넘, 제네릭에 대해서 확실하게 이해할 수 있었던 것 같다 😳

특히 제네릭은 런타임에 한번 지정하고 사라지기 때문에 따로 저장해야 하는 부분이 가장 인상적이었다

다음 과제는 이넘과 제네릭을 좀 더 잘 활용해보고 싶다