음… 한 달 전에 월간 마이크로소프트에 실버라이트 데이터 바인딩에 대해 기고를 했는데요, 3월호에 실렸더군요. 전 모르고 있었는데…(이건 뭔가… ┐─) 이 글은 총 3회로 나누어 쓴 글로 현재 2회까지 기고를 했고 아마 4월호에 두 번째 내용이 나갈거에요. 글의 저작권은 제게 있지만 월간 마이크로소프트가 나오면 그 때 올리도록 할께요.

--------------------------

데이터의 표현과 로직의 분리

표현과 로직의 분리. 얼마나 아름다운 말인가! 그러나 우리는 십수년간 사용해왔던 HTML의 경험으로부터 결코 쉽지만은 않다는 걸 배웠을뿐이다. 그렇지 않은가? 여러분은 디자이너가 만든 HTML 레이아웃을 손대지 않고 얼마나 작업할 수 있는가? 여러분은 데이터의 표현을 얼마나 협업의 고통 없이 수정할 수 있는가?

실버라이트가 출시된지도 벌써 2년이 다 되어간다. 결코 긴 시간은 아니지만 경험을 쌓기엔 충분한 시간이다. 현장에서 접하는 실버라이트는 어떤 모습인가? 필자를 포함한 얼리어답터들이 주장하던 것 처럼 정말로 표현과 로직이 잘 분리되어 디자이너와 개발자가 원활한 협업을 하고 있는가?

필자의 답은 ‘아니오’이다. 우리는 여전히 HTML 시절에 겪었던 문제를 고스란히 안고 있고 여전히 디자이너의 결과물을 개발자가 망치고 있으며 데이터의 표현이 변경될 필요가 있을 때 디자이너와 소모적인 코드 수정을 하고 있다. 다만 HTML에 비해 레이아웃이나 표현을 하기가 쉬워졌고 엄격한 XML 문법으로 인하여 구조적인 문제가 줄었을 뿐이다.

그렇다면 도대체 무엇이 문제일까? 단지 디자이너가 툴에 익숙하지 않아서 협업이 이루어지지 않는다고 말한다면 그것은 ‘비겁한 변명’일 것이다. 필자는 실버라이트가 표현과 로직을 분리하기 위한 도구와 기법들을 제공함에도 불구하고 별반 나아진게 없는 문제의 원인을 패러다임의 변화에 적응하지 못하는 우리 개발자에게 돌리고 싶다. 그러나 처음부터 너무 비관적으로 될 필요는 없다. 문제를 발견했다면 그것을 해결할 뿐. 오히려 문제를 푸는 과정이 재밌다고 느껴지지 않는가? 기존의 경험으로부터 정리된 통찰을 이끌어내는 과정은 짜릿하지 않은가?

물론 모든 문제를 해결하는 마법 같은건 존재하지 않지만 적어도 몇몇 문제를 해결하는 좋은 방법 정도는 존재하는 법이다. 앞으로 연재될 이 글을 통해 데이터의 표현과 로직을 보다 효율적으로 분리하는 방법을 연구해보고 아주 조금이라도 디자이너와 개발자가 협업의 고통을 줄였으면 하는 바램이다.

처음 기고하다 보니 의욕이 넘쳐 서론이 길었다(^^a). 그럼 본격적으로 데이터와 표현과 로직을 분리하기 위해 반드시 익혀야 할 실버라이트의 데이터 바인딩에 대해 연구해보자.

전형적인 데이터 표현 과정

메신저나 커뮤니티 회원 명부 등에서 볼 수 있는 회원 카드 애플리케이션을 생각해보자. 단순하게 ID, 사진, 이메일 주소, 블로그 주소, 생일 정보 정도면 충분할 것이다. 필자는 다음과 같은 구조의 프로젝트를 만들어 봤다.


[그림 1. 샘플 프로젝트의 구조]

먼저 데이터를 다루는 Model 폴더에는 회원 정보 클래스인 Profile.cs가 있고 회원 정보를 제공하는 서비스를 시뮬레이션하는 MockProfileService가 있다. 물론 제대로 하려면 웹 서버를 구축하여 서버에서 데이터를 가져와야 겠지만 그렇게 구성되었다고 가정하겠다. MockProfileService는 다음과 같이 회원 정보를 랜덤하게 반환하는 GetAnyProfile 메서드와 주어진 회원 정보를 업데이트하는 UpdateProfile 메서드를 노출하고 있다.

public Profile GetAnyProfile() //생략
public void UpdateProfile(Profile profile) //생략

[리스트 1. MockProfileService 클래스가 노출하고 있는 메서드]

다음으로 회원 카드를 보여주기 위한 유저 컨트롤인 CardView가 View폴더에 있다. 익스프레션 블렌드를 사용하여 다음과 같이 간단하게 디자인했다.


[그림 2. 간단한 회원 카드 디자인(말할 것도 없이 엉망이다.)]

디자인은 IdLabel, ProfileIamge, BritdayInput과 같이 직관적인 이름으로 오브젝트를 배치했다. 뭔가 아니다 싶은 디자인이 보이는가? 이것이 필자가 개발자라는 직업을 선택한 이유 중 하나이다. 어쨌든 지금은 디자인의 품질이 중요한게 아니므로 이쯤하고 넘어가도록 하자.

앞에서 디자인한 유저 컨트롤의 코드 비하인드인 CardView.xaml.cs 파일을 열어보면 Profile 속성을 설정했을 때 해당 회원의 데이터를 화면에 보여주고, 또한 TextBox의 Text속성이 변경되었을 때 ProfileUpdated 이벤트를 던지는 코드가 작성되어 있다.

private Profile _profile;
public Profile Profile
{
    get { return _profile; }
    set
    {
        _profile = value;
        // 입력 받은 정보로 UI를 업데이트
        IdLabel.Text = _profile.Id;
        BirthdayInput.Text = _profile.Birthday.ToString("yyyy-MM-dd");
        EmailInput.Text = _profile.Email;
        BlogInput.Text = _profile.Blog;
    }
}

// 입력창의 포커스가 벗어났을 때 값을 변경하는 코드들
void BirthdayInput_LostFocus(object sender, RoutedEventArgs e)
{
    Profile.Birthday = DateTime.Parse(BirthdayInput.Text);
    FireProfileUpdated();
}

[리스트 2. CardView의 코드 비하인드에 작성된 지루한 코드의 일부]

마지막으로 루트 비주얼인 Page.xaml에는 버튼 하나와 MyCard라는 이름의 CardView 하나를 올려놓았고 Page의 코드 비하인드인 Page.xaml.cs를 보면 MockProfileService의 인스턴스를 하나 만들어놓고 컨트롤이 초기화되거나 버튼을 클릭했을 때 서비스의 GetAnyProfile 메서드를 호출하여 MyCard의 Profile 속성을 설정하게 되어 있으며, MyCard에서 ProfileUpdated 이벤트가 발생했을 때 MockProfileService의 UpdateProfile 메서드를 호출하여 변경된 데이터를 업데이트하고 있다.

솔루션을 빌드하고 실행해 보면 버튼을 클릭할 때마다 샘플로 작성한 데이터 중 하나를 랜덤으로 표시하는 것을 볼 수 있고 텍스트 박스 중 하나를 변경하면 변경된 데이터가 실행되고 있는 동안에는 유지되는 것을 확인할 수 있다. 자세한 코드와 동작은 다운로드 받은 파일의 압축을 풀고 [DataBinding Take1] 폴더에 있는 소스코드를 열어 직접 확인하길 바란다.


[그림 3. 회원 카드 애플리케이션의 동작 화면]

어쨌든 이 애플리케이션은 잘 동작한다. 실무에서 이 정도 코드면 충분하다. 아니, 충분했었다. 그럼 도대체 무엇때문에 잘 동작하는 코드를 바꿔야 할까? 이 프로젝트는 협업과 유지보수의 관점에서 적어도 네 가지 이상의 문제점을 가지고 있다. 여러분도 읽기 전에 한 번 생각해보길 바란다.

  • 불필요한 임시 데이터가 XAML에 직접 하드코딩 되어 있다. 혹시 임시 데이터라고 아무렇게나 데이터를 넣었다가 실제 런타임에 고객에게 노출된 적은 없는가?
  • 디자인에 들어가야 할 구성요소의 종류가 강제되고 있으며 이름을 정확히 맞춰주지 않으면 CardView.xaml.cs에 있는 코드는 무용지물이 된다. 디자이너의 표현력이 코드 때문에 제한받고 있지 않은가?
  • CardView에서 Profile 속성의 세터(setter)를 보자. Profile 데이터 하나하나를 다시 설정하고 있다. 만약 더 많은 속성이 추가되거나 변경된다면 그때마다 이렇게 단순 반복적이고 소모적인 코드를 다시 작성하고 싶은가?
  • 디자인이 변경되었을 때 역시 이 과정을 다시 반복할 것인가?
  • 또한 입력한 값이 유효한지 확인하는 처리까지 들어간다면 복잡도는 더욱 늘어날 것이다.

데이터 바인딩 예제

데이터 바인딩은 컨트롤의 특수한 속성과 XAML의 몇 가지 표현식을 사용한다. 데이터 바인딩을 설명하기 전에 먼저 사용 예제를 보도록 하자. 앞에서 작성한 코드에서 아주 조금만 고쳐도 상당히 쾌적해진다. 물론 모든 문제를 한꺼번에 해결할 수는 없지만 하나씩 단계적으로 잡아 나가도록 하겠다. 역시 자세한 코드는 [DataBinding Take2 - binding basic] 폴더의 코드를 참고하길 바란다.

먼저 CardView에 작성했던 지루한 설정/업데이트 코드들을 시원하게 날려버리자. 컴포넌트 초기화를 제외한 모든 코드를 말 그대로 지우면 된다. Profile을 받고 XAML에 어떤 디자인에 어떻게 설정해야 할지 고민할 필요가 없다.

public partial class CardView : UserControl
{
    public CardView()
    {
        InitializeComponent();
    }
}

[리스트 3. CardView.xaml.cs: 단순해진, 아니 텅비게 된 CardView의 코드 비하인드]

앞의 코드를 제거하면 업데이트 등으로 사용했던 이벤트가 삭제되어 Page.xaml.cs에서 오류가 발생한다. 여기에서도 불필요한 이벤트 처리등을 제거하자. 다만 버튼을 클릭하여 회원 정보를 다시 받아야 할 때 다음과 같이 MyCard의 DataContext 속성을 설정하기만 하면 된다.

private void UpdateProfile()
{
    // 서비스 호출(웹 서비스 등의 호출을 통해 받았다고 가정)
    Profile profile = _service.GetAnyProfile();

// 회원 카드의 정보 업데이트
    MyCard.DataContext = profile;
}

[리스트 4. Page.xaml.cs: CardView에 대한 의존성이 줄어든 Page의 코드 비하인드]

CardView의 인스턴스인 MyCard를 사용하는 곳이 UpdateProfile 메서드 단 한 곳으로 줄었다. 나중에 뭔가 변경이 생기더라도 간편하게, 대부분은 전혀 손대지 않고도 적용이 가능할 것이다.

다음으로 DataContext에 설정된 회원 데이터를 CardView에 보여줘야 한다. CardView.xaml에 데이터 바인딩 표현식을 사용하여 데이터를 표시하면 된다. 지면 관계상 핵심적인 부분만 표시하였다.

<TextBlock Text="{Binding Id}" />
<Image Source="{Binding Picture}" />
<Grid >
        <TextBlock Text="Birthday" />
        <TextBox Text="{Binding Birthday, Mode=TwoWay}" />
        <TextBlock Text="Email" />
        <TextBox Text="{Binding Email, Mode=TwoWay}"  />
        <TextBlock Text="Blog" />
        <TextBox Text="{Binding Blog, Mode=TwoWay}" />
</Grid>

[리스트 5. CardView.xaml: Binding 표현식으로 설정된 CardView]

센스있는 독자라면 굳이 설명하지않더라도 위의 코드가 무엇을 의미하는지 눈치 챘을 것이다. <TextBlock Text="{Binding Id}" …/> 코드를 예로 들어 간단하게 말로 하자면 “TextBlock의 Text 속성을 Id라는 이름을 가진 속성으로 바인딩(Binding)한다.”라고 할 수 있다. 물론 여기에서 Id라는 이름을 가진 속성은 CardView 유저 컨트롤의 DataContext에 설정된 데이터에서 가져오는 것임은 말 할 것도 없다.

아래쪽에는 좀 더 흥미있는 표현식이 보이는데, <TextBox Text="{Binding Birthday, Mode=TwoWay}" …/> 코드를 예로 들자면 “TextBox의 Text 속성을 Birthday라는 이름을 가진 속성으로 바인딩하되, 바인딩 모드는 양방향(TwoWay)로 한다.”라고 할 수 있다. 여기에서 TwoWay라는 모드는 그 이름에서 추측할 수 있듯이 바인딩 된 대상 오브젝트의 속성이 변경될 경우 역으로 바인딩 된 원본 데이터의 속성도 변경시켜 서로 동기화시킨다는 의미이다. 보통 TextBox나 CheckBox, RadioButton과 같이 사용자가 값을 입력하거나 선택할 수 있는 컨트롤의 속성에 많이 이용된다. 실행결과는 당연하게도 앞의 프로젝트와 동일하다.


[그림4. 데이터 바인딩을 사용한 실행 화면]

자 어떤가? 데이터 바인딩을 사용하여 50라인이 넘는 코드를 삭제하고 아주 단순한 코드와 표현식만으로 서비스에서 받은 데이터를 효과적으로 표시하는 것이 가능해졌다. 특히 CardView의 XAML 코드에는 x:Name이라는 속성 없이도 데이터를 표현하는 것이 가능하므로 디자이너가 더 이상 개발자와 골치아픈 이름 짓기 문제로 싸울 필요가 없게 되었다. 물론 의미가 있는 오브젝트에 x:Name으로 그 오브젝트를 설명하는 것은 훌륭한 시도이다. 그러나 이 이름은 어디까지나 참고일 뿐이지 실제 코드 작성에 반드시 필요한 것이 아니므로 디자이너든 개발자든 마음대로 이름을 지어도 된다. 이것만으로도 행복하지 않은가!

또한, CardView를 제어하고 CardView에서 일어나는 동작을 감시하여 별도의 처리를 해줘야 했던 Page 유저 컨트롤 역시 CardView와의 의존성이 줄어들고 CardView에게 필요한 데이터를 줘 버리고 모른척할 수 있게 되었다. 서로의 의존성을 줄이는 것, 이것이야말로 개체지향 프로그래밍의 성배가 아니었던가!

물론 여기에는 문제점도 남아있다. 블렌드로 CardView 유저 컨트롤을 열어보자.


[그림5. 데이터 바인딩된 유저 컨트롤의 모습]

데이터 바인딩을 위해 사용한 표현식때문에 앞에서 임시로 넣었던 데이터는 온데간데가 없다. 다만 해당 속성에는 데이터 바인딩을 사용하고 있음을 알리는 노란색 테두리가 있을 뿐. 이래서야 디자이너가 디자인을 변경할 때 결과물을 쉽게 예측하기 어렵다. 아니, 불가능하다.

또한 데이터 바인딩을 사용한 실행 결과를 보면 날짜 필드가 우리가 기대한 것과는 달리 년/월/일만 나온게 아니라 월/일/년에 시간까지 나온다. 이래서야 단순 바인딩은 제대로 쓸 수 없다.

사실 필자도 처음에는 이런 문제점 때문에 데이터 바인딩을 사용하지 않았었다! 그렇다면 데이터 바인딩으로 디자이너와의 협업을 보다 원활하게 한다는 필자의 말이 거짓이란 말인가. 물론 아니다. 필자는 앞으로 진행되는 연재를 통해 이 문제를 우아하게(?) 해결하는 법을 소개하고자 한다. 물론 데이터 바인딩을 소개하기 위해 건너뛴 데이터 바인딩의 이론적인 기초 및 중요한 문법에 대해서도 소개할 것을 약속한다.

참고 자료

Posted by gongdo