Steven Sanderson의 저서 Professional Asp.Net MVC 1(장현희 옮김) 그리고 MVC 2 (원서)를 공부하면서 최근에 발표된 MVC 3와 차이점을 비교 학습을 진행합니다.
MVC의 장점이 어디에 있고 MVC 1, 2, 3은 어떠한 변화를 가져왔는지? 어떻게 시작해야 할지 등을 생각해 볼까 합니다.
-------------------------------------------------------------------------------------
모델은 데이터를 표현할 뿐만 아니라 유효성 검사 및 비즈니스 규칙 등의 도메인 로직을 의미합니다. 따라서, 모델은 MVC의 핵심적 혁할을 담당하고 있습니다.
모델을 구현하는 기술은 다양하지만 여기에서는 Linq to Sql과 Entity FrameWork의 기술에 대하여 알아봅니다. Linq to Sql은 버전 1에서 사용하여 계속 사용하고 있으며, Entity framework는 .Net FrameWork 4.0 버전에서 지원하는 기술입니다. 따라서 여기에서는 버전 2, 3에서 계속 이어지는 EF기술에 관하여 중점적으로 살펴볼 예정입니다.
모델클레스 생성
아래 그림은 "Linq to Sql"를 이용하여 생성한 모델입니다.




Linq to Sql를 이용한 모델의 생성은 상기와 같은 방법으로 만들어 집니다.
아래 그림은 Entity FrameWork를 이용하여 생성한 모델입니다. 버전 2, 3에서 사용하는 기술로 버전 3은 버전 2와는 조금 다른 면이 있지만, 큰 틀에는 변화가 없는것 같습니다.



아래 그림을 잘 살펴보세요. 데이터 연결 선택에서 아래와 같이 나오면 잘 못된 것입니다. 저는 "Linq to Sql"을 이용한 모델과 Entity FrameWork 모델을 동시에 만들었더니 아래와 같은 설정이 나와서 다음 진행이 문제가 되어 3시간을 소모했습니다.

아래 "데이터 연결 선택"이 "NerdDinner.mdf"로 나와야 정상적으로 다음작업이 이루어 집니다.

데이터베이스의 테이블명이 대부분 복수명으로 되어있습니다. 이는 다수의 레코드들로 구성되어진 테이블의 특성을 반영한 것입니다. 그러나 모델에서는 한개의 레코드를 불러와 개체를 만든것입니다.

아래 그림은 자동으로 생성되어 진 것입니다.


NerdDinnerEntities Classes
참고 : Linq to Sql은 버전 1,2,3에 계속하여 사용되고 있지만, 1:1만 지원하는 관계로 소규모에 적합하나 좀더 복잡한 포털 개념에는 .NET FrameWork 4.0 에서 포함된 "EF Code First" 기술이 적합할 것 같습니다. 따라서 앞으로는 Entity FrameWork Classes를 중점으로 배워나갈 계획입니다.
Entity FrameWork는 .Net 4.의 일부분으로서 ORM(object relational maper)입니다. 이는 개체 관계형 매퍼로 불리우며 모델과 데이터들의 관계를 표시하기 위한 클래스를 자동으로 생성합니다.
상기 그림에서와 같이 모델 Dinner은 NerdDinner 테이블과 Rsvp테이블의 데이터와 모델의 상호 작용을 위하여 Entities Classes를 자동으로 생성하게 됩니다. 또한 이 Entities Classes는 2개의 속성을 갖게 되는데 이는 데이터베이스의 2개 테이블의 단일 레코드의 속성을 나타냅니다.
다음은 NerdDinnerEntities 개체 인스턴스를 생성하고 LINQ 쿼리를 이용하여 Dinner의 개체 컬렉션을 얻어오는 방법을 보여줍니다.
//모델 개체 인스턴스 생성
NerdDinnerEntities db = new NerdDinnerEntities();
//DinnerID가 1인 Dinner 개체를 찾는다.
Dinners Dinner = db.Dinners.Single(d => d.DinnerID == 1);
//Dinner의 개체의 속성을 바꾼다(update).
Dinner.Title = "잠은 언제 자니?";
Dinner.Description = " 안 잔다!";
// 변경사항 저장 (persist changes)
//db.SubmitChangs(); - Linq to Sql
db.SaveChangs();
상기에서 NerdDinnerEntities 개체는Single 메서드에 의해 리턴된 Dinner 개체에 적용된 변경사항을 자동 추적하여 db.SaveShanges(); 가 호출되면 업데이트를 위한 "Update" SQL 구문을 실행한다.
DinnerRepository Class 구현하기
애플리케이션의 규모가 커지면 컨트롤 개체에서 직접 Linq to Sql의 DataContext 또는 Entity FrameWork의 Entities Classes와 직접 상호 작용하는데 유지보수 및 테스트에 영향을 주게되며, 동일한 쿼리를 여러곳에서 중복 상요할 우려가 있습니다. 따라서 유지보수 및 테스트를 쉽고 편하게 하기위해서 사용되는 것 중 하나가 "저장소(Repository)" 패턴이 있습니다.
Repository Classes 는 데이터 관련 쿼리와 로직을 캡슐화하고 애플리케이션으로부터 데이터베이스 관련 상세구현을 추상화한다. 따라서 애플리케이션의 코드를 깔끔하게 유지하게 되고 차후에 Repository를 변경하기 편리한 장점이 있습니다. 또한 실제 데이터베이스가 없어도 단위 테스트를 수행할 수 있는 등의 여러 장점이 있습니다.
다음은 Repository Class를 구현하는 방법의 예시입다.
public class DinnerRepository
{
private NerdDinnerEntities db = new NerdDinnerEntities();
// 쿼리
public IQueryable<Dinner> FindAllDinners(); //찾기
public IQueryable<Dinner> FindUpComingDinners(); //가져오기?
public Dinner GetDinner(int id); //얻기 ?
//추가, 삭제 메서드
public void Add(Dinner dinner);
public void Delete(Dinner dinner);
//persistence
public void Save();
}
참고 : 이장의 후반에서부터는 DinnerRepository class로부터 "IDinnerRepository 인터페이스"를 추출하여 컨트롤러 클레스에 의존성 삽입(DI : Dependency Injection) 기능을 지원할 것입니다. 현재는 DinnerRepository class를 직접 사용할 것입니다.
다음 그림은 DinnerRepository class를 생성하는 화면입니다.

생성된 DinnerRepository class를 다음과 같이 구현합니다.
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Data.Objects.DataClasses;
namespace NerdDinner.Models { public class DinnerRepository { //객체 인스턴스 생성 private NerdDinnerEntities db = new NerdDinnerEntities();
//Query Method IQueryable<모델 테이블 속성> Find(텍스트 검색 명) **검색 조건 Where를 상용 public IQueryable<Dinner> FindDinnesByText(string q) { return db.Dinners.Where(d => d.Title.Contains(q) || d.Descritpion.Contains(q) || d.HostedBy.Contains(q)); }
//Query Method IQueryable<모델 테이블 속성> FindAll(Dinners)() public IQueryable<Dinner> FindAllDinnes() { return db.Dinners; }
//Query Method IQueryable<모델 테이블 속성> Find(진행 중인 행사)() public IQueryable<Dinner> FindUpComingDinners() { return from dinner in db.Dinners where dinner.EventDate > DateTime.Now orderby dinner.EventDate select dinner; }
public IQueryable<Dinner> FindByLocation(float latitude, float logitude) { var dinners = from dinner in FindUpComingDinners() join i in NearestDinners(latitude, logitude) on dinner.DinnerID equals i.DinnerID select dinner; return dinners; }
//private object NearestDinners(float latitude, float logitude) //{ // throw new NotImplementedException(); //}
public Dinner GetDinner(int id) { return db.Dinners.SingleOrDefault(d => d.DinnerID == id); }
//Insert / Delete Method public void Add(Dinner dinner) { //db.Dinners.Insert[안내]태그제한으로등록되지않습니다-xxOnSubmit(dinner); - Linq to Sql db.Dinners.AddObject(dinner); } public void Delete(Dinner dinner) { //db.Rsvps.DeleteAll[안내]태그제한으로등록되지않습니다-xxOnSubmit(dinner.Rsvps);- Linq to Sql //db.Dinners.Delete[안내]태그제한으로등록되지않습니다-xxOnSubmit(dinner); foreach (var rsvp in dinner.Rsvps.ToList()) //Rsvps => 탐색 속성 이름 { db.Rsvps.DeleteObject(rsvp); } db.Dinners.DeleteObject(dinner);
}
//persistence Method 저장 메서드 public void Save() { db.SaveChanges(); }
// Helper Methods
[EdmFunction("NerdDinnerModel.Store", "DistanceBetween")] //부족한 점 아직 못찾겠음... public static double DistanceBetween(double lat1, double long1, double lat2, double long2) { throw new NotImplementedException("Only call through LINQ expression"); //명시적으로처리되지 않았음을 의미 }
public IQueryable<Dinner> NearestDinners(double latitude, double longitude) //nearest:가까운 { return from d in db.Dinners where DistanceBetween(latitude, longitude, d.Latitude, d.Longitude) < 100 select d; } } } |
DinnerRepository 클래스를 이용하여 데이터베이스 작업 수행하기
데이터 조회
다음 예제는 DinnerID 값을 이용하여 하나의 Dinners 개체를 조회하는 코드이다.
DinnerRespository dinnerRespository = new DinnerRespository();
// 지정된 ID값으로 조회하는 방법
Dinners dinner = dinnerRespository.GetDinner(5)
다음 예제는 아직 완료되지 않은 저녁 모임 목록을 모두 가져와 루프를 실행한다.
DinnerRepository dinnerRepository = new DinnerRepository()
// 완료되지 않은 모든 데이터를 가져온다.
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
//각각의 Dinners의 개체에 대한 루프를 실행한다.
foreach (Dinners dinner in upcomingDinners){}
데이터의 추가와 수정 예제
"Linq to Sql"과 "Entity FrameWork"는 자동으로 트렌젝션 내의 변경사항을 래핑합니다. 따라서 데이터베이스에 변경을 가하는 모든 명령이 성공적으로 완료될 때까지 하나라도 실패하면 아무런 변경사항도 적용하지 않습니다.
DinnerRepository dinnerRepostory = new DinnerRepository();
//첫 번째 Dinners 개체를 생성한다.
Dinners newDinner1 = new Dinners();
new Dinner1.Title = "오늘도 열심히 시작한다만은";
newDinner1.HostedBy = "정우화";
newDinner1.ContactPhone = "238970293";
//두 번째 Dinners 개체를 생성한다.
Dinners newDinner2 = new Dinners();
newDinner2.Title = "이팀장님은 참 열심이야";
newDinner2.HostedBy = "이점희";
newDinner2.ContactPhone = "3089u2309";
//저장소 클래스에 새로운 Dinners 개체들을 추가한다(Add Dinners Respository).
dinnerRepository.Add(newDinner1);
dinnerRepository.Add(newDinner2);
//변경사항을 저장한다.
dinnerRepository.Save();
데이터 삭제
이미 존재하는 개체를 조회한 후 이를 삭제된 것으로 표시한 다음, Repository의 "Save" 메서드를 호출하면 삭제된 것으로 표시된 개체들이 실제로 데이터베이스에서 삭제된다.
DinnerRepository dinnerRepository = new DinnerRepository();
//DinnerID 값을 이용하여 Dinners 개체를 조회한다.
Dinners dinner = dinnerRepository.GetDinner(5);
//조회된 개체를 삭제된 것으로 표시한다.
dinnerRepository.Delete(dinner);
// 변경사항을 저장한다.
dinnerRepository.Save();
모델클래스에 유효성 검사 및 비지니스 규칙 추가하기
유효성 검사와 비즈니스 규치을 적용하는 것은 데이터를 처리하는 애플리케이션에 있어서 필수불가결한 사항이다.
Schema 유효성 검사
(스키마: . 데이터 모델에 의거하여 DB의 성질을 형식적으로 기술한 것. 2. 개체를 관리하기 위한 묶음, 3. 데이터베이스 이름.스키마 이름.개체 이름<테이블>)
엔터티 프레임워크는 모델에서 사용하는 데이터 타입과 데이터베이스의 데이터 타입이 같다. 따라서 형식이 다른 데이터가 들어올 경우 오류가 발생한다. 또한 엔터티 프레임워크는 Sql 구문과 관련된 문자열을 자동으로 처리하므로 SQL 인젝션 공격에 대해 걱정할 필요가 업습니다.
유효성 검사와 비즈니스 규칙
데이터 타입에 대한 유효성 검사는 유용하기는 하지만 충분하다고 보기는 어렵습니다. 실제 시나오들은 여러 소성에 동시에 적용되거나 코드의 실행, 그리고 모델의 상태(예를 들면, 모델이 추가 수정 혹은 삭제된 상태인지 아니면 경우에 따라 보관된 상태인지 등)인식 등에 사용될 수 잇는 보다 풍부한 유효성 검사 로직을 요구합니다.
MVC 2에서는 데이터 주석 유효성검사 속성에 대한 지원을 소개합니다. 그것은 네임스페이스 System.CompontModel.DataAnnotations에 존재하는 속성 세트이며 ASP.NET 3.5 SP1의 동적 데이터 기능의 일부분으로 포함됩니다.이들 속성을 사용해서, 추가 참조창의 .net 탭으로부터 System. CompornentModel.DataAnnotations.dll 어셈블리 참조를 확실하게 합니다.
참고 : 이 어셈블리는 Visual Studio에서 MVC 2를 이용하여 생성할 때 기본적으로 참조되어집니다.
아래 표에서와 같이 .Net FrameWork는 4개의 유효성검사를 포함합니다.
속성 |
설명 |
RangeAttribute |
속성값에 대한 명시적 수의 범위를 제한합니다. |
RegularExpressionAttribute |
명시된 정규 표현식에 일치하는 속성값을 명시함니다. |
StringLengthAttribute |
속성에 따른 문자열의 최대크기를 명시합니다. |
RequiredAttribute |
요구된 속성값에 대한 명시 |
우리는 유효성 검사를 위한 우리의 모델의 속성에 이러한 속성들을 적용할 수 있습니다. 예를 들면, dinner의 Title에 요구하는 것을 나타내려면, 우리는 다음과 같이 RequiredAttribute를 적용할 수 있을것입니다.
public class Dinner
{
[Required(ErrorMassage = "Title is required")]
public string Title{ get; set;}
}
Buddy Class추가
Buddy Class를 추가하는데 있어서, 부분클래스를 추가해야 한다. 부분 클래스는 VS디자이너에 의해 관리되는 클래스들(엔터티 프레임워크 디자이너에 의해 관리되는 Dinners 개체와 같은 클래스들)에 속성이나 메서드 혹은 이벤트를 추가하기 위한 용도로 활용되며, 디자이너 도구들이 우리가 작성한 코드를 날려버리지 않도록 방지하기위한 목적으로도 할용할 수 있다.
아래 그림과 같이 클래스를 추가합니다. 추가를 클릭하면 Dinner.cs파일이 생성됙 해당 파일을 IDE를 통해 열여준다. 그런 후 다음의 코드와 같이 기본적인 유효성 검사 및 비즈니스 규칙을 수행하는 프레임워크를 구현할 수 있습니다.


아래와 같은 코드를 작성한다.