본문 바로가기
카테고리 없음

클린 아키텍처:소프트웨어 구조와 설계의 원칙(Clean Architecture) - 6부 : 세부사항 (34장)

by 라꾸스떼(YR) 2020. 3. 28.
반응형

[34장] 빠져 있는 장

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

 

[계층 기반 패키지]

가장 단순한 첫 번째 설계 방식은 전통적인 수평 계층형 아키텍처다. 기술적인 관점에서 해당 코드가 하는 일에 기반해 그 코드를 분할한다. ‘계층 기반 패키지’

코드는 계층이라는 얇은 수평 조각으로 나뉘며, 각 계층은 유사한 종류의 것들을 묶는 도구로 사용된다. ‘엄격한 계층형 아키텍처’의 경우 계층은 반드시 바로 아래 계층에만 의존해야 한다. 이 아키텍처는 엄청난 복잡함을 겪지 않고도 무언가를 작동시켜 주는 아주 빠른 방법이다. 문제는, 소프트웨어가 커지고 복잡해지기 시작하면 머지 않아 큰 그릇 세 개만으로는 모든 코드를 담기엔 부족하다는 사실을 깨닫고, 더 잘게 모듈화해야 할지를 고민하게 될 것이다. 계층형 아키텍처는 업무 도메인에 대해 아무것도 말해주지 않는다는 문제도 있다.

 

[기능 기반 패키지]

코드를 조직화하는 또 다른 선택지로 ‘기능 기반 패키지’ 구조도 있다. 이는 서로 연관된 기능, 도메인 개념, 또는 Aggregate Root에 기반하여 수직의 얇은 조각으로 코드를 나누는 방식이다. 인터페이스와 클래스는 이전과 같지만, 모두가 단 하나의 패키지에 속하게 된다. 이는 ‘계층 기반 패키지’를 아주 간단히 리팩터링한 형태지만, 이제 코드의 상위 수준 구조가 업무 도메인에 대해 무언가를 알려주게 된다. 유스케이스가 변경될 경우 변경해야 할 코드를 모두 찾는 작업이 더 쉬워질 수 있다. 변경해야 할 코드가 여러 군데 퍼져 있지 않고 모두 한 패키지에 담겨 있기 때문이다. 나는 소프트웨어 개발팀이 수평화 계층화(계층 기반 패키지)의 문제를 깨닫고, 수직적 계층화(기능 기반 패키지)로 전환하는 걸 자주 목격했다. 내가 보기에 두 접근법은 모두 차선책이다.

 

[포트와 어댑터]

'포트와 어댑터(Ports and Adapters' 혹은 육각형 아키텍처(Hexagonal Architecutre)', '경계, 컨트롤러, 엔티티(BCE)' 등의 방식으로 접근하는 이유는 업무/도메인에 초점을 둔 코드가 프레임워크나 데이터베이스 같은 기술적인 세부 구현과 독립적이며 분리된 아키텍처를 만들기 위해서다. 그런 코드 베이스는 '내부'(도메인)와 '외부'(인프라)로 구성됨을 흔히 볼 수 있다. '내부' 영역은 도메인 개념을 모두 포함하는 반면, '외부' 영역은 외부 세계와의 상호작용을 포함한다. 여기서 주요 규칙은 바로 '외부'가 '내부'에 의존하며, 절대 그 반대로는 안 된다는 점이다.

 

[컴포넌트 기반 패키지]

엄격한 계층형 아키텍처에서는 의존선 화살표는 항상 아래를 향해야 하며, 각 계층은 반드시 바로 아래 계층에만 의존해야 한다. 이런 방식으로 멋지고 깔끔한 비순환 의존성 그래프를 만들 수 있을 거라 생각할 수 있지만, 정말로 코드 베이스의 요소들이 서로 의존할 때는 몇 가지 규칙을 반드시 지켜야 한다. 그런데 여기에는 큰 문제가 있다. 속임수를 써서 몇몇 의존성을 의도치 않은 방식으로 추가허달도, 보기에는 여전히 좋은 비순환 의존성 그래프가 생성된다는 사실이다.

'컴포넌트 기반 패키지'를 도입해야 하는 이유는 바로 이 때문이다. 이 접근법은 지금까지 우리가 본 모든 것들을 혼합한 것으로, 큰 단위의 단일 컴포넌트와 관련된 모든 책임을 하나의 자바 패키지로 묶는 데 주안점을 둔다. 이 접근법은 서비스 중심적인 시각으로 소프트웨어 시스템을 바라보며, 마이크로서비스 아키텍처가 가진 시각과도 동일하다. 포트와 어댑터에서 웹을 그저 또 다른 전달 메커니즘으로 귀브하는 것과 마찬가지로, 컴포너트 기반 패키지에서도 사용자 인터페이스를 큰 단위의 컴포넌트로부터 분리해서 유지한다.

본질적으로 이 접근법에서는 '업무 로직'과 영속성 관련 코드를 하나로 묶는데, 이 묶음을 나는 '컴포넌트'라고 부른다. 엉클 밥은 "컴포넌트는 배포 단위다. 컴포넌트는 시스템의 구성 요소로, 배포할 수 있는 가장 작은 단위다."라고 정의 했지만, 컴포넌트에 대한 내 정의는 약간 다르다. "컴포넌트는 멋지고 깔끔한 인터페이스로 감싸진 연관된 기능들의 묶음으로, 애플리케이션과 같은 실행 환경 내부에 존재한다."

소프트웨어 시스템의 정적 구조를 컨테이너, 컴포넌트, 클래스(또는 코드)의 측면에서 계층적으로 생각하는 간단한 방벙비다. 이 방법론에서 소프트웨어 시스템은 하나 잇ㅇ의 컨테이너로 구성되며, 각 컨테이너는 하나 이상의 컴포넌트를 포함한다. 또한 각 컴포넌트는 하나 이상의 클래스(또는 코드)로 구현된다.

컴포넌트 내부에서 관심사의 분리는 여전히 유효하며, 따라서 업무 로직은 데이터 영속성과 분리되어 있다. 하지만 이는 컴포넌트 구현과 관련된 세부사항으로, 사용자는 알 필요가 없다. 이는 마이크로서비스나 서비스 지향 아키텍처를 적용했을 때 얻는 이점과도 유사하다. 모노리틱 애플리케이션에서 컴포넌트를 잘 정의하면 마이크로서비스 아키텍처로 가기 위한 발판으로 삼을 수 있다.

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

모든 타입에서 public 지시자를 사용한다는 건 사용하는 프로그래밍 언어가 제공하는 캡슐화 관련 이점을 활용하지 않겠다는 뜻이다. 이로 인해 누군가가 구체적인 구현 클래스의 인스턴스를 직접 생성하는 코드를 작성하는 일을 절대 막을 수 없으니, 결국 당신이 지향하는 아키텍처 스타일을 위반하게 될 것이다.

 

[조직화 vs. 캡슐화]

아키텍처 원칙을 강제할 때 자기 규율이나 컴파일 후처리 도구를 이용하지 말고, 반드시 컴파일러에 의지할 것을 권장한다.

 

[다른 결합 분리 모드]

소스 코드 수준에서 의존성을 분리하는 방법도 있다. 정확하게는 서로 다른 소스 코드 트리로 분리하는 방법이다.

-업무와 도메인용 소스 코드(즉, 선택된 기수링나 프레임워크와는 독립적인 모든 것)

-웹용 소스 코드

-데이터 영속성용 소스 코드

하지만 이는 너무 이상적인 해결책이다. 현실에서 소스 코드를 이처럼 나누다 보면 성능, 복잡성, 유지보수 문제가 생기기 때문이다.

포트와 어댑터 접근법을 적용할 때는 이보다 간단한 방법을 사용하기도 하는데, 단순히 소스 코드 트리를 두 개만 만드는 것이다.

-도메인 코드('내부')

-인프라 코드('외부')

인프라는 도메인에 대해 컴파일 시점의 의존성을 가진다.

이 접근법은 소스 코드를 조직화할 때 효과가 있겠지만, 잠재적으로 절충해야 할 부분이 있음을 알고 있어야만 한다. 나는 이를 '포트와 어댑터에 대한 페리페리크 안티 패턴'이라고 부른다. 인프라 코드를 단일 소스 코드에 모두 모아둔다는 말은 애플리케이션에서 특정 영역에 있는 인프라 코드가 애플리케이션의 다른 영역에 있는 코드를 직접 호출할 수 있다는 뜻이다. 도메인을 통하지 않고 말이다.

 

[결론:빠져 있는 조언]

이 장은 최적의 설계를 꾀했더라도, 구현 전략에 얽힌 복잡함을 고려하지 않으면 설계가 순식간에 망가질 수도 있다는 사실을 강조하는 데 그 목적이 있다. 설계를 어떻게 해야만 원하는 코드 구조로 매핑할 수 있을지, 그 코드를 어떻게 조직화할지, 런타임과 컴파일타임에 어떤 결합 분리 모드를 적용할지를 고민하라. 가능하다면 선택사항을 열어두되, 실용주의적으로 행하라. 선택된 아키텍처 스타일을 강제하는 데 컴파일러의 도움을 받을 수 있을지를 고민하며, 데이터 모델과 같은 다른 영역에 결합되지 않도록 주의하라. 구현 세부사항에는 항상 문제가 있는 법이다.

반응형

댓글