복잡한 구조에서의 어니언 아키텍처

Anisotropy·2023년 8월 11일

어니언 아키텍처

목록 보기
3/4
post-thumbnail

이 글은 <쏙쏙 들어오는 함수형 코딩>에 소개된 어니언 아키텍처를 실제 프로젝트에 적용할 때 마주치게 되는 문제에 대한 해결책에 관한 것입니다. 일부 용어들은 제가 고안하였으며, 원칙이 아니라 실용적인 대안에 대해 말하고 있습니다. 단호한 어조를 사용했다고 해서 어떤 원칙에 대해 말하고 있는 것은 아닙니다. 이 글을 읽기 전에 이 시리즈의 이전 글 ‘생산성 높은 리액트 프로젝트: 어니언 아키텍처’를 먼저 읽어보시길 권해드립니다.

부모 계층

어니언 아키텍처에서는 모든 함수들이 위계가 있는 계층 중 어느 한 계층에 속합니다. 하지만 코드 전체에 대해 수직적으로 계층을 나누고 함수의 일반성을 세밀하게 나누어서 어떤 함수가 어떤 계층에 속하는지 판단하는 것은 불가능에 가깝습니다. 우리는 ‘부모 계층’과 ‘자식 계층’이라는 개념을 도입함으로써 이 문제에 대처할 수 있습니다. 부모 계층은 실제의 계층이 아니라 몇 개의 부분적인 계층을 묶어서 만드는 가상의 계층입니다. 자식 계층은 부모 계층에 포함된 계층들을 가리키는데, 부모 계층은 또한 그 외부의 부모 계층에 속하는 자식 계층이 될 수도 있습니다. 어떤 부모 계층의 함수가 다른 부모 계층에 속하는 함수를 호출한다면, 부모 계층 사이의 위계가 성립하게 되고, 내부의 자식 계층들은 고려하지 않은 채로 두 부모 계층들의 관계에만 집중할 수 있습니다.

따라서 부모 계층을 단일한 계층으로 취급함으로써 복잡한 계층 구조를 단순한 몇 개의 계층들의 구조로 간주할 수 있으며, 계층 구조에 대한 관점을 쉽게 전환할 수 있습니다. 전체적인 구조를 조망하고 싶다면 가장 외부의 부모 계층들에만 집중하면 되고, 부분적인 구조를 파악하고 싶다면 해당 부분의 부모 계층들 혹은 자식 계층들만을 살펴볼 수 있습니다.

상이한 위계화 기준

각 부모 계층은 서로 다른 독자적인 기준으로 자식 계층을 위계화할 수 있습니다. 예를 들어, 사용자 관련 계층, 주소 관련 계층, 배송 관련 계층이 있다면, 각 계층은 그 계층들을 포함하는 부모 계층에 속하는 동일한 위계의 자식 계층일 수 있습니다. 그리고 각 계층 또한 자신의 자식 계층들을 가질 수 있으며, 그 자식 계층을 나누는 기준은 다른 계층이 자식 계층을 나주는 기준과 다를 수 있습니다. 사용자 계층의 가장 하위 계층의 일반성과 주소 계층의 가장 하위 계층의 일반성은 다를 수 있습니다. 그럼에도 함수 위계화의 원칙은 깨지지 않으며, 가독성도 저하되지 않습니다.

만약 어떤 도메인이 하나 이상의 다른 도메인을 참조해야 한다면, 그 도메인에 대한 계층은 다른 계층보다 상위의 계층이 되어야 할 것입니다. 이 경우에도 그 계층의 자식 계층들이 구분되는 기준과 그 계층보다 하위의 계층의 자식 계층들이 구분되는 기준이 동일할 필요는 없습니다. 예를 들어, 배송 계층이 사용자 계층보다 상위의 계층이라도, 배송 계층의 가장 하위의 자식 계층이 사용자 계층의 가장 상위의 자식 계층보다 일반성이 더 낮을 필요는 없습니다. 하지만 그러한 ‘일반성 역전’은 가독성을 해칠 가능성이 있기 때문에, 배송 계층의 하위 계층일수록 사용자 계층의 상위 계층의 함수를 호출하지 않는 것이 좋습니다.

둘 이상의 도메인이 개념상으로 상호참조한다면, 이에 대한 계층들을 구조화하기 위해서는 두가지 방법이 있습니다. 첫번째는 둘 이상의 도메인을 하나의 부모 계층의 자식 계층들로 구성하는 것이고, 두번째는 둘 이상의 도메인에 대해 각각의 계층을 구성한 다음, 상호참조를 구현하기 위해 그 계층들보다 상위의 계층을 구성하는 것입니다. 예를 들어, 사용자 도메인, 주소 도메인, 배송 도메인이 서로를 참조해야 한다면, ‘주문’이라는 부모 계층에 사용자, 주소, 배송 도메인에 대한 모든 계층을 자식 계층으로 구성할 수도 있고, 사용자, 주소, 배송에 대한 부모 계층을 각각 구성한 다음, 그 계층들보다 상위 계층인 주문 계층을 통해 상호참조 문제를 해결할 수도 있습니다.

모듈화

부모 계층을 좀 더 효과적으로 적용하기 위해 부모 계층을 ‘모듈화’할 수 있습니다. 부모 계층을 모듈화한다는 것은 어떤 부모 계층이 다른 부모 계층의 함수를 호출할 때, 상위 부모 계층의 함수들은 하위 부모 계층의 최상위 자식 계층의 함수만을 호출해야한다는 것을 의미합니다. 예를 들어, 계층의 구조가 주문, 배송, 사용자 순으로 위계를 가지고 있다면, 주문 계층의 자식 계층에서는 배송 계층과 사용자 계층의 가장 상위 자식 계층의 함수들만을 호출할 수 있습니다.

모듈화는 ‘추상화 벽’과 유사하지만, 추상화 벽이 그보다 하위의 모든 계층에 접근하지 못하게 하는 것과는 달리, 모듈화된 부모 계층은 자식 계층(최상위 이외의 자식 계층)에 대한 접근만을 허용하지 않고, 하위 부모 계층에 대해서는 접근을 허용합니다. 이는 각 부모 계층의 변경 사항이 그 계층의 함수를 호출하는 상위 계층에 주는 영향을 최소화할 수 있다는 것을 의미하고, 추상화 벽의 장점을 극대화시키는 결과를 가져옵니다.

좀 더 엄격하게, 상위의 부모 계층도 최상위의 자식 계층에서만 하위 부모 계층의 함수를 호출할 수 있는 것으로 모듈화를 정의할 수도 있을 것입니다. 그렇게 정의한다면 ‘일반성 역전’로 인한 가독성의 저하 문제도 해결할 수 있고 유지보수성도 더 향상 될 수 있을 것입니다.

ESlint를 이용한 모듈화

ESlint 플러그인 중 하나인 eslint-plugin-stratified-deginstratified-imports rule을 사용하면, 부모 계층을 ’모듈화‘하고, 모듈에 대한 규칙을 지키지 못했을 때 에러가 발생하도록 할 수 있습니다. 어떤 폴더 내에 .stratified.json을 포함시키면 그 폴더는 모듈화되어 부모 계층은 .stratified.json에 설정된 최상위 파일/폴더의 함수에만 접근할 수 있습니다. (상위 부모 계층의 최상위 자식 계층만이 하위 부모 계층의 함수를 호출할 수 있는 ‘엄격한’ 모듈화는 아직 지원되지 않습니다.)

예를 들어, 프로젝트가 아래와 같은 파일 구조를 가지고 있다고 가정해봅시다.

 ┣ parent1
 ┃ ┣ index
 ┃ ┣ childA
 ┃ ┗ childB
 ┗ parent2
   ┣ index
   ┣ entry
   ┣ childA
   ┗ childB

.eslintrc를 아래와 같이 설정하고

{
	"plugins": ["stratified-design"],
	"rules": {
		"stratified-design/stratified-imports": ["error"]
	}
}

최상위 폴더 내의 .stratified.json을 아래와 같이 작성하면

[
	["parent1"],
	["parent2"]
]

parent1/parent2/ 보다 상위 계층이 됩니다. 그리고 parent1/ 내의 .stratified.jsonparent2/ 내의 .stratified.json을 각각 아래와 같이 작성하면

// parent1/.statified.json
[
	["index"],
	["childA"],
	["childB"]
]

// parent2/.statified.json
[
	["index", "entry"],
	["childA"],
	["childB"]
]

parent1/childA, childBparent2/index, entry의 함수는 호출할 수 있지만 childA, childB의 함수는 호출할 수 없게 됩니다.

마치며

복잡한 계층 구조를 가질 수 밖에 없는 프로젝트에서는 전체적인 함수 위계화는 불가능에 가깝습니다. 부모 계층이라는 개념을 도입하면, 어니언 아키텍처의 기본 원칙을 지키면서도 서로 다른 위계화 기준을 가진 계층들을 구성할 수 있습니다. 일반성 역전 현상이 나타날 수 있다는 단점이 있지만, 부모 계층의 모듈화를 도입하면, 추상화 벽의 장점을 극대화할 수 있습니다.

profile
프론트엔드 개발자

0개의 댓글