[OOP] 우아한 객체지향 리뷰 (1단계)

청포도봉봉이·2024년 5월 9일
1

우아한 객체지향

목록 보기
1/3

우아한테크세미나에서 조영호님께서 발표해주신 강의를 정리했습니다.

강의 링크

1단계 코드

우아한 객체지향 1단계 리뷰

설계란? 코드를 어떻게 배치할 것인거에 대한 의사결정, 어떤 클래스 또는 패키지에 어떤 코드를 넣을거고 이거에 따라서 설계 모양이 바뀐다.

그렇다면 어떤 곳에 어떤 코드를 넣어야 할까? 변경에 초점을 맞춰야 한다.

Dependency (의존성)

B가 변경될때 A가 변경될 가능성이 있는걸 의존성이라고 한다.

Dependency란 변경에 의해서 영향을 받을 수 있는 가능성이다.

클래스간의 의존성

  1. 연관 관계 (Association)

class A {
	private B b;
}
  1. 의존 관계 (Dependency)

class A {
	public B method(B b) {
    	return new B();
    }
}
  • A에서 B 객체를 파라미터, 리턴 타입, 메서드 안에서 인스턴스를 생성하는 경우
  • 일시적으로 협력을 하는 시점에만 만나고 헤어지는 관계
  1. 상속 관계 (Inheritance)

class A extends B {
	...
}
  1. 실체화 관계 (Realization)

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를 의존하는걸 피해야 한다.

즉 무조건 패키지 사이클이 생기지 않도록 해야한다.

예제 살펴보기

도메인 컨셉 - 가게 & 메뉴

위의 상황이 런타임 상황에선 아래와 같이 실행된다.

도메인 오브젝트 - 가게 & 메뉴

위 가게 & 메뉴에서 중요한건 가게에 메뉴가 있고 그에 따른 옵션 그룹이 있고 옵션 그룹에 해당하는 옵션들이 있다는 것이다.

도메인 컨셉 - 주문

도메인 오브젝트 - 주문

실제 주문을 했다면 위와 같은 상태로 펼쳐질 것이다.

도메인 오브젝트 - 메뉴 & 주문

런타임의 객체들이 위와 같이 엮이게 될 것이다.

문제점 - 메뉴 선택

위와 같이 사장님이 메뉴를 등록했고 옵션 그룹과 옵션을 등록한 상황이다.

문제점 - 장바구니 담기

장바구니의 내역은 핸드폰 로컬에 저장한다. 그래서 핸드폰을 바꾸면 장바구니 내역이 사라진다.

이 상황에서 만약 사장님이 아래와 같이 메뉴와 옵션을 바꾸게 된다면 장바구니 메뉴와 사장님이 판매하신 메뉴는 불일치하게 될 것이다.

문제점 - 메뉴 불일치

주문이 발생할때마다 주문이 전송되는 데이터와 사장님이 등록한 메뉴가 일치하는지를 검증해야 한다.

(실제론 엄청 많은 데이터를 검증한다.)

주문 Validation

협력 설계하기

  1. 메뉴의 이름과 주문항목의 이름 비교
  2. 옵션 그룹의 이름과 주문 옵션 그룹의 이름 비교
  3. 옵션의 이름과 주문 옵션의 이름 비교
  4. 옵션의 가격과 주문 옵션의 가격 비교
  5. 가게가 영업중인지 확인
  6. 주문금액이 최소주문금액 이상인지 확인

  1. 주문
  • 주문하기() 메시지 또는 플로우 실행
  1. 가게
  • 가게가 영업중인지 확인
  • 주문 금액이 최소 주문 금액 이상인지 확인
  1. 주문 항목
  • 주문 항목을 메뉴와 일치하는지 검증한다.
  1. 메뉴
  • 옵션그룹에 검증 요청
  1. 옵션 그룹
  • 옵션 그룹 이름과 주문옵션 그룹의 이름을 비교
  1. 옵션
  • 옵션의 이름과 주문 옵션의 이름 비교
  • 옵션의 가격과 주문 옵션의 가격 비교

위와 같은 상황이 되었을때 주문이 완료된 상황이라고 가정한다.

클래스 다이어그램

클래스 다이어그램에 대한 코드는 위 링크에서 볼 수 있다.

  • 객체간의 관계에는 방향성이 필요하다.
    • 어떤 식으로 방향이 흐를지 생각해야 함 (굉장히 중요하다)

  • 관계의 방향 = 협력의 방향 = 의존성의 방향
  • 관계의 종류 결정하기
    • 연관관계: 협력을 위해 필요한 영구적인 탐색 구조

  • 의존관계: 협력을 위해 일시적으로 필요한 의존성 (파라미터, 리턴타입, 지역변수)

연관관계 = 탐색 가능성

  • Order안에 OrderLineItem이 존재하므로 Order를 통해 OrderLineItem을 가져올 수 있다.

  • 두 객체 사이에 협력이 필요하고 두 객체의 관계가 영구적이라면 연관관계를 이용해 탐색 경로를 구현한다.
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 메서드로 구현된다는 것이다.

Shop & OrderLineIttem 연관관계 연결

place()라는 메세지가 호출됐다면 이제 객체 협력을 시작하는 것이다.

또한 Order 객체는 객체 협력을 위한 ShopOrderLineItem 와 연관관계 연결이 되어 있어야 한다.

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() {
    
    }    
}

위 코드는 아래 그림의 상황이라고 볼 수 있다.

Shop Validation

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에 관한 코드를 그림으로 나타내면 아래와 같다.

레이어 아키텍처

현재까지의 작업은 도메인 영역에서의 관계에서의 코드를 구현한 것이다.

도메인 영역에 대한 작업이 끝났다면 우린 아래와 같이 ServiceInfrastructure 영역의 코드를 작성할 것이다.

여기까지가 전체적인 주문하기 기능의 코드이다.

profile
서버 백엔드 개발자

0개의 댓글