개발지식 먹는 하마 님의 블로그
[내일배움캠프 19일차] _ 키오스크 구현, 트러블 슈팅 본문
🖥️ 키오스크는 어떤 기능들이 있을까?
우리가 식당에 가서 주문을 받을 때 제공받는 키오스크의 주요 기능을 생각해 보자.
아래와 같이 정리해 볼 수 있을 것이다.
👀 사용자에게 메뉴 정보를 제공한다. |
👆 사용자는 원하는 메뉴를 선택한다. |
🛒 선택한 메뉴를 장바구니에 저장한다. |
📈 주문 확인 및 메뉴의 옵션, 수량을 조정 또는 삭제한다. |
💸 총금액에 대해 선택한 결제 수단으로 결제한다. |
🗂️ 기능에 따라 패키지 별로 구분하기
메뉴 정보에 관련된 클래스를 domain 패키지에, 키오스크 기능에 관련된 클래스를 util 패키지에 배치했다.
(Kiosk 내에 선언된 Menu를 Order 내에서 관리하고 싶었으나, 요구사항에 맞게 유지하기 위해서 따로 변경하지 않았다.)
✏️ 결제 기능 구현
🛒 장바구니 Cart
선택된 메뉴를 LinkedHashMap에 저장한다.
키는 MenuItem을, 값은 수량을 나타낸다.
✅ LinkedHashMap을 사용한 이유
- HashMap은 <키-값>의 형태로 데이터를 관리할 수 있다.
- MenuItem에 따른 수량을 관리하는 것이 용이하다.
- LinkedHashMap은 HashMap 또는 Set과는 다르게 순서를 보장한다.
- 문자열을 이용해서 특정 메뉴를 장바구니에서 제거하는 것이 비효율적이라고 생각했다.
(지금까지 정수 입력으로 처리하도록 했는데 갑자기 문자열? 굳이?) - 위치를 나타내는 인덱스를 받아서 이를 이용해 처리하고자 했다.
(그러려면 순서가 보장되어야 한다.) - Map을 사용하면 문자열을 사용하는 방법으로 변환하는 것도 간단해진다.
- 문자열을 이용해서 특정 메뉴를 장바구니에서 제거하는 것이 비효율적이라고 생각했다.
☑️ 장바구니에 메뉴 저장
람다식을 사용하여 구현하였다.
saveCart.compute(item, (key, value) -> (value == null) ? 1 : value + 1);
선택된 MenuItem item과 일치하는 Key가 LinkedHashMap에 존재한다면, Key에 해당하는 Value를 1 증가시킨다.
존재하지 않는다면, Key와 Value를 item, 1로 설정한다.
이 방법으로 장바구니에는 저장된 메뉴와 그에 따른 수량이 저장된다.
☑️ 장바구니 총 금액 계산
반복자 Iterator를 사용하여 LinkedHashMap을 탐색하였다.
Iterator<MenuItem> iterator = saveCart.keySet().iterator(); //반복자 생성
double totalPrice = 0.0;
for(int i = 0; i < saveCart.size(); i++){ //장바구니의 크기만큼 반복(while문 사용해도 무방)
MenuItem item = iterator.next(); //반복자에 따른 값을 가져옮
totalPrice += item.getPrice() * saveCart.get(item); //가격과 수량을 곱한 금액을 총합에 더함
}
return totalPrice;
iterator.next( )로 메뉴아이템을 순서대로 가져온다.
getter인 getPrice로 반환된 금액과 Key(item)에 따른 Value(수량)을 가져와 금액을 계산한다.
☑️ 장바구니에 저장된 메뉴 삭제
스트림을 사용하여 구현하였다.
MenuItem keyToRemove = saveCart.keySet() //장바구니의 모든 키를 순서대로 가져오기
.stream() //스트림으로 변환
.skip(index) //n개의 요소를 건너뜀
.findFirst() //n개 만큼 건너뛴 후 요소가 남아있다면 첫번째를 반환
.orElse(null); //값이 없을 경우 null 반환
이를 반복문으로 반환하면 아래와 같다.
for (MenuItem item : saveCart.keySet()) {
if (index == indexToDelete) { // 삭제할 인덱스에 도달했을 때
keyToRemove = item;
break;
}
index++;
}
saveCart에 저장된 key를 순서대로 가져와
입력받은 위치까지 이동한 후,
해당 위치의 key인 MenuItem을 가져오는 것이다.
if (keyToRemove != null) { //메뉴아이템이 반환된 경우
saveCart.remove(keyToRemove); //해당 아이템 삭제
System.out.println(keyToRemove.getName()+"가 삭제 되었습니다.\n");
calcTotalPrice(); //총 금액 다시 계산
}
찾은 MenuItem을 이용하여 LinkedHashMap에 저장된 해당 아이템을 삭제한다.
✏️ 키오스크 흐름 관리
☑️ 1단계
카테고리 목록을 출력한다.
장바구니에 저장된 메뉴가 있어 결제 가능한 상태라면 Orders 선택지를 출력한다.
☑️ 2단계
카테고리 선택 시, 카테고리 내의 메뉴를 출력한다.
Orders 선택 시, 저장된 메뉴 목록과 합계를 확인하고 선택지를 출력한다.
이 때, 상태를 결제 중으로 설정한다.
☑️ 3단계
결제 중이 아니라면, 선택된 메뉴를 출력한다.
결제 중이고 주문이 선택됐다면, 할인 정보를 출력한다.
삭제가 선택됐다면, 장바구니 목록을 출력한다.
이 때, 상태를 삭제 중으로 설정한다.
☑️ 4단계
결제 중이 아니라면, 메뉴를 장바구니에 추가한다.
결제 중이고 삭제 중이 아니라면, 할인이 적용된 금액에 대해 결제한다.
삭제 중이라면, 선택된 메뉴를 장바구니에서 삭제한다.
✅ 키오스크의 작업 상태를 나타내는 변수 사용
boolean 변수를 이용해, 키오스크의 상태에 따라 처리를 할 수 있도록 하였다.
boolean 변수 | 의미 |
isProcessing | 키오스크 사용을 시작했는가? |
isOrdering | 선택한 주문을 결제하는 중인가? |
isDeleting | 삭제하는 중인가? |
( 정수형 변수 하나를 선언하고 0 = 키오스크 사용 중, 1 = 결제 중, 2 = 삭제 중 이런 식으로 표현하는 것도 가능하다. )
⏳ 트러블 슈팅
키오스크에 이런저런 기능을 추가하니 흐름이 매우 복잡해졌다.
이로 인해 입출력 예외 발생 시 이전 단계로 돌아가기 등의 기능을 수행하는 과정에서 버그가 발생했다.
이를 해결하기 위해 시간을 많이 소요했다.
✅ 문제 발생 위치
🟦 예외 처리 시, 이전 단계로 돌아가기
유효한 입력을 판별하는 과정에서 choice가 유효한 입력이라면 해당 단계에 따른 과정을 수행한다.
그러나 유효하지 않은 입력이라면, 예외를 던진 후 이전 단계로 돌아가도록 한다.
문제는 예외 던지기 후, 이전 단계로 돌아가는 과정에서 발생했다.
✅ 문제 발생 원인
🟦 유효하지 않은 choice로 예외를 던질 시, 이전 상태를 저장한 변수가 제대로 반영되지 않음
예외 던지기를 했을 때, choice에 이전 단계에서 저장된 입력이 제대로 반영되지 않아
잘못된 값이 반복해서 들어가는 것이 원인이었다.
예시
예를 들어 카테고리가 버거로 선택된 후, 버거 카테고리 내에 있는 메뉴 아이템을 선택하는 단계를 진행 중이었다고 하자.
버거 카테고리에 저장된 메뉴의 개수는 4, 0은 뒤로 가기이다.
따라서, 입력 가능한 정수의 범위는 0부터 4까지이다.
따라서, 입력 가능한 정수의 범위는 0부터 4까지이다.
사용자가 -1을 입력한다면 유효하지 않기 때문에 예외가 던져진다.
예외를 던지면 이전 단계로 돌아간다.
이때 별도의 입력을 받거나 choice 변수를 특정 값으로 재설정하지 않는다.
따라서 유효하지 않은 입력이 반복해서 검증되게 되는 것이다.
✅ 문제 해결
🟦 choice에 이전 상태를 반영하는 과정 추가
유효한 값이 입력된 경우, 유효한 입력을 Kiosk 클래스 내의 전역 변수에 저장해 왔다.
예외 처리 시, 이를 choice에 반영하기 위한 단계를 추가하여 유효하지 않은 choice가 유지되지 않도록 하였다.
그 결과, 아래와 같은 흐름으로 choice 값이 변화한다.
🟦 Step 2 : 카테고리의 메뉴 출력
초기 실행 과정은 위에서 설명했던 예시와 똑같이 진행된다.
Step1에서 choice = 1 (버거)을 입력받았고 이것이 유효한 범위 내의 값이기 때문에 Step2가 실행된다.
Step2는 선택된 카테고리 내의 메뉴를 출력한다.
이때, 유효한 입력값 1은 버거에 전역 변수 Save에 저장된다.
🟦 Step 3을 위한 입력받기
Step 3을 위한 입력 -1을 받았다.
Step 3에 대한 유효 입력 범위는 0부터 4까지이기 때문에 -1은 예외로 던져지게 된다.
🟦 예외 처리 후 Choice 값의 변화
버거를 의미하는 1이 저장된 Save의 값을 choice에 반영한다.
또 실행 단계가 Step3에서 다시 Step2로 돌아가게 된다.
🟦 다시 돌아온 Step 2
Step 2에서 1은 유효한 값이기 때문에 다시 버거 카테고리 내의 메뉴가 출력된다.
✅ 해당 영역의 코드
//예외 발생으로 이전 단계 복귀 시, 이전 선택지 복원
public int setChoice(int choice){
if (isPaying) {
return choosePayProcess; //결제 진행 중이라면, 결제 단계 복원
}
switch (step){ //메뉴 선택 중이라면
case 2 : //step2는 카테고리 복원
return chooseCategory + 1; //chooseCategory = choice-1이기 때문에 1을 더함
case 3 : //step3는 메뉴아이템 복원
return chooseMenuItem + 1;
}
return choice;
}
catch (IndexOutOfBoundsException e) {
System.out.printf("해당 값은 선택할 수 없습니다.%n%n");
step--; //이전 단계로 복귀
choice = setChoice(choice); //이전 단계의 선택지 복원
}
💬 회고
Kiosk 클래스 내에서 모든 흐름을 제어하기 때문에 코드가 매우 길고 복잡한 상태였다.
리팩토링도 하지 않았었기 때문에 이 문제를 발견하기 어려웠다.
미리 순서도를 작성했다면, 문제를 파악하기 더 쉬웠을 것 같다.
🤷♂️ 왜 순서도를 미리 짜지 못했는가?
물론 프로젝트를 구현하기 전, 당연히 순서도와 클래스 다이어그램을 작성하고자 하였다.
그런데 이전 프로젝트보다 요구사항이 더 길게 작성되어 있었다.
레벨 별로 요구사항이 조금씩 달라지고, 세부 요구사항은 판단하기 애매하게 적혀있는 부분이 있었다.
(수량과 삭제 기능을 구현하라고 하지만 예시에는 그런 부분이 포함되어있지 않았다.)
lv3 요구사항 구현 시, 의도된 불편함을 느끼고 lv4에서 이를 해결하는 경험을 쌓을 수 있도록 작성된 요구사항도 있었다.
이런 이유로 모든 요구사항을 처음부터 체계적으로 정리하고 구현하는 것이 어려웠다.
📚 개선할 점
프로젝트 구현 자체에서 어려운 점은 크게 없었다.
다만 클래스 다이어그램을 작성할 때, 클래스들 간의 관계를 정의하는 부분에서 어려움을 느꼈다.
시험 준비를 통해 이론은 배웠는데 실제 사용하려니 두 클래스의 관계가 어떤 종류에 해당하는지 판별하는 게 어려웠다.
또한 메서드 간의 의존성을 낮추기 위해 노력했는데, 결합도나 응집도 같은 용어들이 생각나면서
이들이 실제로 어떻게 사용되는지 파악하고 다음 프로젝트 개발 시, 이를 고려하여 설계하고 싶다고 느꼈다.
'내일배움캠프 (CS25)' 카테고리의 다른 글
[내일배움캠프 24일차] _ Builder Pattern로 코드 품질 향상 + 가변 인자 (0) | 2025.03.21 |
---|---|
[내일배움캠프 23일차] _ 키오스크 프로젝트 피드백 (0) | 2025.03.20 |
[내일배움캠프 17일차] _ 거리두기 확인하기 문제 풀이 (0) | 2025.03.12 |
[내일배움캠프 16일차] _ Enum & 리팩토링 (0) | 2025.03.11 |
[내일배움캠프 15일차] _ lower_bound vs find (0) | 2025.03.10 |