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));