MFC에서의 데이터베이스에 대해 살펴 봅니다. 흔히 ‘데이터베이스 프로그래밍’이라고 말하는 분야죠. 상당히 자주 접하지만 필자가 생각하기에는 조금 헷갈리는 표현이기도 합니다. 데이터베이스 프로그래밍이라고 한다면 우선 데이터베이스 시스템을 새롭게 만드는 것이 떠오릅니다. 한마디로 오라클이나 MS-SQL 서버같은 DBMS(Database Management System)를 만드는 것이죠(하지만 혼자서는 그런 대형 프로그램을 만들 수 없습니다). 또한 데이터베이스 프로그래밍이라면 SELECT, INSERT 같은 SQL(Structured Query Language)문으로 작성하는 프로시저도 데이터베이스 프로그래밍이라고 해석할 수 있습니다.
그러므로 여기서는 ‘데이터베이스 접속 프로그래밍’이라 하겠습니다. 한마디로 MS SQL 같은 대형 DBMS를 서버로 두고 MFC 애플리케이션이 클라이언트가 되어 접속한 후 자료를 저장하고 꺼내오는 것입니다. 실제 DB 접속 프로그래밍의 기본은 간단하지만 기본 방법 이외에도 아주 복잡하고 힘든 수준까지 다양하게 마련되어 있으며 그 실행 능력도 차이를 보입니다. 또한 미리 준비된 다양한 메쏘드를 통해 꽤 어려운 작업을 아주 쉽게도 구현할 수 있습니다. 아직 사회에 나오지 않은 주니어 개발자라면 솔직히 DB와 별로 친하지 않을 것입니다.
자료구조를 공짜로, DBMS의 이용
전산학과 2학년의 전공필수 과목인 자료구조는중요한 과목입니다. 주로 정렬과 검색을 다루는 과목이지만 열심히 공부해도 정작 현실적으로 큰 도움(?)이 되지 않을 수도 있습니다. 왜냐하면 정렬과 검색, 리스트와 같은 자료구조는 많은 프로그래밍 언어들에서 함수 차원에서 이미 다 제공해주고 있기 때문입니다. 특별한 일, 예를 들면 대용량 서버 같은 프로젝트가 아니라면 개발자가 직접 자료구조를 만들 일이 거의 없습니다. 하지만 공부한 보람을 언젠가 느끼게 될 것입니다.
자료구조의 끝부분에는 거의 대부분 외부 자료구조인 파일이 나옵니다. 실제 운영체제의 기본 역할은 거대한 외부 자료구조 관리자라고도 할 수 있는데 파일 개념에서 보다 발전된 것이 DBMS라고 생각해도 됩니다. 이 말은 교과서에 나오는 외부 자료구조를 전부 공짜로 이용할 수 있다는 것입니다. 삽입, 합병, 정렬, 검색 등 이름만 들어도 학점이 떠오르며 속이 쓰린 부분을 이미 DBMS를 만든 개발자들이 미리 다 구현해 둔 것입니다. 대개 C 언어 수업 시간의 마지막에는 링크드 리스트(Linked List)를 이용한 주소록이 마지막 리포팅으로 나옵니다. DBMS를 이용하면 모든 작업이 다 끝났습니다. DB의 테이블 자체가 링크드 리스트 자료구조 역할을 할 것이며 삽입, 삭제, 정렬, 검색은 SQL문이 해결해 줍니다. 그와 같이 DBMS를 이용하면 프로젝트는 놀랍도록 간편해질 수 있습니다.
SI 성격을 띈 프로젝트라면 각 트랜잭션별로 로그를 남기기 위해서 모든 연산들을 아예 DBMS에게 맡겨두는 경우도 많습니다. 애플리케이션은 어디까지나 UI만 담당하고 업무를 수행하기 위한 함수들은 전부 DBMS에 존재하는 것이죠. 다양한 기법들이 개발되어 속도도 결코 느리지 않습니다. DBMS의 언어인 SQL문도 각 DBMS별로 놀랍게 발전해 애플리케이션 개발자를 깜짝 놀라게 하는 경우가 많습니다.
이번 시간 동안 DB 접속과 이용 방법을 공부하는 것은 바로 이러한 이유 때문입니다. 앞서 말한 것처럼 사실상 초보 개발자가 DB까지 생각하는 경우는 흔하지 않습니다. 무엇보다 DBMS가 필요할 만큼 저장해야 할 자료가 많지 않기도 하구요. 하지만 데이터가 어느 정도 쌓이게 되면 파일이나 레지스트리에 저장하는 것보다 DBMS를 이용하는 것이 더 유리합니다. 만약 데이터베이스를 왜 이용하는지 필자에게 묻는다면 편하게 작업하기 위해서라고 말하겠습니다.
제트 엔진의 파워, DAO
먼저 DAO(Data Access Object)를 살펴보며 그것을 이용한 애플리케이션을 만들어보겠습니다. DAO는 UDA에 속하지 않는 자동화 객체로써 제트 데이터베이스 엔진(Jet Database Engine)과 통신하는 역할을 맡고 있습니다. DAO는 ODBC 이후에 나온 방법으로 쉽게 말하면 로컬에 있는 데이터베이스에 접속할 때 요긴하게 사용할 수 있습니다. 여기에 비해 UDA는 데이터베이스의 물리적 위치(IP 주소)와 무관합니다. 로컬에 있는 데이터베이스라면 무엇일까요? 뭐니뭐니해도 오피스 멤버 중 하나인 액세스이며 mdb 파일입니다. mdb 이외에도 다음과 같은 형식을 지원합니다.
◆ dBase( III, IV, 5.0)
◆ Lotus( .wks, .wk1, wk3, .wk4 )
◆ 엑셀
◆ FoxPro( 2.0, 2.5, 2.6 )
◆ 패러독스( 3.0, 4.0, 5.0, 7.0 )
◆ 컴마 구분 텍스트(.cvs )
지원 포맷들을 보면 개인용 DBMS에 중심으로 지원하는 것을 알 수 있습니다. 이렇듯 DAO는 개인용 DBMS를 위해 로컬에서 최적화되었습니다. 만약 로컬에 있는 mdb 파일을 ODBC로 접근한다면 이는 더 느린 결과를 초래합니다.
자, 그럼 언제나처럼 여러분의 친구이자 필자의 분신인 레아군의 이야기로 다시 흘러갑니다. 레아군의 DAO 프로젝트는 액세스와 VC++ 6.0을 이용할 것입니다.
DAO로 구현하는 주소록 프로젝트
지난 3개월 동안 연재됐던 ‘MFC UI 대작전’을 계기로 사이가 좋아졌나 싶더니 오늘도 유나와 레아군이 다투는군요. 살짝 들어보면 간단한 주소록을 만드는데 개발 속도 면에서 웹 프로그래밍과 VC++로 만든 애플리케이션 중 어떤 방법이 더 빠른지 다투고 있군요. 결국 둘은 마노 씨의 심판 아래 각자 작업에 들어갔습니다. 한 시간을 종료 시간으로 하고 각자의 개발 툴과 언어로 프로그래밍을 하고 있습니다.
“레아군, 나 벌써 다했는데.”
“어머 그럼 유나가 이긴 것이네. 레아군 뭐해요?”
“지, 지금 포인터에서 에러가 나서… 근데 웹에서도 포인터가 되나요? 자료구조 뭐 썼어요? 리스트? 배열?”
“네? 그냥 입력은 Insert, 검색과 정렬은 Select, 삭제는 Delete하면 되잖아요?”
“그, 그게 무슨 말이죠?”
“그냥, 액세스 썼어요.”
“그게 뭐냐니까요?!”
자, 유나는 간단하게 mdb와 ADO를 이용하여 주소록을 만들었습니다. 이에 반해 레아군은 주소록을 위해 링크드 리스트를 만들다가 한 시간이라는 제한 시간을 넘겼군요. 경기 규칙이나 방법론을 논하기 전에 레아군은 지금 자존심에 상처가 갔습니다. DB를 할 줄 모른다는 점 때문이죠. 언젠가 학교에서 Select니 Insert니 말은 들었다고 기억이 어렴풋이 나지만 SQL 수업을 듣고도 그냥 책에서만 봤을 뿐 실제 DBMS에 대한 경험이 없습니다. 이대로 가라앉으면 개발자가 아니죠. 그럼 레아군을 도와 mdb와 MFC, 그리고 DAO로 만든 간단한 예제를 살펴볼까요? 애플리케이션이라면 자료구조를 먼저 정하듯 자료구조 역할을 맡는 DB 테이블을 제작해 봅시다. 역시 액세스를 이용하여 <표 1>과 같은 단일 테이블을 만들었습니다. 물론 임의로 내용도 입력했습니다.
그럼, VC++를 켜고 첫 프로젝트를 만들어 봐야겠죠. 먼저 DaiView라는 프로젝트를 만듭니다. 그리고 프로젝트 형식은 SDI를 선택합니다. 2번째 스텝에서 <화면 1>과 같이 데이터베이스를 선택하라는 메뉴가 나오는군요. 여기에서 3번째 메뉴인 「Database view withoit file support」를 선택하고 아래에 있는 ‘Data Source’ 버튼을 누릅니다. <화면 2>는 해당 버튼을 눌렀을 때 나오는 윈도우로 ODBC, DAO, OLE DB 중 하나를 다시 선택하라고 묻습니다. 이 시간에는 DAO를 공부하는 시간이므로 DAO를 선택해 줍니다.
다음에는 레코드셋 타입이 함께 나옵니다. 레코드셋이란 무엇일까요? 한마디로 SELECT해서 나온 결과들의 집합입니다. 다시 설명하겠지만 데이터베이스 관련 클래스는 연결과 데이터의 조작을 담당하는 클래스, 그리고 결과 집합을 관리하는 레코드셋 클래스로 나뉩니다. 다음의 메뉴는 그러한 결과 집합의 타입을 묻는 것이죠. Snapshot, Dynaset, Table 등 세 가지가 있는데 각각 살펴볼까요?
◆ Snapshot : 변경되지 않는 데이터를 볼 때 스냅샷을 이용합니다. 또한 DAO에서 스냅샷은 항상 읽기 전용입니다. 스냅샷으로 열었다면 절대 수정할 수 없습니다. ODBC라면 사용되는 드라이버에 따라 업데이트가 가능합니다.
◆ Dyanaset : 스냅샷과 반대의 개념입니다. 다른 사용자가 레코드, 즉 데이터베이스를 업데이트하면 즉시 읽고 있는 다른 사용자에게 알려줍니다. 그러나 누군가가 새로이 내용을 추가했다면 다시 질의(SELECT)할 때까지 변경사항을 볼 수 없습니다.
◆ Table : 레코드셋의 결과가 데이터베이스 내의 테이블 하나일 때(바로 지금과 같은 상황) 사용합니다. 다이나셋과 역할은 같지만 조금 더 빠릅니다. 하지만 실제 물리적인 테이블 하나이므로 레코드셋이 뷰(View)거나 저장 프로시저를 참조한다면 허용되지 않습니다.
이와 같이 대부분의 상황에서 디폴트값인 Dynaset을 참조하게 됩니다. 레코드셋을 결정했다면 애플리케이션 위저드의 마지막 스텝를 주목합니다. 바로 <화면 3>인데 기본 클래스가 CDaoRecordView로 설정되어 있습니다. 애플리케이션 위저드를 마치면 일반적인 폼 뷰(Form View) 프로젝트와 비슷한 프로젝트가 만들어집니다. CDaoRecordView의 생긴 모습은 폼 뷰와 똑같지만 이 속에는 놀라운 기능이 들어 있습니다.
<표 1>에서 테이블 내에 필드를 다섯 개 만들었으니 내용을 확인할 에디트박스도 다섯 개 올립니다. 다 올렸나요? 클래스위저드로 곧장 가봅니다. Add Member에서 각각의 컨트롤들에 멤버 변수를 지정해 봅시다. 그런데 눈치 빠른 독자라면 평소와 무언가가 다르다는 것을 느끼지 않았나요. 멤버 변수 입력란이 언제나처럼 에디트박스가 아닌 <화면 4>처럼 콤보박스로 나타납니다. 사용자가 지정하기도 전에 멤버 변수 이름이 있다는 말일 겁니다. 실제로 멤버 변수를 클릭해 보면 놀랍게도 데이터베이스 내 필드들이 MFC 스타일의 멤버 변수가 되어 있고 자동으로 선택할 수 있다는 것을 알 수 있습니다. 왠지 지난 4월호의 메모장 프로젝트가 생각나지 않습니까? 이게 바로 CDaoRecordView와 애플리케이션 위저드의 선물입니다. 정말 코딩 한번 없이 SELECT를 구현할 수 있었습니다. 컴파일을 해보면 레코드셋에서 앞뒤를 이동하는 버튼까지 훌륭하게 자동으로 구현되어져 있습니다.
이렇듯 MFC의 도움으로 코딩 한번 없이 간단한 주소록을 만들 수 있었습니다. 만약 이 프로그램을 배포한다면 당연히 mdb도 함께 넣어줘야 합니다. 하지만 아직 검색이나 입력, 삭제 같은 기능은 없습니다. 두 번째 프로젝트에서 입력과 삭제를 넣어보고자 합니다.
DAO로 구현하는 일기장
이번에는 App Wizard가 아닌 직접 DAO 클래스를 적용시켜 간단한 일기장을 구현하겠습니다. 또한 레아군의 스타일대로 대화상자 기반에서 시작해 보겠습니다. 우리가 만들 일기장은 간단하지만 일기보기(SELECT), 삭제(DELETE), 입력(INSERT) 등이 포함됩니다. 먼저 우리가 사용할 클래스들을 살펴보죠. DAO를 지원하는 클래스는 무척 많은데 가장 많이 사용되는 클래스는 CDaoDatabase와 CDaoRecordset입니다. 이 두 클래스는 CObject에서 상속받는 클래스들로, 거의 대부분 함께 사용됩니다.
먼저 CDaoDatabase는 DAO를 이용하여 데이터베이스에 접속하는 기능을 갖고 있습니다. 두 번째 기능은 SQL문을 실행시킬 수 있습니다. 새롭게 추가하기 위한 INSERT, 삭제를 위한 DELETE 등이 CDatabase 클래스를 통해 가능합니다. 그런데 같은 SQL이라 할지라도 SELECT와 INSERT, DELETE는 차이가 있습니다. INSERT와 DELETE는 단순히 테이블 내에 명령을 전송하는 것이라면 SELECT는 질의의 결과를 담는 장소가 필요합니다. CDaoRecordset 클래스가 바로 그러한 역할을 맡는 것이죠. 참고로 ODBC를 이용할 때는 CDatabase 클래스와 CRecordset 클래스가 같은 역할을 합니다. DAO와 ODBC는 내부적으로 무척 다른 메커니즘을 가지지만 MFC 개발자의 시선에서 바라본다면 함수들은 거의 동일합니다.
상기의 이런 클래스들은 일종의 유틸리티 클래스이므로 굳히 VC++의 도구를 이용하여 작업하지 않더라도 손으로 직접 코딩해 사용할 수 있습니다. 특히 CDaoDatabase나 CDatabase는 무척 잘 만들어져 있고 간단하기 때문에 별다른 어려움 없이 사용할 수 있습니다. 그러나 SELECT를 이용해 결과를 가져오는 것은 생각보다 골치 아픕니다.
CDaoRecordset과 CRecordset은 각각 CDaoDatabase와 CDatabase에 연계되어 사용되며 선언이나 사용방법도 까다롭습니다. 따라서 SELECT가 들어가는 프로그램이라면(거의 대부분이겠죠?) 손 코딩보다는 당연히 클래스 위저드를 추천합니다. 또한 CDaoRecordset과 CRecordset 클래스는 SQL문을 모르더라도 SQL문과 같은 함수를 지원해 줍니다. 엄밀하게 말하면 MFC 개발자가 DBMS 사용법까지 알 필요가 없다는 가정 하에서 그러한 투명성을 제공해주는 것 같습니다. 하지만 이들로는 분명 한계가 있으며 DBMS의 저장 프로시저를 쓴다든지 할 때에도 곤란한 점이 많기에 반드시 SQL 학습은 필요합니다.
첫 번째 예제와 마찬가지로 일기장을 구현하기 위한 액세스의 테이블을 만들어보겠습니다. <표 2>처럼 중복 입력을 막기 위한 ID, 날짜 정보, 날씨, 제목, 내용들이 필드를 이룹니다. 액세스 테이블 내 ID와 Weather는 숫자, Date는 날짜/시간, Title과 Contents는 텍스트입니다. 테이블만으로도 프로젝트의 성격이 대강 드러났습니다. 날짜, 날씨, 제목, 내용이라는 항목이 들어가는 일기장입니다. 그런데 날짜라는 것은 년/월/일 형식으로 표시됩니다. 하지만 테이블 내의 필드는 이것들을 하나로 묶어 저장합니다. 화면에 보이고 입력할 때는 이것들을 다시 묶어줘야 합니다. 프로젝트에서 데이터베이스 테이블을 가장 먼저 설계하는 것은 바로 이런 이유입니다.
대화 상자 기반으로 DaoDiary라는 새 프로젝트를 만듭니다. 모든 설정은 기본 값을 이용하였습니다. 컨트롤 UI는 <화면 7>과 ‘이달의 디스켓’을 직접 참고하세요. 이제 DAO 클래스가 필요합니다. 언제나처럼 클래스 위저드에서 New Class를 선택해 CDaoRecordset을 선택하고 CMyDaoDB라는 파생 클래스를 만듭니다. 클래스 위저드로 CDaoRecordset을 호출하면 따로 CDaoDatabase를 호출할 필요없이 내부적으로 이미 코드를 만듭니다. CDaoRecordset에서 상속받은 DAO 클래스를 만들면 DAOView 예제에서 봤던 데이터베이스를 선택하는 윈도우가 나옵니다(<화면 2> 참고). 역시 같은 방식으로 데이터베이스와 테이블을 선택합니다. 이제 클래스 뷰에는 CMyDaoDB 클래스가 추가되었습니다. 그럼 CMyDaoDB의 주요 부분을 살펴보겠습니다.
CDaoRecordset의 선물, 테이블 필드가 멤버 변수로
데이터베이스에서 해당 필드를 가져오는 SELECT가 사실은 무척 귀찮은 작업이라 했습니다. 우리의 MFC는 바로 이 작업을 자동으로 구현해 줍니다. 먼저 CMyDaoDB의 생성자 부분을 살펴보죠. <리스트 1>은 대단히 중요한 부분입니다. 생성자에는 m_ID, m_Date와 같은 변수가 있는데 상당히 낯익을 것입니다. 바로 테이블 내에 만들었던 필드명이죠. 필드명을 참조해 멤버 변수를 자동으로 선언했습니다. 이제 테이블 자체가 하나의 객체가 되는 것이며 필드는 객체의 멤버가 되는 것입니다. 아주 편리하지 않습니까?
<표 3>은 액세스와 VC++간의 변경된 자료형을 보여줍니다. 변수들을 건드릴 때 해당하는 자료형에 맞게 이용해야겠죠? 물론 테이블 내의 설계가 변경된다면 직접 손 코딩으로 멤버 변수들을 추가하면 됩니다. 이때 기억할 것은 생성자에서 m_nFields라는 변수입니다. m_nFields는 필드 수를 의미하는데 5라고 설정되어 있죠? 필드 수가 변경되면 이 숫자도 반드시 변경해줘야 합니다.
GetDefaultDBName()에서는 mdb 파일의 로컬 위치를 나타내고 있습니다. mdb와 클라이언트 프로그램을 함께 배포하게 될 때 반드시 이 경로도 수정해줘야 합니다. 가장 편리한 것은 mdb와 클라이언트를 같은 폴더에 넣고 GetCurrentDirectory()를 이용해서 읽는 거겠죠. GetDefaultSQL()은 레코드셋을 위해 가장 기본이 되는 테이블을 적은 것인데 ‘[Diary]’라고 된 것은 사실은 ‘SELECT * FROM Diary’라는 문장입니다. 만약 레코드셋을 만드는 질의를 바꾸고자 한다면 이 부분을 수정하면 됩니다. 예컨대 날씨가 맑은 날의 일기만 보고 싶다면 return _T(SELECT * FROM Diary WHERE Weather = 1); 식으로 고쳐줍니다(이 프로그램에서 맑은 날은 weather 필드가 1로 지정되었습니다).
마지막으로 DoFieldExchange()가 있습니다. ‘MFC UI 대작전’에서 이미 공부한 DDX/DDV와 거의 유사합니다. m_ID와 같은 변수는 DoFieldExchange()에서 DFX_ 매크로에 의해 실제 테이블의 필드와 연결됩니다. 각각의 자료형에 따라 DFX_ 매크로 함수도 다르다는 사실을 잊지 마세요. 바로 이와 같은 과정들이 CDaoRecordset과 클래스 위저드가 만들어 준 부분입니다. 실제 손으로 구현하기 상당히 까다로운 부분이죠. 다시 언급하지만 이제 테이블을 하나의 객체로 사용할 수 있습니다. 테이블 내에 설계가 바뀌지 않는 한(전체 프로그램을 뜯어고칠 상황) 이제 CMyDaoDB 클래스는 다시 찾아올 필요가 없습니다. CMyDaoDB의 객체를 하나 생성해서 그것만 쓰기 때문입니다. 따라서 실제 테이블을 사용하는 CDaoDiaryDlg 클래스에서 CMyDaoDB의 객체를 하나 만듭니다. 여기에서는 간단하게 변수 타입으로 생성했습니다.
DB 접속 프로그래밍 과정
자, 이제 실제 CMyDaoDB를 이용할 것입니다. 그 전에 DB에 접속하고 이용하는 불변의 진리(?)를 살펴보겠습니다.
[1] DB를 연다(Open()).
[2] 거의 대부분 DB를 사용하기 이전에 레코드셋의 가장 앞부분, 혹은 가장 뒷부분으로 이동한다(MoveFirst(), MoveLast()).
[3] 레코드셋의 현재 위치에서 앞으로 이동하거나 뒤로 이동한다(MoveNext(), MovePrev())
[4] 이동을 하면 필드와 연결된 멤버 변수도 자동으로 갱신되어 즉각 화면으로 볼 수 있다.
[5] 내용을 지운다. SQL문에서 DELETE를 이용하거나 Delete()
[6] 내용을 첨가한다. SQL문에서 INSERT를 이용하거나 AddNew(), Update()
[7] 내용을 변경한다. SQL문에서 UPDATE를 이용하거나 Edit(), Update()
[8] 그 외 용도에 맞는 SQL문을 전송한다.
[9] DB를 닫는다(Close()).
이것이 데이터베이스를 이용하는 필수적인 과정이며 함수입니다. 소켓 프로그래밍과 마찬가지로 접속하고 내용을 가져오거나 수정, 삭제, 변경하고 연결을 종료하는 것이 고작입니다. 처음부터 데이터베이스는 자료 저장소에 불과합니다. 자료를 관리하기 위해서는 이것만 있으면 되겠죠?
우리의 일기장도 같은 원리로 작동합니다. 처음 로딩되자마자 가장 마지막 날짜의 일기를 보여주며 ‘새 일기’라는 버튼을 누르면 빈 폼에서 입력할 수 있습니다. 먼저 <리스트 2>를 보겠습니다. m_MyDaoDB는 CMyDaoDB 클래스로 선언한 변수(객체)입니다. 가장 먼저 Open() 함수를 이용해 DB와 접속을 시도합니다. 그 다음 OnNew()를 호출하네요. OnNew()는 새 일기를 쓸 때 작동할 함수이지만 이 안에는 컨트롤을 초기화하는 부분이 함께 들어가 있기에 초기화 목적으로 호출합니다. 가장 마지막 날짜의 일기를 보여줄 것이기 때문에 MoveLast()로 레코드셋의 가장 마지막 위치로 이동합니다. 마지막 FillContents()는 실제 해당하는 커서(레코드셋에서의 위치)에 있는 필드들을 컨트롤에 적어주는 역할을 맡고 있습니다.
<리스트 2>까지는 큰 무리 없이 이해가 가지요? 여기서 신경을 써야 하는 부분은 실제 테이블의 내용과 화면 UI를 맞추는 부분입니다. 년/월/일/날씨 같은 것들은 이미 정해진 양식이 있습니다. 연도는 숫자 4자리, 월/일은 숫자 한자리에서 2자리, 날씨도 맑음/흐림 형식으로 정해져 있습니다. 이런 양식을 사용자에게 매번 입력하라고 시킬 수 없습니다. 따라서 이제까지 이용해 왔던 에디트박스를 버리고 콤보박스(CComboBox)를 이용할 것입니다. 콤보박스는 드롭다운 형식을 가진 리스트와 에디트박스의 입력 기능 등 두 가지를 동시에 지원해주는 컨트롤입니다. 덕분에 연도와 날짜 같은 정해진 변수를 미리 리스트에 표시할 수는 있지만 에디트박스의 특징도 가지고 있기에 다음에 나오는 치명적인 입력 문제가 발생합니다.
긴급! 콤보박스 입력을 원천 봉쇄하라
“하하하 유나, 유나, 이것 보세요. 데이터베이스를 연동한 일기장 프로젝트랍니다.”
“날짜 적고 입력하면 되는거예요? 2004년, 음... 월은 영어로 July이라고 적어드리죠.”
“아앗, 안돼요! 절대로 숫자를 입력해야 해요. ‘07’도 안되고 절대로 그냥 ‘7’이라고 적으세요!”
“대단히 불편한 프로그램이네요.”
마음 가득 상처를 받은 레아군입니다. 콤보박스에서 입력을 봉쇄시키는 것은 의외로 간단합니다. 콤보박스 속성창에서 Type을 Drop List로 바꿔주기만 하면 됩니다. 아주 쉽지요? 그러나 이것만으로는 치명적인 문제가 또 발생합니다. 새 일기 버튼을 눌렀을 때 자동으로 오늘의 날짜를 출력해줘야 합니다. 이런 기능조차 제공하지 않는다면 일기장 프로그램이 아니죠. 이제까지 익히 써온 SetWindowText()는 콤보박스가 Drop List 형식으로 되는 순간 작동하지 않습니다.
그럼 어떻게 슬기롭게 헤쳐나갈까요? SetCurSel() 함수가 이제 그 역할을 대신합니다. SetCurSel()는 리스트에서 현재 커서의 위치를 강제적으로 설정하는 함수입니다. 콤보박스에 미리 정해진 변수들을 채워놓고 그 위치를 강제로 설정해주면 오늘의 날짜를 자동으로 표시할 수 있습니다. 그런 과정들이 <리스트 3>의 OnNew()함수에 있습니다.
먼저 콤보박스에 값을 채우기 위해 AddString()을 이용합니다. for()문으로 루프를 돌며 년/월/일을 채워놓습니다. 그런 다음 GetCurrentTime()으로 오늘 날짜를 구해 날짜에 해당하는 데이터를 설정합니다. 분명 SetWindowText()보다는 불편한 것이 사실이지만 이 루틴은 상당히 자주 사용되는 만큼 어떤 자료형이라 할지라도 정확하게 계산을 해서 원하는 위치를 뽑아낼 수 있어야 합니다. 모든 컨트롤이 다 자기 입맛에 맞는 것은 아니니까요.
이와 함께 날씨 부분도 봐주길 바랍니다. 날씨는 화면상에는 맑음, 흐림과 같은 문자로 표시되지만 실제 테이블상에는 단지 숫자에 불과합니다. 날짜 역시 이미 고정된 표현인데 전부 문자로 넣는다면 데이터베이스 용량을 쓸데 없이 차지할 것입니다. 따라서 1이면 맑음, 2면 흐림 식으로 표현하고 있습니다. 이는 오늘 일기를 저장할 때도 반드시 지켜줘야 하는 값이겠죠? CTime 클래스에 대해서는 분량상 자세히 다룰 수는 없지만 상당히 자주 쓰이는 클래스입니다. 날짜와 시간에 관련된 모든 기능들을 담고 있으니 CTime 클래스가 제공해주는 기능들은 꼭 MSDN에서 찾아보기 바랍니다.
데이터를 가져오기
이제 데이터를 가져와 컨트롤에 채우는 FillContents()를 살펴보겠습니다. CDaoRecordset을 이용했으면 데이터를 가져오는 함수 따위는 없습니다. 그냥 그대로 필드에 해당하는 멤버 변수를 부르면 데이터베이스에 있던 데이터가 나타납니다. FillContents() 역시 콤보박스에 데이터를 채우기 위해 SetCurSel()을 이용합니다. 이미 OnNew()에서 사용했던 방식입니다. 데이터 가져오기는 정말 쉽지요?
다만 한 가지 신경써야 할 부분은 m_Date, 즉 날짜 필드입니다. 액세스에서 날짜/시간 형식으로 저장된 데이터는 VC++에서 COleDateTime 형식으로 건너왔고 날짜와 시간이 혼합된 이 자료형에서 년/월/일을 각각 뽑아오기 위해서 GetYear(), GetMonth(), GetDay()를 이용했습니다. 이 프로젝트에서는 존재하지 않지만 int, CString, CTime, COleDateTime 등 자주 사용되는 이 네 가지 자료형을 요구사항에 맞게 서로 변환하고 계산하는 것은 대단히 필수적인 기법이 됩니다.
예컨대 CString(“2004년 07월 01일 PM 12시 01분 10초“)와 int Year(2004) int Month(7) int Date(10)의 시간 차이를 구하는 것과 같은 연습을 자주 해두기 바랍니다. 왜냐면 데이터베이스 설계자와 데이터베이스를 이용하는 개발자는 각자 서로 편리한 방식으로 코딩하기 때문에 형 변환에서 골치 아픈 일을 자주 겪게 됩니다. 이외에도 자릿수 때문에 고생하는 경우가 심심찮게 있는데 항상 강조하는 말이지만 완벽한 자료형 변환은 개발자의 필수 조건입니다.
OnBack()은 커서를 한 칸 뒤로 옮기는 기능을 갖고 있습니다. MovePrev()로 한 칸 뒤, MoveNext()로 한 칸 앞으로 이동합니다. 이와 함께 너무 중요한 함수가 IsBOF()와 IsEOF()입니다. 레코드셋에서 첫 위치인지, 마지막 위치인지를 리턴해 주는 함수입니다. 만약 레코드의 전체 행을 한꺼번에 보고 싶다면 다음과 같은 식으로 IsEOF가 될 때까지 루프문을 돌려 구현하기 때문입니다.
m_MyDaoDB.MoveFirst();
while (!m_MyDaoDB.IsEOF())
{
필드 멤버 변수;
m_MyDaoDB.MoveNext();
}
쓰기/삭제는 파일 관리의 기본
이제 데이터를 보는 방법과 DaoRecordset에서 어떻게 구현되는지도 살펴봤습니다. 남은 것은 입력과 삭제입니다. 입력을 하기 위해서는 먼저 AddNew()라는 함수를 호출합니다. 이것은 새 필드에 내용을 넣겠다고 선언하는 것이죠. 그런 다음 역시 각각의 멤버 함수들에 새 데이터를 넣어주고 모든 사항들이 맞으면 Update() 함수를 부릅니다. 무척 간단하지만 SQL문을 모르는 개발자를 위해서 아주 유용한 기능입니다. COleDateTime인 m_Date는 SetDate() 함수로 년/월/일을 맞춰 넣어줬습니다.
INSERT 구문을 모르더라도 이렇듯 MFC 함수 차원에서 구현을 할 수 있었습니다. 이와 마찬가지로 현재 필드를 삭제하는 DELETE 역시 Delete()로 구현됩니다. OnDelete()에서 아주 유용한 부분을 덤으로 찾아볼 수 있네요. 어떤 메뉴를 눌렀을 때 ‘정말 XX 할까요?’라고 묻는 부분입니다. AfxMessageBox()에도 어떤 버튼을 눌렀는지 리턴 값이 출력됩니다. 이것을 이용해 YES를 눌렀을 때 함수를 작동시켜주는 것이죠.
void CDaoDiaryDlg::OnInsert()
{
...
int nYear = pYear->GetCurSel() + 1990;
int nMonth = pMonth->GetCurSel() + 1;
int nDate = pDate->GetCurSel() + 1;
int nWeather = pWeather->GetCurSel() + 1;
// 새로 넣을 준비
m_MyDaoDB.AddNew();
m_MyDaoDB.m_Date.SetDate( nYear, nMonth, nDate );
m_MyDaoDB.m_Weather = nWeather;
pTitle->GetWindowText( m_MyDaoDB.m_Title );
pContents->GetWindowText( m_MyDaoDB.m_Contents );
// 수정 완료
m_MyDaoDB.Update();
}
void CDaoDiaryDlg::OnDelete()
{
if(AfxMessageBox (정말 삭제하시겠습니까?, MB_YESNO) == IDYES)
{
m_MyDaoDB.Delete();
OnNew();
AfxMessageBox(삭제하였습니다.“);
}
}
아직은 부족한 데이터베이스 프로그래밍
하지만 이것으로 완벽한 일기장일까요? 절대 아닙니다. 저장된 일기를 수정할 수 있는 기능과 조건별로 검색할 수 있는 기능이 필요합니다. UPDATE와 WHERE 조건이지요. 그리고 지금은 같은 일기 데이터라도 중복되어 입력이 됩니다. 다행히 데이터베이스에서 자동 증가되는 ID 값을 두었기에 구분할 수는 있지만 입력 버튼을 누를 때마다 같은 데이터가 계속 입력된다는 것은 치명적인 문제를 남깁니다. 남은 기능들은 이 일기장을 ODBC 기반의 다른 프로젝트로 바꿔보면서 함께 구현하도록 하겠습니다. 이번에 소개한 내용을 이해했다면 ODBC도 아주 쉽게 이용할 수 있게 된 것입니다. 용기를 내세요! @