백엔드 부트캠프/TIL

[내일배움캠프Spring-15일차] Kiosk Issue

sintory-04 2025. 3. 10. 21:29

    오늘은 내가 고민했던 부분들을 써내려 갈 것이다 !

    1. Main 의 역할

    1) 문제상황

    - add 한 부분을 Main 에 하는게 맞는지 궁금하다. 현재는 이러한 상태로 main 에다가 직접 객체를 삽입하고 있다. 문제상에서도이렇게 적혀있어서 넣긴 하는데 밖으로 빼는게 나을까?

    2) 문제 분석

    - 일단 addMain 에 해도 된다. 과제 수준에서는 이렇게 하는 것이 최선이다. 정 하고 싶다면 이걸 별도의 클래스에서 관리해도 되지만 과제에서 요구하는 바는 이 정도인 거 같았다.

    3) 문제 해결

    - 해당 부분은 일단 추후 마지막 도전과제를 모두 끝낸 후에 별도 클래스를 추가하는 걸 고려하기로 했다.

    2. 카테고리 객체 생성

    1) 문제상황

    Menu burger = new Menu();
    burger.addMenuItems(new MenuItem("Whopper",7100,"**"));

    - 현재는 카테고리 메뉴도 객체로 만들어져 있고, 메뉴아이템도 객체로 만들어져 있는 상태이다.

    - 다른 조원들은 이런식으로 하지 않고 직접 menuItems에 카테고리를 추가하는 형태로 진행한 거 같았다.

    - 내 생각에는 나중에 삽입하고 연결하는 객체 지향적으로 분석하면.. ? 이렇게 개별 객체로 두고 그 객체를 삽입하는 식으로 하는 게 나을 거 같았다. 하지만, 이것은 내 생각이다. 요구 사항에 적절한지 의문이 생겨서 해결하고자 했다.

    2) 문제 분석

    - 카테고리 객체를 만드는 것도 적절한 방법이라고 한다. 따로 튜터님이라면 어떻게 해결할 건지 여쭈어 보았다. 튜터님 같은 경우에도 카테고리 객체 생성하고, 메뉴아이템에 카테고리도 넣을 거 같다라고 하셨다.

    3) 문제 해결

    - 해당 부분은 마지막 도전과제를 다 하고 수정해보고자 한다. 카테고리만 메뉴아이템에 추가하는 방향으로 하고자 한다.

    3. Input Error 처리 방향

    1) 문제상황

    - input 도 별도의 클래스로 구분하고자 한다. 왜냐면 계속해서 반복하기 때문이다.

    - 그런데 꽤 복잡하다 생각했다. 애초에 에러가 많이 나는 Input Scanner 를 다른 클래스로 분리하면 실제 반복문에서는 해당 에러를 어떻게 처리할지 감이 안 잡혔다. 그렇게 되면 중복되는 에러처리이지 않을까? 라는 생각에 고민을 해보았다.

    2) 문제 해결

    - 일단은 해결 방법은 두가지이다.

    - 첫 번째는 에러가 뜰 때 input 클래스에서 boolean 값을 전해주는 것이다. 그래서 실제 반복문에서는 if 문을 통해서 정상일 때는 정상처리를 해주고, 아니면 오류를 던져주는 식으로 처리하는 방법

    - 두 번째는 지금처럼 try Catch 문이지만, 키오스크 코드에다가 한 종류의 에러만 출력되게끔 하는 것이다. 그러니까 input Error 로 나올 수 있는 Error 는 하나의 에러로 묶어서 처리하는 것이다.

    - 나는 두 번째 방식을 채택했다. 이게 이렇게 처리하다보니, scanner 의 고질적인 버퍼 문제 때문에 ! 모든 catch 문에 버퍼를 지워주는 로직을 세워야했다.

      public static int getInput(){
        try {
          return sc.nextInt();
        } catch (InputMismatchException e) {
          Output.printOutput("숫자를 입력하셔야 합니다.");
          Input.printNextLine();
          throw new RuntimeException();
        } catch (NullPointerException e){
          Output.printOutput("입력값이 비어있습니다.");
          Input.printNextLine();
          throw new RuntimeException();
        } catch (NumberFormatException e) {
          Output.printOutput("유요한 숫자가 아닙니다.");
          Input.printNextLine();
          throw new RuntimeException();
        }
      }

    4. Getter 활용

    1) 문제 상황

    // 번호를 받으면 menu.items Get
    List<MenuItem> itemList = menu.getMenuItems();
    System.out.printf("선택한 메뉴: %s | %s | %s \n",itemList.get(orderMenuItem-1).menuName,itemList.get(orderMenuItem-1).menuPrice,itemList.get(orderMenuItem-1).menuDescription);

    - 리스트를 변수에 저장한 후 리스트의 값을 출력해주는 코드다.

    - 근데 이걸 굳이 리스트로 받아오는 게 객체지향적인가? 라는 의문이 생겼다.

    2) 문제 해결

    menu.displaySelectedMenu(orderMenuItem-1);

    - 해당 부분을 Getter 함수(이름은 조금 더 직관적으로 수정했다.)로 활용하여 나타내었다. 확실히 Kiosk 클래스에서 한줄로 줄어들어 코드의 흐름이 한눈에 보였다.

    5. 장바구니 아이템과 장바구니 객체

    1) 문제 상황

    - 장바구니 라는 기능을 만들기 위해 두가지를 고민했다.

    - 첫 째, 냅다 리스트에 모든 값을 넣는 것. -> 이중 리스트 고려

    - 둘 째, 리스트와 리스트 안의 객체를 별도로 생각하여 각각의 클래스 생성할 것

    2) 문제 분석

    - 일단은, 리스트에 모든 값을 넣는 것을 상상해보자. 그렇다면 이중리스트로 구현해야 할 것이다. 혹은 HashMap으로 구현하는 것을 생각해보아야 한다. 그렇다면 과연 객체지향적인가? HashMap을 통해서 구현을 한다면 확실히 컬렉션에 대한 이해도를 챙길 수 있어서 좋을 거 같긴하다. 하지만 문제에서도 정의되어 있지 않았는가? 실제로 Set<String> keys = memberMap.keySet(); 을 이용해서 HashMapKey 와 장바구니 속 Key와 매치해도 괜찮을 거 같았다.

    - 이럴 때에는 목적과는 조금 다른 거 같다. 자료구조를 활용하여 기능을 구현하는 것도 개발자에게 중요한 역량이고, 객체 지향을 활용하여 구현하는 것도 개발자에게 중요한 역량이다. 이번의 과제의 의도, 즉 요구사항에서는 객체지향에 대해 공부하고 그것을 이해하는 부분에서의 의의가 크다. 그래서 이번에는 객체지향을 위해서 각자의 객체를 두고 그것을 연결하는 방법을 선택하고자 했다!

    - 두 개의 클래스로 나누는 게 좋은 이유에 대해서는 아래와 같다 !

    • 책임 분리 (SRP - 단일 책임 원칙)
      • Cart 클래스 → 장바구니 전체를 관리
      • CartItem 클래스 → 개별 상품의 정보 저장
    • 확장성과 유지보수성 향상
      • - CartItem 의 기능 확장 가능
      • - Cart는 여러 개의 CartItem을 관리하는 역할로 분리되므로 코드가 간결해짐 -> 객체 지향적인 설
    • CartItem은 장바구니에 들어가는 개별 상품을 표현하는 객체
    • Cart는 CartItem의 리스트를 관리하는 컨테이너 역할

    3) 문제 해결

    - 아래와 같이 분리해서 쓰는 걸로 해결하였다.

    6. 다중 if 문의 코드 복잡성

    1) 문제상황

    - if 문이 많아져서 코드 복잡성이 증가했다. 이게 다른 사람이 이 코드를 읽었을 때 좋은 코드인가? 아무리 생각해도 복잡해서 난해한 코드가 된거 같다.

    case 4 -> { // 로직이 너무 복잡한데 . .음..
                if(!cart.cartItemsNotEmpty()) {
                  throw new IndexOutOfBoundsException();
                }
                cart.showCartItems();
                // 이거 4번을 누르면 새로 주문하는 클래스가 필요할 거 같단말이지.
                Output.printOutput("1. 주문하기     | 2. 메뉴판으로 돌아가기");
                // switch 문으로 한 번 더 만들기 이중 switch 문...
                int orderConfirmation = Input.getInput();
                if (orderConfirmation==1) {
                  // switch
                  Output.printfStringOutput("주문이 완료되었습니다.","총 금액: "+Integer.toString(cart.getTotalPrice()));
                  isRunning = false;
                  continue;
                } else if ( orderConfirmation==2 ){
                  System.out.println("메뉴판으로 돌아갑니다.");
                  continue;
                } else {
                  throw new IndexOutOfBoundsException();
                }
              }

    - 해당 로직을 어떻게하면 난해하지 않는 코드로 바꾸지에 대해 고민을 해보았다.

    2) 문제 분석

    - if 문을 너무 사용하게 되면, 코드 복잡성이 올라간다. 이러할 경우에는 코드리펙토링 과정을 통해서 중첩 조건문을 수정해야한다.

    3) 문제 해결

    case 4 -> { 
                if (!cart.cartItemsNotEmpty()) {
                  throw new IndexOutOfBoundsException();
                }
                cart.showCartItems();
                Output.printOutput("1. 주문하기     | 2. 메뉴판으로 돌아가기");
                int orderConfirmation = Input.getInput();
                switch (orderConfirmation) {
                  case 1 -> {
                    Output.printfStringOutput("주문이 완료되었습니다.", "총 금액: " + Integer.toString(cart.getTotalPrice()));
                    isRunning = false;
                    continue;
                  }
                  case 2 -> {
                    System.out.println("메뉴판으로 돌아갑니다.");
                    continue;
                  }
                  default -> throw new IndexOutOfBoundsException();
                }
              }

    - 이러할 경우에는 이중 스위치문을 사용하면 된다. 확실히 위의 if 문 보다는 조금 더 직관적으로 읽을 수 있는 것을 알 수 있다 !

    7. Final 이 왜 필요한가 ?

    - Final 에 대해서 알아볼려면 불변성에 대해서 알아야한다.

    1 불변성

    1) 불변성이란?

    " 한 번 값이 정해지면 절대 변경할 수 없는 상태 "를 의미한다.

    즉, 객체나 변수의 값을 변경할 수 없도록 보장하는 개념이다.

    2) 참조(Reference) 자체가 변경되지 않도록 보장

    - final을 사용하면 변수의 참조(Reference) 자체가 변경되지 않도록 보장한다.

    - 하지만! 객체 자체가 불변인지 여부는 따로 관리해야 한다.

    = > setter를 사용하면 final의 의미가 사라져서 불변 객체가 아니게 된다

    3) 완전한 불변 객체(Immutable Object)

    - 객체의 값을 절대 변경할 수 없게 하려면

    > 모든 필드를 private final로 선언

    > 생성자로 초기화 후 setter 제공하지 않기

    > 변경 가능한 필드는 복사본을 제공 (방어적 복사, Defensive Copying)

    4) 객체의 final

    - 객체의 final인 경우 조금 다른 의미를 가진다.

    기본 타입 (int, double, boolean 등) → final을 쓰면 값 자체를 변경할 수 없음.

    참조 타입 (List, Map, Set, ArrayList 등) → final을 써도 객체 내부 값은 변경할 수 있음. → 단, 새로운 객체로 할당(new)은 불가능함!

    - 그러니까 객체에 final을 선언하게 되면, 메모리가 stack에 쌓이고 해당 객체의 인스턴스 값 즉 실제 리스트의 값들은 Heap 저장된다. 여기서 Heap 에 저장되는 아이들은 수정이 가능한데 이러한 성격 때문에 내부 아이템은 변경이 가능하다는 말이 나오는 것이다.

    - 정말 불변 리스트를 만들고 싶은 것이라면, Collections.unmodifiableList(list)를 반환하면 된다. 이러면, 외부에서 값을 변경할 수 없다.

    8. 패키지 분리 방법 추천

    • 도메인 단위> 도메인 단위로 나누기
    • 레이어 > 서비스, 컨트롤러 등의 레이어로 분리

     

    ✅ 오늘의 회고

    - 도전과제 1단계까지는 끝이다 : > 

    - 내일은 2단계까지 끝내는 것이 목표이다 !! 내일도 파이팅 해봐야겠다 😆

    - 이해가 안 가서 이렇게 펼쳐서 생각해보니 이해가 잘 안됐던게 이해가 됐다. !! 😇😇