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() 메서드가 정상적으로 호출되어 데이터가 추가되는 것을 확인할 수 있었습니다.
장바구니에 담긴 총 금액을 계산하여 출력할 때 아래와 같이 출력 결과에 오차가 발생하였습니다.
아래와 같이 주문 하시겠습니까?
[ 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)을 사용하는 것이 더 적합하다고 생각합니다.
장바구니에 담긴 금액을 제한하는 메서드인 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 람다의 캡처 제한을 우회하기 위해 배열을 사용하여 문제를 해결할 수 있습니다.