아키텍처 원칙

이 콘텐츠는 eBook, Architect Modern Web Applications with ASP.NET Core 및 Azure에서 발췌한 것이며, .NET 문서에서 제공되거나 오프라인에서도 읽을 수 있는 PDF(무료 다운로드 가능)로 제공됩니다.

Architect Modern Web Applications with ASP.NET Core and Azure eBook cover thumbnail.

"건축업자가 프로그래머의 프로그램 작성 방식에 따라 건물을 짓는다면 가장 먼저 도착하는 딱따구리가 문명을 파괴할 것입니다."
- Gerald Weinberg

유지 관리를 염두에 두고 소프트웨어 솔루션을 설계 및 디자인해야 합니다. 이 섹션에서 설명한 원칙은 깔끔하고 유지 가능한 애플리케이션으로 이어지는 아키텍처를 결정하는 데 도움이 될 수 있습니다. 일반적으로 이러한 원칙은 애플리케이션의 다른 부분과 긴밀하게 연결되지는 않지만 명시적 인터페이스 또는 메시지 시스템을 통해 통신하는 개별 구성 요소로 애플리케이션을 구축하는 방법을 안내합니다.

일반적인 디자인 원칙

문제의 분리

개발 시 기본 원칙은 문제의 분리입니다. 이 원칙의 핵심은 수행하는 작업의 종류에 따라 소프트웨어를 분리하는 것입니다. 예를 들어 주목할 만한 항목을 식별하여 사용자에게 표시하고, 이러한 항목을 특정 방식에 따라 포맷하여 더욱 눈에 띄게 만드는 논리를 포함하고 있는 애플리케이션이 있다고 가정해 봅시다. 포맷할 항목을 선택하는 책임을 맡은 동작은 항목을 포맷하는 책임을 맡은 동작과 분리되어야 합니다. 왜냐하면 이러한 동작이 서로 관련되는 경우는 거의 없기 때문입니다.

아키텍처 측면에서 볼 때, 핵심 비즈니스 동작을 인프라 및 사용자 인터페이스 논리와 분리하여 애플리케이션이 이 원칙을 따르도록 논리적으로 빌드할 수 있습니다. 비즈니스 규칙 및 논리가 별도의 프로젝트에 있고 애플리케이션의 다른 프로젝트에 종속되지 않는 것이 가장 이상적입니다. 이러한 분리를 사용하면 저수준 구현 세부 정보와 긴밀하게 연결하지 않고도 비즈니스 모델을 쉽게 테스트하고 개선할 수 있습니다(인프라 문제가 비즈니스 계층에 정의된 추상화에 따라 결정되는 경우에도 도움이 됩니다). 문제의 분리는 애플리케이션 아키텍처에서 레이어를 사용할 때 고려해야 할 핵심 사항입니다.

캡슐화

애플리케이션의 여러 부분에서 캡슐화를 사용하여 애플리케이션의 다른 부분과 격리해야 합니다. 애플리케이션 구성 요소 및 레이어는 외부 계약을 위반하지 않는 한 공동 작업자를 중단시키지 않고도 내부 구현을 조정할 수 있어야 합니다. 캡슐화를 적절하게 사용하면 애플리케이션에서 느슨한 결합과 모듈화를 달성하는 데 도움이 됩니다. 동일한 인터페이스가 유지되는 한, 개체 및 패키지를 대체 구현으로 바꿀 수 있기 때문입니다.

클래스에서, 캡슐화는 클래스의 내부 상태에 대한 외부 액세스를 제한하여 달성됩니다. 외부 작업자는 개체 상태를 조작하려는 경우 개체의 전용 상태에 직접 액세스하는 것이 아니라 잘 정의된 함수(또는 속성 setter)를 통해 조작해야 합니다. 마찬가지로, 애플리케이션 구성 요소 및 애플리케이션 자체는 상태를 직접 수정할 수 있도록 허용하는 대신 공동 작업자가 사용할 잘 정의된 인터페이스를 노출해야 합니다. 이 방법을 사용하면 애플리케이션의 내부 디자인이 시간에 따라 개선되더라도 퍼블릭 계약이 유지되는 한 협력자의 작업이 중단될 염려가 없습니다.

변경 가능한 전역 상태는 캡슐화의 반대입니다. 한 함수의 변경 가능한 전역 상태에서 가져 온 값은 다른 함수에서(또는 같은 함수에서도) 동일한 값을 가질 수 없습니다. 변경 가능한 전역 상태 관련 문제의 이해는 C# 같은 프로그래밍 언어가 문에서 클래스에 이르는 모든 곳에서 사용하는 다양한 범위 지정 규칙을 지원하는 대표적인 이유입니다. 애플리케이션 내부 및 애플리케이션 간의 통합에 중앙 데이터베이스를 사용하는 데이터 기반 아키텍처는 데이터베이스가 나타내는 변경 가능한 전역 상태에 의존하기로 선택한다는 사실에 유의해야 합니다. 도메인 기반 디자인 및 클린 아키텍처의 핵심 고려 사항은 데이터에 대한 액세스를 캡슐화하는 방법과, 애플리케이션 상태가 관련 지속성 형식에 대한 직접 액세스에 의해 무효화되지 않게 하는 방법입니다.

종속성 반전

애플리케이션 내에서 종속성의 방향은 구현 세부 정보가 아닌 추상화 방향에 있어야 합니다. 대부분의 애플리케이션은 컴파일 시간 종속성을 런타임 실행 방향으로 전달하여 직접 종속성 그래프를 생성하도록 작성됩니다. 즉, 클래스 A가 클래스 B의 메서드를 호출하고 클래스 B가 클래스 C의 메서드를 호출하는 경우 그림 4-1에 표시된 대로 컴파일 시간에 클래스 A는 클래스 B에 종속되고 클래스 B는 클래스 C에 종속됩니다.

Direct dependency graph

그림 4-1. 직접 종속성 그래프.

종속성 반전 원칙을 적용하면 A는 B가 구현하는 추상화에 대해 메서드를 호출할 수 있고, 런타임에 A가 B를 호출할 수 있게 되지만 B는 컴파일 시간에 A에 의해 제어되는 인터페이스에 종속됩니다. (따라서 일반적인 컴파일 시간 종속성이 ‘반전’됩니다.) 런타임에 프로그램 실행 흐름은 그대로 유지되지만, 인터페이스가 도입된다는 것은 이러한 인터페이스의 여러 구현을 손쉽게 연결할 수 있다는 의미입니다.

Inverted dependency graph

그림 4-2. 반전된 종속성 그래프.

종속성 반전은 느슨하게 결합된 애플리케이션을 빌드하기 위한 핵심 부분입니다. 상위 수준 추상화에 종속되고 상위 수준 추상화를 구현하도록(그 반대가 아니라) 구현 세부 사항을 작성할 수 있기 때문입니다. 그 결과로 얻게 되는 애플리케이션은 테스트 용이성, 모듈성 및 유지 관리 용이성이 더 우수합니다. 종속성 주입의 실행은 종속성 반전 원칙에 따라 가능합니다.

명시적 종속성

메서드 및 클래스는 올바르게 작동하는 데 필요한 공동 개체를 명시적으로 요구해야 합니다. 명시적 종속성 원칙이라 불립니다. 클래스 생성자는 클래스가 유효한 상태를 유지하고 올바르게 작동하기 위해 필요한 항목을 식별하는 기회를 제공합니다. 작성 및 호출이 가능하지만 특정 전역 또는 인프라 구성 요소가 작동하는 경우에만 올바르게 작동하는 클래스를 정의하면 이러한 클래스는 클라이언트에 대해 정직하지 않습니다. 생성자 계약은 클라이언트에 지정된 것만 필요하다고 알려주지만(클래스에서 매개 변수 없는 생성자를 사용하는 경우 아무 것도 필요하지 않을 수 있음), 그 후 런타임에 개체에 다른 것이 필요하게 됩니다.

명시적 종속성 원칙을 따르면 클래스와 메서드는 작동하는 데 필요한 것들을 클라이언트에 정직하게 전달합니다. 이 원칙을 따르면 사용자가 메서드 또는 생성자 매개 변수의 형태로 필요한 것을 제공하는 한, 사용자가 작업하는 개체가 런타임에 올바르게 작동한다고 믿을 수 있으므로 코드가 좀 더 자체 문서화되고 코딩 계약이 좀 더 사용자 친화적으로 됩니다.

단일 책임

단일 책임 원칙은 개체 지향 디자인에 적용되지만, 문제의 분리와 비슷하게 아키텍처 원칙으로 간주할 수도 있습니다. 개체의 책임은 하나여야 하고 변경 사유가 하나여야 합니다. 특히 개체가 한 가지 책임을 수행하는 방식을 업데이트해야 하는 경우에만 개체를 변경해야 합니다. 이 원칙을 따르면 기존 클래스에 책임을 추가하는 대신 여러 종류의 새 동작을 새 클래스로 구현할 수 있으므로 좀 더 느슨하게 결합된 모듈식 시스템을 만드는 데 도움이 됩니다. 새 클래스를 추가하는 방법은 언제나 기존 클래스를 변경하는 방법보다 안전합니다. 어떤 코드도 새 클래스에 종속되지 않기 때문입니다.

모놀리식 애플리케이션의 경우 애플리케이션의 상위 계층에서 레이어에 단일 책임 원칙을 적용할 수 있습니다. 프레젠테이션 책임은 UI 프로젝트에 남아 있어야 하고, 데이터 액세스 책임은 인프라 프로젝트 내에 남아 있어야 합니다. 비즈니스 논리는 쉽게 테스트하고 다른 책임과 독립적으로 발전할 수 있도록 애플리케이션 코어 프로젝트에 남아 있어야 합니다.

이 원칙을 애플리케이션 아키텍처에 적용하고 논리적 엔드포인트까지 확대하면 마이크로 서비스를 얻게 됩니다. 지정된 마이크로 서비스에는 단일 책임이 있어야 합니다. 시스템 동작을 확장해야 하는 경우 일반적으로 기존 마이크로 서비스에 책임을 추가하는 것보다는 마이크로 서비스를 추가하는 것이 더 좋습니다.

마이크로 서비스 아키텍처에 대해 자세히 알아보기

DRY(반복 금지)

애플리케이션이 여러 위치에서 특정 개념과 관련된 동작을 지정하면 안 됩니다. 이는 종종 오류의 원인이 됩니다. 어느 시점에서 요구 사항이 바뀌면 이 동작을 변경해야 합니다. 동작의 인스턴스를 하나 이상 업데이트하지 못하고 시스템이 일관되지 않은 방식으로 동작하게 될 수 있습니다.

논리를 복제하는 대신 프로그래밍 구문에 캡슐화해야 합니다. 이 동작을 통해 단일 기관을 생성하고, 이 동작이 필요한 애플리케이션의 다른 부분에서는 새 구문을 사용하게 합니다.

참고 항목

우연히 반복되는 동작은 함께 바인딩하지 마세요. 예를 들어 두 상수가 같은 값을 갖고 있다고 해도 개념적으로 서로 다른 것을 참조한다면 상수가 하나만 있어야 하는 것은 아닙니다. 중복은 항상 잘못된 추상화와 결합하는 것이 좋습니다.

지속성 무시

PI(지속성 무시)는 지속되어야 하는 형식을 참조하지만, 그 코드는 선택하는 지속성 기술의 영향을 받지 않습니다. .NET에서는 이러한 형식을 POCO(Plain Old CLR Object)라고 부르기도 합니다. 특정 기본 클래스에서 상속하거나 특정 인터페이스를 구현할 필요가 없기 때문입니다. 지속성 무시는 동일한 비즈니스 모델을 여러 가지 방식으로 지속할 수 있도록 허용하여 애플리케이션의 유연성을 향상하므로 유용합니다. 지속성 선택은 시간에 따라 한 데이터베이스 기술에서 다른 데이터베이스 기술로 달라질 수 있으며, 애플리케이션이 시작된 형식(예를 들어 관계형 데이터베이스 외에도 Redis 캐시 또는 Azure Cosmos DB 사용) 외에도 추가적인 지속성 형식이 필요할 수 있습니다.

다음은 이 원칙에 위배되는 몇 가지 예입니다.

  • 필수 기본 클래스.

  • 필수 인터페이스 구현.

  • 자신을 저장할 책임이 있는 클래스(예: 활성 레코드 패턴).

  • 매개 변수 없는 생성자가 필요합니다.

  • 가상 키워드가 필요한 속성.

  • 지속성 관련 필수 특성.

위의 기능 또는 동작을 갖고 있는 클래스의 요구 사항은 지속될 형식과 선택하는 지속성 기술 간의 결합을 추가하므로, 나중에 새로운 데이터 액세스 전략을 채택하기가 더 어려워집니다.

바인딩된 컨텍스트

바인딩된 컨텍스트는 도메인 중심 디자인의 중심 패턴입니다. 대규모 애플리케이션 또는 조직의 복잡성을 별도의 개념 모듈로 분할하여 해결하는 방법을 제공합니다. 그 후 각 개념적 모듈은 다른 컨텍스트와 분리된(따라서 바인딩된) 컨텍스트를 나타내며, 독립적으로 발전할 수 있습니다. 바인딩된 각 컨텍스트가 내부의 고유한 개념 이름을 자유롭게 선택할 수 있는 것이 가장 이상적이며, 자체 지속성 저장소에 배타적으로 액세스할 수 있어야 합니다.

적어도 개별 웹 애플리케이션은 다른 애플리케이션과 데이터베이스를 공유하지 않고 자신의 비즈니스 모델에 대한 자체 지속성 저장소를 통해 자체 바인딩된 컨텍스트가 되기 위해 노력해야 합니다. 바인딩된 컨텍스트 간의 통신은 공유 데이터베이스가 아닌 프로그래밍 방식 인터페이스를 통해 발생하며, 발생하는 변경 내용에 대응하여 비즈니스 논리 및 이벤트가 발생하는 것을 허용합니다. 바인딩된 컨텍스트는 마이크로 서비스와 긴밀하게 매핑되며, 또한 자체적인 개별 바인딩된 컨텍스트로 이상적으로 구현됩니다.

추가 리소스