이번에도 6, 7일차를 같이 포스팅한다. 그 이유는 6-7일차의 일들을 딱딱 잘라서 말하기가 참 힘들기 때문이다.
6일차에 리팩토링한걸 7일차에 다시 리팩토링하고, 6일차에 완성되었다 생각했던 기능이 7일차에 버그가 발견되고... 이 이유 때문에 6, 7일차의 회고를 따로 작성할 수가 없었다.
커밋도 정말 기능 하나가 완성되었다 라는 판단을 하기 전에는 커밋 하기가 꺼려지는데, '어느 정도는' 완벽해지려는 내 성향이 영향을 미친 것 같다.
이번 회고록에도 모든 걸 담을 수 없으니, 고민하고 해결했던 대표적인 몇 가지만 적어보려고 한다.
음료만 주문하여 에러 발생 시, 이후 주문에 에러가 난 음료 주문액이 누적되어 합산되는 버그가 발견됐다.
아래와 같이 최대로 허용하는 주문 개수 내에서 음료만 주문할 시, 요구사항에 따라 에러 메시지를 띄운다. 그리고 그 다음 주문을 정상적으로 입력했다.
이렇게 실행한 결과, 잘못 주문한 "레드와인-20" 의 금액이 그대로 합산되어 출력된다.
초코케이크 1개에 118만원이라니... 있던 고객들도 다 떠나갈 판이다.
디버깅을 해 보니, 잘못된 입력 후에도 Order
객체가 가지고 있는 drink
의 size
가 여전히 1개임을 발견했다.
| ⬆️ OrderService
클래스가 가지고 있는 클래스 필드 변수
알고보니, Order
객체를 생성할 때 주어지는 OrderService
의 클래스 필드 변수들이 초기화되지 않아서 생긴 문제였다. HashMap
값들의 초기화를 위한 clearMenu()
메서드를 만들어주고, 이 메서드를 재입력시마다 실행되게 했다.
해결됐다!
사실 값들의 초기화는 정말 기본적인 영역이라고 생각했는데, 이렇게 버그로 마주하니 뭔가 새롭고 신선했다(?). 이 부분은 구현을 하다 생각의 소용돌이에 빠져 신경을 못 써서 이런 실수를 했는데, 앞으로는 좀 더 기본적인 것도 당연하게 생각하지 말고 꼼꼼히 점검해야겠다는 생각이 들었다.
음식의 종류별 메뉴를 담는 여러 enum 클래스들이 있다.
이 enum 클래스들은 공통된 getTotalAmount()
메서드를 가지고 있는데, 입력받은 메뉴들(각각의 종류별 메뉴들로 분리된 메뉴들이다)을 바탕으로 메뉴별 총 금액을 계산하여 반환해준다. 해당 메서드를 공통으로 사용하기 위해 Menu
인터페이스로 분리해줬다.
Enum 클래스이면서 Menu를 구현하는 클래스가 사용할 수 있게 메서드의 시그니처를 설정해줬고, Enum의 valueOf() 메서드를 사용하기 위해 Class<T> menuClass
를 매개변수로 선언해서 사용했다.
이건 계속 고민했던 문제였다.
Order가 가지는 상태들과 관련된 로직을 Order 외부에서 가져도 괜찮을까?
라는 의문이 있었는데, 결국 이것도 절대적인 건 없다고 생각했다. 그래도 현재 내 상황에서 Order
클래스가 자신의 상태와 관련된 모든 행위들을 가질지, 아니면 SRP와 가독성을 생각해서 OrderService
에서 관리하게 할지를 계속 고민했는데, 결국 Order
클래스 내부로 옮기기로 결정했다. 이 결정에는 OKKY의 선배 개발자분의 조언이 큰 영향을 미쳤다.
해야 할 일들은 기능과 데이터로 분리할 수 있는데 기능은 많아져도 문제 없습니다. String 클래스를 보면 이해가 되실텐데 String 클래스는 byte[] 데이터와 이 byte[] 데이터를 실행할 수 있는 다양한 메소드로 구성되어 있잖아요. 해당 클래스의 역할에 맞는 기능이 많이 모여 있다고 해도 문제는 없습니다.
실제로 String
클래스는 셀 수 없이 많은 메서드로 구성되어 있다. 라인 수는 자그마치 4,662줄이다. 이 예시를 듣고 나니 Order
클래스 내부 메서드가 많아져도 괜찮다는 생각이 들었다. 물론 정말 필요한 메서드들만 존재해야 한다!
아래의 OrderService
클래스에서 Order
클래스에게 의미 있는 로직들을 전부 옮겼다.
package christmas.service;
import static christmas.config.ErrorMessage.MENU_QUANTITY_INPUT_ERROR_MESSAGE;
import christmas.domain.menu.strategy.AppetizerStrategy;
import christmas.domain.menu.strategy.DessertStrategy;
import christmas.domain.menu.strategy.DrinkStrategy;
import christmas.domain.menu.strategy.MainDishStrategy;
import christmas.domain.menu.strategy.MenuStrategy;
import christmas.domain.order.Order;
import christmas.domain.order.OrderMenu;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class OrderService {
private final List<MenuStrategy> strategies;
private final HashMap<String, Integer> appetizers = new HashMap<>();
private final HashMap<String, Integer> dessert = new HashMap<>();
private final HashMap<String, Integer> drink = new HashMap<>();
private final HashMap<String, Integer> mainDish = new HashMap<>();
public OrderService(List<MenuStrategy> strategies) {
this.strategies = strategies;
}
public Order order(Supplier<String> inputSupplier, Runnable messagePrinter, Consumer<String> errorMessagePrinter) {
while (true) {
try {
messagePrinter.run();
clearMenu();
OrderMenu orderMenu = new OrderMenu(inputSupplier.get());
return createOrder(orderMenu, orderMenu.getOrderMenuQuantity(), strategies);
} catch (IllegalArgumentException e) {
errorMessagePrinter.accept(e.getMessage());
}
}
}
private Order createOrder(OrderMenu orderMenu, HashMap<String, Integer> orderMenuQuantity, List<MenuStrategy> strategies) {
for (Entry<String, Integer> menuQuantity : orderMenuQuantity.entrySet()) {
String menuName = menuQuantity.getKey();
int quantity = menuQuantity.getValue();
MenuStrategy strategy = findStrategy(menuName, strategies); // 전략 검색
HashMap<String, Integer> menuMap = getMenuMap(strategy);
if (menuMap.containsKey(menuName)) {
throw new IllegalArgumentException(MENU_QUANTITY_INPUT_ERROR_MESSAGE.getMessage());
}
strategy.putMenu(menuMap, menuName ,quantity); // 전략에 따라 해당하는 MenuMap에 put
}
return new Order(orderMenu, appetizers, dessert, drink, mainDish);
}
private MenuStrategy findStrategy(String menuName, List<MenuStrategy> strategies) {
return strategies.stream()
.filter(strategy -> strategy.acceptMenuName(menuName))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(MENU_QUANTITY_INPUT_ERROR_MESSAGE.getMessage()));
}
private HashMap<String, Integer> getMenuMap(MenuStrategy menuStrategy) {
if (menuStrategy instanceof AppetizerStrategy) return appetizers;
if (menuStrategy instanceof DessertStrategy) return dessert;
if (menuStrategy instanceof DrinkStrategy) return drink;
if (menuStrategy instanceof MainDishStrategy) return mainDish;
throw new IllegalArgumentException(MENU_QUANTITY_INPUT_ERROR_MESSAGE.getMessage());
}
private void clearMenu() {
appetizers.clear();
dessert.clear();
drink.clear();
mainDish.clear();
}
}
이렇게 다이어트에 성공했다.
package christmas.service;
import christmas.domain.menu.strategy.MenuStrategy;
import christmas.domain.order.Order;
import christmas.domain.order.OrderMenu;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class OrderService {
public Order order(Supplier<String> inputSupplier, Runnable messagePrinter,
Consumer<String> errorMessagePrinter, List<MenuStrategy> strategies) {
while (true) {
try {
messagePrinter.run();
OrderMenu orderMenu = new OrderMenu(inputSupplier.get());
return createOrder(orderMenu, strategies);
} catch (IllegalArgumentException e) {
errorMessagePrinter.accept(e.getMessage());
}
}
}
private Order createOrder(OrderMenu orderMenu, List<MenuStrategy> strategies) {
return new Order(orderMenu, strategies);
}
}
제출과 테스트까지 다 통과하고, 소감문까지 작성 후 4주간의 프리코스 여정을 마무리 했다. 프리코스를 끝낸 감상은 전체 회고록에서 다시 쓸 예정이니, 지금은 이번 4주차 과제에서만 얘기하려 한다.
일단 생각할 게 많았다. 요구사항이 되게 많았기에 여러가지를 생각했어야 했고, 한 기능을 구현할 때도 그 기능과 연계되는 다른 기능과의 조합을 잘 생각하고 구현했어야 했다. 그리고 최종 코테가 이 정도 난이도로 나온다면 모든 요구사항을 다 구현할 수 없을 것 같았다. 스스로의 개발 방법론(?)을 만들고 익숙해져야 할 듯 싶다.
그리고 제일 재밌었던 과제였다. 요구사항이 훨씬 많았기에 난이도가 올라간 복잡한 퍼즐, 게임을 하는 기분이었다. 그 만큼 구현에 완료했을 때의 성취감도 제일 컸던 것 같다...!
우테코 톡방이랑 디스코드는 이미 난리다 😅 특히 요구사항이 많고 난이도가 있었던 과제라 다들 하고 싶었던 말이 많은 것 같다(나도 마찬가지). 뭔가 고등학생 때 시험이 끝나고 공부 잘하는 친구 책상에 모여 서로 답을 맞춰보는 풍경인 것 같았다.
아무튼, 끝이다! 그래도 끝이란게 아직 실감이 나지는 않는다.
주말까지는 좀 쉬어야겠다. 과제하고 학습하느라 매일 평균 5시간 정도밖에 못 잤으니 밀린 잠을 좀 자고, 개인적으로 해야 할 일들을 좀 끝내놓고 월요일부터 다시 시작해야겠다. 4주차 회고글들을 다듬고, 전체 회고글도 쓰고, 코드 리뷰도 하고, 스터디도 만들어볼까 생각중이다.
모든 요구사항을 지켜 제출할 수 있었던 것
요구사항이 정말 많았는데, (내가 알기로) 모든 요구사항을 잘 지켰던 것 같아서 뿌듯했다 :-)
이전 과제에 비해 많은 긍정적인 변화가 일어난 것
특히 컨트롤러와 서비스 클래스의 다이어트가 가장 눈에 돋보이는 변화다.
도메인 객체에 대해, 상태와 비즈니스 로직을 동시에 가진다는 것에 많은 고민을 하고 학습할 수 있었던 것
DDD를 하려고 했던 건 아니지만, 상태를 스스로 관리하게 만들려는, 객체가 객체답게 일하게 만드려는 노력과 성과를 보여서 좋았다!