아키텍처 원칙Architectural principles

"건축업자가 프로그래머의 프로그램 작성 방식에 따라 건물을 짓는다면 가장 먼저 도착하는 딱따구리가 문명을 파괴할 것입니다.""If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization."
- Gerald Weinberg- Gerald Weinberg

유지 관리를 염두에 두고 소프트웨어 솔루션을 설계 및 디자인해야 합니다.You should architect and design software solutions with maintainability in mind. 이 섹션에서 설명한 원칙은 깔끔하고 유지 가능한 애플리케이션으로 이어지는 아키텍처를 결정하는 데 도움이 될 수 있습니다.The principles outlined in this section can help guide you toward architectural decisions that will result in clean, maintainable applications. 일반적으로 이러한 원칙은 애플리케이션의 다른 부분과 긴밀하게 연결되지는 않지만 명시적 인터페이스 또는 메시지 시스템을 통해 통신하는 개별 구성 요소로 애플리케이션을 구축하는 방법을 안내합니다.Generally, these principles will guide you toward building applications out of discrete components that are not tightly coupled to other parts of your application, but rather communicate through explicit interfaces or messaging systems.

일반적인 디자인 원칙Common design principles

문제의 분리Separation of concerns

개발 시 기본 원칙은 문제의 분리입니다.A guiding principle when developing is Separation of Concerns. 이 원칙의 핵심은 수행하는 작업의 종류에 따라 소프트웨어를 분리하는 것입니다.This principle asserts that software should be separated based on the kinds of work it performs. 예를 들어 주목할 만한 항목을 식별하여 사용자에게 표시하고, 이러한 항목을 특정 방식에 따라 포맷하여 더욱 눈에 띄게 만드는 논리를 포함하고 있는 애플리케이션이 있다고 가정해 봅시다.For instance, consider an application that includes logic for identifying noteworthy items to display to the user, and which formats such items in a particular way to make them more noticeable. 포맷할 항목을 선택하는 책임을 맡은 동작은 항목을 포맷하는 책임을 맡은 동작과 분리되어야 합니다. 왜냐하면 이러한 동작이 서로 관련되는 경우는 거의 없기 때문입니다.The behavior responsible for choosing which items to format should be kept separate from the behavior responsible for formatting the items, since these behaviors are separate concerns that are only coincidentally related to one another.

아키텍처 측면에서 볼 때, 핵심 비즈니스 동작을 인프라 및 사용자 인터페이스 논리와 분리하여 애플리케이션이 이 원칙을 따르도록 논리적으로 빌드할 수 있습니다.Architecturally, applications can be logically built to follow this principle by separating core business behavior from infrastructure and user-interface logic. 비즈니스 규칙 및 논리가 별도의 프로젝트에 있고 애플리케이션의 다른 프로젝트에 종속되지 않는 것이 가장 이상적입니다.Ideally, business rules and logic should reside in a separate project, which should not depend on other projects in the application. 이러한 분리 덕분에 비즈니스 모델을 쉽게 테스트할 수 있으며 하위 수준 구현 세부 정보와 긴밀하게 결합하지 않고도 비즈니스 모델이 발전할 수 있습니다.This separation helps ensure that the business model is easy to test and can evolve without being tightly coupled to low-level implementation details. 문제의 분리는 애플리케이션 아키텍처에서 레이어를 사용할 때 고려해야 할 핵심 사항입니다.Separation of concerns is a key consideration behind the use of layers in application architectures.

캡슐화Encapsulation

애플리케이션의 여러 부분에서 캡슐화를 사용하여 애플리케이션의 다른 부분과 격리해야 합니다.Different parts of an application should use encapsulation to insulate them from other parts of the application. 애플리케이션 구성 요소 및 레이어는 외부 계약을 위반하지 않는 한 공동 작업자를 중단시키지 않고도 내부 구현을 조정할 수 있어야 합니다.Application components and layers should be able to adjust their internal implementation without breaking their collaborators as long as external contracts are not violated. 캡슐화를 적절하게 사용하면 애플리케이션에서 느슨한 결합과 모듈화를 달성하는 데 도움이 됩니다. 동일한 인터페이스가 유지되는 한, 개체 및 패키지를 대체 구현으로 바꿀 수 있기 때문입니다.Proper use of encapsulation helps achieve loose coupling and modularity in application designs, since objects and packages can be replaced with alternative implementations so long as the same interface is maintained.

클래스에서, 캡슐화는 클래스의 내부 상태에 대한 외부 액세스를 제한하여 달성됩니다.In classes, encapsulation is achieved by limiting outside access to the class's internal state. 외부 작업자는 개체 상태를 조작하려는 경우 개체의 전용 상태에 직접 액세스하는 것이 아니라 잘 정의된 함수(또는 속성 setter)를 통해 조작해야 합니다.If an outside actor wants to manipulate the state of the object, it should do so through a well-defined function (or property setter), rather than having direct access to the private state of the object. 마찬가지로, 애플리케이션 구성 요소 및 애플리케이션 자체는 상태를 직접 수정할 수 있도록 허용하는 대신 공동 작업자가 사용할 잘 정의된 인터페이스를 노출해야 합니다.Likewise, application components and applications themselves should expose well-defined interfaces for their collaborators to use, rather than allowing their state to be modified directly. 이렇게 하면 공용 계약이 유지되는 한, 이렇게 해도 공동 작업자가 중단될 걱정 없이 시간이 지나면 애플리케이션의 내부 디자인이 발전합니다.This frees the application's internal design to evolve over time without worrying that doing so will break collaborators, so long as the public contracts are maintained.

종속성 반전Dependency inversion

애플리케이션 내에서 종속성의 방향은 구현 세부 정보가 아닌 추상화 방향에 있어야 합니다.The direction of dependency within the application should be in the direction of abstraction, not implementation details. 대부분의 애플리케이션은 컴파일 시간 종속성을 런타임 실행 방향으로 전달하여 직접 종속성 그래프를 생성하도록 작성됩니다.Most applications are written such that compile-time dependency flows in the direction of runtime execution, producing a direct dependency graph. 즉, 모듈 A가 모듈 B의 함수를 호출하고 모듈 B는 모듈 C의 함수를 호출하는 경우 그림 4-1처럼 컴파일 시간에 A는 B에 종속되고 B는 C에 종속됩니다.That is, if module A calls a function in module B, which calls a function in module C, then at compile time A will depend on B, which will depend on C, as shown in Figure 4-1.

직접 종속성 그래프

그림 4-1.Figure 4-1. 직접 종속성 그래프.Direct dependency graph.

종속성 반전 원칙을 적용하면 A는 B가 구현하는 추상화에 대한 메서드를 호출할 수 있고, 런타임에 A가 B를 호출할 수 있게 되지만 B는 컴파일 시간에 A에 의해 제어되는 인터페이스에 종속됩니다(따라서 일반적인 컴파일 시간 종속성이 반전됨).Applying the dependency inversion principle allows A to call methods on an abstraction that B implements, making it possible for A to call B at runtime, but for B to depend on an interface controlled by A at compile time (thus, inverting the typical compile-time dependency). 런타임에 프로그램 실행 흐름은 그대로 유지되지만, 인터페이스가 도입된다는 것은 이러한 인터페이스의 여러 구현을 손쉽게 연결할 수 있다는 의미입니다.At run time, the flow of program execution remains unchanged, but the introduction of interfaces means that different implementations of these interfaces can easily be plugged in.

반전된 종속성 그래프

그림 4-2.Figure 4-2. 반전된 종속성 그래프.Inverted dependency graph.

종속성 반전은 느슨하게 결합된 애플리케이션을 빌드하기 위한 핵심 부분입니다. 상위 수준 추상화에 종속되고 상위 수준 추상화를 구현하도록(그 반대가 아니라) 구현 세부 사항을 작성할 수 있기 때문입니다.Dependency inversion is a key part of building loosely coupled applications, since implementation details can be written to depend on and implement higher-level abstractions, rather than the other way around. 그 결과로 얻게 되는 애플리케이션은 테스트 용이성, 모듈성 및 유지 관리 용이성이 더 우수합니다.The resulting applications are more testable, modular, and maintainable as a result. 종속성 주입의 실행은 종속성 반전 원칙에 따라 가능합니다.The practice of dependency injection is made possible by following the dependency inversion principle.

명시적 종속성Explicit dependencies

메서드 및 클래스는 올바르게 작동하는 데 필요한 공동 개체를 명시적으로 요구해야 합니다.Methods and classes should explicitly require any collaborating objects they need in order to function correctly. 클래스 생성자는 클래스가 유효한 상태를 유지하고 올바르게 작동하기 위해 필요한 항목을 식별하는 기회를 제공합니다.Class constructors provide an opportunity for classes to identify the things they need in order to be in a valid state and to function properly. 작성 및 호출이 가능하지만 특정 전역 또는 인프라 구성 요소가 작동하는 경우에만 올바르게 작동하는 클래스를 정의하면 이러한 클래스는 클라이언트에 대해 정직하지 않습니다.If you define classes that can be constructed and called, but that will only function properly if certain global or infrastructure components are in place, these classes are being dishonest with their clients. 생성자 계약은 클라이언트에 지정된 것만 필요하다고 알려주지만(클래스에서 매개 변수 없는 생성자를 사용하는 경우 아무 것도 필요하지 않을 수 있음), 그 후 런타임에 개체에 다른 것이 필요하게 됩니다.The constructor contract is telling the client that it only needs the things specified (possibly nothing if the class is just using a parameterless constructor), but then at runtime it turns out the object really did need something else.

명시적 종속성 원칙을 따르면 클래스와 메서드는 작동하는 데 필요한 것들을 클라이언트에 정직하게 전달합니다.By following the explicit dependencies principle, your classes and methods are being honest with their clients about what they need in order to function. 이 원칙을 따르면 사용자가 메서드 또는 생성자 매개 변수의 형태로 필요한 것을 제공하는 한, 사용자가 작업하는 개체가 런타임에 올바르게 작동한다고 믿을 수 있으므로 코드가 좀 더 자체 문서화되고 코딩 계약이 좀 더 사용자 친화적으로 됩니다.Following the principle makes your code more self-documenting and your coding contracts more user-friendly, since users will come to trust that as long as they provide what's required in the form of method or constructor parameters, the objects they're working with will behave correctly at run time.

단일 책임Single responsibility

단일 책임 원칙은 개체 지향 디자인에 적용되지만, 문제의 분리와 비슷하게 아키텍처 원칙으로 간주할 수도 있습니다.The single responsibility principle applies to object-oriented design, but can also be considered as an architectural principle similar to separation of concerns. 개체의 책임은 하나여야 하고 변경 사유가 하나여야 합니다.It states that objects should have only one responsibility and that they should have only one reason to change. 특히 개체가 한 가지 책임을 수행하는 방식을 업데이트해야 하는 경우에만 개체를 변경해야 합니다.Specifically, the only situation in which the object should change is if the manner in which it performs its one responsibility must be updated. 이 원칙을 따르면 기존 클래스에 책임을 추가하는 대신 여러 종류의 새 동작을 새 클래스로 구현할 수 있으므로 좀 더 느슨하게 결합된 모듈식 시스템을 만드는 데 도움이 됩니다.Following this principle helps to produce more loosely coupled and modular systems, since many kinds of new behavior can be implemented as new classes, rather than by adding additional responsibility to existing classes. 새 클래스를 추가하는 방법은 언제나 기존 클래스를 변경하는 방법보다 안전합니다. 어떤 코드도 새 클래스에 종속되지 않기 때문입니다.Adding new classes is always safer than changing existing classes, since no code yet depends on the new classes.

모놀리식 애플리케이션의 경우 애플리케이션의 상위 계층에서 레이어에 단일 책임 원칙을 적용할 수 있습니다.In a monolithic application, we can apply the single responsibility principle at a high level to the layers in the application. 프레젠테이션 책임은 UI 프로젝트에 남아 있어야 하고, 데이터 액세스 책임은 인프라 프로젝트 내에 남아 있어야 합니다.Presentation responsibility should remain in the UI project, while data access responsibility should be kept within an infrastructure project. 비즈니스 논리는 쉽게 테스트하고 다른 책임과 독립적으로 발전할 수 있도록 애플리케이션 코어 프로젝트에 남아 있어야 합니다.Business logic should be kept in the application core project, where it can be easily tested and can evolve independently from other responsibilities.

이 원칙을 애플리케이션 아키텍처에 적용하고 논리적 엔드포인트까지 확대하면 마이크로 서비스를 얻게 됩니다.When this principle is applied to application architecture and taken to its logical endpoint, you get microservices. 지정된 마이크로 서비스에는 단일 책임이 있어야 합니다.A given microservice should have a single responsibility. 시스템 동작을 확장해야 하는 경우 일반적으로 기존 마이크로 서비스에 책임을 추가하는 것보다는 마이크로 서비스를 추가하는 것이 더 좋습니다.If you need to extend the behavior of a system, it's usually better to do it by adding additional microservices, rather than by adding responsibility to an existing one.

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

DRY(반복 금지)Don't repeat yourself (DRY)

애플리케이션이 여러 위치에서 특정 개념과 관련된 동작을 지정하면 안 됩니다. 이는 종종 오류의 원인이 됩니다.The application should avoid specifying behavior related to a particular concept in multiple places as this practice is a frequent source of errors. 어느 시점에서 요구 사항이 바뀌면 이 동작을 변경해야 합니다.At some point, a change in requirements will require changing this behavior. 동작의 인스턴스를 하나 이상 업데이트하지 못하고 시스템이 일관되지 않은 방식으로 동작하게 될 수 있습니다.It's likely that at least one instance of the behavior will fail to be updated, and the system will behave inconsistently.

논리를 복제하는 대신 프로그래밍 구문에 캡슐화해야 합니다.Rather than duplicating logic, encapsulate it in a programming construct. 이 동작을 통해 단일 기관을 생성하고, 이 동작이 필요한 애플리케이션의 다른 부분에서는 새 구문을 사용하게 합니다.Make this construct the single authority over this behavior, and have any other part of the application that requires this behavior use the new construct.

참고

우연히 반복되는 동작은 함께 바인딩하지 마세요.Avoid binding together behavior that is only coincidentally repetitive. 예를 들어 두 상수가 같은 값을 갖고 있다고 해도 개념적으로 서로 다른 것을 참조한다면 상수가 하나만 있어야 하는 것은 아닙니다.For example, just because two different constants both have the same value, that doesn't mean you should have only one constant, if conceptually they're referring to different things.

지속성 무시Persistence ignorance

PI(지속성 무시) 는 지속되어야 하는 형식을 참조하지만, 그 코드는 선택하는 지속성 기술의 영향을 받지 않습니다.Persistence ignorance (PI) refers to types that need to be persisted, but whose code is unaffected by the choice of persistence technology. .NET에서는 이러한 형식을 POCO(Plain Old CLR Object)라고 부르기도 합니다. 특정 기본 클래스에서 상속하거나 특정 인터페이스를 구현할 필요가 없기 때문입니다.Such types in .NET are sometimes referred to as Plain Old CLR Objects (POCOs), because they do not need to inherit from a particular base class or implement a particular interface. 지속성 무시는 동일한 비즈니스 모델을 여러 가지 방식으로 지속할 수 있도록 허용하여 애플리케이션의 유연성을 향상하므로 유용합니다.Persistence ignorance is valuable because it allows the same business model to be persisted in multiple ways, offering additional flexibility to the application. 지속성 선택은 시간에 따라 한 데이터베이스 기술에서 다른 데이터베이스 기술로 달라질 수 있으며, 애플리케이션이 시작된 형식(예를 들어 관계형 데이터베이스 외에도 Redis 캐시 또는 Azure Cosmos DB 사용) 외에도 추가적인 지속성 형식이 필요할 수 있습니다.Persistence choices might change over time, from one database technology to another, or additional forms of persistence might be required in addition to whatever the application started with (for example, using a Redis cache or Azure Cosmos DB in addition to a relational database).

다음은 이 원칙에 위배되는 몇 가지 예입니다.Some examples of violations of this principle include:

  • 필수 기본 클래스.A required base class.

  • 필수 인터페이스 구현.A required interface implementation.

  • 자신을 저장할 책임이 있는 클래스(예: 활성 레코드 패턴).Classes responsible for saving themselves (such as the Active Record pattern).

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

  • 가상 키워드가 필요한 속성.Properties requiring virtual keyword.

  • 지속성 관련 필수 특성.Persistence-specific required attributes.

위의 기능 또는 동작을 갖고 있는 클래스의 요구 사항은 지속될 형식과 선택하는 지속성 기술 간의 결합을 추가하므로, 나중에 새로운 데이터 액세스 전략을 채택하기가 더 어려워집니다.The requirement that classes have any of the above features or behaviors adds coupling between the types to be persisted and the choice of persistence technology, making it more difficult to adopt new data access strategies in the future.

바인딩된 컨텍스트Bounded contexts

바인딩된 컨텍스트는 도메인 중심 디자인의 중심 패턴입니다.Bounded contexts are a central pattern in Domain-Driven Design. 대규모 애플리케이션 또는 조직의 복잡성을 별도의 개념 모듈로 분할하여 해결하는 방법을 제공합니다.They provide a way of tackling complexity in large applications or organizations by breaking it up into separate conceptual modules. 그 후 각 개념적 모듈은 다른 컨텍스트와 분리된(따라서 바인딩된) 컨텍스트를 나타내며, 독립적으로 발전할 수 있습니다.Each conceptual module then represents a context that is separated from other contexts (hence, bounded), and can evolve independently. 바인딩된 각 컨텍스트가 내부의 고유한 개념 이름을 자유롭게 선택할 수 있는 것이 가장 이상적이며, 자체 지속성 저장소에 배타적으로 액세스할 수 있어야 합니다.Each bounded context should ideally be free to choose its own names for concepts within it, and should have exclusive access to its own persistence store.

적어도 개별 웹 애플리케이션은 다른 애플리케이션과 데이터베이스를 공유하지 않고 자신의 비즈니스 모델에 대한 자체 지속성 저장소를 통해 자체 바인딩된 컨텍스트가 되기 위해 노력해야 합니다.At a minimum, individual web applications should strive to be their own bounded context, with their own persistence store for their business model, rather than sharing a database with other applications. 바인딩된 컨텍스트 간의 통신은 공유 데이터베이스가 아닌 프로그래밍 방식 인터페이스를 통해 발생하며, 발생하는 변경 내용에 대응하여 비즈니스 논리 및 이벤트가 발생하는 것을 허용합니다.Communication between bounded contexts occurs through programmatic interfaces, rather than through a shared database, which allows for business logic and events to take place in response to changes that take place. 바인딩된 컨텍스트는 마이크로 서비스와 긴밀하게 매핑되며, 또한 자체적인 개별 바인딩된 컨텍스트로 이상적으로 구현됩니다.Bounded contexts map closely to microservices, which also are ideally implemented as their own individual bounded contexts.

추가 자료Additional resources