Clean Architecture로 작성된 Blazor Server 프로젝트

현재 제작 중인 프로젝트를 기왕이면 Clean Architecture 규칙에 맞게 작성해보고 싶어서

여러가지 검색해보다가 해당 프로젝트를 찾았습니다.

클린 아키텍처가 무엇인지 또는 Blazor Server에서는 어떤 식으로 구현하면 좋을 지에 대한 좋은 참고자료가 될 것 같습니다.

솔루션 구조는 대강 이렇습니다.

이 구조가 한 눈에 들어오질 않는 걸 봐선 아직 입문자를 못 벗어난 듯 싶습니다.

어질어질하네요…

고수들에게는 놀이터요, 하수들에겐 지옥인 이 바닥에서

얼른 놀이터를 경험하고 싶습니다… :cry:

5개의 좋아요

이것 대단히 좋은 source 입니다. 아 언른 파야 하는데

1개의 좋아요

저는 클린 아키텍쳐의 꽃은 UseCase 라고 생각합니다.

링크된 솔루션 구조에서 UseCase가 명확히 드러나지 않는 것 같아, 제 입장에서는 전체 구조 파악이 좀 힘든 면이 있군요.

제가 클린 아키텍쳐를 선호하는 이유는, 아래의 책과 연관이 있습니다.

Beginning C# Object-Oriented Programming - Dan Clark - Google 도서

이 책에서는 OOP 로 설계할 때, 가장 먼저 SRS(Software Requirement Specification : 소프트웨어 요구 사양서)를 작성하도록 하는데,

  1. 소프트웨어 사용자 집단을 액터로 구분하고,
  2. 각 액터의 요구 사항을 산문 형식의 UseCase로 작성하고,
  3. 파악된 UseCase 들의 집합으로 시스템을 표현하는 것이죠.

image

이 책의 UseCase 개념과 클린 아키텍쳐의 UseCase는 정확히 일치하는 것으로 바라보고 있습니다.

실제로 프로젝트에 적용할 때는, UseCase를 클래스로 작성하고, 클래스 이름을 UseCase 를 잘 표현하도록 길게 합니다.

이렇게 할 때 가장 큰 이점은 클래스 이름만 봐도 시스템 구조가 한 눈에 들어오고, 각 UseCase 클래스가 가져야 할 코드가 무엇인지 명확히 드러난다는 점입니다.

뿐만 아니라, UseCase 클래스로 SRS/UML을 갈음하는 편법을 사용할 수 있다는 이점도 있습니다.

namespace UseCases.Customer; // UseCases.CustomerActor;
public class CustomerViewsProductsUseCase : ICutomerViewsProductsUseCase
{
    public Task<IEnumerable<Product>> ExecuteAsync()
    {
        //....  
    }
}

public sealed class CustomerAddProductsOntoShoppingCartUseCase
    : ICustomerAddProductsOntoShoppingCartUseCase
 { ... }
namespace UseCases.InventoryManager;
public class InventoryManagerShipsShopingCartItemsUseCase 
    : IInventoryManagerShipsShopingCartItemsUseCase
{ ... }
namespace UseCases.ApiClient;
public sealed class ApiClientRequestOnBoardTimeUseCase
    : IApiClientRequestOnBoardTimeUseCase
{ ... }

UseCase 는 클래스 라이브러리 프로젝트에 모아 두며, SRS에서 드러난 Actor 별 폴더로 구분합니다.

MySolution/Usecases/
MySolution.Usecases.csproj
 /Customer/
 /InventoryManager/
 /ApiClient/
 ...

위의 예제 코드에서 각 UseCase 는 Agile 측면을 고려해서 인터페이스를 노출하고 있습니다.

그러나, 저는 혼자 개발하기 때문에, 인터페이스를 노출하지 않고 클래스를 바로 씁니다.

다들 경험해보셨겠지만, 인터페이스와 클래스가 1:1 인 경우, 개발이 전혀 Agile 하지 않게 됩니다.
하나의 수정에 항상 두 개의 파일을 관리해야 하고, 소비 코드에서 F12 (정의로 이동)를 누르면 인터페이스 파일로 날아가기 일쑤이기 때문에 여간 귀찮은 게 아닙니다.

2개의 좋아요

저도 F12 눌러서 인터페이스로 이동하는거 진짜 귀찮기는 해요… 지금도 적응이 잘 안되고…ㅎㅎ
근데 상수만 정의된 클래스, 공용 변수들은 어떻게 분류하고 관리할지, 유닛 테스팅은 어떻게 구성하는지, Repository 패턴은 어떻게 구성해놓았으며 인터페이스는 어떻게 해놨는지 등등…
이런 것들을 다른 사람들은 어떻게 관리하고 있을까… 궁금해서 한번 찾아봤습니다^^
저 프로젝트에서 구현된 방식은 규모나 기능에 비해 좀 복잡한 구조로 보이긴 하네요…
그래도 참고하기엔 좋아보여서 열심히 뜯어보고는 있습니다^^;;

@BigSquare 님은 Actor별로 구분하신다고 했는데, 혹시 Usecase가 중간에 걸친(?) 기능일 경우 분류를 어떻게 하시나요? 그것만 별도로 쪼개서 구성하시나요 아니면 최대한 근접한 기능에 같이 분류하시나요?

UseCase 는 언제나 특정 Actor를 주어로 시작합니다.

하나의 행위, 예를 들면, ViewsProducts 라도 이를 요구하는 액터에 따라 서로 다른 UseCase 가 됩니다.

  • CustomerViewsProductsUseCase
  • InventoryManagerViewsProductsUseCase
  • SalesManagerViewsProductsUseCase

물론 이들은 하나의 기능에 의존하게 됩니다.

    private readonly IProductRepository; // 의존성.

이 기능은 클린 아키텍쳐의 다른 레이어에서 제공합니다.
그러나, UseCase는 입력과 출력을 제어하는 역할을 갖습니다.

CustomerViewsProductsUseCase.ExecuteAsync()
{
    return await IProductRepository.GetAllAsync()
        .Where( p => p.LocationCode = InventoryCode.FreeStock) 
}

InventoryManagerViewsProductsUseCase.ExecuteAsync()
{
    return await IProductRepository.GetAllAsync()
        .Where( p => p.LocationCode = InventoryCode.OQCStock) 
}
1개의 좋아요

Core한 로직과 사용자별로 필요한 로직을 분리하고
단발성 내지는 개인한 된 use case의 로직을 분리해서
매인 스트림의 영향이 최소한 하는 clean architecture 철학을
잘설명해주셨네요(제가 잘알아들었는지 모르겠지만요^^)
class 명의 되도록 많은 의미를 내포함으로써
개발자로 하여금 해당 class의 용도파악이 쉽고 (네이밍 규칙에
얽매이지도 않고)
어차피 core단의 영향은 없으니 actor 별로 개인화된 business 처리
가 가능할것 같습니다.
code를 보고 생간한것데 interface 는 상위 레벨로 이걸 상속해서
use case를 처리하게끔 하는 일종의 가이드 역할도 할것 같습니다.
이걸 상속하고 use case를 만들어라 ? 뭐이런식으로요
@BigSquare 님이 보는 CA를 보는 지향적이랑 제가 보는 지향적은
좀 다른것 같아요 저는 Usecase보다 Domain 을 위주로 고민하고있어요
생각해보니 refactoring 을 위해서 @BigSquare 님 방식을 많이 참조해보겠습니다.
그리고 c# 에서 CA 관련 대가이시고 제가 많은 영향을 받은 분은

이분이었어요 책읽다 토할뻔했지만요 ㅎ

3개의 좋아요

링크된 책의 내용은 소트프웨어를 UseCase의 집합으로 설계하라는 취지인 것 같습니다.
이 경우, 하나의 시스템에 서로 다른 UseCase 구현이 존재할 수 없습니다.

UseCase가 달라지면, 인터페이스도 달라져야 하기에 1:1 의 관계를 벗어날 수 없습니다.

코드의 Interface 는 UseCase를 소비하는 다른 레이어를 동시에 구현할 때나 필요한 구조입니다.
예를 들어, UseCase와 UseCase를 소비하는 View가 동시에 구현되는 경우에만 필요합니다.

이 경우에도 요구 사항 변경이나, 요구 사항 파악 오류로 인해, UseCase가 달라지면, UseCase와 View 구현 모두 달라져야 합니다.

도메인과 관련해서도, UseCase를 먼저 작성하면, 시스템에 필요한 도메인이 쉽게 드러납니다.
이 객체들이 갖는 속성도, UseCase 에서 사용하는 것만 정의하면 되기 때문에, 도메인 객체에 대한 고민이 많이 줄어듭니다.

2개의 좋아요

개인적으로 한국 아니 해외도 확실한 use case 분류및 그런 require ment 가 딱 정의되기 힘들고
언제나 중구난방일것 같습니다.
저는 이런 요구사항을 반영할때 이것 버려도 된다 생각하고 그냥 code를 남발하는 스타일입니다.
예를들어 A 요구사항에 RFP 가 처음에 정해진다면
후반부에가면 A,B,C,D 요구사항이 되는것이 현실인만큼
code를 짤때 art로 짤부분과 gabage로 짤부분을 나눠서
최악의 경우 gabage가 문제가 생겨도
그냥 버리고 나중에 생긴 D 요구사항을 A,B,C,D 를 조합하거나
아예 수시로 class를 남발해서 해결할려고 합니다.

2개의 좋아요

이건 개인 프로젝트를 수행해도 늘상 있는 일이지 않나요?
보통 요구 사항이 많아져서, 스스로를 괴롭히죠. ^^

UseCase는 요구 사항을 격리합니다. 추가된 요구 사항이 과거의 UseCase에 영향을 주지 않기 때문에, 추가하거나 과거의 것을 빼거나 할 때도 편리합니다.

2개의 좋아요

답변 감사합니다. 핵심은 Actor가 다르면 같은 기능이라도 Usecase를 분리해라! 이거군요.

Model, DBContext → Repository Interface → Repository → Usecase Interface → Usecase

제가 지금 작성하는 방식이 이런 식의 흐름을 갖고 있는데…

인터페이스를 두 번씩이나 거치다보니 정의를 찾아가기도 귀찮고, 불필요하게 느껴지기까지 해요…
(윈폼 고인물의 한계인걸까요…)

결국 메인 쿼리나 핵심은 Respository에 거의 구현되어 있는데 Actor별로 Usecase를 쪼갠다고 해도

프로젝트가 크고 복잡한 경우, 나중에는 인터페이스 지옥에 파묻힐 것 같은 불안함이 생깁니다…ㅋㅋ

이게 맞는건가… 싶어서 찾아본거구요…

혹시 Repsository, Usecase를 잘 구현해놓은 참고 자료나 프로젝트가 있을까요?

1개의 좋아요

너무 잘 구현해놓으셔서 저도 한다가 1/4 정도 이해한것 같습니다.
CA는 깊게 들어가면 저도 밑천이 금새 드러나고 ;;
뭔가 답인것 알겠는데 혼자 하기에는 대단히 버거운 뭔가 같습니다 ㅎ

2개의 좋아요

Blazor에서 Usecase, Clean Architecture는 딱 이렇게 해야 돼! 하고

누가 딱! 정해줬으면 좋겠네요…ㅋㅋ

1개의 좋아요

DbContext 자체가 unit of work 에 대한 추상 레이어라서, 우리는 이를 구현해서 사용합니다.

ApplicationDbContext : DbContext

만약, 데이터 베이스가 아니라, 파일에 저장하는 경우를 고려하면 IRepository 로 한번 더 추상화할 필요도 있겠지만, 대규모 프로젝트에서 데이터 베이스를 사용하지 않는다는 것은 상상할 수 없는 얘기죠.

자료의 저장소로 데이터 베이스가 언제나 전제된다면, DbContext를 IRepository로 감싸는 것은 추상에 대한 추상일 뿐으로, 불필요한 복잡성에 지나지 않습니다.

따라서, 제가 든 코드의 예에서 IRepository를 사용한 것은, DbContext를 사용하는 환경이라면, 불필요하게 복잡하게 설계한 오류입니다.

다만 인터페이스를 사용하면서 혼란을 느끼신다면, 아래의 글을 참고하시면 좋을 것 같습니다.

윗 글은 인터페이스를 누가 정의해야 하고, 누가 구현해야 하는 지를 설명하고 있습니다.

여기에서 "누가"는 시스템을 구성하는 레이어를 가리킬 수도 있고, 그 레이어를 책임지는 개발자를 가리킬 수 있습니다.

마지막으로,

클린 아키텍쳐는 어렵지 않고, 구현하고자 하는 시스템에 대해 많은 통찰을 주는 것 같습니다.

작성 순서를 UseCase 부터 작성해보는 건 어떨까요?

UseCase => Model
UseCase => UI (윈폼, 웹, …)

UseCase 가 사용하지 않는 서비스는 시스템에 필요가 없습니다.
UseCase 를 소비하는 다른 모듈은 UseCase 이상으로 뭔가를 더 할 필요도 없습니다.

그리고, UseCase를 Interface로 감싸는 복잡성은, 다른 댓글에서 설명했다시피, 다른 책임자와 협업할 때, 업무의 경계를 확실히 나눌 때 빼고는 스스로를 괴롭히는 것에 지나지 않을 수 있습니다.

시스템(에 대한 요구 사항)이 달라지면 UseCase도 달라져야 합니다.
반대로, UseCase가 달라진다는 것은 시스템이 변화 - 다른 프로그램이 된다는 것을 의미합니다.

즉, UseCase는 시스템 자체이기 때문에, 굳이 한 번 더 추상화를 할 필요가 없습니다. 이는 우리가 Model 객체를 인터페이스로 감싸지 않는 것과 동일합니다.

2개의 좋아요

좋은 설명 감사합니다.
말씀해주신 내용을 하나하나 코드에 녹여보는 연습을 해봐야겠습니다.
댓글 내용을 두고두고 보면서 연습해야겠어요.
다시 한번 감사합니다.

2개의 좋아요