주도권 관점으로 이해하는 소프트웨어 아키텍쳐

계약

일반적인 상거래에는 두 가지의 계약이 존재합니다.

  1. Order
    발주서는 소비자가 구매 의사를 표시하는 계약서입니다. 발주서는 보통 소비자에게 유리하도록 작성됩니다. .

  2. Offer
    견적서는 공급자가 공급 의사를 표시하는 계약서입니다. 견적서도 공급자에게 유리하도록 작성됩니다.

위 두 가지 계약서 중, 어떤 것이라도 성립하기만 한다면 공급자는 계약서에 명시된 (재화나) 서비스를 공급해야 하는 의무가 발생합니다.

주도권

종종, 완전히 동일한 내용을 갖는 발주서와 견적서가 동시에 발행되기도 합니다.

이 경우, 둘 중에 하나가 선택되는데, 어떤 계약서를 채택할 지는 오로지 계약 당사자 사이의 주도권에 의해 결정됩니다.

보통의 경우, 이 주도권이 구매자에 있는 것이라고 여겨집니다. 구매자를 “갑”, 공급자를 "을"로 표시하고, "갑"의 권한이 더 큰 것으로 생각하는 것이죠.

실제로, 중소 기업 공급자 A회사는 자신이 발행한 견적서를 있는 그대로 받아들이라는 주장을 삼성전자에게 할 수는 없죠. 주도권을 가진 구매자인 삼성전자가 발행한 발주서를 그대로 받아 들일 수 밖에 없습니다.

그런데, 주도권이 항상 구매자에게 있지는 않습니다.

반대의 예로, 구매자인 삼성전자도 세계에서 가장 경쟁력 있는 반도체 설비 회사 B에게 자신의 발주서를 그대로 받아 들이라는 주장할 수는 없습니다. 오로지, B회사가 발행한 견적서를 따를 수 밖에 없습니다. 이 경우, 주도권이 공급자에게 있기 때문입니다.

Plugin

소프트웨어도 때로는 외부에서 서비스를 공급 받기도 합니다.
이 공급에 필요한 계약서도 주도권을 가진 일방이 작성합니다.

만약, 소비자에게 주도권이 있다면, 서비스는 소비자가 제시한 계약서 = 발주서를 통해 결정됩니다.

public interface IAppleOrder
{
    Apple Provide(decimal money);
}

발주서를 준수하는 책임은 외부 공급자에게 있습니다.

주도권을 가진 소비자는 서비스가 어떻게 제조 되는 지에는 관심이 없고, 오로지 소비만 하게 됩니다.

// Apple 소비자
public class JamProducer
{
    private IAppleOrder _order;

    public JamProducer(IAppleOrder order)
    {
         _order = order;
    }

    public AppleJam Produce()
    {
        var apple = _order.Provide(10);
        // ...
    }
}

소비자가 발행한 발주서를 통해 공급되는 서비스를 "플러그인"이라고 합니다.

Plug

반대로, 공급자에게 주도권이 있다면, 소비자가 소비할 서비스는 공급자가 발행한 계약서 = 견적서에 의해 확정됩니다.

public interface IAppleJamOffer
{
    AppleJam Produce();
}

공급자는 견적서도 발행하지만, 견적서를 충족하는 서비스를 반드시 제공해야 합니다.

// Apple의 소비자이자, AppleJam의 공급자
public class JamProducer : IAppleJamOffer
{
    //...
}

공급자의 견적서를 통해 결정되는 서비스를, 플러그인과 대비되는 개념으로, "플러그"라고 부르도록 하겠습니다.

정리하면,

플러그인은 소비자의 요청에 의해 제조되는 서비스,
플러그는 공급자의 의지에 의해 제조되는 서비스

로 이해할 수 있습니다.

위의 예제 코드의 JamProducer는 Apple 거래와 AppleJam 거래에 모두 주도권을 갖는 행운을 얻었기에, Apple 공급자에게는 발주서(IAppleOrder)를 강요하고, AppleJam 소비자에게는 견적서(IAppleJamOffer)를 강요합니다. 그 결과로,

Apple 공급자는 반드시 Apple Provide(decimal money) 를 준수해서 공급해야 하고,
AppleJam 소비자도 반드시, AppleJam Produce() 를 준수해서 소비해야 합니다.

플러긴과 플러그의 혼동

때로는 발주서와 견적서를 혼동하기도 합니다. 이 혼동으로 인해, 아래와 같은 형태의 코드가 나타나곤 합니다.

플러긴을 구현하는 소비자

만약, 소비자가 플러긴을 스스로 구현한다면 어떻게 될까요?
이 경우, 문제는 없지만, 발주서는 불필요한 계약 - 복잡도가 됩니다.

매일 아침을 스스로 만들어 먹으면서, 반드시 배민에 주문을 해야 하는 규칙을 만든 것과 비슷합니다.

플러그를 구현하지 않는 공급자

만약, 공급자가 플러그를 제공하지 않으면, 어떻게 될까요?
이는, 계약 불이행으로, 사용할 서비스가 존재하지 않게 되는 심각한 문제에 봉착하게 됩니다.

구현 노출

플러그 혹은 플러긴은 소비자에 의해 소비됩니다.

    //...
    IAppleJamOffer o = new JamProducer();
    // services.AddTransient<IAppleJamOffer, JamProducer>();
    //...

위 코드는 소비자가 플러그를 소비하는 예를 보여주고 있는데, 여기에는 소비자가 플러그 제조 과정까지 알고 있다는 함정이 숨어 있습니다.

이 함정의 원인은 공급자가 제조 과정을 노출(public)했기 때문입니다.

public class JamProducer : IAppleJamOffer
{
    //...
}

이 경우, 소비자는 공급자의 주도권 따위 무시해버리고, 서비스를 직접 제조할 수 있습니다.

    //...
    JamProducer jp = new JamProducer();
    // services.AddTransient<JamProducer>();
    //...

플러그/플러긴 소비 강제

플러그 공급자가 주도권을 잃지 않기 위해서는, 플러그 제조 과정을 숨기고, 오로지 플러그를 통해서만 소비되도록 강제해야 합니다.

public interface IAppleJamOffer
{
    AppleJam Produce();
}

internal class JamProducer : IAppleJamOffer
{
    //...
}

public static class JamOfferBuilder
{
    public static IAppleJamOffer Build(IAppleOrder order)
    {
         return new JamProducer(order);
    }
}

소비자는 이제 공급자 주도권을 무시할 수 없습니다.

IAppleJamOffer  o = JamOfferBuilder.Build(appleOrder)
// services.AddTransient<IAppleJamOffer>( () => JamOfferBuilder.Build(appleOrder));

이렇게 플러그를 통한 소비를 강제하는 경우의 예로는, 닷넷의 "서비스 컨테이너"가 있습니다.
이 서비스 컨테이너는 자신의 실질은 감추고, 오로지 IServiceCollection 이라는 견적서만 노출합니다.
우리는 이 견적서를 통해 서비스 컨테이너 플러그만을 소비하게 됩니다.

주도권과 소프트웨어 아키텍쳐

지금까지의 내용은 주도권은 계약서 발행 권리와 같다는 점을 말하기 위함입니다.

우리는 소프트웨어를 설계할 때, 특정 건축 방식(Software Architecture)을 채택하는 경우가 많습니다.

각 건축 방식은 대체로 소프트웨어를 좀 더 작은 모듈 단위로 분해하고, 각 모듈 사이에 계약을 통해 결합하도록 하는 공통점과, 주도권을 어떤 모듈에 두느냐에 중요한 차이점이 있습니다.

각 건축 방식의 특징은 계약의 주도권 관점에서 이해해볼 수 있습니다.

예를 들어, 클린 아키텍쳐는 UseCase 모듈에 주도권을 둔다고 할 수 있습니다.

이는 UseCase 모듈이 계약서의 발행 권한을 갖고, 이 모듈과 결합하는 다른 모듈은 UseCase 모듈이 발행한 계약서를 따라야 한다는 것으로 이해할 수 있습니다.

또한, 주도권을 가진 모듈은 사실 소프트웨어 작성의 시작점으로 이해할 수 있습니다. 왜냐하면, 이 모듈이 발행한 발주서가 있어야, 다른 모듈이 공급을 할 수 있고, 견적서가 있어야 UseCase 모듈을 소비할 수 있기 때문입니다.

클린 아키텍쳐의 예를 계속 이어나가자면, 이 방법론에서는 주도권이 있는 UseCase 모듈부터 코딩하기 시작하면 됩니다.

namespace UseCases;

public class UserSearchWeatherUseCase
{
     public Weather Execute(Datetime date, Region region) ...
}

여기까지의 코드만 적어도, 우리 소프트웨어에 필요한 도메인 객체가 드러납니다.

public class Weather { ... }
public class Region { ... }

클린 아키텍쳐에서는 UseCase 모듈이 사용하는 도메인 객체들을 별도의 모듈 - Entities 모듈(혹은 Core 모듈)에 보관하라고 합니다. 즉, Entities 모듈이 어떤 멤버를 가질 지는 오로지 UseCase 모듈이 결정합니다.

동일한 소프트웨어를 만들더라도, 도메인 모듈에 주도권을 부여한 DDD 개발론을 따른다면, 도메인 모듈에 도메인 객체를 정의하는 것부터 시작하고, 이들을 소비하는 모듈을 작성하게 됩니다.

다시, UseCase 모듈로 돌아와서, 이 모듈이 특정 서비스를 외부 모듈에서 공급 받기로 결정했다면, 그에 합당한 발주서를 발행합니다.

// 발주서
public interface IWeatherProvider
{
    Weather Get(Datetime date, Region region);
}

public class UserSearchWeatherUseCase
{
    IWeatherProvider _wp;

    public UserSearchWeatherUseCase(IWeatherProvider wp)
    {
        _wp = wp;
    }

    public Weather Execute(Datetime date, Region region) => _wp.Get(date, region);
}

이 발주서는 발행자인 UseCase 모듈이 소유합니다.

namespace UseCases.Plugins;

public interface IWeatherProvider
{
    Weather Get(Datetime date, Region region);
}

UseCase 모듈이 소비하는 플러그인 제조 책임은 다른 모듈에 있는데, 플러그인의 성격에 따라, 코어 모듈 혹은 인프라 모듈로 구분됩니다.

그리고, UseCase 모듈은 자신을 소비하는 다른 모듈에게 견적서를 강요할 수 있습니다.

namespace UseCases.Plugs;

public interface IUserSearchWeatherUseCase
{
    Weather Execute(Datetime date, Region region);
}
namespace UseCases;

internal class UserSearchWeatherUseCase : IUserSearchWeatherUseCase
{
//...

견적서 역시, 발행 주체인 UseCase 모듈이 보관합니다.
UseCase는 견적서를 통해 자신을 플러그로 제공합니다.

UseCase 모듈 작성자는 모듈 소비자에게 UseCase 자체를 공개할 지, 플러그를 통한 소비를 강제할 지 선택할 수 있습니다. 위의 코드는 후자를 선택했습니다.

UseCase 모듈의 소비자에는 UI 모듈이 대표적입니다.

//Asp.Net Core
services.AddTransient<IUserSearchWeatherUseCase>( () => ...);

// Razor Page, Blazor
@inject IUserSearchWeatherUseCase SearchWeatherUC

// MVC
    public WeatherController(IUserSearchWeatherUseCase SearchWeatherUC)
 
//...

주도권 개념은 다른 아키텍쳐에도 동일하게 적용할 수 있습니다.

예를 들어, UI에 주도권을 두는 방식으로 설계한다면, 모든 계약서의 발행 권한은 UI 모듈이 갖게 되고, 코딩의 시작점 또한 UI 모듈이 됩니다.

다만, UI 모듈은 최종 소비자인 사용자에게 직접 소비된다는 특징으로 인해, 다른 모듈을 위한 견적서를 발행할 필요가 없습니다. 그래서, 이 모듈은 플러그인 소비자의 지위만 갖기 때문에, 발주서만 발행하고, 다른 모듈들은 이 발주서를 위한 플러그인 공급자가 됩니다.

계약서

마지막으로 계약서의 형태가 반드시 interface 일 필요는 없습니다.
class, virtual class, abstract class, delegate 등 어떠한 것도 가능합니다.(C#에서는, virtual class 는 virtual 멤버를 가진 클래스를 의미합니다)

예를 들면, Entity Framework 는 견적서로 virtual 클래스인 DbContext를 제공합니다.

다만, 추상 객체 - interface, abstract class - 를 계약서로 사용하면 모듈 사이의 경계선이 명확해지는 이점이 있습니다.

모듈의 경계선은 책임의 경계선과 같습니다. 모듈 단위로 개발과 테스트를 진행해도 나중에 결합하는 시점에 아무런 문제가 없게 됩니다.

6개의 좋아요

제가 이해하기로는
interface 상속을 플러그
의존성 주입을 플러그인 개념으로
해석하신것 같네요

근데 개인적으로 이걸 주도권 플러그, 플러그인 으로 해석하신것 약간
신선합니다.

마지막의 계약서 (Contract)는 저는 순저히 model로만
생각하고 있어서 도메인 개념으로 생각합니다.

저렇게 그 Model을 mediator 패턴으로 구현
해보고있습니다.

3개의 좋아요

플러그든 플러그인 이든, 의존성입니다.
의존성을 주입 받을 수도 있고, 자가 생성할 수도 있습니다.

요지는 의존성에 관한 정의의 책임과 구현의 책임은 계약의 주도권으로 결정된다는 점으로, 의존성 정의를 계약으로 보고, 주도권이 있는 측이 계약을 작성함을 말하고 있습니다.

계약서 작성자가 구현하면 플러그, 계약 상대방이 구현하면 플러그인으로 묘사했습니다.

3개의 좋아요