관리 메뉴

개발공부기록

자바로 키오스크 만들기 - 도전 기능 구현(장바구니, 구매하기 기능 추가, Enum과 람다 & 스트림을 활용) 개발 본문

프로젝트 회고/토이프로젝트

자바로 키오스크 만들기 - 도전 기능 구현(장바구니, 구매하기 기능 추가, Enum과 람다 & 스트림을 활용) 개발

소소한나구리 2025. 3. 12. 16:21
728x90

키오스크 도전 기능 개발 회고


키오스크 - 도전 기능 구현

도전 LV1 -  장바구니, 구매하기 기능 추가하기

요구 사항

더보기

장바구니 생성 및 관리 기능

  • 사용자가 선택한 메뉴를 장바구니에 추가할 수 있는 기능
  • 메뉴명, 수량, 가격 정보를 저장하며 항목을 동적으로 추가 및 조회할 수 있어야 함
  • 사용자가 잘못된 선택을 했을 경우 예외를 처리함(유효하지 않은 메뉴 번호 입력)

장바구니 출력 및 금액 계산

  • 사용자가 결제를 시도하기 전에 장바구니에 담긴 모든 메뉴와 총 금액을 출력
  • 각 메뉴의 이름, 가격 수량, 총 금액 합계가 출력되야 함

장바구니 담기 기능

  • 메뉴를 클릭하면 장바구니에 추가할 지 물어보고 입력값에 따라 "추가", "취소" 처리하고 메뉴는 한 번에 1개만 담을 수 있음
  • 장바구니에 담은 목록을 출력

주문 기능

  • 장바구니에 담긴 모든 항목을 출력
  • 합산하여 총 금액을 계산하고"주문하기"를 누르면 장바구니를 초기화

도전 LV1 구현

PriceFormatter클래스

package challenge.util;

public abstract class PriceFormatter {

    public static String priceFormat(long price) {
        DecimalFormat df = new DecimalFormat("#,###");
        return df.format(price);
    }
}
  • Kiosk 클래스와 ShoppingCart 클래스에서 사용할 한국 통화 포맷으로 포맷팅하는 유틸 추상 클래스를 작성하였으며 이후 LV2에서도 사용할 것이므로 challenge 하위의 util 패키지를 만들어서 보관하였다
  • 객체 생성을 하지 못하도록 abstract 클래스로 작성하였으며 간편하게 사용할 수 있도록 static 메서드로 제공한다

ShoppingCart 클래스

package challenge.lv1;

import static challenge.util.PriceFormatter.priceFormat;

public class ShoppingCart {

    private final Map<MenuItem, Integer> cart = new HashMap<>();

    // 메서드 따로 설명
}
  • 장바구니를 관리하기 위해 Map을 생성하고 구현체로 HashMap을 선택하였는데 보편적으로 Map자료 구조에서 HashMap이 성능이 빠르기도하고 특별히 연결 구조나 정렬이 필요한 트리구조가 필요한 것이 아니라면 일반적으로 HashMap을 선택한다고 하여 선정하였다
  • Map 자료구조를 선택한 이유는 MenuItem을 key 값으로 수량을 관리하여 장바구니의 아이템에 담긴 수량을 조절하기 위해 List가 아닌 Map으로 하였다.
  • 보통 지금껏 예제에서 다뤄왔던 장바구니 구조에서는 아이템(MenuItem)이 수량을 가지도록 코드를 작성했었는데 여기에서는 기본 구조를 건들지 않고 장바구니를 추가해보고 싶어서 선택했다.

addCart()

public void addCart(MenuItem menuItem, int quantity) {
    cart.put(menuItem, quantity);
    System.out.println(menuItem.getName() + " "+ quantity + "개를 장바구니에 추가 하였습니다.");
}
  • 장바구니인 자료구조에 키오스크에서 선택된 MenuItem과 수량을 저장하고 담긴 목록을 출력해준다.
  • 수량을 입력할 때 잘못된 타입을 입력하면 0이 반환되고 의도적으로 0으로 수량을 입력하면 장바구니에 저장하지 않도록 예외를 처리한다.

showCart()

public String showCart() {
    long totalPrice = 0;
    System.out.println("[Orders]");
    for (MenuItem menuItem : cart.keySet()) {
        System.out.printf("%-18s | %d개 | 상품 합계: ₩ %10s | %s%n",
                // itemPrice(): 수량이 반영된 개별 메뉴당 합계 가격
                menuItem.getName(), cart.get(menuItem), priceFormat(itemPrice(menuItem)), menuItem.getDescription());
        totalPrice += itemPrice(menuItem);
    }
    System.out.println();
    System.out.println("[Total]");
    String priceFormat = priceFormat(totalPrice);
    System.out.println("₩ " + priceFormat);
    return priceFormat;
}
  • 장바구니의 내역을 cart.keySet()으로 Map의 key들을 Set 자료 구조로 뽑아서 하나씩 순회하면서 출력한다
  • 그리고 itemPrice()메서드를 호출하여 상품별 합계 금액을 계한하고 PriceFormat.priceFormat()메서드로 포맷팅해서 출력한다
  • 여기서 순회를 한 김에 전체 금액도 계산하는데 이때도 계산된 금액을 priceFormat()메서드로 포맷팅하여 출력하고 반환한다.
  • 사실 여기 코드가 썩 마음에 들지 않는데, showCart()에서는 내역만 반환하고 전체 금액은 다른 별도의 메서드로 분리하려고 했으나 불필요한 멤버 변수가 사용되거나 반복문을 한번 더 돌게 되는 것 같아서 포기했다
  • 구조상으로는 totalPrice는 별도의 메서드에서 관리하고 싶지만 현재 나의 능력으로는 지금과 같은 구조가 성능저하가 없을 것 같아서 이대로 진행 하였다

deleteAllCart()

public void deleteAllCart() {
    cart.clear();
}
  • 장바구니에 담긴 데이터를 싹 날리는 메서드이다.

itemPrice()

private long itemPrice(MenuItem menuItem) {
    return cart.get(menuItem) * menuItem.getPrice();
}
  • showCart()에서 사용하는 상품의 주문수량 * 단가를 반환하는 메서드이다
  • ShoppingCart 클래스에서만 사용하기 때문에 private으로 선언하였다

Kiosk 클래스

package challenge.lv1;

import static challenge.util.PriceFormatter.priceFormat;

public class Kiosk {

    private boolean flag;
    private final ShoppingCart shoppingCart = new ShoppingCart();
    
    // 다른 필드는 필수 LV5와 동일
    // 메서드 따로 설명
}
  • 주문 메뉴를 활성화 할 것인지 말 것인지를 판단하기 위한 flag를 선언했으며 false 이면 주문 메뉴는 활성화 되지 않는다
  • kiosk 에서 ShoppingCart 객체를 사용해야 하므로 생성했고 그 외의 필드는 도전과제 LV5와 같으므로 생략했다
  • 전체 흐름을 관장하거나 흐름상에서 각종 처리해야하는 예외 로직들이 모두 Kiosk 클래스에 있다.
  • 추가되거나 변경된 메서드들 위주로 설명하고 변경되지 않은 메서드드나 필드는 설명에서 제외했다.

start()

public void start() {
    Scanner scanner = new Scanner(System.in);
    int menusSize = menus.size();       // 등록한 메뉴의 개수를 변수화 (자주 사용함)

    /**
     * 프로그램 시작
     */
    while (true) {
        categoryPrinter(menus);                         // 메인 메뉴 -> 메뉴의 대분류(카테고리) 출력

        // flag == true : 주문 로직 활성화
        if (flag) {
            orderMenuPrinter();                         // 주문 메뉴 출력
        }

        int mainInput = inputValidator(scanner);        // 입력값 검증 -> 메뉴 선택 or flag가 true일 때 주문 및 취소 선택

        if (mainInput == 0) {
            System.out.println("프로그램을 종료합니다.");
            break;
        }

        /**
         * 카테고리(Menu)가 고정개수가 아니라 여러 카테고리가 늘어날 수 있기 때문에 memusSize를 기준으로 로직을 작성
         * flag == true: 주문 메뉴가 활성화되므로 menusSize + 2보다 크면 잘못된 입력값 간주(주문 메뉴는 2개로 고정임)
         * flag == false: 주문 메뉴가 활성화 되지 않았으므로 menusSize보다 크면 잘못된 입력값으로 간주
         */
        if (flag && mainInput > menusSize + 2|| !flag && mainInput > menusSize){
            System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
            continue;
        }

        /**
         * 주문 관련 로직
         */
        if (mainInput == menusSize + 1) {                       // mainInput == 4 -> 주문 진행
            String lastPriceFormat = order(shoppingCart);       // 주문 내역 출력

            int lastSelect = inputValidator(scanner);           // 입력값 검증 -> 주문, 메뉴판 이동 선택
            if (lastSelect == 1) {                              // lastSelect == 1 -> 최종 주문
                System.out.println("주문이 완료 되었습니다. 금액은 ₩ " + lastPriceFormat + " 입니다");
                shoppingCart.deleteAllCart();                   // 주문이 완료되면 장바구니 초기화 후 flag = false 설정
                flag = false;
                continue;
            } else if (lastSelect == 2) {                       // lastSelect == 2 -> 메뉴판 이동
                System.out.println("메뉴로 돌아갑니다");
                continue;
            } else {
                System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
                continue;
            }
        }

        /**
         * 주문 취소 로직 - 장바구니 초기화
         */
        if (mainInput == menusSize + 2) {
            System.out.println("장바구니가 비워졌습니다.");
            System.out.println();
            shoppingCart.deleteAllCart();
            flag = false;                           // flag == false: 주문 메뉴 비활성화
            continue;
        }

        /**
         * 상세 메뉴 로직
         */
        Menu menu = selectMainMenu(mainInput);
        while (true) {
            menuPrinter(menu);                          // 카테고리 하위 메뉴 출력
            int menuInput = inputValidator(scanner);    // 입력값 검증(재사용) -> 상세 메뉴 입력

            if (menuInput == 0) {
                System.out.println("처음으로 이동합니다.");
                break;
            }

            if (menu.findAllMenuItem().size() < menuInput) {
                System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
                continue;
            }

            MenuItem menuItem = selectMenuItem(menuInput, menu);    // 선택된 메뉴를 출력하고 반환

            /**
             * 장바구니 관련 로직
             */
            cartMenuPrinter(menuItem);                  // 장바구니 관련 메뉴 출력
            int cartAddInput = inputValidator(scanner); // 입력값 검증 -> 장바구니 담기, 취소 선택
            if (cartAddInput == 1) {
                flag = true;
                System.out.println("몇 개 주문 하시겠습니까? ");
                int quantity = inputValidator(scanner); // 주문 수량
                if (quantity == 0) {
                    System.out.println("장바구니에 담을 수 없습니다.");
                    flag = false;
                    continue;
                }
                shoppingCart.addCart(menuItem, quantity);
            } else if (cartAddInput == 2) {
                System.out.println("취소 하셨습니다. 이전으로 돌아갑니다.");
                continue;
            } else {
                System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
                continue;
            }
            break;
        }
    }
}
  • 전체 흐름을 관리하는 start()메서드의 변화가 가장 많다
  • 이부분도 if - else 문 범벅이라 가독성이 좋지 않아 커맨드 별로 관리하고 싶었지만 일단 완성이 우선이라는 생각에 할 수 있는 예외 처리를 우선으로 하고 리펙토링은 LV2가 끝나고 시간이 남으면 할 예정이다
  • flag 값에 따라 주문 로직을 활성화 하는 로직이 들어있고, flag의 활성화 여부에 따라 처음 입력받는 값의 검증 로직이 동적으로 변경되도록 작성하였다
  • flag가 true가 되면 주문 로직이 활성화 되어 주문 로직에 접근할 수 있게 된다
  • 상세 메뉴 로직에 접근하면 마지막에 선택한 아이템을 장바구니에 담을 수 있으며 장바구니에 담게 되면 자동으로 주문 로직이 활성화가 된다.
  • 요구사항에는 없지만 장바구니에 담을 때 담을 아이템의 상품의 개수를 입력하는 기능도 추가했다.
  • 주문 취소 버튼을 누르게 되면 장바구니의 내역이 모두 삭제되고 장바구니에 내역이 없으므로 flag를 false로 변환하여 주문 관련 로직이 나타나지 않도록 했다.
  • 기본적으로 잘못된 메뉴를 선택하게 되면 해당 반복문이 다시 시작되도록 예외 로직을 처리하였고 각 메뉴마다 취소 버튼이나 처음으로 돌아가는 버튼도 동작하도록 작성되어있다.
  • 최종적으로 주문을 확정하면 주문이 완료되며 최종 지불한 금액을 보여주고 다시 키오스크의 최초 메뉴로 돌아가며 이때 flag는 false로 장바구니는 초기화 시킨다.

order()메서드

private String order(ShoppingCart shoppingCart) {
    System.out.println("아래와 같이 주문 하시겠습니까?");
    System.out.println();
    String totalPriceFormat = shoppingCart.showCart();

    System.out.println();
    System.out.println("1. 확인\t 2. 메뉴판");
    return totalPriceFormat;
}
  • 주문 로직에서 동작하는 메서드로 shoppingcart.showCart()를 호출하여 장바구니에 담긴 전체 내역과 장바구니에 담긴 총 금액을 출력한다.

각종 메뉴를 출력하는 메서드들

private void orderMenuPrinter() {
    System.out.println();
    System.out.println("[ORDER MENU]");
    System.out.println("4. Orders\t| 장바구니를 확인 후 주문합니다.");
    System.out.println("5. Cancel\t| 진행중인 주문을 취소 합니다");
}

private void menuPrinter(Menu menu) {
    List<MenuItem> menuItems = menu.findAllMenuItem();

    System.out.println("[" + menu.getCategory() + " MENU]");

    int count = 1;
    for (MenuItem menuItem : menuItems) {
        System.out.printf("%d. %-18s | ₩ %7s | %s%n", count++, menuItem.getName(), priceFormat(menuItem.getPrice()), menuItem.getDescription());        }
    System.out.println("0. 처음으로             | 처음으로 이동");

}

private void cartMenuPrinter(MenuItem menuItem) {
    System.out.println('"' + menuItem.getName() + " | ₩ " + priceFormat(menuItem.getPrice()) + " | " + menuItem.getDescription() + '"');
    System.out.println("위 메뉴를 장바구니에 추가 하시겠습니까?");
    System.out.println("1. 확인\t 2. 취소");
}
  • 각종 메뉴를 출력하는 메서드들이다.
  • 주문 관련 메뉴를 출력하는 orderPrinter()와 장바구니 관련 메뉴를 출력하는 cartMenuPrinter()가 추가 되었고, 기존 상세 메뉴를 출력하는 menuPrinter에 priceFormat()을 적용하여 금액에 포맷을 적용시켰다
  • printf()로 출력할 때 칸이 무너지는걸 방지하기 위해 %7s도의 여유공간을 확보하여 출력하도록 변경했다

실행 결과

더보기
  • 출력되는 메뉴의 가격이 포맷팅 되어 출력된다
  • 메뉴를 선택하면 장바구니에 메뉴를 추가하거나 취소할 수 있고, 장바구니에 메뉴를 추가하면 상품을 몇 개 주문할 것인지 개수를 입력할 수 있다
  • 수량을 입력 하면 선택한 상품과 수량을 출려해주고 flag가 true가 되면서 주문 메뉴가 활성화 된다
  • 장바구니에 물건 담기를 취소하면 상품 선택 메뉴로 돌아간다
  • 주문 메뉴가 활성화 된 상태에서 Orders 버튼을 누르게 되면 주문내역과 전체 금액을 보여주고 최종 주문 확인을 할 것인지 메뉴판으로 돌아갈 것인지 선택하는 메뉴를 보여준다
  • 여기서 메뉴판으로 돌아가면 장바구니의 메뉴는 유지한 채로 처음으로 돌아가고, 확인을 누르면 주문이 완료되며 총 주문 금액이 출력되며 처음으로 돌아간다.
  • 이때 flag가 false가 되어 주문 메뉴는 비활성화 되고 장바구니도 초기화 된다
  • 주문 메뉴가 활성화 되어있을 때 Cancel을 선택하면 주문을 취소하면서 장바구니가 초기화 되고 주문 로직도 비활성화 된다
  • 모든 메뉴에서 숫자만 입력될 수 있도록 검증 로직은 여전히 동작한다
  • 수량 입력시 문자열을 입력하거나 0을 입력하면 장바구니에 물건이 담기지 않는다

도전 LV2 -  Enum, 람다 & 스트림을 활용하여 주문 및 장바구니 관리하기

요구 사항

더보기

목적

  • Enum을 통해 상수를 안전하게 관리
  • 제네릭을 활용하여 데이터의 유연성을 높이고 재사용 가능한 코드를 설계해보기
  • 스트림 API를 사용하여 데이터를 필터링하고 간결한 코드로 동작을 구현

Enum을 활용한 사용자 유형별 할인율 관리하기

  • 사용자 유형의 Enum 정의 및 각 사용자 유형에 따른 할인율 적용
  • 주문 시 사용자 유형에 맞는 할인율을 적용하여 총 금액을 계산

람다 & 스트림을 활용한 장바구니 조회 기능

  • Menu의 MenuItem을 조회 할 때 스트림을 사용하여 출력하도록 수정
  • 기존 장바구니에서 특정 메뉴의 빼기 기능을 추가(스트림 활용)
  • 기존 장바구니에 담긴 물건을 또 담으면 기존 장바구니의 메뉴가 증가 되도록 수정

도전 LV2 구현

구조 변경 적용 이유

  • 처음에 도전 LV1을 구현할 때도 고민했던 부분인데, 장바구니를 적용할 때 수량을 어떤 클래스가 보관하면 좋을지 고민을 많이 했었다.
  • 위에서 언급했듯 다뤄봤던 장바구니의 구조는 아이템 클래스에서 장바구니의 수량을 다루었는데 Map으로 장바구니를 구현하면 key로 바로 조회가 가능하니 장점도 있고 중복도 안되니 장점이 있을 것이라고 생각하여 Map<아이템, 수량> 구조로 장바구니의 구조로 LV1을 개발했고 LV2도 이 구조를 가지고 진행하고 있었다.
  • 그러나 과제 요구사항에서 스트림을 사용해야하는 요구사항이 있어 구현에 어려움을 겪어 튜터링을 2번 받고 구조를 엎기로 결정했다.
  • 일단 튜터링에서도 map으로 장바구니를 구현한 이유도 있고 개발한 것도 있으니 일단 이대로 구현하고 주석이나 설명으로 풀어도 된다고 하셨긴 했지만 MenuItem에서 수량을 관리하는게 맞다고 말씀 하셨기도 하고 Map보다는 List를 장바구니로 구현하는 것이 과제의 목적이기도 한 것 같아서 다시 개발했다.
  • 아래는 다시 개발한 도전 LV2의 변경된 코드들에 대한 설명이다

DiscountPolicy - Enum

package challenge.lv2;

public enum DiscountPolicy {

    PATRIOT(1, 10, "국가유공자"),
    SOLDIER(2, 5, "군인"),
    STUDENT(3, 3, "학생"),
    BASIC(4, 0, "일반")
    ;

    private final int input;
    private final int discountRate;
    private final String description;

    DiscountPolicy(int input, int discountRate, String value) {
        this.input = input;
        this.discountRate = discountRate;
        this.description = value;
    }

    public int getDiscountRate() {
        return discountRate;
    }

    public int getInput() {
        return input;
    }

    public String getDescription() {
        return description;
    }

    /**
     * totalPrice에 할인율을 적용하여 할인금액을 반환하는 메서드를 Enum에서 제공
     *
     * @param totalPrice 할인전 금액
     * @return 할인 정책에 따른 할인 금액을 반환
     */
    public long discount(long totalPrice) {
        return totalPrice * discountRate / 100;
    }
}
  • 할인 정책을 관리하는 Enum 클래스이며 국가유공자(10%), 군인(5%), 학생(3%), 일반(할인 없음)으로 나뉘어져 있다
  • 각 상수의 필드는 키오스크의 입력값이 숫자로 입력되므로 매칭 될 수 있는 input 값과, 할인율, 그리고 설명 필드가 있다
  • 사실 ordinal()로 상수의 선언된 순서를 통해 Enum을 찾아도 되지만 이 방법은 실무에서 좋지 않다고 배웠다
    • 예제에서는 할인정책이 변경되거나 할일은 없겠지만 실무에서는 할인 정책이 변경될 수도 선언된 순서도 바뀔 수 있기 때문에 버그가 발생할 가능성이 높아 가급적 다른 방법을 사용하는 것을 권장한다고 배웠다
    • 그래서 여기서도 ordinal()을 사용하지않고 input필드와 입력값을 비교하여 할인 정책을 찾는 방법을 선택했다
  • Enum에서 할인율을 적용하여 할인 금액을 반환하는 메서드를 제공하도록 하여 할인 정책을 사용하는 곳에서는 할인 정책을 찾아서 discount()메서드만 호출하면 할인 금액을 알아내서 활용할 수 있다.

MenuItem 클래스

package challenge.lv2;

public class MenuItem {

    // 기존 코드들은 그대로 유지 설명 생량

    /**
     * 장바구니에 담은 수량
     */
    private int quantity;

    public int getQuantity() {
        return quantity;
    }

    public void addQuantity(int quantity) {
        this.quantity += Math.abs(quantity);
    }

    public long itemPrice() {
        return quantity * price;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        MenuItem menuItem = (MenuItem) o;
        return getPrice() == menuItem.getPrice() && Objects.equals(getName(), menuItem.getName()) && Objects.equals(getDescription(), menuItem.getDescription());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getName(), getPrice(), getDescription());
    }
}
  • 변경된 설계로 MenuItem에서 수량을 관리하는 필드를 추가했으므로 수량을 얻을 수 있는 게터도 추가했다.
  • 수량을 추가하기 위해 addQuantity(int quantity) 메서드를 추가하여 this.quantity 필드에 매개변수로 넘어온 수량을 더해지도록 했으며, 이때 실수로 음수가 작성되더라도 양수로 더해지도록 예외 처리 로직을 작성했다.
  • 수량과 단가를 곱한 장바구니에 담긴 MenuItem의 금액을 출력하기 위한 itemPrice() 메서드도 제공한다
    • 메서드 이름을 지을 때 너무 고민이 많았다..
    • 총 금액도 아니고 합계 금액도 아니고 각 상품의 단가 * 수량 금액을 뭐라 표현할지 몰라서 단순하게 itemPrice()로 결정 지었다
  • 이후 로직에서 객체를 동등성 비교를 할일이 많이 있어서 equals()와 hashCode()를 오버라이딩 해두었다.

ShoppingCart 클래스

package challenge.lv2;

public class ShoppingCart {

    private final List<MenuItem> cart = new ArrayList<>();
    
    //  메서드들 별도로 설명
}
  • 변경된 구조로 코드를 개선하기 위해 장바구니를 List로 선언하고 구현체를 ArrayList를 선택했다
  • 중복이 되지 않는 Set 컬렉션으로 하려고 했으나 순서가 보장이 안되므로 List 자료구조를 선택했다.

addCart()

public void addCart(MenuItem menuItem, int quantity) {
    boolean matchResult = cart.stream().anyMatch(cartItem -> cartItem.equals(menuItem));

    if (matchResult) {
        menuItem.addQuantity(quantity);
    } else {
        menuItem.addQuantity(quantity);
        cart.add(menuItem);
    }

    System.out.println(menuItem.getName() + " " + quantity + " 개가 장바구니에 추가 되었습니다.");
}
  • 장바구니에 아이템을 저장하고 수량을 MenuItem 필드에 추가하는 메서드이다.
  • Stream을 활용하여 저장할 MenuItem이 기존 장바구니에 있는지 확인하여 있다면 MenuItem에 수량만 추가하고, 없다면 MenuItem에 수량을 저장하고 해당 MenuItem을 장바구니에 담는다
  • MenuItem에서 이미 equals()와 hashCode()를 오버라이딩 해두었으므로 해당 필드가 아니라 객체 자체를 equals()비교 해도 동등성 비교가 적용된다.
  • 이후 저장한 상품과 수량을 출력해준다.

showCart(), getTotalPrice()

public void showCart() {
    System.out.println("[Orders]");
    cart.stream().forEach(menuItem -> System.out.printf("%-18s | %d개 | 상품 합계: ₩ %10s | %s%n",
            // itemPrice(): 수량이 반영된 개별 메뉴당 합계 가격
            menuItem.getName(), menuItem.getQuantity(), priceFormat(menuItem.itemPrice()) , menuItem.getDescription()));
    System.out.println();
}

public long getTotalPrice() {
    long totalPrice = cart.stream()
            .mapToLong(MenuItem::itemPrice)
            .sum();

    System.out.println("[Total]");
    String priceFormat = priceFormat(totalPrice);
    System.out.println("₩ " + priceFormat);
    return totalPrice;
}
  • 기존에는 showCart()에서 장바구니의 내역과 전체 금액을 보여주는 로직이 분리 되지 않고 한 번에 작성되어 있어서 마음에 안들었는데 구조를 개편하고 요구사항을 충족하기 위해서는 분리가 필요해보여서 분리했다.
  • showCart()는 단순히 장바구니의 내역을 Stream을 활용하여 반복해서 값을 꺼내 출력해주고, 이 때 가격은 포맷팅해서 보여준다
  • getTotalPrice()도 마찬가지로 스트림을 통해 장바구니의 MenuItem들을 순회하여 itemPrice() 메서드를 호출하여 각 MenuItem의 단가 * 수량의 가격을 꺼내서 sum() 최종 연산자로 반환한다
    • 이 때 단일 함수를 사용하고 메서드 참조로도 가독성이 나쁘지 않아서 람다식을 메서드 참조로 활용했다.
    • 람다식으로 표현한다면 (menuItem -> menuItem.itemPrice())로 작성했을 것이다.
  • 반환된 장바구니의 최종 합계 금액을 포맷팅하여 화면에 출력하고, 메서드의 반환값으로는 포맷팅 하기전의 long 타입의 최종 금액을 반환하는데, 이 값을 가지고 이후 할인 정책에 따라 할인을 적용할 것이다.

getDiscountPrice()

public String getDiscountPrice(long totalPrice, int discountNumber) {

    DiscountPolicy[] values = DiscountPolicy.values();
    DiscountPolicy discountPolicy = Arrays.stream(values)
            .filter(policy -> policy.getInput() == discountNumber)
            .findFirst().get();

    long discount = discountPolicy.discount(totalPrice);
    return priceFormat(totalPrice - discount);
}
  • DiscountPolicy.values()를 활용하여 Enum을 배열로 변환한 후 Arrays.stream()을 활용하여 배열을 스트림으로 변환하여 filter 조건에 맞는 할인 정책을 반환하도록 했다.
    • 원래 자주 사용하던 코드라면 향상된 반복문이나 forEach()를 사용했을테지만 도전 LV2는 가급적 Stream을 사용하는 것을 권장하는 같아서 Stream을 사용했다
  • 여기서 Optional()로 반환되는 객체를 get()으로 단순히 반환했는데 할인 정책을 찾지 못해서 예외가 터질 가능성이 없으므로 이렇게 코드를 작성했다.
  • 반환된 할인 정책의 discount(totalPrice)를 호출하여 정책에 적용된 할인 금액을 반환하고 최종적으로 장바구니의 전체 금액에 할인금액을 빼서 포맷팅하여 반환한다.

deleteOneCartList()

public void deleteOneCartList(String cancelMenuItemName) {
    int index = IntStream.range(0, cart.size())
            .filter(i -> cart.get(i).getName().equalsIgnoreCase(cancelMenuItemName))
            .findFirst()
            .getAsInt();

    System.out.println(cart.get(index).getName() + " 가 장바구니에서 제거 되었습니다.");
    cart.remove(index);
}
  • 장바구니의 특정 메뉴를 제거하는 메서드이다.
  • IntStream.range(0, cart.size())로 스트림의 반복을 0부터 cart.size() -1까지 순회하도록 하고 그 이후 .filter를 통해 cart.get(i)로 조회한 장바구니의 MenuItem의 상품명과 매개변수로 삭제할 상품명을 대소문자 구분 없이 비교하도록 했다. 
    • 사용자의 입력값을 대소문자 구분 없이 검증한 이유는 사용자의 편의를 위함이다..
    • 사실 사용자의 입력값을 int로 받아서 입력받은 int값을 index로 활용하여 바로 장바구니의 데이터를 O(1)의 시간복잡도로 지울 수 있지만 스트림을 활용해야한다는 요구사항때문에 매개변수로 상품명을 입력받도록 구성했다.
  • filter 하나라도 검증이 완료되면 getAsInt()로 스트림의 반복 횟수값을 반환받아서 이것을 index로 활용하여 장바구니의 상품을 remove(index)를 사용하여 O(1)의 삭제 성능을 보이도록 작성했다.

Kiosk 클래스 

  • 필드의 구조는 동일하므로 생략하고 변경되는 메서드만 작성했다

start()

public void start() {
    Scanner scanner = new Scanner(System.in);
    int menusSize = menus.size();       // 등록한 메뉴의 개수를 변수화 (자주 사용함)

    while (true) {
        categoryPrinter(menus);                         // 메인 메뉴 -> 메뉴의 대분류(카테고리) 출력

        // flag == true : 주문 로직 활성화
        if (flag) {
            orderMenuPrinter();                         // 주문 메뉴 출력
        }

        int mainInput = inputValidator(scanner);        // 입력값 검증 -> 메뉴 선택 or flag가 true일 때 주문 및 취소 선택
        if (mainInput == 0) {
            System.out.println("프로그램을 종료합니다.");
            break;
        }

        if (flag && mainInput > menusSize + 2|| !flag && mainInput > menusSize){
            System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
            continue;
        }

        if (mainInput == menusSize + 1) {                   // mainInput == 4 -> 주문 진행
            long totalPrice = checkOrder(shoppingCart);     // 주문 내역 확인 및 전체 금액 반환

            int orderInput = inputValidator(scanner);       // 입력값 검증 -> 주문, 메뉴판 이동 선택
            if (orderInput == 1) {                              // lastSelect == 1 -> 최종 주문
                discountWithOrder(scanner, totalPrice);         // 할인 적용 및 최종 주문 로직
                continue;
            } else if (orderInput == 2) {                       // lastSelect == 2 -> 메뉴판 이동
                System.out.println("메뉴로 돌아갑니다");
                continue;
            } else {
                System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
                continue;
            }
        }

        if (mainInput == menusSize + 2) {                 // mainInput == 5 -> 주문 취소 진행
            System.out.println("1. 전체 취소\t 2. 부분 취소");
            int cancelInput = inputValidator(scanner);    // 입력값 검증(재사용) -> 취소 메뉴

            if (cancelInput == 1) {     // 전체 취소
                allCancel();
                continue;
            } else if (cancelInput == 2) {  // 부분 취소
                partCancel(scanner);        // 대,소문자 구분하지 않음
                continue;
            }
        }

        Menu menu = selectMainMenu(mainInput);
        while (true) {
            menuPrinter(menu);                          // 카테고리 하위 메뉴 출력
            int menuInput = inputValidator(scanner);    // 입력값 검증(재사용) -> 상세 메뉴 입력

            if (menuInput == 0) {
                System.out.println("처음으로 이동합니다.");
                break;
            }

            if (menu.findAllMenuItem().size() < menuInput) {
                System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
                continue;
            }

            MenuItem menuItem = selectMenuItem(menuInput, menu);    // 선택된 메뉴를 출력하고 반환

            cartMenuPrinter(menuItem);                  // 장바구니 관련 메뉴 출력
            int cartAddInput = inputValidator(scanner); // 입력값 검증 -> 장바구니 담기, 취소 선택
            if (cartAddInput == 1) {
                flag = true;
                System.out.println("몇 개 주문 하시겠습니까? ");
                int quantity = inputValidator(scanner); // 주문 수량
                if (quantity == 0) {
                    System.out.println("장바구니에 담을 수 없습니다.");
                    flag = false;
                    continue;
                }
                shoppingCart.addCart(menuItem, quantity);
            } else if (cartAddInput == 2) {
                System.out.println("취소 하셨습니다. 이전으로 돌아갑니다.");
                continue;
            } else {
                System.out.println("잘못된 메뉴를 선택하였습니다 다시 입력해 주세요.");
                continue;
            }
            break;
        }
    }
}
  • 전제적인 실행 흐름은 도전 LV1과 동일한데 일부 기능을 메서드화 시켰고, 장바구니 취소 관련 로직과 할인 정책을 적용한 주문 관련 로직이 추가 되었다
  • 장바구니에 물건을 담은 후 활성화된 주문 로직으로 진입하면 checkOrder()메서드를 통해 주문 내역과 전체금액을 먼저 반환하고 그 이후 주문을 하게 되면 disWithCountOrder()메서드를 통해 할인 정책을 선택하고 최종 주문이 완료 된다.
  • 주문을 취소하는 로직도 특정 장바구니의 상품을 제외하는 로직이 추가 되었는데 해당 로직을 allCancel(), partCencel()로 메서드화 하며 메서드 이름으로 구분지을 수 있도록 그나마 가독성을 챙겨주었다.

checkOrder(), discountWithOrder(), discountMenuPrinter() - 주문 관련 로직 메서드

private long checkOrder(ShoppingCart shoppingCart) {
    System.out.println("아래와 같이 주문 하시겠습니까?");
    System.out.println();
    shoppingCart.showCart();
    long totalPrice = shoppingCart.getTotalPrice();

    System.out.println();
    System.out.println("1. 확인\t 2. 메뉴판");
    return totalPrice;
}

private void discountWithOrder(Scanner scanner, long totalPrice) {
    discountMenuPrinter();
    int discountInput = inputValidator(scanner);    // 입력값 검증 -> 할인 정책 적용

    if (discountInput == 0) {
        System.out.println("잘못된 할인 정책을 입력하셨습니다. 다시 주문을 확인해 주세요");
        return;
    }

    String discountPriceFormat = shoppingCart.getDiscountPrice(totalPrice, discountInput);
    System.out.println("주문이 완료 되었습니다. 금액은 ₩ " + discountPriceFormat + " 입니다");
    shoppingCart.deleteAllCart();                   // 주문이 완료되면 장바구니 초기화
    flag = false;                                   // flag = false 설정
}

private void discountMenuPrinter() {
    System.out.println("할인 메뉴를 선택해 주세요.");
    Arrays.stream(DiscountPolicy.values())
            .forEach(policy ->
                    System.out.printf("%d. %-6s : %2d%%%n", policy.getInput(), policy.getDescription(), policy.getDiscountRate()));
}
  • checkOrder()
    • 기존에 order() 메서드라는 이름으로 존재했던 메서드이며 기능은 동일하다
    • 차이점은 반환값을 포맷팅된 장바구니의 총 금액이 아니라 long 타입으로 반환하여 이후에 해당 값을 받아서 수정할 수 있다
  • discountWithOrder()
    • 주문이 확정되면 discountMenuPrinter()를 호출하여 할인 메뉴를 보여주고 사용자가 할인 정책을 선택하면 shoppingCart.getDiscountPrice(totalPrice, discountInput)을 장바구니의 총 금액과 사용자가 선택한 할인 정책 번호를 인자로 넘겨주면서 호출한다
    • 반환값으로 할인 금액이 적용된 최종 주문 금액을 받아서 출력해주고 장바구니 초기화 및 flag = false로 설정하여 키오스크의 초기 메뉴로 돌아간다.
  • discountMenuPrinter()
    • 사용자가 할인 정책을 선택할 수 있도록 할인 메뉴를 출력해주며, 나름 포맷을 맞춰보기 위해 printf문을 사용했다.
    • Stream연습을 위해 강제로 Stream()을 사용해 보았으며 일반적로는 향상된 for문을사용했을 것 같다.

allCancel(), partCancel() - 주문 취소(장바구니 목록 제거) 관련 메서드들

 

private void allCancel() {
    System.out.println("장바구니가 비워졌습니다.");
    System.out.println();
    shoppingCart.deleteAllCart();
    flag = false;           // flag == false: 주문 메뉴 비활성화
}

private void partCancel(Scanner scanner) {
    System.out.println("장바구니에서 삭제할 상품 이름을 입력해 주세요.");
    shoppingCart.showCart();
    System.out.print("삭제할 상품명:");

    String cancelMenuInput = scanner.nextLine();
    shoppingCart.deleteOneCartList(cancelMenuInput);
}
  • allCancel()
    • 도전 LV1에서 start()로직에 존재했던 주문 취소 로직을 부분 취소가 생김으로 인해 메서드화 했다.
    • 로직은 동일하며 shoopingCart.deleteAllCart()를 호출하여 장바구니를 초기화 시키고 flag = false로 세팅하며 처음으로 돌아간다
  • partCancel()
    • 장바구니의 특정 메뉴를 삭제하는 메서드로 사용자가 장바구니에서 삭제할 대상을 문자열로 입력하면 입력된 값을 shoppingCart.deleteOneCartList(cancelMenuInput)으로 인자로 넘기면서 호출한다.
    • 이때 deleteOneCartList()메서드 내부에서는 사용자 편의를 위해 대소문자를 구분하지 않는다.
    • 요구사항을 충족해보기 위해 deleteOneCartList()에서 강제로 스트림을 사용하도록 구현하여 문자열을 입력 받았지만 원래 구조대로하면 숫자로 입력 받아서 이 값을 index로 활용하여 장바구니의 리스트를 삭제하도록 구현할 것 같다.

실행 결과

더보기
  • 주문이 활성화 되고 주문 로직에 진입한 후 확인을 누르게 되면 할인 정책을 선택할 수 있고, 할인 정책을 선택하면 할인된 금액이 출력되면서 주문이 완료된다
  • 주문이 완료되면 키오스크가 처음으로 돌아간다
  • 여러가지 할인 정책이 적용되는 모습을 확인할 수 있따.

 

  • 주문 로직이 활성화 된 상태에서 주문 취소 로직으로 들어가면 장바구니를 전체를 비울지, 부분적으로 비울지 선택할 수 있다.
  • 이때 부분 취소로 진입하면 삭제할 장바구니의 메뉴명을 문자열로 입력하면 해당 아이템만 장바구니에서 지워지고 처음으로 돌아간다.
  • 삭제하고 주문창으로 들어가서 장바구니의 내역을 확인해보면 장바구니에서 해당 아이템이 삭제된 것을 확인할 수 있다.

도전 LV2 개발 완료 회고

  • 이것으로 개발 과제는 최종적으로 완료하여 제출할 예정이며 이후에 받은 피드백을 적용하여 고칠 부분이 있다면 리펙토링 후기를 남길 예정이다.
  • 생각보다 키오스크가 예외처리할 부분도 많고 단일 반복문에서 프로그램의 흐름을 깔끔하게 통제하는 것이 쉽지 않았다.
  • 3일만에 구현할 생각으로 하지 않고 좀더 여유있게 한다면 프로그램 구조를 더 생각하고 별도의 클래스 등을 만들어서 키오스크와 장바구니에 역할이 과중되어있는 부분을 분리하는 구조로 시도해볼 수 있을 것 같다.
  • 입력값을 검증하고 반환하는 로직을 try - catch를 활용하여 예외를 정상흐름으로 바꾸는 로직을 구성했기 때문에 누군가는 이펙티브 자바에서 나오는 예외처리와 어긋난다고 생각할 수도 있을 것 같다.
  • 이부분은 내가 아직 잘 모르는 부분이기도하고 더 잘짠 코드 더 모던한 자바 코드에 대해서 공부를 해야할 것 같다.
728x90