이번 과제에서는 키오스크 프로그램을 구현하는 것이 목표다.
요구사항은 다음과 같다.
Scanner
클래스를 사용해 사용자 입력 처리MenuItem
클래스 생성MenuItem
객체 생성을 통해 이름
, 가격
, 설명
을 저장menuItems
리스트를 탐색하며 반복적으로 출력Kiosk
클래스 생성main
함수에서 관리하던 입력 및 반복문 로직을 start
함수에서 관리main
함수에서 Kiosk
객체를 생성할 때 값을 할당뒤로가기
및 종료
기능 구현MenuItem
클래스를 관리하는 Menu
클래스 생성menuItem
을 포함하는 카테고리 이름
필드 추가Cart
클래스 생성 (기존 요구사항엔 없음)Input
클래스 생성 (기존 요구사항엔 없음)Enum
을 활용해 사용자 유형별 할인율 관리이번 과제는 레벨별로 구현해야 하는 클래스가 명확히 지정되어 있다. 필수 과제부터 도전 과제까지 진행하면서 점진적으로 리팩토링을 수행하며 객체지향적인 사고에 익숙해지도록 하는 의도가 보인다. 다행히 이번에는 기능 구현 과정에서 큰 기술적 문제는 없었다. 그래서 주로 리팩토링한 내용을 정리해보려고 한다.
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
파라미터를 활용하면 메뉴 확장되더라도 유연한 검증이 가능하다.
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()
메서드로 기능을 분리했다.
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;
}
기존 코드는 사용자 유형이 추가되면 Enum
과 getDiscountPrice()
메서드까지 수정해야 했지만, 수정된 코드는 Enum
만 수정하면 되도록 개선했다.
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
를 통해 필터링 조건을 받아
새로운 리스트로 변환하는 방식의 로직으로 구현해봤다.
이전 과제(계산기)에서는 기능 구현에만 집중해서 리팩토링할 부분이 많았다. 하지만 이번에는 처음부터 각 객체와 메서드의 역할을 고민하며 개발할 수 있었다.
리팩토링할 내용이 많지 않다는 점이 오히려 내가 발전했다는 증거 아닐까?