IT/WEB

클린 아키텍처1

개발자 두더지 2023. 2. 13. 21:38
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주면 감사하겠습니다.

 

 클린 아키텍처의 가장 추정적인 것이라고 한다면 바로 이 그림일 것이다. 따라서 이번 포스팅에서는 샘플코드와 함께 이 그림에 대해 해설해보고자 한다.

 

 

샘플 코드


https://github.com/nrslib/CleanArchitecture

 언어는 C#이다. 프로젝트 구성등 실제의 제품과 동일한 형식으로 기재되어 있기 때문에 괜찮다면 참고하길 바란다.

 

 

원 그림의 해설


 원 그림은 레이어를 의미한다. 레이어의 이름은 이 위치에 기재되어 있다. 그럼 하나 하나 설명하도록 하겠다.

 

Enterprise Business Rules 

 노란색 레이어인 Enterprise Business Rules는 비즈니스 로직을 표현하는 오브젝트가 소속된 레이어이다. 트랜잭션 스크립트나 도메인 구동 설계관련의 엔티티 등은 여기에 포함된다. 이 레이어가 제일 중요하다.

 

Application Business Rules

 빨간색 레이어는 Application Business Rules이다. 이 레이어는 "소프트웨어가 어떤 것이 가능한가"를 표현한다. Enterprise Business Rules에 소속된 오브젝트와 협동하여 유스 케이스를 달성한다.

 도메인 구동 설계관련 부분의 어플리케이션 서비스 같은 것들이 여기에 포함된다.

 

Interface Adapters

 초록색 레이어는 Interface Adapters로 입력, 영속화, 표시를 담당하는 오브젝트가 소속되는 부분이다. 입력이란 Application Business Rules에 전달하기 위한 데이터 가공을 의미하며, 영속화는 데이터의 저장을 의미하고, 표시는 결과의 표시를 의미한다.

 일반적인 MVC 프레임워크나 단체 클래스 등이 이 레이어에 소속된다.

 

Frameworks & Drivers

 Web 프레임워크나 데이터 베이스 조작 오브젝트 등의 긱 코드가 여기에 모인다. 프론트엔드의 UI등도 여기에 포함된다.

 

화살표의 방향

 이 화살표는 의존 방향을 나타낸다.

public class UserRepository{
  public void Save(User user){
    // 저장처리
  }
}

public class CreateUser{
  private readonly UserRepository userRepository;

  public CreateUser(UserRepository userRepository){ // ← UserRepository가 필요한 것으로 UserRepository에 의존하고 있다.
    this.userRepository = userRepository;
  }
}

 CreateUser 클래스가 컴포지션하고 있는 UserRepository는 구상 클래스이다. 이 경우 CreateUser 클래스는 UserRepository이라는 구상 클래스에 의존하고 있는 것을 표현한다.

 다음의 예를 보길 바란다.

public interface IUserRepository{
  public void Save(User user);
}

public class UserRepository : IUserRepository {
  public void Save(User user){
    // 저장 처리
  }
}

public class CreateUser{
  private readonly IUserRepository userRepository;

  public CreateUser(IUserRepository userRepository) { // userRepository에는 UserRepository가 대입되어 있다.
    this.userRepository = userRepository;
  }
}

 이 경우는 CreateUser는 IUsetRepository를 컴포지션하고 있어, IUserRepository에 의존하고 있는 상태이다. 이것은 프로그래밍의 테크닉으로 "인터페이스에 관해 프로그래밍한다" 혹은 "오브젝트가 아닌 추상에 의존한다"이라는 원리에 따라는 경우의 코드이다.

 이 원리를 지키면, 모듈은 특정의 구상 클래스에 의존하지 않게 된다(CreateUser 클래스의 스코프상 UserRepository이라는 클래스가 일체 표현되지 않는다).

 추상을 컨스트럭터에서 받도록 모듈을 만들면 동작시에 의존해야 마땅할 구상 클래스는 어떠한 형태(샘플 코드에서는 컨스트럭터)로 인도받게 되기 때문에 동일한 처리라도 유연성이 있게 된다.

 즉, 예를 들면 다음과 같은 인메모리에서 동작하는 테스트용의 모듈을 이용하는 것으로, CreateUser 클래스에는 변경을 가하지 않고 동작을 변경하는 것이 가능하게 된다.

public class InMemoryUserRepository : IUserRepository {
  private readonly Dictionary<string, User> db = new Dictionary<string, User>();

  public Save(User user) {
    db[user.Id] = user;
  }
}

public class CreateUser{
  private readonly IUserRepository userRepository;

  public CreateUser(IUserRepository userRepository) { // userRepository에는 InMemoryUserRepository오브젝트가 대입되어 있다.
    this.userRepository = userRepository;
  }
}

 이 화살표는 내부 쪽 층의 오브젝트는 외부쪽 층의 오브젝트에 의존하지 않도록하는 것을 의미한다.

 

 

용어와 구현


 레이어에 대해서 어느정도 설명을 끝냈으므로 그 외의 용어에 대해서 설명하고자한다. 지금까지의 용어를 이해하기 위해서는 구체적으로 구현된 코드를 보는 것이 좋을 것 같으므로 같이 첨부하도록 하겠다.

 자주 볼 수 있는 유스 케이스인 "유저 등록"을 예로 샘플 코드를 만들어보자. 먼저 콘솔에서 움직이는 것을 목표로 한다.

 

UserCase

그림에서는 UserCases로 기재되어 있는데 UseCase가 여러개 존재하고 있음을 나타낸다. 시스템에는 몇 가지의 유스 케이스가 있다. 그 유스 케이스를 표현하도록 UseCase를 만들어보자.

 유저 등록의 UseCase를 다음과 같이 만들었다.

public interface IUserCreateUseCase{
  void Handle(UserCreateInputData inputData);
}

  물론 유저 등록에는 파라미터가 있다. 즉 입력용 파라미터가 필수이다.

public class UserCreateInputData{
  public UserCreateInputData(string userName){
    UserName = userName;
  }

  public string UserName { get; }
}

 

 입력 데이터는 DTO(Data Transfer Object)로 준비한다. 또한 유저를 등록하면 그 정보가 필수로 필요한 경우가 있다. 따라서 출력용의 데이터도 필요하다.

public class UserCreateOutputData{
  public UserCreateOutputData(string userId, DateTime created){
    UserId = userId;
    Created = created;
  }

  public string UserId { get; }
  public DateTime Created { get; }
}

 이러한 DTO로 이용할 데이터 타입은 프리미티브한 것이나 플레인형으로 구성한다.  열거형이나 값 오브젝트를 이용하고 싶은 경우는 Application Business Rules보다 내부의 레이어의 Enterprise Business Rules의 레이어 형태라면 참조해도 문제 없다.

 외부 레이어형태는 절대적으로 참조해서는 안된다. 의존 룰을 파괴하는 것이 되기 때문이다. 의존 룰을 헤치는 경우 외부의 레이어 변경이 내부 레이어에 영향을 끼치는 사태가 발생하게 된다.

 

Repository

Repository는 Interface Adapter 레이어에 있는 GateWays와 관련이 있다.

 리포지토리 패턴으로 알려져 있으며, 특정 모델의 데이터 영속화관련해서 추상화한 오브젝트이다. 이번의 "유저 등록"에서는 유저이라는 모델의 영속화가 필요할 것이라고 생각된다. 이것을 표현한 리포지토리는 다음과 같다.

public interface IUserRepository{
  User FindByUserName(string userName);
  void Save(User user);
}

 이 유저 리포지토리를 예를 들어 mysql에 데이터를 저장하도록 구현한다면 코드를 다음과 같이 쓰게 된다.

public class UserRepository : IUserRepository {
  public User FindByUserName(string username) {
    using (var con = new MySqlConnection(Config.ConnectionString)) {
      con.Open();
      using (var com = con.CreateCommand()) {
        com.CommandText = "SELECT * FROM t_user WHERE username = @username";
        com.Parameters.Add(new MySqlParameter("@username", username));
        var reader = com.ExecuteReader();
        if (reader.Read()) {
          var id = reader["id"] as string;
          return new User(
            id,
            username
          );
        } else {
          return null;
        }
      }
    }
  }

  public void Save(User user) {
    using (var con = new MySqlConnection(Config.ConnectionString)) {
      con.Open();

      bool isExist;
      using (var com = con.CreateCommand()) {
        com.CommandText = "SELECT * FROM t_user WHERE id = @id";
        com.Parameters.Add(new MySqlParameter("@id", user.Id.Value));
        var reader = com.ExecuteReader();
        isExist = reader.Read();
      }

      using (var command = con.CreateCommand()) {
        command.CommandText = isExist
          ? "UPDATE t_user SET username = @username WHERE id = @id"
          : "INSERT INTO t_user VALUES(@id, @username)";
        command.Parameters.Add(new MySqlParameter("@id", user.Id.Value));
        command.Parameters.Add(new MySqlParameter("@username", user.UserName));
        command.ExecuteNonQuery();
      }
    }
  }
}

 그 외에도 테스트용으로 메모리 상에 데이터 베이스와 같은 동작을 하는 리포지토리등도 구현할 수 있다.

public class InMemoryUserRepository : IUserRepository {
  private readonly Dictionary<string, User> data = new Dictionary<string, User>();

  public void Save(User user) {
    data[user.Id] = cloneUser(user);
  }

  public User FindByUserName(string username) {
    return data.Select(x => x.Value).FirstOrDefault(x => x.UserName == username);
  }

  public IEnumerable<User> FindAll() {
    return data.Values;
  }

  private User cloneUser(User user) {
    return new User(user.Id, user.UserName);
  }
}

 

Presenter

 Presenter은 표현하기 위한 데이터 가공이 주목적이다. 예를 들어 이번의 OutputData에서는 DateTime이라는 데이터 형태가 사용된다.

public class UserCreateOutputData {
  public UserCreateOutputData(string userId, DateTime created) {
    UserId = userId;
    Created = created;
  }

  public string UserId { get; }
  public DateTime Created { get; }
}

 그렇다면 이 Created 필드를 화면에 표시할 때, 보통이라면 어떠한 타이밍에서 문자열 데이터 형태로 바꿀 것이다. 이 포맷에서도 스마트폰이나 PC 등의 플랫폼에 따라  '2018/9/1'보다 '2018년 9월 1일'이 적합하거나 표현 방법이 다른 경우가 발생한다.

  만약 이것을 비즈니스 로직이 서포트하면 어떻게 될까? OutputData에 다음과 같으 프로퍼티를 추가하게 된다.

public class UserCreateOutputData {
  public UserCreateOutputData(string userId, DateTime created, string createdForSystemA, string createdForSystemB) {
    UserId = userId;
    Created = created;
    CreatedForSystemA = createdForSystemA;
    CreatedForSystemB = createdForSystemB;
  }

  public string UserId { get; }
  public DateTime Created { get; }
  public string CreatedForSystemA { get; } // '2018/9/1'형식
  public string CreatedForSystemB { get; } // '2018년9월1일'형식
}

 포맷화된 데이터는 컨스트럭트에서 받으므로, 날짜 형식을 각 종 문자열로 포맷하는 처리가 비즈니스 로직에 기재된다. 그리고 그것은 표현 방법이 늘어날 때마다 비즈니스 로직에 추가 수정이 필요한 것을 의미한다. 

 이 문제에 대응하기 위해서는 이러한 표현의 차이는 비즈니스 로직이 서포트하지 않도록 해야할 필요가 있다. 여기서 이용하는것이 Presenter이다.

 Presenter는 UseCase가 출력하는 OutputData를 View하기 위한 ViewModel로의 변환을 한다. 구체적인 구현 코드는 다음과 같다.

public interface IUserCreatePresenter{
  void Complete(UserCreateOutputData outputData);
}

 Presenter의 interface를 준비한다.

'2018/9/1' 형식을 필요로하는 UI용 Presenter은 다음과 같이 쓸 수 있다.

public class UserCreateViewModel{
  public UserCreateViewModel(string userId, string createdDate){
    UserId = userId;
    CreatedDate = createdDate;
  }

  public string UserId { get; }
  public string CreatedDate { get; }
}

public class UserCreatePresenter : IUserCreatePresenter{
  public void Complete(UserCreateOutputData outputData){
    var userId = outputData.UserId;
    var createdDate = outputData.Created;
    var createdDateText = createdDate.ToString("yyyy/MM/dd");
    var model = new UserCreateViewModel(userId, createdDateText);
    Console.WriteLine("id:" + model.UserId + " created:" + model.CreatedDate);
  }
}

 또한 2018년9월1일 형식을 필요하는 UI용의 Presenter은 다음과 같이 된다.

public class SystemBUserCreateViewModel{
  public SystemBUserCreateViewModel(string userId, string createdDate){
    UserId = userId;
    CreatedDate = createdDate;
  }

  public string UserId { get; }
  public string CreatedDate { get; }
}

public class SystemBUserCreatePresenter : IUserCreatePresenter{
  public void Complete(UserCreateOutputData outputData){
    var userId = outputData.UserId;
    var createdDate = outputData.Created;
    var createdDateText = createdDate.ToString("yyyy년MM월dd일"); // 다른점은 여기뿐
    var model = new SystemBUserCreateViewModel(userId, createdDateText);
    Console.WriteLine("id:" + model.UserId + " created:" + model.CreatedDate);
  }
}

 또한 경우에 따라 유저가 생성된 사실만을 알려도 괜찮은 경우도 있다. 그러한 경우는 bool의 플래그를 갖도록 하는 ViewModel을 준비하게 된다.

 이러한 프레젠테이션마다의 표현의 차이를 흡수하기 위한 오브젝트가 Presenter이다.

 

Interactor

 UseCase는 interface로 준비되어 있으므로 코드 작성은 필요없다. 코드 작성은 Interactor이라고 부르는 오브젝트에서 한다. 

 Interactor는 Enterprise Business Rule에 소속된 오브젝트를 협조시켜 유스 케이스를 달성한다. 도메인 구동 설계의 어플리케이션 서비스이다. 

 비즈니스 로직 그 자체를 일컫는 것이 아닌, 그 모델(Entity)의 조절에 맞도록 구현한다.

 유저 등록의 Interactor은 다음과 같다.

public class UserCreateInteractor : IUserCreateUseCase {
  private readonly IUserRepository userRepository;
  private readonly IUserCreatePresenter presenter;

  public UserCreateInteractor(IUserRepository userRepository, IUserCreatePresenter presenter) {
    this.userRepository = userRepository;
    this.presenter = presenter;
  }

  public void Handle(UserCreateInputData inputData) {
    var username = inputData.UserName;
    var duplicateUser = userRepository.FindByUserName(username);
    if (duplicateUser != null) {
      throw new Exception("duplicated");
    }

    var user = new Domain.Users.User(username);
    userRepository.Save(user);

    var outputData = new UserCreateOutputData(user.Id, DateTime.Now);
    presenter.Complete(outputData);
  }
}

 리포지토리와 프레젠터는 각각 interface에서 받으므로, 그 스크립터는 특정 인프라에 의존하지 않는다.

 

Controller

 Controller는 유저 입력을 해석하여 UseCase에 전달해준다. Presenter이 출력을 위한 변환을 하는 것에 반해 Controller는 입력을 UseCase를 위해 변환한다.

 TV의 리모컨이나 게임의 컨트롤러 등을 대입해보면 이해하기 쉬울지도 모른다. TV의 리모든튼 버튼을 누르면 그 누른 정보를 텔레비전의 신호로 "변환"하여, 보낸다. 컨트롤러는 시스템을 위해 변환한다.

 구현 코드는 다음과 같다.

public class UserController {
  private readonly IUserCreateUseCase userCreateUseCase;

  public UserController(IUserCreateUseCase userCreateUseCase) {
    this.userCreateUseCase = userCreateUseCase;
  }

  public void CreateUser(string userName) {
    var inputData = new UserCreateInputData(userName);
    userCreateUseCase.Handle(inputData);
  }
}

 컨트롤러는 유저가 입력한 userName을 UseCase가 이해할 수 있도록 입력 데이터로 변환한다.

 

interface와 그 실태

 지금까지 소개한 클래스에는 일부, 컨스트럭터에서 interface을 받고 있는 것이 있다.

  • IUserCreateUseCase
  • IUserRepository
  • IUserPresenter

 이것들이 interface이다. 그럼 interface에 맞는 구상 클래스는 어디에 정의되어 있을까?

 이러한 컨스트럭터에서 받은 interface에 어떤 구상 클래스를 적용할 것인가를 정의하는 일반적인 방법으로 DIContainer가 있다.

var serviceCollection = new ServiceCollection();

// IUserRepository가 요구되는 UserRepository를 전달
serviceCollection.AddTransient<IUserRepository, UserRepository>();

// IUserCreatePresenter가 요구되는 UserCreatePresenter를 전달
serviceCollection.AddTransient<IUserCreatePresenter, UserCreatePresenter>();

// IUserCreateUsecase가 요구되는 UserCreateInteractor를 전달
serviceCollection.AddTransient<IUserCreateUsecase, UserCreateInteractor>();

var provider = serviceCollection.BuildServiceProvider();

// IUserCreateUseCase를 요구되는 UserInteractor의 인스턴스를 획득
var interactor = provider.GetService<IUserCreateUseCase>();

 마지막의 interactor은 UserInteractor의 인스터스가 대입된다. 또한 그 UserInteractor의 인스턴스를 만들 때는 아래와 같이 설정된 구상 클래스의 인스턴스가 전달된다.

public class UserCreateInteractor : IUserCreateUseCase {
  private readonly IUserRepository userRepository;
  private readonly IUserCreatePresenter presenter;

  //                             ↓ UserRepository의 인스턴스     ↓ UserPresenter의 인스턴스
  public UserCreateInteractor(IUserRepository userRepository, IUserCreatePresenter presenter) {
    this.userRepository = userRepository;
    this.presenter = presenter;
  }

 이 DI 프레임워트는 C#용이므로 언어에 따라 기재되는 방법의 차이가 있을 것이라고 생각되지만, 기본적인 기능은 동일하다.

 이러한 DIContainer등을 이용하여 프로그램 실행시에 추상 클래스와 그와 연결된 구상 클래스를 엮는 설정을 한다.

 로컬에서 동작하는 디버그용의 설정이나 실제 환경의 프로덕트용의 설정을 하는 스크립터를 만들어보자.

 static class Startup
{
  public static IServiceCollection ServiceCollection { get; } = new ServiceCollection();

  public static void Run() {
#if DEBUG
    setupDebug();
#else
    setupProduct();
#endif
  }

  private static void setupProduct() {
    ServiceCollection.AddTransient<IUserRepository, UserRepository>();
    ServiceCollection.AddTransient<IUserCreatePresenter, UserCreatePresenter>();
    ServiceCollection.AddTransient<UserController>();
  }

  private static void setupDebug() {
    ServiceCollection.AddTransient<IUserRepository, InMemoryUserRepository>();
    ServiceCollection.AddTransient<IUserCreatePresenter, UserCreatePresenter>();
    ServiceCollection.AddTransient<UserController>();
  }
}

 이 처럼 프로젝트의 구성 설정등을 이용하여 테스트와 실제 환경을 전환하는 것이 가능하다.

 

콘솔 프로그램

 지금까지의 코드를 활용하여 콘솔 프로그램으로 완성하면 다음과 같다.

class Program {
  static void Main(string[] args) {
    Startup.Run();
    var serviceCollection = Startup.ServiceCollection;
    var serviceProvider = serviceCollection.BuildServiceProvider();

    Console.WriteLine("=======================================");
    Console.WriteLine("Welcome to sample of clean architecture");
    Console.WriteLine("=======================================");
    Console.WriteLine();
    Console.WriteLine("Enter the name of the new user.");
    Console.WriteLine("username:");
    Console.Write(">");
    var username = Console.ReadLine();
    var controller = serviceProvider.GetService<UserController>();
    controller.CreateUser(username);

    Console.WriteLine("press any key to exit.");
    Console.ReadKey();
  }
}

 처리의 흐름에 대해 설명하고자한다.  디버그 구성으로 프로그램을 실행하는 것을 가정한다.

 먼저 프로그램은 Program.Main의 처리부터 시작한다. 여기에서 유저 입력이 UserController에 전달된다.

class Program {
  static void Main(string[] args) {
    Startup.Run();
    var serviceCollection = Startup.ServiceCollection;
    var serviceProvider = serviceCollection.BuildServiceProvider();

    Console.WriteLine("=======================================");
    Console.WriteLine("Welcome to sample of clean architecture");
    Console.WriteLine("=======================================");
    Console.WriteLine();
    Console.WriteLine("Enter the name of the new user.");
    Console.WriteLine("username:");
    Console.Write(">");
    var username = Console.ReadLine();
    var controller = serviceProvider.GetService<UserController>(); // UserController を DIContainer から取得して
    controller.CreateUser(username); // UserController에 유저입력 내용을 전달

 유저 컨트롤러를 직접 인스턴스화하는 것이 아닌, DIContainer를 이용하여 각각 설정한 인스턴스가 이용되게 된다. 계속해서 UserControoler의 처리이다.

public class UserController {
  private readonly IUserCreateUseCase userCreateUseCase;

  public UserController(IUserCreateUseCase userCreateUseCase) {
    this.userCreateUseCase = userCreateUseCase; // 실제는 DIContainer에 등록된 UserCreateInteractor
  }

  public void CreateUser(string userName) {
    var inputData = new UserCreateInputData(userName); // Controller은 여이서 유저 입력을 UseCase가 알 수 있는 입력치로 변환
    userCreateUseCase.Handle(inputData); // 변환된 입력 데이터를 이용하여 UseCase를 실행한다.
  }
}

 입력 데이터는 IUserCreateUseCase를 경유하여 실태 클래스의 UserCreateInteractor에 전달된다.

public class UserCreateInteractor : IUserCreateUseCase {
  private readonly IUserRepository userRepository;
  private readonly IUserCreatePresenter presenter;

  public UserCreateInteractor(IUserRepository userRepository, IUserCreatePresenter presenter) {
    this.userRepository = userRepository; // InMemoryUserRepository
    this.presenter = presenter; // UserCreatePresenter
  }

  public void Handle(UserCreateInputData inputData) {
    var username = inputData.UserName;
    var duplicateUser = userRepository.FindByUserName(username); // InMemoryUserRepository.FindByUserName가 호출됨
    if (duplicateUser != null) {
      throw new Exception("duplicated");
    }

    var user = new Domain.Users.User(username);
    userRepository.Save(user);

    var outputData = new UserCreateOutputData(user.Id, DateTime.Now);
    presenter.Complete(outputData); // UserCreatePresenter.Complete가 호출됨
  }
}

 이 userRepository와 presenter은 각각 InMemoryUserRepository와 UserCreatePresenter이다.

 리포지토리를 이용하여 지금까지 작성하려고 한 유저가 없는 것을 확인하면 신규 유저를 등록하고, 그 등록된 정보가 IUserCreatePresenter를 통해 UserCreatePresenter에 전달된다.

public class UserCreatePresenter : IUserCreatePresenter{
  public void Complete(UserCreateOutputData outputData){
    var userId = outputData.UserId;
    var createdDate = outputData.Created;
    var createdDateText = createdDate.ToString("yyyy/MM/dd");
    var model = new UserCreateViewModel(userId, createdDateText);
    Console.WriteLine("id:" + model.UserId + " created:" + model.CreatedDate);
  }
}

 위 처리 흐름을 정리하면 다음과 같다.

1. UserController가 IUserCreateUseCase에 입력 데이터를 전달됨

2. IUserCreateUseCase의 실체인 UserCreateInteractor에 처리가 이양됨.

3. UserCreateInteractor은 처리를 하여 그 결과를 IUserCreatePresenter에 출력 데이터를 전달함.

4. IUserCreatePresenter의 실체인 UserCreatePresenter에 처리가 이양됨.

5. UserCreatePresenter은 표현함.

 이 그림에서 나타내는 것은 아래와 같다.

 이 그림은 원 오른쪽 아래에서 봤을 것이다. 다시 이 그림을 보면 <I>는 interface를 나타내고 화살표의 경우 빈 흰색 화살표는 UML의 범용화, 평범한 화살표는 의존을 표시하는 것을 알 수 있다.

 

 

이렇게 해서 어떤 메리트를 얻을 수 있을까?


 지금까지의 설명을 봤을 때 매우 확장적인 코드라고 느껴질 것이라고 생각된다. 이렇게 구현하여 얻을 수 있는 메리트는 과연 무엇일까? 가장 큰 장점은 "테스트할 수 있는 형태가 된다"는 것이다.

 

프론트엔드의 테스트

 프론트엔드와 백엔드를 다른 팀에서 만들고 있다고 상정하자. 혹시 백엔드가 아직 안됐을 때 프론트엔드의 팀은 백엔드가 완성할 때까지 기다려야할 것인가?

 다른 작업이 있다면 그것을 해도 좋겠지만, 없다면 백엔드가 완성될 때까지 아까운 시간이 흘러간다. 이럴 때 프론트 개발 테스트용 Interactor를 이용하여 프론트엔드를 작성하면 된다.

public class MockUserInteractor : IUserCreateUseCase {
  public static int id;
  private readonly IUserCreatePresenter presenter;

  public MockUserInteractor(IUserCreatePresenter presenter){
    this.presenter = presenter;
  }

  public void Handle(UserCreateInputData inputData){
    var outputData = new UserCreateOutputData(id++, DateTime.Now);
    presenter.Complete(outputData);
  }
}

 이 MockUserInteractor를 DIContainer에 설정해주면 UserController은 이 테스트용 오브젝트를 이용하므로 백엔드가 완성되지 않아도 선행하여 프로덕트 개발을 할 수 있다.

 또한, 재현성이 낮은 에러 테스트도 간단하게 할 수 있다. 혹시 Interactor에서 발생하기 어려운 에러가 존재한다고 가정하여, 그것에 대해 프론트엔드의 동작을 확인하고 싶은 경우는 그 전용 Interactor를 만들어 발생시키면 된다.

public class ThrowComplexExceptionUserInteractor : IUserCreateUseCase {
  public void Handle(UserCreateInputData inputData){
    throw new ComplexException();
  }
}

 발생시키기 어려운 에러는 몇 가지가 있을 것이다. 그러한 에러에 대해 핸들링을 작성한 것에 대해 정합성있는 데이터 준비가 어려워 테스트를 하지 않고 릴리즈한다라는 위험을 감수하지 않아도된다.

 

백엔드의 테스트

 특정 인프라에 엮여있는 부분이 interface로 되어 있어 즉 추상화되어 있으므로, 데이터베이스나 API등을 이용하지 않고 비즈니스 로직의 테스트할 수 있다.

 프론트엔드 테스트의 경우에도 설명했지만, 백엔드에서도 테스트하기 어려운 에러 핸드링이 많이 존재한다. 예를 들어 데이터 베이스에 예외가 발생했을 때의 핸들리 테스트를 한다고 가정한 경우에는 리포지토리에 예외를 발생시키면 테스트할 수 있다.

public class ThrowSQLExceptionUserRepository : IUserRepository{
  public void Save(User user) {
    throw new SQLException();
  }

  public User FindByUserName(string userName){
    throw new SQLException();
  }
}

 이러한 스탭을 매번 만드는 것은 귀찮으므로 Mock을 만드는 라이브러리(C#의 경우 Moq같은)를 이용하여 만드면 된다.


참고자료

https://qiita.com/nrslib/items/a5f902c4defc83bd46b8

728x90

'IT > WEB' 카테고리의 다른 글

[Spring] @Autowired  (0) 2023.07.09
클린 아키텍처2  (0) 2023.02.15
좋지 않은 Repository 패턴(안티패턴)  (0) 2023.02.02
SPA, SSR, SSG는 무엇인가?  (0) 2023.01.24
MVC 모델  (0) 2023.01.22