Lv.6 키오스크를 구현하다가 메인 메뉴와 메뉴 아이템 로직을 여러 메서드로 분리하는 작업을 했다. 요구사항에 있던 내용은 아니지만, 점점 깊어지는 depth가 매우 거슬리고 가독성도 안좋은 것 같아 사실상 갈아엎었다..🫠
이렇게 길고 긴 start()
메서드를 좀 더 간결하게 수정했다. if문이 점점 많아져서 내가 코드를 짜는건지 코드가 나를 짜는건지 내가 짰는데도 헷갈리는 상황이 와서 일단 분리해봤다.
public void start() {
try (Scanner sc = new Scanner(System.in)) {
while (true) {
// 메뉴 출력
showMenu();
System.out.print("메인 메뉴를 선택하세요: ");
if (!sc.hasNextInt()) {
System.out.println("숫자를 입력해주세요.");
sc.next();
continue;
}
// 메뉴 입력 받기
int selectedMenu = sc.nextInt();
if (selectedMenu == 0) {
System.out.println("프로그램을 종료합니다.");
break;
}
if (isValidMenu(selectedMenu)) {
// 메뉴 아이템 출력
showMenuItems(selectedMenu);
// 메뉴 아이템 입력 받기
System.out.print("메뉴를 선택하세요: ");
int selectedMenuItem = sc.nextInt();
// 0 입력 시 뒤로가기(메인 메뉴)
if (selectedMenuItem == 0) continue;
// 선택한 메뉴 아이템 출력
if (isValidMenuItem(selectedMenu, selectedMenuItem)) {
displaySelectedMenuItems(selectedMenu, selectedMenuItem);
} else {
System.out.println("올바른 메뉴 아이템 번호를 입력해주세요.");
}
} else { // 메뉴에 없는 값을 입력할 경우
System.out.println("올바른 메뉴 번호를 입력해주세요.");
}
System.out.println();
}
}
}
변경한 start()
메서드는 이렇다. 물론 아직 장바구니 로직을 다 짜지 못해서 미완성인 상태이다. 장바구니 로직까지 분리해서 잘 넣으면 깔끔해지지 않을까 싶다.
public void start() {
while (true) {
kioskInit();
if (selectedMenu == 0) {
System.out.println("프로그램을 종료합니다.");
return;
}
// 장바구니 추가 질문
int cartSelection = getUserInput(sc, "위 메뉴를 장바구니에 추가하시겠습니까?\n1. 확인\t2. 취소\n");
if (cartSelection == 1) {
// 장바구니 추가
cart.add(menus.get(selectedMenu-1), selectedMenuItem);
cart.showAddedItem();
cart.showAllCartItems();
} else {
// 취소
System.out.println("장바구니 추가 취소");
}
}
}
kioskInit()
메서드 안에는 메뉴와 메뉴 아이템(Burger Menu, Drinks Menu 등)을 세팅하는 메서드가 있다. 그리고 settingMenu()
메서드와 settingMenuItem()
를 이용해 메뉴 리스트를 출력하고, 해당 값을 입력받는다.
private void kioskInit() {
settingMenu();
if (selectedMenu == 0) return;
settingMenuItem();
}
private void settingMenuItem() {
showMenuItems(selectedMenu);
getMenuItem();
displaySelectedMenuItems(selectedMenu, selectedMenuItem);
}
private void getMenuItem() {
while (true) {
try {
selectedMenuItem = getUserInput(sc, "메뉴를 선택하세요: ");
if (selectedMenuItem == 0) return;
else if (!isValidMenuItem(selectedMenu, selectedMenuItem)) {
System.out.println("올바른 메뉴 아이템 번호를 입력해주세요.");
return;
}
break;
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}
private void settingMenu() {
showMenu();
getMenu();
}
private void getMenu() {
while (true) {
try {
selectedMenu = getUserInput(sc, "메인 메뉴를 선택하세요: ");
if (selectedMenu == 0) return;
if (!isValidMenu(selectedMenu)) {
System.out.println("올바른 메뉴 번호를 입력해주세요.");
continue;
}
break;
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}
public class Cart {
private final Map<MenuItem, Integer> items = new HashMap<>();
private int index;
public void add(Menu menu, int index) {
MenuItem item = menu.getMenuItems().get(index - 1);
items.put(item, items.getOrDefault(item, 0) + 1);
}
public List<MenuItem> getItems() {
return new ArrayList<>(items.keySet());
}
public void showAddedItem() {
for (MenuItem item : items.keySet()) {
System.out.println(item.getName() + "이(가) 장바구니에 추가되었습니다.");
}
}
public void showAllCartItems() {
for (MenuItem item : items.keySet()) {
System.out.printf("%s | W %.1f | %s ---- %d개%n",
item.getName(),
item.getPrice(),
item.getDescription(),
items.get(item));
}
}
public void clearCart() {
items.clear();
}
}
배열에서 데이터를 검색할 때는 하나하나 비교해야하기 때문에 O(n)의 시간이 걸리고, 배열의 장점인 인덱스를 사용할 수 없다. 왜냐하면 인덱스와 데이터의 값이 서로 다르기 때문이다. 인덱스로 검색을 하려면 다음 코드와 같이 데이터 값을 인덱스 번호로 사용하면 된다. 이렇게 하면 인덱스로 바로 데이터를 찾을 수 있어 O(1)의 시간이 걸린다.
Integer[] inputArray = new Integer[10];
inputArray[1] = 1;
inputArray[2] = 2;
inputArray[5] = 8;
inputArray[8] = 8;
위의 배열을 출력하면 이렇게 나올 것이다. 찾는 것은 바로 찾을 수 있겠지만, 수많은 null
값들이 매우 거슬린다.🫤 만약 99라는 값이 들어간다면, 배열은 입력 값의 범위 만큼 큰 배열을 사용해야 한다. 따라서 낭비되는 공간이 많이 발생한다.
inputArray = [null, 1, 2, null, null, 5, null, null, 8, null]
이런 문제는 나머지 연산을 통해 해결할 수 있다. 배열의 크기에 맞추어 나머지 연산을 한 결과는 다음과 같다. 이렇게 하면 0 ~ 9 범위로 인덱스 값이 나온다. 이렇게 배열의 인덱스로 사용할 수 있도록 원래의 값을 계산한 인덱스를 해시 인덱스(hashIndex)라 한다.
다음과 같이 나머지 연산을 하는 hashIndex()
메서드와 해시 인덱스를 이용해 값을 추가하는 add()
메서드를 구현했다. 넣을 때 hashIndex()
메서드를 통해 나온 값에 데이터를 넣고, 검색을 할 때도 검색하고자 하는 값을 hashIndex()
메서드에 넣어 해시 인덱스를 구하면 된다.
private static void add(Integer[] inputArray, int value) {
int hashIndex = hashIndex(value);
inputArray[hashIndex] = value;
}
static int hashIndex(int value) {
return value % CAPACITY;
}
하지만 여기에도 한계가 있다..🫠 바로 해시 충돌이 날 수 있다는 것이다. 만약 9와 99를 입력한다면 둘 다 10으로 나눈 나머지가 9이기 때문에 덮어씌워져 마지막에 저장한 값만 남게 된다.
이 문제를 해결하기 위해 배열의 크기를 늘리는 방법도 있겠지만 위에서 보았듯이 메모리 낭비가 심해질 것이다. 따라서 해시 충돌이 낮을 확률로 일어날 수 있다고 인정하고 같은 해시 인덱스의 값을 같은 인덱스에 저장하면 문제를 해결할 수 있다. 배열 안에 또 다른 배열(혹은 리스트)를 만들어 같은 해시 인덱스 값을 가진 데이터를 저장한다. 다음 코드는 LinkedList를 사용해 해시 충돌이 일어날 경우를 대비한 코드이다.
private static void add(LinkedList<Integer>[] buckets, int value) {
int hashIndex = hashIndex(value);
LinkedList<Integer> bucket = buckets[hashIndex]; // O(1)
if (!bucket.contains(value)) { // 중복 체크 - O(n)
bucket.add(value);
}
}
private static boolean contains(LinkedList<Integer>[] buckets, int searchValue {
int hashIndex = hashIndex(searchValue);
LinkedList<Integer> bucket = buckets[hashIndex]; // O(1)
return bucket.contains(searchValue); // O(n)
}
static int hashIndex(int value) {
return value % CAPACITY;
}
데이터를 검색할 때는 LinkedList의 contains()
메서드를 이용해 모든 항목을 다 순회하면서 찾는 데이터가 있는지 확인한다. 따라서 O(n)의 성능이고, 해시 충돌이 발생하지 않아 데이터가 1개만 들어있는 경우에만 O(1)의 성능이다.
💡 정리
해시 인덱스를 사용할 때 최악의 경우는 거의 발생하지 않고, 대부분 O(1)의 빠른 성능을 제공한다.
lv.6 키오스크가 시간이 생각보다 많이 걸려서 주말까지 힘내서 해봐야겠다. 자바 스터디도 있어서 괜찮을까 싶지만..일단 해보자!