2012년 2월 7일 화요일

NHibernate DTO를 AutoMapper로 구현하기

NHibernate 엔티티를 외부에 서비스하기 위해서는 전통적 방식 중에 하나가 DTO (Data Transfer Object) 패턴을 사용하는 것입니다. 물론 다른 ORM도 마찮가지구요.
다만 문제는 NHibernate 의 Association 이 유지되느냐의 문제인데, 유지를 원할 수도 있고, 안 원할 수도 있습니다.


Entity Framework 처럼 WCF DataService를 이용하면, Domain Layer의 모든 정보를 간단하게 외부에 서비스 (노출) 할 수 있습니다. 정말 환상이긴 합니다만, OOP 의 객체 안정성을 위해, 대부분의 정보를 은폐하는 것을 추천하듯이, 있는 그대로 노출하는 것보다 DTO 패턴을 사용하여, 원하는 내용만 서비스(노출) 하는 것을 더 추천하고 싶습니다.

이 예를 DB 간의 Data 공유 시에 Table 들을 OPEN 해주는 것과 VIEW 를 만들어서 제공하는 것은 다른 의미가 있습니다. 실제 Data를 쓸 수 없다하더라도, 구조를 알게되면 보안에 문제가 생기겠죠.
또 한가지 문제점은 서비스마다 VIEW 를 만든다던가, 공통의 큰 VIEW 하나를 만든다면, 유지보수나 성능상에 문제가 큽니다.

여기 NHibernate 엔티티를 DTO로 만들 때, 단순 엔티티가 아니라 Association 된 부분도 같이 자동으로 Mapping 해주는 기능을 가진 AutoMapper 를 사용해 봅시다.

원문 : AutoMapper 의 Nested Mappings

원문을 보시면 아 하시겠지만, NHIbernate의 Lazy Loading 을 위한 Proxy에 대해서도 과연 될까요? 그럼 함 해보지요^^

 

우선 NHibernate 엔티티로서 Parent-Child 를 구성했습니다.

[Serializable]
public class Parent : DataEntityBase<Guid>, IParent
{
public Parent()
{
Id = Guid.NewGuid();
Version = -1;
}

public virtual int Version { get; set; }
public virtual int Age { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }

private IList<Child> _children;

public virtual IList<Child> Children
{
get { return _children ?? (_children = new List<Child>()); }
protected set { _children = value; }
}

public override int GetHashCode()
{
if(IsSaved)
return base.GetHashCode();

return HashTool.Compute(Id, Version);
}
}


[Serializable]
public class Child : DataEntityBase<Guid>
{
public Child()
{
Id = Guid.NewGuid();
Version = -1;
Name = "Some Child";
}
public Child(string name, Parent parent):this()
{
Name = name;
Parent = parent;
}
public Child(Guid id, string name, Parent parent): this(name, parent)
{
Id = id;
}

public virtual int Version { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
public virtual Parent Parent { get; set; }

public override int GetHashCode()
{
return IsSaved ? base.GetHashCode() : HashTool.Compute(Parent, Name);
}
}


============================================================



자 그럼 Parent – Child 에 대응되는 DTO 들을 정의해보면,



[Serializable]
public class ParentDTO : ValueObjectBase
{
public ParentDTO(string name, int age)
{
Name = name;
Age = age;
}

public Guid Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }

private IList<ChildDTO> _children;
public IList<ChildDTO> Children
{
get { return _children ?? (_children = new List<ChildDTO>()); }
set { _children = value; }
}

public override int GetHashCode()
{
return HashTool.Compute(Name);
}
public override string ToString()
{
return string.Format("ParentDTO# Id=[{0}], Name=[{1}], Age=[{2}]", Id, Name, Age);
}
}



[Serializable]
public class ChildDTO : ValueObjectBase
{
public Guid Id { get; set; }
public int Version { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ParentDTO Parent { get; set; }

public override int GetHashCode()
{
return HashTool.Compute(Id);
}
public override string ToString()
{
return string.Format("ChildDTO# Id=[{0}], Name=[{1}], Description=[{2}], Parent=[{3}]", Id, Name, Description, Parent);
}
}


 



두 매핑 클래스들이 유사하지요? 다만 NHibernate 엔티티들은 DataEntityBase<T>  라는  NHIbernate용 클래스를 상속 받고, DTO 들은 일반 Persistent Object를 나타내는 ValueObjectBase 클래스를 상속받습니다. 그리고 둘 간의 매핑 정보는 양방향 관계를 가지도록 했습니다.



자 그럼 이제 Mapping 이 제대로 되는지 확인해 봅시다.



/// <summary>
///
AutoMapper 를 이용하여, Association 을 가진 엔티티도 DTO로 매핑이 가능합니다.
/// 즉 Parent-Child 를 ParentDTO-ChildDTO 로 매핑이 가능합니다.
/// </summary>
[TestFixture]
public class DtoMappingFixture : NHRepositoryTestFixtureBase
{
#region << logger >>

private static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();

#endregion

protected override void
OnTestFixtureSetUp()
{
base.OnTestFixtureSetUp();

Mapper.CreateMap<Parent, ParentDTO>();
Mapper.CreateMap<Child, ChildDTO>();
Mapper.AssertConfigurationIsValid();
}

[Test]
public void MapToDto()
{
UnitOfWork.CurrentSession.Clear();

var parent = Repository<Parent>.Get(parentsInDB[0].Id);

Assert.IsNotNull(parent);
Assert.AreEqual(parentsInDB[0].Name, parent.Name);
Assert.AreEqual(2, parentsInDB[0].Children.Count);
Assert.AreEqual(2, parent.Children.Count);

var parentDTO = Mapper.Map<Parent, ParentDTO>(parent);
Assert.IsNotNull(parentDTO);
Assert.AreEqual(parentsInDB[0].Name, parentDTO.Name);
Assert.AreEqual(2, parentDTO.Children.Count);

foreach(var childDto in parentDTO.Children)
Console.WriteLine(childDto);
}
}




보시다시피 NUnit 을 이용하여 테스트 코드를 작성하였습니다.



우선 테스트 시작 시에 OnTestFixtureSetUp()에서 CreateMap 을 이용하여 각각 Parent – ParentDTO, Child – ChildDTO 에 대한 매핑을 정의하고, 매핑이 유효한지 확인 합니다.



이제 엔티티를 로드하고, Mapper.Map 을 통해 Parent 엔티티를 매핑해서 ParentDTO 를 만들었습니다. 단지 그것만 했는데, parentDTO.Children 에 값이 제대로 들어오는군요.



이제 DTO를 이용한 외부 서비스가 상당히 간단해질 것입니다. 물론 WCF DataService용으로 NHibernateDataContext를 만들어도 되지만, 전 DTO 방식이 노가다가 좀 더 들어가서 고생은 되지만, 유연성이나 보안등에서 더 땡기네요^^



울 회사 PMS 팀에서 이거 보면 뒤집어지겠군요^^



PS. Assertion을 해야 하는데, 어제 밤에 갑자기 해보는 바람에 Console.WriteLine 을 써 버렸네요… 여러분은 Assertion을 하세요. 꼭!!!

댓글 없음: