음식점에서 음식을 주문하는 프로세스를 구현하기 위한 도메인 모델링 과정을 요약하면 다음과 같습니다:
1. 도메인 구성 객체 식별
- 도메인 내 객체: 음식점의 주문 프로세스에서 중요한 역할을 하는 객체를 식별합니다. 이에는
손님
,메뉴판
, 다양한음식
(예: 돈까스, 냉면, 만두),요리사
,요리
가 포함됩니다.
2. 객체 간 관계 분석
- 손님과 메뉴판: 손님은 메뉴판을 통해 음식을 선택합니다.
- 손님과 요리사: 손님은 요리사에게 음식을 요청(주문)합니다.
- 요리사와 요리: 요리사는 손님의 주문에 따라 요리를 준비합니다.
3. 도메인 모델링과 추상화
- 객체 추상화: 식별된 객체들을 정적인 타입으로 추상화하여, 각 객체의 역할과 책임을 명확히 합니다.
손님
타입: 주문하는 역할을 담당합니다.요리
타입: 돈까스, 냉면, 만두 등의 구체적인 요리를 추상화합니다.메뉴판
타입: 사용 가능한 메뉴를 리스트업합니다.메뉴
타입: 개별 메뉴 항목을 나타냅니다.
4. 협력 설계
- 객체 간 협력: 객체들이 어떻게 협력하여 목표를 달성할지 설계합니다. 예를 들어, 손님이 메뉴판에서 음식을 선택하고, 이 정보를 바탕으로 요리사가 요리를 준비하는 과정을 정의합니다.
5. 책임 할당
- 타입별 책임: 각 객체(타입)에 적절한 책임을 할당하여, 시스템의 유연성과 확장성을 보장합니다. 예를 들어,
메뉴판
은 사용자에게 선택 가능한 메뉴를 제공하는 책임을 가집니다.
6. 구현
- 시스템 구현: 위 단계에서 정의된 객체, 관계, 책임을 바탕으로 실제 시스템을 구현합니다. 이 과정에서 프로그래밍 언어와 프레임워크 선택, 데이터베이스 설계, 인터페이스 개발 등이 포함됩니다.
이러한 과정을 통해, 음식 주문 시스템의 요구 사항을 충족시키는 효과적이고 유지보수가 용이한 소프트웨어 아키텍처를 설계하고 구현할 수 있습니다.
Java
테스트 코드
package org.example.restaurant.domain;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatCode;
public class DishTest {
@Test
@DisplayName("Given Dish 이름과 가격이 주어졌을 때, When Dish 인스턴스를 생성하면, Then 예외가 발생하지 않는다.")
void createTest() {
// Given
String name = "만두";
int price = 5000;
// When & Then
assertThatCode(() -> new Dish(name, price))
.doesNotThrowAnyException();
}
}
package org.example.restaurant.domain;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class MenuOptionTest {
@Test
@DisplayName("Given 메뉴 항목 이름과 가격이 주어졌을 때, When MenuOption 인스턴스를 생성하면, Then 예외가 발생하지 않는다.")
void createTest() {
// Given
String name = "만두";
int price = 5000;
// When & Then
Assertions.assertThatCode(() -> new MenuOption(name, price))
.doesNotThrowAnyException();
}
}
package org.example.restaurant.domain;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
public class MenuTest {
@Test
@DisplayName("Given 메뉴에 여러 메뉴 항목이 있을 때, When 특정 메뉴 항목을 선택하면, Then 해당 메뉴 항목이 반환된다.")
void createMenuTest() {
// Given
Menu menu = new Menu(List.of(
new MenuOption("돈까스", 5000),
new MenuOption("우동", 5000),
new MenuOption("스시", 10000)
));
// When
MenuOption selectedOption = menu.choose("돈까스");
// Then
Assertions.assertThat(selectedOption).isEqualTo(new MenuOption("돈까스", 5000));
}
@Test
@DisplayName("Given 메뉴에 여러 메뉴 항목이 있을 때, When 특정 메뉴 항목을 선택하면, Then IllegalArgumentException 예외 객체가 반환된다.")
void chooseTest2() {
// Given
Menu menu = new Menu(List.of(
new MenuOption("돈까스", 5000),
new MenuOption("우동", 5000),
new MenuOption("스시", 10000)
));
// When && Then
Assertions.assertThatCode(() -> menu.choose("통닭"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("해당 메뉴아이템은 없습니다.");
}
}
package org.example.restaurant.domain;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class CookingServiceTest {
@Test
@DisplayName("Given 메뉴 항목이 주어졌을 때, When CookingService를 사용해서 요리를 만들면, Then 해당하는 Dish가 생성된다.")
void makeCookTest() {
// Given
CookingService cookingService = new CookingService();
MenuOption menuOption = new MenuOption("돈까스", 5000);
// When
Dish dish = cookingService.makeCook(menuOption);
// Then
Assertions.assertThat(dish).isEqualTo(new Dish("돈까스", 5000));
}
}
package org.example.restaurant.domain;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
public class CustomerTest {
@Test
@DisplayName("Given 메뉴와 CookingService가 주어졌을 때, When Customer가 메뉴 항목을 주문하면, Then 예외가 발생하지 않는다.")
void orderTest() {
// Given
Menu menu = new Menu(List.of(
new MenuOption("돈까스", 5000),
new MenuOption("우동", 5000),
new MenuOption("스시", 10000)
));
CookingService cookingService = new CookingService();
Customer customer = new Customer();
// When & Then
Assertions.assertThatCode(() -> customer.order("스시", menu, cookingService))
.doesNotThrowAnyException();
}
}
구현 코드
package org.example.restaurant.domain;
/**
* 음식 준비와 관련된 서비스를 제공하는 클래스입니다.
*/
public class CookingService {
/**
* 메뉴 옵션에 해당하는 요리(Dish)를 만듭니다.
*
* @param menuOption 메뉴 옵션
* @return 만들어진 Dish 객체
*/
public Dish makeCook(MenuOption menuOption) {
Dish dish = new Dish(menuOption);
return dish;
}
}
package org.example.restaurant.domain;
/**
* 음식점의 고객을 나타내는 클래스입니다.
*/
public class Customer {
/**
* Customer 객체 생성자.
*/
public Customer() {
}
/**
* 메뉴 이름을 사용하여 주문을 합니다. 주문된 음식은 CookingService를 통해 준비됩니다.
*
* @param menuName 메뉴 이름
* @param menu 메뉴 객체
* @param cookingService 요리 서비스
*/
public void order(String menuName, Menu menu, CookingService cookingService) {
MenuOption menuOption = menu.choose(menuName);
Dish dish = cookingService.makeCook(menuOption);
}
}
package org.example.restaurant.domain;
import java.util.Objects;
/**
* 요리(음식)를 나타내는 클래스입니다.
*/
public class Dish {
private final String name;
private final int price;
/**
* Dish 객체 생성자.
*
* @param name 요리 이름
* @param price 요리 가격
*/
public Dish(String name, int price) {
this.name = name;
this.price = price;
}
/**
* MenuOption 객체를 사용한 Dish 객체 생성자.
*
* @param menuOption 메뉴 옵션
*/
public Dish(MenuOption menuOption) {
this.name = menuOption.getName();
this.price = menuOption.getPrice();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Dish dish = (Dish) o;
return price == dish.price && Objects.equals(name, dish.name);
}
@Override
public int hashCode() {
return Objects.hash(name, price);
}
}
package org.example.restaurant.domain;
import java.util.List;
/**
* 음식점의 메뉴를 나타내는 클래스입니다. 메뉴 옵션의 목록을 관리합니다.
*/
public class Menu {
private final List<MenuOption> menuOptions;
/**
* Menu 객체 생성자.
*
* @param menuOptions 메뉴 옵션 목록
*/
public Menu(List<MenuOption> menuOptions) {
this.menuOptions = menuOptions;
}
/**
* 메뉴 이름에 해당하는 메뉴 옵션을 선택합니다.
*
* @param name 메뉴 이름
* @return 선택된 MenuOption 객체
* @throws IllegalArgumentException 메뉴 이름이 목록에 없는 경우
*/
public MenuOption choose(String name) {
return this.menuOptions.stream()
.filter(menuItem ->menuItem.matches(name))
.findFirst()
.orElseThrow(()->new IllegalArgumentException("해당 메뉴아이템은 없습니다."));
}
}
package org.example.restaurant.domain;
import java.util.Objects;
/**
* 메뉴 옵션을 나타내는 클래스입니다. 각 메뉴 옵션은 이름과 가격을 가집니다.
*/
public class MenuOption {
private final String name;
private final int price;
/**
* MenuOption 객체 생성자.
*
* @param name 메뉴 옵션 이름
* @param price 메뉴 옵션 가격
*/
public MenuOption(String name, int price) {
this.name=name;
this.price=price;
}
/**
* 제공된 이름이 이 메뉴 옵션의 이름과 일치하는지 확인합니다.
*
* @param name 확인할 이름
* @return 이름이 일치하면 true, 그렇지 않으면 false
*/
public boolean matches(String name){
return this.name.equals(name);
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MenuOption menuOption = (MenuOption) o;
return price == menuOption.price && Objects.equals(name, menuOption.name);
}
@Override
public int hashCode() {
return Objects.hash(name, price);
}
}
코드 설명
위의 코드는 자바로 구현된 음식 주문 시스템의 주요 컴포넌트를 나타냅니다. 시스템은 음식 주문 과정을 처리하기 위해 CookingService
, Customer
, Dish
, Menu
, MenuOption
클래스를 포함합니다. 각 클래스는 음식점 도메인의 다양한 역할과 책임을 나타내며, 이들은 상호 작용하여 사용자가 메뉴 항목을 선택하고 주문할 수 있게 합니다.
CookingService 클래스
CookingService
는 메뉴 옵션에 기반하여 요리(Dish
)를 만드는 서비스를 제공합니다.makeCook
메서드는MenuOption
객체를 받아 해당 정보를 기반으로 새로운Dish
객체를 생성하고 반환합니다.
Customer 클래스
Customer
클래스는 음식점의 고객을 나타냅니다. 고객은order
메서드를 통해 메뉴 이름과Menu
,CookingService
객체를 사용하여 주문을 합니다. 이 메서드는 선택된 메뉴 옵션을 기반으로 요리를 준비하는 과정을 처리합니다.
Dish 클래스
Dish
클래스는 주문된 요리를 나타냅니다. 요리는 이름과 가격을 속성으로 가지며,MenuOption
을 기반으로 새로운Dish
인스턴스를 생성할 수 있는 또 다른 생성자도 제공합니다.
Menu 클래스
Menu
클래스는 메뉴 옵션의 목록을 관리하며, 메뉴 이름에 해당하는 메뉴 옵션을 선택하는 기능을 제공합니다.choose
메서드는 주어진 메뉴 이름에 일치하는MenuOption
객체를 찾아 반환하고, 만약 해당 이름의 메뉴 옵션이 없는 경우 예외를 발생시킵니다.
MenuOption 클래스
MenuOption
클래스는 메뉴 옵션을 나타냅니다. 각 메뉴 옵션은 고유한 이름과 가격을 가지며,matches
메서드는 제공된 이름이 메뉴 옵션의 이름과 일치하는지 여부를 확인합니다.
테스트 코드
테스트 코드는 JUnit
과 AssertJ
라이브러리를 사용하여 각 클래스의 주요 기능을 검증합니다. DishTest
, MenuOptionTest
, MenuTest
, CookingServiceTest
, CustomerTest
클래스는 각각 Dish
, MenuOption
, Menu
, CookingService
, Customer
클래스의 기능을 테스트하며, 이를 통해 주어진 조건에서 예외가 발생하지 않거나, 예상된 객체가 생성되거나, 적절한 예외가 발생하는지 등을 검증합니다.
이 코드는 객체 지향 프로그래밍 원칙을 따르며, 음식 주문 시스템의 핵심 로직을 모듈화하여 구현합니다. 각 클래스는 특정 책임을 가지며, 시스템의 다른 부분과 명확하게 상호 작용합니다.
Python
from dish_order.domain.dish import Dish
from dish_order.domain.menu_option import MenuOption
class CookingService:
"""음식 준비와 관련된 서비스를 제공한다. """
def make_cook(self, menu_option: MenuOption) -> Dish:
"""
제공된 메뉴 옵션을 바탕으로 요리(Dish)를 만듭니다.
:param MenuOption menu_option: 요리를 만드는 데 사용될 메뉴 옵션
:return: 메뉴 옵션을 바탕으로 만들어진 Dish 객체
:rtype: Dish
"""
return Dish.from_menu_option(menu_option)
from dish_order.domain.cooking_service import CookingService
from dish_order.domain.dish import Dish
from dish_order.domain.menu import Menu
from dish_order.domain.menu_option import MenuOption
class Customer:
"""음식점의 고객을 나타냅니다."""
def __init__(self):
"""
Customer 인스턴스를 초기화합니다.
"""
pass
def order(self, menu_name: str, menu: Menu, cooking_service: CookingService) -> None:
"""
메뉴 이름을 사용하여 주문을 합니다. 주문된 음식은 CookingService를 통해 준비됩니다.
:param str menu_name: 주문할 메뉴의 이름
:param Menu menu: 메뉴 객체
:param CookingService cooking_service: 음식 준비 서비스 객체
"""
menu_option: MenuOption = menu.choose(menu_name)
dish: Dish = cooking_service.make_cook(menu_option)
print(f"Ordered dish: {dish.name} for {dish.price} price")
from dish_order.domain.menu_option import MenuOption
class Dish:
"""요리(음식)를 나타냅니다."""
def __init__(self, name: str, price: int):
"""
Dish 객체 생성자.
:param str name: 요리의 이름
:param int price: 요리의 가격
"""
self.name = name
self.price = price
@classmethod
def from_menu_option(cls, menu_option: MenuOption) -> 'Dish':
"""
MenuOption 인스턴스를 사용하여 Dish 인스턴스를 생성합니다.
:param MenuOption menu_option: Dish 생성에 사용될 메뉴 옵션
:return: 생성된 Dish 객체
:rtype: Dish
"""
return cls(menu_option.name, menu_option.price)
def __eq__(self, other):
"""Dish 인스턴스 간의 동등성을 비교합니다."""
if not isinstance(other, Dish):
return NotImplemented
return self.name == other.name and self.price == other.price
def __repr__(self):
return f"Dish(name={self.name}, price={self.price})"
from dish_order.domain.menu_option import MenuOption
class Menu:
"""음식점의 메뉴를 나타냅니다."""
def __init__(self, menu_options: list[MenuOption]):
"""
Menu 객체 생성자.
:param list[MenuOption] menu_options: 메뉴 옵션 객체의 리스트
"""
self.menu_options = menu_options
def choose(self, name: str) -> MenuOption:
"""
메뉴 이름에 해당하는 메뉴 옵션을 선택합니다.
:param str name: 선택하고자 하는 메뉴 옵션의 이름
:return: 선택된 메뉴 옵션 객체
:rtype: MenuOption
:raises ValueError: 해당 이름의 메뉴 옵션이 없는 경우 오류를 발생시킵니다.
"""
for option in self.menu_options:
if option.matches(name):
return option
raise ValueError("해당하는 메뉴아이템은 없습니다.")
class MenuOption:
"""메뉴 옵션을 나타냅니다."""
def __init__(self, name:str, price:int):
"""
MenuOption 객체 생성자.
:param str name: 메뉴 옵션의 이름
:param int price: 메뉴 옵션의 가격
"""
self.name = name
self.price = price
def matches(self, name):
"""
제공된 이름이 이 메뉴 옵션의 이름과 일치하는지 확인합니다.
:param str name: 확인하고자 하는 이름
:return: 이름이 일치하면 True, 그렇지 않으면 False를 반환합니다.
:rtype: bool
"""
return self.name == name
def __eq__(self, other):
"""MenuOption 인스턴스 간의 동등성을 비교합니다."""
if not isinstance(other, MenuOption):
return NotImplemented
return self.name == other.name and self.price == other.price
def __repr__(self):
return f"MenuOption(name={self.name}, price={self.price}"
테스트 코드
from dish_order.domain.cooking_service import CookingService
from dish_order.domain.dish import Dish
from dish_order.domain.menu_option import MenuOption
def test_make_cook():
# given
cooking_service = CookingService()
menu_option = MenuOption("돈까스", 5000)
# when
dish: Dish = cooking_service.make_cook(menu_option)
# then
assert dish == Dish("돈까스", 5000), "The dish should match the given menu option"
from dish_order.domain.cooking_service import CookingService
from dish_order.domain.customer import Customer
from dish_order.domain.menu import Menu
from dish_order.domain.menu_option import MenuOption
def test_order():
# given
menu:Menu = Menu([
MenuOption("돈까스", 5000),
MenuOption("탕수육", 5000),
MenuOption("우동", 4000)
])
cooking_service: CookingService = CookingService()
customer: Customer = Customer()
# when
# then
customer.order("돈까스", menu, cooking_service)
from dish_order.domain.dish import Dish
def test_dish_creation():
# given
name = "만두"
price = 5000
# when & then
dish:Dish = Dish(name, price)
assert dish.name == name and dish.price == price, "Dish should have the correct name and price"
from dish_order.domain.menu_option import MenuOption
def test_menu_option_creation():
# Given
name = "만두"
price = 5000
# When & Then
menu_option: MenuOption = MenuOption(name, price)
assert menu_option.name == name and menu_option.price == price, "MenuOption should have the correct name and price"
import pytest
from dish_order.domain.menu import Menu
from dish_order.domain.menu_option import MenuOption
def test_choose_menu_item():
# given
돈까스 = MenuOption("돈까스", 5000)
menu = Menu([
돈까스
])
# when
selected_option: MenuOption = menu.choose("돈까스")
# then
assert selected_option == 돈까스, "Should return the correct menu option"
def test_choose_invalid_menu_item():
# given
돈까스 = MenuOption("돈까스", 5000)
menu = Menu([
돈까스
])
# when & then
with pytest.raises(ValueError) as error:
menu.choose("통닭")
assert str(error.value) == '해당하는 메뉴아이템은 없습니다.', "Should raise ValueError for invalid menu item"
코드 설명
위 파이썬 코드는 음식 주문 시스템을 구현한 것으로, 각 클래스와 함수는 특정 역할을 수행하며, 파이썬의 특징을 활용해 구현되었습니다. 파이썬 코드의 특징과 핵심 요소는 다음과 같습니다:
파이썬의 특징 활용
- 타입 힌팅(Type Hinting): 파이썬 3.5 이상부터 도입된 타입 힌트를 사용하여, 함수의 매개변수와 반환값의 타입을 명시함으로써 코드의 가독성을 높이고, 개발자가 타입 관련 오류를 미리 인지할 수 있게 도와줍니다.
- 클래스 메서드(@classmethod):
Dish
클래스에서from_menu_option
과 같이 클래스 메서드를 사용하여, 인스턴스 대신 클래스 자체에 작업을 수행하게 함으로써, 다른 객체를 인자로 받아 해당 클래스의 인스턴스를 생성하는 팩토리 메서드 패턴을 구현합니다. __eq__
매직 메서드: 객체 비교를 위해__eq__
메서드를 오버라이드하여, 인스턴스 간의 동등성 비교를 사용자 정의 방식으로 구현합니다. 이는 테스트 코드에서 객체 비교 시 유용하게 사용됩니다.- 리스트 컴프리헨션과 조건문:
Menu
클래스의choose
메서드에서는 리스트 컴프리헨션과 조건문을 활용하지 않고, for 루프와 if 문을 통해 메뉴 옵션을 검색하고 있습니다. 파이썬에서는 이러한 패턴을 더 간결하게 리스트 컴프리헨션으로 표현할 수도 있습니다.
핵심 요소
- 명확한 에러 처리: 메뉴 이름에 해당하는 메뉴 옵션이 없을 경우
ValueError
를 발생시킴으로써, 함수의 사용자에게 명확한 에러 메시지를 제공합니다. 이는 파이썬에서 일반적으로 사용되는 예외 처리 방식을 따르고 있습니다. - 테스트 코드의 구성: 파이썬의
assert
문을 활용하여, 각 기능의 정상 작동을 검증합니다. 이는 파이썬의 단정문을 이용한 간결한 테스트 코드 작성 방식을 보여줍니다. 또한,pytest
라이브러리를 사용한 예외 처리 테스트에서는with
문과 함께 사용하여, 예외 상황을 명확하게 테스트하고 있습니다. - 객체 지향 프로그래밍(OOP)의 적용: 클래스를 이용해 도메인 모델을 구성하고, 상호작용하는 객체들 간의 관계를 통해 음식 주문 시스템의 비즈니스 로직을 구현합니다. 이는 파이썬에서 객체 지향 프로그래밍 패러다임을 어떻게 활용할 수 있는지 보여주는 좋은 예시입니다.
파이썬 코드에서 구현 시 나타나는 이러한 특징들은 파이썬의 유연성, 가독성 및 간결함을 잘 보여주며, 동시에 강력한 객체 지향 설계를 가능하게 합니다.
'여러가지 > 이것저것' 카테고리의 다른 글
[OOP]사칙연산 계산기 (1) | 2024.04.04 |
---|---|
OOP? (0) | 2024.04.03 |
AMQP? (0) | 2024.04.03 |
MQTT? (0) | 2024.04.03 |
커널(Kernel) (0) | 2024.03.22 |