본문 바로가기
Thoughts/우아한테크코스 프리코스

점심 메뉴 추천 - 카테고리 추천 기능

by dokkisan 2023. 11. 27.

우테코 최종 코테를 준비하며 기출 문제 구현을 연습하고 있다.

작년에 출제된 점심 메뉴 추천을 구현하며 겪었던 문제를 기록하고자 한다.

 

1. 콘솔에서 입력이 닫히지 않음


문제 발생

스터디하면서 구현했던 코치 정보 입력, 카테고리 추천 부분 리팩터링 중인데 갑자기 콘솔에서 입력이 닫히지 않는다.

public MenuController() {
		this.inputView = new InputView();
		this.outputView = new OutputView();
	}

	public void run() {
		createCoaches(getCoachNames());
		getAvoidFoods();

		viewMenuRecommendationResult();
	}

	private void createCoaches(List<String> names) {
		CoachRepository coachRepository = new CoachRepository();
		for (String name : names) {
			coachRepository.save(new Coach(name));
		}
	}

	private List<String> getCoachNames() {
		outputView.printMessage(OperationMessage.INPUT_NAMES.getMessage());
		while (true) {
			try {
				return inputView.inputNames();
			} catch (IllegalArgumentException e) {
				outputView.printMessage(e.getMessage());
			}
		}
	}

	private void getAvoidFoods() {
		CoachRepository coachRepository = new CoachRepository();
		List<Coach> coaches = coachRepository.findAll();
		for (Coach coach : coaches) {
			outputView.printMessage(coach.getName() + OperationMessage.INPUT_AVOID_FOODS.getMessage());
			List<String> avoidFoods = inputView.inputAvoidFoods();
			coach.setAvoidFoods(avoidFoods);
			System.out.println(avoidFoods);
		}
	}

 

public class MenuManager {
	private List<MenuCategory> recommendedCategories = new ArrayList<>();
	private Map<String, List<String>> recommendedMenusResult;

	public MenuManager() {
		this.recommendedCategories = recommendCategories();
		// this.recommendedMenusResult = recommendedMenusResult;
	}

	public List<MenuCategory> getRecommendedCategories() {
		return recommendedCategories;
	}

	public Map<String, List<String>> getRecommendedMenusResult() {
		return recommendedMenusResult;
	}

	private List<MenuCategory> recommendCategories() {
		final int MAX_RECOMMEND_RANGE = 2;

		for (int i = 0; i < DayOfWeek.values().length; i++) {
			MenuCategory category = MenuCategory.get(Randoms.pickNumberInRange(1, 5));
			while (Collections.frequency(recommendedCategories, category) <= MAX_RECOMMEND_RANGE) {
				category = MenuCategory.get(Randoms.pickNumberInRange(1, 5));
			}
			recommendedCategories.add(category);
		}
		System.out.println("recommendedCategories = " + recommendedCategories);
		return recommendedCategories;
	}

 

문제 정의

리팩터링을 하며 수정한 부분은 다음과 같다.

MenuController.createCoaches(): 기존에 List<String>에 저장 후 반환하던 로직을 Coach 객체를 생성하도록 수정

MenuController.getAvoidFoods(): Coach 객체에 setter를 사용해 avoidFoods 초기화

MenuManager.recommendCategories(): while문 추가해 한 주에 같은 카테고리를 2번 이상 추천하지 못하도록 함

 

디버깅을 해보니 Coach 객체 생성에는 문제 없이 기능이 동작하는데, viewMenuRecommendationResult()로 이동한 뒤 콘솔 입력이 계속 열려있다.

ManuManager 생성까지 와서는, MenuManager 객체를 생성하지 못하고 콘솔이 계속 열려있다.

 

package menu.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import camp.nextstep.edu.missionutils.Randoms;

public class MenuManager {
	private List<MenuCategory> recommendedCategories = new ArrayList<>();
	private Map<String, List<String>> recommendedMenusResult;

	public MenuManager() {
		this.recommendedCategories = recommendCategories();
		// this.recommendedMenusResult = recommendedMenusResult;
	}

	public List<MenuCategory> getRecommendedCategories() {
		return recommendedCategories;
	}

	public Map<String, List<String>> getRecommendedMenusResult() {
		return recommendedMenusResult;
	}

	private List<MenuCategory> recommendCategories() {
		final int MAX_RECOMMEND_RANGE = 2;

		for (int i = 0; i < DayOfWeek.values().length; i++) {
			MenuCategory category = MenuCategory.get(Randoms.pickNumberInRange(1, 5));
			while (Collections.frequency(recommendedCategories, category) <= MAX_RECOMMEND_RANGE) {
				category = MenuCategory.get(Randoms.pickNumberInRange(1, 5));
			}
			recommendedCategories.add(category);
		}
		System.out.println("recommendedCategories = " + recommendedCategories);
		return recommendedCategories;
	}

	private void recommendMenu(List<Coach> coaches) {
		for (Coach coach : coaches) {
			List<String> recommendedMenus = new ArrayList<>();
			for (MenuCategory category : recommendedCategories) {
				String recommendedMenu = Randoms.shuffle(Menu.getMenuBy(category)).get(0);
				if (coach.isAvoidFood(recommendedMenu)) {
					recommendedMenu = Randoms.shuffle(Menu.getMenuBy(category)).get(0);
				}
				recommendedMenus.add(recommendedMenu);
			}
			recommendedMenusResult.put(coach.getName(), recommendedMenus);
		}
	}
}

 

MenuManager의 생성자를 보면 recommendCategories()를 호출하고 있다.

혹시 while문에서 무한 루프를 돌고 있는건 아닐까..?

생성자를 삭제해버리니 MenuManager가 생성되었고, 다음 코드로 넘어가는 것을 볼 수 있다.

MenuManager가 생성된 모습
콘솔에 잘 출력되는 중

생성자를 삭제해버려서 카테고리 추천 로직이 동작하지 않아, 카테고리 리스트에 아무 값도 들어있지 않다.

 

문제 해결

private List<MenuCategory> recommendCategories() {
	final int MAX_RECOMMEND_RANGE = 2;

	for (int i = 0; i < DayOfWeek.values().length; i++) {
		MenuCategory category = MenuCategory.get(Randoms.pickNumberInRange(1, 5));
		while (Collections.frequency(recommendedCategories, category) <= MAX_RECOMMEND_RANGE) {
			category = MenuCategory.get(Randoms.pickNumberInRange(1, 5));
		}
		recommendedCategories.add(category);
	}

	return recommendedCategories;
}

 

이 로직은 recommendedCategories(추천 카테고리 카테고리 리스트)에 category(새로 추천하는 카테고리)와 동일한 이름의 값이 2개 이하일 때, categoryrecommendedCategories에 추가한다는 의도를 가지고 작성했다.

 

웁쓰.. 잘못된 부분을 발견했다!

while문의 조건에 따르면, 카테고리가 2개 이상이 아니라면 while문 내부에는 add()가 있어야 한다..

조건문을 아래와 같이 수정했다.

while (Collections.frequency(recommendedCategories, category) > MAX_RECOMMEND_RANGE) {...}

 

이번엔 NPE가 뜬다. 😂

첫 번째 카테고리가 생성된 뒤..
NPE가 발생한다....

MenuManager의 31번째 라인은 방금 수정한 while문의 조건문이다.

필드에서 recommendedCategories를 초기화해 해결했다.

 

2. 조건문이 의도대로 동작하지 않음(최대 카테고리 범위를 벗어남)


문제 발생

이번엔 조건문이 의도대로 동작하지 않는다.

양식이 3번이나 들어갔다.

사실 이번에 Collections.frequency()를 처음 사용해 봤는데, 객체를 비교하는 부분이 잘 동작하지 않는 것 같다.

하지만 Enum은 값 자체로 객체 동등 비교가 가능하기 때문에 객체 비교에는 문제가 없다.

 

문제 정의

우선 Collections.frequency()가 반환하는 값을 출력해보려고 했다.

private List<MenuCategory> recommendCategories() {
	final int MAX_RECOMMEND_RANGE = 2;

	for (int i = 0; i < DayOfWeek.values().length; i++) {
		MenuCategory category = MenuCategory.get(Randoms.pickNumberInRange(1, 5));
    	
		int frequency = Collections.frequency(recommendedCategories, category);
		while (frequency <= MAX_RECOMMEND_RANGE) {
			category = MenuCategory.get(Randoms.pickNumberInRange(1, 5));
		}
		recommendedCategories.add(category);
	}

	return recommendedCategories;
}

 

Collections.frequency()의 반환값을 얻기 위해 int 변수를 선언해 할당했는데, 이렇게 되면 while문 안의 코드가 실행되더라도 조건문에 있는 frequency가 업데이트 되지 않는 것이 문제였다.

그렇다고 아래와 같이 수정하면 코드가 중복된다.

int frequency = Collections.frequency(recommendedCategories, category);

while (frequency > MAX_RECOMMEND_RANGE) {
	category = MenuCategory.get(Randoms.pickNumberInRange(1, 5));
	frequency = Collections.frequency(recommendedCategories, category);
}

 

if문을 사용하는 것 말고는 떠오르지 않는다.

또, frequency가 2가 아닌 1이 되어야 같은 카테고리가 2개까지만 List에 저장된다.

따라서 조건문 안에 쓰인 비교 연산자를 '>'에서 '=='로 수정해 문제를 해결했다.

int frequency = Collections.frequency(recommendedCategories, category);

if (frequency == MAX_RECOMMEND_RANGE) {
	category = MenuCategory.get(Randoms.pickNumberInRange(1, 5));
}

 

3줄 요약

1. 조건문 작성할 때는 집중하자.. 의외로 이 부분에서 실수를 많이 한다

2. 컬렉션이 초기화 되었는지 확인 후 메서드에서 참조하자

3. while문을 사용할 때는 탈출 조건을 명확히 하자