프로그램은 현실세계의 추상화

고난이도의 알고리즘에 기반한 프로그램이거나 혹은 무수한 쿼리문을 날려 그 결과값을 화면에 뿌리기만 하는 관리 프로그램이라도 그 근본 구조는 다르지 않다. 객체지향 프로그래밍에선 결국 프로그램은 현실세계의 추상화이기 때문이다. 사용자의 요구사항을 추상화된 모델을 만들어 처리하고 응답하는 것이다.

 

회사의 자재관리 프로그램을 짠다고 생각해 보자. MVC 패턴에 입각하면 UI는 단순히 모델의 반영이며, 모델에 값을 전달하는 것에 불과하다. 물론 요즘 프로그램의 UI는 복잡하고 화려해지면서 그 자체가 하나의 프레임웍으로 구현된다.

UI프레임웍은 이미 충분히 일반화 되어 있다 . MFC, WPF, WinForm등 그저 갖다 쓰기만 하면 된다.

하지만 모델은 어떤가. 모든 프로젝트는 결코 동일하지 않듯이 프로그램도 동일한 비지니스 로직을 표현하는 것은 없다. 불가능한 것은 아니나 비지니스 로직을 프레임웍으로 만들기는 대단히 어렵다. 처음부터 비지니스 프레임웍을 구성하도록 하기보다는 프로그램에 맞도록 모델들을 잘 연계한 구조 (아키텍처, 골격)를 가져야 확장성과 유지보수성을 극대화할 수 있다. 

 

  • UI 결합 코드의 일반적인 사례

여러 회사에서 여러 프로그램을 접해보면서 놀라운점은 대부분의 프로그램이 UI에 밀접하게 연계되어 있다는 것이다. UI를 분리할경우 어떠한 작업도 진행할 수 없도록 짜여져 있다. 더욱 놀라운 것은 그게 문제인지조차 모르는 경우가 허다하다. 너무나 많은 프로그래머들이 그런식으로 작성해왔기 때문이다.

 

모델과 뷰의분리가 이해가 가지않는다면 역설적으로모델과 UI결합된 것을 보면된다. 다음 화면은 간단한 곱셈을 연산한 값을 출력하는 프로그램이다.

 

단순한프로그램을 어떻게구현할까? 초보자들은 흔히 다음과 같이 짜곤 한다. 

 

voidCBadCalculatorDlg::OnBnClickedBtnResult()

{

       CStringsLValue, sRValue;

       m_editLValue.GetWindowText(sLValue);

       m_editRValue.GetWindowText(sRValue);

       double rLValue = _tstof(sLValue);

       double rRValue = _tstof(sRValue);

       double rRet = rLValue * rRValue;     

 CStringsRetValue;

       sRetValue.Format(_T("%.4f"), rRet);

       m_editRetValue.SetWindowText(sRetValue);

} 

 

계산버튼 누르는이벤트 안에서값을 입력하는 에디터박스 안의값을 가져와서곱하기를 다시결과 값을에디터 박스에넣어 표현한다. 뭐가 문제일까? UI 있을 모델 객체가 없다. 모델객체가 없다면곱하기 테스트같은 것이불가능 해지고, 또한 다른 프로그램에서 계산기를 재사용하는 것이 어려워진다. 물론 복사신공(?)으로 가능하지만, 그건 객체지향 세계에선 가장 범죄 행위(?)중 하나이다.

 

그럼모델객체를 만들어보자. 혹자는 단순한 곱하기인데 굳이 클래스를 만들 필요가 있느냐고 생각할지 모르겠다.  현재의 상황만 보자면 사실 충분하기도 하다. 하지만 프로그램은 생물과 매우 유사한 진화 과정을 거치게 됨을 생각해야 한다.

 

#pragma once

namespace Indy

{

       class Calculator

       {

       public:

             Calculator(void);

             virtual ~Calculator(void);

             double GetResult() { returnm_rResult; }

             double Multiply(doublelValue, double rValue);

             double Multiply(doubleRValue);

       protected:

             double m_rResult;

       };

};

Calculator 구현

#include "StdAfx.h"

#include "IndyCalculator.h"

using namespace Indy;

Calculator::Calculator(void)

{

       m_rResult = 0;

}

doubleCalculator::Multiply(double lValue, double rValue)

{

       m_rResult =lValue * rValue;

       return m_rResult;

}

doubleCalculator::Multiply(double RValue)

{

       m_rResult *=RValue;

       return m_rResult;

}

 

위의 선언부를보면 개의 값을곱하는 함수가있고, 인자가 하나뿐인 곱하기 함수도 있다. 우리가계산기를 사용할 이전의연산 결과에다시 곱하는경우가 있는데, 인자가 하나뿐이 함수의 구현부를 보면 이전의 결과값을 누적함을 확인할 있다.  곱하기를 수행했는지를 알고 싶다면? 곱하기를 히스토리를 30단계까지 보고 싶다면? 곱하기의 결과를 자리씩 끊어서 “,” 붙여 회계 형으로 만들고 싶다면 어덯게 할까?

이제슬슬 머리가 아파오기시작할 것이다. 만약모델이 없고 UI이런 기능을 추가하려면조금씩 UI 기능양쪽과 서로 연관되면서 복잡도는 더욱더커지게 된다.

 

나는 골격을 프레임워크의 단계라고 본다. 만약 여러분이 유사한 프로그램을 정도 만든다고 하면 여러분이 만든 골격이 차츰 프레임워크로 발전할 것이지만 그렇지 않은 경우에는 골격이 아키텍처 차원의  활용성을 가지기는 매우 어렵다. 그러나 짜인 아키텍처는 품질을 유지하고, 개발의 속도를 높이는 데는 막강한 역할을 담당하게 될 것이다.

 

  • 골격이란 프로그램의 핵심기능이 구현된 중요한 구조(아키텍처) 구현물

프로젝트의 초창기엔주변의 자잘한기능이 아닌핵심 기능에집중해야 한다. 우리는 흔히 개발 공정을 작성할 프로그램의 전체 기능을 순서대로 개발하는 오류를 범하기도 한다.

예를 들어 XML 값을 읽어 들여 연산을 결과를 3차원 그래프 출력하는프로그램을 개발할경우 XML파서기 개발부터 시작하는 식이다. 계산기를 만들경우 계산하는기능에 우선집중해야 한다. 결과를 3차원 그래프 표현하거나 XML 출력하는 기능은 핵심 기능이 아니다. 프로젝트의전체를 관통하는주요 흐름만을최대한 빠른시간 내에구현해야 한다.

전체 개발기간의 3-40% 이내의 기간 내에 골격이 완성 되고, 핵심 기능이 구현되지 못한다면 프로젝트는 실패를 예고하고 있는 것이다. XML파서기를 아무리 만들든 무슨소용이 있겠는가? 계산 성능에서심각한 문제가 후반에 발견된다면 말이다.

바로이러한 문제를 조기에확인하기 위해 골격을구현하고 테스트하는 것은 중요한 작업이다. 이 작업이 마무리되면 프로젝트 중, 후반의 작업은즐거운 살붙이기 작업이 것이다.

 

프로그램이 자꾸 버그가 꼬리를 물때, UI의 작은 변경에도 많은 클래스들을 변경해야 할때 자신의 프로그램을 자세히 들여다 보라. 골격이 있는지, 또 있다면 UI와 완전히 분리되어 있는지 살펴볼 일이다.

 

'Successfull Project > Architecture Design' 카테고리의 다른 글

객체지향 코드 - 캡슐화  (0) 2018.12.28

캡슐화의 정의와  C++에서의 응용

캡슐화란 객체가 가지고 있는 내부의 속성을 외부에서 제어하지 못하도록 하는 것을 의미한다.

완벽한 캡슐화라면 내부의 속성값을 직접적으로 바꾸는 행위는 적절하지 않겠지만, 우리는 보통 Setter를 이용하여 속성을 변경하고 Getter를 이요하여 값을 가져오는 것으로 캡슐화를 유지한다.

문제는 Setter는 만들지 않고 Getter만 생성하고 값을 이용하려고 할때 발생한다. Getter는 값을 가져오는 것이라 캡슐화를 깨지 않아야 하지만 속성 객체를 레퍼런스로 넘겨 받게 되면 Getter이자 Setter가 되어 버린다.

Getter가 가진 이런 문제를 해결하기 위해 C++에서는 반환 시 const 형의 레퍼런스를 반환하여 사용자가 수정할 수 없도록 처리 했다

 

class Group

{

public:

//- 빌트인 타입( int, double ......)

int GetID() { return m_nID; }

void SetID(int nID) { m_nID = nID; }

 

//- 객체 타입

const Item& GetItem() { return m_item; }

protected:

int m_nID;

Item m_item;

};

 

위와 같은 심플 객체 외에 List 와 같은 자료구조가 속성인 경우 Getter/Setter를 만드는 것은 대단히 위험한 일 중의 하나였기때문에 필자같은 경우 만든 사례가 없었으며 그런 자료구조를 통채로 넘겨 받아야 하는 구조라면 구조가 잘못된 것이니 고쳐야지 코딩의 기법으로 해결할 문제는 아니라고 생각한다. 자료구조에 접근하는 것은 자료구조를 가지고 있는 클래스에게 요청하여 하나씩 가져오도록 구혔했다.

 

  • C#에서의 캡슐화 적용 방법에 대한 고민 1 - 단순 객체

c#을 공부하면서 이놈 한테는 const가 없고, 모든 클래스는 레퍼런스로 반환되어 Getter를 만드는 것이 상당히 조심스러웠다. 어설프게 Getter를 만들면 넘겨받은 객체가 뭔짓을 해도 막을 방법이 없게 되고, 자신의 속성임에도 불구하고 제어권이 넘어가 버린다. 따라서 개발을 진행하면서 가급적 Setter/Getter를 만들지 않고 꼭 필요한 경우 Getter는 Clone 객체를 생성해서 반환하는 식으로 구현하였다.

 

public class Test2

{

protected Item _slectedItem;

 

public Item SelectedItem

{

get { return _selectedItem.Clone(); } 

//set { _selectedItem = value; }

}

 

}

 

  • C#에서의 캡슐화 적용 방법에 대한 고민 2 - MVVM 을 위한 콜렉션 객체 반환

C#을 이용한 WPF 프로그래밍 시 MVVM 을 이용하여 UI와 모델을 분리하고 Binding 하는 것은 가장 중요한 개발 패러다임의 하나이다. 필자는 이 매커니즘이야 말로 WPF 의 핵심 개발 아키텍처라고 생각한다. 하지만 이때문에 C++과는 다르게 내부의 콜렉션을 넘겨줄 필요성이 있었다.

콜렉션 객체를 클론하여 넘겨주는 것은 부담이 컸고, 그대로 넘겨 버리면 넘겨 받은 쪽에서 마음대로 객체를 추가해도 막을 수 없게 된다. 콜렉션이 가지고 있는 요소들의 변형은 어찌 되었던 간에 최소한 콜렉션 자체가 가지고 있는 추가 삭제 기능만은 막아야 한다고 생각 했다. 한동안 고민을 지속 하다가 콜렉션을 반환할 때 껍데기인 콜렉션을 배열로 변경해 버리는 방식으로 처리했다.

 

public class Test2

{

protected List<Item> _items = new List<Item>();

 

//- 이렇게 넘겨주면 넘겨받은 쪽에서 Items를 통해 마음껏 객체를 추가할 수 있게 된다.

public List<Item> Items1

{

get { return _items; } 

}

 

//- 이렇게 넘겨주면 넘겨받은쪽에서 루프를 이용해서 사용할 수 있지만 객체를 추가할 수 없게된다.

public Item[] Items2

{

get { return _items.ToArray(); }


}

 

}

 

  • 남아있는 과제

Items2 와 같은 방식으로 변경하면 객체를 추가 삭제 할 수는 없지만 MVVM에 필수적인으로 활용되는 ItemsSource 로 binding 할 수 있게된다. 아직은 완전히 해결된 것은 아니다 콜렉션이 가지고 있는 Item의 변형을 막을 방법은 없기 때문이다. 자 캡슐화를 이정도까지 하면 될까? 대부분의 경우 이정도면 충분하겠지만 item의 변경까지 막을 방법을 생각해 봐야겠다.

물론 보다 디자인적인 접근으로 Setter의 작성은 언제나 최소화해야 할 것이다. 완벽한 설계는 더이상 넣을 것이 없을때가 아니라 뺄것이 없을 때가 아니겠는가.

 

+ Recent posts