Ch18. 반응형 아키텍처와 어니언 아키텍처

·

4 min read

반응형 아키텍처 ReactiveArchitecture

  • n x m 문제: 변경에 대한 원인과 효과가 강력하게 결합되어 나타나는 문제

  • 핵심 원칙: 이벤트에 대한 반응으로 일어날 일을 지정하는 것, "이벤트 핸들러"

  • 동작

    • 하려고 하는 것만 처리

    • 모든 것은 어떤 일이 일어나면 그에 대한 응답으로 처리

      • X를 하고 Y를 하는 대신, X가 일어나면 언제나 Y를 함

      • X가 일어나면 Y -> Z -> A..

  • 언제 사용할 수 있을까?

    • 만능 열쇠는 없음, 언제, 어떻게 사용할지는 스스로 판단해야 함

    • 원인과 효과가 결합한 것을 분리해야 할 때

      • 어떤 경우는 분리하면 코드가 더 읽기 어려워짐. 다만 코드가 더 유연하고 하려고 하는 것을 정확하게 표현할 수 있음
    • 여러 단계를 파이프라인으로 처리할 때

    • 순서를 표현하는 방법을 뒤집어, 타임라인을 유연하게 만들 때

상태 모델

  • 변경 가능한 값을 일급 함수로 만들기

    • 반응형 아키텍처로 만든 스프레드시트

    • watcher: observer, listener, event-handler.. 등 유사한 개념

  • ValueCell

      function ValueCell (initialValue) {
          let currentValue = initialValue;
          const watchers = [];
    
          return {
              val() {
                  return currentValue;
              },
    
              /** 교체 패턴 */
              update(f) {
                  const oldValue = currentValue;
                  const newValue = f(oldValue);
    
                  if ( oldValue !== newValue ){
                      currentValue = newValue;
                      watchers.forEach(watcher => watcher(newValue));
                  }
              },
    
              addWatcher (f) {
                  watchers.push(f);
              }
          }
      }
    
    • 장바구니 예제에서 shoppingCart

    • Redux store, Recoil atom

    • Clojure Atom, Elixir Agent, Haskell TVar

    • update에는 계산(순수함수)만 전달

    • 다른 타임라인에서 읽거나 쓰는 순서를 보장하지 않지만,

    • 어떤 값이 저장되어도 그 값이 항상 올바른 값이라는 것은 보장

  • FormulaCell

    • ValueCell에서 파생해 만드는 셀

    • ValueCell의 값이 변경되면 값을 다시 계산

    function FormulaCell(upstreamCell, f) {
        const myCell = ValueCell(f(upstreamCell.val()));
        upstreamCell.addWatcher(newUpstreamValue => {
            myCell.update(currentValue  => f(newUpstreamValue))
        });

        return {
            val: myCell.val,
            addWatcher: myCell.addWatcher
        }
    }
  • 장바구니 예제에서 cartTotal
  • 기존 코드 vs 반응형 아키텍처 비교

    • 기존 코드
    const shoppingCart = {};
    function addItemToCart (name,price) {
        const item = makeCartItem(name, price);
        shoppingCart = addItem(shoppingCart, item);

        const total = calcTotal(shoppingCart);
        setCartTotalDom(total);
        updateShippingIcons(shoppingCart);
        updateTaxDom(total);
    }
  • 반응형 아키텍처
    const shoppingCart = ValueCell({});
    const cartTotal = FormulaCell(shoppingCart, calcTotal);

    function addItemToCart(name, price) {
        const item = makeCartItem(name, price);
        shoppingCart.update(cart => addItem(cart, item));
    }

    shoppingCart.addWatcher(updateShippingIcons);
    cartTotal.addWatcher(setCartTotalDom);
    cartTotal.addWatcher(updateTaxDom);

반응형 아키텍처가 바꾼 시스템

원인-효과가 결합된 것을 분리

  • 원인과 효과의 중심이 무엇인가를 파악 => 장바구니

  • m x n 문제 해결

    • 일반적인 아키텍처: 장바구니 바꾸는 방법 5가지 X 장바구니가 바뀔 때 해야할 액션 4가지 => 20가지

    • 반응형 아키텍처: 장바구니 바꾸는 방법 5가지 + 장바구니가 바뀔 때 해야할 액션 4가지 => 9가지

  • 장바구니처럼 원인과 효과의 중심이 없다면 분리하지 마라

    • 문제가 없는데 이 방법으로 분리하는 것은 좋지 않다

    • 코드에 액션을 순서대로 표현하는 것이 더 명확할 수 있다

여러 단계를 파이프라인으로 처리

  • 파이프라인은 작은 액션과 계산들을 조합한 하나의 액션

    • 데이터가 파이프라인으로 들어가 각 단계에서 처리
  • 어떤 일이 발생하는 여러 단계가 있다면 파이프라인으로 처리하는 것이 좋음

    • 각 단계에서 생성된 데이터는 다음 단계의 입력값

    • 데이터를 전달하지 않는다면 이 패턴을 사용하지 않는 것이 좋음

  • 구현 방식

    • 언어에 맞는 기본형을 사용하는 것이 좋다

    • 반응형 프레임워크, Promise

    • 이벤트 스트림이 필요하다면 ReactivX 라이브러리 활용; RxJS

    • 외부 스트림 서비스가 필요하다면 Kafka, RabbitMQ 등

유연한 타임라인

  • 공유하는 자원이 없으면 타임라인이 많아져도 문제가 없음

  • ValueCell, FormulaCell 은 watcher 호출할 때 현재 값을 넘겨주기 때문에 watcher가 직접 Cell의 값을 읽을 필요가 없음


어니언 아키텍처 OnionArchitecture

  • 서비스 전체를 구성하는데 사용, 반응형 아키텍처보다 더 넓은 범위에 사용

  • 계층

    • 인터랙션 계층 - 바깥 세상에 영향을 주고 받는 액션

    • 도메인 계층 - 비즈니스 규칙을 정의하는 계산

    • 언어 계층 - 언어 유틸리티와 라이브러리

전통적인 계층형 아키텍처 ( #LayeredArchitecture )와의 비교

  • 전통적인 계층형 아키텍처

    • 웹 인터페이스 계층

    • 도메인 계층

    • 데이터베이스 계층

  • 함수형 아키텍처

    • 데이터베이스는 변경 가능하고 접근하는 모든 것을 액션으로 만듦

    • 함수형 아키텍처는 액션과 계산을 명확하게 구분하고, 도메인 로직은 모두 계산으로 만들어야 함

    • 데이터베이스를 도메인과 분리해야 함

변경과 재사용이 쉬운 아키텍처

  • 어떤 것이 바꾸기 쉬운가?

    • 소프트웨어 아키텍처는 변화를 다루는 일

    • 이 질문에 답할 수 있다면 아키텍처의 반은 결정한 것

  • 인터랙션 계층

    • 변경하기 쉬운 것

    • 데이터베이스, 서비스 프로토콜

    • API handler

  • 도메인 계층

    • 재사용하기 쉬운 것

    • cartTotal

도메인 규칙 domain rule

  • 프로그램의 핵심 로직, 비즈니스 규칙 business rule

  • 도메인 규칙은 도메인 용어를 사용

    • 전자 상거래 도메인 용어에 AJAX 요청은 없다 => 인터랙션 계층에 속하는 코드

도메인의 가독성, 균형

  • 특정 패러다임의 장점이 항상 좋은 것은 아님

  • 도메인 역시 항상 계산으로 만드는 것이 권장되지만, 문맥에 따라 액션이 더 읽기 좋은 경우가 있음

  • 현실 세계 문제와 이상적 다이어그램 사이 균형을 유지하기 위해 고려할 것

    • 코드 가독성

      • 가독성을 결정하는 몇 가지 요소

        • 프로그래밍 언어

        • 라이브러리

        • 레거시 코드와 코드 스타일

        • 개발자들의 습관

      • 함수형 코드가 아닌 코드가 얼마나 더 명확해지는지를 봐야

    • 개발 속도

      • 비즈니스 이유로 빠르게 출시해야하는 경우

      • 많은 것을 타협해야 하지만, 이런 경우에도 나중에 아키텍처에 맞춰 코드를 정리할 준비를 하는 것이 좋다

    • 시스템 성능

      • 변경 가능한 데이터 구조는 불변 데이터 구조보다 빠름

      • 성능 개선과 도메인을 계산으로 만드는 것은 별개의 문제

        • 인터랙션 계층 => 최적화

        • 도메인 계층 => 재사용 가능한 계산 만들기