키오스크 과제 트러블 슈팅

dereck·2025년 1월 17일

TIL

목록 보기
16/21

들어가기 전에

뭔가 구현을 하는 것보다 요구사항을 이해하는게 힘들었다. 사실 동작은 어떻게든 돌아가게 만들 순 있었는데 항상 요구사항에 맞게 한 번에 개발한 적이 없는 것 같다.

다음 과제를 하게 된다면 시간을 좀 더 쓰더라도 천천히 읽어보면서 문서 정리라도 해봐야겠다..

본격적인 트러블 슈팅

아래의 순서는 하면서 막힌 순서가 아니라 생각나는대로 적어놓은 순서이다.

1. 할인 적용해서 총 가격 구하기

문제

장바구니에 품목을 저장하고, 할인율을 받아서 계산할 때 계산 이후 double의 부동소수점 방식으로 인해 정확한 값이 아닌 근사치가 나오게 되었다.

해결

BigDecimal을 사용하는 것으로 해결했다. 이때 doubleBigDecimal로 바꿔도 해결이 안됐을 때가 있었는데 BigDecimal 클래스 내부에 있는 문서를 보고 왜 그런지 알 수 있었다.

대충 요약하면 doubleBigDecimal로 바꾸는 경우 정확한 값이 아니게 된다. 따라서 double 타입을 Double.toString(double) 메서드롤 통해 String으로 변환해서 정확한 값을 문자열로 저장한 뒤 BigDecimal의 생성자를 사용해서 변환해야 한다는 말이다.

2. Menu 클래스 내의 MenuItem 출력

구현하고 싶었던 것

Menu 클래스 내의 List<MenuItem> menuItems를 출력할 때 스트림을 사용하려고 했다. 이때 최종 연산인 forEach()를 사용하는 것까진 알았지만 일반 for문을 사용할 때처럼 loop를 돌 때마다 1씩 추가되는 번호도 출력하고 싶었다.

문제

일반 for-each와 다른 점은 단순한 int count를 사용할 수 없다는 것이었다. 스트림하면 빼놓을 수 없는 람다는 함수형 프로그래밍으로 불변값이 아니면 사용할 수 없다. 따라서 count++count += 1 같은 방법은 사용할 수 없다는 뜻이다.

해결

문제 사진에도 나와있지만 AtomicInteger로 변환하고 getAndIncrement()로 해결할 수 있었다. 해당 메서드는 현재 값을 원자적으로 증가시키기 때문에 람다에서도 사용할 수 있는 것이다.

원자적이란?

원자적으로 읽는다는 의미는 다중 스레드 환경에서 변수의 값을 읽는 작업이 중단되거나 다른 스레드에 의해 간섭받지 않고, 단일 작업(Atomic Operation)으로 수행된다는 의미이다.

3. 장바구니에 품목 추가

구현하고 싶었던 것

장바구니에 동일한 품목 추가 시 같은 메뉴가 여러 번 출력되는 대신 한 번만 출력되도록 하고, 수량만 추가하게 만들고자 했다.

처음 생각한 것

사실 수량을 올리는 것은 그리 어려운 작업이 아니었다. List<ShoppingCart> carts 필드가 있다고 할 때 스트림을 생성해서 filter로 리스트 내의 값과 매개변수로 받은 값 중에 같은 값이 있다면 찾아서 quantity += 1을 해주고 값이 없는 경우엔 새로 추가하도록 하니까 됐었다.

리팩토링

기능은 구현했지만 말 그대로 돌아가기만 하는 코드라는 생각이 들었다. 곰곰히 생각한 결과 굳이 isEmpty()를 할 필요가 없었다. 그리고 findFirst()를 한 뒤에 조건문을 사용할 필요도 없었다. findFirst() 이후 Optional로 리턴을 받게 되는데, 여기서 별도의 선언을 하지 않고 계속 체이닝을 이어가면 모든 걸 해결할 수 있었다.

바꾸고 난 뒤의 모습이다. 알아보기 힘든 조건문을 빼고 ifPresentOrElse()를 사용해서 메서드 내에서 분기를 진행했다.

4. 품목 삭제

문제

품목 삭제를 스트림을 사용해서 구현하는 도중, filter 까진 문제 없었으나 최종 연산을 어떻게 할 지 감이 안잡혔었다. 그래서 일단 toList()로 최종 연산을 수행하도록 했다.

처음 실행할 땐 품목이 1개여서 문제가 없는 것 같아 보였다. 하지만 품목이 여러 개일 때 삭제 메서드를 호출하니 리스트에 예상하지 못한 값만 남게 되었다.

ShoppingCart savedItem = carts.get(selectRemoveItem - 1);
    if (savedItem.getAmount() > 0) {
        carts = removeItemInCart(savedItem.getMenuItem().getMenuName());
    }
    carts.removeIf(item -> item.getAmount() == 0);

// 위 코드는 아래 코드를 호출하는 곳

private List<ShoppingCart> removeItemInCart(String menuName) {
        return carts.stream()
            .filter(item -> item.getMenuItem().getMenuName().equalsIgnoreCase(menuName))
            .peek(ShoppingCart::decreaseAmount)
            .collect(Collectors.toList());
    }
}

해결

우선 리스트가 초기화되는 이유는 filter()는 조건에 맞는 것들만 따로 빼서 가져오게 되는데 가져오고 난 뒤 개수를 줄이는 메서드를 호출하고, 남은 값들로만 새로 List를 반환하기 때문에 개수를 줄인 품목 이외엔 전부 삭제 되었기 때문이었다.

문제는 여기서 끝이 아니었는데 toList()는 리스트 반환 시 불변 리스트를 반환한다. 즉, carts.removeIf()에서 예외가 발생한다는 의미다.

그래서 일단 리턴 타입을 void로 바꾸고, 마지막 연산을 어떻게 할 지 고민했다. 중간에 close()도 사용해봤는데 의미 없었고, 해답은 반복문을 통한 개수 차감이었다.

이러면 기존 리스트를 유지하면서 개수만 차감할 수 있고, 개수가 0이 되어서 품목을 삭제할 때도 문제없이 삭제할 수 있게 되었다.

5. 예외 처리

문제 1

예외 처리를 위해 try-catch를 사용하면서 핸들링을 하는 코드를 작성하고 있었다.

위 코드가 Kiosk 내부 start() 메서드인데 문제는 여기서 스캐너 입력 예외가 발생하면 상위 계층으로 던질 수가 없었다. 상위 계층은 start()를 실제로 호출하는 main() 메서드인데 해당 위치에서 예외 처리를 하면 애플리케이션이 중단된다. 반복문은 main() 안에 있는 것이 아닌 start() 내부에 있기 때문이다.

다른 방법으로 break;를 사용했더니 역시나 예외 처리 이후 바로 죽어버렸다.

해치웠나?

굳이 예외를 던지지 않고, 출력도 하지 않았다. 대신 로그를 찍어서 확인할 수 있도록 했다.

} catch (InputMismatchException ime) {
	logger.log(INFO, "com.example.lv6.kiosk.Kiosk start: ", ime);
}

여기서 해결한 줄 알았다.

문제 2

실제로 실행시켜서 확인해보니 예외를 발생시키면 무한 반복이 발생했다.

스크롤이 땅을 뚫으려고 한다..

진짜 해결

같은 예외가 반복적으로 발생하고, 해당 예외가 발생할 부분은 오직 한 군데이기 때문에 입력했던 값이 남아있기 때문이라고 생각했다. 그래서 위의 코드에 입력 버퍼를 정리할 수 있도록 한 줄을 추가해서 해결했다.

} catch (InputMismatchException ime) {
	logger.log(INFO, "com.example.lv6.kiosk.Kiosk start: ", ime);
    scanner.nextLine();  // 버퍼 정리
}

6. 리소스 정리

하고 싶었던 것

스캐너 객체를 생성하고 사용이 끝나면 닫아줘야 하는데 이것을 반복문 이후 작성하는 대신 try-resource를 사용해보고 싶었다. 현재 코드에선 try 구문을 조금 늘려도 괜찮을 것 같다는 답변을 받고, 바로 적용시켰다.

public void start() {
    while (true) {
        printMainMenu();
        int selectNum;

        try (Scanner scanner = new Scanner(System.in)) {
            if (carts.isEmpty()) {
                selectNum = scanner.nextInt();
                if (selectNum == 0) {
                    break;
                } else if (selectNum >= 1 && selectNum <= 3) {
                    handleCartOperations(scanner, selectNum, true);
                } else {
                    throw new BaseException("메뉴에 있는 번호를 입력해주세요.");
                }
            } else {
                printOrderMenu();
                selectNum = scanner.nextInt();
                if (selectNum == 0) {
                    break;
                } else if (selectNum >= 1 && selectNum <= 5) {
                    handleCartOperations(scanner, selectNum, false);
                } else {
                    throw new BaseException("메뉴에 있는 번호를 입력해주세요.");
                }
            }
        } catch (BaseException be) {
            System.out.println(be.getMessage());
        }
    }
    System.out.println("프로그램이 종료되었습니다.");
}

문제

적용까진 괜찮았지만 실행을 시켜보니 또 다른 에러가 나를 반겼다. 왜 멈추는지 몰라서 바로 검색을 시작했다. 한국어 검색으론 안나와서 영어로도 검색했다. (강제 영작은 너무 힘들다)

그리고 얼추 해답이라고 할만한 결과를 찾았다.

https://stackoverflow.com/questions/57969142/is-it-possible-to-use-try-with-resources-along-with-an-input-stream

해결 ?

사실 써보고 싶긴 했지만 단순히 스캐너 하나를 정리하기 위해서 사용할 필요는 없었고, 스캐너 리소스 정리 또한 한 줄만 추가하면 되기 때문에 안쓰는 걸로 타협해서 해결(?)했다.

7. 카테고리 안에 세부 메뉴

문제

카테고리 안에 세부 메뉴가 나오도록 했어야 했는데 어떻게 해결할 지 도통 감을 못잡았다. 출력하니까 생각한 것과는 다른 결과만 출력됐었다.

해결

Kiosk에서 MenuList로 받고, Menu의 생성자에 String category를 넣어주면 카테고리 별로 저장된 List<Menu>에 해당하는 List<MenuItem>을 넣을 수 있다.

아직도 왜 생각을 못했는지 의문..

References

0개의 댓글