Patterns in Practice

구성보다 규칙

Jeremy Miller

목차

언어 혁신의 영향
한 번만 말하라
합리적인 기본값
구성보다 규칙
다음 단계

프로젝트에서 핵심 문제에 투자하는 시간과 순수한 기술적 문제와 씨름하는 시간 중 어떤 시간이 더 많은지 생각해 본 적이 있습니까? 어떤 사람들은 모든 새로운 소프트웨어 기술의 목표는 소프트웨어에 대한 개발자의 의도와 이러한 의도를 코드에서 현실화하는 데 따르는 차이를 줄이는 것이라고 말합니다. 업계에서는 지속적으로 추상화 수준을 높임으로써 개발자가 실제 개발에 사용할 수 있는 시간을 늘리고 저수준 인프라를 작성하는 시간을 줄이고 있지만 아직도 가야할 길은 멀다고 할 수 있습니다.

한 가지 예를 들어 보겠습니다. 정말로 코드를 보고 싶어하는 비즈니스 관계자에게 여러분의 코드를 보여 준다고 가정한다면 과연 이들이 코드에 얼마나 신경을 쓸까요? 비즈니스 관계자들은 시스템의 비즈니스 기능에 관련된 부분에만 신경을 쓸 것입니다. 이러한 코드가 시스템의 "핵심"입니다. 반면에 이들은 코드에서 형식 선언이나 구성 설정, try/catch 블록, 제네릭 제약 조건 등에 대해서는 전혀 관심을 갖지 않을 것입니다. 이러한 코드는 개발자가 코드를 제공하기 위해 거쳐야 하는 인프라이며 말하자면 "의례"라고 할 수 있습니다.

이전 호 칼럼에서는 상당히 오랫동안 디자인 규범으로 자리 잡은 기본적인 디자인 개념과 원칙을 살펴보았습니다. 이번 칼럼에서는 여러분의 프로젝트에 적용하여 코드에서 의례를 줄이는 데 사용할 수 있는 몇 가지 새로운 기술을 살펴보겠습니다. 이러한 개념 중 몇 가지는 처음 접하는 것일 수 있지만 몇 년 내에 .NET 메인스트림에 포함될 것입니다.

언어 혁신의 영향

고려하려는 첫 번째 요인은 프로그래밍 언어의 선택과 프로그래밍 언어를 사용하는 방법에 대한 것입니다. 프로그래밍 언어가 코드 의례에 미치는 영향을 확인하기 위해 잠시 오래된 역사 이야기를 살펴보겠습니다.

몇 년 전 필자는 Visual Basic 6.0을 사용하여 대규모 시스템을 개발한 적이 있습니다. 이 시스템의 모든 메서드는 그림 1과 비슷하게 작성해야 했습니다. 이 코드의 모든 부분은 의례라고 할 수 있습니다.

그림 1 의례적인 코드

Sub FileOperations()
    On Error Goto ErrorHandler

    Dim A as AType
    Dim B as AnotherType

    ' Put some code here

    Set A = Nothing
    Set B = Nothing
    ErrorHandler:
        Err.Raise Err.Number, _
            "FileOperations" & vbNewLine & Err.Source, _
            Err.Description, _
            Err.HelpFile, _
            Err.HelpContext
Exit Sub

디버깅을 수월하게 하기 위해 스택 추적과 비슷한 기능의 구현을 목적으로 각 메서드에 상당한 양의 상용구를 사용했습니다. 또한 이러한 개체를 해제하기 위해 메서드 내에서 변수를 역참조(Set A = Nothing)해야 했습니다. 이 때문에 단지 각 메서드에 오류 처리와 개체 정리가 올바르게 코딩되었는지 확인하는 엄격한 코드 검토 표준을 적용해야 했으며 시스템의 핵심인 실제 코드는 이러한 모든 의례 속에 섞여서 여기저기에 분산되어 있었습니다.

현재로 돌아와서 C#이나 Visual Basic .NET과 같은 최신 프로그래밍 언어의 경우를 살펴보겠습니다. 현재는 Visual Basic 6.0 프로그래밍에서 거쳐야 했던 명시적인 메모리 정리 작업의 대부분을 가비지 수집이 대신하고 있습니다. 예외에서의 스택 추적은 Microsoft .NET Framework 자체에 내장되었기 때문에 이제는 더 이상 직접 이 작업을 할 필요가 없습니다. .NET Framework로 전환하면서 이러한 기계적인 상용구 코드가 모두 제거되었다고 생각한다면 의례 코드가 줄어들었기 때문에 .NET 호환 언어는 Visual Basic 6.0보다 생산적이고 읽기 쉽다고 말할 수 있을 것입니다.

.NET Framework는 큰 발전이지만 언어의 혁신은 아직 완성되지 않았습니다. 다음은 기존 방식의 간단한 C# 구현 속성의 예입니다.

public class ClassWithProperty {
  // Higher Ceremony
  private string _name;
  public string Name {
    get { return _name; }
    set { _name = value; }
  }
}

이 코드의 핵심은 단순히 ClassWithProperty 클래스에 Name이라는 문자열 속성이 있다는 것입니다. 다음은 C# 3.0에서 자동 속성을 사용하여 같은 작업을 해 보겠습니다.

public class ClassWithProperty {
  // Lower Ceremony
  public string Name { get; set; }
}

이 코드는 기존 스타일의 속성과 정확하게 동일한 의도를 가지고 있지만 "컴파일러 잡음" 코드가 크게 줄어들었습니다.

그러나 일반적으로 소프트웨어 개발자는 프로그래밍 언어에 대한 완전한 제어권을 가지지 않는 경우가 많습니다. 물론 필자는 새로운 프로그램 언어 혁신은 물론 대안 언어까지 활용해야 한다고 생각하지만 현재로서는 메인스트림 C#과 Visual Basic으로 사용이 가능한 디자인 아이디어에 대해 알아보아야 합니다.

도메인 중심 유효성 검사

.NET Framework에서는 ASP.NET 유효성 검사기 컨트롤과 같은 도구를 통해 사용자 인터페이스에 선언적 필드 수준 유효성 검사를 추가하기가 매우 쉬워졌습니다. 그러나 이와 동시에 필자는 다음과 같은 이유 때문에 실제 도메인 모델 클래스나 적어도 도메인 서비스와 가까운 위치에 유효성 검사 논리를 배치하는 것이 좋다고 생각합니다.

  1. 유효성 검사 논리는 비즈니스 논리와 연관되며 필자는 모든 비즈니스 논리를 비즈니스 논리 클래스 내에 포함시키는 것을 선호합니다.
  2. 유효성 검사 논리를 사용자 인터페이스와 연결이 끊어진 도메인 서비스나 도메인 모델에 배치하면 여러 화면에서 잠재적으로 중복을 줄일 수 있으며 동일한 유효성 검사 논리를 응용 프로그램이 공개하는 사용자 인터페이스가 아닌 서비스(예: 웹 서비스)에서 한 번만 수행할 수 있습니다.
  3. 모델의 유효성 검사 논리에 대한 단위 및 수용 테스트를 작성하는 것이 사용자 인터페이스로 구현되는 동일한 논리를 테스트하는 것보다 훨씬 쉽습니다.

한 번만 말하라

프로젝트가 진행되는 중간에 단일 데이터 필드 선언에서 변경이 필요한 내용을 발견했다면 어떻게 해야 할까요? 데이터베이스에서 수행한 작은 변경이 하나의 논리적인 변경을 위해 중간 계층의 여러 부분, 데이터 액세스 코드, 그리고 심지어 사용자 인터페이스 계층에 비슷한 변경을 수행하는 동안 물결치듯이 영향을 미치는 경우가 매우 많습니다.

필자가 이전에 근무하던 곳에서는 작은 데이터 필드의 변경에서 시작되는 이러한 물결 효과를 웜홀 반-패턴이라고 불렀습니다. 한 곳의 작은 변경에 대한 필요성이 계층을 통해 여기저기로 전파되는 상황을 피하고 싶을 것입니다. 불필요한 계층을 줄임으로써 웜홀 효과를 늦출 수 있으며 이것이 첫 번째 단계입니다. 어렵기는 하지만 더 효과가 좋은 방법은 이러한 변경을 한 번만 말하면 되는 길을 찾는 것입니다.

한 번만 말하라 원칙은 시스템 전체에서 사실이나 원칙의 믿을 만한 원본이 한 개만 있어야 한다고 정의합니다. CRUD(만들기, 읽기, 업데이트 및 삭제) 기능이 있는 웹 응용 프로그램을 작성하는 예를 살펴보겠습니다. 이러한 시스템에서는 데이터 필드 편집과 저장이 큰 부분을 차지합니다. 이러한 필드는 화면에서 편집하고, 서버에서 유효성을 검사하며, 데이터베이스에 저장되어야 하고, 사용자 환경을 개선하기 위해 클라이언트에서도 유효성을 검사하면 좋을 것입니다. 그리고 "이 필드는 입력해야함 및/또는 이 필드는 50자 이하여야 함"과 같은 조건을 코드에서 한 번만 지정하기를 원할 것입니다.

두 가지 다른 방법을 선택할 수 있습니다. 첫째, 데이터베이스 스키마로 마스터 정의를 만들고 이 스키마를 통해 중간 계층 및 사용자 프레젠테이션 코드를 생성할 수 있습니다. 둘째, XML 파일과 같은 일종의 외부 메타데이터 저장소에 데이터 필드를 정의하고 코드 생성을 사용하여 데이터베이스 스키마, 모든 중간 계층 개체 및 사용자 인터페이스 화면을 생성할 수 있습니다. 개인적으로 필자는 대규모 코드 생성을 그다지 좋아하지 않기 때문에 필자의 팀에서는 다른 방향을 선택합니다.

우리는 일반적으로 도메인 모델 클래스를 먼저 디자인하고 유효성 검사 논리는 도메인 모델 엔터티 클래스의 역할이라고 생각합니다. 필수 필드나 최대 문자열 길이 규칙과 같이 간단한 유효성 검사 규칙의 경우에는 그림 2에 나오는 Address 클래스처럼 속성을 유효성 검사 특성으로 표시합니다.

그림 2 유효성 검사 특성 사용

public class Address : DomainEntity {
  [Required, MaximumStringLength(250)]
  public string Address1 { get; set; }

  [Required, MaximumStringLength(250)]
  public string City { get; set; }

  [Required]
  public string StateOrProvince { get; set; }

  [Required, MaximumStringLength(100)]
  public string Country { get; set; }

  [Required, MaximumStringLength(50)]
  public string PostalCode { get; set; }

  public string TimeZone { get; set; }
}

특성 사용은 유효성 검사 규칙 지정을 위한 간단하고 비교적 일반적인 기술입니다. 유효성 검사 규칙이 명령형 코드로 구현되는 것이 아니라 선언적으로 표현되었으므로 핵심과 의례 테스트를 통과했다고 말할 수 있습니다.

그러나 이제는 필수 필드와 최대 문자열 길이 규칙을 데이터베이스로 복제해야 합니다. 필자의 팀에서는 지속성 메커니즘으로 NHibernate를 사용합니다. NHibernate 매핑으로부터 DDL(데이터 정의 언어) 코드를 생성하는 기능은 NHibernate의 강력한 기능 중 하나입니다. 이렇게 생성된 DDL은 데이터베이스 스키마를 만들고 도메인 모델과 동기화를 유지하는 데 사용할 수 있습니다. 이 전략은 새로운 프로젝트에서 잘 작동합니다. 도메인 모델로부터 데이터베이스를 생성하는 이 전략이 유용하려면 null이 아닌 필드를 표시하고 문자열 길이를 지정하기 위해 NHibernate 매핑에 추가 정보를 추가할 수 있어야 합니다.

우리는 개체 매핑을 정의하기 위해 새로운 Fluent NHibernate 메커니즘을 사용했습니다. 우리의 Fluent NHibernate 설정 코드에서는 그림 3에 나오는 코드를 사용하여 모델 클래스 내에 [Required]와 [MaximumStringLength] 특성이 있는 경우 이를 처리하는 방법을 Fluent NHibernate에 지시함으로써 매핑에서 자동 규칙을 설정했습니다.

그림 3 NHibernate에서 특성 처리

public class MyPersistenceModel : PersistenceModel {
  public MyPersistenceModel() {
    // If a property is marked with the [Required]
    // attribute, make the corresponding column in
    // the database "NOT NULL"
    Conventions.ForAttribute<RequiredAttribute>((att, prop) => {
      if (prop.ParentIsRequired) {
        prop.SetAttribute("not-null", "true");
      }
    });

    // Uses the value from the [MaximumStringLength]
    // attribute on a property to set the length of 
    // a string column in the database
    Conventions.ForAttribute<MaximumStringLengthAttribute>((att, prop) => {
      prop.SetAttribute("length", att.Length.ToString());
    });
  }
}

이러한 규칙은 이제 프로젝트의 모든 매핑에 적용됩니다. Address 클래스의 경우에는 간단하게 지속될 속성을 Fluent NHibernate에 알려 줍니다.

public class AddressMap : DomainMap<Address> {
  public AddressMap() {
    Map(a => a.Address1);
    Map(a => a.City);
    Map(a => a.TimeZone);
    Map(a => a.StateOrProvince);
    Map(a => a.Country);
    Map(a => a.PostalCode);
  }
}

이제 유효성 검사 특성에 대한 정보를 Fluent NHibernate에 제공했으므로 Address 테이블에 대한 DDL을 생성할 수 있습니다(그림 4 참조). 그림 4의 SQL에서 문자열 길이는 Address 클래스에 있는 [MaximumStringLength] 특성의 정의와 일치합니다. 비슷하게 NULL / NOT NULL 값은 Address 클래스의 [Required] 특성에 따라 결정됩니다.

그림 4 DDL 코드 생성

CREATE TABLE [dbo].[Address](
  [id] [bigint] IDENTITY(1,1) NOT NULL,
  [StateOrProvince] [nvarchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
  [Country] [nvarchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
  [PostalCode] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
  [TimeZone] [nvarchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
  [Address1] [nvarchar](250) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
  [Address2] [nvarchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
  [City] [nvarchar](250) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
PRIMARY KEY CLUSTERED 
(
  [id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

도메인 모델 개체에 있는 유효성 검사 특성으로부터 데이터베이스 구조를 파생하는 몇 가지 인프라에 시간을 투자했지만 아직도 클라이언트 쪽 유효성 검사와 클라이언트 쪽 표시라는 과제가 남아 있습니다. 우리는 사용자 환경을 개선하기 위해 브라우저 내에서 간단한 입력 유효성 검사를 수행하고 일정한 방법으로 필수 필드 요소를 표시하기를 원합니다.

우리 팀에서 최종적으로 선택한 솔루션은 유효성 검사 특성을 인식하는 입력 요소의 HTML 렌더링을 사용하는 것이었습니다. 환경으로는 ASP.NET MVC(Model View Controller, Framework 베타 1을 사용했으며, 뷰 엔진으로는 Web Forms 엔진을 사용했습니다. 그림 5는 이 시스템에서 주소 뷰의 태그를 보여 줍니다.

그림 5 주소 뷰 태그

<div class="formlayout_body">
  <p><%= this.TextBoxFor(m => m.Address1).Width(300)%></p>
  <p><%= this.TextBoxFor(m => m.Address2).Width(300) %></p>
  <p>
    <%= this.TextBoxFor(m => m.City).Width(140) %>
    <%= this.DropDownListFor(m => 
        m.StateOrProvince).FillWith(m => m.StateOrProvinceList) %>
  </p>
  <p><%= this.TextBoxFor(m => m.PostalCode).Width(80) %></p>        
  <p><%= this.DropDownListFor(m => 
         m.Country).FillWith(m => m.CountryList) %></p>
  <p><%= this.DropDownListFor(m => 
         m.TimeZone).FillWith(m => m.DovetailTimeZoneList) %></p>
</div>

TextBoxFor와 DropDownListFor는 우리의 MVC 아키텍처에서 모든 뷰에 사용되는 공용 기본 뷰의 작은 HTML 도우미입니다. TextBoxFor의 용법은 다음과 같습니다.

public static TextBoxExpression<TModel> TextBoxFor< TModel >(
  this IViewWithModel< TModel > viewPage, 
  Expression<Func< TModel, object>> expression)

  where TModel : class {
    return new TextBoxExpression< TModel >(
    viewPage.Model, expression);
  }

이 코드에서 중요한 것은 입력 인수가 Expression(정확하게 말해 Expression<Func<TModel, object>>)이라는 것입니다. TextBoxExpression 클래스는 텍스트 상자의 실제 HTML을 구성하는 동안 다음 작업을 수행합니다.

  1. Expression을 구문 분석하고 바인딩된 속성에 대한 정확한 PropertyInfo 개체를 찾습니다.
  2. PropertyInfo를 조사하여 유효성 검사 특성이 있는지 확인합니다.
  3. 텍스트 상자의 HTML을 적절하게 렌더링합니다.

[Required] 특성으로 표시된 속성에 바인딩된 모든 HTML 요소에 required라는 클래스를 추가했습니다. 이와 비슷하게 바인딩된 속성에 [MaximumStringAttribute]가 있는 경우 특성과 일치시키고 허용되는 길이로 사용자 입력을 제한하기 위해 HTML 텍스트 상자의 maxlength 특성을 설정합니다. 결과 HTML은 다음과 같습니다.

<p><label>Address:</label>
<input type="text" name="Address1" value="" 
       maxlength="250" style="width: 300px;" 
       class="required textinput" /></p>

필요한 필드의 모양은 CSS 클래스 required의 모양을 편집하여 간단하게 제어할 수 있습니다. 여기에서는 필수 필드의 색을 밝은 파랑으로 설정했습니다. 실제 클라이언트 쪽 유효성 검사에는 jQuery Validation 플러그 인을 사용했으며 이 경우에는 간단하게 입력 요소에 required 클래스가 있는지 여부를 확인합니다. 텍스트 요소의 최대 길이는 간단하게 input 요소의 maxlength 특성을 설정하여 적용합니다.

물론 이것은 완전한 구현과는 거리가 멀지만 점차적으로 실제 구현을 만들기는 그리 어렵지 않습니다. 까다로운 부분은 여러 계층에서 반복적인 메타데이터와 코딩을 제거하는 방법을 찾는 것입니다. 개체 모델로부터 데이터베이스를 생성한 필자 팀의 방법에 그다지 관심이 없는 팀이 많이 있겠지만 한 번만 말하라 원칙을 사용하여 개발 노력을 간소화하는 방법을 설명하는 것이 필자의 목표이므로 관계없습니다.

합리적인 기본값

이전 섹션에서는 Fluent NHibernate를 사용하여 ORM(개체 관계형 매핑)을 나타내는 아주 작은 예(AddressMap 클래스를 통해)를 살펴보았습니다. 다음은 Site 클래스로부터 참조를 Address 개체에 나타내는 조금 더 복잡한 예입니다.

public SiteMap() {
  // Map the simple properties
  // The Site object has an Address property called PrimaryAddress
  // The code below sets up the mapping for the reference between
  // Site and the Address class
  References(s => s.PrimaryAddress).Cascade.All();
  References(s => s.BillToAddress).Cascade.All();
  References(s => s.ShipToAddress).Cascade.All();
}

ORM 도구를 구성할 때 일반적으로 다음 작업을 해야 합니다.

  1. Entity 클래스가 매핑되는 테이블 이름을 지정합니다.
  2. Entity의 기본 키 필드를 지정하고 일반적으로 기본 키 값을 할당하기 위한 일종의 전략도 지정합니다. 이를 위해서 자동 숫자/데이터베이스 시퀀스인지, 시스템이 기본 키 값을 할당하는지, GUID를 기본 키로 사용하는지 등을 고려해야 합니다.
  3. 개체 속성이나 필드를 테이블 열로 매핑합니다.
  4. Site에서 Address로의 매핑처럼 한 개체에서 다른 개체로 "일대일" 관계를 연결할 때는 ORM 도구가 부모와 자식 레코드를 결합하는 데 사용할 외래 키 열을 지정해야 합니다.

이러한 모든 작업은 일반적으로 지루하며, ORM이 개체를 지속하기 위해 따라야 하는 의례입니다. 다행스럽게도 약간의 합리적인 기본값을 코드에 추가하여 이러한 지루함을 조금이나마 덜 수 있습니다.

매핑 예에서 테이블 이름, 기본 키 전략 또는 외래 키 필드 이름을 지정하지 않았다는 것을 알 수 있을 것입니다. 필자 팀의 Fluent NHibernate 매핑 슈퍼클래스에 매핑을 위한 약간의 기본값을 설정했습니다.

public abstract class DomainMap<T> : ClassMap<T>, 
  IDomainMap where T : DomainEntity {

  protected DomainMap() {
    // For every DomainEntity class, use the Id property
    // as the Primary Key / Object Identifier
    // and use an Identity column in SQL Server,
    // or an Oracle Sequence
    UseIdentityForKey(x => x.Id, "id");
    WithTable(typeof(T).Name);
  }
}

자기 주장이 있는 소프트웨어

필자가 규칙의 적용을 "제한적"이라고 설명했다는 것을 알 수 있을 것입니다. 구성보다 규칙 우선의 개념 중에는 디자인에 인공적인 제약 조건을 적용하는 "자기 주장이 있는 소프트웨어"를 만드는 것이 있습니다.

자기 주장이 있는 프레임워크에서 개발자는 유연성이 거의 없는 수준으로 작업을 처리하게 됩니다. 자기 주장이 있는 소프트웨어의 제안자는 이러한 제약 조건을 통해 개발자가 내려야 할 결정을 줄이고 일관성을 향상시킴으로써 더 효율적으로 개발할 수 있다고 말하고 있습니다.

필자의 팀에서 사용하고 있는 한 가지 주장은 모든 도메인 모델 클래스가 Id라는 단일 long 속성으로 식별된다는 것입니다.

public virtual long Id { get; set; }

이것은 간단한 규칙이지만 디자인에 몇 가지 심오한 영향을 미칩니다. 모든 엔터티 클래스를 같은 방법으로 식별할 수 있으므로 각 최상위 엔터티를 위한 특수한 리포지토리 클래스를 작성할 필요 없이 단일 리포지토리 클래스를 사용할 수 있습니다. 같은 맥락에서 웹 응용 프로그램의 URL 처리는 각 엔터티를 위한 특별한 라우팅 규칙을 등록할 필요 없이 여러 엔터티 클래스에서 일관적입니다.

이러한 주장을 따르면 새로운 엔터티 형식을 추가하는 데 필요한 인프라 비용을 줄일 수 있습니다. 이 방법의 단점은 개체 식별자에 자연 키나 복합 키를 추가할 수 없는 것은 물론 GUID도 사용할 수 없다는 것입니다. 필자의 팀에서는 이것이 문제가 되지 않았지만 다른 팀에서는 이 주장을 받아들이는 데 걸림돌이 될 수 있을 것입니다.

이러한 주장을 어떻게 적용할 수 있을까요? 첫 번째 단계는 팀 내에서 이러한 주장에 대한 공통적인 이해와 합의를 이끌어내는 것입니다. 충분한 정보를 제공한다면 개발자가 이러한 주장이 적용된 디자인 선택을 적절하게 활용할 수 있는 가능성이 높습니다. 그러나 이와 동시에 개발자가 기존의 규칙에 익숙하지 않거나 규칙이 혼란스러운 경우에는 구성보다 규칙이 우선이라는 원칙 때문에 심각한 문제가 발생할 수 있습니다.

프로젝트 규칙을 자동으로 적용하는 지속적인 통합 빌드의 일부로서 정적 코드 분석 도구를 사용하는 것도 고려해 볼 수 있습니다.

이 코드에서는 DomainEntity 하위 클래스가 있는 모든 클래스를 ID 전략으로 할당된 Id 속성에 따라 식별한다는 정책을 설정하고 있습니다. 테이블 이름은 클래스 이름과 같다고 가정합니다. 예를 들어 SQL Server의 예약어와의 충돌을 피하기 위해 User라는 클래스를 Users라는 테이블로 매핑하는 것처럼 클래스별로 이러한 선택을 재정의하는 것이 가능하지만 그럴 필요는 거의 없습니다. 마찬가지로 Fluent NHibernate는 다른 클래스를 참조하는 속성 이름을 바탕으로 외래 키 이름을 가정합니다.

물론 이 방법을 사용하더라도 각 매핑 클래스에서 그리 많은 코드를 줄일 수 있는 것은 아니지만 매핑에서 전반적인 잡음 코드를 줄여서 매핑의 일부를 훨씬 읽기 쉽게 만들 수 있습니다.

구성보다 규칙

소프트웨어 개발자들은 동작을 명령형 코드에서 선언적 XML 구성으로 이동함으로써 생산성을 높이고 시스템을 더욱 동적으로 만들기 위한 노력을 계속하고 있습니다. XML 구성이 지나치게 많이 확산되었으며 점차 해로운 관행이 되고 있다고 생각하는 개발자들이 많습니다. 명시적 구성보다 기본값 우선 전략은 구성보다 규칙 우선 원칙이라고도 알려져 있습니다.

구성보다 규칙은 명시적인 코드를 사용하지 않고 코드 구조에 암시된 기본값을 적용하기 위한 디자인 철학이자 기술입니다. 개발자가 응용 프로그램과 아키텍처에서 고유한 부분에만 집중할 수 있도록 하여 개발을 간소화한다는 것이 개념입니다.

현재 많은 사람들이 ASP.NET MVC Framework를 테스트하고 이를 다양한 방법으로 사용하기 위한 실험을 하고 있습니다. 웹 개발의 MVC 모델에는 구성보다 규칙 우선 원칙을 적용하기에 매우 적당한 반복적인 코드 위치가 두 군데 있습니다.

다음은 MVC 모델에서 단일 요청의 기본적인 흐름에 포함되는 5단계입니다.

  1. 클라이언트에서 URL을 수신합니다. 라우팅 하위 시스템이 URL을 구문 분석하고 이 URL을 처리하는 컨트롤러의 이름을 알아냅니다.
  2. 라우팅 하위 시스템이 알아낸 컨트롤러 이름에서 올바른 컨트롤러 개체를 만들거나 찾습니다.
  3. 올바른 컨트롤러 메서드를 호출합니다.
  4. 올바른 뷰를 선택하고 컨트롤러 메서드에서 생성된 모델 데이터를 이 뷰로 마샬링합니다.
  5. 뷰를 렌더링합니다.

기본적으로 ASP.NET MVC Framework로 웹 페이지를 작성하는 데는 약간의 반복적인 의례가 있지만 몇 가지 제한적인 규칙을 적용하여 이러한 의례를 완화할 수 있습니다.

첫 번째 작업은 들어오는 URL을 올바른 컨트롤러 클래스가 있는 웹 사이트로 연결하는 것입니다. MVC Framework의 라우팅 라이브러리를 사용하면 URL을 조사하고 컨트롤러의 이름을 알아낼 수 있습니다. 그러면 MVC Framework는 들어오는 URL에서 알아낸 컨트롤러 이름과 일치하는 컨트롤러와 일치하는 컨트롤러 개체를 등록된 IControllerFactory 개체에 요청합니다.

컨트롤러 생성을 IOC(제어 반전) 도구에 위임하는 팀이 많습니다. 필자의 팀에서는 이름으로 컨트롤러 인스턴스를 확인하는 데 공개 소스 StructureMap 도구를 사용했습니다.

public class StructureMapControllerFactory 
  : IControllerFactory {

  public IController CreateController(
    RequestContext requestContext, string controllerName) {

    // Requests the named Controller from the 
    // StructureMap container
    return ObjectFactory.GetNamedInstance<IController>(
      controllerName.ToLowerInvariant());
  }
}

컨트롤러를 요청하는 것은 상당히 간단하지만 먼저 모든 컨트롤러 클래스를 이름으로 IOC 컨테이너에 등록해야 합니다. 그렇지만 이렇게 하면 아키텍처에 약간의 의례가 추가되는 것이 아닐까요? 일이 년 전만 해도 다음과 같은 컨트롤러 클래스의 명시적 IOC 구성을 추가해야 했습니다.

public static class ExplicitRegistration {
  public static void BootstrapContainer() {
    ObjectFactory.Initialize(x => {
      x.ForRequestedType<IController>().AddInstances(y => {
        y.OfConcreteType<AddressController>().WithName("address");
        y.OfConcreteType<ContactController>().WithName("contact");

        // and so on for every possible type of Controller
      });
    });
  }
}

이 코드는 IOC 도구에 정보를 제공하는 것 외에 다른 이유는 없는 순수하게 지루한 의례입니다. 등록 코드를 자세히 보면 일정한 패턴을 따른다는 것을 알 수 있습니다. AddressController는 주소로 등록되고 Contact­Controller는 연락처로 등록됩니다. 각 컨트롤러를 명시적으로 구성하는 대신 간단하게 각 컨트롤러 클래스의 라우팅 이름을 자동으로 결정하는 규칙을 만들 수 있습니다.

다행스럽게도 StructureMap에는 규칙 기반 등록을 위한 직접적인 지원이 있으므로 IController의 모든 구체적 형식을 자동으로 등록하는 새로운 ControllerConvention을 만들 수 있습니다.

public class ControllerConvention : TypeRules, ITypeScanner {
  public void Process(Type type, PluginGraph graph) {
    if (CanBeCast(typeof (IController), type)) {
      string name = type.Name.Replace("Controller", "").ToLower();
      graph.AddType(typeof(IController), type, name);
    }
  }
}

이제 그림 6에 나와 있는 것처럼 새로운 규칙으로 StructureMap 컨테이너를 부트스트랩하는 약간의 코드가 필요합니다. 새로운 ControllerConvention이 준비되고 IOC 컨테이너의 일부에 대한 부트스트래핑이 수행된 후에는 응용 프로그램에 새 컨트롤러 클래스를 추가하면 개발자 수행한 명시적인 구성 없이도 자동으로 IOC 등록에 추가됩니다. 따라서 개발자가 새 화면을 위한 새 구성 요소를 추가하는 것을 잊는 경우에 발생했던 오류나 버그가 더 이상 문제가 되지 않습니다.

그림 6 StructureMap을 위한 새 규칙

/// <summary>
/// This code would be in the same assembly as 
/// the controller classes and would be executed
/// in the Application_Start() method of your
/// Web application
/// </summary>
public static class SampleBootstrapper {
  public static void BootstrapContainer() {
    ObjectFactory.Initialize(x => {
      // Directs StructureMap to perform auto registration
      // on all the Types in this assembly
      // with the ControllerConvention
      x.Scan(scanner => {
        scanner.TheCallingAssembly();
        scanner.With<ControllerConvention>();
        scanner.WithDefaultConventions();
      });
    });
  }
}

이러한 자동 등록 전략은 IOC 컨테이너가 프로그래밍 방식 등록 API를 제공하는 한 .NET Framework에 대한 필자가 알고 있는 모든 IOC 컨테이너에서 작동이 가능합니다.

다음 단계

최종적으로 지금까지 설명한 내용은 모두 개발자의 의도와 이러한 의도를 코드에 현실화하는 데 따르는 거리와 마찰을 줄이기 위한 것입니다. 이 칼럼에서 소개한 기술은 대부분 명시적 코드 대신 명명 규칙을 사용하여 코드가 필요한 정보를 "직접 알아내도록"하거나 시스템에서 정보가 중복되는 것을 피하는 방법에 대한 것입니다. 필자는 또한 특성에 숨겨진 정보를 재활용하여 기계적인 작업을 줄이는 몇 가지 사려 깊은 기술을 소개했습니다.

이러한 모든 디자인 아이디어로 개발 과정에서 반복적인 의례를 줄일 수 있지만 그에 따르는 비용이 있습니다. 구성보다 규칙 우선 원칙을 반대하는 사람들은 이 방식의 근본적인 "매력"에 대해 불평합니다. 코드나 프레임워크에 주장을 삽입하면 이러한 주장이 바람직하지 않은 새로운 시나리오에서는 재사용하기가 어렵습니다.

아직 소개하지 못한 내용이 많이 있으며 기회가 있으면 향후 칼럼에서 다루도록 하겠습니다. 언어 중심 프로그래밍, F#, IronRuby 및 IronPython과 같은 대안 언어, 그리고 내부 도메인별 언어 사용이 소프트웨어 디자인 프로세스에 미치는 영향과 같은 내용을 소개할 기회가 있기를 기대합니다.

질문이나 의견이 있으면 mmpatt@microsoft.com으로 보내시기 바랍니다.

Jeremy Miller는 C# 부문 Microsoft MVP이며, .NET 상에서 종속성 주입을 위한 공개 소스 StructureMap(structuremap.sourceforge.net) 도구와 .NET 상에서 과급된 FIT 테스트를 위한 StoryTeller (storyteller.tigris.org) 도구를 개발하고 있습니다. CodeBetter 사이트의 일부인 그의 블로그(The Shade Tree Developer)에 방문해 보십시오.