결론부터 말하자면 이번 과제는 많이 부족했던 것 같다🙈

하지만 어쩌겠는가,,, 이미 시간은 지났고 되돌릴 수 없다

지금부터 고민했던 점들에 대해서 정리해보도록 하고 다음 프로젝트는 이번 문제점을 고쳐서 발전시키는 방향으로 가자😳😳😳

 

고민점

장바구니 기능을 클래스로 빼는게 맞는지에 대한 고민

  • 장바구니기능이 없는 lv5만 구현했을 때까지만 하더라도 Kiosk 클래스는 사용자의 주문에 대한 입출력에 대해서 기능적인 부담이 크지 않다고 생각
  • 하지만, 장바구니 요구사항을 보니 내용이 꽤 많았음.. (추가 삭제 조회 등등, + 예외 처리)
  • 그렇기 때문에 이 모든 요구사항을 Kiosk에 넣어버리면 코드도 엄청 무거워지고 알아보기가 너무 힘들 것 같다고 생각

-> 장바구니와 관련된 역할은 Cart 클래스에서 하기로 결정!

public class Cart {
    private List<MenuItem> cartItems = new ArrayList<>(); // 장바구니에 넣은 물건을 담는 리스트
    private double total;
    private Scanner sc = new Scanner(System.in);

    public void addCart(MenuItem menuItem){
    // 장바구니에 물건 추가
    }

    public void showCartItems() {
	// 장바구니에 담긴 물건 조회
    }

    public void showTotalCost() {
    // 장바구니 물건 합계 출력
    }

    public void showCartAddMsg(MenuItem menuItem) {
    // 장바구니에 메뉴를 추가할지 묻는 메서드
    }

    public void deleteCartItem() throws MenuItemNotFoundException{
    // 장바구니 메뉴 삭제
    }

    public void cancelCart() {
    // 장바구니 물건 전체 삭제
    }

    public double getTotal() {
    }

    public boolean isCartEmpty(){
    }

}

 

 

작은 기능들까지도 메서드로 분리하는게 맞는 것인지에 대한 고민 + 여전한 Kiosk의 막대한 책임

  • 요구조건에 맞게 구현하려다 보니 start()메서드가 너무 방대하고 복잡해져 버림
  • 더군다나 출력에 관한 것도 너무 많고 지져분해져서 개발을 하면서도 너무 복잡하게 느껴짐 (아래보다 훠얼씬 많음 ㅠ_ㅜ)

  • 그러한 이유로 먼저 출력에 관한 메서드들을 생성해서 분리시킴 -> 확실히 가독성 측면에서 훨씬 깔끔했고, start()메서드의 길이가 줄어서 개발할 때 중요도가 낮은 출력 메서드들의 내부 로직을 볼 필요가 없어서 한결 편했음

하지만 이렇게 자잘하게 메서드를 분리해서 만드는 것이 과연 옳은지 고민이 된다...

역할이라고 하기엔 너무 하찮지 않나 싶은 생각도 들고, 어쩌면 태초로 돌아가서 객체지향 설계가 잘못된 것 같다는 생각이 들기도 했다

키오스크 클래스에 모든 책임들이 너무 방대하게 있다는 생각이 쉽사리 지워지지 않았다

 

메서드 분리 후에도 여전한 문제점

  • 주석 없이도 코드에 대해서 이해하고 파악할 수 있어야 하는데 도통 주석 없이는 이해하기 힘들다는 생각
  • 메서드로 최대한 많이 분리했지만 결국 start메서드 자체는 여전히 복잡함
  • Kiosk 의 책임은 여전히 너무나 큼

설계의 잘못됨(?)

하지만 또 생각해보면..

사용자에게 메뉴를 보여주고 입력을 받고 흐름을 제어하는 것 => Kiosk

장바구니와 관련된 역할을 수행하는 것 => Cart

가 아닐까 생각이 들기도 한다. 아니면 역할을 애초에 사용자의 키오스크 화면 상에서 카테고리와 관련된 로직을 수행하는 클래스, 메뉴와  관련된 로직을 수행하는 클래스, 장바구니와 관련된 로직을 수행하는 클래스 이런식으로 3개를 나누어서 설계를 했어야 하나 싶은 고민이 들기도 한다...

이번 키오스크 프로젝트에서 과연 객체지향적으로 적절하게 설계한 답이 너무 궁금하다 (나오면 바로 분석해야지)

 

 

초기화 메서드를 최초 한번만 실행하기 위한 방법에 대한 고민

  • menuItems의 리스트 인덱스를 생성을 딱 한번만 하기 위해서 어떤 식으로 할지 고민하다가 showMenuItems 메서드를 실행할 때 리스트 인덱스가 비어있으면 생성하고, 비어있지 않다면 생성하지 않는 식으로 구현
public void showMenuItems(){
        if (menuItemIndexList.isEmpty()){
            setMenuItemIndex();
        }
        for(int i = 0; i < menuItems.size(); i++){
            System.out.println( i + 1 + ". " + menuItems.get(i).getName() + " | W " + menuItems.get(i).getPrice() + " | " + menuItems.get(i).getDescription());
        }
    }

    public void setMenuItemIndex() {
        for(int i = 0; i < menuItems.size(); i++){
            menuItemIndexList.add(i+1);
        }
    }

 

그 밖의 메모해뒀던 여러 잡다한 고민들...

하지만 내가 생각한 이 고민들의 근원은 미약한 설계 이다.

결국 클래스 설계부터 기타 로직까지 충분한 시간을 가지고 생각하고 구현을 했어야 했는데 그냥 냅다 기존에 진행했던 과제를 기반으로 신나게 기능들만 열심히 구현을 해서 이런 생각이 든 것 같다. (그래서 막상 코드가 너무 복잡하고 더러워졌다. 그냥 절차지향의 코드인듯)

알고리즘을 풀 때도 종이랑 펜으로 먼저 설계라고 코딩을 하듯이 앞으로 프로젝트도 설계를 좀 더 주의깊게 한 다음에 코딩을 시작해야겠다!

 

Enum과 할인 적용

  • number(할인 정보 인덱스 번호), type(할인 타입), discountRate(할인률)로 구성
  • 할인을 적용하는 메서드(discount)는 추상메서드로 두고, 각각의 할인 객체는 이를 오버라이딩
  • 할인 정보를 출력하는 메서드(showInformation)와 사용자로부터 입력받은 값이 어떤 할인 객체에 해당되는지 확인하는 메서드(fromDiscount)는 정적(static)으로 선언 
  • fromDiscount 메서드에서 해당되는 할인 객체를 찾으면 해당 객체에서 discount를 실행한 후, 값을 바로 return
package org.example.kiosk.challenge;

import org.example.kiosk.common.exception.InvalidInputRangeException;

public enum Discount {
    NATIONALHONOREE("1", "국가유공자", 0.1) {
        @Override
        public double discount(double total) {
            return total - total * getDiscountRate();
        }
    },
	// 군인..학생..등등

    private final String number; // 할인 정보 인덱스 번호
    private final String type; // 할인 타입
    private final double discountRate; // 할인률

    public String getNumber() {
        return number;
    }

    public String getType() {
        return type;
    }

    public double getDiscountRate() {
        return discountRate;
    }

    Discount(String number, String type, double discountRate) {
        this.number = number;
        this.type = type;
        this.discountRate = discountRate;
    }

    // 각 클래스에 맞게 할인률이 적용되어 할인을 진행할 메서드
    public abstract double discount(double total);

    // 입력받은 문자열과 매칭되는 클래스인지 확인 후, 해당 할인 클래스의 할인을 수행시킨 후 적용된 할인값을 return
    public static double fromDiscount(String inputNumber, double total) throws InvalidInputRangeException{
        for (Discount discount : values()) {
            if (discount.getNumber().equals(inputNumber)) {
                return discount.discount(total);
            }
        }
        // 해당 할인 정보 화면에서 존재하지 않는 값을 입력했다면 에러 발생
        throw new InvalidInputRangeException();
    }

    // 할인 정보를 출력하는 메서드, 굳이 인스턴스화 시킬 필요가 없을 것 같아서 static으로 선언
    public static void showInformation(){
    }
}

 

Exception

  • InvalidInputException : 숫자가 아닌 다른 값을 입력할 경우 발생하는 예외 객체
  • InvalidInputRangeException : 화면에 출력된 값이 아닌 다른 숫자를 입력할 경우 발생하는 예외 객체
  • MenuItemNotFoundException : 메뉴를 삭제할 때, 장바구니 리스트에 없는 이름을 입력한 경우 발생하는 예외 객체

 

 


다시 결론적으로 앞으로는 설계를 충분히 고민하고 구현을 시작하자!!

고민했던 점

1. Kiosk의 start()메서드의 복잡함

  • Kiosk에서 사용자의 입력에 따라서 출력을 보여주는 start()메서드의 로직이 너무 길고 복잡해보여서 개발을 진행하면서도 너무 복잡하고 알아보기가 쉽지 않았음
  • 카테고리를 출력하는 부분과, 메뉴를 출력하는 부분을 각각 showCategories(), showMainMenes(Menu menu) 메서드로 분리하여 해결
private void showCategories() {
        System.out.println("[ MAIN MENU ]");
        for (int i = 0; i < menus.size(); i++) {
            System.out.println(i + 1 + ". " + this.menus.get(i).getCategory());
            this.indexList.add(i);
        }
        System.out.println("0. 종료 | 종료");
    }

 

 

2. 사용자로부터 적절한 값을 검증하는 부분에 대한 고민

  • 화면에 출력되는 부분에서 보여지는 숫자값만을 입력해서 검증하는 부분에서 이를 어떤 식으로 할 지 고민을 많이 했음
  • 화면에서 리스트 값들을 출력할 때 인덱스 리스트를 만들어, 이 리스트에 값들을 함께 추가한 다음에 사용자가 이 리스트에 해당되는 값만 입력했는지를 검증하는 방법으로 구현
private void showCategories() {
        System.out.println("[ MAIN MENU ]");
        for (int i = 0; i < menus.size(); i++) {
            System.out.println(i + 1 + ". " + this.menus.get(i).getCategory());
            this.indexList.add(i);
        }
        System.out.println("0. 종료 | 종료");
    }

 

검증

private void validInputRangeValue(int inputIndex, List<Integer> validIndexList) throws InvalidInputRangeException {
        if (!validIndexList.contains(inputIndex)){
            throw new InvalidInputRangeException();
        }
    }

 

3. 검증메서드를 실행할 때, 유효한 인덱스 리스트의 조회 방법에 대한 고민

  • 동일한 클래스 내부에 있는 indexList 필드이기 때문에 그냥 this.indexList로 검증메서드 매개변수에 전달해야할지 아니면 getter를 만들어서 getter를 통하여 전달해야 할지 고민이 됐음 
  • 검색을 해본 결과 지금 같은 경우는 상관없지만 JPA 프록시를 다루게 되면 중요해진다고 한다. (김영한 선생님의 답변..)
    • 나중에 JPA를 다뤄볼 때 기억할 것!

 

MenuItem

  • 카테고리 내부에 있는 해당 음식들을 관리하는 클래스
  • 음식 이름(name), 음식 가격(price), 음식 설명(description) 필드를 가지고 있음
  • 무결성을 지키기 위해 각각의 필드는 private 으로 설정하고 getter, setter을 이용 (캡슐화)
public class MenuItem {
    
    private String name;
    private double price;
    private String description;

    MenuItem (String name, double price, String description) {
        this.name = name;
        this.price = price;
        this.description = description;
    }

    // 각각의 필드에 대한 getter, setter
}

 

Menu

  • 카테고리별로 메뉴를 관리하는 클래스, 해당 카테고리에 해당하는 메뉴 리스트를 가지고 있음
  • 음식리스트(menuItems), 해당 음식의 카테고리(category), 음식리스트의 인덱스리스트(menuItemIndexList)
  • menuItemIndexList를 만든 이유
    • 사용자의 입력번호에 따라서 해당 로직(선택한 음식 출력)을 수행해야 하는데, 유요한 값을 검증하는 부분에서 해당 리스트에 벗어나는 값을 입력하는 경우 InvalidInputRangeException 을 수행하도록 함
    • 음식리스트(menuItems)를 출력할 때 menuItemIndexList에 값을 추가함
public class Menu {
    private final String category;
    private final List<MenuItem> menuItems;
    private final List<Integer> menuItemIndexList = new ArrayList<>();

    public Menu(String category, List<MenuItem> menuItems) {
        this.category = category;
        this.menuItems = menuItems;
    }

    public void showMenuItems(){
        for(int i = 0; i < menuItems.size(); i++){
            System.out.println( i + 1 + ". " + menuItems.get(i).getName() + " | W " + menuItems.get(i).getPrice() + " | " + menuItems.get(i).getDescription());
            menuItemIndexList.add(i+1);
        }
    }

    public List<MenuItem> getMenuItems(){
        return this.menuItems;
    }

    public String getCategory(){
        return this.category;
    }

    public List<Integer> getMenuItemIndexList() {
        return this.menuItemIndexList;
    }

}

 

Kiosk

  • 사용자에게 메뉴를 보여주고, 해당 입력값에 따라서 결과를 보여주는 역할을 수행하는 클래스
  • 카테고리별 메뉴 리스트(menus), 사용자에게 숫자를 입력받았는지를 검증하는 정규표현식 변수(NUMBER_REG), 카테고리 인덱스 리스트(indexList)
  • 카테고리 인덱스 리스트(indexList) 또한 menuItemIndexList를 만든 이유와 동일
  • 에러를 검증하는 메서드
    • validInputValue : 사용자가 숫자를 입력했는지 검증, 숫자 이외의 값을 입력했을 시에 InvalidInputException 객체 생성
    • validInputRangeValue : 사용자가 유효한 범위의 숫자를 입력했는지 검증, 유효하지 않은 범위의 숫자를 입력했을 시에 InvalidInputRangeException에러 객체 생성 
private void validInputValue(String inputIndex) throws InvalidInputException {
        if (!inputIndex.matches(NUMBER_REG)){
            throw new InvalidInputException();
        }
    }
    private void validInputRangeValue(int inputIndex, List<Integer> validIndexList) throws InvalidInputRangeException {
        if (!validIndexList.contains(inputIndex)){
            throw new InvalidInputRangeException();
        }
    }
public class Kiosk {

    private final List<Menu> menus;
    private final String NUMBER_REG = "[0-9]+";
    private final List<Integer> indexList = new ArrayList<>();

    public Kiosk(List<Menu> menus) {
        this.menus = menus;
    }

    public void start() {
        Scanner sc = new Scanner(System.in);

        while (true) {
            try {
                showCategories();

                String inputCategoryIndex = sc.next();
                if ("0".equals(inputCategoryIndex)) {
                    System.out.println("프로그램이 종료되었습니다.");
                    break;
                }

                validInputValue(inputCategoryIndex);
                int categoryIndex = Integer.parseInt(inputCategoryIndex) - 1;
                validInputRangeValue(categoryIndex, this.getIndexList());

                Menu menu = this.menus.get(categoryIndex);
                showMainMenes(menu);

                while (true){
                    String inputMenuIndex = sc.next();
                    if ("0".equals(inputMenuIndex)) {
                        System.out.println("메인 화면으로 돌아갑니다.");
                        break;
                    }

                    validInputValue(inputMenuIndex);
                    int menuIndex = Integer.parseInt(inputMenuIndex) - 1;
                    validInputRangeValue(menuIndex, menu.getMenuItemIndexList());

                    MenuItem menuItem = menu.getMenuItems().get(menuIndex);
                    System.out.println("선택한 메뉴 : " + menuItem.getName() + " | W " + menuItem.getPrice() + " | " + menuItem.getDescription());
                }

            } catch (InvalidInputException | InvalidInputRangeException e){
                System.out.println(e.getMessage());
            }
        }

    }

    public List<Integer> getIndexList() {
        return this.indexList;
    }

    private void showCategories() {
        System.out.println("[ MAIN MENU ]");
        for (int i = 0; i < menus.size(); i++) {
            System.out.println(i + 1 + ". " + this.menus.get(i).getCategory());
            this.indexList.add(i);
        }
        System.out.println("0. 종료 | 종료");
    }

    private void showMainMenes(Menu menu) {
        System.out.println("[ " + menu.getCategory()+ " MENU ]");
        menu.showMenuItems();
        System.out.println("0. 뒤로가기");
    }

}

입출력 

멘토님 코드

  • 음수 입력시에 재입력을 받도록 처리가 되어 있음
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();
        }
    },
    }

 

 

 

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

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

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

제네릭 타입 제한

  • 제네릭 타입 제한은 클래스, 메서드, 인터페이스 선언 부분에서만 가능함
public class Calculator<T extends Number> {
    private final List<T> resultList = new ArrayList<>(); // T는 Number의 하위 타입
}

 

  • 클래스에서 T의 제한을 걸면, 타입의 제한이 클래스 내부 전체에 걸림
public class Calculator<T extends Number> {
    private final List<T> resultList = new ArrayList<>(); // T는 Number의 하위 타입
}

 

-> 지난번에 내가 과제에서 수행했던 것처럼 메서드 수준에서 제네릭 제한을 걸어버리면, results 에 타입을 Number로 지정해서 해결했어야 했음

 

지난번 짠 코드

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;
    }

 

results 타입을 Number로 지정해서 해결

public class Calculator {
    private final List<Number> resultList = new ArrayList<>();

    public <T extends Number> void addResult(T result) {
        resultList.add(result); // T는 Number의 하위 타입
    }
}

 

필드 타입이 제네릭이라면 값을 지정해 줄 때에도 제네릭으로 전달

  • Double 값 자체로 값을 넣어서 전달해버리면 'add(T)' in 'java.util.List' cannot be applied to '(java.lang.Double)' 에러 발생
public class Calculator <T extends Number>{
    private final List<T> resultList = new ArrayList<>();

    public Double calculate(){
        Double a = 10.0;
        resultList.add(a);
        return a;
    }
    
}

 

  • 제네릭 타입으로  값을 전달해줘야 함
  • resultList.add((T)10.0); -> 불가 타입이 명확해야 Number의 하위 타입인 제네릭이라고 판단 할 수 있음 
public class Calculator <T extends Number>{
    private final List<T> resultList = new ArrayList<>();

    public Double calculate(){
        Double a = 10.0;
        String b = "sdfsaf";
        resultList.add((T)a);
        return a;
    }

    public List<T> getResult() {
        return resultList;
    }
}

 

결론적으로 제네릭타입의 필드로 컬렉션 프레임워크를 명시를 해줬으면, 해당 컬렉션에 접근할 때도 제네릭으로 접근해야 한다!

기존 OperatorType

  • 연산자 클래스별로 연산자를 나타내는 char값만 존재함
public enum OperatorType {
    ADD('+'),
    SUBTRACT('-'),
    MULTIPLY('*'),
    DIVIDE('/');
    private final char operator;
    public char getOperator(){
        return operator;
    }
    OperatorType(char operator) {
        this.operator = operator;
    }
}
public class ArithmeticCalculator {
    private OperatorType addOperator = OperatorType.ADD;
    private OperatorType subtractOperator = OperatorType.SUBTRACT;
    private OperatorType divideOperator = OperatorType.DIVIDE;
    private OperatorType multiplyOperator = OperatorType.MULTIPLY;
    private ArrayList<Double> results = new ArrayList<>();
    public <T extends Number> Double calculate(T number1, T number2, char operator){
        double answer = 0.0;
        //연산 수행
        if (operator == addOperator.getOperator()) {
            answer = number1.doubleValue() + number2.doubleValue();
        } else if (operator == subtractOperator.getOperator()) {
            answer = number1.doubleValue() - number2.doubleValue();
        } else if (operator == multiplyOperator.getOperator()) {
            answer = number1.doubleValue() * number2.doubleValue();
        } else if (operator == divideOperator.getOperator()) {
            answer = number1.doubleValue() / number2.doubleValue();
        }
        results.add(answer);
        return answer;
    }
}

 

이런식으로 Enum 클래스에 연산자만 선언해서 계산기 클래스 내부에서 해당 연산자값을 비교해오는 방식으로 사용했었는데, 아무리 생각해봐도 뭔가 연산자만 비교하기에는 아쉬운 것 같다는 생각이 들었다. 연산자 char 값만 비교하기에는 너무 메모리상으로도 낭비같고 차라리 계산기 클래스 내부에서 static final로 상수를 선언하면 되지 않나 싶었다.

 

그래서 연산자 값 뿐만 아니라 차라리 메서드를 Enum 클래스에서 따로 구현해서 수행하는 편이 훨씬 의미가 있을 거라는 생각이 들었다. 나중에 더하기 로직을 내부적으로 +10 씩 더한다거나 등등 유지보수성과 명확성을 위해서는 훨씬 유용할 것 같다는 생각이 들어서 수정하게 되었다

 

수정된 OperatorType

  • calculate 메서드를 추상 메서드로 선언 후 각각의 연산자 타입에서 오버라이딩 하여 다르게 구현
package com.example.calculator3;

import com.example.common.exception.DivideByZeroException;

// 연산자 및 해당 연산자에 해당하는 연산메서드를 선언한 enum class
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 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;
    }
    }

 

예외처리 추가

  • 잘못 입력받은 연산자 값과 피연산자 값, 0으로 나눈 경우의 예외 처리 클래스를 생성

  • ArithmeticCalculator에 입력값 예외처리에 대한 내용을 넣기에는 ArithmeticCalculator의 책임이 커지는 것 같다고 생각했기에 Parser를 생성하여 Parser객체에서 입력값에 대한 예외처리에 대한 내용을 처리하도록 책임을 분리
  • ArithmeticCalculator에서는 연산 수행과 관련된 예외 처리만 수행하도록 함
/*
* 입력값을 검증하는 class, ArithmeticCalculator에 입력값 검증까지 포함하면 책임이 너무 커지기에 분리
*/
public class Parser {
    private static final String OPERATION_REG = "[+\\-*/]";
    private static final String NUMBER_REG = "^[0-9]*$";

    private String firstInput;
    private String secondInput;
    private String operator;

    private final ArithmeticCalculator arithmeticCalculator = new ArithmeticCalculator();

    public ArithmeticCalculator getArithmeticCalculator() {
        return arithmeticCalculator;
    }

    //입력받은 피연산자가 숫자가 아닌 경우룰 처리하는 예외 처리 메서드
    public void validateInputNumbers(String firstInput, String secondInput) throws BadInputException {
        if (!Pattern.matches(NUMBER_REG, firstInput) || !Pattern.matches(NUMBER_REG, secondInput)) {
            throw new BadInputException("숫자타입");
        }
        setFirstInput(firstInput);
        setSecondInput(secondInput);
    }

    //입력받은 연산자가 사칙연산자에서 벗어난 경우룰 처리하는 예외 처리 메서드
    public void validateOperator(String operator) throws InvalidOperatorException{
        if (!Pattern.matches(OPERATION_REG, operator)) {
            throw new InvalidOperatorException();
        }
        setOperator(operator);
    }

    //필드로 가지고 있는 ArithmeticCalculator를 호출하여 연산을 수행
    public Double executeCalculator() throws DivideByZeroException {
        // 여기서 String 타입의 피연산자들을 실수형이나 정수형으로 변환하여 자유롭게 넣어 줄 수 있음, Integer도 가능
        Double firstNumber = Double.parseDouble(firstInput);
        Double secondNumber = Double.parseDouble(secondInput);
        return arithmeticCalculator.calculate(firstNumber, secondNumber, operator);
    }

    public void setFirstInput(String firstInput) {
        this.firstInput = firstInput;
    }

    public void setSecondInput(String secondInput) {
        this.secondInput = secondInput;
    }

    public void setOperator(String operator) {
        this.operator = 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;
    }
    }

 

Lambda

Lambda란 인스턴스를 생성하지 않고 작성할 수 있는 익명함수!

  • 메서드는 클래스에 포함되어야 하기 때문에 번거로운 제약들이 많음 → 람다의 등장으로 메서드를 변수처럼 다루는 것이 가능 → 람다를 통해서 메서드가 1급 객체가 됨 (이를 통해 자바는 객체지향프로그래밍 뿐만 아닌 함수지향 프로그래밍도 가능)
    • 1급객체 : 메서드(함수)를 변수, 인자, 리턴값에 할당 가능한 것
  • 람다식은 사실 메서드가 아닌, 익명 클래스의 객체

사용법

  • 람다의 모든 매개변수 타입은 생략하는 것이 좋음
  • 매개변수가 하나라면 괄호도 생략  (a) -> a * a 이 아닌 a -> a * a

실체

(int a, int b) -> a > b ? a : b; 의 실제 모습은 아래와 같음 즉, 람다식은 익명 클래스의 객체

new Objcet() {
  int max(int a, int b) {
    return a > b ? a : b;
  }
}

 

Stream

연속적인 데이터들의 순회로, 여러 필터링, 변환과 같은 작업들을 할 수 있게 도와주는  자바의 API

  • 한번에 한개씩 만들어지는 연속적인 데이터 항목들의 모임
  • 프로그램들은 입력 스트림을 통해 데이터를 하나씩 읽고, 출력 스트림을 통해 한개씩 기록
  • 기존에 반복문과 조건문을 활용하여 만든 로직보다 각 단계별로 어떤 "무엇을" 수행할 것인지 확실하게 명시되어 있어 의도를 파악하기 좋음
  • 스트림은 데이터 소스를 변경하지 않음
  • 일회용임

 

특정 값보다 큰 값들만 조회하기

1.  첫 번째 시도

  • cannot resolve symbol “r” 에러
 public List<Double> getResult(int minimum){
        return this.results.stream()
                .filter( r > minimum)
                .collect(Collectors.toList());
    }

 

의문이 들었다.. 내가 찾아본 예시와 다를게 없었기 때문이다. 아래 예시랑 다를게 없다고 생각했는데 이건 정말 실행이 잘 됐기 때문이다.

void filterTest3() {
    List<Human> tmpHumans = humans.stream()
            .filter(h -> h.getMoney() > 2000)
            .collect(Collectors.toList());

 

2.  두 번째 시도

  • 아직 리스트 내부의 값들이 선언이 안되서 그런건가 싶어서 한번 값을 선언해보고 했는데 이것도 동일한 에러가 뜨면서 실행되지 않음
  • 아까와 동일한 cannot resolve symbol “r” 에러
List<Integer> ra = Arrays.asList(1, 2, 3, 4);

    public List<Integer> getResult(int minimum){
        List<Integer> filterdResults = this.ra.stream()
                .filter( r > minimum)
                .collect(Collectors.toList());

        return filterdResults;
    }

 

하지만...!! 아주 바보같은 실수를 하고 만 것이다...!!

다시 차분히 진정하고 비교해보니 람다 표현식에서 매개변수 값을 안넣어버린 것이다...!! (바보🙈)

매개변수는 안넣고 냅다 리턴만 해버리면 될리가 없지..!

 

3.  완성

이번엔 람다 표현식을 정확하게 썼더니 에러가 발생하지 않았다!

public String getResult(int minimum){
        String filterdResults =  this.results.stream()
                .filter( result -> result > minimum)
                .collect(Collectors.toList())
                .toString();
        
        return filterdResults;
    }

 

 

덕분에 람다식을 쓸 때 앞으로는 확실히 알 수 있을 것 같다!!

람다는  매개변수 -> 리턴 형식!

 

여러 자료를 보며 내가 이해한 제네릭이란 메서드나 클래스의 타입에 대한 지정을 최초 생성시에 하는 것이 아닌 인스턴스를 만들 때 외부에서 지정해주는 것이었다.

 

한마디로 핵심은 외부에서 타입을 지정해 준다는 것!

 

1. 첫번째 시도

  • 계산의 결과를 담는 results 배열의 타입을 제네릭으로 지정해주고, 계산을 수행하는 calculate 메서드의 리턴타입과 매개변수 타입을 동일한 제네릭으로 선언
  • Operator ‘+’ cannot be applied to ‘T’, ‘T’ 에러 발생
public class A<T> {
    private ArrayList<T> results = new ArrayList<>();

    public T calculate(T number1, T number2){

        results.add(number1);
        return number1 + number2;
    }

}

 

gpt 에게 물어본 결과, 제네릭인 T타입이 어떤 데이터 타입이 올 수 있을지 모르기 때문에 '+'연산 자체를 지원하는지 여부도 알 수 없기 때문에 에러가 났다고 알려줬다 -> 결론적으로 "타입의 제한이 필요하다"는 결론이 나왔다

 

2. 두번째 시도

제네릭의 타입 제한 : <K extends T>   - K는 T이거나 T를 상속받은 하위타입이어야 함

  • 피연산자의 타입을 Number의 하위 타입만 받을 수 있게 가능하도록 제한함
  • 동일한 에러 발생
public class A<T extends Number> {
    private ArrayList<T> results = new ArrayList<>();

    public T calculate(T number1, T number2){

        results.add(number1 + number2);
        return  result;
    }

}

    

 

타입의 제한을 걸어주었지만, Integer가 들어오는 경우와 Double이 들어오는 경우에 따라서 더하기 연산을 수행하면 결과타입이 double이 될지 int가 될지 자바 컴파일러는 모르기 때문에 에러가 난 것 같다

 

이 부분에서 시간이 꽤 들었는데 그 이유는 아래의 예시는 잘 작동이 됐었기에 당연히 각각의 타입을 알아서 구분하고 더하기까지 해줄 수 있을 거라고 생각해서 그랬던 것 같다🙄

 

3. 세번째 시도

메서드 수준에서의 제네릭 : public <T> Double calculate(T number1, T number2), return 타입 앞에 <T> 붙이기

  • calculate 메서드의 return 타입을 Double로 지정해 주고, results 의 타입 또한 Double로 지정
    • 값이 정수로 들어오든, 부동소수점형으로 들어오든지 간에 정확한 계산 결과값을 보여주기 위해서는 Double로 고정하는 게 좋다고 생각했기 때문이다!
public class A {
    private ArrayList<Double> results = new ArrayList<>();

    public <T extends Number> Double calculate(T number1, T number2){
        Double result = number1.doubleValue() + number2.doubleValue();
        results.add(result);
        return  result;
    }
}

 

완성! 이제 이 코드를 바탕으로 과제에 반영해보았다

 

4. 완성

피연산자에 정수를 넣어도 실수를 넣어도 결과가 정확하게 나왔다!

public class ArithmeticCalculator {

    private OperatorType addOperator = OperatorType.ADD;
    private OperatorType subtractOperator = OperatorType.SUBTRACT;
    private OperatorType divideOperator = OperatorType.DIVIDE;
    private OperatorType multiplyOperator = OperatorType.MULTIPLY;

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

    public <T extends Number> Double calculate(T number1, T number2, char operator){
        double answer = 0.0;

        //연산 수행
        if (operator == addOperator.getOperator()) {
            answer = number1.doubleValue() + number2.doubleValue();
        } else if (operator == subtractOperator.getOperator()) {
            answer = number1.doubleValue() - number2.doubleValue();
        } else if (operator == multiplyOperator.getOperator()) {
            answer = number1.doubleValue() * number2.doubleValue();
        } else if (operator == divideOperator.getOperator()) {
            answer = number1.doubleValue() / number2.doubleValue();
        }

        results.add(answer);
        return answer;
    }

    public String getAllResults(){
        return results.toString();
    }

    public Double getResults(int index){
        return results.get(index);
    }

    public void setResult(int index, double value){
        results.set(index, value);
    }

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

}

 

 

추가적으로 안 것

추가적으로 이번 과제를 하면서 git에 시간을 엄청 많이 쓴 것 같다 ㅎㅎ...

 

잘못 올린 커밋 메시지를 수정하고 싶어서 vim에서 esc후 :wq를 아무리 눌러봐도 아무런 동작도 일어나지 않았다 ㅠㅠ

거의 몇시간동안 시도해보고... 심지어 친구 노트북으로도 똑같이 수행해봤는데 너무 잘 되서 계속 스트레스를 받던 도중에 나의 경우는 vim이 아니라 Nano가 기본 편집기로 설정되어 있다는 것을 깨닫고 해결했다,,,

 

Push 후 Commit 수정

https://kyn1013.tistory.com/167

 

그 다음으로는 실수로 이상하게 연속적으로 커밋한 기록을 발견하고 push가 끝난 후의 커밋을 삭제하는 방법도 알게 됐다

Push 후 Commit 삭제

https://kyn1013.tistory.com/175

 

 

import java.util.Scanner;

public class CalculatorApp {

    public static boolean start() throws Exception { //throws Exception : 위험하다는 표시, 이에 대한 메서드의 예외 처리를 해줘야 함
        Parser parser = new Parser();
        Scanner scanner = new Scanner(System.in);

        System.out.println("첫번째 숫자를 입력해주세요!");
        String firstInput = scanner.nextLine();
        parser.parseFirstNum(firstInput);

        System.out.println("연산자를 입력해주세요!");
        String operator = scanner.nextLine();
        parser.parseOperator(operator);

        System.out.println("두번째 숫자를 입력해주세요!");
        String secondInput = scanner.nextLine();
        parser.parseSecondNum(secondInput);

        System.out.println("연산 결과 : " + parser.executeCalculator());
        return true;
    }


}
package org.tesk;

import java.util.regex.Pattern;

public class Parser {
    private static final String OPERATION_REG = "[+\\-*/]";
    private static final String NUMBER_REG = "^[0-9]*$";

    private final Calculator calculator = new Calculator();

    public Parser parseFirstNum(String firstInput) throws BadInputException{
        if (!Pattern.matches(NUMBER_REG, firstInput)) {
            throw new BadInputException("정수값");
        }

        this.calculator.setFirstNumber(Integer.parseInt(firstInput));
        return this;
    }

    public Parser parseSecondNum(String secondInput) throws BadInputException {
        if (!Pattern.matches(NUMBER_REG, secondInput)){
            throw new BadInputException("정수값");
        }

        this.calculator.setSecondNumber(Integer.parseInt(secondInput));
        return this;
    }

    public Parser parseOperator(String operationInput) throws BadInputException{
        if (!Pattern.matches(OPERATION_REG, operationInput)){
            throw new BadInputException("사칙연산 연산자");
        }

        switch (operationInput) {
            case "+" -> this.calculator.setOperation(new AddOperation());
            case "-" -> this.calculator.setOperation(new SubstractOperation());
            case "*" -> this.calculator.setOperation(new MultiplyOperation());
            case "/" -> this.calculator.setOperation(new DivideOperation());
        }

        return this;
    }

    public double executeCalculator() {
        return calculator.calculate();
    }
}
package org.tesk;

public class BadInputException extends Exception {
    public BadInputException(String type) {
        super("잘못된 입력입니다! " + type + "을 입력해주세요!");
    }
}
package org.tesk;

public class Calculator{

    private int firstNumber;
    private int secondNumber;

    private AbstractOperation operation;

    public Calculator(AbstractOperation operation){
        this.operation = operation;
    }

    public Calculator() {
    }

    public void setOperation(AbstractOperation operation) {
        this.operation = operation;
    }

    public void setFirstNumber(int firstNumber) {
        this.firstNumber = firstNumber;
    }

    public void setSecondNumber(int secondNumber) {
        this.secondNumber = secondNumber;
    }

    public double calculate() {
        double answer = 0;
        answer = operation.operate(this.firstNumber, this.secondNumber);
        return answer;
    }

}

 

package org.tesk;

public class Main {
    public static void main(String[] args) {
        boolean calculateEnded = false;

        try{
            calculateEnded = CalculatorApp.start();
        } catch (BadInputException e) {
            System.out.println(e.getMessage());
        }

    }
}

 

과제를 스스로 해결하기 어려웠던 점

  • public Parser parseFirstNum 메서드 부분에서 리턴을 어떻게 해줘야 할지 몰랐음
    • 그냥 this로 리턴을 해주면 됨
  • 이어서 리턴을 할 때 왜 굳이 저 메서드에서 Parser객체를 리턴해 줘야 하는지 고민을 했었다. return을 객체로 안해주면 오류가 안잡히나 생각을 해봤는데, void로 변경하고 실행해도 오류는 잘 잡혔다(당연히) 그냥 큰 이유없이 넣으신 것 같다는 결론이 나왔다

  • CalculatorApp 클래스의 존재에 대해서 망각하고 있어서 도대체 parser객체로만 어떻게 해야하나 생각한 것 같다
  • throws에 대한 개념이 약간 모호해서 이를 어떻게 연관된 클래스끼리 연결시켜서 던져야 되는지 어려웠던 것 같다

 

예외처리의 구조

  • throw : 예외 객체를 생성하고, 발생된 예외는 가장 가까운 try-catch 블록에서 처리
  • throws : 메서드 선언부에서 예외를 선언하면, 호출하는 쪽에서 해당 예외를 처리해야 함
public void checkDivision(int divisor) throws ArithmeticException {
    if (divisor == 0) {
        throw new ArithmeticException("Divisor cannot be zero.");
    }
    System.out.println(100 / divisor);
}

public static void main(String[] args) {
    try {
        checkDivision(0); // 0으로 나누기 시도
    } catch (ArithmeticException e) {
        System.out.println("Caught exception: " + e.getMessage());
    }
}

 

throw를 통해 ArithmeticException이라는 예외객체를 발생시키고 가장 가까운 try catch블록에서 처리하려고 했으나 throws 가 메서드에 선언되어 있기 때문에 이 메서드를 호출한 곳으로 에러처리를 위임 -> 그래서 main 메서드의 try-catch에서 에러를 잡아서 처리 

 

과제에서의 구조

  • 메서드 옆에 throws BadInputException 이 명시되어 있기 때문에 이를 호출한 메서드로 BadInputException을 위임
    public void parseFirstNum(String firstInput) throws BadInputException{
        if (!Pattern.matches(NUMBER_REG, firstInput)) {
            throw new BadInputException("정수값");
        }

        this.calculator.setFirstNumber(Integer.parseInt(firstInput));
//        return this;
    }

 

  • 이를 호출한 start()도 throws가 되어있으므로 또 위임
public class CalculatorApp {
    public static boolean start() throws BadInputException{
        Parser parser = new Parser();
        Scanner scanner = new Scanner(System.in);

        System.out.println("첫번째 숫자를 입력해주세요!");
        String firstInput = scanner.nextLine();
        parser.parseFirstNum(firstInput);

 

  • start()를 호출한 mian에서 catch로 에러 잡음
public class Main {
    public static void main(String[] args) {
        boolean calculateEnded = false;

        while (!calculateEnded) {
            try {
                calculateEnded = CalculatorApp.start();
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

 

-> 그냥 throw는 에러객체를 생성하고 가장 가까운 곳에 있는 try-caatch문에서 처리해야 하는 것이고, throws는 자신의 메서드를 호출한 메서드로 에러객체를 던져서 처리를 위임하는 것!

+ Recent posts