[TIL-CH2] 키오스크 과제 트러블슈팅

김유란·2025년 1월 17일

CH 2 키오스크 과제 트러블슈팅🚨


1️⃣ 초기화 생략으로 인한 오류

💡 배경

Lv5의 Main클래스에서 Kiosk 객체 생성 및 데이터 추가 부분을 수정하던 중에 오류가 발생했습니다.

🔍 발단

Kiosk 객체에 데이터 추가 및 변경의 유연성을 높이고자 Kiosk 객체 생성 및 초기화가 한 번에 이루어지던 코드에서 객체 생성과 데이터 추가 부분을 분리하여 작성하고자 하였습니다.

// 수정 전
Kiosk kiosk = new Kiosk(
				new ArrayList<>() {{
						add(burgers);
            add(drinks);
            add(desserts);
        }}
);
// 수정 후
Kiosk kiosk = new Kiosk();

kiosk.addCategoryMenu(burgers);
kiosk.addCategoryMenu(drinks);
kiosk.addCategoryMenu(desserts);

⚠️ 전개

코드를 수정하면서 매개변수를 받는 생성자 등 다른 생성자가 따로 선언되지 않았다면 기본 생성자는 자동으로 생성되므로 선언을 생략했으나 코드 실행 시 오류가 발생하였습니다.

💣 위기

List<Menu> categoryMenu;

수정 전에는 Kiosk 객체를 생성하면서 categoryMenu를 초기화하고 있었기에 위와 같이 Kiosk 클래스 내부의 categoryMenu 변수는 따로 초기화하지 않고 선언만 하였습니다. 그러나 Main클래스 수정 후 categoryMenu 필드가 null로 초기화된 상태에서 add() 메서드가 호출되어 NullPointerException이 발생하고 있다는 것을 알았습니다.

✅ 절정

add() 메서드를 호출하기 전에 categoryMenu 필드는 초기화된 상태여야 하며 초기화하는 방법은 아래와 같습니다.

(1)
private List<Menu> categoryMenu = new ArrayList<>();

//or

(2)
private List<Menu> categoryMenu;
// 기본 생성자 안에 초기화
public Kiosk() {
    this.categoryMenu = new ArrayList<>();
}

📌 결말

categoryMenu 변수 선언 시 초기화하는 방법으로 코드를 수정하였고 이후 실행 시 add() 메서드가 정상적으로 호출되어 데이터가 추가되는 것을 확인할 수 있었습니다.


2️⃣ 부동소수점 연산의 정밀도 문제

💡 배경

장바구니에 담긴 총 금액을 계산하여 출력할 때 아래와 같이 출력 결과에 오차가 발생하였습니다.

아래와 같이 주문 하시겠습니까?
[ Orders ]
Cola             | 수량: 1| W 2.90
Lemonade         | 수량: 1| W 5.00
ShackBurger      | 수량: 2| W 6.50
Cheeseburger     | 수량: 1| W 6.90

[ Total ]
W 27.799999999999997

1. 주문			 2. 메뉴판

🔍 발단

double 타입의 변수 27.8을 출력할 때 27.799999999999997와 같이 출력되는 이유는 부동소수점 연산의 정밀도 문제 때문이라는 것을 알게되었습니다. 이는 컴퓨터가 실수를 이진수로 표현하는 방식에서 기인합니다.

⚠️ 전개

double은 IEEE 754 표준을 따르는 64비트 부동소수점 형식으로 저장됩니다. 이 형식은 고정소수점 형식에 비하여 표현 범위가 넓지만 모든 실수를 정확이 표현할 수는 없어 값의 근사치로 저장됩니다.

💣 위기

자바는 BigDecimal이라는 클래스를 통하여 오차 없는 소수점의 연산을 제공하고 있어 이를 사용하면 실수 연산에서 더 높은 정밀도를 유지할 수 있습니다. 따라서 아래와 같이 BigDecimal을 사용하여 값을 변경하였는데 여전히 출력 값에 오차가 발생하였습니다.

BigDecimal.valueOf(total)

✅ 절정

total 변수는 이미 double 타입으로 선언되어 있기에 BigDecimal로 변환해도 total이 포함하고 있는 부동소수점 오차를 그대로 전파합니다. 이를 해결하기 위한 방법으로는 String 타입으로 변환 후 BigDecimal 생성자를 사용하거나 반올림을 적용한 BigDecimal을 생성하는 방법이 있습니다.

(1) String 변환 후 BigDecimal 생성

BigDecimal(String.valueOf(value))

BigDecimal(Double.toString(value))

(2) BigDecimal 생성 후 반올림

BigDecimal.valueOf(total).setScale(2, RoundingMode.HALF_UP);

📌 결말

키오스크 과제에서는 출력 값의 소수점 자릿수를 조정하고 싶어 방법 (2)을 사용하였지만 주로 정확한 값을 저장하고 싶을 때 BigDecimal을 사용하므로 대부분의 경우 방법 (1)을 사용하는 것이 더 적합하다고 생각합니다.


3️⃣ Java 람다에서 변수 캡처 제한

💡 배경

장바구니에 담긴 금액을 제한하는 메서드인 dropItemsOverLimit(double max) 을 구현할 때 변수 사용에 오류가 발생하였습니다.

🔍 발단

람다를 사용하여 메서드를 구현하던 중에 메서드 내에 선언한 지역 변수를 람다 표현식 내부에서 값을 변경하며 사용하여 컴파일 에러를 발생시켰습니다.

⚠️ 전개

람다 표현식 내부에서는 final 또는 effectively final 변수만 사용 가능합니다. effectively final 변수란 값이 재할당되지 않아 final 변수처럼 동작하는 변수입니다. 따라서 람다 내부에서는 값을 변경할 수 있는 일반적인 변수를 사용할 수 없습니다.

💣 위기

람다가 외부에 정의된 변수를 참조할 때 해당 변수의 복사본을 생성하여 사용하는데, 이 과정에서 지역 변수는 스택 영역에 할당되므로 람다 내부에서 값이 변경될 경우 일관성이 깨질 수 있어 값을 변경할 수 없도록 제한이 적용됩니다.

✅ 절정

배열은 참조형 객체로, 배열 내부의 값을 변경하더라도 참조 자체는 변경되지 않습니다. 따라서 배열을 사용하면 람다 내부에서 값을 누적하거나 변경할 수 있기에 배열을 활용해서 누적된 금액을 계산할 수 있었습니다.

// 장바구니에 담긴 금액을 제한하는 메서드
public void dropItemsOverLimit(double max) {
		final double[] total = {0};  // 계산 결과 누적할 배열
    cartItems = cartItems.stream()
            .takeWhile(cartItem -> {
                double newTotal = total[0] + cartItem.getCount() * cartItem.getPrice();
                if (newTotal > max) {  // 누적된 금액이 20을 초과하면 스트림 종료
                    return false;
                }
                total[0] = newTotal;
                return true;
            })
            .collect(Collectors.toCollection(ArrayList::new));  // ArrayList 타입으로 저장
}

📌 결말

람다 표현식 내부에서는 값을 변경할 수 있는 변수를 사용할 수 없습니다. Java 람다의 캡처 제한을 우회하기 위해 배열을 사용하여 문제를 해결할 수 있습니다.

0개의 댓글