지난 몇 년 동안, 우리는 시스템 아키텍처에 관한 다양한 아이디어를 봐왔습니다. 이들에는 다음이 포함됩니다:
- Alistair Cockburn에 의해 제안되고 Steve Freeman 및 Nat Pryce가 그들의 멋진 책 "Growing Object Oriented Software"에서 채택한 Hexagonal Architecture (일명 Ports and Adapters)
- Jeffrey Palermo에 의한 Onion Architecture
- 작년 제 블로그에서의 Screaming Architecture
- James Coplien과 Trygve Reenskaug에 의한 DCI
- Ivar Jacobson의 책 Object Oriented Software Engineering: A Use-Case Driven Approach에서 나온 BCE
이 아키텍처들은 세부사항에서 약간씩 다르지만 매우 유사합니다. 모두 관심사의 분리라는 같은 목표를 가지고 있으며, 소프트웨어를 레이어로 나누어 이 분리를 달성합니다. 각각은 최소한 하나의 비즈니스 규칙 레이어와 인터페이스 레이어를 가지고 있습니다.
이 아키텍처들은 다음과 같은 시스템을 만듭니다:
- 프레임워크 독립적. 아키텍처는 특정 라이브러리의 존재에 의존하지 않습니다. 이를 통해 프레임워크를 도구로 사용할 수 있으며, 시스템을 그들의 제한된 제약에 맞출 필요가 없습니다.
- 테스트 가능. 비즈니스 규칙은 UI, 데이터베이스, 웹 서버 또는 기타 외부 요소 없이 테스트할 수 있습니다.
- UI 독립적. UI를 쉽게 변경할 수 있으며, 시스템의 나머지 부분을 변경하지 않고도 웹 UI를 콘솔 UI로 교체할 수 있습니다.
- 데이터베이스 독립적. Oracle이나 SQL Server를 Mongo, BigTable, CouchDB 또는 다른 것으로 교체할 수 있습니다. 비즈니스 규칙은 데이터베이스에 구속되지 않습니다.
- 어떤 외부 기관과도 독립적. 실제로 비즈니스 규칙은 외부 세계에 대해 전혀 모릅니다.
이 아키텍처들을 하나의 실행 가능한 아이디어로 통합하려는 시도가 이 글 상단의 다이어그램에 나타나 있습니다.
의존성 규칙
동심원은 소프트웨어의 다른 영역을 나타냅니다. 일반적으로, 안으로 갈수록 소프트웨어의 수준이 높아집니다. 외부 원은 메커니즘이며, 내부 원은 정책입니다.
이 아키텍처를 작동시키는 주요 규칙은 의존성 규칙입니다. 이 규칙은 소스 코드 의존성이 오직 내부를 향할 수만 있다고 말합니다. 내부 원의 어떤 것도 외부 원에 대해 전혀 알아서는 안 됩니다. 특히, 외부 원에서 선언된 어떤 것의 이름도 내부 원의 코드에 의해 언급되어서는 안 됩니다. 이는 함수, 클래스, 변수 또는 기타 명명된 소프트웨어 엔티티를 포함합니다.
같은 맥락으로, 외부 원에서 사용되는 데이터 포맷은 내부 원에서 사용되어서는 안 됩니다. 특히 그 포맷이 외부 원의 프레임워크에 의해 생성된 경우에는 더욱 그렇습니다. 우리는 외부 원의 어떤 것도 내부 원에 영향을 미치길 원하지 않습니다.
엔티티
엔티티는 기업 전반의 비즈니스 규칙을 캡슐화합니다. 엔티티는 메소드를 가진 객체일 수도 있고, 데이터 구조와 함수의 집합일 수도 있습니다. 중요한 것은 엔티티가 기업 내 다양한 애플리케이션에서 사용될 수 있다는 것입니다.
만약 여러분이 기업용 애플리케이션이 아닌 단일 애플리케이션만 개발하고 있다면, 이 엔티티들은 그 애플리케이션의 비즈니스 객체가 됩니다. 이들은 가장 일반적이고 고수준의 규칙을 캡슐화하며, 외부에서 변화가 생겼을 때 가장 변화될 가능성이 적습니다. 예를 들어, 페이지 내비게이션 또는 보안이 변경되었다고 해서 이 객체들이 영향을 받을 것으로 기대하지 않습니다. 특정 애플리케이션의 운영상의 변경이 엔티티 레이어에 영향을 미쳐서는 안 됩니다.
사용 사례
이 레이어의 소프트웨어는 애플리케이션 특정 비즈니스 규칙을 포함합니다. 이는 시스템의 모든 사용 사례를 캡슐화하고 구현합니다. 이 사용 사례들은 엔티티로부터 데이터의 흐름을 조정하고, 그 엔티티들이 사용 사례의 목표를 달성하기 위해 기업 전체의 비즈니스 규칙을 사용하도록 지시합니다.
이 레이어에서의 변경사항이 엔티티에 영향을 미치지 않을 것으로 예상합니다. 또한, 이 레이어는 데이터베이스, UI 또는 어떤 공통 프레임워크와 같은 외부 변경사항에 영향을 받지 않을 것으로 예상합니다. 이 레이어는 이러한 고려사항으로부터 격리되어 있습니다.
하지만, 애플리케이션의 운영 변경사항이 사용 사례와 따라서 이 레이어의 소프트웨어에 영향을 미칠 것으로 예상합니다. 사용 사례의 세부사항이 변경되면, 이 레이어의 일부 코드가 확실히 영향을 받을 것입니다.
인터페이스 어댑터
이 레이어의 소프트웨어는 사용 사례와 엔티티에 가장 편리한 형식에서 데이터베이스나 웹과 같은 외부 기관에 가장 편리한 형식으로 데이터를 변환하는 일련의 어댑터입니다. 예를 들어, GUI의 MVC 아키텍처는 이 레이어에 전적으로 포함됩니다. 프레젠터, 뷰, 컨트롤러는 모두 여기에 속합니다. 모델은 컨트롤러로부터 사용 사례로, 그리고 다시 사용 사례로부터 프레젠터와 뷰로 전달되는 데이터 구조일 가능성이 높습니다.
마찬가지로, 데이터는 이 레이어에서 엔티티와 사용 사례에 가장 편리한 형태에서 사용되는 영속성 프레임워크에 가장 편리한 형태로 변환됩니다. 즉, 데이터베이스입니다. 이 원 안쪽의 코드는 데이터베이스에 대해 전혀 알아서는 안 됩니다. 데이터베이스가 SQL 데이터베이스인 경우, 모든 SQL은 이 레이어에 제한되어야 하며, 특히 데이터베이스와 관련된 이 레이어의 부분에 제한되어야 합니다.
또한, 외부 서비스와 같은 어떤 외부 형태의 데이터를 사용 사례와 엔티티가 사용하는 내부 형태로 변환하는 데 필요한 어떤 다른 어댑터도 이 레이어에 포함됩니다.
프레임워크와 드라이버
가장 바깥쪽 레이어는 일반적으로 데이터베이스, 웹 프레임워크 등과 같은 프레임워크와 도구로 구성됩니다. 일반적으로 이 레이어에서는 내부 원으로 통신하는 접착 코드 외에는 많은 코드를 작성하지 않습니다.
이 레이어는 모든 세부사항이 가는 곳입니다. 웹은 세부사항입니다. 데이터베이스는 세부사항입니다. 우리는 이러한 것들을 외부에 두어 가장 적은 피해를 입힐 수 있게 합니다.
꼭 네 개의 원이어야 하는가?
아니요, 원은 개념적입니다. 꼭 이 네 개만 필요하다고 할 수는 없습니다. 항상 이 네 개만 있어야 한다는 규칙은 없습니다. 하지만, 의존성 규칙은 항상 적용됩니다. 소스 코드 의존성은 항상 내부를 향합니다. 안으로 갈수록 추상화 수준이 증가합니다. 가장 바깥쪽 원은 낮은 수준의 구체적인 세부사항입니다. 안으로 갈수록 소프트웨어는 더 추상화되며, 더 높은 수준의 정책을 캡슐화합니다. 가장 안쪽 원이 가장 일반적입니다.
경계를 넘나드는 방법
다이어그램의 오른쪽 하단에는 우리가 원 경계를 넘나드는 방법의 예가 있습니다. 컨트롤러와 프레젠터가 다음 레이어의 사용 사례와 통신하는 것을 보여줍니다. 제어 흐름을 주목하세요. 이는 컨트롤러에서 시작하여 사용 사례를 통과한 다음 프레젠터에서 실행을 마칩니다. 또한 소스 코드 의존성을 주목하세요. 각각이 사용 사례를 향해 내부를 가리킵니다.
우리는 보통 이러한 명백한 모순을 의존성 역전 원칙을 사용하여 해결합니다. 예를 들어, Java와 같은 언어에서는 소스 코드 의존성이 경계를 넘어 제어 흐름과 정확히 반대되는 지점에서 인터페이스와 상속 관계를 정리합니다.
예를 들어, 사용 사례가 프레젠터를 호출해야 하지만, 이 호출은 의존성 규칙을 위반하기 때문에 직접적이어서는 안 됩니다: 외부 원의 이름이 내부 원에 의해 언급되어서는 안 됩니다. 그래서 우리는 사용 사례가 내부 원에 있는 인터페이스(여기서는 사용 사례 출력 포트로 표시됨)를 호출하고, 외부 원에 있는 프레젠터가 이를 구현하도록 합니다.
이 같은 기술은 아키텍처의 모든 경계를 넘는 데 사용됩니다. 우리는 동적 다형성을 활용하여 제어 흐름의 방향에 관계없이 의존성 규칙을 준수할 수 있도록 소스 코드 의존성이 제어 흐름과 반대되도록 만듭니다.
경계를 넘는 데이터는 무엇인가
일반적으로 경계를 넘는 데이터는 간단한 데이터 구조입니다. 기본 구조체나 간단한 데이터 전송 객체를 사용하거나, 함수 호출의 인자로 데이터를 전달할 수 있습니다. 또는 해시맵에 데이터를 패킹하거나 객체로 구성할 수 있습니다. 중요한 것은 격리된, 간단한, 데이터 구조가 경계를 넘어 전달된다는 것입니다. 우리는 엔티티나 데이터베이스 행을 전달하는 것을 속임수로 사용하고 싶지 않습니다. 우리는 데이터 구조가 의존성 규칙을 위반하는 어떤 종류의 의존성을 가지고 있기를 원하지 않습니다.
예를 들어, 많은 데이터베이스 프레임워크는 쿼리에 대한 응답으로 편리한 데이터 형식을 반환합니다. 우리는 이를 RowStructure라고 부를 수 있습니다. 우리는 이 행 구조를 경계를 넘어 내부로 전달하고 싶지 않습니다. 그것은 외부 원에 대해 내부 원이 무언가를 알아야 한다는 것을 의미하기 때문에 의존성 규칙을 위반할 것입니다.
그래서 우리가 경계를 넘어 데이터를 전달할 때, 항상 내부 원에 가장 편리한 형태로 전달됩니다.
결론
이 간단한 규칙을 준수하는 것은 어렵지 않으며, 앞으로 많은 두통을 덜어줄 것입니다. 소프트웨어를 레이어로 분리하고 의존성 규칙을 준수함으로써, 본질적으로 테스트 가능한 시스템을 만들게 되며, 이는 모든 이점을 의미합니다. 시스템의 외부 부분이, 예를 들어 데이터베이스나 웹 프레임워크와 같이, 구식이 되었을 때, 그 구식 요소를 최소한의 번거로움으로 교체할 수 있습니다.
[참고] 본 게시글은 아래 글을 번역한 내용입니다.
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html