오늘은 설계를 완료하고 조금의 코드를 작성했다.
양은 적지만 내 수준에서 추상화 퀄리티가 높은 코드를 작성한 것 같아 매우매우 뿌듯했다.
그리고 시간의 부담이 적으니 포스팅을 좀 더 정성들여 쓸 수 있게 됐다. 여전히 예쁘게 꾸미는 능력은 없지만,
내가 어떤 의문을 가졌고, 이를 해결하기 위해 어떤 고민을 했으며,
새로운 것을 학습했고 그 결과 이런 코드를 작성했다!
를 잘 보여주는 포스팅을 쓰려고 노력했다. 이게 매일매일의 내 성장 스토리니까.
일단은 요구사항에 따라 구현할 기능 목록을 정리해 보았다.
## 구현할 기능 목록
* 입출력
* [사용자에게서 입력 받기] (서비스 로직)
* [에러 메시지 출력] (서비스 로직)
* [혜택 결과 출력] (서비스 로직)
* 증정 이벤트에 해당하지 않으면 증정 메뉴를 “없음” 으로 보여준다.
* 고객에게 적용된 이벤트 내역만 보여준다.
* 적용된 이벤트가 하나도 없다면 혜택 내역 "없음"으로 보여준다.
* 이벤트 배지가 부여되지 않는 경우, "없음"으로 보여준다.
* 검증 (도메인 로직)
* [식당 방문 날짜 입력 검증]
* 1 이상 31 이하의 숫자가 아닌 입력의 경우, "[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요." 메시지 출력
* 예외 상황
* 숫자 이외의 문자를 입력했을 때
* 공백을 입력했을 때
* 아무 값도 입력하지 않았을 때
* 음수, 소숫점, 계산식을 입력했을 때
* 맨 앞자리가 0일 때
* 정해진 방문 날짜 범위를 벗어낫을 때 (1~31)
* 자료 구조에 담을 수 없는 범위의 숫자를 입력했을 때
* [주문 메뉴와 개수 입력 검증]
* 에러 메시지는 “[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."
* 예외 상황
* 메뉴판에 없는 메뉴를 입력했을 때
* 메뉴의 개수가 1 이상의 숫자를 입력하지 않았을 때
* 메뉴 형식이 예시와 다를 때
* 중복 메뉴를 입력할 때
* 도메인(비즈니스) 로직 기능
* [총 주문 금액 계산]
* [총 혜택 금액 계산]
* [크리스마스 디데이 할인 적용]
* 1,000원으로 시작하여 크리스마스까지 할인 금액이 매일 100원 증가한다.
* 총 주문 금액에서 해당 금액만큼 할인한다.
* [평일 할인 적용]
* 일요일~목요일에는 디저트 메뉴를 1개당 2,023원 할인한다.
* [주말 할인 적용]
* 금요일, 토요일에는 메인 메뉴를 메뉴 1개당 2,023원 할인한다.
* [특별 할인 적용]
* 이벤트 달력에 별이 있으면 총 주문 금액에서 1,000원 할인한다.
* [증정 이벤트 적용]
* 할인 전 총 주문 금액이 12만원 이상일 때, 샴페인 1개를 증정한다.
* [배지 부여]
* 총 혜택 금액에 따라 다른 이벤트 배지를 부여한다.
* 5천원 이상 : 별
* 1만원 이상 : 트리
* 2만원 이상 : 산타
* [이벤트 주의 사항 적용]
* 총 주문 금액 10,000원 이상부터 이벤트 적용이 가능하다.
* 음료만 주문 시, 주문할 수 없다.
* 메뉴는 한번에 최대 20개까지만 주문 가능하다.
확실히 요구사항이 많긴 많아졌다. 그래도 이렇게 정리해보니 어떻게 구현해야 할 지 조금 감이 잡히긴 한다.
3주차 과제때는 도메인 객체의 역할을 거의 고려하지 않고 구현을 한 것 같아서, 이번에는 도메인 객체의 역할을 고려하며 설계를 해 보았다.
우아한 기술블로그의 포스팅이 많은 도움이 되었다. 객체지향의 사실과 오해
책을 기반으로 포스팅을 작성하셨는데, 나도 이 책을 읽어본지라 더 잘 이해할 수 있었다.
그리고 이렇게 설계를 해 봤다. (악필 주의)
이렇게 역할에 따라 객체를 정하니, 확실히 흐름이 전체적으로 그려지는 느낌이다. 이번에는 도메인 객체가 객체스럽게 행동할 수 있게 구현을 해보려고 한다.
이번에는 입출력과 검증을 먼저 구현하지 않고 가장 중요한 기능부터 구현해보기로 했다. 어제 코수타에서 준 코치님이 말씀해주신 조언 때문이었다.
먼저 어플리케이션의 핵심 기능을 한 줄로 적어본다.
그리고 그것이 동작하기 위한 가장 작은 버전을 만든다.
일단은 가장 먼저 '주문' 객체를 만들었다.
고민한 것이 이 주문 객체가 사용자가 입력한 주문들인 orderMenu
와 날짜인 date
를 상태로 가져야 할지, 아니면 메뉴별 주문 목록과 이것들을 바탕으로 계산된 총 금액인 totalPurchaseAmount
를 상태로 가져야 할지 고민했는데, orderMenu
와 date
는 Customer
클래스를 만들어서 따로 가지게 하는게 낫겠다는 판단 하에 아래와 같이 작성했다.
public class Order {
private final HashMap<String, Integer> appetizers;
private final HashMap<String, Integer> dessert;
private final HashMap<String, Integer> drink;
private final HashMap<String, Integer> mainDish;
private final int totalPurchaseAmount;
public Order(HashMap<String, Integer> appetizers,
HashMap<String, Integer> dessert,
HashMap<String, Integer> drink,
HashMap<String, Integer> mainDish) {
this.appetizers = appetizers;
this.dessert = dessert;
this.drink = drink;
this.mainDish = mainDish;
this.totalPurchaseAmount = calculateTotalPurchaseAmount();
}
private int calculateTotalPurchaseAmount() {
int appetizerAmount = Appetizer.getTotalAmount(appetizers);
int dessertAmount = Dessert.getTotalAmount(dessert);
int drinkAmount = Drink.getTotalAmount(drink);
int mainDishAmount = MainDish.getTotalAmount(mainDish);
return appetizerAmount + dessertAmount + drinkAmount + mainDishAmount;
}
public int getTotalPurchaseAmount() {
return totalPurchaseAmount;
}
}
그리고 Menu
인터페이스를 만들었다. 처음에는 검증 메서드 void validate()
메서드 하나를 가지는 인터페이스였는데, 구현을 해가면서 처음 생각했던 것과 다르게 '음식 메뉴' 라는 것을 알리는 인터페이스로만 구현이 되었다.
public interface Menu {
}
이 Menu 인터페이스는 Appetizer
, Dessert
, Drink
, MainDish
등 요구사항에서 정해진 종류의 메뉴를 가지는 enum 클래스들이 구현한다.
나는 에피타이저, 디저트, 음료, 메인 요리를 하나의 Menu
클래스 안에 두고 싶지 않았다. 지금이야 메뉴 수가 적지만, 나중에 메뉴가 100개로 늘어난다면? 분명히 음식의 종류별로 enum 클래스를 가져야 할 필요가 생길 것 같았다. 그리고 요구사항 중에 이런 조건이 있다.
음료만 주문할 수는 없다
그래서 이런 조건을 충족하기 위해서도 음식 종류별로 enum 클래스를 구현할 필요가 있을 것 같았다.
물론 한 Menu 클래스안에 다 구현해놓으면 복잡한 로직이 필요가 없다.
public enum Menu {
티본스테이크(55_000, 메인),
바비큐립(54_000, 메인),
제로콜라(3_000, 음료),
양송이수프(6_000, 에피타이저);
...
이렇게 구현하면 음식의 종류 값도 꺼내오기는 쉽다. 하지만 난 위의 이유로 음식의 종류대로 분리가 필요하다고 생각했고, 이것들이 각각의 살아있는 '객체' 이어야 된다고 생각했다. 그게 더욱 객체의 책임을 명확히 설정해줄 것 같았다.
아래는 여러 종류별 음식 클래스 중 MainDish
클래스를 구현한 것이다.
import java.util.HashMap;
import java.util.Map.Entry;
public enum MainDish implements Menu {
티본스테이크(55_000),
바비큐립(54_000),
해산물파스타(35_000),
크리스마스파스타(25_000);
private final int amount;
MainDish(int amount) {
this.amount = amount;
}
public int getAmount() {
return amount;
}
public static int getTotalAmount(HashMap<String, Integer> orderedMenuQuantities) {
int totalAmount = 0;
for (Entry<String, Integer> menu : orderedMenuQuantities.entrySet()) {
totalAmount += valueOf(menu.getKey()).getAmount() * menu.getValue();
}
return totalAmount;
}
}
enum 클래스 내부에 구현된 getTotalAmount()
메서드는 메뉴 종류별 주문 목록
을 받아 해당 종류의 메뉴들의 금액을 합산해서 반환한다.
전략 패턴은 뭔가 다른 쓸만한 패턴이 없나 - 하면서 찾아보고 학습하다 발견하게 된 패턴이다. 각기 다른 대상에 따라 다른 전략을 적용하게 하는 패턴인데, 주로 실시간으로 바뀌는 대상이 있을 때 적용하는 패턴이다.
나는 이 패턴을 입력된 메뉴들을 각각의 종류(에피타이저, 디저트 등)로 구분하기 위해 사용했다.
import java.util.HashMap;
public interface MenuStrategy {
boolean acceptMenuName(String menuName);
void putMenu(HashMap<String, Integer> orderedMenuQuantities, String menuName, int quantity);
}
acceptMenuName()
은 입력된 메뉴가 해당 종류의 enum 클래스에서 가지고 있는지 판단하고, putMenu()
는 종류별 Map 자료구조에 메뉴 이름과 수량을 put 해준다.
import christmas.domain.menu.MainDish;
import java.util.HashMap;
public class MainDishStrategy implements MenuStrategy {
@Override
public boolean acceptMenuName(String menuName) {
return MenuPresenceChecker.isEnumPresent(MainDish.class, menuName);
}
@Override
public void putMenu(HashMap<String, Integer> orderedMenuQuantities, String menuName, int quantity) {
orderedMenuQuantities.put(menuName, quantity);
}
}
public class MenuPresenceChecker {
public static <T extends Enum<T> & Menu> boolean isEnumPresent(Class<T> enumClass, String enumName) {
return Arrays.stream(enumClass.getEnumConstants())
.anyMatch(e -> e.name().equals(enumName));
}
}
MenuPresenceChecker
클래스는 특이한 제네릭을 가진다.
public static <T extends Enum<T> & Menu>
나는 이 정적 메서드가 Menu
인터페이스를 구현하는 Enum
클래스들에게만 사용되게 하고 싶었고, 그를 위한 제네릭을 설정했다. 제네릭에도 이런 조건을 설정할 수 있다는 것을 학습하면서 처음 알게 됐다.
isEnumPresent(Class<T> enumClass, String enumName)
isEnumPresent()
메서드는 Class<T>
라는 타입의 매개변수를 받는다.
이는 Menu 인터페이스를 구현하는 Enum 클래스만을 매개변수로 받기 위해서 사용했다.
오늘 한건 여기까지다. 설계에 시간을 좀 쓰기도 했지만,
를 고민하고 해결하기 위해 정말 많은 시간을 썼다.
그래도 마음에 드는 코드 작성이었다. 3주차에는 시간이 없어 일단 동작하게 하고 이후에 리팩토링을 진행했는데,
지금은 시간이 많으니 더 좋은 설계, 더 좋은 구현, 더 좋은 객체지향을 고민할 수 있었다. 특히 전략 패턴을 사용한 부분과 메뉴별 enum 클래스가 스스로 주문 금액을 계산하고, Order
클래스가 스스로 총 주문 금액을 계산하는 부분은 참 맘에 든다.
좋은 애플리케이션을 만들기 위해 많은 고민을 할 수 있었던 것
객체지향적으로 맘에 드는 코드를 작성한 것
인터페이스를 이전보다 더 자연스럽게 사용하게 된 것
이전의 내게 인터페이스는 뭔가 두려운(?) 미지의 영역이었다.
도메인 객체가 정말 객체다운 객체가 되기 위한 설계, 고민을 한 것
오늘은 없다. 비록 구현한 코드는 적지만 내 결과물에 매우 만족한다!
| [Java8 Time API] LocalDate, LocalTime, LocalDateTime 사용법
| [item 38] 확장할 수 있는 Enum Type이 필요하면 인터페이스를 사용하라
| ☕ 자바 Enum 열거형 타입 문법 & 응용 💯 정리