[Java] 키오스크 리팩토링

thezz9·2025년 3월 10일
1

이번 과제에서는 키오스크 프로그램을 구현하는 것이 목표다.

요구사항은 다음과 같다.

README 보러가기


요구 사항

필수 Lv.1

  • 실행 시 햄버거 메뉴가 표시되고 입력받은 숫자에 따라 다른 로직을 실행
  • 특정 문자열을 입력하기 전까지 무한 반복
  • Scanner 클래스를 사용해 사용자 입력 처리

필수 Lv.2

  • 개별 음식 항목을 관리하는 MenuItem 클래스 생성
  • MenuItem 객체 생성을 통해 이름, 가격, 설명을 저장
  • menuItems 리스트를 탐색하며 반복적으로 출력

필수 Lv.3

  • 프로그램 메뉴를 관리하고 사용자 입력을 처리하는 Kiosk 클래스 생성
  • 기존 main함수에서 관리하던 입력 및 반복문 로직을 start 함수에서 관리
  • main 함수에서 Kiosk 객체를 생성할 때 값을 할당
  • 유효하지 않은 입력에 대한 오류 메시지 출력
  • 뒤로가기종료 기능 구현

필수 Lv.4

  • MenuItem 클래스를 관리하는 Menu 클래스 생성
  • 여러 menuItem을 포함하는 카테고리 이름 필드 추가
  • 메뉴 카테고리 이름을 반환하는 메서드 구현

필수 Lv.5

  • 각 클래스의 필드에 직접 접근하지 못하도록 캡슐화 적용

도전 Lv.1

  • 장바구니를 관리하는 Cart 클래스 생성 (기존 요구사항엔 없음)
  • 사용자가 선택한 메뉴를 장바구니에 추가하는 기능 구현
  • 결제 전 장바구니 내 모든 메뉴 및 총 금액 출력
  • 주문 완료 또는 주문 취소 시 장바구니 초기화

도전 Lv.2

  • 입력값을 검증하는 Input 클래스 생성 (기존 요구사항엔 없음)
  • Enum을 활용해 사용자 유형별 할인율 관리
  • 람다 & 스트림을 활용한 장바구니 조회 기능 추가
  • 특정 메뉴를 제외하고 출력하는 기능 구현

과제 분석

이번 과제는 레벨별로 구현해야 하는 클래스가 명확히 지정되어 있다. 필수 과제부터 도전 과제까지 진행하면서 점진적으로 리팩토링을 수행하며 객체지향적인 사고에 익숙해지도록 하는 의도가 보인다. 다행히 이번에는 기능 구현 과정에서 큰 기술적 문제는 없었다. 그래서 주로 리팩토링한 내용을 정리해보려고 한다.


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

기존 코드

    private int getValidInput(Scanner sc, int maxOption) {
        int categoryChoice;
        while (true) {
            if (!sc.hasNextInt()) {
                System.out.println("올바른 숫자를 입력하세요.");
                sc.next();
                continue;
            }
            categoryChoice = sc.nextInt();
            if (categoryChoice >= 0 && categoryChoice <= maxOption) {
                return categoryChoice;
            } else {
                System.out.println("올바른 메뉴 번호를 입력하세요.");
            }
        }
    }

수정 코드

    /** 숫자 검증 코드 */
    private int getValidInput() {
        while (!sc.hasNextInt()) {
                System.out.println("올바른 숫자를 입력하세요.");
                sc.next();
        }
        return sc.nextInt();
    }

    /** 유효한 숫자 검증: maxOption 파라미터로 메뉴의 범위를 전달 */
    private int getValidInputInRange(int maxOption) {
        int menuChoice;
        while (true) {
            menuChoice = getValidInput();
            if (menuChoice >= 0 && menuChoice <= maxOption) {
                break;
            }
            System.out.println("올바른 메뉴 번호를 입력하세요.");
        }
        return menuChoice;
    }

키오스크 프로그램은 숫자를 입력받아 진행되므로 기존 코드도 문제없이 작동했다. 하지만 하나의 메서드에서 두 가지 기능(입력값 검증 + 범위 체크)을 동시에 수행하는 것은 좋은 설계가 아니다.

또한, 코드를 작성한 본인은 이해할 수 있지만, maxOption이라는 파라미터의 역할이 직관적이지 않을 수 있어 주석을 추가했다.

유연한 검증 예시

	// 메뉴 확장 시 입력값 검증의 유연함을 위해 변수 사용
    int menuSize = menus.size();
    System.out.println("[ MAIN MENU ]");
    for (int i = 0; i < menuSize; i++) {
    	System.out.println((i + 1) + ". " + menus.get(i).getCategory());
    }

    // 장바구니가 비어있으면 메뉴 미출력
    if (!cart.getCartItems().isEmpty()) {
    	System.out.println("[ ORDER MENU ]");
        System.out.println("4. Orders       | 장바구니를 확인 후 주문합니다.");
        System.out.println("5. Cancel       | 진행중인 주문을 취소합니다.");
        menuSize = menuSize + 2;
     }
     System.out.println("0. 종료      | 종료");

     // 사용자 입력 받기
     System.out.print("카테고리 번호를 입력하세요: ");
     int categoryChoice = getValidInputInRange(menuSize);

이렇게 menuSize 변수와 maxOption 파라미터를 활용하면 메뉴 확장되더라도 유연한 검증이 가능하다.


2. 객체에 맞는 역할 분배

기존 코드

	case 4:
    	double totalPrice = 0;
        System.out.println("[ ORDERS ]");
        for (MenuItem item : cart.getCartItems()) {
        	System.out.println(item.getName() + " | W " +
            item.getPrice() + " | " + item.getDescription());
            totalPrice = totalPrice + item.getPrice();
            }
        System.out.println("[ TOTAL ]");
        System.out.println("W " + totalPrice);
        System.out.println("1. 주문      2. 메뉴판");
        int confirm = getValidInput(sc, 2);
        if (confirm == 1) {
        	System.out.println("주문이 완료되었습니다. 금액은 W " + totalPrice + "입니다.");
            cart.clearCart();
            break;
        } else {
        	continue;
        }

수정 코드

	case 4:
		double totalPrice = cart.getTotalPrice();
        System.out.println("1. 주문      2. 메뉴판");
        int confirm = getValidInputInRange(2);
        if (confirm == 1) {
        	System.out.println("주문이 완료되었습니다. 금액은 W " + totalPrice + "입니다.");
            cart.clearCart();
            break;
        } else {
            continue;
        }
                    
	/** 장바구니 내 상품 출력 및 총 금액 계산 (반올림 적용) */
	public double getTotalPrice() {
		double totalPrice = 0;
        System.out.println("[ ORDERS ]");
        for (MenuItem item : getCartItems()) {
            System.out.println(item.getName() + " | W " +
                    item.getPrice() + " | " + item.getDescription());
            totalPrice = totalPrice + item.getPrice();
        }
        totalPrice = Math.round(totalPrice * 100.0) / 100.0;
        System.out.println("[ TOTAL ]");
        System.out.println("W " + totalPrice);
    return totalPrice;
    }

Kiosk 클래스는 흐름을 제어하는 역할인데, 장바구니 출력과 총 결제금액까지 처리하는 것은 부적절하다. 따라서 Cart 클래스의 getTotalPrice() 메서드로 기능을 분리했다.


3. Enum을 활용해 사용자 유형별 할인율 관리

기존 코드

  public enum Discount {
      VETERAN(0.10), SOLDIER(0.05), STUDENT(0.03), GENERAL(0);

      private final double discount;

      Discount(double discount) {
          this.discount = discount;
      }

      public double getDiscount() {
          return discount;
      }
  }
	/**
     * 유형별 할인율에 따른 총 금액 계산 (반올림 적용)
     */
    public double getDiscountPrice(int discountChoice, double totalPrice) {
        switch (discountChoice) {
            case 1:
                totalPrice = totalPrice - (totalPrice * Discount.veteran.getDiscount());
                break;
            case 2:
                totalPrice = totalPrice - (totalPrice * Discount.soldier.getDiscount());
                break;
            case 3:
                totalPrice = totalPrice - (totalPrice * Discount.student.getDiscount());
                break;
            case 4:
                break;
        }
        totalPrice = Math.round(totalPrice * 100.0) / 100.0;
        return totalPrice;
    }

수정 코드

  public enum Discount {
      VETERAN(1, 0.10),
      SOLDIER(2, 0.05),
      STUDENT(3, 0.03),
      GENERAL(4, 0);

      private final int discountChoice;
      private final double discount;

      Discount(int discountChoice, double discount) {
          this.discountChoice = discountChoice;
          this.discount = discount;
      }

      public double getDiscount() {
          return discount;
      }

      public int getDiscountChoice() {
          return discountChoice;
      }

      public static double getDiscountForChoice(int discountChoice) {
          for (Discount dc : Discount.values()) {
              if (dc.getDiscountChoice() == discountChoice) {
                  return dc.getDiscount();
              }
          }
          return GENERAL.discount;
      }
  }
    /**
     * 유형별 할인율에 따른 총 금액 계산 (반올림 적용)
     */
    public double getDiscountPrice(int discountChoice, double totalPrice) {
        double discount = Discount.getDiscountForChoice(discountChoice);
        totalPrice = Math.round((totalPrice - (totalPrice * discount)) * 100.0) / 100.0;
        return totalPrice;
    }

기존 코드는 사용자 유형이 추가되면 EnumgetDiscountPrice() 메서드까지 수정해야 했지만, 수정된 코드는 Enum만 수정하면 되도록 개선했다.


4. 람다 & 스트림을 적용한 상품 삭제

	else if (confirm == 3) { // 삭제
    	cart.getTotalPrice();
        System.out.print("삭제할 상품 번호를 입력하세요: ");
        int itemIndex = input.getValidInputInRange(cart.getCartItems().size());
        cart.removeCartItem(itemIndex);
        if (cart.getCartItems().isEmpty()) {
        	break;
        }
	/**
     * 장바구니 선택 삭제 메서드 (itemIndex: 상품 번호)
     */
    public void removeCartItem(int itemIndex) {
        cartItems = IntStream.range(0, cartItems.size()) // 인덱스 생성
                .filter(i -> i != itemIndex - 1) // 삭제할 itemIndex가 아닌 요소만 유지
                .mapToObj(cartItems::get) // 해당 인덱스의 cartItems 요소를 가져옴
                .collect(Collectors.toList()); // 필터링된 요소를 새로운 리스트로 변환
        System.out.println("상품이 삭제되었습니다.");
    }

이것도 기능추가에 가까운 부분이다.
장바구니를 출력한 후, 삭제를 원하는 상품의 itemIndex를 통해 필터링 조건을 받아
새로운 리스트로 변환하는 방식의 로직으로 구현해봤다.


느낀점

이전 과제(계산기)에서는 기능 구현에만 집중해서 리팩토링할 부분이 많았다. 하지만 이번에는 처음부터 각 객체와 메서드의 역할을 고민하며 개발할 수 있었다.

리팩토링할 내용이 많지 않다는 점이 오히려 내가 발전했다는 증거 아닐까?

profile
개발 취준생

0개의 댓글