[내배캠/18일차] TIL - Lv.6 키오스크 시작, 해시 알고리즘

euphony·2025년 1월 17일
0

내일배움캠프

목록 보기
33/66

✅오늘의 한 일

  • Lv.6 키오스크 완성(진행중)
  • 트러블 슈팅 및 몰랐던 내용 정리
  • 자바 중급2편 섹션7 완료
  • 자바 중급2편 섹션6 ~ 7 정리 시작

💻오늘의 학습

Lv.6 키오스크 시작

메인 메뉴 로직 & 메뉴 아이템 로직 분리

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());
            }
        }
    }

Cart 클래스 구현

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();
    }
}

자바 중급2편 강의

해시 알고리즘

배열에서 데이터를 검색할 때는 하나하나 비교해야하기 때문에 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)라 한다.

  • 1 % 10 = 1
  • 2 % 10 = 2
  • 5 % 10 = 5
  • 8 % 10 = 8
  • 14 % 10 = 4
  • 99 % 10 = 9

다음과 같이 나머지 연산을 하는 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이기 때문에 덮어씌워져 마지막에 저장한 값만 남게 된다.

  • 99 % 10 = 9
  • 9 % 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)의 빠른 성능을 제공한다.

  • 데이터 저장
    • 평균: O(1)
    • 최악: O(n)
  • 데이터 조회
    • 평균: O(1)
    • 최악: O(n)

📝오늘의 회고

lv.6 키오스크가 시간이 생각보다 많이 걸려서 주말까지 힘내서 해봐야겠다. 자바 스터디도 있어서 괜찮을까 싶지만..일단 해보자!

📌내일의 할 일

  • Lv.6 키오스크 완성
  • 트러블 슈팅 및 몰랐던 내용 정리
  • 자바 중급2편 섹션6 ~ 7 정리 완료

0개의 댓글

관련 채용 정보