블로그

  • 클린 아키텍처 .NET Core(2부: 구현)

    깨끗한 아키텍처 권장 사항에 따라 빌드된 경우 ASP.NET Core 애플리케이션의 아키텍처에 대한 자세한 보기입니다. 소스 — 일반적인 웹 애플리케이션 아키텍처

    이전 기사 에 따라 클린 아키텍처의 기본 사례를 소개했습니다. 이제 디렉터리 구조부터 시작하여 ASP.NET Core 3을 사용하여 애플리케이션을 빌드할 것입니다. 계속 진행하기 전에 빠른 요약.

    요약

    이 다이어그램에서 종속성은 가장 안쪽 원으로 흐릅니다. Application Core는 이 다이어그램의 핵심 위치에서 이름을 따왔습니다. 그리고 다이어그램에서 Application Core가 다른 애플리케이션 계층에 대한 종속성이 없음을 확인할 수 있습니다. 애플리케이션의 엔티티와 인터페이스는 바로 그 중심에 있습니다. 바로 바깥에 있지만 여전히 Application Core에는 일반적으로 내부 원에 정의된 인터페이스를 구현하는 도메인 서비스가 있습니다. Application Core 외부에서 UI 및 인프라 계층은 모두 Application Core에 의존하지만 서로 의존하지는 않습니다(반드시).

    Application Core는 인프라에 의존하지 않기 때문에 이 계층에 대한 자동화된 단위 테스트를 작성하기가 매우 쉽습니다.

    UI 계층은 인프라 프로젝트에 정의된 유형에 대한 직접적인 종속성이 없기 때문에 테스트를 용이하게 하거나 변화하는 애플리케이션 요구 사항에 대한 응답으로 구현을 교체하는 것도 마찬가지로 매우 쉽습니다. ASP.NET Core의 종속성 주입에 대한 기본 제공 사용 및 지원 덕분에 이 아키텍처는 사소하지 않은 모놀리식 애플리케이션을 구조화하는 가장 적절한 방법입니다.

    전제 조건

    1. 비주얼 스튜디오
    2. .NET Core SDK (사용 중인 Visual Studio 버전을 지원하는 SDK를 찾으세요. 저는 Visual Studio 2019 및 .NET Core 3.1을 사용 중입니다.)
    3. .NET Core 웹 애플리케이션, MVC, C#, SQL Server, 마이그레이션, ID, Entity Framework 및 Visual Studio에 대한 기본적인 이해

    Visual Studio 2017 또는 이전 버전을 사용 중인 경우 .NET Core 2.1이 지원되는 버전입니다. 올바른 버전 받기

    이 문서는 .NET Core가 아니라 Clean Architecture를 위한 프로젝트 구조를 설정하는 방법에 관한 것입니다.

    혼란을 피하기 위해 이 프로젝트를 복제하고 프로젝트를 확인하여 단계별로 문서를 읽는 것이 좋습니다.

    nishanc/CleanArchitectureDemo

    .NET Core Clean 아키텍처를 기반으로 구축된 데모 프로젝트 – nishanc/CleanArchitectureDemo

    github.com

    디렉토리 구조

    빈 솔루션을 만듭니다.

    파일 -> 새로 만들기 -> 프로젝트에서 빈 솔루션 프로젝트를 검색합니다. ‘빈 솔루션’을 선택하고 ‘다음’을 클릭합니다.
    프로젝트 이름을 지정하고 위치를 찾은 다음 ‘만들기’를 클릭합니다.

    이제 솔루션 탐색기는 다음과 같아야 합니다.

    이제 개별 프로젝트를 저장하는 데 사용할 폴더를 생성하겠습니다.

    솔루션 내에 다음 폴더를 만드십시오. 이것은 단지 제안일 뿐입니다.

    솔루션 -> 추가 -> 새 솔루션 폴더를 마우스 오른쪽 버튼으로 클릭하십시오.
    여기에 표시된 대로 폴더를 만듭니다.

    애플리케이션 은 인터페이스, 서비스, 비즈니스 규칙을 관리합니다. 도메인 은 엔티티로 구성되며 데이터는 데이터 에 액세스하는 방법을 알고 IoC (Inversion of Control)는 종속성 주입에 도움이 됩니다.

    사용자 인터페이스

    UI.MVC 폴더 아래에 MVC 웹 애플리케이션을 생성해 봅시다.

    “CleanArchitecture.UI.MVC”를 마우스 오른쪽 버튼으로 클릭 -> 추가 -> 새 프로젝트
    ASP.NET Core 웹 애플리케이션 을 선택 합니다. ( 이 프로젝트 유형이 표시되지 않으면 이 stackoverflow 질문 을 따르십시오 .)

    동일한 명명 규칙에 따라 프로젝트 이름을 ‘CleanArchitecture.MVC’로 지정하고 ‘만들기’를 누릅니다.

    웹 애플리케이션(Model-View-Controller) 을 선택 하고 인증을 ‘ 개별 사용자 계정 ‘ 으로 변경합니다.

    이렇게 하면 새 프로젝트가 생성되고 일부 코드가 스캐폴딩됩니다. 클린 아키텍처 접근 방식을 취하고 있으므로 나중에 모델을 나중에 엔터티로 옮길 것입니다. 완료되면 SQL Server 데이터베이스를 가리키도록 연결 문자열을 빠르게 변경해 보겠습니다. (SQL Server가 설치되어 있지 않은 경우 여기에서 개발자 버전을 다운로드하고 여기에서 SQL Server Management Studio를 다운로드 하는 것이 좋습니다 . )

    로 이동 하여 서버를 가리키도록 연결 문자열을 변경합니다. Demoappsettings.json 라는 데이터베이스를 만듭니다 .

    https://nishanc.medium.com/media/5d0aa99af02a57b8fae94a0531404a36

    마이그레이션을 사용하여 ID 모델 클래스로 데이터베이스를 업데이트해 보겠습니다. 패키지 관리자 콘솔(도구 -> NuGet 패키지 관리자 -> 패키지 관리자 콘솔)을 열고 다음을 입력합니다.

    add-migration "InitialMigration" -Context ApplicationDbContext

    나중에 다른 엔터티에 대해 다른 DbContext를 갖게 되므로 여기에 DbContext 클래스 이름을 지정했습니다 . 이 명령은 “InitialMigration”이라는 마이그레이션을 생성합니다. 비어 있는 것처럼 보이지만 원하는 ID 테이블 세트를 생성합니다.

    원하는 경우 ID 마이그레이션을 위한 다른 프로젝트도 가질 수 있습니다. MVC 프로젝트에서 핵심 엔터티를 이동하여 이를 수행하는 방법을 보여 드리겠습니다. 그러나 먼저 마이그레이션을 생성한 후 데이터베이스를 업데이트하겠습니다.

    데이터베이스 갱신

    완료되면 데이터베이스에 새로운 테이블 집합이 있는 것을 볼 수 있습니다.

    이 시점에서 slandered toolbar에서 시작 프로필을 ‘CleanArchitecture.MVC’로 변경하여 이 프로젝트를 실행하고 새 사용자를 등록하고 작동하는지 확인할 수 있습니다.

    애플리케이션을 반영하도록 솔루션 폴더의 이름을 바꿀 수 있다는 점을 지적하고 싶습니다. 제 이름 지정 방식을 따를 필요는 없지만 물론 깨끗한 아키텍처를 고수해야 합니다.

    도메인

    핵심 엔터티를 보유할 도메인 계층에 클래스 라이브러리 프로젝트를 추가해 보겠습니다.

    클래스 라이브러리 선택 (.NET Core)
    도메인 접미사 로 이름 지정

    Class1.cs 를 제거 하고 새 폴더 Models (또는 Entities)를 추가합니다. 이 기사에서는 라이브러리 시스템을 구축한다고 가정 하고 Models 폴더에 Book 모델을 추가합니다.

    마우스 오른쪽 버튼 -> 추가 -> 클래스를 클릭하고 이름을 Book 으로 지정하고 추가 를 클릭 합니다.

    엔터티에 원하는 속성을 추가합니다.

    https://nishanc.medium.com/media/2ec0215f5b2eeeb47d4575ead6421cbd

    수업을 공개했습니다.

    이제 이 새 모델로 데이터베이스를 업데이트할 수 있도록 새 데이터베이스 컨텍스트를 만들어 보겠습니다. 인프라 계층 에서 수행할 것입니다 . Infrastructure.Data 아래 에 CleanArchitecture.Infra.Data 라는 새 클래스 라이브러리를 추가 하고 해당 폴더 아래에 Context 를 추가 합니다. 다음으로 LibraryDbContext 라는 클래스를 만듭니다 .

    계속해서 이 프로젝트를 구성하는 데 필요한 패키지를 추가해 보겠습니다. 하지만 그 전에 패키지 관리자 콘솔에서 dotnet restore지금까지 가지고 있는 3개의 프로젝트를 모두 실행합니다. 그런 다음 Build -> Rebuild Solution 을 수행하십시오.

    CleanArchitecture.Infra.Data 의 종속성 을 마우스 오른쪽 단추로 클릭하고 NuGet 패키지 관리 를 선택 하십시오 . 그런 다음 찾아보기 탭을 클릭하고 설치하십시오.

    Microsoft.EntityFrameworkCore 
    Microsoft.EntityFrameworkCore.Design
    Microsoft.EntityFrameworkCore.SqlServer
    Microsoft.EntityFrameworkCore.Tools

    모두 안정적인 버전(미리보기가 아님)이고 동일한 버전인지 확인하세요.

    이제 Microsoft.EntityFrameworkCore 에서 제공하는 DbContext에서 LibraryDbContext를 상속할 수 있습니다 . CleanArchitecture.Domain.Models 에서 제공하는 Book 엔터티를 참조하고 있음을 알 수 있습니다 . LibraryDbContext 에 다음 콘텐츠를 추가합니다.

    https://nishanc.medium.com/media/fe17133abc36b71be5c732b1f942b09c

    MVC 프로젝트의 Startup.cs에서 이 새 DbContext를 구성해 보겠습니다. ConfigureServices 메서드 에 다음 줄을 추가 합니다.

    https://nishanc.medium.com/media/4709b932ead0f3ff94eb72ebc451f9b6

    LibraryConnection 연결 문자열을 확인 하여 appsettings.json에 추가해야 합니다. 이 새 연결 문자열은 도메인 엔터티를 저장할 다른 데이터베이스를 가리킵니다.

    "ConnectionStrings": { 
    "DefaultConnection": "Server=.;Database=Demo;Trusted_Connection=True;MultipleActiveResultSets=true",
    "LibraryConnection": "Server=.;Database=Library;Trusted_Connection=True;MultipleActiveResultSets=true"
    }

    완료되면 LibraryDbContextadd-migration 에 대해 실행합니다.

    CleanArchitecture.Infra.Data 를 기본 프로젝트로 선택했는지 확인하십시오 .

    이렇게 하면 인프라 아래에 새로운 마이그레이션이 생성됩니다.

    다음을 사용하여 데이터베이스 업데이트,

    업데이트 데이터베이스 -Context LibraryDbContext

    애플리케이션 코어

    모든 서비스, 인터페이스 및 ViewModel이 여기로 이동합니다. 이전 단계와 마찬가지로 ‘ CleanArchitectureDemo.Application ‘ 폴더 아래에 CleanArchitecture.Application이라는 새 클래스 라이브러리(.NET Core) 를 만들고 다음과 같이 해당 폴더를 추가합니다.

    이제 ViewModel에 대해 작업하겠습니다. A view model는 정적 텍스트에 사용되든 데이터베이스에 추가(또는 편집 가능)할 ​​수 있는 입력 값(텍스트 상자 및 드롭다운 목록과 같은)에 사용되든 보기/페이지에 표시하려는 데이터를 나타냅니다. ). 그것은 당신의 domain model. 뷰를 위한 모델입니다. 즉, 도메인 모델에 대한 마스크를 생성합니다.

    BookViewModel 이라는 ViewModels 폴더 아래에 새 클래스를 만듭니다 . 당분간 우리는 데이터베이스에서 책 목록을 얻을 것입니다. BookViewModel.cs 에 다음 코드를 추가합니다 . CleanArchitecture.Domain.Models 에서 책 을 가져오고 있음을 알 수 있습니다 .

    https://nishanc.medium.com/media/579e240626cd602f65b810d90bb5f531

    구현하려는 기능에 대한 계약 역할을 할 새 인터페이스를 만듭니다. 나는 당신이 인터페이스에 대해 잘 알고 있기를 바라며 OOP에서 인터페이스를 만드는 이유는 이 기사의 범위를 벗어나기 때문에 여기에서 논의하지 않을 것입니다. Interfaces 폴더에서 새 인터페이스 를 만듭니다 .

    이름, IBookService.cs

    https://nishanc.medium.com/media/6be49239458378bc1aca0204f9a527b2

    구현될 때 이 메서드는 책 목록을 반환하고 ViewModel에 대해서만 알고 핵심 도메인 모델인 Book에 대해서는 알지 못하므로 모든 것을 한 곳에 두는 대신 이렇게 하여 핵심 엔터티를 추상화합니다.

    IBookService에 대한 구현을 작성하기 전에 데이터베이스에서 데이터를 가져오는 방법을 정의해야 합니다. 이를 위해 일반적으로 .NET에서 사용하는 것은 Entity Framework라는 ORM이지만 저장소 패턴 을 사용하여 비즈니스를 분리합니다. 응용 프로그램의 논리 및 데이터 액세스 계층.

    C#의 리포지토리 디자인 패턴은 도메인 개체에 액세스하기 위해 컬렉션과 같은 인터페이스를 사용하여 도메인과 데이터 매핑 계층 사이를 중재합니다. 즉, 리포지토리 디자인 패턴은 애플리케이션의 나머지 부분과 데이터 액세스 논리 사이에서 중개자 또는 중간 계층 역할을 한다고 말할 수 있습니다.

    CleanArchitecture.Domain 프로젝트 아래에 Interfaces라는 다른 폴더를 추가합니다 . IBookRepository.cs 라는 새 인터페이스를 추가합니다.

    이 시점에서 우리 프로젝트는 다음과 같아야 합니다.

    다음 메서드를 추가합니다. 이번에는 ViewModel이 아니라 도메인 엔터티 자체인 Book입니다.

    https://nishanc.medium.com/media/9d1355c233166b1613a1d855163b8ec8

    좋습니다. 이 시점에서 MVC 프로젝트 또는 Presentation Layer( 도메인 엔터티 Book 에 대해 전혀 모르는 ) 가 “이봐, 책 목록을 원해요!”라고 말하면 BookService (우리가 IBookService 를 사용하여 아직 구현하지 않았 으며 BookService 는 BookRepository 에서 가져와야 합니다 ( IBookRepository 를 사용하여 아직 구현하지 않음 ) .

    구현해 봅시다. 먼저, BookService입니다.

    CleanArchitecture.Application 프로젝트 아래 services 폴더 아래에 새 클래스인 BookService.cs를 추가하고 IBookService 에서 상속합니다 .

    https://nishanc.medium.com/media/d50d9acd1c12f9d59a7ea656bd40fba8

    이제 IBookRepository를 주입해야 합니다. 일반적으로 .NET에서 종속성 주입을 수행하는 것처럼 주입합니다.

    https://nishanc.medium.com/media/c31dc9484a248d633e37b5b7762c4606

    다음은 BookRepository 입니다. CleanArchitecture.Infra.Data 프로젝트 에서 Repositories 라는 새 폴더 를 만들고 그 아래에 새 클래스인 BookRepository 를 만듭니다 .

    BookService 를 구현한 것처럼 IBookRepository 에서 BookRepository 를 구현 한 다음 데이터베이스 컨텍스트를 주입해야 데이터베이스와 통신할 수 있습니다.

    https://nishanc.medium.com/media/add1b07b98fcddd9feef295910d0421d

    우리는 아직 BookService 및 BookRepository 에 메서드를 구현하지 않았 으므로 계속해서 해당 메서드를 구현해 보겠습니다.

    먼저 BookRepository 로 이동 하고 주입된 컨텍스트를 사용하여 데이터베이스에서 책을 검색합니다.

    https://nishanc.medium.com/media/0bb2b2851a1ad7dc199455e29cf51f32

    다음은 BookService입니다.

    https://nishanc.medium.com/media/1ed4b05b591a8211774fe8f37712bee0

    다음으로 종속성을 포함하고 분리하는 데 도움이 되는 IoC 프로젝트의 구현을 살펴볼 필요가 있습니다.

    통제 역전

    따라서 CleanArchitectureDemo.Infrastructure.IoC 아래에서 CleanArchitecture.Infrastructure.IoC 라는 이전 프로젝트에서 했던 것처럼 새로운 .NET Core 클래스 라이브러리를 생성합니다 . Class1.cs를 제거하고 종속성 -> NuGet 패키지 관리 -> 찾아보기 탭으로 이동하여 검색하십시오.

    Microsoft.Extensions.DependencyInjection

    우리가 설치한 이전 종속성과 동일한 버전을 설치하고 안정적인 버전인지 확인하십시오.

    이제 CleanArchitecture.Infrastructure.IoC 프로젝트 아래에 종속성 컨테이너 클래스를 생성해 보겠습니다.

    https://nishanc.medium.com/media/e608e82a60c70e9f942fc58fdde3416c

    여러 프로젝트의 인터페이스와 구현을 단일 참조 지점으로 연결하는 방법에 주목하십시오. 이것이 IoC 계층의 목적입니다. 또한 차이점 을 알고 싶다면 여기에서 자세한 내용을 읽으십시오.

    이제 이 새로운 서비스 컨테이너에 대해 MVC 프로젝트에 알려야 합니다. Configure 메서드 뒤에 다음 메서드를 추가 한 후 Startup.cs로 이동 합니다.

    https://nishanc.medium.com/media/821853243bc688ccc9d330c46a2ca29a

    이제 ConfigureServices 메서드 내에서 이 메서드를 호출해야 합니다.

    RegisterServices(서비스);

    전체 Startup.cs 는 다음과 같습니다 .

    이제 모든 레이어가 준비되었습니다. 그래서 다음으로 여기에서 논의한 모든 것을 사용하여 Controller 를 생성 하고 UI를 구현하는 방법을 살펴보겠습니다 .

    먼저 데이터베이스에 일부 데이터를 추가해 보겠습니다. GetBooks 메서드 만 구현하고 있으므로 일부 데이터를 수동으로 추가해야 합니다.

    https://nishanc.medium.com/media/a842b4678e7e2e9abbc83006893e574f

    데이터베이스에 데이터가 있으면 Controllers 폴더 아래 CleanArchitecture.MVC 프로젝트에 컨트롤러를 생성해 보겠습니다.

    Controllers 폴더 -> 추가 -> 컨트롤러를 마우스 오른쪽 버튼으로 클릭 하고 MVC 규칙에 따라 BookController 로 이름을 지정합니다 . 그런 다음 다음과 같이 이 컨트롤러 에 BookService 를 주입합니다.

    애플리케이션 계층에서 IBookService를 참조하고 있습니다.

    https://nishanc.medium.com/media/8403aee16f7967c12305d6ab3a660641

    이제 BookViewModel을 뷰에 전달합니다.

    https://nishanc.medium.com/media/e803f5f65fa3cdfef88d703590d5b0ca

    얼마나 깨끗한 지 보세요. 그것이 깨끗한 건축의 아름다움입니다. 이제 우리는 BookViewModel을 뷰에 전달했습니다. 계속해서 해당 뷰를 정의하겠습니다.

    Views 폴더를 마우스 오른쪽 버튼으로 클릭하고 Book 이라는 새 폴더를 추가합니다 . Book 폴더 를 마우스 오른쪽 버튼 으로 클릭하고 Index.cshtml이라는 Razor View 를 추가합니다 .

    Index.cshtml 에서 BookViewModel 을 가져오고 모델의 모든 항목(책)을 반복하여 테이블에 표시하는 다음 코드 세그먼트를 추가합니다.

    https://nishanc.medium.com/media/30a9846157203f5d2712a47cda7227cf

    그런 다음 Views/Shared 아래의 _Layout.cshtml 로 이동하고 방금 만든 보기에 다른 탐색 링크를 추가합니다. Book 컨트롤러와 Index 작업 을 가리킵니다 .

    https://nishanc.medium.com/media/cfeffb0cf40db9e13321f83c9633478d

    이제 MVC 프로젝트를 실행하고 https://localhost:5001/Book 으로 이동하여 책 을 볼 수 있습니다!

    하지만 아직 로그인을 하지 않은 상태이기 때문에 사용자가 인증 없이 데이터를 볼 수 있도록 허용하는 것은 문제이며 해결하기 쉽습니다.

    https://nishanc.medium.com/media/bfeec32c512c656d7f31a6331dfd19f8

    컨트롤러 자체의 Index() 메서드에 [Authorize] 특성을 추가 한 다음 https://localhost:5001/Book 으로 다시 이동해 봅니다.

    로그인하라는 메시지가 표시됩니다. 또한 우리가 생성한 서비스와 리포지토리에 중단점을 배치하고 런타임에서 어떻게 작동하는지 확인할 수 있습니다.

    그게 다야, 여기까지 와줘서 고마워. 이것은 모든 것을 설정하는 방법에 대한 소개 가이드일 뿐이며 클린 아키텍처를 구현하는 다양한 방법이 있지만 여기에서 논의한 핵심 개념이 있습니다. 따라서 다음에 더 광범위한 튜토리얼을 접할 때 길을 잃지 않을 것입니다.

    이것에서 더 나아가고 싶다면 JASON TAYLOR 의 비디오를 보거나 Clean Architecture 에 대한 그의 놀라운 기사를 읽는 것이 좋습니다 .

  • 클린 아키텍처 .NET Core(1부: 소개)

    원본링크 : Clean Architecture .NET Core (Part 1: Introduction)

    클린 아키텍처란?

    코드에 다음과 같은 문제가 있습니까?

    1. 자주 버그 생성
    2. 디버깅 및/또는 새 기능 추가는 고통스럽습니다.
    3. 데이터베이스/웹 서버 없이 테스트를 작성할 수 없습니다.
    4. 다른 개발자는 코드의 의도를 이해할 수 없습니다.
    5. 비즈니스 논리와 혼합된 프레젠테이션 논리 또는 데이터 액세스 논리와 혼합된 비즈니스 논리가 있습니다.

    축하합니다! 당신은 버그가 많은 코드를 작성합니다! 잠깐만요.. 그래서 그것에 대한 해결책이 없나요? 음.. 모범 사례를 따르기만 하면 됩니다. SOLID 또는 다른 디자인 패턴 과 같습니다 . 그들은 모두 관심의 분리라는 동일한 목표를 가지고 있습니다. 그들은 모두 소프트웨어를 계층으로 나누어 이러한 분리 를 달성합니다. 각각에는 비즈니스 규칙에 대한 계층과 인터페이스에 대한 계층이 하나 이상 있습니다.

    유지 관리하기 쉬운 대형 프로젝트를 구축하는 비결은 파일이나 클래스를 다른 구성 요소와 독립적으로 변경할 수 있는 구성 요소로 분리하는 것입니다. 몇 가지 이미지로 설명하겠습니다.

    이미지 제공 : pusher.com

    이미지 (a)에서 가위를 칼로 교체하려면 어떻게 해야 하나요? 바쁜 직업이라고 해도 과언이 아닙니다. 이미지 (b)에서 가위를 어떻게 교체합니까? 포스트잇 아래에서 가위의 실을 빼내고 칼에 묶인 새 실을 추가하기만 하면 됩니다. 훨씬 더 쉽습니다. Post -it 노트는 끈이 묶여 있지 않기 때문에 신경 쓰지 않습니다.

    두 번째 이미지가 나타내는 아키텍처는 분명히 변경하기가 더 쉬웠습니다. 클린 아키텍처 이면의 핵심 규칙 은 정확히 이것 또는 더 기술적으로는 소스 코드 종속성 이 안쪽 만을 가리킬 수 있다고 명시하는 종속성 규칙 입니다. 그래서 이것은 무엇을 의미합니까? 구경하다.

    이미지 제공 : fullstackmark.com

    내부 원의 어떤 것도 외부 원의 어떤 것에 대해 전혀 알 수 없습니다. 특히 외부 원에서 선언된 이름은 내부 원의 코드에서 언급되어서는 안 됩니다. 여기에는 함수, 클래스가 포함됩니다. 변수 또는 기타 명명된 소프트웨어 엔터티. 요지는 아키텍처 모델의 각 “링”에 종속성이 캡슐화되어 있으며 이러한 종속성은 내부만 가리킬 수 있다는 것입니다.

    엔터티: 전사적 비즈니스 규칙을 캡슐화합니다.

    엔터티는 애플리케이션 기능에 중요한 관련 비즈니스 규칙 집합입니다. 개체 지향 프로그래밍 언어에서 엔터티에 대한 규칙은 클래스의 메서드로 함께 그룹화됩니다. 적용이 없더라도 이러한 규칙은 여전히 ​​존재합니다.

    예를 들어 , 대출에 대해 10%의 이자를 부과하는 것은 은행이 가질 수 있는 규칙입니다. 이는 이자가 종이로 계산되든 컴퓨터를 사용하여 계산되든 마찬가지입니다. 엔티티는 다른 레이어에 대해 아무것도 모릅니다. 그들은 아무것도 의존하지 않습니다. 즉, 외부 계층에 있는 다른 클래스나 구성 요소의 이름을 사용하지 않습니다.

    사용 사례 : 애플리케이션별 비즈니스 규칙을 포함합니다.

    그들은 시스템을 자동화하는 방법을 알려줍니다. 이는 앱의 동작을 결정합니다. 이러한 사용 사례는 엔터티 간 데이터 흐름을 오케스트레이션하고 해당 엔터티가 전사적 비즈니스 규칙을 사용하여 사용 사례의 목표를 달성하도록 지시합니다. 다음은 샘플 사용 사례입니다.

    신규 대출 정보 수집입력: 이름, 주소, 생년월일 등 
    출력: 동일한 정보 + 신용 점수규칙:1.이름
    확인 2.주소 등 확인 3.
    신용 점수 받기
    4.신용 점수 < 500인 경우 거부 활성화
    5.그렇지 않으면 고객(엔티티) 생성 및 대출 견적 활성화

    사용 사례는 엔터티와 상호 작용하고 엔터티에 의존하지만 레이어에 대해 더 이상 알지 못합니다. 웹페이지든 iPhone 앱이든 상관하지 않습니다 . 데이터가 클라우드 또는 로컬 SQLite 데이터베이스에 저장되어 있는지 상관하지 않습니다.

    그러나 우리 는 응용 프로그램 작동에 대한 변경 사항이 사용 사례와 이 계층의 소프트웨어에 영향을 미칠 것으로 예상합니다. 사용 사례의 세부 사항이 변경되면 이 계층의 일부 코드가 확실히 영향을 받습니다.

    Interface Adapters : 유스 케이스와 엔터티에 가장 편리한 형식의 데이터를 변환합니다.

    GUI의 MVC 아키텍처를 완전히 포함하는 것은 이 계층입니다. 발표자, 보기 및 컨트롤러가 모두 여기에 속합니다. 모델은 컨트롤러에서 전달되는 데이터 구조일 가능성이 높습니다.

    이 원 안에 있는 코드는 데이터베이스에 대해 전혀 알지 못합니다. 데이터베이스가 SQL 데이터베이스인 경우 모든 SQL은 이 계층으로 제한되어야 합니다.

    Frameworks and Drivers : 모든 I/O 구성 요소가 있는 곳.

    이 레이어는 모든 세부 사항이 있는 곳입니다. 웹은 세부 사항입니다. 데이터베이스는 세부 사항입니다. 우리는 이러한 것들을 거의 해를 끼치지 않는 외부에 보관합니다. 가장 불안정한 계층입니다. 이 계층의 항목은 변경될 가능성이 높기 때문에 보다 안정적인 도메인 계층에서 가능한 한 멀리 유지됩니다. 그것들은 분리되어 있기 때문에 상대적으로 쉽게 변경하거나 한 구성 요소를 다른 구성 요소로 교체할 수 있습니다. (예: UI, 데이터베이스, 프레임워크, 장치)

    교차 경계

    위 다이어그램의 오른쪽 하단(아래에 설명됨)에는 원 경계를 교차하는 방법의 예가 있습니다.

    다음 계층에서 사용 사례와 통신하는 컨트롤러 및 발표자를 보여줍니다. 제어 흐름에 유의하십시오. 컨트롤러에서 시작하여 사용 사례를 통해 이동한 다음 Presenter에서 실행됩니다. 이것을 어떻게 구현합니까? ” 종속성 역전 원칙 “.

    종속성 역전 원칙(DIP)

    SOLID의 D입니다. 즉, 덜 안정적인 클래스와 구성 요소는 더 안정적인 클래스와 구성 요소에 의존해야 하며 그 반대는 아닙니다. 안정적인 클래스가 불안정한 클래스에 의존하는 경우 불안정한 클래스가 변경될 때마다 안정적인 클래스에도 영향을 미칩니다. 따라서 종속성의 방향을 반대로 해야 합니다. 이것은 어떻게 이루어 집니까? 추상 클래스를 사용하거나 인터페이스 뒤에 안정적인 클래스를 숨김으로써.

    따라서 안정적인 클래스 대신 다음과 같이 휘발성 클래스의 이름을 사용하십시오.

    class StableClass {
        void myMethod(VolatileClass param) {
            param.doSomething();
        }
    }

    휘발성 클래스가 구현하는 인터페이스를 만들 수 있습니다.

    class StableClass {
        interface StableClassInterface {
            void doSomething();
        }
        void myMethod(StableClassInterface param) {
            param.doSomething();
        }
    }
        class VolatileClass implements StableClass.StableClassInterface {
            public void doSomething() {
              // Do something
            }
        }

    예를 들어 사용 사례에서 발표자를 호출해야 한다고 가정합니다. 그러나 이 호출은 종속성 규칙 을 위반하므로 직접 호출해서는 안 됩니다. 외부 원의 이름은 내부 원에서 언급할 수 없습니다. 그래서 우리는 유스 케이스가 내부 원에서 인터페이스(여기서는 유스 케이스 출력 포트 로 표시됨 )를 호출하고 외부 원의 발표자가 이를 구현하도록 합니다.

    경계를 넘는 데이터는 무엇입니까?

    일반적으로 경계를 넘는 데이터는 단순한 데이터 구조입니다. 원하는 경우 기본 구조체 또는 간단한 데이터 전송 객체를 사용할 수 있습니다. 또는 데이터는 단순히 함수 호출의 인수일 수 있습니다. 또는 해시맵으로 압축하거나 개체로 구성할 수 있습니다. 중요한 것은 격리되고 단순한 데이터 구조가 경계를 넘어 전달된다는 것입니다.

    • 우리는 엔터티 또는 데이터베이스 행을 속이고 전달하고 싶지 않습니다.
    • 우리는 데이터 구조가 종속성 규칙을 위반하는 어떤 종류의 종속성도 가지기를 원하지 않습니다.

    결론

    The Clean Architecture의 본질은 다음과 같이 요약할 수 있습니다. 같은 시간에 같은 이유로 변경될 수 있는 클래스는 구성 요소로 함께 그룹화해야 합니다. 비즈니스 규칙 구성 요소는 더 안정적이며 UI, 데이터베이스, 웹, 프레임워크 및 기타 세부 사항을 처리하는 보다 변동성이 큰 인프라 구성 요소에 대해 전혀 알지 못합니다. 구성 요소 계층 간의 경계는 계층 간의 데이터를 변환하고 보다 안정적인 내부 구성 요소의 방향을 가리키는 종속성을 유지하는 인터페이스 어댑터를 사용하여 유지됩니다.

    다음 위로..

    다음 포스트에서는 .NET Core를 사용하여 The Clean Architecture를 구현하려고 합니다.

  • Model-View-(Reactive)Presenter 패턴이란

    환경

    UniRx 입문 시리즈 목차는 이쪽

    GUI 주변의 디자인 패턴

    Model-View-(Reactive)Presenter패턴(약칭하여 MV(R)P 패턴)이란 Unity에서 GUI 주변의 설계 패턴 의 일종입니다.
    「GUI」란, 이른바 「유저 인터페이스」를 말해, 게임중에 있어서의 「화면상에 표시되는 정보」나 「메뉴」나 「버튼」이라고 하는 것을 가리킵니다.
    (자꾸 말하면, uGUI일이라고 생각해 주세요)

    GUI 주위의 구현수법이라고 하는 것은 Unity에 한정되지 않고, 복잡하게 되기 쉽다 어려운 부분입니다.
    그 때문에 여러가지 설계 패턴이 고안되어 왔습니다.
    대표적인 것으로 말하면 약간 MVC등 MVVM을 들 수 있습니다.

    그 중에서도 MVP패턴이라는 것이 있고, Model-View-(Reactive)Presenter패턴은 이것을 바탕으로 한 설계 패턴입니다.

    왜 디자인 패턴이 필요한가?

    GUI 주위의 구현시에 이러한 복수의 설계 패턴이 왜 등장하는가 하면, GUI 주위를 깨끗하게 구현하는 것이 중요하기 때문 입니다.
    GUI 주변은 그 용도에서 매우 복잡해지기 쉽습니다.

    • 화면상의 오브젝트에 표시하는 데이터는, 그 뒤편에서 서로 연동하고 있다
    • 인간에게 실시간으로 정보를 표시해야 함
    • 사람이 GUI를 통해 데이터 처리를 방해할 수 있음

    GUI는 데이터 구조가 복잡하게 되기 쉬운 부분이 있는데, 그 이외에 실시간성, 상호 작용성이 필요한 부분입니다. 그래서 빠르게 구현하다보면 “상호 참조”, “순환 참조”, “비지웨이트”, “이벤트의 무한 루프”등이 발생할 우려가 매우 높은 곳이다.

    • 비지웨이트: 단지 대기만 하는 것에 불과한데 쓸데없이 CPU를 소비하여 다른 프로그램이나 스레드에 영향을 미치게 되는 현상

    이처럼 복잡하게 되기 쉬운 GUI를 “책임을 분리하여 객체 간의 상호 작용을 구축하면 좀더 나아지지 않을까?”라는 생각에서 MVC, MVVM, MVP 설계가 나온 배경입니다.

    빠르게 구현한 예

    설계패턴을 사용하지 않고 우직하게 uGUI를 사용하면 이렇게 됩니다는 예입니다.

    엉성하게 만들면 다음과 같이 “게임 중에 사용하는 중요한 수치”와 “uGUI에 접근하는 부분”이 같은 클래스 안에 그대로 쓰여지거나 하게 됩니다.

    using UnityEngine;
    using UnityEngine.UI;
    
    namespace Yabai
    {
        /// <summary>
        /// UI와 데이터와 엉망이 된 클래스
        /// </summary>
        public class Data : MonoBehaviour
        {
            [SerializeField] private Slider _slider;
    
            // 프로퍼티 (외부에서 읽고, 쓰기 가능)
            public float CurrentValue { get; set; }
    
            private void Update()
            {
                // Update로 매 프레임 쓸데없이 값을 체크 한다 (최악).
                if (_slider.value != CurrentValue)
                {
                    _slider.value = CurrentValue;
                }
            }
    
            // 코드에서는 참조가 없지만 Unity에서 호출되는 public 메서드
            public void OnSliderValueChanged()
            {
                CurrentValue = _slider.value;
            }
        }
    }

    Unity 상에서 객체의 상호 참조가 발생하는 어느 한쪽이 부족하면 오류가 발생하여 게임이 시작되지 않거나 하는 경우가 생길 수 있습니다.

    이렇게 되는 것을 피할 목적으로 Unity에서 흔히 쓰이는 것이 MV(R)P 패턴입니다.

    Model-View-(Reactive)Presenter 패턴을 말하기 전에 이것의 기초가 되는 Model-View-Prsenter 패턴에 대해 먼저 설명하겠습니다.

    Model-View-Presenter패턴

    Model-View-Presenter(MVP) 패턴은 GUI의 설계 패턴 중 ‘ Presenter‘이라는 개념을 사용한 것입니다.

    GUI 주변의 구성 요소를 다음의 3개로 나누어 생각합니다.

    • Model: 데이터의 실체. GUI와는 직접 적인 관계가 없는 어플리케이션의 본체의 요소 부분.
    • View: GUI를 제어하는 부분. 데이터를 화면에 표시하거나 반대로 사용자의 조작을 받는 부분.
    • Presenter: Model과 View를 연결하는 중재자 역할.

    MVP에서 가장 중요한 부분은

    • “Presenter가 존재하지 않으면 View와 Model은 완전히 독립된 상태가 된다”는 것입니다.

      View와 Model을 연결하는 존재는 Presenter 뿐이어서 Presenter를 배제하면 이 2개는 완전히 독립적이게 됩니다.

        즉 “View는 Model을 완전히 모르는 상태”, “Model은 View를 완전히 모르는 상태”가 됩니다.

        그리고 이 MVP 패턴은 실제로 코드를 구현시 문제가 발생합니다.

        그것은 “Model과 View를 실시간 연동시키려면 어떻게 하면 좋을까?”입니다.

        “Model의 변화를 View에 즉각 반영했다” “유저로부터 View의 입력을 Model로 즉시 전달”이란, 실시간 동작을 별도의 방법을 사용하여 구현할 필요가 생긴 것입니다.

        이처럼 “실시간으로 동작을 하는 것”을 별도로 마련할 필요가 있는 것이 MVP 패턴의 결점이었습니다.

        MVP 으로 MV(R)P

        “실시간으로 동작하는 것”에 대해 별도의 처리가 필수 였던 Model-View-Presenter 패턴에 UniRx를 추가함으로써 해당 문제를 보완한 것이 Model-View-(Reactive)Presenter 패턴 입니다.

        Unirx가 가진 리액티브 객체, 특히 ReacrtiveProperty의 편리성에 주목하여 MVP 패턴을 응용한 것이 MV(R)P 패턴입니다.

        Model-View-(Reactive)Presenter 패턴 설명

        다시 「Model-View-(Reactive)Presenter패턴( MV(R)P패턴)」의 해설을 합니다.

        개요

        Model-View-(Reactive)Presenter패턴은 MVP 패턴과 UniRx를 병용한 Unity에서의 GUI 주위의 구현 기법 입니다.
        GUI의 구성 요소를 Model, View의 Presenter세 가지로 분해하고 그들을 UniRx (의 ReactiveProperty)를 사용하여 협력하는 기술이되었습니다.

        (Model의 변화를 Presenter를 통해 View에 반영. View의 변화를 Presenter를 통해 Model에 반영. 그 중개에 UniRx를 사용한다.)

        MV(R)P패턴이 말하는 것 / 말하지 않은 것

        이런 설계 패턴을 이용할 때에 조심해야 하는 부분은 “이 디자인 패턴은 무엇을 말하고 있는가” 입니다.

        가끔 해당 패턴에 깊게 빠져 들어 본래의 의미와는 전혀 다른 것을 주장하는 사람이 있거나 하므로 주의가 필요합니다.

        MV(R)P패턴이 말하는 것, 말하지 않은 것을 아래에 정리 했습니다.


        MV(R)P패턴이 말하는 것

        • GUI(특히 uGUI)의 구현 패턴이다.
        • View와 Model을 Presenter라는 얇은 레이어로 연결 한다.
        • 각 객체 연결에는 Observable(ReactiveProperty)을 활용한다.

        MV(R)P패턴이 말하지 않은 것

        • Unity GameObject등은 모두 View이다 (← 말하지 않는다. Model하지만 GameObject사용해도 별로 좋다)
        • MV(R)P패턴은 Unity 종속 계층과 Unity 종속 계층을 분리하는 것입니다 (← 말하지 않음).
        • MV(R)P패턴을 사용하면 게임을 무엇이든 깨끗하게 구현할 수 있다(← 말하지 않는다.)
        • 인게임에 MV(R)P패턴을 사용하면 설계를 잘 할 수 있다(← 말하지 않는다. 클린 아키텍처라든지와 이야기가 섞이지 않는다?)

        MV(R)P 패턴은 GUI 구현을 이렇게 만들면 좋아 라는 내용밖에 말하지 않았습니다.

        그 이상의 것은 MV(R)P 패턴에 기대를 지나치게 기대한 억측으로 보입니다.

        다양한 객체의 역할

        MV(R)P패턴에는 다음의 3개의 오브젝트가 등장합니다.

        • Model
        • View
        • Presenter

        강조하면 좋은 부분은 이들의 객체는 실제 구현자의 관점에서 상대적으로 결정되는 것 이라는 점입니다.

        즉 “앞으로 Model을 만들어야지!”라는 역할을 정하고 객체를 만드는 것이 아닌” 이 객체는 이 GUI에게 Model 역할을 해라”라고 결정하는 형태가 맞습니다.

        요약하면 Model과 View등이 엄격하게 정해진 부분이 아닌 시스템 전체의 흐름을 보고 결정하는 것입니다.

        Model은

        • GUI에 대해서 표시하는 데이터의 실체를 가지는 부분

        MV(R)P 패턴에서의 Model은 데이터의 실체를 가진 부분을 말합니다.

        바꾸어 말하면 GUI에 표시하는 데이터의 실체를 가진다 입니다.

        액션 게임의 예를 들면 “플레이어의 좌표”, “플레이어의 체력”, “적의 체력”, “현재 스테이지 정보”, “나머지 제한 시간”등을 들 수 있습니다.

        조금 더 깊이 있는 표현을 한다면 Model은 **“게임의 구성에 필수 불가결한 정보(도메인)”**을 다루고 있는 오브젝트 입니다.

        그리고 MV(R)P 패턴에서의 Model의 중요한 부분은 ReactiveProperty를 사용하여 Presenter의 접근을 가능하게 하고 있다는 점입니다.

        ReactiveProperty을 공개함으로써 Model의 내부 상태가 변화했을 때 그것이 Observable로서 외부에 통보할 수 있습니다.

        Model의 구현 방법

        한 객체를 MV(R)P 패턴 Model로 다루기 위해서 필요한 절차는 다음과 같습니다.

        • 상태를 IObservable<T> 또는 ReactiveProperty<T>로 공개한다.
        • (필요하면) Model 내부 상태를 수정하는 프로퍼티나 메서드를 준비한다.

        요약하면 UniRx를 사용하고 상태를 공개한다 입니다.

        예를 들면, 다음의 Player는 “체력”을 ReactiveProperty로 공개하고 있습니다. (또 동시에 Enemy와 충돌하면 체력이 줄어듭니다)

        using UniRx;
        using UnityEngine;
        
        namespace MVRP.Models
        {
            /// <summary>
            /// 액션 게임의 플레이어
            /// "화면에 체력을 나타낸다"라는 시점에서 보면 모델
            /// </summary>
            public sealed class Player : MonoBehaviour
            {
                /// <summary>
                /// 체력
                /// Reactive Property로서 외부에 상태를 ReadOnly로 공개
                /// </summary>
                public IReadOnlyReactiveProperty<int> Health => _health;
        
                private readonly IntReactiveProperty _health = new IntReactiveProperty(100);
        
                /// <summary>
                /// 충돌 이벤트
                /// </summary>
                private void OnCollisionEnter(Collision collision)
                {
                    // Enemy와 충돌하면 체력을 줄인다
                    if (collision.gameObject.TryGetComponent<Enemy>(out var _))
                    {
                        _health.Value -= 10;
                    }
                }
        
                private void OnDestroy()
                {
                    _health.Dispose();
                }
            }
        }

        덧붙여, 체력을 Reactive Property를 public으로 공개하여 값의 Read/Write를 직접 가능하게 하거나, Read 전용으로 IReadonly Reactive Property<T>만을 공개하고 Write는 다른 메소드를 경유 시킬지는 어느 쪽이든 상관없습니다.

        이 부분은 팀의 설계 규칙이나 개인 취향 등에 따라 정해도 무방 합니다.

        View는

        MV(R)P 패턴에서 View는 “사용자(유저)에 정보를 표시하거나 사용자의 입력을 받는 부분”을 말합니다.

        더 간단하게 말하면, uGUI를 사용하는 부분이라고 생각해도 무방합니다(물론 uGUI 이외를 사용해도 괜찮습니다)

        간단하게 uGUI의 “Text”, “Button”, “Slider”의 컴포넌트를 그대로 View로 다룰 수 있습니다.

        또한 Tween을 추가하는 등 UI 요소가 애니메이션하는 경우에도 그 애니메이션을 관리하는 기능도 View로 봐도 무방합니다.

        View의 구현 방법

        한 객체를 View로 다루도록 하기 위해 필요한 것은 Presenter에서 참조 가능하게 하도록 하면 됩니다.

        uGUI의 컴포넌트를 직접 View로 본다면 Presenter부터 uGUI 컴포넌트를 직접 참조할 뿐입니다.

        무언가 자기가 만든 컴포넌트를 참조시킨다면 Presenter에서 보이는 위치에 그 컴포넌트를 배치할 필요가 있습니다.

        예를 들어 다음의 컴포넌트는 uGUI의 Slider를 제어하는 컴포넌트입니다.

        이 컴포넌트는 틀림없이 View 요소이므로, Presenter에서 참조해도 문제 없습니다.

        using DG.Tweening;
        using UnityEngine;
        using UnityEngine.UI;
        
        namespace MVRP.Views
        {
            /// <summary>
            /// uGUI의 Slider를 애니메이션으로 만드는 컴포넌트(View)
            /// </summary>
            public sealed class AnimationSlider : MonoBehaviour
            {
                [SerializeField] private Slider _slider;
        
                public void SetValue(float value)
                {
                    // 애니메이션하는 동안 슬라이더를 움직입니다.
                    DOTween.To(() => _slider.value,
                        n => _slider.value = n,
                        value,
                        duration: 1.0f);
                }
            }
        }

        (슬라이더를 애니메이션화 하면서 변화시킨다)

        Presenter는

        MV(R)P 패턴에서의 Presenter는 Model과 View를 중개하는 객체입니다.

        • Model → View: Model 내부 상태의 변화를 ReactiveProperty 혹은 Observable를 통해서 감지하고, 그것을 View에 반영
        • View → Model: View 상태 변화를 감지해서 그것을 Model에 반영
        • View와 Model 사이에 필요한 데이터의 변환 ( string → int 등의 형 변환이나 값의 표현 범위의 조정 등)

        같은 역할을 갖습니다.

        (경우에 따라서는 Model → View의 흐름으로 끝나는 경우도 많습니다)

        Presenter는 데이터의 변환 정도의 논리 밖에 없는 매우 얇은 레이어 입니다.

        Presenter에는 게임 로직과 View의 관리 로직을 가지고 있으면 안되며, 기본적으로 데이터의 교환과 간단한 데이터 변환만 해야 합니다.

        Presenter의 구현 방법

        Presenter은 인스펙터의 연결을 통해 Model 및 View 양쪽을 모두 참조할 필요가 있습니다.

        그래서 이들 2개가 참조할 수 있는 계층 및 모듈에 정의하고 각각을 연결시킵니다.

        실제 구현은

        Model의 Observable 혹은 ReactiveProperty을 Subscribe() 하고 그 상태 변화를 View에 전달한다.

        View의 상태 변화를 Model에 전달한다. 이러한 처리를 구현할 뿐입니다.

        다음 구현은 앞의 Player와 AnimationSlider를 연결한 Presenter 입니다.

        using MVRP.Models;
        using MVRP.Views;
        using UniRx;
        using UnityEngine;
        
        namespace MVRP.Presenters
        {
            /// <summary>
            /// 플레이어의 체력을 View에 반영하는 Presenter
            /// </summary>
            public sealed class PlayerHealthPresenter : MonoBehaviour
            {
                // Model
                [SerializeField] private Player _player;
        
                // View
                [SerializeField] private AnimationSlider _animationSlider;
        
                private void Start()
                {
                    // Player의 Health 모니터링
                    _player.Health
                        .Subscribe(x =>
                        {
                            // View에 반영
                            _animationSlider.SetValue((float)x / _player.MaxHealth);
                        }).AddTo(this);
                }
            }
        }

        Presenter를 잘 만드는 요령은 “Presenter로 복잡한 논리를 작성하지 않는다”, “Presenter에 상태를 가지지 못하게 한다” 입니다.

        Presenter는 어디까지난 Model과 View를 잇는 만큼의 존재이며, Presenter의 여부가 게임의 동작에 영향을 주어서는 안됩니다.

        (Presenter를 배치하지 않으면 Model의 동작이 바뀌거나 에러를 내어 동작하지 않는다는 등은 좋지 않습니다)

        “Presenter와 View는 미 구현으로도 Model만 있으면 게임 자체는 에러를 내지 않고 동작한다(제대로 플레이는 되지 않을 지라도)”라고 생각하세요.

        샘플의 동작 예

        위에서 작성한 Player와 AnimationSlider의 조합은 실제로 동작시키면 아래 처럼 됩니다.

        ( Player(캡슐)에 Enemy(상자)가 충돌 할 때마다 체력이 감소하고, 그것이 Slider에 반영된다)

        샘플의 장면 구현

        Presenter라는 GameObject에 PlayerHealthPresenter가 부착되어 있습니다.

        이것이 유니티의 hierarchy 윈도우상에서 Player와 Animation Slider를 직접 참조하고 있습니다.

        별첨 예: Model→View/View→Model의 상호 교환이 있는 사례

        아까의 예는 Model→View의 일방 통행이었습니다.

        아래는 View→Model의 경로도 있는 패턴 입니다.

        (슬라이더의 조작이 Model에 반영되고 Model의 조작이 슬라이더와 Text에 반영된다)
        using UniRx;
        using UnityEngine;
        
        namespace MVRP.Models
        {
            /// <summary>
            /// 데이터의 실체를 가진 클래스
            /// </summary>
            public sealed class SampleModel : MonoBehaviour
            {
                // Count 수
                public IReadOnlyReactiveProperty<int> Count => _count;
        
                // 최대값
                public readonly int MaxCount = 100;
        
                private readonly IntReactiveProperty _count = new IntReactiveProperty(0);
        
                public void SetCount(int value)
                {
                    // 숫자 범위 조정
                    value = Mathf.Clamp(value, 0, MaxCount);
                    _count.Value = value;
                }
        
                private void OnDestroy()
                {
                    _count.Dispose();
                }
            }
        }
        using MVRP.Models;
        using UniRx;
        using UnityEngine;
        using UnityEngine.UI;
        
        namespace MVRP.Presenters
        {
            /// <summary>
            /// Count의 Presenter
            /// </summary>
            public sealed class CountPresenter : MonoBehaviour
            {
                // Views
                // uGUI 구성 요소를 직접 참조
                [SerializeField] private Slider _slider;
                [SerializeField] private Text _text;
        
                // Model
                [SerializeField] private SampleModel _model;
        
                private void Start()
                {
                    // Model -> View
                    _model.Count.Subscribe(v =>
                        {
                            // Slider
                            _slider.value = ((float)v / _model.MaxCount);
        
                            // Text
                            _text.text = $"{v}%";
                        })
                        .AddTo(this);
        
                    // View -> Model
                    _slider.OnValueChangedAsObservable()
                        .Subscribe(x =>
                        {
                            // Slider는 0.0~1.0이므로 보정
                            // 이와 같은 Model-View간의 값의 범위 보정도
                            // Presenter의 책임
                            var value = (int)(100 * x);
        
                            // 모델에 반영
                            _model.SetCount(value);
                        })
                        .AddTo(this);
                }
            }
        }

        MV(R)P 패턴의 흔한 질문과 답변


        MV(R)P 패턴이 잘 이해가 가지 않는 다는 사람이 모아준 질문과 답변을 정리했습니다.

        MV(R)P 패턴은 MonoBehaviour를 사용해야 하나요?

        MV(R)P 패턴은 MonoBehaviour의 사용 여부를 말하고 있지 않습니다.

        MonoBehaviour는 모든 객체에서 사용해도 좋고 쓰지 않아도 됩니다. 즉 아무래도 좋습니다.

        가끔 Model은 Unity에 비 의존할 필요라고 주장하는 사람도 있지만 그런 일은 없습니다.

        원래 MV(R)P 패턴은 “GUI 구현을 깨끗하게 하는 것”을 목적으로 설계 한 패턴입니다.

        그래서 “GUI를 깨끗하게 구현할 수 있다면 세부 구현은 아무래도 괜찮다” 입니다.

        예를 들어 액션게임에서 ‘게임 중에 존재하는 캐릭터의 상태를 화면에 그린다’라는 패턴은 빈번합니다. 이 경우는 게임 중 캐릭터도 GameObject 위에 올려 움직이는 경우가 많겠죠.

        이때 MV(R)P패턴이 이루고 싶은 것은 ‘캐릭터의 상태를 GUI에 표시하고 싶다’입니다.

        (플레이어의 체력바를 그리는 등 자주 하는데 이 플레이어는 GameObject잖아?)

        만약 여기서 “Model에 MonoBehaviour를 사용하면 안 된다”라는 제약을 추가해 버리면 “GUI에 정보를 표시 하고 싶을 뿐인데 캐릭터를 MonoBehaviour 비 의존에으로 다시 설계 해야 한다”라는 상황이 되어 버립니다.

        “이렇게 하면 이야기가 너무 커져서” “GUI 구현을 깨끗하게 하고 싶다” 라는 목적에서 벗어나 버립니다.

        몇 번이나 말할게요. MV(R)P패턴의 목적은 “GUI 구현을 깨끗하게 하는 것”이 목적입니다.

        MonoBehaviour의 유무나 무엇이 유니티에 의존하고/하지 않는다는 논의는 말도 안됩니다.

        각 객체의 초기화 방법을 모르겠다.

        Model, View, Presenter이들 초기화는 고민의 문제입니다.

        여러가지 방식은 있지만, 최종적으로 “Presenter가 View 와 Model를 참조하고 있다”라는 형태를 만들 수 있으면 뭐든지 좋습니다.

        지금 떠오르는 한에서 3개의 패턴 정도 있으니 참고하세요.

        초기화 방법 A: 인스펙터 윈도우 화면 위에서 인스펙터에 할당

        Model, View, Presenter가 모두 처음부터 장면에 배치되어 있는 경우에 사용할 수 있는 패턴입니다.

        동적으로 생성되거나 삭제되는 일이 없다면 유니티 인스펙터 윈도우상에서 처음부터 연결해 두면 됩니다.

        (인스펙터상에서 Presenter에 미리 묶어 두는 것이 가장 간단)

        초기화 방법 B: DIContainer를 사용

        방법A와 별로 다르지 않지만 동적으로 오브젝트가 생성/삭제되지 않는다면 DIContainer(Zenject나 VContainer)를 통해 오브젝트를 Presenter에게 전달해 버리는 방법도 있습니다.

        초기화 방법 C: 동적으로 바인딩 한다.

        이것이 가장 까다로운 패턴입니다. 모델이 나중에 동적으로 추가되거나 삭제되는 것을 어떻게 다루는가입니다.

        제가 자주 사용하는 방법은 별도 Dispatcher라는 객체를 배치하고 그것이 Model의 생성/삭제의 이벤트를 구독하는 방법입니다.

        예를 들어 다음과 같은 것을 생각해 봅시다.

        • Player 체력 바 표시
        • Player는 동적으로 나중에 늘어난다.

        이런 상황을, Dispatcher를 사용하고 구현해 보도록 하겠습니다.

        Model

        (위의 Player와 마찬가지 코드)

        using System;
        using UniRx;
        using UnityEngine;
        
        namespace MVRP.Models
        {
            /// <summary>
            /// 액션 게임의 플레이어
            /// 「화면에 체력을 나타낸다」라고 하는 관점에서 보면 Model
            /// </summary>
            public sealed class Player : MonoBehaviour
            {
                /// <summary>
                /// 체력
                /// ReactiveProperty로 외부에 상태를 ReadOnly로 공개
                /// </summary>
                public IReadOnlyReactiveProperty<int> Health => _health;
        
                // 체력 최대치
                public readonly int MaxHealth = 100;
        
                private readonly IntReactiveProperty _health = new IntReactiveProperty(100);
        
        
                private void Start()
                {
                    _health.Value = MaxHealth;
                }
        
                /// <summary>
                /// 충돌 이벤트
                /// </summary>
                private void OnCollisionEnter(Collision collision)
                {
                    // Enemy와 충돌 시 체력 감소
                    if (collision.gameObject.TryGetComponent<Enemy>(out var _))
                    {
                        _health.Value -= 10;
                    }
                }
        
                private void OnDestroy()
                {
                    _health.Dispose();
                }
            }
        }

        Model을 동적으로 생성하는 Manager

        Player를 동적으로 생성하는 Manager가 있고 Player 생성 이벤트를 발행합니다.

        using System;
        using Cysharp.Threading.Tasks;
        using UniRx;
        using UnityEngine;
        using Random = UnityEngine.Random;
        
        namespace MVRP.Models
        {
            public sealed class PlayerManager : MonoBehaviour
            {
                /// <summary>
                /// 플레이어 목록
                /// ReactiveCollection으로 인해 플레이어 수가 증가하면 알림
                /// </summary>
                public IReactiveCollection<Player> Players => _players;
        
                private readonly ReactiveCollection<Player> _players = new ReactiveCollection<Player>();
        
                // Player의 Prefab
                [SerializeField] private Player _playerPrefab;
        
                private async UniTaskVoid Start()
                {
                    // 초기화 처리
                    // Player를 시간차로 4개 생성
        
                    await UniTask.Delay(TimeSpan.FromSeconds(1));
                    _players.Add(CreatePlayer(1));
        
                    await UniTask.Delay(TimeSpan.FromSeconds(1));
                    _players.Add(CreatePlayer(2));
        
                    await UniTask.Delay(TimeSpan.FromSeconds(1));
                    _players.Add(CreatePlayer(3));
        
                    await UniTask.Delay(TimeSpan.FromSeconds(1));
                    _players.Add(CreatePlayer(4));
                }
        
                private Player CreatePlayer(int id)
                {
                    // Player 생성
                    var player = Instantiate(_playerPrefab, Vector3.right * Random.Range(1f, 2f), Quaternion.identity);
                    player.name = $"Player{id}";
                    return player;
                }
            }
        }

        Presenter

        Presenter는 인수로 밖에서 Player와 View의 조합을 받아 바인딩할 수 있도록 해 둡니다.

        이번에는 단일 Presenter로 복수의 Player와 View를 다루게 합니다.

        using MVRP.Models;
        using MVRP.Views;
        using UniRx;
        using UnityEngine;
        
        namespace MVRP.Presenters
        {
            // Player의 Presenter
            public sealed class PlayerPresenter : MonoBehaviour
            {
                // Player가 생성되면 Bind
                public void OnCreatePlayer(Player player, PlayerView view)
                {
                    view.SetName(player.name);
        
                    // Player의 Health 모니터링
                    player.Health
                        .Subscribe(x =>
                        {
                            // View에 반영
                            view.SetHealth((float)x / player.MaxHealth);
                        }).AddTo(this);
                }
            }
        }

        View

        이번에는 View로 ‘이름표시공간’과 ‘체력바’가 있습니다.

        이 UI를 다루는 Canvas와 각 요소를 제어하는 컴포넌트를 준비해 Prefab화해 둡니다.

        using DG.Tweening;
        using UnityEngine;
        using UnityEngine.UI;
        
        namespace MVRP.Views
        {
            /// <summary>
            /// Player 상태 표시 View
            /// </summary>
            public sealed class PlayerView : MonoBehaviour
            {
                [SerializeField] private Text _nameplate;
                [SerializeField] private Slider _healthSlider;
        
                public void SetName(string name)
                {
                    _nameplate.text = name;
                }
        
                public void SetHealth(float value)
                {
                    // 애니메이션하는 동안 슬라이더를 움직입니다.
                    DOTween.To(() => _healthSlider.value,
                        n => _healthSlider.value = n,
                        value,
                        duration: 1.0f);
                }
            }
        }

        Dispatcher

        PlayerManager 이벤트를 감지하고 View를 생성하고 Presenter에 바인딩하는 Dispatcher를 준비합니다.

        이번에는 이 Dispatcher를 장면에 미리 배치하고 있는데 DIContainer등을 사용해도 무방합니다.

        using MVRP.Models;
        using MVRP.Presenters;
        using MVRP.Views;
        using UniRx;
        using UnityEngine;
        
        namespace MVRP.Dispatchers
        {
            /// <summary>
            /// Player 생성을 감지하고 View 할당
            /// </summary>
            public sealed class PlayerDispatcher : MonoBehaviour
            {
                // Model을 제공하는 Manager
                [SerializeField] private PlayerManager _playerManager;
        
                // Player의 Presenter
                [SerializeField] private PlayerPresenter _presenter;
        
                // View의 Prefab
                [SerializeField] private PlayerView _viewPrefab;
        
                private void Start()
                {
                    // 지금 목록에 있는 Player을 Dispatch
                    foreach (var p in _playerManager.Players)
                    {
                        Dispatch(p);
                    }
        
                    // 이후 신규 작성된 것을 Dispatch
                    _playerManager.Players.ObserveAdd().Subscribe(x => Dispatch(x.Value)).AddTo(this);
                }
        
                private void Dispatch(Player player)
                {
                    // Player의 자식 요소로 View 만들기
                    var view = Instantiate(_viewPrefab, player.transform, true);
        
                    // 위치 조정
                    view.transform.localPosition = Vector3.up * 1.5f;
        
                    // Presenter와 결합하여 알림
                    _presenter.OnCreatePlayer(player, view);
                }
            }
        }

        이번에는 Presenter를 미리 장면에 배치하고 있지만, Presenter도 또한 prefab로 만들어 버려서 때마다 생성하는 형태로 해도 문제 없습니다(어떤 방법을 사용해도 무방합니다)

        동작의 모습

        ( Player이 동적으로 생성되어도 체력 바가 각각 표시되고 각각의 체력이 반영됩니다)

        Presenter를 어디에 두면 될까요?

        Presenter를 장면의 어디에 배치할 것인가. 아니면 MonoBehaviour를 사용하지 않고 퓨어 클래스로 DIContainer에 관리해야 하는가. 라는 질문입니다.

        대답은 자유롭게 해도 된다는 것입니다.

        관리할 수 있다면 어디에 Presenter를 배치해도 됩니다. uGUI의 캔버스의 게임 오브젝트에 Presenter를 붙여도 되고, 빈 게임 오브젝트를 만들어 거기에 붙여도 됩니다.

        자신이 관리만 할 수 있다면 어디에 어떻게 놓아도 좋습니다.

        Presenter와 View의 규모감을 모르겠습니다.

        이것은 “하나의 Presenter가 복수의 View를 관리해도 되는가?”, “View 내부에서 상태를 가지고 있어도 되는가?”라는 질문과 같습니다.

        Presenter의 규모감

        “1개의 Presenter가 어떻게 View를 관리하느냐인데 이것의 답은 “자신이 관리할 수 있다면 자유롭게 해도 된다” 입니다.

        • 1개의 게임 오브젝트에 1개의 Presenter만 할당하는 방법
        • 1개의 게임 오브젝트에 복수의 Presenter를 할당하는 방법
        • 1개의 Presenter로 복수의 모델이나 뷰를 할당하는 방법

        (어떻게 해도 무방하다.)

        몇 가지 패턴은 들 수 있지만 어떤 것을 사용해도 무방합니다.

        View의 규모감

        “View 내부에서 상태를 가져도 되는가”, “View를 관리하는 컴포넌트도 View로 취급해도 되는지”이지만

        View 자신이 상태를 가지고 View가 자기 자신을 관리하는 것은 MV(R)P패턴적으로는 문제가 없습니다.

        예를 들어 다음과 같은 “RGB의 삼색 슬라이더”가 있다고 합시다.

        여기에는 “3개의 슬라이더를 관리하는 컴포넌트”가 부착되어 있습니다.

        using UniRx;
        using UnityEngine;
        using UnityEngine.UI;
        
        namespace MVRP.Views
        {
            /// <summary>
            /// RGB 슬라이더를 관리하는 뷰 컴포넌트
            /// </summary>
            public sealed class ColorSlider : MonoBehaviour
            {
                /// <summary>
                /// 지금의 슬라이더가 가리키는 색
                /// </summary>
                public readonly ColorReactiveProperty Color
                    = new ColorReactiveProperty(UnityEngine.Color.white);
        
                [SerializeField] private Slider _red;
                [SerializeField] private Slider _green;
                [SerializeField] private Slider _blue;
        
                private void Start()
                {
                    Color.Subscribe(c =>
                        {
                            _red.value = c.r;
                            _green.value = c.g;
                            _blue.value = c.b;
                        })
                        .AddTo(this);
        
                    // 슬라이더가 어느 하나라도 변화하면 반영
                    Observable.Merge(
                            _red.OnValueChangedAsObservable(),
                            _green.OnValueChangedAsObservable(),
                            _blue.OnValueChangedAsObservable())
                        .Subscribe(_ =>
                        {
                            Color.Value = new Color(_red.value, _green.value, _blue.value);
                        })
                        .AddTo(this);
                }
            }
        }
        using MVRP.Models;
        using MVRP.Views;
        using UniRx;
        using UnityEngine;
        using UnityEngine.UI;
        
        namespace MVRP.Presenters
        {
            // Presenter
            public class ColorPresenter : MonoBehaviour
            {
                [SerializeField] private ColorModel _model;
                [SerializeField] private ColorSlider _colorSlider;
                [SerializeField] private Text _text;
        
                private void Start()
                {
                    // Model -> View
                    _model.Color.Subscribe(x =>
                        {
                            _text.text = x.ToString();
                            _text.color = x;
        
                            _colorSlider.Color.Value = x;
                        })
                        .AddTo(this);
        
                    // View -> Model
                    _colorSlider.Color.Subscribe(x =>
                        {
                            _model.Color.Value = x;
                        })
                        .AddTo(this);
                }
            }
        }
        using UniRx;
        using UnityEngine;
        
        namespace MVRP.Models
        {
            // Model
            public class ColorModel : MonoBehaviour
            {
                public readonly ColorReactiveProperty Color
                    = new ColorReactiveProperty();
            }
        }

        이 값은 View나 Model 중 어느 쪽이 가져야 할 상태인가요?

        이 질문은 예컨대 “클릭되면 색이 변경되는 버튼이 있을 때 이 “색” 의 정보는 View와 Model어느 쪽이 가지는 것입니까?” 같은 질문입니다.

        답은 “**그 색 정보가 게임의 도메인 요소에 해당하는지 아닌지”**로 결정됩니다.

        (‘도메인’이란 ‘게임의 핵심 요소’와 같은 의미라고 생각해 주세요)

        그 ‘색 정보”가 게임 성립에 필수불가결한 요소라면 그것은 Model에서 관리해야 될 정보가 됩니다.

        한편 게임의 성립에 직접 관계없이 어디까지나 연출의 목적으로 버튼의 색을 바꾸는 정도라면 그것은 View에서 관리할 정보가 됩니다.

        예를 들어 “3색 리버시” 등은 색정보가 게임 성립에 절대 필수적인 요소입니다.

        그렇기 때문에 3색 리버시를 MV(R)P패턴으로 구축한다면 “색상 정보는 Model이 관리해야 한다”가 됩니다.

        • 3색 리버시 = 3색 오셀로 색으로 상대방을 구별한다.

        “한편으로 “좀 보기에 풍부한 토글 버튼”이라는 취급이라면 이것은 단지 연출일 뿐이고, 최악의 경우 버튼 색의 변화가 없어도 게임으로서 성립합니다.

        이런 경우는 View로 관리해야 된다고 생각 합니다.

        Model 요소가 되는 경우

        • 그 정보(데이터)가 게임의 성립에 필수 불가결하다.
          • 게임의 진행이나 상태 관리에 필수적이다

        View 요소가 되는 경우

        • 그 정보(데이터)가 없어도 게임으로서 성립된다.
          • 있다면 연출 적인 요소가 가미 되겠지만 최악의 경우 없어도 괜찮은 정도.

        View→Presenter→Model→Presenter→View라는 값이 루프 하지 않나요?

        1. View 변화를 검사하고 Presenter가 Model에 반영
        2. Model의 상태가 변한다
        3. Model의 상태가 변화하는 것을 감지하고 Presenter가 View에 반영
        4. View의 상태가 변한다
        5. View 변화를 감지하고 Presenter가 Model에 반영
        6. (이하 무한 루프)

        라는 현상이 일어나지 않느냐는 질문입니다.

        답은 “ReactiveProperty”를 사용하는 경우에 대해서는 루프 하지 않습니다”

        ReactiveProperty는 “직전과 같은 값이 오는 경우 이벤트를 발행하지 않는” 구조로 되어 있습니다. 그래서 이벤트가 1번은 와도 2번째 루프에서는 실행되지 않도록 되어 있습니다.

        다만 ReactiveProperty.SetValueAndForceNotify()를 쓰는 경우에는 강제 메시지 발행이 실행 되기 때문에 무한 루프를 일으킬 가능성이 있습니다.

        Model/View/Presenter마다 Assembly Definition Files를 나누는게 좋나요?

        답은 “마음대로 하세요” 입니다.

        엄격하게 하여 나누어도 좋고, 귀찮아서 나누지 않아도 됩니다.

        이 부분은 정말 마음대로 해도 문제 없습니다.

        의존 관계 역전의 법칙을 사용하고 의존 관계를 View → Presenter → Model로 하는게 좋을까요?

        참조 관계는 view ← Presenter → Model 이지만 모듈로서의 의존 관계는 View → Presenter → Model이 되는 건가요? 라는 질문입니다.

        의미를 모르는 분들은 아래 슬라이드를 참고하세요.

        Unity에 있어서의 설게 패턴(56쪽)

        결론은 “복잡하게 되므로 할 필요는 없다”고 나는 생각합니다.

        엄밀하게 하고 싶으면 해도 되는데 수고에 비해 이득이 많지 않을 겁니다.

        MVP 패턴을 사용하면 Unity 비 의존에서 깨끗하게 만들 수 있다고 들었습니다.

        답은 “못할 것은 없으나 그 이전에 생각할 일이 많다” 입니다.

        MVP 패턴(MV(R)P패턴)은 원래 GUI용 설계 패턴입니다.

        View(GUI)를 어떻게 관리하면 깨끗하게 구현할 수 있느냐는 얘기였습니다.

        그러나 이 View라는 개념을 확대 해석하고 View(Unity)와 Model(Unity 비의존)을 나누어 예쁘게 만들기 위한 설계 패턴이라고 착각 하고 있는 사람이 가끔 있습니다.

        • MVP 패턴을 사용하면 GameObject를 사용하지 않고 게임 로직이 설계 가능
        • MVP 패턴을 사용하면 액션 게임을 깨끗하게 만들 수 있다
        • MVP 패턴을 사용하면 설계가 모두 아름다워진다.

        이들은 모두 MVP 패턴에 지나치게 기대하여 만들어진 이야기 입니다.

        다시 말하지만 “MVP 패턴이 말하는 View란 GUI를 말한다”. GameObject나 MonoBehaviour를 말하는 것이 아닙니다. MVP패턴은 “GUI를 구현하기 위한 패턴”입니다.

        “프레임워크(Unity)”와 “도메인(게임 코어 논리)”를 분리한 설계는 확실히 이상적입니다.

        하지만 이를 실현하기 위해서는 “MVP패턴”만으로는 부족하고 다른 설계 패턴이나 아키텍쳐의 지식도 필요로 합니다 (예를 들어 최근 화제의 “클린 아키텍쳐”는 바로 “복잡화된 도메인에서 체제 의존을 어떻게 분리하는가”를 말하고 있는 아키텍쳐가 있습니다)

        (참고: Unity에서의 설계 레벨을 정의)

        그럼 MVP패턴을 확대해석해서 ‘프레임워크와 도메인 분리’에 이용하는 것이 안되는가 하면 딱히 그렇지 않습니다. 상황에 따라서는 MVP패턴을 사용할 수 있는 장면도 있기도 합니다.

        예를 들어 ‘서버 사이드에 로직과 데이터가 있다’ ‘Unity는 그 Viewer로 사용한다’처럼 분명하게 구분이 되어 있다면 MVP 패턴을 사용해도 잘 될 가능성이 있습니다.

        하지만 한편으로 ‘액션 게임’과 같은 ‘Unity에 밀접하게 의존한 구조 위에서 움직이는 게임’ 등은 애초에 ‘프레임워크와 도메인 분리’ 자체가 어렵기도 합니다. 이런 애초에 분리가 어려운 종류의 것을 억지로 분리해서 만드는 것은 매우 구현이 어려워서 잘 되지 않을 가능성이 높습니다.

        그래서 ‘프레임워크와 도메인의 분리’를 하는 경우는 우선 애초에 그것을 하는 의의가 있는지 생각할 필요가 있습니다. “프레임워크와 도메인 분리’는 확실히 설계상 깨끗해질 것 같지만 실제로는 실시 비용이 높고 개발 속도가 늦어진다는 단점이 있습니다.

        • 만들려고 하는 게임의 규모감
        • 개발자의 인원 수
        • 일정
        • 게임의 장르
        • 시스템 아키텍쳐(클라이언트 만인지 서버와 연계할지 등)

        “위 내용을 판단 근거로 ‘애초에 프레임워크와 도메인을 완전히 분리해서 만드는 것에 정말 장점이 있는가”, “어떤 아키텍쳐를 채택할것인가?”를 생각할 필요가 있습니다.

        그래서 답은 “(MVP 패턴을 사용하여 Unity 비 의존적인 구현은) 할 수 없는 것은 아니지만 그 이전에 생각할 일이 있다” 입니다

        다시 말하지만 “MVP 패턴은 GUI 설계를 위한 패턴”입니다.

        이를 응용하고, “프레임워크와 도메인의 분리”에 사용하는 것 자체는 나쁜 것이 없습니다. 하지만 그것을 하기에는 설계에 대한 다른 지식이나 그만한 각오가 필요합니다.

        그리고 그것을 초심자에게 요구하는 것은 가혹해서, MVP패턴은 순순히 GUI에만 사용하는 것이 안전하다는 말이 됩니다.

        정리


        • MV(R)P 패턴은 Unity에서 GUI 구현을 위한 설계 패턴
        • Model과 View를 Presenter라는 얇은 레이어로 연결 한다.
        • Model과 View의 연결에는 UniRx를 활용 (ReactiveProperty는 편리하다)
        • 위 내용 이외에는 자유롭게 사용해도 문제 없다.
          • 절대라는 것 없어서 자기만의 방식으로 해석해서 사용해도 무방하다.
          • 다만 어디까지나 GUI 설계 패턴이다 라는 것을 염두해 두자.