Ch18. 반응형 아키텍처와 어니언 아키텍처
반응형 아키텍처 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 요청은 없다 => 인터랙션 계층에 속하는 코드
도메인의 가독성, 균형
특정 패러다임의 장점이 항상 좋은 것은 아님
도메인 역시 항상 계산으로 만드는 것이 권장되지만, 문맥에 따라 액션이 더 읽기 좋은 경우가 있음
현실 세계 문제와 이상적 다이어그램 사이 균형을 유지하기 위해 고려할 것
코드 가독성
가독성을 결정하는 몇 가지 요소
프로그래밍 언어
라이브러리
레거시 코드와 코드 스타일
개발자들의 습관
함수형 코드가 아닌 코드가 얼마나 더 명확해지는지를 봐야
개발 속도
비즈니스 이유로 빠르게 출시해야하는 경우
많은 것을 타협해야 하지만, 이런 경우에도 나중에 아키텍처에 맞춰 코드를 정리할 준비를 하는 것이 좋다
시스템 성능
변경 가능한 데이터 구조는 불변 데이터 구조보다 빠름
성능 개선과 도메인을 계산으로 만드는 것은 별개의 문제
인터랙션 계층 => 최적화
도메인 계층 => 재사용 가능한 계산 만들기