본문 바로가기
프로그래밍 놀이터/디자인 패턴, 리펙토링

[책 정리] 34. 빠져 있는 장 - Clean Architecture

by 돼지왕 왕돼지 2022. 11. 15.
반응형

-

소프트웨어는 올바르게 정의된 경계, 명확한 책임, 그리고 통제된 의존성을 가진 클래스와 컴포넌트로 구성되어야 한다.

하지만 악마는 항상 디테일(구현 세부사항)에 있는 법이며, 이점을 심사숙고하지 않는다면 마지막 고비에 걸려 넘어지기 십상일 것이다.

 

 

 

계층 기반 패키지

 

-

코드는 계층이라는 얇은 수평 조각으로 나뉘며, 각 계층은 유사한 종류의 것들을 묶는 도구로 사용된다.

'엄격한 계층형 아키텍처'의 경우 계층은 반드시 바로 아래 계층에만 의존해야 한다.

 

 

-

계층형 아키텍처는 엄청난 복잡함을 겪지 않고도 무언가를 작동시켜 주는 아주 빠른 방법이다.

문제는 소프트웨어가 커지고 복잡해지기 시작하면, 머지 않아 큰 그릇 몇 개 만으로는 모든 코드를 담기엔 부족하다는 사실을 깨닫고, 더 잘게 모듈화해야 할지를 고민하게 될 것이다.

 

 

-

계층형 아키텍처는 업무 도메인에 대해 아무것도 말해주지 않는다는 문제도 있다.

전혀 다른 업무 도메인이라도 코드를 계층형 아키텍처로 만들어서 나란히 놓고 보면, 웹, 서비스, 리포지터리로 구성된 모습이 기분 나쁠 정도로 비슷하게 보일 것이다.

 

 

 

기능 기반 패키지

 

-

기능 기반 패키지 구조는 서로 연관된 기능, 도메인 개념, 또는 (도메인 주도 설계 용어를 사용한다면) Aggregate Root 에 기반하여 수직의 얇은 조각으로 코드를 나누는 방식이다.

하나의 기능이 하나의 자바 패키지에 속하며 패키지 이름은 그 안에 담긴 개념을 반영해 짓는다.

 

 

-

나는 소프트웨어 개발팀이 수평적 계층화(계층 기반 패키지)의 문제를 깨닫고, 수직적 계층화(기능 기반 패키지)로 전환하는 걸 자주 목격했다.

그러나 내가 보기에 두 접근법은 모두 차선책이다.

 

 

 

포트와 어댑터

 

-

엉클 밥에 따르면 '포트와 어댑터(Ports and Adapters)' 혹은 '육각형 아키텍처(Hexagonal Architecture)', '경계, 컨트롤러, 엔티티(BCE)' 등의 방식으로 접근하는 이유는 업무/도메인에 초점을 둔 코드가 프레임워크나 데이터베이스 같은 기술적인 세부 구현과 독립적이며 분리된 아키텍처를 만들기 위해서다.

요약하자면 그런 코드 베이스는 내부(도메인)와 외부(인프라)로 구성됨을 흔히 볼 수 있다.

 

 

-

내부 영역은 도메인 개념을 모두 포함하는 반면, 외부 영역은 외부 세계(예를 들면 UI, 데이터베이스, 서드파티 통합)와의 상호작용을 포함한다.

여기서 주요 규칙은 바로 외부가 내부에 의존하며, 절대 그 반대로는 안 된다는 점이다.

 

 

-

Orders 라는 이름은 도메인 주도 설계라는 세계관에서 비롯된 명명법으로, 도메인 주도 설계에서는 내부에 존재하는 모든 것의 이름은 반드시 유비쿼터스 도메인 언어(uniquitous domain language)' 관점에서 기술하라고 조언한다.

바꿔 말하면 도메인에 대해 논의할 때 우리는 '주문' 에 대해 말하는 것이지, '주문 리포지터리'에 대해 말하는 것이 아니다.

 

 

 

컴포넌트 기반 패키지

 

-

계층형 아키텍처의 목적은 기능이 같은 코드끼리 서로 분리하는 것이다.

웹 관련 코드는 업무 로직으로부터 분리하고, 업무 로직은 다시 데이터 접근으로부터 분리한다.

구현 관점에서 보면 각 계층은 일반적으로 자바 패키지에 해당한다.

 

엄격한 계층형 아키텍처에서는 의존성 화살표는 항상 아래를 향해야 하며, 각 계층은 반드시 바로 아래 계층에만 의존해야 한다.

이 방식으로는 이상한 비순환 의존성 그래프가 생성되기 쉽다.

 

 

-

많은 팀들이 "우리는 훌륭한 규율, 코드리뷰를 통해서 특정 원칙을 강제합니다. 우리는 개발자를 믿습니다." 라며 쉽게 말한다.

이 같은 신뢰는 듣기에는 좋지만, 자금이 바닥나거나 납기가 다가오면 무슨 일이 벌어지는지를 우리는 이미 잘 알고 있다.

 

 

-

훨씬 적은 수의 팀만이 빌드 시 정적 분석 도구를 사용해 아키텍처적인 위반 사항이 없는지를 검사하여 자동으로 강제한다.

대체로 정규 표현식이나 와일드카드 문자열로 표현되며 **/web 페키지에 있는 타입은 절대로 **/data 에 있는 타입에 접근해서는 안 된다라는 형태로 정의된다.

이들 규칙은 컴파일 단계가 끝난 후 실행된다.

 

 

-

두 접근법 모두 오류가 있을 수 있지만, 손을 쓰지 않고 그대로 두면 문제는 더 커진다.

개인적으로 가능하면 컴파일러를 사용해서 아키텍처를 강제하는 방식을 선호한다.

 

 

-

컴포넌트 기반 패키지는 큰 단위(coarse-grained)의 단일 컴포넌트와 관련된 모든 책임을 하나의 자바 패키지로 묶는 데 주안을 둔다.

이 접근법은 서비스 중심적인 시각으로 소프트웨어 시스템을 바라보며, 마이크로서비스 아키텍처가 가진 시각과도 동일하다.

포트와 어댑터에서 웹을 그저 또 다른 전달 매커니즘으로 취급하는 것과 마찬가지로, 컴포넌트 기반 패키지에서도 사용자 인터페이스를 큰 단위의 컴포넌트로부터 분리해서 유지한다.

 

 

-

엉클 밥은 컴포넌트에 대해 아래와 같이 정의했다.

 

컴포넌트는 배포 단위다.

컴포넌트는 시스템의 구성 요소로, 배포할 수 있는 가장 작은 단위다.

자바의 경우 jar 파일이 컴포넌트다.

 

 

-

컴포넌트는 멋지고 깔끔한 인터페이스로 감싸진 연관된 기능들의 묶음으로, 앱과 같은 실행 환경 내부에 존재한다.

 

 

-

소프트웨어 시스템은 하나 이상의 컨테이너(웹 앱, 모바일 앱, 독립 앱, 데이터베이스, 파일 시스템 등)로 구성되며, 각 컨테이너는 하나 이상의 컴포넌트를 포함한다.

또한 각 컴포넌트는 하나 이상의 클래스(또는 코드)로 구현된다.

이 때 각 컴포넌트가 개별 jar 파일로 분리될지 여부는 직교적인 관심사(orthogonal concern)다.

 

 

 

구현 세부사항엔 항상 문제가 있다.

 

-

모든 타입에서 public 지시자를 사용한다는 건 사용하는 프로그래밍 언어가 제공하는 캡슐화 관련 이점을 활용하지 않겠다는 뜻이다.

이로 인해 누군가가 구체적인 구현 클래스의 인스턴스를 직접 생성하는 코드를 작성하는 일을 절대 막을 수 없으니, 결국 당신이 지향하는 아키텍처 스타일을 위반하게 될 것이다.

 

 

 

조직화 vs. 캡슐화

 

-

public 타입을 코드 베이스 어디에서도 사용할 수 있다면 패키지를 사용하는 데 따른 이점이 거의 없다.

따라서 사실상 패키지를 사용하지 않는 것과 같다.

패키지를 무시해 버리면 (캡슐화나 은닉을 하는 데 아무런 도움도 되지 않으므로) 최종적으로 어떤 아키텍처 스타일로 만들려고 하는지는 아무런 의미가 없어진다.

 

 

 

다른 결합 분리 모드

 

-

프로그래밍 언어가 제공하는 방법 외에도 소스 코드 의존성을 분리하는 방법은 존재할 수 있다.

예를 들어 자바에는 OSGi 같은 모듈 프레임워크나 자바9에서 제공하는 새로운 모듈 시스템이 있다.

모듈 시스템을 제대로 사용하면 public 타입과 외부에 공표할 타입을 분리할 수 있다.

모든 타입을 public 으로 지정하더라도, 그 중 일부 타입만을 외부에서 사용할 수 있도록 공표할 수 있다.

 

 

-

이상적으로는 떨어져야 하는 코드들을 트리로 만드는 것이다.

다시 말하면 트리 node 들을 모두 분리해서 모듈이나 프로젝트 단위로 만들어야 한다.

그리고 빌드 도구(maven, gradle, MSBuild)를 통해 프로젝트를 구성시켜야 한다.

하지만 이는 너무 이상적인 해결책이다. 성능, 복잡성, 유지보수 문제가 생긴다.

 

 

-

간단한 방법으로는 2개의 소스 코드 트리만 만들 수 있다.

도메인 코드(내부), 인프라 코드(외부).

이 접근법은 소스 코드를 조직화할 떄 효과가 있겟지만, 잠재적으로 절충해야 할 부분이 있다.

이를 '포트와 어댑터에 대한 페리페리크(Peripherique) 안티 패턴'이라 부른다.

인프라 코드를 단일 소스 코드에 모두 모아둔다는 말은 앱에서 특정 영역(예를 들어 웹 컨트롤러)에 있는 인프라 코드가 앱의 다른 영역(예를 들어 DB 리포지터리)에 있는 코드를 직접 호출 할 수 있다는 뜻이다. 도메인을 통하지 않고 말이다.

해당 코드에 적절한 접근 지시자를 적용하는 걸 잊어버린 경우라면 이러한 호출을 막기는 더욱 힘들다.

 

 

 

결론: 빠져 있는 조언

 

-

설계를 어떻게 해야만 원하는 코드 구조로 매핑할 수 있을지, 그 코드를 어떻게 조직화할지, 런타임과 컴파일타임에 어떤 결합 분리 모드를 적용할지를 고민하라.

가능하다면 선택사항은 열어두되, 실용주의적으로 행하라.

그리고 팀의 규모, 기술 수준, 해결책의 복잡성을 일정과 예산이라는 제약과 동시에 고려하라.

또한 선택된 아키텍처 스타일을 강제하는 데 컴파일러의 도움을 받을 수 있을지를 고민하며, 데이터 모델과 같은 다른 영역에 결합되지 않도록 주의하라.

구현 세부사항에는 항상 문제가 있는 법이다.

 

 

반응형

댓글