문제
Cart 클래스 내의 `CartItemList`에 동일한 `menuItem`을 여러 번 담은 상황에서,
기존의 코드는 출력하면 `menuItem`이 개수만큼 출력됐다.
`menuItem`을 한 번만 출력하되 수량을 함께 출력할 수 있게 코드를 아래와 같이 수정했다.
// Cart.java
private List<MenuItem> cartItemList = new ArrayList<>();
public boolean checkDuplication(MenuItem item){
if(cartItemList.get(0)==item){
return false;
}else{
for (int i = 0; i < cartItemList.indexOf(item); i++) {
if(cartItemList.get(i)==item){
return true;
}
}
return false;
}
}
// MenuView.java
for(MenuItem item : cart.getCartItemList()){
if(!cart.checkDuplication(item)){
System.out.print(item.getName() + " | ");
System.out.print(item.getPrice() + " | ");
System.out.print(cart.getQuantity(item) + " | ");
System.out.println(item.getExplanation());
}
}
나의 의도는 `getCartItemList()` 메서드로 Cart 클래스 안에 있는 `CartItemList`에 접근해서 for-each문으로 각각의 `menuItem`을 출력하는 것이다.
이때 `checkDuplication()` 메서드에 각 요소를 전달한다.
`checkDuplication()` 메서드 안에서는 전달받은 요소가 첫 번째 요소면 무조건 출력할 수 있도록 바로 중복 여부를 false로 반환하고,
그 이후의 요소들은 이전에 존재하는 요소인지 확인하는 과정을 거치고, 처음 나온 요소인 경우만 출력할 수 있도록 한다.
그러나 위 코드의 문제는 중복을 점검하는 메서드를 거쳐도 여전히 중복되어 나온다는 것이다.
원인 분석
A버거 2개, B버거 2개를 장바구니에 담아놓고 출력하는 과정을 디버깅으로 확인했다.
같은 요소는 동일한 주소값을 가지고 있기 때문에, (A버거는 1059, B버거는 1066)
A버거의 경우는 `equals()`나 `==`을 사용해서 비교하면 같은 값으로 인식해서 중복 여부가 false가 되어 버린다.
if(cartItemList.get(0)==item){
return false;
}
그렇다면 A버거 이후에 장바구니에 담았던 B버거는 위의 조건문에 해당하지 않으므로 제대로 중복없이 출력이 되어야 하지만 B버거도 마찬가지로 중복으로 출력된다.
그 원인은 indexOf 메서드에 있다.
for (int i = 0; i < cartItemList.indexOf(item); i++) {
if(cartItemList.get(i)==item){
return true;
}
}
return false;
`indexOf()`는 해당 요소의 정확한 인덱스 번호를 반환하는 게 아니라, 리스트 내부를 순회하다가 가장 처음 만난 동일한 요소의 인덱스를 반환한다.
그렇기 때문에 세 번째로 입력한 B버거와 네 번째로 입력한 B버거 모두, for문의 조건식 `i < cartItemList.indexOf(item)`이 `i < 2`로 적용된다.
세 번째로 입력한 B버거는 앞서 입력된 A버거와 다른 인스턴스이기 때문에 조건문에 해당하지 않아, 정상적으로 반복문 밖에 있는 return false가 실행된다.
네 번째로 입력한 B버거 역시 indexOf로 인해 A버거와만 비교되기 때문에, 반복문 밖의 return false를 실행하게 된다.
해결
이 문제를 해결하기 위해서는 Cart 클래스에 전역 변수를 선언해서, checkDuplication 메서드를 호출한 횟수를 세어야 한다.
횟수를 조건문에 적용해서 첫 번째 A버거와 두 번째 A버거, 첫번째 B버거와 두 번째 B버거를 비교해가면서 중복 여부를 판단하기에는 너무 복잡한 메서드가 되고,
장바구니 내역을 출력할 때마다 전역 변수를 초기화 해주어야 한다.
위 알고리즘을 적용하기에는 너무 가독성이 떨어지고 이해하기 어려운 코드가 되기 때문에,
List가 아닌 Map 자료구조를 적용하기로 했다.
Map은 List와 다르게 중복을 허용하지 않으면서, key-value로 값을 입력한다.
중복을 허용하지 않으면 같은 메뉴 아이템을 어떻게 여러 번 담을 수 있을까?
바로 기존의 데이터를 덮어쓰는 방식으로 진행해야 한다.
조건문으로 이전에 동일한 요소가 저장된 적이 있는지 확인할 필요없이 코드 한 줄로 해결이 가능하다.
cartItemList.put(item, cartItemList.getOrDefault(item, 0) + 1)
`getOrDefault(key, defaultValue)`는 동일한 key값이 저장된 적 있으면 기존의 값을 가져오고,
저장된 적이 없으면 defaultValue에 지정한 값을 가져온다.
따라서 위의 코드를 해석하면 요소를 value값과 함께 Map에 저장할 건데,
이전에 동일한 요소가 저장된 게 없으면 value값을 1로 지정해서 요소를 넣고,
이전에 동일한 요소가 있으면 저장되어 있던 value값에 1을 더해서 덮어쓰겠다! 하는 코드이다.
즉, 여기서 value는 요소의 개수(quantity)가 되므로 따로 `getQuantity()`의 로직을 만들 필요 없이 아래처럼 Map이 제공하는 메서드 `get()`을 이용하면 된다.
public int getQuantity(MenuItem item) {
return cartItemList.get(item);
}
결과를 출력하는 클래스에서도 중복을 확인하는 과정없이 아래 코드로 작성하면 된다.
여기서 `keySet()`은 Map에서 key값만 가지는 Set<T>을 반환하는 메서드로, 장바구니에 저장되어 있던 `menuItem` Set이 된다.
for (MenuItem item : cart.getCartItemList().keySet()) {
System.out.print(item.getName() + " | ");
System.out.print(item.getPrice() + " | ");
System.out.print(cart.getQuantity(item) + "개 | ");
System.out.println(item.getExplanation());
}
결과
이미지를 보면 각 요소가 중복되지 않고 출력되는 것을 볼 수 있고, 수량도 제대로 2개씩 출력된다.
+2025/03/14 추가
과제 해설 세션을 통해 `map.put(key, map.getOrDefault(key, 0) + 1)`와 같은 기능을 하면서 조금 더 간단한 메서드를 알게 됐다.
map.merge(key, value, remappingFunction)
Map<String, Integer> map = new HashMap<>();
map.put("apple", 2);
map.merge("apple", 1, Integer::sum); // apple의 값은 3이 됨
key가 없으면 value를 새로 삽입하고, key가 이미 존재하면 remappingFunction을 통해 두 값을 병합하는 메서드이다.
Integer.sum() 메서드를 통해 기존에 입력된 2와 새로 머지할 1을 더하기 연산하여 저장할 수 있다.
'트러블 슈팅' 카테고리의 다른 글
[Java/Spring] 406 Not Acceptable / 검증 어노테이션 에러 메시지 변경 (0) | 2025.03.24 |
---|---|
[Java] 키오스크 프로그램에 스트림 적용 중 발생한 이슈 (0) | 2025.03.13 |
[Java] BufferedWriter 출력 안 됨 이슈 (1) | 2025.03.11 |
[Java] 계산기 프로그램 추가 기능 구현 중 발생한 오류 해결 (0) | 2025.03.05 |
[Java] 계산기 프로그램의 출력값 오류 이슈 (0) | 2025.02.28 |