.NET Framework 4.X 에서 Option Pattern 사용하기

Option Pattern은 Ubiquitous design pattern 중 하나로 web.config와 같은 Application의 모든 환경 설정 넣어서 사용하는 파일을 목적에 맞는 클래스들로 분리 시키고 인터페이스로 추상화하여 사용할 수 있도록 해준다.

이러한 이유로, Option Pattern을 사용하면 소프트웨어 엔지니어링 원칙중 2가지를 따를 수 있게 된다. 하나의 기능에 대한 환경 설정을 위한 클래스로 분리해서 사용하기 때문에 SRP(Single Responsibility Principle)를 따르게 되며, 인터페이스를 통해서 클래스의 값을 주입 받기 때문에 ISP(Interface Segregation Principle)를 따르게 된다.

기존에 web.config에 설정된 환경 변수들을 사용할때는 다음과 같이 String 타입인 XML 값을 읽어와야 했다. 그리고 필요에 따라 String 타입을 필요한 타입에 맞추어 Cast 해서사용해야 했다.

string SecretName = ConfigurationManager.AppSettings["SecretName"]

하지만, Option Pattern을 사용하다면, 미리 정의된 클래스 속성에 맞추어 Injection 과정에서 Cast가 진행되기 때문에 바로 사용할 수 있는 편리함이 있다. 그리고 미리 정의된 클래스 속성 이름을 참조하여 사용하기 때문에 항상 동일한 이름을 일관되게 코드에 적용 할 수 있게 되며, 참조 추적을 통해 얼마나 많은 곳에 해당 환경 설정이 사용되고 있는지 영향도 파악을 할 수 있게 되는 장점을 얻을 수 있다.

Option Pattern은 .Net Core부터는 Option pattern이 기본 패키지로 제공되기 때문에 쉽게 사용할 수 있다. 하지만 점점 EOS(End Of Service)가 공지되고 있는 .Net Framework는 그렇지 않기 때문에 만들어 사용해야 한다.

아래 구현된 소스코드는 .Net Framework 4.8 LTS 기준으로 작성되었고 Injector는 Simple Injector를 사용하였다. 전체 소스는 Github를 참고하길 바란다.

Web.Config

<appSettings>    
  <!--Application Settings-->
  <add key="SecretName" value="Name" />
  <add key="SecretKey" value="p@ssWord!" />    
</appSettings>

Options Class

public class SecretOptions
{    
    [Option("SecretName")]
    public string secretName { get; set; }
               
    [Option("SecretKey")]
    public string secretKey { get; set; }             
}

Options Interface

public interface IOptions<T>
{
    T Value { get; }
}

Options Concrete

public class Options<T> : IOptions<T>
{
    private readonly T model;
    private readonly List<string> AppSettingNames;
    
    public Options()
    {      
        AppSettingNames = ConfigurationManager.AppSettings.AllKeys.ToList();           

        T instance = Activator.CreateInstance<T>();

        model = (T)SetConfig(instance.GetType());
    }

    private object SetConfig(Type type)
    {
        object instance = Activator.CreateInstance(type);

        var instanceProperties = instance.GetType().GetProperties();

        foreach (var property in instanceProperties)
        {
            //If it is a hierarchical option class, it is searched recursively.
            if (property.GetCustomAttribute<OptionObjectIgnoreAttribute>() != null)
            {
                property.SetValue(instance, SetConfig(property.PropertyType));
            }

            //If an attribute does not have an 'OptionAttribute', the value is searched by the attribute's original name. 
            //but if there is, the value is searched by the set name at 'OptionAttribute'
            var name = AppSettingNames.FirstOrDefault(m => property.GetCustomAttribute<OptionAttribute>() == null ?
                                                            property.Name.Equals(m, StringComparison.InvariantCultureIgnoreCase) :
                                                            property.GetCustomAttribute<OptionAttribute>().keyName.Any(n => n.Equals(m, StringComparison.InvariantCultureIgnoreCase)));

            var value = ConfigurationManager.AppSettings.Get(name);

            if (!value.Equals(String.Empty))
            {
                property.SetValue(instance, Convert.ChangeType(value, property.PropertyType));
            }
        }

        return instance;
    }

    public T Value
    {
        get
        {
            return model;
        }
    }
}

Global.asax

var container = new Container();

container.RegisterMvcControllers(Assembly.GetExecutingAssembly());

//Injection form Web.config settings to Option Model  
container.Register<IOptions<SecretOptions>, Options<SecretOptions>>(SimpleInjector.Lifestyle.Singleton);

DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(container));

Dependency Injection 개념과 Ninject 사용법

소프트웨어를 잘 만들기 위해서 많은 디자인 패턴들이 사용되는데, 그중에서 DI(Dependency Injection, 의존성 주입)에 대한 개념과 .NET MVC에서 많이 사용되는 DI Framework들 중 하나인 Ninject(Open Source Project)에 대해서 알아보겠다.

DI(Dependency Injection) 란?

프로그래밍에서 사용되는 객체들 사이의 의존관계를 소스코드가 아닌 외부 설정파일 등을 통해 정의하게 하는 디자인 패턴이다. 개발자는 각 객체의 의존관계를 일일이 소스코드에 작성할 필요 없이 설정파일에 의존관계가 필요하다는 정보만 된다. 그러면 객체들이 생성될때, 외부로부터 의존관계를 주입 받아 관계가 설정 된다.

DI를 적용하면, 의존관계 설정이 컴파일시 고정 되는 것이 아니라 실행시 설정파일을 통해 이루어져 모듈간의 결합도(Coupling)을 낮출 수 있다.
결합도가 낮아지면, 코드의 재사용성이 높아져서 모듈을 여러 곳에서 수정 없이 사용할 수 있게 되고, 모의 객체를 구성하기 쉬워지기 때문에 단위 테스트에 용이해 진다.

이제, .NET에서 MVC 프로젝트를 만들때 DI를 구현하기 위해 가장 많이 사용하는 Open Source인 Ninject에 대해서 알아보자.

NInject 알아보기

(공식 페이지 : http://www.ninject.org/)

간단히 이름부터 살펴보면, NInject는, N(Ninja) + Inject로 대표 이미지로도 Ninja로 되어 있다.

홈페이지 대문에 보면 “Be fast, be agile, be precise”라는 슬로건이 있는데 닌자처럼 빠르고 민첩하며 정확하게 프로그램을 만들수 있게 하겠다는 정신이 담겨있는 것 같다.

(Nate Kohari라는 소프트웨어 엔지니어가 최초 개발을 했는데, 개인적인 생각으로는 N을 중의적인 의미로 사용한게 아닌가 싶다. 참고 : https://www.infoq.com/articles/ninject10-released/)

NInject는 Open Source 라이브러리로 Apache License 2.0에 따라 배포되었으며,
2007년 부터 .NET 어플리케이션의 DI를 구현하기 쉽게 해주도록 지원하고 있다.

이제, 실제로 사용해 보자

NInject 사용해 보기

  1. Package Install
    • Visual Studio에서 제공하는 Nuget Package Installer를 사용하여 다음 Package들을 설치한다.
      – Ninject
      – Ninject.Web.WebApi
      – Ninject.Web.Common
      – Ninject.Web.Common.WebHost
  2. Edit Ninject.Web.Common.cs
    • 위 Package 설치가 완료되면, 프로젝트 최상단에서 App_Start 폴더에 Ninject.Web.Common.cs 파일이 생성된 것을 확인 할 수 있다.
    • 해당 파일을 열어 보면, CreateKernel이라는 method가 있는데 다음 코드를 추가한다. 그러면 NInject가 controller의 의존성 주입을 구성해 줄 수 있게 된다.
RegisterServices(kernel);
GlobalConfiguration.Configuration.DependencyResolver = new NinjectDependencyResolver(kernel);
return kernel;
  1. Register Service
    • 스크롤을 조금 내려 보면, RegisterServices라는 method를 확인 할 수 있는데, 실제로 의존관계를 설정(bind)하는 곳이다. 의존성을 주입할 객체들의 관계를 다음과 같이 추가한다.
private static void RegisterServices(IKernel kernel)
{
    kernel.Bind<ICommonStore>().To<CommonStore>();
}
  1. Use it on controller
    • 이제 의존성 주입을 위한 IoC 설정과, 의존관계 설정(bind)작업을 모두 하였으니 Controller에서 사용해보자.
public class CommonStoreController : Controller
{    
    public CommonStoreController(ICommonStore common)
    {
        this.commonStore = common;
    }

    private ICommonStore commonStore;

    public int GetItemCount(string id)
    {
        return commonStore.Add(id);
    }
}

Design Pattern 중 DI(Dependency Injection, 의존성 주입)이라는 패턴에은 표준 프로그래밍을 할 때 중요한 요소이긴하지만 무조건 사용해야 하는 것은 아니다. 간단한 프로그램이나 객체간의 결합이 명확하여 구분하지 않아도 되는 경우 굳이 프로젝트를 무겁게(?) 만들 필요없다.

그리고 Ninject는 .NET에서 많이 사용되는 Open Source DI Framework 중 하나로 DI를 할 때 쉽게 구현할 수 있어서 선호되는 편이지만 다양한 DI Open Source Framework들이 있으며, 성능 면에서도 훨씬 더 좋은 것들이 있으니 확인하고 사용하길 바란다.
(참고 : https://www.claudiobernasconi.ch/2019/01/24/the-ultimate-list-of-net-dependency-injection-frameworks/)

[.NET]An existing connection was forcibly closed by the remote host 해결 방법

SSL-TLS(이하 TLS)보안 인증된 서버 Application의 API 호출 할때, An existing connection was forcibly closed by the remote host에러가 발생하는 경우가 있다.

이런 경우는 서버의 TLS 버전을 기본값인 1.0을 사용하지 않고 상위 버전인 1.1이나 1.2를 사용하고 있기 때문이다. (TLS 버전은 2018년 4월 기준으로 1.0부터 1.3까지 있으나 1.3은 아직 정식으로 발표되지 않았다.)
각 버전에 따라 네트워크 전송계층에서 사용하는 암호와 알고리즘이 달라지기 때문에 Server-Client 간에 사용 버전이 다르면 바로 An existing connection was forcibly closed by the remote host오류가 발생한다.

.NET에서는 별도의 설정이 없으면 TLS 버전을 기본값 1.0으로 셋팅하여 HTTP 요청을 하도록 되어 있기 때문에 상위 TLS 버전을 사용하는 서버를 그냥 호출하면 위와 같은 오류가 나온다. 이런경우, 상위 TLS 버전을 사용하여 통신 할수 있도록 System.Net.Security DLL을 이용하여 Security Protocol을 다음과 같이 설정해 주어야 한다.

(ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;)

using (HttpClient client = new HttpClient())
{
    string jsonString = "data list";

    client.BaseAddress = new Uri("https://<Your App Name>.azurewebsites.net");
    
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
    var response = client.PostAsync("/api/getYourData", new StringContent(jsonString, Encoding.UTF8, "application/json")).Result;

    if (response.IsSuccessStatusCode)
    {
        result = response.Content.ReadAsStringAsync().Result;
    }
}

호출 대상 서버의 TLS 제공 버전을 정확하게 알고 있으면 좋겠지만, 그렇지 않은 경우, 위 예시 코드처럼 지원하는 TLS버전 리스트를 Or(‘|’)로 묶어서 설정 하면 Client에서 요청 할때 알아서 알맞는 TLS Protocol 버전으로 통신(Handshake)을 하도록 되어 있어서 크게 신경 쓰지 않아도 된다.

단지, 너무 오래된(취약점이 있는)버전의 경우 보안 이슈가 될 수 있으니 적당한 지원 버전 영역을 관리 하는 것을 권장한다. 

Azure Cloud Service Too many redirects

Azure의 Cloud Service를 개발 할 때, Local에서 테스트 할 때는 정상 작동했는데, 프로젝트 패키지를 프로덕션(혹은 스테이징)에 배포하고 확인 하니 Too many redirects 라는 오류 페이지가 뜨고 아무것도 확인 할 수 없을 때가 있다.

이런 오류는 보통 Cloud Service에 설정된 Framework과 로컬에서 프로젝트에 구성한 Framework버전이 달라서 생기는 문제다.

우선 WebRole을 비롯한 솔루션에 추가된(종속된) 프로젝트들의 속성에 들어가 그림과 같이 Target Framework(대상 프레임워크) 버전을 확인 한다. (되도록이면, 사용하는 프로젝트들의 Framework 버전중 최상위 버전 1가지로 통일 하는 것을 추천하며 예제에서는 4.6을 사용한다고 가정한다.) 

각 프로젝트의 버전을 확인 한 후 Cloud Service에서 해당 Framework 버전을 지원하는 Azure Guest OS Release 버전을 확인 한다.  
(참조 링크: https://docs.microsoft.com/ko-kr/azure/cloud-services/cloud-services-guestos-update-matrix)

링크에 들어가서 보면 알 수 있겠지만 글 작성일 기준으로 각 Guest OS에 설치된 Framework Version을 정리해 보면 다음 표와 같다.

Guest OS Release Installed .NET Framework Version
Release 5, Windows Server 2016 4.0, 4.5, 4.5.1, 4.5.2, 4.6, 4.6.1, 4.6.2
Release 4, Windows Server 2012 R2 4.0, 4.5, 4.5.1, 4.5.2
Release 3, Windows Server 2012 4.0, 4.5, 4.5.1, 4.5.2
Release 2, Windows Server 2008 R2 SP1 3.5, 4.0, 4.5, 4.5.1, 4.5.2

이제 위에서 확인한 프로젝트들의 target Framework 버전을 지원하는 Guest OS가 설정되어 있는지 Cloud Service 환경설정을 확인 해보아야 한다. Cloud Service의 환경설정을 확인 하기 위해서 아래 그림의 위치의 *.cscfg 파일을 열어 보자.

확인 해 보면 Line:2의 ‘ServiceConfiguration‘태그의 ‘osFamily‘속성을 확인 해 볼 수 있는데, 이곳이 OS Release 버전을 설정하는 곳이다. 프로젝트들의 Target Framework(대상 프레임웍)버전 설정이 Cloud Service에 설정된 osFamily에서 지원하는 것인지 확인하여 지원하는 것으로 고쳐 준다. (예제는 Release 4로 설정되어 있는 osFamily 속성을 4.6버전을 지원하는 Release 5로 변경하였다.)

AS-IS

<ServiceConfiguration serviceName="CloudServiceSample1" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration" osFamily="4" osVersion="*" schemaVersion="2015-04.2.6">

TO-BE

<ServiceConfiguration serviceName="CloudServiceSample1" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration" osFamily="5" osVersion="*" schemaVersion="2015-04.2.6">

이제 다시 빌드 하고 배포하면 정상적인 프로젝트 동작을 확인 해 볼 수 있다.

[.NET]HttpResponseMessage 클래스 메서드 오류 CS1061 해결 방법

프로젝트에서 API를 호출하고 나서 결과를 읽어 Model(Object)에 담을때 다음과 같이 코드를 한다.

그런데 간혹 잘 코딩 했다고 생각을 했어도 다음과 같은 오류 메시지가 나올 때가 있다. 

오류 CS1061 ‘HttpContent’에는 ‘ReadAsAsync’에 대한 정의가 포함되어 있지 않고, ‘HttpContent’ 형식의 첫 번째 인수를 허용하는 확장 메서드 ‘ReadAsAsync’이(가) 없습니다. 
using 지시문 또는 어셈블리 참조가 있는지 확인하세요. 

이런경우 참조 패키지가 잘 준비되지 않은 경우이다. 
먼저 NuGet Package에 들어가서 System.Net.Http 버전을 최신버전으로 업데이트 하고,bMicrosoft.AspNet.WebApi.Client 패키지가 설치 되어 있는지 확인 해본다. 아마 안되어 있거나 구 번전일 것이다.

install-package Microsoft.AspNet.WebApi.Client

설치 및 업데이트가 완료 되었다면 문제 없이 작동 할 것이다.