Front-end 클린 아키텍처
https://dev.to/bespoyasov/clean-architecture-on-frontend-4311
위 글의 일부를 번역 & 정리한 글입니다.
아키텍처와 설계
시스템 설계는 나중에 다시 조립할 수 있도록 시스템을 분리하는 것입니다.
그리고 너무 많은 작업 없이 쉽게 조립할 수 있어야 한다는 것입니다.
아키텍처의 또 다른 목표는 시스템의 확장성입니다.
프로그램에 대한 요구사항은 지속적으로 변경되며, 새로운 요구사항을 충족하기 위해 쉽게 변경할 수 있어야 하는데, 클린 아키텍처는 이러한 목표를 달성하는 데에 도움을 줍니다.
클린 아키텍처
애플리케이션 도메인에 대한 근접성에 따라, 책임과 기능 부분을 분리하는 방법입니다.
도메인
- 이것은 실제 세계의 변환을 반영하는 데이터 변환입니다.
- 예를 들어, 제품 이름을 업데이트한 경우 이전 이름을 새 이름으로 바꾸는 것은 도메인 변환입니다.
- 도메인이란 프로그램으로 모델링하는 실제 세계의 일부를 의미합니다.
클린 아키텍처는 기능이 계층으로 나누어져 있기 때문에 종종 3계층 아키텍처라고도 합니다.
도메인 레이어
중심에는 도메인 레이어가 있습니다.
엔터티(entities)와 데이터는 응용 프로그램의 주제 영역과 해당 데이터를 변환하는 코드입니다.
도메인은 한 응용 프로그램을 다른 응용 프로그램과 구별하는 핵심입니다.
도메인은 React에서 Angular로 변경하거나, 어떤 시나리오를 변경한다고 해서 변경되는 것이 아닙니다.
도메인은 가게로 치면 주문, 상품, 장바구니와 같은 것들입니다.
혹은 그 데이터들을 변경하기 위한 기능들입니다.
도메인 엔터티의 데이터 구조와 변환의 본질은 외부 세계와 독립적입니다.
외부 이벤트는 도메인 변환을 트리거하지만, 이벤트가 일어나는 방식을 결정하지는 않습니다.
예를 들면, 장바구니에 물건을 추가하는 기능은 정확히 어떤 방식으로 물건이 담기는지를 서술하지는 않습니다.
사용자가 직접 구매 버튼을 통해 넣는 것인지, 혹은 자동으로 프로모션 코드를 통해 추가하는 것인지에 관여하지 않습니다.
두 경우 모두, 아이템을 받고 → 그 아이템을 장바구니에 담아 → 변경된 장바구니를 반환할 뿐입니다.
애플리케이션(응용 프로그램) 계층
도메인 주변에는 응용 프로그램 계층이 있습니다.
이 계층은 사용 사례(Use Cases), 즉 사용자 시나리오입니다.
이것들은 어떤 사건이 발생한 후에 일어나는 일에 대한 책임이 있습니다.
예를 들어, "장바구니에 추가" 시나리오는 "버튼을 클릭한 후 취해야 하는 조치"에 대한 것입니다.
서버로 이동하여 요청을 보내고 → 도메인 변환을 수행하고 → 응답 데이터를 사용해 UI 그리기
포트
응용 프로그램 계층에는 외부 세계와 통신하기 위해 필요한 응용 프로그램의 사앙으로써 포트가 있습니다.
일반적으로 포트는 인터페이스로, 동작에 대한 약속입니다.
포트는 응용 프로그램의 희망과 현실 사이의 완충 영역이라고 생각하면 됩니다.
입력 포트는 응용 프로그램이 외부 세계와 어떻게 연결되기를 원하는지(희망)를 알려줍니다.
출력 포트는 응용 프로그램이 외부 세계와 통신하는 방법을 알려줍니다.
어댑터 레이어
가장 바깥쪽 레이어에는 외부 서비스에 대한 어댑터가 있습니다.
호환되지 않는 외부 서비스 API를 응용 프로그램에 호환되도록 변환하기 위해 필요합니다.
어댑터는 우리 코드와 외부 코드 간의 결합을 낮추는 좋은 방법입니다.
낮은 결합은 다른 모듈이 변경될 때, 다른 모듈들을 변경해야 하는 필요성을 줄이니까요.
어댑터는 일반적으로 다음과 같이 나뉩니다.
- driving - 우리 응용 프로그램에 신호를 보냄
- driven - 우리 응용 프로그램으로부터 신호를 받음
driving 어댑터
driving 어댑터는 주로 사용자 상호 작용 부분을 맡습니다.
예를 들어, UI 프레임워크의 버튼 클릭 처리는 driving 어댑터의 작업입니다.
브라우저 API를 통해 발생한 이벤트를 우리 응용 프로그램이 이해할 수 있는 신호로 이벤트를 변환합니다.
driven 어댑터
driven 이벤트는 주로 인프라와 상호 작용 합니다.
프론트엔드에서 대부분의 인프라는 백엔드 서버이지만, 때로는 검색 엔진과 같은 다른 서비스와 직접 상호 작용할 수 있습니다.
중심에서 멀어질수록 코드 기능이 "서비스 지향적"이고, 응용 프로그램의 도메인 지식에서 멀어집니다.
이것은 나중에 어떤 모듈이 어느 계층에 와야하는지 결정할 때 중요하게 작용합니다.
종속성 규칙(Dependency Rule)
3계층 아키텍처에는 종속성 규칙이 있습니다.
외부 계층만 내부 계층에 종속될 수 있다는 것입니다.
- 도메인은 독립적이어야 함
- 응용 프로그램 계층은 도메인에 종속될 수 있음
- 외부 레이어는 무엇이든 의존할 수 있음
때로는 이 규칙을 위반할 수도 있지만, 준수하는 것이 좋습니다.
예를 들어, 아래 코드처럼 도메인에서 종속성이 없는 라이브러리 같은 코드를 사용하면 편할 때가 있습니다.
// domain/order.ts
import { currentDatetime } from "../lib/datetime"; // 종속성이 없는 라이브러리 같은 코드
import { Product, totalPrice } from "./product";
export function createOrder(user: User, cart: Cart): Order {
return {
user: user.id,
cart,
created: currentDatetime(), // 그대로 사용하면 매우 편함
status: "new",
total: totalPrice(products),
};
}
위 코드는 currentDatetime 에 의존합니다.
이러한 종속성을 제거하여, 완전(pure)한 형태를 취하는 것이 좋습니다.
// domain/order.ts
import { currentDatetime } from "../lib/datetime"; // 종속성이 없는 라이브러리 같은 코드
import { Product, totalPrice } from "./product";
export function createOrder(user: User, cart: Cart): Order {
return {
user: user.id,
cart,
created: currentDatetime(), // 그대로 사용하면 매우 편함
status: "new",
total: totalPrice(products),
};
}
그러나 이렇게 created 가 라이브러리에 의존하지 않도록하여, 의존성 규칙을 위반하지 않습니다.
종속성 규칙을 위반하면 다음과 같은 일이 발생할 수 있습니다.
- 순환 종속성. 모듈 A는 B에 종속, B는 C에, C는 다시 A에 종속되는 문제
- 작은 부분을 테스트하기 위해 전체 시스템을 시뮬레이션 하는 문제
- 너무 높은 결합으로 결과적으로 모듈 간의 취약한 상호 작용
클린 아키텍처의 장점
별도의 도메인
모든 주요 응용 프로그램의 기능이 도메인 한 곳에 격리되고 모여있습니다.
테스트 용이
도메인의 기능은 독립적이므로, 테스트하기가 쉽습니다.
모듈 종속성이 적을수록 테스트에 필요한 인프라가 줄어들어, 필요한 mock 개수도 줄어듭니다.
작업 파악 용이
독립 실행형 도메인은 비지니스 기대치에 대해 테스트하기도 쉽습니다.
이는 신규 개발자가 애플리케이션이 수행해야 하는 단위 작업을 파악하는 데에 도움을 줍니다.
독립적인 사용 사례(use case)
우리는 외부 세계를 우리의 필요에 맞게 조정합니다.
이를 통해, 타사 서비스를 선택할 수 있는 더 많은 자유를 얻을 수 있습니다.
교체 가능한 타사 서비스
어댑터로 인해 외부 서비스를 교체할 수 있습니다.
인터페이스를 변경하지 않는 한, 인터페이스를 구현하는 외부 서비스가 무엇인지는 중요하지 않습니다.
다른 사람의 코드 변경이, 우리 자신의 코드에 직접적인 영향을 미치지 않습니다.
어댑터는 또한 런타임에서 버그 전파를 제한합니다.
클린 아키텍처의 비용
시간이 걸린다
주요 비용은 시간입니다.
어댑터를 작성하는 것보다 타사 서비스를 직접 호출하는 것이 더 쉽기 떄문에, 설계뿐만 아니라 구현에도 시간이 필요합니다.
모든 모듈의 상호작용을 미리 생각하는 것도 어렵습니다.
설계할 때 시스템이 어떻게 변경될 수 있는지 염두에 두고, 확장의 여지를 남겨두어야 합니다.
때로는 지나치게 장황하다
때로는 이것이 해로울 수 있습니다.
프로젝트가 작은 경우, 신규 인원의 진입 장벽을 올리는 오버 엔지니어링이 돼버릴 수 있습니다.
따라서, 예산이나 기한 내에서 유지가능한 설계 절충안을 만들어야 할 수 있습니다.
온보딩을 더 어렵게 만들 수 있다
사용 방법에 대한 지식이 필요하기 때문에, 클린 아키텍처를 완전히 구현하면 온보딩이 더 어려우질 수 있습니다.
이것을 염두에 두고, 코드를 단순하게 유지해야 합니다.
코드 양을 늘릴 수 있다
최종 번들의 코드 양을 늘릴 수 있습니다.
브라우저에 더 많은 코드를 제공할수록, 더 많이 다운로드, 구문 해석 및 분석이 일어나야 합니다.
따라서, 어디에서 무엇을 잘라내야 하는지에 대한 결정을 내려야 합니다.
- 사용 사례를 좀 더 간단하게 설명
- 사용 사례를 우회하여 어댑터에서 직접 도메인 기능에 액세스
- 코드 분할 정도 조정
비용 줄이는 방법
아키텍처의 "청결함"을 희생하여 시간과 코드의 양을 줄일 수 있습니다.
규칙을 어기는 것이 더 실용적이라면, 규칙을 어기는 편이 낫습니다.
그러나 확실히 투자할 가치가 있는 최소 요구사항이 있습니다.
도메인 추출
추출된 도메인은 우리가 일반적으로 무엇을 디자인하고 어떻게 작동해야 하는지 이해하는 데 도움이 됩니다.
신규 개발자가 응용 프로그램과 해당 엔티티, 그들 간의 관계를 더 쉽게 이해할 수 있도록 합니다.
다른 계층은 건너 뛰더라도, 추출된 도메인을 가지고 작업 및 리팩토링 하는 것이 더 쉬울 것입니다.
다른 계층들은 필요에 따라 추가하는 것이 좋습니다.
종속성 규칙 준수
버려서는 안 되는 두 번째 규칙은 종속성의 규칙, 즉 방향입니다.
외부 서비스는 우리의 필요에 맞게 조정되지 않으면 제대로 동작하지 않습니다.
검색 API를 호출할 수 있도록 코드를 "미세 조정"하고 있다고 생각되면 문제가 있는 것입니다. (외부서비스에 종속되어 돌아가고 있다는 뜻)
문제가 확산되기 전에 어댑터를 작성하는 것이 좋습니다.
실제 프로젝트에서 더 복잡할 수 있는 것
비지니스 로직 나누기
가장 중요한 문제는 개체를 적절히 설명하고 나눌 수 있을 정도로, 우리는 그 주제에 대하여 충분한 지식을 갖추고 있지 않다는 점입니다.
확장될 기본 엔터티가 있어야 하는지, 정확히 어떻게 확장해야 하는지, 추가 필드가 있어야 할지 등등에 대해서 말입니다.
관련된 사람들이 모두 시스템이 실제로 어떻게 작동해야 하는지를 모르기 때문에, 질문과 답변만 많고 분석 마비 상태에 빠질 수 있습니다.
특정 솔루션은 특정 상황에만 국한되므로, 여기서는 일반적인 몇 가지만 추천하겠습니다.
확장이라고 해서 반드시 상속을 사용하지는 말자
인터페이스가 실제로 상속된 것처럼 보일지라도, 분명한 계층 구조가 있는 것처럼 보일지라도 섣부르게 상속을 하지말고, 기다리세요.
코드의 copy & paste 가 항상 나쁜 것은 아닙니다. 이것은 도구입니다.
거의 동일한 두 개체를 만들고, 실제로 어떻게 행동하는지 관찰하고 또 관찰하세요.
어느 시점에서, 그것들이 매우 달라졌거나 실제로 한 분야에서만 다르다는 것을 알아챌 것입니다.
상속해놓고 모든 조건과 변형에 대한 검사를 생성하는 것보다, 두 개의 유사한 엔터티를 필요할 때 병합하는 것이 더 쉽습니다.
그래도 상속해야 한다면...
- 공분산(covariance), 반공변(contravariance), 불변(invariance)를 항상 염두에 두어, 실수로 해야할 것보다 더 많은 작업을 수행하지 않도록 하세요.
- 다른 엔티티를 만들 것인지 아니면 확장할 것인지를 선택할 때, BEM의 블록 및 수정자(modifier)를 통해 유추하세요. BEM의 맥락에서 생각한다면, 별도의 엔터티가 있는지 혹은 modifier 확장 코드가 있는지 확인하는 데에 많은 도움이 됩니다.위 글의 일부를 번역 & 정리한 글입니다.