Skip to main content

18장 반응형 아키텍처와 어니언 아키텍처

written by
positiveko
positiveko 🏆Front End Engineer

이번 장에서 살펴볼 내용

  • 반응형 아키텍처로 순차적 액션을 파이프라인으로 만드는 방법을 배웁니다.
  • 상태 변경을 다루기 위한 기본형을 만듭니다.
  • 도메인과 현실 세계의 상호작용을 위해 어니언 아키텍처를 만듭니다.
  • 여러 계층에 어니언 아키텍처를 적용하는 방법을 살펴봅니다.
  • 전통적인 계층형 아키텍처와 어니언 아키텍처를 비교해 봅니다.

두 아키텍처 패턴은 독립적입니다.

Two separate architectural patterns 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-18/9

반응형 아키텍처는 순차적 액션 단계에 사용하고, 어니언 아키텍처는 서비스의 모든 단계에 사용합니다.

반응형 아키텍처

  • 반응형 아키텍처는 순차적 액션의 순서를 뒤집습니다.
  • 효과와 그 효과의 대한 원인을 분리.

어니언 아키텍처

  • 현실 세계와 상호작용하기 위한 서비스 구조를 만듭니다.

반응형 아키텍처

반응형 아키텍처는 무엇일까?

반응형 아키텍처는 애플리케이션을 구조화하는 방법입니다.

반응형 아키텍처는 변화에 대한 시스템의 대응성과 확장성을 강조하는데요, 반응형 시스템은 시스템의 상태가 변경되면, 그 상태를 반영하는 효과를 발생시킵니다. 이 효과는 원인이 되는 상태 변경을 발생시키는 액션을 통해 발생합니다. 반응형 아키텍처는 이러한 효과와 원인을 분리합니다.

책에서는 이벤트 핸들러의 예제를 들었는데요, 반응형 아키텍처를 기반으로 하는 이벤트 핸들러는 이벤트 스트림을 구독하여, 이벤트가 발생할 때마다 반응하는 코드를 실행합니다.

이벤트를 처리하는 방식에는 여러 가지가 있지만, 대표적으로 다음과 같은 방식들이 있습니다.

  • 이벤트를 처리하는 함수 (event handler function) : 이벤트가 발생할 때마다 실행되는 함수를 정의하여, 이벤트 처리를 구현합니다.
  • 옵저버 패턴 (Observer pattern) : 이벤트 스트림을 구독하는 객체들이 이벤트가 발생할 때마다 실행되는 코드를 구현합니다.

따라서 반응형 시스템은 이벤트의 스트림을 구독하며, 이를 통해 시스템은 실시간으로 변화에 대응할 수 있습니다.

반응형 아키텍처의 절충점

반응형 아키텍처는 코드에 나타난 순차적 액션의 순서를 뒤집습니다. 전통적인 아키텍처와 비교했을 때,

  • 원인과 효과가 결합한 것을 분리합니다.
    • 반응형 아키텍처는 코드에 나타난 순차적 액션의 순서를 뒤집습니다.
    • 일반적인 아키텍처에서는 원인에 대한 효과가 결합되어 있는 것을 분리하면서 이벤트 스트림을 구독하여 원인과 효과를 분리합니다.
  • 여러 단계를 파이프라인으로 처리합니다.
    • 여러 단계를 파이프라인으로 처리하면서, 이벤트를 처리하는 과정을 더욱 유연하게 구성할 수 있습니다.
  • 타임라인이 유연해집니다.
    • 이벤트 스트림을 구독하면서 처리하기 때문에 실시간으로 이벤트를 처리하며, 이벤트가 발생할 때마다 처리할 수 있기 때문입니다.

이제 강력한 일급 상태 모델을 만들어 보자. 상태 모델을 만들고 생성한 상태 모델을 통해 위에서 설명한 반응형 아키텍처를 구현해 보겠습니다.

셀은 일급 상태입니다

function ValueCell(initialValue: number) {
let currentValue = initialValue; // 초기값으로 전달받은 값으로 초기화.
return {
val: () => currentValue, // 현재값 리턴
update: (f) => {
// 교체 패턴
let oldValue = currentValue;
let newValue = f(oldValue);
currentValue = newValue;
},
};
}

위의 코드를 기반으로 아래와 같이 장바구니 구현 코드를 리팩터링할 수 있습니다.

const shopping_cart = ValueCell({});
function add_item_to_cart(name: string, price: number) {
const item = make_cart_item(name, price);
// shopping_cart = add_item(shopping_cart, item);
// 위 코드를 다음과 같이 리팩터링
shopping_cart.update((cart) => add_item(cart, item));
const total = calc_total(shopping_cart.val());
set_cart_total_dom(total);
update_shipping_icons(shopping_cart.val());
update_tax_dom(total);
}

valueCell을 반응형으로 만들어 보자

// 이전 코드
function ValueCell(initialValue: string) {
var currentValue = initialValue;
return {
val: () => currentValue,
update: function (f) {
const oldValue = currentValue;
const newValue = f(oldValue);
currentValue = newValue;
},
};
}

function ValueCell(initialValue: string) {
let currentValue = initialValue;
var watchers = []; // 감시자 목록 추가
return {
val: () => currentValue,
update: function (f) {
const oldValue = currentValue;
const newValue = f(oldValue);
if (oldValue !== newValue) {
currentValue = newValue;
forEach(watchers, (watcher) => watcher(newValue)); // 감시자 실행
}
},
// 새로운 감시자 추가
addWatcher: function (f) {
watchers.push(f);
},
};
}
  • 감시자란 currentValue의 값이 변경될 때마다 수행할 작업을 정의할 함수.
  • update 함수는 새로운 값이 이전 값과 다른 경우에만 currentValue를 변경하고 감시자를 실행합니다.
  • addWatcher 함수: 인자로 전달받은 함수를 watchers 배열에 추가하여 관리. => ValueCell 객체에 감시자를 추가할 수 있는 기능과, 현재 값이 바뀔때 감시자를 실행시켜 어떤 작업을 수행하도록 하는 기능이 추가.

셀이 바뀔 때 배송 아이콘을 갱신하자

const shopping_cart = ValueCell({});
function add_item_to_cart(name: string, price: number) {
const item = make_cart_item(name, price);
shopping_cart.update(function (cart) {
return add_item(cart, item);
});
const total = calc_total(shopping_cart.val());
set_cart_total_dom(total);
// 이전 코드
// update_shipping_icons(shopping_cart.val());
update_tax_dom(total);
}
// 장바구니가 변경될 때 항상 update_shipping_icons가 실행
shopping_cart.addWatcher(update_shipping_icons);

따라서 add_item_to_cart 핸들러에서 DOM 갱신하는 부분을 하나 삭제했습니다. 이제 남은 DOM 갱신 코드는 2개입니다.

formulaCell은 파생된 값을 계산합니다.

ValueCell에 감시자 기능을 추가해서 반응형으로 만든 것처럼, FormulaCell로 이미 존재하는 셀에서 파생한 셀을 생성할 수 있습니다. 다른 셀의 변화가 감지되면 값을 다시 계산합니다.

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

이 FormulaCell로 코드를 리팩터링 해봅시다.

// ValueCell 객체 생성. 장바구니에 담긴 상품 목록을 저장
const shopping_cart = ValueCell({});
// formulaCell 생성. 장바구니에 담긴 상품의 총 금액을 계산. shopping_cart가 바뀔 때 cart_total도 변경된다.
const cart_total = FormulaCell(shopping_cart, calc_total);
function add_item_to_cart(name: string, price: number) {
const item = make_cart_item(name, price);
shopping_cart.update(function (cart) {
return add_item(cart, item);
});
// 이전 코드에서 삭제
// const total = calc_total(shopping_cart.val());
// set_cart_total_dom(total);
// update_tax_dom(total);
}
shopping_cart.addWatcher(update_shipping_icons);
// 새롭게 추가. cart_total이 바뀌면 DOM이 업데이트.
cart_total.addWatcher(set_cart_total_dom);
cart_total.addWatcher(update_tax_dom);

함수형 프로그래밍과 변경 가능한 상태

함수형 프로그래밍은 변경 가능한 상태를 피하고, 변경 불가능한 상태를 사용한다고 흔히 이야기 합니다. 변경 가능한 상태를 남용하지 않고, 변경 가능한 상태를 잘 관리한다면 함수형 프로그래밍의 이점을 누리면서 개발할 수 있습니다.

위의 예제에서 ValueCell 객체의 update() 메서드를 사용하면 현재 값을 올바르게 유지할 수 있는데, 이는 이 메서드가 호출될 때마다 '계산'을 넘기기 때문입니다. 즉, (계산이 올바르다는 전제 하에) 계산은 현재 값을 받아서 새로운 값을 생성하기 때문에 항상 올바른 값을 유지할 수 있습니다.

반응형 아키텍처가 시스템을 어떻게 바꿨나요

How reactive architecture reconfigures systems 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-18/9

  1. 원인과 효과가 결합된 것을 분리
  2. 여러 단계를 파이프라인으로 처리
  3. 타임라인이 유연

1) 원인과 효과가 결합한 것을 분리합니다

How reactive architecture reconfigures systems How reactive architecture reconfigures systems 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-18/9

3가지 원인에 의해 배송 아이콘을 갱신하던 로직을 '전역 장바구니 변경'이라는 원인 하나에 기인하도록 수정했습니다.

다만, 코드에 액션을 순서대로 표현하는 것이 더 명확할 수 있습니다. 장바구니처럼 원인과 효과의 중심이 없다면 분리하지 맙시다.

2) 여러 단계를 파이프라인으로 처리합니다.

반응형 아키텍처에서는 각 단계를 독립적으로 처리합니다. 각 단계는 입력을 처리하고 출력을 생성하며, 이러한 출력은 다음 단계에 전달됩니다.

즉 간단한 액션과 계산을 조합해서 복잡한 계산을 만들 수 있습니다.

만약 여러 단계가 있지만 데이터를 전달하지 않는다면 이 패턴을 사용하지 마세요. 데이터를 전달하지 않으면 파이프라인이라고 볼 수 없습니다.

3) 타임라인이 유연해집니다.

Flexibility in your timeline 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-18/9

반응형 아키텍처는 이벤트 기반의 아키텍처로, 상태 변화를 이벤트로 감지하고 감지한 이벤트에 대해 응답하는 구조를 가지고 있습니다. 그래서 상태 변화가 발생할 때마다 바로바로 응답할 수 있다는 의미에서 타임라인이 유연해집니다.

15장에서 짧은 타임라인이 좋은 것이라고 했지만 많은 것도 좋지 않습니다. 공유하는 자원이 많지 않다면 타임라인이 많아져도 상관 없습니다. 따라서 위의 타임라인은 서로 다른 자원을 사용하기 때문에 안전합니다.

어니언 아키텍처

어니언 아키텍처는 반응형 아키텍처보다 더 넓은 범위에 사용합니다. 어니언 아키텍처는 서비스 전체를 구성하는 데 사용하는데, 반응형 아키텍처와 함께 사용한다면 반응형이 어니언 아키텍처 안에 들어가는 걸 볼 수 있습니다.

어니언 아키텍처란?

What is the onion architecture? 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-18/9

어니언 아키텍처는 현실 세계와 상호 작용하기 위한 서비스 구조를 만드는 방법인데요, 특정 계층이 꼭 필요한 것은 아니지만 대부분의 경우 위와 같은 구조를 가지고 있습니다.

  • 인터랙션: 어니언 아키텍처는 인터랙션 스트림을 기반으로 하여 사용자 인터랙션을 처리합니다. 이벤트를 스트림으로 관리하고 이벤트를 기반으로 데이터를 처리합니다.
  • 도메인: 어니언 아키텍처는 도메인 기반의 애플리케이션을 구성합니다. 즉, 도메인 로직과 애플리케이션 로직을 분리하여 각각을 함수로 구성하여 애플리케이션을 쉽게 테스트 할 수 있도록 합니다.
  • 언어: 어니언 아키텍처는 함수형 프로그래밍 언어를 사용하여 애플리케이션을 구성합니다. 이러한 함수형 언어는 순수 함수를 사용하여 부작용을 최소화하고, 값을 변경하지 않습니다.
  1. 현실 세계와 상호작용은 인터랙션 계층에서 합니다.
  2. 계층에서 호출하는 방향은 중심 방향입니다.
  3. 계층은 외부에 어떤 계층이 있는지 모릅니다.

전통적인 계층형 아키텍처 vs 함수형 아키텍처

  • 전통적인 계층형 아키텍처는 애플리케이션을 계층적으로 구성하는 아키텍처 스타일입니다. 각 계층은 특정 기능을 수행하며, 이러한 계층들은 서로 의존적입니다. 각 계층은 하위 계층으로부터 서비스를 제공받습니다.
  • 함수형 아키텍처는 일반적으로 순수함수, 계산로직을 기반으로 하고, 부작용을 최소화하는 방법으로 애플리케이션을 구성합니다. 이러한 함수들은 상태를 공유하지 않고 입력 값으로만 작동하며, 애플리케이션을 재사용하고 테스트하기 쉽도록 합니다.

A functional architecture 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-18/9

  • 데이터베이스는 변경 가능하고 접근하는 모든 것을 액션으로 만든다는 것이 핵심
  • 함수형 개발자는 액션에서 계산을 빼내어야 하는데, 액션과 계산을 명확하게 구분하려고 하고 도메인 로직은 모두 계산으로 만들어야 한다. (for 가독성, 유지보수성, 재사용성, 테스트...)

변경과 재사용이 쉬워야 한다

  • 어니언 아키텍처는 인터랙션 계층을 바꾸기 쉽습니다. 그래서 도메인 계층을 재사용하기 좋습니다.

    어니언 아키텍처는 도메인 로직과 인터랙션 계층을 분리하면서, 그 사이에서 인터페이스를 정의하여 의존성을 최소화 하는데 초점을 두고 있습니다.

    인터랙션 계층을 바꾸기 쉽다는 것은 어플리케이션이 서로 다른 환경에서 작동할 수 있다는 뜻입니다. 즉, 인터랙션 계층만 바꾸면 어플리케이션을 웹, 모바일, 서버less 등 다양한 환경에서 실행 가능하게 만들 수 있습니다. 다양한 인터렉션 계층에서 분리된 도메인 계층을 재사용할 수 있게 되는 것입니다.

  • 전형적인 아키텍처에서 도메인 규칙은 데이터베이스를 부릅니다. 하지만 어니언 아키텍처에서는 그렇게 하면 안됩니다.

    어니언 아키텍처는 도메인 로직과 인터랙션 계층, 그리고 언어 계층을 분리하는데 초점을 두고 있습니다. 전통적인 아키텍처는 데이터를 저장하고, 저장된 데이터를 가져와서 계산을 수행하는 구조로 데이터베이스에서 장바구니 합계를 가져와 도메인에서 처리하지만,

    어니언 아키텍처에서는 인터랙션 계층에서 값을 가져오고 도메인 계층에서 합산을 합니다.

도메인 규칙은 도메인 용어를 사용합니다.

프로그램의 핵심 로직을 '도메인 규칙' 혹은 '비즈니스 규칙'이라고 합니다.

도메인 규칙은 도메인 용어를 사용하는데, 도메인 규칙에 속하는지 인터랙션 계층에 속하는지 판단하려면 코드에서 사용하는 용어를 보면 됩니다.

"제품, 이미지, 가격, 할인"

가독성을 따져 봐야 합니다.

특정 패러다임이 항상 좋은 것은 아닙니다. 도메인을 계산으로 만드는 것도 마찬가지인데 다음의 사항을 고려해야 합니다.

  1. 코드의 가독성: 도메인 계층을 계산으로 만들고 인터랙션 계층과 분리하면서 읽기 좋은 코드를 만들어야 합니다.
  2. 개발 속도: 비즈니스를 고려하는 것 또한 중요합니다.
  3. 시스템 성능: 불변 데이터 구조보다 변경 가능한 데이터 구조가 성능이 좋습니다. 성능 개선과 도메인을 계산으로 만드는 건 분리해서 생각합시다.

결론

반응형 아키텍처와 어니언 아키텍처에 대해 알아보았습니다.

  • 반응형 아키텍처는 순차적 액션의 순서를 뒤집고, 액션과 계산을 조합해 파이프라인으로 만듭니다.
  • 어니언 아키텍처는 소프트웨어를 인터랙션, 도메인, 언어 계층 3가지로 나눕니다.
    • 가장 바깥인 인터랙션 계층은 액션으로 되어 있는데, 도메인 계층과 액션을 사용을 조율합니다.
    • 도메인 계층은 도메인 로직 같은 소프트웨어 동작으로 되어 있는데, 도메인 계층은 대부분 계산으로 구성됩니다.
    • 언어 계층은 소프트웨어를 만들 수 있는 언어 기능과 라이브러리로 되어 있습니다.
    • 어니언 아키텍처는 프랙털입니다. 액션의 모든 추상화 수준에서 찾을 수 있습니다.