우아한테크세미나에서 조영호님께서 발표해주신 강의를 정리했습니다.
설계란? 코드를 어떻게 배치할 것인거에 대한 의사결정, 어떤 클래스 또는 패키지에 어떤 코드를 넣을거고 이거에 따라서 설계 모양이 바뀐다.
그렇다면 어떤 곳에 어떤 코드를 넣어야 할까? 변경에 초점을 맞춰야 한다.
B가 변경될때 A가 변경될 가능성이 있는걸 의존성이라고 한다.
Dependency란 변경에 의해서 영향을 받을 수 있는 가능성이다.
class A {
private B b;
}
class A {
public B method(B b) {
return new B();
}
}
class A extends B {
...
}
class A implements B {
...
}
인터페이스를 구현하는 관계이다.
패키지 A가 패키지 B를 의존한다고 하면 패키지의 B의 클래스가 바뀌면 패키지 A가 바뀌는 것
쉽게 클래스를 열었을때 B 패키지의 클래스가 import 되어있다면 의존하는 것이다.
class A {
private B b;
public void setA(B b) {
this.b = b;
this.b.setA(this);
}
}
class B {
private A a;
public void setA(A a) {
this.a = a;
}
}
위 코드는 의존성의 정의를 생각해보면 A
가 변경될때 B
가 바뀌고 B
가 바뀌면 A
가 바뀐다. 이런 코드는 하나의 클래스를 억지로 찢어놓은 것이라고 할 수 있고 여러 가지 문제들이 발생할 수 있다. 이런 경우 아래와 같이 단방향 의존관계로 바꿔주자.
class A {
private B b;
public void setA(B b) {
this.b = b;
}
}
class A {
private Collection<B> bs;
}
위의 코드처럼 클래스를 컬렉션을 가지는 것보단 아래 코드처럼 작성하자.
class A {
}
class B {
private A a;
}
class A {
private B b;
}
class B {
}
class A {
}
class B {
}
패키지 A가 패키지 B를 의존하고, 패키지 B가 패키지 A를 의존하는걸 피해야 한다.
즉 무조건 패키지 사이클이 생기지 않도록 해야한다.
위의 상황이 런타임 상황에선 아래와 같이 실행된다.
위 가게 & 메뉴에서 중요한건 가게에 메뉴가 있고 그에 따른 옵션 그룹이 있고 옵션 그룹에 해당하는 옵션들이 있다는 것이다.
실제 주문을 했다면 위와 같은 상태로 펼쳐질 것이다.
런타임의 객체들이 위와 같이 엮이게 될 것이다.
위와 같이 사장님이 메뉴를 등록했고 옵션 그룹과 옵션을 등록한 상황이다.
장바구니의 내역은 핸드폰 로컬에 저장한다. 그래서 핸드폰을 바꾸면 장바구니 내역이 사라진다.
이 상황에서 만약 사장님이 아래와 같이 메뉴와 옵션을 바꾸게 된다면 장바구니 메뉴와 사장님이 판매하신 메뉴는 불일치하게 될 것이다.
주문이 발생할때마다 주문이 전송되는 데이터와 사장님이 등록한 메뉴가 일치하는지를 검증해야 한다.
(실제론 엄청 많은 데이터를 검증한다.)
주문
가게
주문 항목
메뉴
옵션 그룹
옵션
위와 같은 상황이 되었을때 주문이 완료된 상황
이라고 가정한다.
클래스 다이어그램에 대한 코드는 위 링크에서 볼 수 있다.
class Order {
private List<OrderLineItem> orderLineItems;
public void place() {
validate();
ordered();
}
private void validate() {
...
// 연관관계를 통해 협력한다.
for (OrderLineItem orderLineItem : orderLineItems) {
orderLineItem.validate();
}
}
}
public class Order {
public void place() {
validate();
ordered();
};
private void validate() {
}
private void ordered() {
}
}
객체가 어떤 메시지를 받는다는건 그 객체에 public
메서드로 구현된다는 것이다.
place()
라는 메세지가 호출됐다면 이제 객체 협력을 시작하는 것이다.
또한 Order
객체는 객체 협력을 위한 Shop
과 OrderLineItem
와 연관관계 연결이 되어 있어야 한다.
public class Order {
...
@ManyToOne
@JoinColumn(name="SHOP_ID")
private Shop shop;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name="ORDER_ID")
private List<OrderLineItem> orderLineItems = new ArrayList<>();
...
public void place() {
validate();
ordered();
};
private void validate() {
}
private void ordered() {
}
}
위 코드는 아래 그림의 상황이라고 볼 수 있다.
class Order {
public void place() {
validate();
ordered();
}
private void validate() {
if (orderLineItems.isEmpty()) {
throw new IllegalStateException("주문 항목이 비어 있습니다.");
}
if (!shop.isOpen()) {
throw new IllegalArgumentException("가게가 영업중이 아닙니다.");
}
if (!shop.isValidOrderAmount(calculateTotalPrice())) {
throw new IllegalStateException(String.format("최소 주문 금액 %s 이상을 주문해주세요.", shop.getMinOrderAmount()));
}
for (OrderLineItem orderLineItem : orderLineItems) {
orderLineItem.validate();
}
}
private void ordered() {
this.orderStatus = OrderStatus.ORDERED;
}
}
class Shop {
...
public boolean isValidOrderAmount(Money amount) {
return amount.isGreaterThanOrEqual(minOrderAmount);
}
public void open() {
this.open = true;
}
...
}
위 코드들을 그림으로 보면 아래와 같다.
class Order {
private void validate() {
...
for (OrderLineItem orderLineItem : orderLineItems) {
orderLineItem.validate();
}
...
}
}
class OrderLineItem {
...
public void validate() {
menu.validateOrder(name, convertToOptionGroups());
}
...
}
class Menu{
...
public void validateOrder(String menuName, List<OptionGroup> optionGroups) {
if (!this.name.equals(menuName)) {
throw new IllegalArgumentException("기본 상품이 변경됐습니다.");
}
if (!isSatisfiedBy(optionGroups)) {
throw new IllegalArgumentException("메뉴가 변경됐습니다.");
}
}
private boolean isSatisfiedBy(List<OptionGroup> cartOptionGroups) {
return cartOptionGroups.stream().anyMatch(this::isSatisfiedBy);
}
private boolean isSatisfiedBy(OptionGroup group) {
return optionGroupSpecs.stream().anyMatch(spec -> spec.isSatisfiedBy(group));
}
...
}
class OptionGroupSpecification {
...
public boolean isSatisfiedBy(OptionGroup optionGroup) {
return !isSatisfied(optionGroup.getName(), satisfied(optionGroup.getOptions()));
}
private boolean isSatisfied(String groupName, List<Option> satisfied) {
if (!name.equals(groupName)) {
return false;
}
if (satisfied.isEmpty()) {
return false;
}
if (exclusive && satisfied.size() > 1) {
return false;
}
return true;
}
...
}
위의 전체적인 flow에 관한 코드를 그림으로 나타내면 아래와 같다.
현재까지의 작업은 도메인 영역에서의 관계에서의 코드를 구현한 것이다.
도메인 영역에 대한 작업이 끝났다면 우린 아래와 같이 Service
와 Infrastructure
영역의 코드를 작성할 것이다.
여기까지가 전체적인 주문하기 기능의 코드이다.