우아한 객체지향 세미나를 보고..

Jeuk Oh·2023년 3월 25일
0

ㅋㅅㅋ

목록 보기
7/10

세미나 슬라이드
코드 자료

해당 영상을 보며 요약 및 생각 정리.
처음엔 보기만 하려다가 너무 좋은 영상이라 정리가 필요하다고 생각.


감상

1. 인상 깊었던 말들.

코드를 짜면 의존성을 직접 그려보아라.
추상적이란 말은 상대적으로 잘 안변하는 것을 의미한다.
개념을 구현할 수 있는 방법은 굉장히 많은데, 1대1로 생각하는 오해가 많다.
ex. '연관 관계'라는 개념이 있고, '객체 참조'라는 구현이 있고 '객체 참조'는 일반적인 방법이다.
계속 조회 로직을 추가하다 보면 알게 모르게 양방향 연관관계가 많이 생김.
계속 비즈니스 로직을 추가하다보면 객체 별로 트랜잭션이 걸리는 빈도가 달라지는 것 + 경합이 생길 수 있음을 주의.
객체를 묶는 기준은 비즈니스 요구사항에 의해 결정되는 것
디펜던시를 쫓아가다보면 자연스럽게 도메인 개념이 강화된다.
의존성을 따라 시스템을 진화시켜라.

2. 좋은 리펙토링 방향성

최근 회사에서 프로덕트 MVP 개발을 마치었는데, 만들 땐 재미있게 잘 만들었다고 생각했다가 요즘 보면 가끔 명쾌하게 설명하긴 어려운 찜찜함이 느껴질 때가 있다. 속된 말로 냄새나는 코드라고 해야하나, 다만 요구사항을 다 만족하였고, 이미 설계대로 잘 마무리 하여서 무엇이 문제인지, 어떻게 수정해야하는지를 파악하기가 어려웠다. 이 세미나를 들으면서 사내 프로덕트에 개선할 점을 발견할 수 있을 것 같아 기쁘다. 디펜던시를 그려보아야겠다. 코드 디자인 패턴을 내가 결정해서 특히 더 무게감이 크다. 다음에 기회가 되면 Django에서 쓴 디자인 패턴에 대한 포스팅을 해야겠다.

3. 도메인 기반 시스템 분리

반년 전쯤 배달의 민족의 MSA 전환기 를 보았었는데, 그 땐 그냥 뛰어난 아키텍처 분들이 TF 팀을 구성하여 처음부터 잘 설계해서 레거시를 버리고 MSA로 전환하였다고 생각했었다. 코드 설계 관점에서 보면 이러한 방법으로 레거시를 차근차근 보완하면서 MSA 도입을 준비하지 않았을까 싶다. 해당 영상을 다시 보면 또 배울 것이 많다고 생각한다.

4. 발표

원래 밥 먹으면서 잠깐 보려다가, 발표를 너무 잘하셔서 그대로 쭉 보게 되었다. 2시간 가까이 깊이 있는 세미나와 잘 정리된 자료를 보면서 참 감탄했다. 얼마 전 자그만한 사내 세미나를 해본 적이 있었는데, 쉬운 주제를 가지고도 발표 경험은 굉장히 실망스러웠던 기억이 있다. (준비 부족이었다.) 준비를 많이 하셨고 잘 이해하고 있다는 것이 잘 느껴지는, 흡입력 있는 세미나였다. 다음 세미나를 준비해야지.. 여러모로 자극을 많이 받는 영상이다.

원랜 강의를 다 듣고 간단하게 내 생각만 쓰려고 했다가, 어쩌다보니 강의를 계속 돌려들으면서 받아쓰게 되더라. 흠. 아직 당연하게도 체득되진 않은 모양이다.

간단하게 쓰려했는데, 또 오바했네~


요약

다 쓰고 보니 요약보단 필기 노트에 가까워서 아래로 뺍니당.
토글로 가리려고 했는데, Velog는 아직 지원을 안하는군요. 쩝

1. 의존성

개념 : A와 B가 있을 때 A가 B의 변경에 의한 영향을 받는다면 A는 B를 의존한다.
구현체 예제: A Class, Package 내에서 B Class, Package를 호출하거나 임포트하면 A는 B를 의존한다.

  1. 클래스 의존성의 종류
    (개념적인 이야기지 구현 이야기가 아님을 주의)
  • 연관관계 : 탐색 가능성. 영구적인 경로, 물리적인 경로, 객체 직접 참조 후 사용.

  • 의존관계 : 일시적인 경로, 메서드 인자, 메서드 내에서 객체 생성 후 리턴 등으로 객체 참조

  • 상속관계 : class 상속,

  • 실체화관계 : interface 상속

  1. 의존성을 고려한 설계 원칙
  • 양방향 의존성을 피하라
    양방향 의존 시에 두 객체는 그냥 같은 객체나 다름이 없다. 패키지에서 양방향 의존성 발생 시 cycle import 이슈도 있음.

  • 다중성이 적은 방향을 선택하라.
    List<B> = A.Bs 보단 A = B.A 가 좋다.

  • 의존성이 필요 없다면 제거하라.

2. 예제 - 배달앱

배달의 민족 앱을 기준으로 이용자가 '가게'에서 '메뉴''장바구니'에 담고 '주문' 시에 '주문완료'까지 과정을 구현한다 가정. 장바구니는 모바일 앱 로컬 스토리지에서 관리하여 BE에서는 가게, 메뉴, 주문, 3가지 도메인을 관리해야함.

  1. 도메인 컨셉
    먼저 기능 요구사항에 맞게 ERD를 그리고, 실제 리소스가 어떻게 서로를 참조하는지 예시를 그려본다.

    자세한 자료는 영상 및 슬라이드 에서 확인하자. (꼭 보세요!)
    해당 도표내 화살표가 데이터 관점에서 참조 방향을 의미함. (*는 ManyToOne)

     # 가게 도메인
     가게 <-* 메뉴 <-* 옵션 그룹 <-* 옵션
       ↑	   ↑
       *	   *
      주문 <-* 주문항목 <-* 주문 옵션 그룹 <-* 주문 옵션 
      # 주문 도메인
  코드에선 Domain을 정의하고 Package를 분리, Layer를 생각할 수 있는 단계
  1. 제약 사항 정의
    세부 요구 사항인 검증 되어야 하는 제약사항 요건을 정리하고, 어떤 객체끼리 협력해야하는 지 설계한다.

    주문을 가게에 전달하기 전,
    가게의 상태 및 최소주문금액에 대한 검증,
    가게가 등록한 메뉴 도메인 내 항목들과 사용자가 요청한 주문 도메인 내 항목들이 일치하는 지 검증하는 과정이 필요하다.
    (사용자가 메뉴를 장바구니에 담는 중 메뉴 및 가게 상태의 변경을 생각하면 검증이 필요함.)

      가게 영업 상태, 
      주문 가격과 가게의 최소 금액
      주문 항목과 메뉴
      옵션 그룹과 주문 옵션 그룹,
      옵션과 주문옵션 

    객체들의 각각 상호 검증이 필요한 상황.

  2. 협력 설계
    주문이 생성되었을 때 어떤 객체를 호출할 지 '기능 관점'에서 설계함.

    Class Diagram을 작성할 수 있는 단계.

    해당 도표내 화살표가 의존성의 방향을 의미함.
    예를 들어 주문 객체 내에서 주문항목 객체를 불러다가 쓴다. (일반 화살표는 연관관계, *는 의존관계를 의미함)

      가게 		 	메뉴 		->		옵션 그룹 		-> 		옵션
    
      ↑ (영업여부, 주문금액)   ↑ (메뉴와 주문 일치 검증) 	   ↓ (주문옵션그룹 가져오기, 		↓ (주문옵션 가져오기,
                                    옵션 그룹 일치 검증)		   옵션 일치 검증)
    
      주문		->	주문항목  	->		주문 옵션 그룹 		-> 		주문 옵션
           (항목 검증)										

    (그냥 스크린샷 찍을 껄 후회중)

  1. 관계의 종류 결정하기.
    3에서 정의한 의존성을 구체화하기 위해 어떤 관계를 사용할 지와 관계의 방향을 결정.
    영구적인 관계인지 일시적인 관계인지에 따라 직접 참조와 메서드의 인자 타입으로 사용할지를 결정함.
    절대적인 답은 없고, 어떻게 의존성을 구현할지도 여러가지 방법이 있다.
    결과는 3번 항목 도표에 같이 정리.

    Interface를 작성할 수 있는 단계.
  2. 구현
    3, 4에서 설계한 바를 기반으로 메서드를 실제로 구현한다. 예제 구현체는 강의 자료로 넘긴다.
    다만 해당 단계를 쭉 보면서 그래서 해당 설계의 문제가 뭘까 생각해보는 것이 재밌었다.

3. 설계 개선하기

  1. 의존성 살펴보기
    구현체가 실제로 어떻게 서로 의존하는지 살펴보니, Package Cycle이 있는 것을 확인할 수 있다.
    주문 Package에선 주문과 주문항목이 가게와 메뉴를 의존하는데,
    가게 Package에선 옵션과 옵션그룹이 주문옵션과 주문옵션그룹을 의존하면서 Cycle이 도는 것을 알 수 있다.
    한 Package가 수정이 되면 다른 Package도 수정되어야 하는 상황이다.
  1. Cycle 해결책 (중간 객체 활용)
    가게 Package 내 기존의 옵션과 옵션 그룹이 주문옵션과 주문옵션그룹을 바로 의존하는 것이 아닌,
    가게 Package 내 새로 정의된 추상화된 옵션 그룹을 보도록 하고
    주문 Package에서 가계 Package를 의존하여 가게 Pacakge에 정의된 Option을 반환하도록 하는 것.

      
       가게 		 	 메뉴 		->		옵션 그룹 		-> 		옵션  
      
       ↑ (영업여부, 주문금액)  ↑ (메뉴와 주문 일치 검증) 	   ↓ (주문옵션그룹 가져오기, 		↓ (주문옵션 가져오기,
                                      옵션 그룹 일치 검증)		   옵션 일치 검증)
                                      
       #가게 레이어		  			     옵션그룹 추상클래스		            옵션 추상클래스	
       --------------------------------------------------------------------------------------------------------------------
       #주문 레이어
                              ↑ (의존)			   ↑ (의존)
    
        주문		->	주문항목  	->		주문 옵션 그룹 		-> 		주문 옵션
             (항목 검증)		(주문 옵션 그룹 가져오기 (추상클래스로 변환))	 (주문 옵션 가져오기(추상클래스로 변환))			
       
       

    (그냥 스크린샷 찍을 껄이라고 후회할 때 찍을 껄 후회중)

    // before
    // domain/order/OrderLineItem.java 
    public void validate() {
            menu.validateOrder(name, this.orderOptionGroups);
        }
    
    // after
    // domain/order/OrderLineItem.java 
    public void validate() {
            menu.validateOrder(name, convertToOptionGroups());
        }

    해당 클래스 추가로 인해 변경된 코드는 다음과 같다. (깃 코드에는 before 코드가 없고 발표자료에서만 확인할 수 있어서 확인이 어려웠다.)

    Django에서 Model에 클래스와 메서드를 작성하고, View에서 Service를 작성하였으면 어떤 문제가 있었을까? 예상되는 코드는

# Shop/model.py
class Option:
    def is_satisfied(self, cart_option):
        # assert isinstance(cart_option, CartOption)
        # 해당 코드가 있다면 Cart/model에서  CartOption을 임포트해오면서 패키지 단에선 같은 문제가 발생할 것이다.
        return (self.name == cart_option.name && self.price == cart_option.price)

기본적으로 python에선 클래스 타입을 체크하지 않고 사용하여 인터페이스만 맞추면 패키지 의존 없이 런타임은 잘 동작할 것으로 기대된다. 다만 코드의 주석과 같이 엄밀하게 Class를 검증하기 시작하면 같은 이슈가 발생할 것으로 본다.

  1. 성능 이슈

    3.1
    객체 참조로 연관관계를 구현하면서 데이터 조회 시 조회 경계가 너무 광범위해짐.
    Order를 조회했더니 조인 걸린 Shop과 menu가 다 딸려나오는 현상.

    3.2
    데이터 수정 시 트랜잭션의 경계가 모호해짐.
    배달 완료 기능을 한 트랜잭션으로 묶으면, 배달 상태 변경 -> 주문 상태 변경 -> 가게 상태 변경. 3가지 리소스를 한번에 수정함.
    배달, 주문, 가게에 다른 기능이 접근 시 경합으로 인한 성능 이슈 발생.

  2. 해결책

    4.1
    id 값을 저장하고 Repository.getId를 활용한 약한 결합으로 변경
    라이프사이클이 같은 애들끼리는 강한 결합. (주문, 주문항목, 주문옵션..)
    도메인 제약사항을 공유하는 애들끼리 강한 결합.
    가능하면 분리하기.

    4.2
    강하게 결합된 애들끼리만 트랜잭션을 공유하도록 변경.

    장점 : 강하게 결합된 애들 (도메인)은 하나의 단위가 되어 독립된 저장소를 쓸 수 있음.

  3. Refactoring
    5.1
    객체 참조를 간접 참조로 바꾸고 실행해보면 Validation 로직에서 바로 (당연히) Compile Error
    객체에 작성된 메서드 로직들을 모아서 OrderValidation이란 클래스 한 곳에 모은 뒤 Order.shopId, Order.MenuId 간접 참조된 Id들을 받아 객체를 fetch해주고 그대로 실행. 절차지향형으로 모아서 해결.
    응집도가 올라가는 장점도 있음.

    5.2
    여러 도메인의 객체의 상태를 순차적으로 바꾸는 기존 로직의 경우,
    1번과 같이 절차지향적으로 해결하기 or 이벤트 퍼블리싱 방식으로 해결하기.

    5.2.1
    절차지향적으로 해결할 시, Cycle Import 이슈가 또 발생할 수 있음 DI로 해결해주기.
    도메인 이벤트 퍼블리싱으로 해결 중 Shop에 Billing을 하려는 기능으로 인해 Cycle이 또 발생. Billing을 독립적인 Package로 분리하여 해결. -> 도메인 분리

    패키지 의존성을 끊는 3가지

  4. 새로운 객체 추가 (Adaptor 용)

  5. DI

  6. 새로운 패키지 추가

4. 의존성과 시스템 분리

기존 -> Service와 Domain Layer 단으로 모듈화
도메인 이벤트 사용 후 -> 도메인 단위로 모듈화가 가능해짐 -> 시스템 분리 (MSA)의 초석

profile
개발을 재밌게 하고싶습니다.

0개의 댓글