
Paul DiLascia (영문)
이번 호에서는 이번 호(MSDN 2005년 4월호)에서 저의 칼럼("Wrappers: Use Our ManWrap Library to Get the Best of .NET in Native C++ Code (영문)")에서 설명한 RegexWrap 라이브러리를 사용해서 만든 재미있는 응용 프로그램에 대해서 설명할 까 합니다. RegexForm는 MFC에 기반한 정규식을 이용한 폼의 데이터 유효성을 검사하는 시스템입니다. 여기서의 응용 프로그램의 목적은 RegexWrap의 구현에 있습니다. 그러나, 여기서의 설명이 정규식 자체와는 크게 연관되어 있지 않고, RegExForm자체에 중점을 두고 설명 드리도록 하겠습니다.
제가 위에 있는 글에서 언급했듯이, 정규식을 이용하는 많은 프로그램들의 실제적인 목적은 사용자 입력의 유효성을 검사하는 일입니다. 정규식을 이용하면, 우편 번호, 전화번호, 신용카드 번호 등- 모든 종류의 실생활 정보를 검사하는 일을 쉽게 해 줍니다. 하나의 정규식은 수십 혹은 수백 라인의 프로그램 코드를 대체할 수 있습니다. 유닉스와 펄 같은 웹 프로그래밍 언어들은 시작했던 날부터 정규식을 사용할 수 있었지만, Windows 혹은 MFC 에서는 .NET 프레임웍이 출현한 요즘에도 다른 회사에 제공하는 라이브러리를 이용하지 않고는 정규식을 사용할 수 없습니다. [편집자 주 - 3/15/2005: ATL서버 라이브러리에서 CAtlRegExp 와 CAtlREMatchContext와 같은 클래스들을 정규식을 지원하기 위해 제공하고 있습니다.] .NET의 경우는 정규식을 완전히 지원하는 라이브러리를 제공하는데, 왜 MFC 기반의 응용 프로그램에서는 아직도 정규식을 사용할 수 없는 걸까요? 이 글에서 묘사하는 RegexWrap 라이브러리를 사용하면, 정규식을 지원하기 위해서 Managed Extension이나 /clr 옵션도 필요치 않습니다.
MFC는 이미 다이얼로그 데이터 교환 (Dialog Data Exchange 이하 DDX)를 통해서 이미 사용자의 입력을 유효성을 검증할 수 있는 메커니즘을 가지고 있습니다. 기술적으로, DDX는 스크린과 사용자 입력간에 데이터를 전달해주고, 데이터의 유효성을 검증합니다. DDX는 여러분의 다이얼로그의 OnOK 핸들러에서 UpdateData를 호출했을 때 시작됩니다:
// 사용자가 OK 버튼을 클릭했을 때:
void CMyDialog::OnOK() {
UpdateData(TRUE); // 다이얼로그의 데이터를 얻어온다
...
}
UpdateData는 CWnd의 가상함수이고, 개발자가 특정 다이얼로그 클래스 안에서 오버라이드(Override)할 수 있습니다. 이 함수의 불린(Boolean) 인자는 스크린에서 다이얼로그 객체로 혹은 그 반대로 데이터를 복사할 지를 말해주는 역할을 합니다. (여러분은 UpdateData(FLASE)를 OnInitDialog에서 여러분 다이얼로그를 초기화하기 위해서 호출할 수 있습니다.) 기본 CWnd의 구현은 CDataExchange 객체를 생성해서, 각각의 데이터 멤버에 대해서DoDataExchange라는 다른 가상 함수에 전달해주고, 개발자는 각각의 데이터 멤버에 대해서 데이터를 전송하기 위해서 각기 다른 고유한 DDX 함수를 호출하기 위해서 이 함수를 오버라이드 해야 합니다:
void CMyDialog::DoDataExchange(CDataExchange* pDX) {
CDialog::DoDataExchange(pDX);
DDX_Text(pDX, IDC_NAME, m_name);
DDX_Text(pDX, IDC_AGE, m_age);
...
// 기타.
}
위에서의 IDC_NAME과 IDC_AGE는 각 에디트 컨트롤의 ID이며, m_name과 m_age는 CString형과 정수형의 데이터 멤버들 입니다. DDX_Text는 사용자가 입력한 이름과 나이를 m_name과 m_age에 복사합니다. (오버로드를 통해서 Age를 정수형으로 바꾸도록 해서 데이터를 복사를 가능하게 합니다). CDataExchange::m_bSaveAndValidate변수가 TRUE이면 스크린에서 다이얼로그로, FALSE이면 반대 방향의 데이터 복사를 의미하기 때문에, DDX 함수들은 문제없이 데이터를 전송해야 하는 방향을 파악할 수 있습니다. MFC는 모든 종류의 데이터와 컨트롤 타입에 대한 많은 DDX 함수들을 가지고 있습니다. 예를 들어서, DDX_Text는 데이터를 복사하고, CString, 정수형, 부동소수점형, COleCurrency 등등 많은 자료형에 특화된 적어도 수십 개의 함수를 가지고 있습니다. 체크박스의 상태를 정수형으로 변환하기 위한 DDX_Check 함수와, 라디오 버튼에 대해서 동일한 일을 수행하는 DDX_Radio라는 함수도 있습니다.
DDX 함수가 데이터를 전달한다면, DDV 함수들은 이 데이터들의 유효성을 확인하는 일을 합니다. 예를 들어서, 사용자의 이름을 35자리로 제한하기 위해서, 보통은 개발자라면 아래와 같이 코드를 작성할 것 입니다:
// CMyDialog::DoDataExchange 안에서 DDX_Text(pDX, IDC_NAME, m_sName); // 값을 가져오거나 설정합니다 DDV_MaxChars(pDX, m_sName, 35); // 유효성을 검사합니다
그리고, 사용자의 나이를 1에서 120 사이로 제한하기 위해서, 보통의 개발자들은 아래와 같은 코드를 작성할 것 입니다:
// m_age 는 정수형입니다 DDX_Text(pDX, IDC_AGE, m_age); DDV_MinMaxInt(pDX, m_age, 1, 120);
DDX가 잘 작동함에 비해서, DDV는 약간 원시적이라고 할 수 있습니다. MFC는 수행할 수 있는 유효성 검사의 방법이 제한되어 있습니다. 개발자는 텍스트 필드에서 글자의 최대 수를 제한하거나, 여러 가지 자료형에 대해서 최대/최소 제한을 할 수 있습니다. 최소/최대는 나쁘지 않습니다, 하지만 우편번호나 전화번호의 유효성을 확인하고 싶을 때는 어떻게 해야 할까요? MFC에서는 이것에 대해서 쓸 수 있는 방법이 없습니다. 개발자가 개발자 자신의 DDV 함수를 작성해야 합니다. 제가 정규식을 사용해서, 데이터 유효성 검사를 처음으로 구현했을 때, 제가 한 것은 아래와 같은 한 가지 함수를 작성한 것뿐이었습니다:
void DDV_Regex(CDataExchange* pDX, CString#160; val, LPCTSTR pszRegex)
{
if (pDX->m_bSaveAndValidate) {
CMRegex r(pszRegex);
if (!r.Match(val).Success()) {
pDX->Fail(); // 예외를 던집니다
}
}
}
이것은 정규식을 사용해서, 쉽게 아래와 같이 사용자의 입력의 유효성을 검사하게 할 수 있게 해 줍니다:
// CMyDialog::DoDataExchange 안에서
DDX_Text(pDX, IDC_ZIP, m_zip);
DDV_Regex(pDX, m_zip,_T("^\\d{5}(-\\d{4})?$"));
단 네 줄의 코드 치고는 상당히 괜찮지 않습니까? (물론, 이것은 여러분이 RegexWrap 라이브러리를 사용한다는 가정하의 구현입니다-그렇지 않으면, 여러분은 managed extension에서 직접 .NET 프레임웍의 Regex 클래스를 직접 호출해야 합니다.) DDX_Regec는 MFC의 DDX/DDV 함수들 안에서 상당히 잘 동작했고, 저는 여러 가지 필드들을 더하기 시작하면서, DDX/DDV 함수를 사용했을 때 발생하는 큰 단점을 발견하게 되었습니다. 하나를 예로 들자면, 각각의 DDV 함수들은 에러 메시지 박스를 표시한 뒤에 만약, 필드가 개발자의 의도와 일치하지 않으면, exception을 던집니다. 그래서, 만약 5개의 필드에 문제가 있다면, 사용자는 다섯 개의 메시지 박스를 보게 됩니다. 게다가, 저는 DDV 호출 안에 정규식을 하드코딩하기를 원하지 않았습니다. 그러나 DDX/DDV를 사용하는 것에 반대하는 저의 이유 중의 하나는 이 함수들을 사용하는 과정이 너무 절차적이라는 데에 있습니다. 새로운 필드를 검증하기 위해서, 개발자는 새로운 데이터 멤버를 추가하고, DoDataExchange안에 코드를 좀 더 작성해야 합니다. 이러한 코드의 추가는 결국에는 아래와 같이 두서없이 긴 코드가 될 확률이 큽니다:
DDX_Text(pDX, IDC_FOO,...); DDV_Mumble(pDX, ...) DDX_Text(pDX, IDC_BAR,...); DDV_Bletch(...) ...
왜 프로그램 실행 시에 동적으로 변하지 않는 데이터의 유효성을 검증하는 법칙을 설명하기 위해서 이러한 절차적인 코드를 작성해야 할까요? 저의 프로그래밍의 원칙 중 두 가지를 잠시 설명 드리자면: 첫 번째가 절차적인 코드를 무조건 피해라, 그리고 다른 하나는 하나의 테이블은 천 라인의 가치가 있다 입니다. 이 글을 읽는 독자 분들은 쉽게 저의 이 두 개의 원칙이 위의 코드에서 보여진 문제점을 해결하는 시작점이라는 것을 짐작하실 수 있을 실 겁니다. 그리고, 저는 규칙에 기반한 그래서 테이블에 기반한 데이터 유효성을 검사하는 시스템을 끝내는 작성할 수 있었습니다. 이 시스템은 DDX 위에서 동작하면서, DDV의 사용을 피하고, 그래서 훨씬 더 보기 좋은 사용자 인터페이스를 가지고 있습니다. 물론, 사용하기도 쉽고, 유효성 검사를 하기 위해서 정규식을 사용합니다. 모든 것은 CRegexForm 클래스에 캡슐화되어, 여러분은 어떤 MFC 다이얼로그에서도 사용하실 수 있습니다.

그림 1 TestForm 툴팁 힌트
저는 이 클래스가 어떻게 동작하는지 테스트하기 위해서, 테스트 프로그램을 만들었습니다. 처음 눈으로 보기에 TestForm 응용 프로그램은 흔히 볼 수 있는 MFC 다이얼로그에 기반을 둔 응용 프로그램처럼 보입니다. 메인 다이얼로그는 몇 개의 에디트 필드가-우편번호(Zipcode: 역자주-미국의 우편번호는 일반적으로는 9자리의 체계를 이용하고 있습니다. 이 중 뒤의 4자리는 실제로는 생략가능하여, 실제로는 5자리의 우편번호가 널리 쓰이고 있습니다. ), 주민번호(SSN), 전화번호, 그리고 기타 등등의 필드가 있습니다. 그러나, 여러분이 TestForm을 테스트할 때, 생각하시는 것 이상의 무언가가 있다는 사실을 바로 알 수 있습니다.
입력 필드에 포커스를 줄 때, TestForm은 그림 1에서 보는 바와 같이 입력할 수 있는 데이터를 묘사하는 툴팁을 표시해 줍니다. 그리고, 만약 잘못된 문자-예를 들어서, 전화번호에 문자 값 같은-TestForm은 그 문자 값을 거부하고 경고음을 울립니다. 여러분이 Enter 키를 치거나 OK 버튼을 눌렀을 때, 그림 2와 같이 잘못된 필드 값들에 대한 경고 메시지를 볼 수도 있습니다. 앞에서 각각의 에러에 대해서 각각의 메시지 박스가 나왔던 것과는 다르게, 모든 에러는 하나의 메시지 박스에서 보여집니다. 그리고, 사용자가 이러한 잘못된 필드를 선택하면, TestForm은 그림 3과 같이 다이얼로그 안에 응용프로그램이 제공한 피드백 윈도우에 에러 메시지를 다시 표시해 줍니다. 그래서, 사용자가 에러 메시지를 기억하지 않아도 되게끔, 폼이 사용자에게 각각의 잘못된 필드 값을 고칠 때 마다 에러 메시지를 다시 확인하게 해 줍니다. 그리고 만약 단 하나의 필드 값만이 잘못되었다면, TestForm은 메시지 박스를 출력하는 대신 피드백 윈도우에 에러 메시지를 출력해 줍니다.

그림 2 잘못된 여러 개의 필드 값을 보여주는 메시지 박스
CRegexForm 는 이러한 마술 같은 일을 이 클래스 하나로 해결하고 있습니다. 여러분이 해야 하는 일은 사용법이 단순 명료한, 이 클래스를 사용하는 일 입니다. 처음에, 여러분은 여러분의 폼을 정의해야 합니다. 아래에서 TestForm에서는 MainDlg.Cpp에서 어떻게 정의했는지가 나와 있습니다:
// 폼/필드 맵 BEGIN_REGEX_FORM(MyRegexForm) RGXFIELD(IDC_ZIP,RGXF_REQUIRED,0) RGXFIELD(IDC_SSN,0,0) RGXFIELD(IDC_PHONE,0,0) RGXFIELD(IDC_TOKEN,0,0) RGXFIELD(IDC_PRIME,RGXF_CALLBACK,0) RGXFIELD(IDC_FAVCOL,0,CMRegex::IgnoreCase) END_REGEX_FORM()
위의 매크로들은 각각의 에디트 필드를 정의하는 스태틱 필드(Static Field)를 정의합니다. 거의 대부분의 경우에서, 이 매크로가 필요로 하는 것은 컨트롤 ID이지만, 플래그와 RegexOptions을 위한 공간도 있습니다. 예를 들어서, TestForm에서 우편번호는 반드시 요구되고(RGXF_REQUIRED), 소수를 입력하는 필드는 콜백을 사용하고, 가장 좋아하는 칼럼리스트(IDC_FAVCOL) 필드는 대소문자를 가리지 않는다(CMRegex::IgnoreCase)고 명시해 놓았습니다.

그림 3 Matt Pietrek를 제일 좋아하신다 구요? 저는 그렇게 생각하지 않습니다!
테이블을 보면서, 여러분은 정규식은 어디에 존재하는지 의아해 할 것입니다. 대답은: 리소스 파일에 있습니다. 각각의 필드/컨트롤 아이디에 대해서, CRegexForm는 똑 같은 ID의 리소스 스트링을 필요로 합니다. 리소스 문자열은 ‘\n’으로 구별되는 다섯 개의 문자열로 구성되어 있습니다. 일반적인 포맷은 "이름\n정규식\n정규식에서 쓰이는 문자들의 정규식\n힌트\n에러메세지" 로 구성되어 있습니다. IDC_ZIP에 대한 이러한 리소스 문자열은 아래와 같습니다:
"Zip Code\n^\\d{5}(-\\d{4})?$\n[\\d-]\n##### or #####-####"
처음에 나오는 문자열 “Zip Code”는 필드 이름입니다. 두 번째는 "^\d{5}(-\d{4})?$,"는 유효성 검증을 위해 사용되는 정규식입니다. 세 번째는 정규식에서 사용되는 문자 값들을 나타내는 또 다른 정규식입니다. 예를 들어서 우편 번호의 경우는, 숫자나 하이픈(-)만을 허용하는 “[\d-]”가 사용됩니다. 만약, 문자 값의 제한이 없다면 개행 문자를 두 번 타이핑(“\n\n”)해서 이 부분을 생략할 수도 있습니다. 네 번째 문자열은 툴팁의 힌트입니다. 마지막으로, 특정 필드 값이 잘못 되었을 때 에러 메시지가 표시되기를 원한다면, 에러 메시지를 다섯 번째 인자로 추가할 수 있습니다. 여기서의 우편 번호의 예에서는, 에러 메시지를 포함하지는 않았습니다. 이렇게 에러 메시지가 없는 경우는, CRegexForm이 “Should be xxx”라는 메시지를 생성하고, 여기서의 xxx는 힌트로 대체되어 보여지게 됩니다. 이러한 문자열 값들 중에서, 실제로는 제일 처음의 필드 이름만이 반드시 요구되는 인자입니다.
왜 필드 맵에 직접 코딩 하지 않고, 이렇게 리소스 문자열을 사용하는 것 일까요? 한 가지만 말씀 드리자면, 이 모든 정보를 맵에 직접 코딩 하는 것은 여러분의 코드를 상당히 지저분하게 만들 것 입니다. 그리고 직접 코딩을 하게 되면, 매크로들이 옵션 인자를 가질 수 없게 됩니다. 그렇게 되면 개발자들은 얼마나 많은 인자를 사용하는가에 따라서, RGXFIELD3, RGXFIELD4, 그리고 RGXFIELD5같은 여러 개의 매크로들이 필요하게 됩니다. 그러나 리소스 문자열이 필요한 진짜 이유는 지역화를 쉽게 하기 위해서 입니다. 문자열 리소스를 사용하게 되면, 번역하는 사람들이 문자열을 번역해서 다른 로케일에 맞게 리소스 DLL을 생성할 수 있게 됩니다. 정규식 자체도 지역화가 필요하기 때문에, (예를 들어 우편번호의 경우 한국과 미국의 경우 서로 다른 체계를 사용하기 때문에, 다른 정규식이 필요합니다.) 이러한 정규식 역시 리소스에 들어가는 편이 좋습니다.
잠시 정규식을 이용해서 이러한 부분 문자열(substring)을 파싱하는 것이 얼마나 쉬운 일인지 살펴보도록 하겠습니다. MFC는 Document의 부분 문자열을 파싱하기 위해서, AfxExtractSubString이라는 26줄의 코드를 가진 함수를 가지고 있습니다. 그러나 CRegexForm은 CMRegex 클래스를 이용해서 이 것을 한 줄에 할 수 있습니다.
CString str;
str.LoadString(nID);
vector<CString> substrs = CMRegex::Split(str, _T("\n"));
위와 같은 코드를 실행하면, substrs[i]에 i번째 부분 문자열이 있고, substrs.size()를 이용하면, 얼마나 많은 부분 문자열이 존재하는지 알 수 있습니다.
일단 BEGIN/END_REGEX_FORM을 사용하여 필드 맵을 정의하였고, 리소스 문자열에 필요한 정보를 다 입력했다면, 다음에 할 일은 여러분의 다이얼로그에서 CRegexForm 클래스를 인스턴스화해서 초기화 해야 합니다:
// in OnInitDialog m_rgxForm.Init(MyRegexForm, this, IDS_MYREGEXFORM, MYWM_RGXFORM_MESSAGE);
당연히, CRegexForm 클래스는 필드 맵과 다이얼로그에 대한 포인터가 필요합니다; 두 번째와 세 번째는 인자는 다른 문자열 리소스와 콜백 메시지 ID 입니다. 다른 각 필드 문자열처럼, 초기화 문자열도 여러 개의 부분 문자열들이 개행문자로 구별되어 구성되어 있습니다. TestForm 의 경우, IDS_MYREGEXFORM은 "Error: %s\nRequired\nShould be: %s\nBad Value" 입니다. 처음의 부분 문자열 “Error: %s”는 에러에 대한 접두사(prefix)입니다. CRegexForm은 이 접두사를 “Error: xxx”라는 형식으로 에러 메시지를 표시하기 위해서 사용합니다 (위에서의 xxx는 실제의 에러 메시지 입니다). 두 번째 부분 문자열, “Required”는 반드시 요구되는 필드 (RGXF_REQUIRED)에서 사용되는 “단어/구문” 입니다. 세 번째 부분 문자열, "Should be: %s" 는 제가 위에서 이미 묘사했습니다. CRegexForm은 이 문자열을 에러 메시지 “Should be: xxx”를 생성하기 위해서 사용합니다 (여기서의 xxx은 필드의 힌트 문자열 입니다.) 마지막의 부분 문자열, “Bad Value,”는 CRegexForm가 특정 필드의 힌트 혹은 에러 메시지가 존재하지 않을 때 사용하는 모든 에러에 대한 메시지 입니다. 프로그램 사용자들은 이 글을 읽는 개발자 분들이 모든 필드에 대해서 힌트 나 에러 메시지를 제공해서, 이러한 메시지를 보지 않아야 합니다.
가장 마지막 인자, MYWM_RGXFORM_MESSAGE,는 CRegexForm 클래스가 여러분의 응용 프로그램에 특수한 절차적인 코드가 요구되는 유효성 검사 등을 할 때 사용되는 응용 프로그램에서 정의한 콜백 메시지 아이디입니다. 만약 여러분이 수학적인 알고리즘을 사용하거나, 사용자의 입력을 검증하기 위해서 특별한 작업이 필요하다면, 여러분은 RGXF_CALLBACK을 필드 플래그에 설정하여 CRegexForm이 유효성 검사를 할 때 여러분의 다이얼로그에게 notification 코드가 RGXNM_VALIDATEFIELD로 설정된 콜백 메시지를 보내도록 할 수 있습니다. TestForm은 소수 필드의 값을 검증하기 위해서 콜백을 사용하고 있습니다; 그림 4를 참고해 보시기 바랍니다.
CRegexForm은 여러분이 각각의 텍스트 필드에 대해서 데이터 멤버를 정의하지 않아도 되도록, 내부적으로 CString을 이용하여 DDX를 하고 있습니다. 여러분이 해야 하는 일은 CRegexForm을 호출하여 데이터를 전송하게 하는 것 입니다.
void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
m_rgxForm.DoDataExchange(pDX);
}
여러분이 CRegexForm을 초기화할 때, 내부적으로는 여러분의 맵에 대한 각각의 필드에 대해서, 외부 클래스에서 값을 참고할 수 없는 FLDINFO 구조의 배열을 할당합니다. FLDINFO 멤버들 중 한 가지는 FLDINFO::val 이고, 현재 필드 값을 가지고 있는 CString 형의 변수입니다. 여러분의 CRegexForm::GetFieldValue 혹은 SetFieldValue를 통해서 내부 값을 얻어 오거나 할당할 수 있고, 두 개의 함수 모두 필드를 구별하기 위해서 ID를 필요로 합니다.
m_rgxForm.SetFieldValue(IDC_ZIP,_T("10025"));
CRegexForm은 모든 값을 텍스트로 취급하고, CString 형의 값으로 저장하지만, GetFieldValueInt 혹은 GetFieldValDouble 메써드를 통해서 값을 정수형 혹은 부동소수점 형으로 얻어올 수 있습니다. 다른 자료형에 대해서는, 여러분이 직접 변환을 해줘야 하거나-DoDataExchange 안의 MFC DDX 함수를 쓸 수 있습니다. TestForm은 그림 3에서 보여지는 것과 같이 CRegexForm::SetFieldValue 함수를 통하여 샘플 값으로 폼을 채우게 하는 Populate 버튼이 있습니다.
일반적으로, CRegexForm은 컨트롤 아이디로 필드를 사용하여 필드를 구별합니다. 이 클래스는 GetFieldName, GetFieldHint, 그리고 GetFieldError 함수를 이용하여 필드의 이름, 힌트, 그리고 에러 코드를 얻어오고-이 모든 함수들이 컨트롤 아이디를 파라미터로 받습니다.
지금까지, 저는 여러분께 어떻게 필드맵의 생성, 리소스 문자열 작성 방법, CRegexForm의 초기화 방법, DDX와의 연동에 대해서 보여 드렸습니다. 이제 남아 있는 것은 실제로 사용자의 입력의 유효성 검사하는 일 입니다. 이러한 일을 하는 때는 사용자가 OK를 눌렀을 때입니다:
void CMyDialog::OnOK()
{
UpdateData(TRUE); // copy screen->dialog
int nBad = m_rgxForm.Validate();
if (nBad>0) {
m_badFields = m_rgxForm.GetBadFields();
...
}
UpdateData는 MFC의 DDX 메커니즘을 이용하여, 여러분의 다이얼로그의 DoDataExchange를 호출하게 됩니다. DoDataExchange는 그리고 나서 CRegexForm::DoDataExchange를 호출해서, 사용자의 입력을 내부의 FLDINFO 구조체에 복사합니다. 그 후에 CRegexForm::Validate는 필드들을 검사해서, CMRegex::Match 를 호출하여 각각의 필드 값이 그 필드에서 요구하는 정규식과 일치하는지 일일이 확인합니다. 만약 특정 필드 값에 이상이 있다면, CRegexForm은 내부의 FLDINFO 구조체에 RGXERR_NOMATHC 에러 코드를 설정하고, 꼭 정보가 필요한 필드의 값이 비어 있을 때 RGXERR_MISSING을 에러 코드로 설정하게 됩니다. 유효성 검사는 이상이 있는 필드들의 수를 반환하고, 하나 이상의 에러가 존재할 때, 여러분은 CRegexForm::GetBadFields를 호출하여 이상이 있는 필드 ID들의 배열(STL 벡터)를 얻어올 수 있습니다. 그리고 나서, 각각의 에러 코드와 에러 메시지를 얻기 위해서 배열을 순환 반복할 수 있습니다. 이러한 일들이 TestForm에서 CMainDlg가 그림 2에서 볼 수 있는 에러 메시지 박스를 만들기 위해서 하는 일들 입니다. 만약 단 하나의 필드 값에 이상이 있다면, CMainDlg는 필드를 하이라이트 하고, 그림 3 에서와 같이 에러 메시지를 피드백 윈도우에 표시해 주기 위해서 CRegexForm::ShowBadField를 호출합니다. 만약 모든 필드 값이 정상적이라면, TestForm은 사용자가 입력한 모든 값들을 보여주는 메시지 박스(그림 5)를 출력하게 됩니다. 실제 프로그램에서는, 최종적인 목적을 위해서 이 값들을 어디론가 복사하게 될 것 입니다. 그림 6은 CMainDlg::OnOk의 전체 코드를 보여주고 있습니다. 데이터 교환과 데이터 유효성 검사를 분리함으로써, CRegexForm은 여러분의 UI를 자유 자재로 컨트롤 할 수 있게 해주고, MFC에 하드코딩 된 에러 메시지의 사용을 피하게 하고 있습니다.

그림 5 입력된 데이터의 확인
저는 위에서 피드백 윈도우에 대해서 잠깐 언급했었습니다. CRegexForm은 전적으로 이 윈도우를 관리하고 있습니다; 여러분이 단 한 가지 해야 할 일이 있다면, CRegexForm::SetFeedBackWindow 함수의 호출을 통하여 피드백 윈도우를 제공해 주는 일입니다. 여러분은 에러 메시지에 대한 색깔까지도 설정할 수 있습니다. CRegexForm은 힌트까지도 관리합니다. 기본으로, 이 클래스는 사용자가 컨트롤을 활성화 시켰을 때만 힌트를 보여 줍니다(그림 1 참고). 개발자는 CRegexForm::SetShowHints(FALSE)의 호출을 통하여 이 기능을 잠시 중지시킬 수 있습니다. SetShowHints(TRUE, nDelay, nTimeout) 함수는 이 툴팁을 다시 보여줄 수 있게하고, 여기서 nDelay는 힌트를 보여주기 이전에 대기하는 밀리 세컨드 시간 값이고(기본 값은 250입니다), nTimeout은 힌트를 사용자에게 보여주는 시간을 의미하며, 그 단위는 밀리세컨드입니다 (기본 값은 0이고, 계속 힌트를 보여줍니다.). CRegexForm은 자동으로 컨트롤이 포커스를 잃었을 때(EN_KILLFOCUS) 자동으로 힌트를 숨기게 합니다. TestForm은 힌트를 보여주거나 (그림 1 참조)혹은 숨기게 하는데 SetShowHints라는 함수를 사용합니다.
또 한 가지 말하기가 약간 망설여지는 기능이 있습니다. CRegexForm은 입력 즉시 사용자가 제공한 값의 유효성을 검사하는 옵션도 있습니다. 저는 이 기능을 권장하고 싶지는 않습니다. 왜냐하면, 이 기능은 GUI의 디자인에 있어서 좋지 못하다고 생각하기 때문입니다. 그러나, 때때로는 사용자가 제공한 값을 그 자리에서 바로 확인하셔야 할 때도 있을 것 같습니다. 그런 경우에는, 필드의 플래그에 RGXF_IMMED 필드를 설정하시거나, 모든 필드에 대해서 즉각적인 유효성 검사를 하게 하려면, SetValidateImmed 함수를 호출하시기 바랍니다. TestForm은 이러한 즉각적인 유효성 검사 기능을 조정할 수 있는 체크 박스가 있습니다. 만약, 여러분이 이 체크박스를 선택하면, 왜 이러한 즉각적인 유효성 검사가 그 다지 좋지 못한 생각인지 아시게 될 것 입니다.
마지막으로, 최대/최소와 글자 길이의 제한에 대해서 알아 보도록 하겠습니다. 최대/최소는 정규식 자체만으로는 할 수 없는 일 입니다. 그리고, ".{0,35}"이 모든 문자열의 길이가 최대 35자의 글자라는 것을 묘사하는 정규식이고, 여러분이 EM_LIMITTEXT를 이용하여 에디트 컨트롤의 글자 길이를 제한해서, 사용자가 너무 많은 문자를 입력 시에 컨트롤이 경고음을 출력하게 하고 싶으실 것 입니다. 저는 개인적으로 MFC에서 이미 지원되는 기능을 지원할 수 없는 폼 유효성 검사 시스템을 만드는 것을 참을 수 없기 때문에, 저는 의사 정규식(pseudo regular expressions)의 개념을 고안하였습니다. 예를 들어서, IDC_AGE의 정규식은 "rgx:minmax:int:1:120,maxchars:3" 이와 같이 됩니다. 당연히, 이것은 진짜 정규식은 아닙니다. 그러나 CRegexForm은 이 의사 정규식을 해석하고 그것 자체를 해석할 수 있습니다. 일반적인 포맷은"rgx:expr,expr,..expr"와 같고 각각의 표현은 서로 다른 제약 조건을 묘사합니다. 현재로는 단 두 개만이 지원됩니다: "minmax:자료형:minval:maxval" (여기서의 자료형은 정수형과 부동소수점형이 지원됩니다.) 와 "maxchars:최대글자수" 가 바로 그것입니다. CRegexForm은 이러한 의사 정규식(Pseudo Regular Expression)을 특별하게 해석해서, machars에 대해서는 DDV_MinMaxInt와 같이 EM_LIMITTEXT를 사용합니다. 자세하게는, 소스코드를 받아 보시기 바랍니다. 문자열 리소스를 이용한 정규식들은 이러한 또 다른 표현을 해석하는 일을 아주 쉽게 해줍니다.
저는 여기서 CRegexForm이 할 수 있는 모든 일을 보여 드렸습니다. 어떠셨나요? 이 지면을 통해서 전체를 다 설명 드릴 수는 없지만, 대략의 중요한 큰 그림을 보여 드릴 수 있었습니다. CRegexForm은 CSubclassWnd를 사용하여 여러분의 다이얼로그를 서브클래싱 합니다. 이 클래스를 보지 못한 독자 분들은, CSubclassWnd은 제가 아주 옛날에 서브클래싱을 할 때 사용할 목적으로 만든 클래스입니다. 이 클래스의 가장 큰 장점은, 여러분이 만든 코드의 계층에 새로운 코드를 끼워 넣지 않아도, 서브클래싱이 가능하다는 점 입니다. 저는 CDialog을 상속받아 CRegexForm을 만들 수도 있었습니다, 그러나 저는 여러분이 CRegexForm을 상속받아 여러분의 다이얼로그를 만드는 것을 보고 싶지 않았습니다. 만약에 여러분이 CDialog로부터 상속을 받아 CBetterDialog을 구현했다면 어떨까요? 이 경우에는, CRegexForm을 끼워 넣기 위해서 약간의 추가적인 작업이 더 필요하고, 결과는 예측 불능입니다.
이것이야 말로, MFC의 가장 큰 단점 중의 하나 입니다: 이 MFC는 서브클래싱을 구현하기 위해서 상속을 사용해서, 클래스의 계층도 측면에서 정확히 윈도우 서브클래싱을 반영합니다. 그러나, 실제로는 이렇게 할 필요가 없습니다. 많은 경우 클래스의 계층을 수정하지 않고 여러분의 다이얼로그를 서브클래싱하는 CRegexForm같은 플러그인 클래스를 작성하는 편이 더 선호됩니다. 저의 경우, 이 CSubclassWnd와 불가분의 관계이고, 이 클래스 없이 저는 프로그래밍을 할 수 없을 정도 입니다!
CRegexForm은 EN_KILLFOCUS와 EN_SETFOCUS를 해석해서 힌트를 보여주거나 숨기기 위해서, 사용자가 무엇인가를 입력하자 마자 필드의 에러를 제거할 목적으로EN_CHANGE를 사용하고, 이것을 위해서CSubclassWnd를 사용합니다. CRegexForm은 저의 200년 9월 2000 (영문) 칼럼에서 설명되었던 CPopupText 클래스를 사용해서 힌트를 구현하고 있습니다. 사용자에게 금지된 문자를 타이핑하는 것을 방지하기 위해서, CRegexForm은 또 다른 CSubclassWnd에 기반한 훅을 LegalChars 정규식이 있는 모든 에디트 컨트롤에 대해서 설치합니다. 이 중첩된 클래스는 CRegexForm::CEditHook이고, 에디트 컨트롤에 보내지는 WM_CHAR 메시지를 해석해서 , 허락되지 않는 문자가 입력되었을 때 조용히 문자를 무시하고 경고음을 출력하는데 사용되고 있습니다. 좀 더 자세한 사항은, 이번 호에 있는 저의 글("Wrappers: Use Our ManWrap Library to Get the Best of .NET in Native C++ Code (영문)")에 관련된 소스코드를 다운로드 받아서 확인하실 수 있습니다.
질문이나 이 글에 대한 감상을 cppqa@microsoft.com로 보내주시기 바랍니다.