post-custom-banner

이어서...


OrderService 클래스

어제에 이어서, 주문에 필요한 모든 것들을 조립해 Order 클래스를 만드는 OrderService 클래스를 작성했다.

import christmas.domain.order.Order;
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 java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.StringTokenizer;
import java.util.function.Supplier;

public class OrderService {
  private static final String orderMenuRegex = "^([가-힣]+-[1-9]\\d*(,\\s*[가-힣]+-[1-9]\\d*)*)$";
  private final List<MenuStrategy> strategies = List.of(
          new AppetizerStrategy(),
          new DessertStrategy(),
          new DrinkStrategy(),
          new MainDishStrategy()
  );

  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 Order start(Supplier<String> inputSupplier) {
      while (true) {
          try {
              System.out.println("주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)");
              return createOrder(inputSupplier.get());
          } catch (IllegalArgumentException e) {
              System.out.println(e.getMessage());
          }
      }
  }

  public Order createOrder(String inputMenu) {
      String inputOrderMenu = validateRegexOrderMenu(inputMenu);
      HashMap<String, Integer> splitMenuQuantity = splitOrderMenu(inputOrderMenu);
      for (Entry<String, Integer> menuQuantity : splitMenuQuantity.entrySet()) {
          String menuName = menuQuantity.getKey();
          int quantity = menuQuantity.getValue();

          MenuStrategy strategy = findStrategy(menuName); // 전략 검색
          HashMap<String, Integer> menuMap = getMenuMap(strategy);
          if (menuMap.containsKey(menuName)) {
              throw new IllegalArgumentException("[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요.");
          }
          strategy.putMenu(getMenuMap(strategy),menuName ,quantity); // 전략에 따라 해당하는 MenuMap에 put
      }
      if (appetizers.isEmpty() && dessert.isEmpty() && mainDish.isEmpty()) {
          throw new IllegalArgumentException("[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요.");
      }
      return new Order(appetizers, dessert, drink, mainDish);
  }

  private String validateRegexOrderMenu(String inputOrderMenu) {
      if (!inputOrderMenu.matches(orderMenuRegex)) {
          throw new IllegalArgumentException("[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요.");
      }
      return inputOrderMenu;
  }

  private HashMap<String, Integer> splitOrderMenu(String inputOrderMenu) {
      HashMap<String, Integer> split = new HashMap<>();
      StringTokenizer st = new StringTokenizer(inputOrderMenu, ",-");
      int menuAmount = 0;
      while (st.hasMoreTokens()) {
          String menuName = st.nextToken();
          int amount = Integer.parseInt(st.nextToken());
          menuAmount += amount;
          if(menuAmount > 20) {
              throw new IllegalArgumentException("[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요.");
          }
          split.put(menuName, amount);
      }
      return split;
  }

  private MenuStrategy findStrategy(String menuName) {
      return strategies.stream()
              .filter(strategy -> strategy.acceptMenuName(menuName))
              .findFirst()
              .orElseThrow(() -> new IllegalArgumentException("[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."));
  }

  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("[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요.");
  }
}

원래 초기에 내가 생각했던 OrderService 클래스와는 조금 구현이 다르게 되었다.

사실 원래는 날짜와 주문 목록을 가지는 Customer 이라는 도메인 객체를 만들고, CustomerService 라는 서비스 클래스를 만들어 이곳에서 날짜와 주문 목록을 모두 검증 후 이 정보를 바탕으로 OrderService 객체가 Order 객체를 생성하게 하려 했는데, 잘못된 입력이 있을 경우 그 부분부터 재입력을 받아야 한다는 요구사항 때문에 OrderService 에서 재입력을 받게 구현할 수는 없었다. 내가 생각한 대로 만들면 Customer 객체는 이미 검증 받은 날짜, 주문 입력을 가지는데, 이 주문 목록이 중복 되는지, 존재하는 메뉴인지는 OrderService 에서 검증되기 때문이다.

처음에는 CustomerService, OrderService 가 한번에 완성되고 실패시 CustomerService 부터 롤백되게 구현하기 위해 트랜잭션에 대해서 알아보다가, 스프링을 사용해야 한다는 걸 알고 포기했다.

추후 저 OrderService 클래스에서 validateRegexOrderMenu(), splitOrderMenu() 메서드는 클래스 분리를 해야하지 싶다.

VisitDate 클래스

package christmas.domain.date;

import java.time.LocalDate;
import java.time.YearMonth;

public class VisitDate {
    private final int date;
    private final String day;

    public VisitDate(String date) {
        this.date = validateDate(date);
        this.day = LocalDate.of(2023, 12, this.date).getDayOfWeek().toString();
        System.out.println(day);
    }

    private int validateDate(String date) {
        int localDate;

        if (date.startsWith("0")) {
            throw new IllegalArgumentException("[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요.");
        }

        try {
            localDate = Integer.parseInt(date);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요.");
        }

        if (localDate < 1 || localDate > YearMonth.of(2023, 12).lengthOfMonth()) {
            throw new IllegalArgumentException("[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요.");
        }
        return localDate;
    }

    public int getDate() {
        return date;
    }

    public String getDay() {
        return day;
    }
}

원래는 Customer 라는 객체가 가질 날짜라는 상태를 VisitDate 객체가 가지게 했다. 그래야 컨트롤러에서 날짜와 주문에 대한 입력을 각각 줘서 잘못된 입력만 재입력받을 수 있기 때문이다.

특이한 점은 date(날짜) 에 검증시 YearMonth.of() 메서드, day(요일) 정의시 getDayOfWeek() 메서드를 사용해서 이후 2023, 12 값들을 enum으로 빼내 다른 년도, 다른 월에도 재사용 가능하게 로직을 구현했다.

DateService 클래스

public class DateService {
    public VisitDate getDateInput(Supplier<String> inputSupplier) {
        while (true) {
            try {
                System.out.println("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)");
                return new VisitDate(inputSupplier.get());
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

DateService 클래스는 간단하다. 입력된 날짜를 검증할 조건이 주문 메뉴에 비해 적기 때문이다. 입력받은 값으로 VisitDate 객체를 만들고, VisitDate의 생성자에서 값의 할당이 실패해 에러를 캐치하면 Supplier 함수형 인터페이스를 통해 다시 입력을 받게 만든다.


마무리

오늘도 코드 작성은 많이 하지 못했다. OrderService 의 구현에 공을 많이 들였기 때문이다. 몇번이나 코드를 쓰고 지우고 하는 과정을 거쳤다.

사실 중간에 좀 타협하고 싶은 생각이 들기도 했다. 전략 패턴을 적용하지 않고 MenuValidator 같은 클래스를 만들어 if문으로 일단 다 검증시킨 후에 만들어진 값을 넣어주면 빠르게 구현할 수 있었다.

그런데 그렇게 하지 않은 이유는, 객체가 정말 객체스러운가? 라는 의문 때문이었다.
MenuValidator 에 주는 값은 최초의 입력값일 것이고, MenuValidator 는 이 입력값이 일단 형식에 맞는지 확인하고, 쪼개고, 중복된 메뉴가 있는지, 음료만 주문하지는 않았는지, 20개 이상의 메뉴를 주문하지는 않았는지... 등등등 검증할 것이다.
그렇게 해서 검증 이후 반환하는 건? 입력 그대로의 문자열일까? 전체 메뉴 목록을 담은 자료구조? 아니면 Order 객체를 생성해서 반환해야할까?

입력 그대로의 문자열을 반환한다면, Order 클래스는 MenuValidator 에서 했던 지겨운 쪼개기 과정과 메뉴 종류별 주문 분류를 반복한 후에야 주문 메뉴라는 상태를 가진다.

전체 메뉴 목록을 담은 자료구조를 줘도, 메뉴 종류별 주문을 상태로 가지는 Order 클래스는 다시 한번 메뉴 종류별로 주문을 분류해야 한다.

Order 클래스를 생성하는 건... 정말로 이게 검증 객체 의 역할일까?

결국 이런 구조의 OrderService 를 만든건, Order 라는 도메인 객체의 역할과, 그것과 비례하는 뚱뚱함(?), 단일 책임 원칙 사이에서 적절히 균형을 맞추려 했기 때문이다.

추후에 리팩토링 때 OrderService() 내에서 Order 객체가 처리할 수 있는 부분은 Order 객체에게 넘기고, 또 분리가 필요한 다른 메서드들을 이동시키면 더 깔끔한 코드가 될 거라고 생각한다.

새롭게 알게된 점

  1. 트랜잭션
    트랜잭션을 사용해 보기 위해 학습을 했으나, Spring의 @Transactional 또는 SQL, DB를 사용하지 않는 환경에서 사용은 어렵다고 판단했다.

  2. getDatOfWeek() 메서드
    DayOfWeek 이라는 클래스를 처음 알게 됐다. 이 클래스 덕분에 추후 변경될 수 있는 이벤트 적용 범위를 더욱 가변적으로 설정할 수 있게 되었다.

좋았던 점

  1. 상대적으로 많은 시간으로 인해 객체지향에 더욱 신경쓸 수 있었던 것

  2. OrderService 를 구현하고 생각한 대로 테스트 동작에 성공했을 때의 기쁨

도움이 된 자료들

| [Java] 특정 날짜의 요일 구하기 (숫자, 영문, 한글)

| [Java8 Time API] LocalDate, LocalTime, LocalDateTime 사용법

| 트랜잭션(Transaction)의 예외(Exception)에 따른 롤백 처리

profile
자바 백엔드 개발자
post-custom-banner

0개의 댓글