키오스크 코드를 리팩토링하는 과정을 기록한다.
프로그램은 언제나 확장될 수 있다.
따라서, 코드는 확장에 용이해야 한다.
확장할 때 부가적인 작업이 많아질 수록 확장에 용이하지 않은 코드라고 볼 수 있다.
내 코드에서는 switch 문이 그러했다.
case 별로 메뉴를 선택하도록 설계했는데,
메뉴를 추가하게 되었을 때 새로운 case를 추가해줘야하고,
주문 번호와 취소 번호를 추가된 메뉴만큼 증가시켜줘야 했다.
예를 들어 메뉴의 갯수가 4 -> 5로 변경되었다면
기존에는
1번 버거
2번 치킨
3번 디저트
4번 맥주
5번 주문
6번 취소
였을 때
5번에 새로운 메뉴를 추가하고
5번과 6번은 각각 +1씩 해줘야 한다.
이는 확장에 용이하지 않다고 판단했다.
어떻게 이 문제를 해결할까 고민해봤다.
case 문을 for문으로 처리해서 메뉴의 크기만큼 돌도록하고
해당 인덱스를 case 로 처리해주려 했지만 불가능했다.
고민하다가 HashMap이 떠올랐다.
key 값을 Integer로 설정하고, Value를 ProductMenu로 설정한다면
메뉴가 증가되거나, 축소되었을 떄 HashMap만 수정해주면 되었다.
코드로 보자면 이전 코드는 아래와 같다.
while (true) {
// 메인 메뉴리스트를 출력합니다.
// 메인 메뉴가 변경될 때마다 main.MainMenu 클래스를 변경되지 않도록 클래스를 분리하여 결합도를 낮춥니다.
MainMenuList mainMenuList = new MainMenuList();
mainMenuList.mainMenuList(menuName);
// 메인 메뉴판 초이스 버거, 디저트, 음료, 치킨 중...
int choiceMenu = in.nextInt();
// 화면 전환 효과를 위해 공백을 입력합니다.
T.clearScreen();
// 상세 메뉴를 추상화한 Product
// Product 객체를 통해 case 문마다 객체를 갈아끼워주지 않아도 됨.
Product product;
switch (choiceMenu) {
case 1:
product = new Product(new Buger());
KioskApp.start(product,choiceDetailMenu, choiceProduct, orderOrCancel, addProductCnt, orderList);
break;
case 2:
product = new Product(new Buger());
KioskApp.start(product,choiceDetailMenu, choiceProduct, orderOrCancel, addProductCnt, orderList);
break;
case 3:
product = new Product(new Buger());
KioskApp.start(product,choiceDetailMenu, choiceProduct, orderOrCancel, addProductCnt, orderList);
break;
case 4:
product = new Product(new Chicken());
System.out.println("롯데리아에 오신걸 환영합니다.");
System.out.println("아래 상품메뉴판을 보시고 상품을 골라 입력해주세요.");
System.out.println();
product.detailMenu();
choiceDetailMenu = in.nextInt();
T.clearScreen();
choiceProduct = product.choiceProduct(choiceDetailMenu);
orderOrCancel = in.nextInt();
T.clearScreen();
if (orderOrCancel == 1) {
addProductCnt++;
orderList.addProduct(choiceProduct, addProductCnt);
} // case chicken end
break;
case 5: // 최종 주문
// 장바구니에 담긴 상품 목록 출력
orderList.orderProductList();
//최종적으로 주문할지 취소할지 결정
System.out.println();
System.out.println("1. 주문 2. 메뉴판");
int order = in.nextInt();
T.clearScreen();
if (order == 1) {
// addProductCnt = 0 주문과 함께 장바구니를 초기화함.
// 주문 했는데, 장바구니를 비우지 않으면, 다음 이용자에게 전 이용자가 사용한 장바구니 기록이 남게되어서
// 예상치 못한 결과가 발생함.
addProductCnt = 0;
orderList.clearOrder();
long delay = 3000;
totalOrderCnt++;
System.out.println();
System.out.println("주문이 완료되었습니다!");
System.out.println();
System.out.println("대기번호는 [ " + totalOrderCnt + " ] 번 입니다.");
System.out.println("(" + delay / 1000 + "초 후 메뉴판으로 돌아갑니다.)");
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000);
System.out.print(i + 1 + " ");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T.clearScreen();
}
break; // case 5 end
case 6: // 주문 취소
System.out.println("진행하던 주문을 취소하시겠습니까?");
System.out.println("1. 확인 2. 취소");
// 진행하던 주문 확인(1) 또는 취소(2)
int orderCancel = in.nextInt();
if (orderCancel == 1) {
// 장바구니 비우기
orderList.clearOrder();
// 진행하던 주문 취소 시 장바구니를 비워야 하므로 addProductCnt = 0
addProductCnt = 0;
System.out.println("진행하던 주문이 취소되었습니다.");
}
System.out.println();
System.out.println();
break;
default:
System.out.println("존재하지 않는 메뉴입니다.");
System.out.println();
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
}
위 코드에서 아래와 같은 코드는 반복되는 코드를 축소시킨 형태이다. 이 부분에대해서도 설명할 것이다.
product = new Product(new Buger());
KioskApp.start(product,choiceDetailMenu, choiceProduct, orderOrCancel, addProductCnt, orderList);
break;
어쨌든, 위와 같이 case 문으로 메뉴를 선택하도록 설계되어있었는데
HashMap을 사용하면 아래와 같이 변모한다.
Map<Integer, Product> menuMap = new HashMap<>();
menuMap.put(1, new Product(new Buger()));
menuMap.put(2, new Product(new Dessert()));
menuMap.put(3, new Product(new Drinks()));
menuMap.put(4, new Product(new Chicken()));
menuMap.put(5, new Product(new Bear()));
while (true) {
// 메인 메뉴리스트를 출력합니다.
// 메인 메뉴가 변경될 때마다 MainMenu 클래스를 변경하지 않도록 클래스를 분리하여 결합도를 낮춥니다.
MainMenuList mainMenuList = new MainMenuList();
mainMenuList.mainMenuList(menuName);
// 메인 메뉴판 선택 버거, 디저트, 음료, 치킨 중...
int choiceMenu = in.nextInt();
// 화면 전환 효과를 위해 공백을 입력합니다.
clearScreen();
// 상세 메뉴를 추상화한 Product
// Product 객체를 통해 객체를 갈아끼워주기만 하면 됨.
Product product = menuMap.get(choiceMenu);
if (product != null) { // 상품 선택
addProductCnt = KioskApp.start(product, choiceDetailMenu, choiceProduct, orderOrCancel, addProductCnt, orderList);
} else if (choiceMenu == menuMap.size() + 1) { // 주문 Order
// 장바구니에 담긴 상품 목록 출력
orderList.orderProductList();
//최종적으로 주문할지 취소할지 결정
System.out.println();
System.out.println("1. 주문 2. 메뉴판");
int order = in.nextInt();
clearScreen();
if (order == 1) {
// addProductCnt = 0, 주문과 함께 장바구니를 초기화함.
// 주문 했는데, 장바구니를 비우지 않으면, 다음 이용자에게 전 이용자가 사용한 장바구니 기록이 남게되어서 예상치 못한 결과가 발생
addProductCnt = 0;
orderList.clearOrder();
long delay = 3000;
totalOrderCnt++;
System.out.println();
System.out.println("주문이 완료되었습니다!");
System.out.println();
System.out.println("대기번호는 [ " + totalOrderCnt + " ] 번 입니다.");
System.out.println("(" + delay / 1000 + "초 후 메뉴판으로 돌아갑니다.)");
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000);
System.out.print(i + 1 + " ");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
clearScreen();
}
} else if (choiceMenu == menuMap.size() + 2) { // 주문 취소 Cancel
System.out.println("진행하던 주문을 취소하시겠습니까?");
System.out.println("1. 확인 2. 취소");
// 진행하던 주문 확인(1) 또는 취소(2)
int orderCancel = in.nextInt();
if (orderCancel == 1) {
// 장바구니 비우기
orderList.clearOrder();
// 진행하던 주문 취소 시 장바구니를 비워야 하므로 addProductCnt = 0
addProductCnt = 0;
System.out.println("진행하던 주문이 취소되었습니다.");
}
System.out.println();
System.out.println();
} else {
System.out.println("존재하지 않는 메뉴입니다.");
System.out.println();
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
이로써 case 문을 일일히 선언하지 않아도 된다.
위 코드에서 보면
if (product != null) { // 상품 선택
addProductCnt = KioskApp.start(product, choiceDetailMenu, choiceProduct, orderOrCancel, addProductCnt, orderList);
}
이 부분이 정말 핵심 포인트다.
개인적으로 case 문에서 KioskApp.start() 메서드가 반복되는걸 보다가
위처럼 단 한번만 선언되는걸 보면 기분이 너~무 좋다.
이 이전의 코드는 ... case 별로 공통되는 로직을 반복해서 처리했으니
위와 같이 선언하게 된게 어찌보면 정말 엄청난 리팩토링이라고 스스로 자부한다.
kioskApp.start() 메서드는 계산기 예외처리 과제를 풀다가 아이디어를 얻었다.
App클래스를 따로 만들어서 Main 클래스의 비중을 낮출 수 있다.
또한, 반복되는 코드를 최!소!화! 시키는데 한몫한다.
하나. KioskApp 클래스를 만들어 MainMenu에서 반복되는 로직을 KioskApp.start() 메서드로 처리했다.
둘. case 문을 없애고, HashMap을 사용하여 확장 가능한 설계를 함과 동시에 중복되는 코드를 더더더더 제거했다.
만약 메뉴가 백개라면 이 효과는 더욱 빛을 바랄 것이다.
case 문을 백개를 만들어야 하는데,
if (product != null) { // 상품 선택
addProductCnt = KioskApp.start(product, choiceDetailMenu, choiceProduct, orderOrCancel, addProductCnt, orderList);
}
위 로직 하나만 선언하면 된다니! 와우
ArrayList를 사용할 때 중복되는 상품이 장바구니에 담기면 화면에 한번만 출력되도록 설정해주려 할 때 contains를 쓰려했다.
오늘 Java 강의를 들으면서 Collections 강의를 보고 있었는데 아이디어가 떠올라서 바로 적용해봤다.
잘 작동되는 듯 했으나, 음료 메뉴에서 문제가 있었다.
알고보니 콜라와 제로 콜라 두 메뉴에서 콜라라는 이름이 중복되기 때문에 발생하는 문제였다.
그래서 contains 대신 == 키워드를 사용함으로써 해결했다.
상품 메뉴 선택 화면에서 다른 상품을 선택하면 IndexOutOfBounds가 발생하는 문제를 해결했다.
이건 tryCatch문을 사용해서 아래와 같이 선언해줬다.
while (flag) {
try {
choiceProduct = product.choiceProduct(choiceDetailMenu);
flag = false;
} catch (IndexOutOfBoundsException e) {
System.out.println("****존재하지 않는 상품입니다. 다시 선택해주세요****");
System.out.println();
product.detailMenu();
choiceDetailMenu = in.nextInt();
clearScreen();
}
}
처음에는 choiceDetailMenu를 건들여야할지, mainMenu 클래스에서 직접 처리해줘야할지 고민이 많았는데, 계속 고민하다 보니 번뜩 아이디어가 떠올라서 바로 적용해봤는데 해결되었다.
그리고 Kisok.start() 메서드를 생성하면서 전역으로 설정된 addProductCnt 값이 변경되지 않는 문제도 있었는데,
Kiosk.start() 반환타입을 int로 두고 addProductCnt를 return 해줌으로써 해결하였다.
addProductCnt = KioskApp.start(product, choiceDetailMenu, choiceProduct, orderOrCancel, addProductCnt, orderList);
기능을 추가하는 것도 재밌지만 프로젝트를 리팩토링하면서
견고한 프로젝트가 되어갈 때 더 큰 재미를 느끼는 것 같다.
뭔가 깊이 있는 소프트웨어 개발자가 되는 느낌?
디테일을 놓치지 않는 개발자가 되는 느낌?
그런게 너무 좋다.
앞으로도 단순히 기능 개발만을 목적으로 하기보다 견고하고 유지보수하기 좋은 코드를 만들고 싶다.
메뉴 증가할 때마다 case 문 일일히 추가 해주는걸 메서드화 시키는건 참 신기하네요!
피곤해서 내일 자세히 보도록하겠습니다! 감사합니다! 깊이있는 개발자 성수님!!