[Design pattern - 실무편] MVC 모델에서 사용되는 Route에 대한 패턴 (중재자 + 인터프리터 패턴)


Study/Design Pattern  2019. 6. 20. 09:00

안녕하세요. 명월입니다.


이 글은 MVC 모델에서 사용되는 Route에 대한 패턴에 대한 설명입니다.


MVC 모델은 최근 웹 개발에서 가장 많이 사용되는 프레임 워크 구조 개념인 듯 싶습니다.

저도 업무의 90%이상이 웹 개발입니다만 이제는 MVC 모델 개념이 없으면 프로그램을 어떻게 만들까 할 정도로 MVC 모델에 적응이 되었고 이상적이라고 생각합니다.


그 중 MVC 모델에서 Controller 과 Model과 View의 관계는 인터프리터 패턴과 중재자 패턴으로 만들어 집니다.(제 예상입니다.) 실제 C#의 MVC나 Java의 Spring에서는 이 부분이 이미 다 만들어져 있어서 굳이 유저가 만들어서 사용할 필요는 없습니다만 그래도 혹시 나중에 프레임 워크를 수정할 일이 있으면 알아두면 좋을 듯 싶습니다.


// 컨트럴러
class AController
{
  public View Index(AModel model)
  {
    return new View();
  }
  public View Main(BModel model)
  {
    return new View();
  }
}
// 컨트럴러
class BController
{
  public View Index(AModel model)
  {
    return new View();
  }
  public View Main(BModel model)
  {
    return new View();
  }
}
// 모델
class AModel
{
  public String Data { get; set; }
}
// 모델
class BModel
{
  public String Data { get; set; }
}
// 뷰
class View
{

}

C#의 MVC나 Java의 Spring을 구성하면 대강 저런 형태의 모습으로 구성합니다. View의 경우는 보통 cshtml이나 jsp의 모습이기는 하는데 예제이니깐 가볍게 만들었습니다.

지금부터는 C#의 MVC를 예로 설명하겠습니다.


여기서 우리가 웹에서 접속하길 「/A/Index?Data=test」의 형태로 접속하면 AController의 Index의 함수가 호출되고 파라미터 AModel 클래스의 Data 프로퍼티에 test값이 들어가 있습니다.

using System;
using System.Collections.Generic;

namespace Example
{
  // 실행 결과를 알기 위해 조금 수정했다.
  class AController
  {
    public View Index(AModel model)
    {
      Console.WriteLine(model.Data);
      model.Data = "AController Index AModel = " + model.Data;
      return new View(model);
    }
    public View Main(BModel model)
    {
      Console.WriteLine(model.Data);
      model.Data = "AController Main BModel = " + model.Data;
      return new View(model);
    }
  }
  // 실행 결과를 알기 위해 조금 수정했다.
  class BController
  {
    public View Index(AModel model)
    {
      Console.WriteLine(model.Data);
      model.Data = "BController Index AModel = " + model.Data;
      return new View(model);
    }
    public View Main(BModel model)
    {
      Console.WriteLine(model.Data);
      model.Data = "BController Main BModel = " + model.Data;
      return new View(model);
    }
  }

  class AModel
  {
    public String Data { get; set; }
  }

  class BModel
  {
    public String Data { get; set; }
  }
  // 실행 결과를 알기 위해 조금 수정했다.
  class View
  {
    public View(object data)
    {
      foreach (var p in data.GetType().GetProperties())
      {
        Console.WriteLine(p.Name + " = " + p.GetValue(data));
      }
    }
  }

  class Route
  {
    // Contoller를 재사용하기 위해 flyweight 패턴을 사용했다.
    private Dictionary<Type, object> flyweight = new Dictionary<Type, object>();
    public void Run(String url)
    {
      // Url를 파싱한다.
      // ?의 이후는 파라미터의 값이다.
      var buffer = url.Split('?');
      Dictionary<string,object> param = new Dictionary<string, object>();
      // 파라미터가 존재하면 탐색하기 편하게 Dictionary의 데이터로 만든다.
      if (buffer.Length > 1)
      {
        foreach(var b in buffer[1].Split('&'))
        {
          var data = b.Split('=');
          if(data.Length == 2)
          {
            param.Add(data[0], data[1]);
          }
        }
      }
      url = buffer[0];
      // url를 분할해서 /Controller이름/메서드 이름으로 분할한다.
      buffer = url.Split('/');
      
      // Controller에 후위 Controller의 문자열을 붙힌다.
      string controllerName = buffer[1] + "Controller";
      // 두번째 열은 메소드 이름이다.
      string methodName = buffer[2];
      // Example 네임스페이스에 컨트럴러 클래스가 존재하는가 찾는다.
      Type type = Type.GetType("Example."+controllerName);
      // 존재하지 않으면 에러이다.
      if (type == null)
      {
        throw new NotSupportedException();
      }
      // 컨트럴러 안에 메서드가 존재하는가 찾는다.
      var method = type.GetMethod(methodName);
      // 존재하지 않으면 에러이다.
      if (method == null)
      {
        throw new NotSupportedException();
      }
      // 컨트럴러의 재사용을 위해 flyweight 패턴을 사용했다.
      if (!flyweight.ContainsKey(type))
      {
        // 인스턴스를 생성한다.
        flyweight.Add(type, Activator.CreateInstance(type));
      }
      // flyweight로 부터 인스턴스를 가져온다.
      object instance = flyweight[type];
      var paramInstance = new List<Object>();
      // 매서드의 파라미터를 조사한다.
      foreach(var p in method.GetParameters())
      {
        // 파라미터의 인스턴스를 생성한다.
        object obj = Activator.CreateInstance(p.ParameterType);
        paramInstance.Add(obj);
        // 파라미터의 인스턴스 안에 프로퍼티 이름과 Url의 파라미터의 Key이 일치하는 것을 찾는다.
        foreach(var key in param.Keys)
        {
          var property = p.ParameterType.GetProperty(key);
          if (property != null)
          {
            // 존재하면 값을 주입한다.
            property.SetValue(obj, param[key]);
          }
        }
      }
      // 준비가 끝났다. 함수를 호출하자..
      method.Invoke(instance, paramInstance.ToArray());
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      var route = new Route();
      // BController의 Main 함수가 호출된다.
      route.Run("/B/Main");
      // AController의 Index 함수가 호출된다.
      route.Run("/A/Index?Data=test");

      Console.WriteLine("Press any key...");
      Console.ReadKey();
    }
  }
}

결과는 생각한 대로 나온 듯합니다. 실제 MVC의 Route는 제가 만든 것처럼 간단하지는 않습니다. 좀 더 Exception에 대한 제약도 강하고 저는 override에 대한 대책도 구현이 되지 않았습니다.

물론 Post의 경우도 구현이 되지 않았습니다.


그러나 제가 구현하고자 하는 것은 실제 MVC안에서는 이런 흐름이고 실무에서는 프레임워크를 이렇게 작성한다라는 것을 설명하고 싶었던 것입니다.

아마 MVC 프레임워크가 없는 배치 프로그램 등에서 이런 식으로 구현하면 편할 듯 싶습니다. (설명하다 보니 Run함수에 flow를 다 때려 박았습니다. 이렇게 작성하면 안됩니다. 우리는 Facade 패턴을 알고 있기 때문에...)


여기까지 MVC 모델에서 사용되는 Route에 대한 패턴에 대한 설명이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.