[Java Project_키오스크 과제] 도전 기능 구현 및 트러블 슈팅
개요
CH 2 키오스크 과제 도전 기능을 구현한 방법에 대해서 작성해보겠습니다.
📌 필수 기능 TIL
2025.01.15 - [Dev Projects] - [Java Project_키오스크 과제] 필수 기능 구현 및 단계별 설계
[Java Project_키오스크 과제] 필수 기능 구현 및 단계별 설계
개요CH 2 키오스크 과제를 진행하며 level별로 구현한 방법에 대해서 작성해보겠습니다 🤓 📌 키오스크 과제 필수 기능 가이드더보기클래스 정의Main : 시작 지점이 되는 클래스, public static void ma
mannakingdom.tistory.com
📌 키오스크 과제 도전 기능 가이드
### Lv 1. 장바구니 및 구매하기 기능을 추가하기
- **요구사항이 가지는 의도**
- **의도**: 클래스 간 연계를 통해 객체 지향 프로그래밍의 기본적인 설계를 익히고, 사용자 입력에 따른 프로그램 흐름 제어와 상태 관리를 학습
- **목표**
- 클래스 간 역할 분리를 이해하고, 적절히 협력하는 객체를 설계
- 프로그램 상태 변경 및 데이터 저장을 연습
- 사용자 입력에 따른 예외 처리와 조건 분기를 연습
- **장바구니 생성 및 관리 기능**
- 사용자가 선택한 메뉴를 장바구니에 추가할 수 있는 기능을 제공합니다.
- 장바구니는 메뉴명, 수량, 가격 정보를 저장하며, 항목을 동적으로 추가 및 조회할 수 있어야 합니다.
- 사용자가 잘못된 선택을 했을 경우 예외를 처리합니다(예: 유효하지 않은 메뉴 번호 입력)
- **장바구니 출력 및 금액 계산**
- 사용자가 결제를 시도하기 전에, 장바구니에 담긴 모든 메뉴와 총 금액을 출력합니다.
- 출력 예시
- 각 메뉴의 이름, 가격, 수량
- 총 금액 합계
- **장바구니 담기 기능**
- 메뉴를 클릭하면 장바구니에 추가할 지 물어보고, 입력값에 따라 “추가”, “취소” 처리합니다.
- 장바구니에 담은 목록을 출력합니다.
- **주문 기능**
- 장바구니에 담긴 모든 항목을 출력합니다.
- 합산하여 총 금액을 계산하고, “주문하기”를 누르면 장바구니를 초기화합니다.
[ MAIN MENU ]
1. Burgers
2. Drinks
3. Desserts
0. 종료 | 종료
1 <- // 1을 입력
[ BURGERS MENU ]
1. ShackBurger | W 6.9 | 토마토, 양상추, 쉑소스가 토핑된 치즈버거
2. SmokeShack | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거
3. Cheeseburger | W 6.9 | 포테이토 번과 비프패티, 치즈가 토핑된 치즈버거
4. Hamburger | W 5.4 | 비프패티를 기반으로 야채가 들어간 기본버거
0. 뒤로가기
2 <- // 2를 입력
선택한 메뉴: SmokeShack | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거
// 2번을 누르면 나오는 메뉴입니다.
"SmokeShack | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거"
위 메뉴를 장바구니에 추가하시겠습니까?
1. 확인 2. 취소
1 <-
// 1번을 누르면 나오는 메뉴입니다.
SmokeShack 이 장바구니에 추가되었습니다.
// 장바구니에 물건이 들어 있으면 아래와 같이 [ ORDER MENU ] 가 추가로 출력됩니다.
// 만약에 장바구니에 물건이 들어 있지 않다면 [ ORDER MENU ] 가 출력되지 않습니다.
// 미출력일 때 4,5 번을 누르면 예외를 던저줘야 합니다.
아래 메뉴판을 보시고 메뉴를 골라 입력해주세요.
[ MAIN MENU ]
1. Burgers
2. Drinks
3. Desserts
0. 종료 | 종료
[ ORDER MENU ]
4. Orders | 장바구니를 확인 후 주문합니다.
5. Cancel | 진행중인 주문을 취소합니다.
4 <- // 4를 입력
// 4번을 누르면 나오는 메뉴입니다.
아래와 같이 주문 하시겠습니까?
[ Orders ]
SmokeShack | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거
[ Total ]
W 8.9
1. 주문 2. 메뉴판
1 <-
// 1번을 누르면 나오는 메뉴입니다.
주문이 완료되었습니다. 금액은 W 8.9 입니다.
### Lv 2. Enum, 람다 & 스트림을 활용한 주문 및 장바구니 관리
- **요구사항이 가지는 의도**
- **의도** : 고급 자바 기능을 활용해 프로그램의 효율성과 코드의 가독성을 개선하는 것을 목표로 합니다.
- **목적**
- Enum을 통해 상수를 안전하게 관리하고, 프로그램 구조를 간결하게
- 제네릭을 활용하여 데이터 유연성을 높이고, 재사용 가능한 코드를 설계
- 스트림 API를 사용하여 데이터를 필터링하고, 간결한 코드로 동작을 구현
- **Enum을 활용한 사용자 유형별 할인율 관리하기**
- 사용자 유형의 Enum 정의 및 각 사용자 유형에 따른 할인율 적용
- 예시 : 군인, 학생, 일반인
- 주문 시, 사용자 유형에 맞는 할인율 적용해 총 금액 계산
- **람다 & 스트림을 활용한 장바구니 조회 기능**
- 기존에 생성한 Menu의 MenuItem을 조회 할 때 스트림을 사용하여 출력하도록 수정
- 기존 장바구니에서 특정 메뉴 빼기 기능을 통한 스트림 활용
- 예시 : 장바구니에 SmokeShack 가 들어 있다면, stream.filter를 활용하여 특정 메뉴 이름을 가진 메뉴 장바구니에서 제거
- 구조 예시
[ MAIN MENU ]
1. Burgers
2. Drinks
3. Desserts
0. 종료 | 종료
1 <- // 1을 입력
[ BURGERS MENU ]
1. ShackBurger | W 6.9 | 토마토, 양상추, 쉑소스가 토핑된 치즈버거
2. SmokeShack | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거
3. Cheeseburger | W 6.9 | 포테이토 번과 비프패티, 치즈가 토핑된 치즈버거
4. Hamburger | W 5.4 | 비프패티를 기반으로 야채가 들어간 기본버거
0. 뒤로가기
2 <- // 2를 입력
선택한 메뉴: SmokeShack | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거
// 2번을 누르면 나오는 메뉴입니다.
"SmokeShack | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거"
위 메뉴를 장바구니에 추가하시겠습니까?
1. 확인 2. 취소
1 <-
// 1번을 누르면 나오는 메뉴입니다.
SmokeShack 이 장바구니에 추가되었습니다.
// 장바구니에 물건이 들어 있으면 아래와 같이 [ ORDER MENU ] 가 추가로 출력됩니다.
// 만약에 장바구니에 물건이 들어 있지 않다면 [ ORDER MENU ] 가 출력되지 않습니다.
// 미출력일 때 4,5 번을 누르면 예외를 던저줘야 합니다.
아래 메뉴판을 보시고 메뉴를 골라 입력해주세요.
[ MAIN MENU ]
1. Burgers
2. Drinks
3. Desserts
0. 종료 | 종료
[ ORDER MENU ]
4. Orders | 장바구니를 확인 후 주문합니다.
5. Cancel | 진행중인 주문을 취소합니다.
4 <- // 4를 입력
// 4번을 누르면 나오는 메뉴입니다.
아래와 같이 주문 하시겠습니까?
[ Orders ]
SmokeShack | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거
[ Total ]
W 8.9
1. 주문 2. 메뉴판
1 <-
// 1번을 누르면 할인 정보를 제공해줍니다.
할인 정보를 입력해주세요.
1. 국가유공자 : 10%
2. 군인 : 5%
3. 학생 : 3%
4. 일반 : 0%
4 <-
// 4번을 누르면 나오는 메뉴입니다.
주문이 완료되었습니다. 금액은 W 8.9 입니다.
🍔 Lv 6. 장바구니 및 구매하기 기능을 추가하기
이번 단계에서는 키오스크 프로그램에 장바구니 기능을 추가하고, 이를 통해 주문, 취소 등의 작업을 처리하도록 구현했습니다. 이 과정에서 Java 컬렉션(`HashMap`)의 활용을 집중적 학습했습니다.
✨ 주요 변경 사항
1. Cart 클래스 추가
Cart 클래스는 장바구니를 관리하는 역할의 클래스입니다.
- 필드
- `HashMap<MenuItem, Integer> cartItemMap`: 메뉴 아이템과 수량을 저장
- 주요 메서드
- `addCartItem(MenuItem item)`: 장바구니에 메뉴 추가 (중복 키일 경우 수량 업데이트)
- `printCartItemsWithTotal()`: 장바구니에 담긴 메뉴와 총 금액 출력
- `cancelOrder()`: 장바구니 비우기
- `placeOrder()`: 장바구니 주문 완료 및 초기화
2. Kiosk 클래스에 장바구니 관련 로직 추가
Kiosk 클래스에 장바구니 기능을 통합하여 프로그램 흐름 제어를 합니다.
- 필드 추가
- `Cart cart`: 사용자 장바구니 항목 저장
- 추가 메서드
- `startAddToCart(MenuItem item)`: 특정 메뉴를 장바구니에 추가하는 작업 처리
- `startPlaceOrder()`: 장바구니 내용을 확인하고 주문을 완료하는 기능
- `startCancelOrder()`: 진행 중인 주문을 취소하고 장바구니를 초기화
🛒 Cart 클래스의 HashMap을 사용한 장바구니 구현
사용자가 선택한 메뉴를 저장하기 위해 장바구니 클래스 Cart를 생성해 사용했습니다.
Cart 클래스의 필드로 `HashMap<MenuItem, Integer>`를 사용했습니다.
🔍 HashMap을 선택한 이유
HashMap은 키-값(key-value) 구조로 데이터를 저장합니다. 키를 통해 값에 빠르게 접근할 수 있으며, 이러한 특성이 장바구니 기능을 구현할 때 유용하다고 생각했습니다.
- 키 중복을 허용하지 않음
- HashMap은 같은 키가 입력되면 기존 값(value)를 대체하는 특징이 있습니다.
- 장바구니에서 같은 메뉴를 여러번 추가할 때 수량만 갱신할 때 활용하기 좋았습니다.
- 자료형 설정
- HashMap은 키와 값의 자료형을 설정할 수 있습니다.
- 이번 구현에서는 다음과 같이 설정했습니다.
- Key: `MenuItem` 객체 (메뉴)
- Value: `Integer` (메뉴 수량)
🛠️ HashMap의 Object 타입 Key
HashMap은 내부적으로 해시 테이블을 사용하여 데이터를 저장합니다.
- `hashCode` 메서드: 키의 해시코드 값을 기반으로 저장 위치를 계산
- `equals` 메서드: 해시코드 충돌이 발생할 경우, 키를 비교하여 값의 동일성을 확인
HashMap에서의 동일성은 `hashCode`와 `equals` 메서드로 결정됩니다.
➡️ Object 타입을 키로 사용할 때, `hashCode`와 `equals`를 오버라이드하지 않으면 기본 구현이 사용됩니다.
- 기본 구현:
Object 클래스의 `hashCode`는 메모리 주소를 기반으로 값을 계산하고, `equals`는 참조 비교를 수행 - 문제점:
동일한 데이터여도 다른 객체로 생성되면 서로 다른 해시코드와 참조를 가지기 때문에,
HashMap에서는 이를 서로 다른 키로 인식하여 동일성이 보장되지 않는다.
📍 Cart 클래스의 필드 `cartItemMap`
private final HashMap<MenuItem, Integer> cartItemMap;
위 필드에서는 키의 동일성을 판단해야하기 때문에, MenuItem 클래스에서 `hashCode`와 `equals`의 오버라이드가 필요했습니다.
📍 MenuItem 클래스에서 `hashCode`와 `equals` 오버라이드
MenuItem 클래스의 필드 값(이름(MenuName), 가격(MenuPrice), 설명(MenuDescription))이 동일한 객체를 같은 키로 간주하기 위해서 `hashCode`와 `equals` 메서드를 오버라이드 해줬습니다.
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
MenuItem menuItem = (MenuItem) o;
return Double.compare(menuPrice, menuItem.menuPrice) == 0 && Objects.equals(menuName, menuItem.menuName) && Objects.equals(menuDescription, menuItem.menuDescription);
}
@Override
public int hashCode() {
return Objects.hash(menuName, menuPrice, menuDescription);
}
(IntelliJ Generate를 활용했습니다.)
제가 구현한 Level 6 코드에서는
- Main 클래스의 `main` 메서드에서 MenuItem 객체를 생성하고,
- MenuItem의 List를 Menu의 객체로,
- Menu의 List를 Kiosk 클래스의 필드로 사용합니다.
위의 코드 처럼 `main` 메서드에서 객체를 생성한 이후로는 새로운 MenuItem 객체를 생성하는 일이 없습니다.
즉, MenuItem 객체의 상태 변경 가능성이 없습니다.
따라서, 동일한 필드 값을 가진 서로 다른(해시코드값과 참조값이 다른) MenuItem 객체도 사용될 일도 없기 때문에, 객체의 해시코드나 참조가 변할 가능성은 없는 것입니다! 😅
그래도! HashMap에서 일반적인 Object 키를 사용할 때 적용하는 원칙을 따르기 위해 `hashCode`와 `equals` 메서드를 오버라이드하여 객체의 동일성을 정확히 판단할 수 있도록 구현했습니다.
참고:
https://www.java67.com/2013/06/how-get-method-of-hashmap-or-hashtable-works-internally.html
https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode/
🧑💻 HashMap을 활용한 장바구니 코드
1. 장바구니에 항목 추가
HashMap의 `put` 메서드를 사용해 메뉴를 추가하거나 수량을 갱신했습니다.
public void addCartItem(MenuItem item) {
int itemQuantity = cartItemMap.getOrDefault(item, 0) + 1;
cartItemMap.put(item, itemQuantity);
}
- `getOrDefault(item, 0)`: item이 이미 존재하면, 현재 수량을 반환하고, 없으면 기본값 0을 반환합니다.
- `put(item, itemQuantity)`: 동일한 키가 있을 경우, 기존 값을 새 값(itemQuantity)으로 대체합니다.
2. 장바구니 토탈 금액 계산
각 메뉴의 수량과 가격을 곱해 총 금액을 계산합니다.
public double getTotalPrice() {
double totalPrice = 0;
for( MenuItem cartItem : cartItemMap.keySet() ){
totalPrice += (cartItem.getMenuPrice() * cartItemMap.get(cartItem));
}
return totalPrice;
}
3. 장바구니 항목과 토탈 금액 출력
저장된 모든 항목을 출력합니다.
public void printCartItemsWithTotal() {
StringBuilder sb = new StringBuilder();
sb.append("\n[ Orders ]\n");
for( MenuItem cartItem : cartItemMap.keySet() ){
sb.append("수량: ").append(cartItemMap.get(cartItem)).append("\t | ");
sb.append(MenuItem.formatMenuItem(cartItem));
sb.append("\n");
}
sb.append("\n[ Total ]\n");
sb.append("W ").append(getTotalPrice()).append("\n");
System.out.println(sb);
}
- keySet에 대해서 순회하면서 출력합니다.
🤖 Kiosk 클래스의 메뉴 주문과 취소
1. 장바구니에 메뉴 추가하기
private void startAddToCart(MenuItem item) {
final int ADD_TO_CART_OPTION = 1; // 장바구니에 추가
final int CANCEL_OPTION = 2; // 취소
System.out.println("\n\"" + MenuItem.formatMenuItem(item) + "\"");
System.out.println("위 메뉴를 장바구니에 추가하시겠습니까?");
System.out.println("1. 확인\t2. 취소");
Scanner sc = new Scanner(System.in);
String addToCartChoice = sc.next();
try {
int addToCartChoiceNumber = Integer.parseInt(addToCartChoice);
if (addToCartChoiceNumber == ADD_TO_CART_OPTION) {
cart.addCartItem(item);
System.out.println("\n"+ item.getMenuName() + " 이 장바구니에 추가되었습니다.");
System.out.println("\n아래 메뉴판을 보시고 메뉴를 골라 입력해주세요.");
}
else if (addToCartChoiceNumber == CANCEL_OPTION) {
System.out.println("\n취소되었습니다.");
System.out.println("\n아래 메뉴판을 보시고 메뉴를 골라 입력해주세요.");
}
else {
throw new InvalidMenuSelectionException(ADD_TO_CART_OPTION, CANCEL_OPTION);
}
} catch (NumberFormatException e) {
System.out.println("\"" + addToCartChoice + "\"은/는 숫자가 아닙니다.");
} catch (InvalidMenuSelectionException e) {
System.out.println(e.getMessage());
} catch (Exception e) {
System.out.println("알 수 없는 오류가 발생했습니다. : " + e.getMessage());
}
}
- 사용자 입력값을 검증한 뒤 메뉴를 장바구니에 추가하거나 취소합니다.
- 잘못된 입력값에 대해서 예외를 발생시킵니다.
2. 주문하기
private void startPlaceOrder() {
final int PLACE_ORDER_OPTION = 1; // 주문하기
final int RETURN_TO_MENU_OPTION = 2; // 메뉴판으로 돌아가기
System.out.println("\n아래와 같이 주문하시겠습니까?");
cart.printCartItemsWithTotal();
System.out.println("1. 주문\t2. 메뉴판");
Scanner sc = new Scanner(System.in);
String placeOrderChoice = sc.next();
try {
int placeOrderChoiceNumber = Integer.parseInt(placeOrderChoice);
if (placeOrderChoiceNumber == PLACE_ORDER_OPTION) {
cart.placeOrder();
}
else if (placeOrderChoiceNumber == RETURN_TO_MENU_OPTION) {
System.out.println("\n메뉴판으로 돌아갑니다.");
System.out.println("\n아래 메뉴판을 보시고 메뉴를 골라 입력해주세요.");
}
else {
throw new InvalidMenuSelectionException(PLACE_ORDER_OPTION, RETURN_TO_MENU_OPTION);
}
} catch (NumberFormatException e) {
System.out.println("\"" + placeOrderChoice + "\"은/는 숫자가 아닙니다.");
} catch (InvalidMenuSelectionException e) {
System.out.println(e.getMessage());
} catch (Exception e) {
System.out.println("알 수 없는 오류가 발생했습니다. : " + e.getMessage());
}
}
- 장바구니에 담긴 메뉴와 총 금액을 확인한 뒤 주문을 완료합니다.
🧾 Lv 7. Enum, 람다 & 스트림을 활용한 주문 및 장바구니 관리
Lv 7에서는 키오스크 프로그램에 고급 자바 문법을 적용하여 코드의 가독성, 유지보수성, 그리고 효율성을 향상시키는 것을 목표로 했습니다.
Enum과 람다 & 스트림을 활용해 기존 기능을 개선하고 품절 메뉴 처리 및 사용자 할인 기능을 추가했습니다.
💫 주요 변경사항
Lv 6에서 구현한 장바구니 및 주문 기능을 개선하고, 다음과 같은 기능을 추가했습니다.
1. 사용자 유형에 따른 할인
- `UserType` Enum 클래스를 생성하여 사용자 유형을 정의하고, 각 유형별 할인율을 설정했습니다.
- 주문 시 사용자 유형을 입력받아 총 금액에서 할인율을 적용합니다.
2. 품절 메뉴 처리
- MenuItem 클래스에 `isSoldOut` 필드를 추가해 메뉴의 품절 여부를 관리하도록 했습니다.
- 품절 상황을 가정(품절 상품 임의 지정)하여 키오스크가 동작하도록 했습니다.
- 품절 메뉴는 장바구니에 담을 수 없으며, 주문 전에 품절 메뉴가 장바구니에서 삭제됩니다.
3. 람다 & 스트림을 활용한 코드 간소화
- 메뉴 출력, 장바구니 필터링 등 반복문으로 구성돼있던 로직을 스트림으로 변경해 가독성을 향상시켰습니다.
🧮 Enum을 활용한 사용자 할인 처리
👀 UserType Enum
`UserType` Enum은 사용자 유형과 관련된 정보를 정의한 클래스입니다.
public enum UserType {
NATIONAL_MERIT(1, "국가유공자", 0.1),
MILITARY(2, "군인", 0.05),
STUDENT(3, "학생", 0.03),
GENERAL(4, "일반", 0.0);
private final int discountMenuNumber;
private final String discountMenuName;
private final double discountRate;
// 생성자 생략
// Getter 생략
}
🛠️ UserType의 static 메서드
1. 할인 정보 출력
모든 사용자 유형과 할인율을 출력하여 사용자가 할인 정보를 고를 때 참고할 수 있도록 합니다.
public static void printDiscountInformation() {
StringBuilder sb = new StringBuilder();
for (UserType type : UserType.values()) {
String discountInfoString = String.format(
"%d. %-5s\t: %d%%\n",
type.discountMenuNumber,
type.discountMenuName,
(int) Math.round(type.discountRate * 100)
);
sb.append(discountInfoString);
}
System.out.print(sb);
}
2. 사용자 선택에 따른 할인율 반환
public static UserType selectedUserType(int inputNumber) throws InvalidMenuSelectionException {
for (UserType type : UserType.values()) {
if (type.discountMenuNumber == inputNumber) {
return type;
}
}
throw new InvalidMenuSelectionException(NATIONAL_MERIT.discountMenuNumber, GENERAL.discountMenuNumber);
}
- 사용자에게 입력 받은 할인 정보에 따라 UserType을 반환하고, 유효하지 않은 입력은 예외 처리합니다.
🧑💻 UserType을 활용한 주문하기 코드
1. Kiosk 클래스 `startApplyDiscount` 메서드
private void startApplyDiscount() {
Scanner sc = new Scanner(System.in);
boolean isValidInput = false;
System.out.println("\n할인 정보를 입력해주세요.");
UserType.printDiscountInformation();
while (!isValidInput) {
String userTypeChoice = sc.next();
try {
int userTypeChoiceNumber = Integer.parseInt(userTypeChoice);
UserType userType = UserType.selectedUserType(userTypeChoiceNumber);
isValidInput = true;
cart.placeOrder(userType);
} catch (NumberFormatException e) {
System.out.println("\"" + userTypeChoice + "\"은/는 숫자가 아닙니다.\n메뉴를 다시 입력하세요.");
} catch (InvalidMenuSelectionException e) {
System.out.println(e.getMessage());
} catch (Exception e) {
System.out.println("알 수 없는 오류가 발생했습니다. : " + e.getMessage());
}
}
}
- UserType의 `selectedUserType` 메서드로 사용자 입력을 검증합니다.
- 유효한 타입이라면 Cart 클래스의 `placeOrder` 메서드에 userType을 매개변수로 전달하여 호출합니다.
2. Cart 클래스 `placeOrder` 메서드
public void placeOrder(UserType userType) {
StringBuilder sb = new StringBuilder("\n주문이 완료되었습니다. ");
if (userType.getDiscountRate() > 0) {
String orderString = String.format(
"\"%s\" 할인이 적용되어, 금액은 W %.2f 입니다.",
userType.getDiscountMenuName(),
calculateTotalPrice(userType.getDiscountRate())
);
sb.append(orderString);
}
else {
String orderString = String.format(
"금액은 W %.2f 입니다.",
calculateTotalPrice()
);
sb.append(orderString);
}
System.out.println(sb);
System.out.println("**************************************************************");
cartItemMap.clear();
}
- 사용자가 선택한 userType의 할인율에 맞게 토탈 금액을 계산하여 출력하고, 주문을 완료한다.
3. Cart 클래스 `calculateTotalPrice` 메서드 오버로딩
Lv 6에서 구현한 `calculateTotalPrice`메서드에, 매개변수로 할인율을 전달하면 할인율을 적용하여 토탈 금액이 반환되도록 오버로딩 했습니다.
public double calculateTotalPrice() {
double totalPrice = 0;
for( MenuItem cartItem : cartItemMap.keySet() ){
totalPrice += (cartItem.getMenuPrice() * cartItemMap.get(cartItem));
}
return totalPrice;
}
public double calculateTotalPrice(double discountRate) {
double originalTotalPrice = calculateTotalPrice();
return originalTotalPrice * (1 - discountRate);
}
😎 람다 & 스트림 활용
🖨️ 메뉴 출력
출력에 사용하던 기존 반복문을 스트림으로 변환했습니다.
변경 전: 반복문 사용
for (int i = 0; i < menuItems.size(); i++) {
String menuItemString = String.format(
"%d. %s",
i + 1,
MenuItem.formatMenuItem(menuItems.get(i))
);
System.out.println(menuItemString);
}
변경 후: 스트림 사용
menuItems.stream()
.map(item -> String.format(
"%d. %s%s",
menuItems.indexOf(item) + 1,
MenuItem.formatMenuItem(item),
item.isSoldOut() ? " (품절)" : ""
))
.forEach(System.out::println);
- 메뉴의 품절 여부(`isSoldOut`)를 조건에 맞게 표시되도록 했습니다.
⛑️ 장바구니 품절 메뉴 처리
장바구니에 담아둔 메뉴가 품절되었다고 가정하고 품절 메뉴는 장바구니에서 삭제하는 로직을 만들었습니다.
private void startPlaceOrder() {
// "기존 장바구니에서 특정 메뉴 빼기 기능을 통한 스트림 활용"을 위해
// 장바구니에 담아둔 상품(SmokeShack)이 품절된 경우 가정
menuList.get(0).getMenuItem(1).setSoldOut(true);
cart.removeSoldOutItems();
if (cart.getCartItemMap().isEmpty()) {
System.out.println("\n장바구니가 비었습니다.");
System.out.println("\n아래 메뉴판을 보시고 메뉴를 골라 입력해주세요.");
return;
}
// 주문 로직 생략
}
장바구니에서 item 삭제 로직을 작성하던 중 ConcurrentModificationException이 발생했습니다.
🤯 트러블 슈팅: ConcurrentModificationException 발생 해결 과정
📌 문제 상황
Cart 클래스의 `removeSoldOutItems` 메서드에서 스트림을 사용해 품절된 상품을 필터링하고, 이를 `forEach`로 장바구니에서 제거하려 했습니다.
그러나 아래 코드 실행 시 java.util.ConcurrentModificationException이 발생했습니다.
문제 발생 코드:
public void removeCartItem(MenuItem item) {
cartItemMap.remove(item);
}
public void removeSoldOutItems() {
cartItemMap.keySet().stream()
.filter(MenuItem::isSoldOut)
.forEach(this::removeCartItem);
}
오류 메세지:
📋 문제 원인
ConcurrentModificationException:
- Java 컬렉션(HashMap, ArrayList 등)을 반복하면서 동시에 수정하려고 할 때 발생하는 예외
- `cartItemMap.keySet().stream()`으로 키 집합을 처리하면서 `forEach`에서 컬렉션에
직접적인 수정 작업(`cartItemMap::remove`)을 수행했기 때문에 예외가 발생
➡️ 스트림(stream())은 컬렉션의 요소를 반복적으로 처리할 수 있지만,
스트림 내부에서 컬렉션 변경 작업은 수행할 수 없다!
💡 문제 해결
수정한 코드는 다음과 같습니다.
public void removeSoldOutItems() {
List<MenuItem> soldOutItems = cartItemMap.keySet().stream()
.filter(MenuItem::isSoldOut)
.toList();
System.out.println();
soldOutItems.forEach(item -> System.out.println("\"" + item.getMenuName() + "\"은 품절되어 장바구니에서 삭제합니다."));
soldOutItems.forEach(cartItemMap::remove);
}
- 스트림에서 품절된 상품만 필터링한 후, 별도의 리스트(`soldOutItems`)로 저장한 뒤 반복문으로 제거
(스트림 내부에서 컬렉션 수정 금지‼️)
📝 `stream().forEach()`와 `forEach()`의 차이점
구분 | stream().forEach() | forEach() |
동작 방식 | 스트림을 통해 필터링, 매핑 등이 적용된 요소 처리 | 컬렉션 자체의 모든 요수를 단순 반복 처리 |
컬렉션 수정 가능 여부 |
수정 불가! (ConcurrentModificationException 발생) |
가능 |
적용 대상 | 스트림 | 컬렉션(List, Set, Map) |
🤓 배운 점
- 스트림의 안전한 사용: 스트림 내부에서는 원본 컬렉션을 수정하지 않기
- 리스트 활용: 컬렉션의 수정 작업이 필요한 경우, 먼저 필터링된 결과를 별도의 리스트로 변환해 처리하기
- 스트림과 컬렉션의 동작 차이 알기
이 과정을 통해 스트림을 더 안전하고 효과적으로 사용하는 방법을 학습했습니다. 😊
참고:
https://green-bin.tistory.com/111
마무리
🎈 키오스크 과제를 통해 배운 점
1. 객체지향 설계의 중요성
- Level 1부터 Level 7까지 단계별로 객체지향 설계를 점진적으로 추가하며 구현했습니다.
- 클래스를 역할별로 나누고, 각 클래스에 적합한 메서드를 설계하면서 코드의 가독성과 재사용성이 향상됨을 체감했습니다.
- 특히, `Kiosk`, `Cart`, `Menu`, `MenuItem` 등 객체 간의 역할 분담이 명확해지면서 유지보수성이 향상되었습니다.
2. 고급 Java 기능 활용
- Enum을 활용해 사용자 유형과 할인율을 관리하며, Enum의 실제 활용 사례와 장점을 이해하게 되었습니다.
- Stream을 활용하며 데이터를 필터링하고 처리하는 방법을 학습했습니다.
- 특히, Stream 내부에서 컬렉션을 수정할 때 발생하는 ConcurrentModificationException 문제를 직접 경험하며,
스트림과 컬렉션을 안전하게 다루는 방법을 익혔습니다.
- 특히, Stream 내부에서 컬렉션을 수정할 때 발생하는 ConcurrentModificationException 문제를 직접 경험하며,
3. 예외 처리
- 이전 계산기 과제에서는 커스텀 예외를 사용하지 않았지만, 이번 과제에서는 커스텀 예외를 사용해봤습니다.
- `InvalidMenuSelectionException`을 활용하여 사용자 입력 오류를 명확하고 구조적으로 처리할 수 있었습니다.
- 커스텀 예외를 통해 예외 처리가 직관적으로 설계되었습니다.
🎈 아쉬운 점
과제 요구사항 중 하나였던 "기존 장바구니에서 특정 메뉴 빼기 기능을 통한 스트림 활용"을 구현하기 위해, 메뉴의 품절 상황을 가정하여 설계했습니다.
이 과정에서 품절 메뉴를 직접 수동으로 설정한 점이 아쉽습니다.
관리자용 키오스크 실행을 추가해서 실시간으로 품절 상태를 설정/해제할 수 있도록 구현해보고 싶습니다!
https://github.com/mannaKim/java-kiosk-project
GitHub - mannaKim/java-kiosk-project
Contribute to mannaKim/java-kiosk-project development by creating an account on GitHub.
github.com