변경 내역
-
2006.12.26 작성.
이 글은 월간 마이크로소프트웨어(일명 마소) 2006년 12월호 프로그래밍 노트 칼럼에 기고한 글입니다. 물론 구성이나 내용 상의 차이가 있을 수 있습니다.
프로그래밍이 즐거운 이유.
무언가를 만든다는 순수한 즐거움.
다른 이에게 유용한 것을 만든다는 기쁨
복잡한 퍼즐 같은 물체를 맞추는 데서 오는 매력
항상 배우는 즐거움
다루기 쉬운 매체를 갖고 노는 기쁨
– Frederic, Brooks의 The Mythical Man-Month 중에서 발췌 번역
대학으로 돌아온 지 벌써 한 달이 됐다. 3년 가까이 이 날을 기다려왔건만, 복학의 즐거움을 느낄 새도 없이 이렇게 시간이 흘렀다. 쌓인 욕구를 푸느라 많은 과목을 수강하고 외부 활동까지 하다 보니 하루하루가 만만치 않다. 하지만 무엇보다도 쉽지 않은 것은 3년 새 변해버린 학교 분위기였다.
필자의 학교는 2학년 때 전공을 선택하는데, 그때가 2001년이었다. 당시엔 한국 소프트웨어 산업에 대한 낙관적인 분위기가 지배적이었다. 벤처 기업에 몸 담아 성공한 선배들의 이야기가 식사 시간의 화제거리가 되던 때였다. 전산과 2학년 과목은 수강생이 너무 많아 분반까지 해야 하는 초유의 사태가 벌어질 정도였다. 그런데 5년이 지나고 보니, 분주하던 전산동의 모습이 완전히 달라져 있었다. 바뀐 것은 더 있었다. 졸업 준비하는 후배들과 진로에 관해 여러 번 대화를 나눴는데, 하나같이 개발은 하기 싫다고 한다.
아마도 여러분도 필자가 겪은 경험을 공유하고 있으리라 생각한다. 필자는 후배와의 대화 후에 이런저런 생각을 했다. (지금도 하고 있다.) 신문이나 잡지 칼럼이 이 문제에 대해 제 나름의 의견을 내놓지만, 필자는 다른 생각을 해본다. 대학 커리큘럼이나 외부 환경이 개발을 즐겁지 않은 잡일로 변질시키고 있는 것은 아닐까? 학점 A를 맞기 위해 과거에 표준이었던 SQL 명령문을 하나하나 외운다던가, 이제는 눈감고도 짤 수 있을 정도로 똑 같은 어플리케이션을 작성한다면 전산이 재미있을 리 없다. 이런 생각을 하고 나니 그 동안 필자가 재미보다는 지식 전달에 힘써왔음을 깨달았다. 그래서 오늘은 약간 쉽지만 프로그래밍의 즐거움을 느낄 수 있는 소재를 찾아봤다.
Coding for Fun – System Monitor
마이크로소프트는 수많은 개발자 커뮤니티 사이트를 갖고 있다. Technet이나 Patterns & Practices 등이 대표적이다. 하지만 이런 사이트는 필자의 지난 기사처럼 전문적이긴 해도 즐거움을 느끼긴 어려운 곳이다. Coding4Fun은 이와 반대로 매우 재미있는 장소이다. 레고 마인드스톰이나 수도쿠 게임 등의 예제를 제공한다. 흥미로운 어플리케이션 중에서 필자는 System Monitor를 선택했다. 말 그대로 중요한 시스템 상태의 변화가 발생하면 사용자에게 알려주는 어플리케이션인데, 개발 동기부터 재미있다.
근 6개월 사이에 세 개의 하드디스크가 망가졌다. 하나는 집의 하드디스크였고, 나머지 두 개는 회사 것이었다. 매번 다가올 재앙을 암시하는 경고가 이벤트 로그에 기록되었지만, 자주 확인하지 않았기 때문에 모르고 지나쳤다. 유틸리티 프로그램이 존재하지만, USB 하드디스크는 지원하지 않는 듯 하다. 나는 집에서 세 개의 USB 하드디스크를 쓰고 있고, 회사에선 중요한 데이터를 담고 있는 두 개의 USB 하드디스크를 사용하기 때문에, 이 문제는 매우 중요하다.
실행시켜보기
우선 C#용 System Monitor 소스코드를 다운로드 받아서 설치하자. Windows Installer가 알아서 설치해주니 편리하다. 1메가바이트도 안 되니 기본 경로에 설치해도 충분할 것이다. 설치 후에 해당 경로가 자동으로 열리니, 어디에 설치됐나 고민할 필요가 없다. 설치 경로에 Visual Studio 2005 솔루션 파일(SystemMonitor.sln)이 있으니 열어보자.
참고. 이 기사 내용이 반영된 소스 코드를 다운로드 받고 싶다면 여기를 방문하면 된다.
기능 관찰하기
메뉴는 크게 두 가지로 나뉜다. Persistent엔 네트워크 연결 여부와 시스템 로그(관리도구/이벤트 로그)의 변경 여부를 감지하는 기능이 있다. Scheduled엔 하드디스크의 여유 공간이 어느 정도 남았는지, 특정 네트워크 종점이 Ping에 반응하는지 여부를 측정하는 기능이 있다. 두 기능이 나뉜 기준은 간단한데, 변경 여부를 인터럽트로 받아볼 수 있느냐 여부일 뿐이다. 네트워크 연결이 끊기면 운영체제가 그 사실을 즉각 전달한다. 그러나 MediaCenter가 죽었다고 해도 외부에서 그 사실을 바로 알 수 있는 방법은 없다. 그러니 주기적으로 Ping을 보내서 잘 지내고 있는지 안부를 물어볼 수밖에 없다.
System Monitor을 1분간 실행시켜 놓으면 갑자기 메시지 박스가 뜬다. 필자의 경우에는 하드디스크의 용량이 부족하고 MediaCenter가 Ping에 응답하지 않는다는 메시지가 차례대로 뜬다. 필자는 이것을 알림이 Notifier 기능이라 부를 것이다.
구현 분석하기
어떤 기능이 있는지 알아봤다. 이제 어떻게 구현되어 있는지 살펴볼 차례다. 처음 System Monitor의 소스코드를 봤을 때 필자는 감탄을 금치 못했다. “깔끔하다” 이것이 첫인상이었다. C# 2.0의 특징과 디자인 패턴을 적절히 사용해서 단순 명료한 코드가 탄생했다. 이번 시간엔 아쉽지만 이러한 철학보다는 기능 분석과 구현에 집중할 생각이다. 하지만 소스 파일, 클래스, 변수 등의 적절한 이름 붙이기라던가, 솔루션 구성만 보더라도 여러분이 필자의 느낌을 이해하는데 충분하리라 믿는다.
System Monitor 솔루션은 다음과 같이 구성되어 있다. 부연 설명이 필요할 것 같은 부분만 적어놨다.
<리스트 1> 성능 병목지점 수정하기
<monitor runFrequency="00:01" type="SystemMonitor.Monitors.PingMonitor,SystemMonitor"> <settings> <setting name="host" value="MediaCenter" /> </settings> <notifiers> <notifier type="SystemMonitor.Notifiers.MessageBoxNotifier,SystemMonitor" /> </notifiers> </monitor>
-
메인 폴더
-
App.config (<리스트 1> 참조)
앞서 <화면 1>에서 봤던 모니터링 기능에 대한 상세한 설정 값을 볼 수 있다. 그 중에서 Ping을 살펴보자. (왜냐하면 앞으로 이와 유사한 기능을 추가할 생각이기 때문이다.) 1분 주기(runFrequency = 00:01)로 MediaCenter라는 네트워크 종점에 Ping을 던진다. 만약 호스트가 응답하지 않으면 메시지 박스(SystemMonitor.Notifiers.MessageBoxNotifier)를 띄워서 사용자에게 알린다. 물론 최초의 설정 값대로라면 반드시 메시지 박스를 볼 수 있는데, MediaCenter라는 호스트가 존재하지 않기 때문이다.
-
MonitorType
<화면 1>에서 Persistent, Scheduled 두 가지 감시 방법이 있음을 확인했다.
-
-
Properties
Resources.resx는 어플리케이션에 필요한 리소스를 포함한다. 이 파일을 더블 클릭하면 <화면 1>에서 본 아이콘을 다시 볼 수 있다. Resources 폴더를 열면, Resources.resx 파일에 포함된 아이콘 파일을 직접 확인하고 편집할 수 있다.
-
Configuration
App.config의 XML 설정 값을 객체로 역직렬화하거나 반대로 객체를 XML 파일로 직렬화할 수 있다.
-
Forms
물론 <화면 1>에서 봤던 윈도우 폼이 들어있는 폴더이다.
-
Monitors
이 폴더에는 <화면 1>에서 봤던 모니터링 기능이 구현되어 있다. 예를 들어 디스크의 여유공간(DiskSpaceMonitor.cs)이나 네트워크 종점에 Ping을 보내는 기능(PingMonitor.cs)이 있다. 이 모든 모니터링 구현은 추상 클래스 MonitorBase를 상속받는다.
-
Notifiers
앞서 알림 기능을 목격했다. MediaCenter가 Ping에 응답하지 않자 메시지 박스(MessageBoxNotifier )가 나타났다. 알림 기능엔 그 밖에도 이메일(EmailNotifier) 전송 등이 있고, 이 모든 구현은 추상 클래스 NotifierBase를 상속 받는다.
소켓 서버를 모니터링하기
Ping은 네트워크 종점이 살아있는지 여부를 알 수 있다. 하지만 Ping만으로 충분하지 않을 경우가 많다. 보통 하나의 호스트에는 여러 개의 서버 어플리케이션이 운용되기 마련이다. 필자의 경우, 노트북 외에도 데스크 탑 두 대를 사용하고 있는데 여기엔 다음과 같은 서버 어플리케이션 등이 설치되어 있다.
-
IIS (Web server, FTP)
-
Apache, MySQL
이 중에서 Apache나 MySQL 서버가 오작동을 일으켜서 서비스가 중지되는 경우를 잠시 상상해보자. 오랜만에 블로그에 글이나 써볼까 싶어서 웹 브라우저를 열고 https://andromedarabbit.net 을 친다. “서버를 찾을 수 없음”이라니. 재빨리 [관리도구/서비스]를 열어본다. 곧 Apache 서비스가 중지되어 있음을 알게 된다. 서비스를 작동시키고 나니 다시 블로그에 접속이 되지만 이런 의문이 든다. 언제부터 서비스가 멈춰 있었던 걸까? 하루? 이틀? 아니면 일주일?
이 경우에는 PingMonitor는 아무런 도움이 되지 못했다. https://andromedarabbit.net를 호스팅하는 컴퓨터의 윈도우는 여전히 작동하고 있었고, 단지 Apache 서버만 작동을 멈췄기 때문이다.
소켓 모니터링 – 요구사항
필자는 두 번 다시 똑 같은 문제로 고생하고 싶지 않다. 그래서 System Monitor에 소켓 서버를 감시하는 기능을 추가할 생각이다. 간단하게 요구사항을 정리해봤다.
서버 어플리케이션의 IP Address:Port로 접속을 시도해본다. (https://andromedarabbit.net의 경우에는 andromedarabbit.net:80이 될 것이다.) 접속이 실패하면 사용자에게 이메일로 알리고, 로컬 컴퓨터의 이벤트 로그에 기록한다.
서버 어플리케이션엔 Apache만 있는 것이 아니다. MySQL이 작동하지 않는다면 블로그가 오작동하기는 마찬가지다. 그러므로 Apache에 한정하지 않고, 어떤 소켓 서버 어플리케이션이라도 감시할 수 있어야 한다. 말인 즉, 어플리케이션의 네트워크 주소를 설정 파일(App.config)에 적을 수 있어야 한다.
사실상 Ping Monitor와 요구사항이 일치한다. 하지만 너무 똑같으면 재미없으니 이벤트 로그 기능을 추가했다.
소켓 모니터링 – 설정파일
우선 설정 파일(App.config)부터 수정해보자. 새로 추가될 기능의 요구사항이 PingMonitor와 비슷하므로, <리스트 1>의 설정 내용을 복사한 후 구미에 맞게 수정<리스트 2> 참고)해봤다. SocketMonitor라는 걸 추가할 생각인데, 이 객체가 필요로 하는 정보는 감시할 대상의 네트워크 주소와 포트 번호뿐이다. PingMonitor에 포트 번호를 추가한 것에 불과하다. 요구사항 분석 시에 언급한 바와 같이 이벤트 로그 알림 기능을 추가한다. 아직 이벤트 로그에 필요한 정보가 무엇인지 모르니, 간단하게 앞으로 추가될 클래스의 이름 EventLogNotifier을 추가하는 데 그쳤다.
<리스트 2> 설정 파일 수정, 그 첫 번째 시도
<monitor runFrequency="00:01" type="SystemMonitor.Monitors.SocketMonitor,SystemMonitor"> <settings> <setting name="host" value="andromedarabbit.net" /> <setting name="port" value="80" /> </settings> <notifiers> <notifier type="SystemMonitor.Notifiers.MessageBoxNotifier,SystemMonitor" /> <notifier type="SystemMonitor.Notifiers.EventLogNotifier,SystemMonitor" /> </notifiers> </monitor>
소켓 모니터링 – 아이콘 추가
<화면 1>을 보자. 모니터링 기능마다 서로 다른 아이콘을 쓰고 있다. 새로운 기능을 위해서 새로운 아이콘을 추가하는 성실함 정도는 보여주자. Resource 폴더에 새로운 아이콘을 추가하자. 필자는 아이콘을 직접 제작하기 귀찮은 나머지 Computer Icon이라는 키워드로 구글링했다. 이제 새로운 아이콘을 리소스에 추가할 차례다. [Properties/Resources.resx]를 클릭하면 <화면 2>를 볼 수 있다. System Monitor 어플리케이션이 사용하는 아이콘이 보인다.
[리소스 추가 (드롭 다운)/기존 파일 추가] 파일 탐색 창(<화면 3> 참고)이 열리면 조금 전에 만든 Socket.ico 파일을 추가하자. 추가한 아이콘의 이름을 SocketIcon으로 수정한다. 큰 의미는 없다. 단지 기존 소스코드의 관행을 따르기 위함이다.소켓 모니터링 – 모니터 추가
System Monitor의 최대 장점이라고 한다면, 디자인 패턴이 적절하게 적용된 덕분에 기능을 확장하기 쉽다는 점이다. 이미 구현되어 있는 소스 코드를 필요한 만큼 수정하면 간단하게 기능을 추가할 수 있다.
우선 PingMonitor.cs를 SocketMonitor.cs로 복사하자. Ping 대신 소켓 연결을 하고, 이에 필요한 포트 번호 정보를 Initialize 메써드에서 초기화한다. PingIcon 대신 앞서 추가한 SocketIcon을 사용한다. 그리고 메시지 내용을 소켓 통신에 맞게끔 수정하면 모든 작업이 완료된다. (<리스트 3> 참고)
<리스트 3> SocketMonitor.cs
using System; using System.Collections.Generic; using System.Drawing; using System.Net.Sockets; namespace SystemMonitor.Monitors { class SocketMonitor : MonitorBase { private string _host; private int _port; public override void Execute() { try { using(TcpClient client = new TcpClient()) { client.Connect(_host, _port); if(client.Connected == false) { Notify("Failed connect", "Connect failed."); } } } catch(Exception ex) { Notify("Failed connect", string.Format("Connect of '{0}:{1}' failed with an exception: {2}", _host, _port, ex.Message)); } } protected override void Initialize(Dictionary<string, string> settings) { _host = settings["host"]; _port = int.Parse(settings["port"]); } public override string Description { get { return string.Format("Connecting '{0}:{1}'.", _host, _port); } } public override MonitorType MonitorType { get { return MonitorType.Scheduled; } } public override Icon Icon { get { return Properties.Resources.SocketIcon; } } } }
이쯤에서 테스트를 해보자. 실행시키자마자 오류 메시지가 뜬다. 디버거에 연결하지 않고 실행시키면, 오류 메시지 창이 뜬다. 하지만 정보를 보내라는 메시지만 뜰 뿐이다. 어플리케이션이 실행되면 MainForm::CreateTasks()이 호출되는데, 이때 설정파일에 명시된 클래스를 동적으로 생성하는 작업이 이뤄진다. 클래스 SocketMonitor는 구현했지만, 클래스 EventLogNotifier를 아직 구현하지 않았다. 그래서 EventLogNotifier의 인스턴스를 동적으로 생성하다가 오류가 발생한 것이다.
문제가 있다면, 오류 메시지가 사용자에겐 전혀 도움이 안 된다는 점이다. 설정 파일이 문제인지, 필수 컴포넌트가 설치되지 않은 탓인지 알 수가 없다. 이쯤에서 사용자 친화적인 환경을 만들고 싶다는 욕구가 마구마구 치솟는다. 그래서 요구사항을 하나 더 추가한다.
예외처리
동적 인스턴스 생성 과정에서 오류가 난다고 했다. 구체적으론 MainForm::CreateTasks() 메써드 중 IMonitor monitor = (IMonitor)Activator.CreateInstance(Type.GetType(monitorElement.TypeName));와 INotifier notifier = (INotifier)Activator.CreateInstance(Type.GetType(notifierElement.TypeName));에서 ArgumentNullException이 발생한다. Type.GetType(“SystemMonitor.Notifiers.EventLogNotifier”)이 NULL을 반환하기 때문이다. 각각의 코드를 <리스트 4>와 같이 수정하면 된다.
<리스트 4> 예외 메시지를 구체적으로 만들기
private void CreateTasks() { …… Type monitorType = Type.GetType(monitorElement.TypeName); CheckTypeInstance(monitorType, monitorElement.TypeName); IMonitor monitor = (IMonitor)Activator.CreateInstance(monitorType); …… } private static void CheckTypeInstance(Type monitorType, string monitorName) { if (monitorType == null) { throw new ApplicationException(string.Format("Class '{0}' has not been found. Check app.config.", monitorName)); } }
다시 System Monitor를 실행시켜 보자. 예상과 달리 “Class ‘ SystemMonitor.Notifiers.EventLogNotifier’ has not been found. Check app.config.”라는 메시지는 어디에서도 볼 수 없다. 이것은 처리되지 않은 예외를 처리하기 위한 핸들러를 등록하면 해결되는 문제이다. 진입점(클래스 Program)을 <리스트 5>과 같이 수정한다. 그러면 원하는 메시지 창을 볼 수 있다. 그럼에도 불구하고 의문은 계속된다. 왜냐하면 오류 보고서를 제공하라는 메시지가 뜨고 어플리케이션은 종료되기 때문이다. 어플리케이션 내에서 발생하는 모든 예외를 처리할 핸들러가 있는데도 어플리케이션은 종료된다. 이것은 .NET Framework의 버그가 아니다. 심각하지 않은 예외라면 별도의 처리 루틴이 있어야 하며, 그렇지 않을 때에는 어플리케이션을 종료하여 사용자의 오해(아무런 문제도 없다는)를 막고, 오작동이 지속되지 않도록 하는 것이 마이크로소프트 개발팀의 디자인 철학이다.
<리스트 5> UnhandledExceptionHandler
static class Program { private static MainForm _mainForm; [STAThread] static void Main() { AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.UnhandledException += new UnhandledExceptionEventHandler(GlobalExceptionHandler); Application.EnableVisualStyles(); _mainForm = new MainForm(); Application.Run(_mainForm); } static void GlobalExceptionHandler(object sender, UnhandledExceptionEventArgs args) { Exception e = (Exception)args.ExceptionObject; MessageBox.Show(e.Message, "처리되지않은예외가발생했습니다"); } }
이벤트 로그 알림이 – 설정 파일 수정
설정파일 App.config를 수정했던 것이 기억나는가? 소켓 통신을 위해서 종점의 네트워크 주소와 포트 번호가 필요하다는 사실은 알고 있었다. 그러나 이벤트 로그를 기록하기 위해 어떤 정보가 필요한지는 알지 못했다. MSDN에 따르면 이벤트 로그는 다음과 같은 세 가지 속성으로 이뤄졌다고 한다. 그래서 <리스트 1>의 설정 파일을 수정한다.
EventLog 구성 요소 인스턴스 구성과 관련된 세 가지 주요 속성이 있습니다.
Log 속성은 상호 작용할 로그를 나타냅니다.
MachineName 속성은 사용 중인 로그가 있는 컴퓨터를 나타냅니다.
Source 속성은 구성 요소가 로그에 엔트리를 쓸 때 구성 요소 식별에 사용할 소스 문자열을 나타냅니다.
<리스트 6> 설정 파일 수정, 그 두 번째 시도
<monitor runFrequency="00:01" type="SystemMonitor.Monitors.SocketMonitor2,SystemMonitor"> <settings> <setting name="host" value="kaistizen2.net" /> <setting name="port" value="80" /> </settings> <notifiers> <notifier type="SystemMonitor.Notifiers.MessageBoxNotifier,SystemMonitor" /> <notifier type="SystemMonitor.Notifiers.EventLogNotifier,SystemMonitor"> <settings> <setting name="machine_name" value="." /> <setting name="source" value="My System Monitor" /> <setting name="log" value="Application" /> </settings> </notifier> </notifiers> </monitor>
<리스트 6>를 부연 설명하자면, 우선 Machine Name 값 “.”는 로컬 컴퓨터를 나타낸다. Log 값 Application은 [관리도구/이벤트 뷰어] 중 응용 프로그램에 로그를 남기라는 뜻이다. 마지막으로 Source 값은 지금부터 기록할 로그의 출처가 System Monitor임을 알 수 있다. 앞으로 보겠지만, 이런 구성 값대로라면 <화면 4>와 같이 로그가 남는다.
이벤트 로그 알림이 – 구현하기
이번에도 EmailNotifier.cs를 EventLogNotifier.cs로 복사한다. EventLogMonitor를 구현했을 때와 그다지 다르지 않다. 초기화 함수 Initialize에서 설정 파일에 기록된 Machine Name 등을 읽어온 후, MSDN의 예제대로 이벤트 로그 객체를 초기화한다. 메시지를 보내야 할 때 Execute 메써드가 호출될 것이므로, 이벤트 로그에 기록하는 기능을 구현하면 끝이다. 별도의 설명이 필요 없을 정도로 매우 간단하다.
<리스트 7> 이벤트 알림 구현
using System; using System.Collections.Generic; using System.Text; using System.Diagnostics; namespace SystemMonitor.Notifiers { class EventLogNotifier : NotifierBase { private string _machineName; private string _source; private string _log; private EventLog _eventLog = new EventLog(); public override void Initialize(Dictionary<string, string> settings) { _machineName = settings["machine_name"]; _source = settings["source"]; _log = settings["log"]; } public override void Execute(string title, string message) { InitializeEventLog(); _eventLog.WriteEntry(string.Format("Notification!!! {0}{0}[TITLE] {1}{0}[MESSAGE]{2}", Environment.NewLine, title, message), EventLogEntryType.Warning); } private void InitializeEventLog() { if (!EventLog.SourceExists(_source)) { EventLog.CreateEventSource(_source, _log); } _eventLog.MachineName = _machineName; _eventLog.Source = _source; } } }
마치는 글
쓸만한 어플리케이션을 만드는 일은 보통 쉽지 않다. 하지만 너무 심각하게 받아들여야 할 이유도 없다. Coding4Fun이나 오픈 소스 프로젝트의 작품을 분석하고 개선하면 짧은 시간 내에 여러분만의 소프트웨어를 쉽게 작성할 수 있다. 게다가 제대로 된 작품을 선택하기만 하면 올바른 디자인을 배우고, 번뜩이는 영감을 얻을 수 있을지도 모른다. 지금 이 순간 지체 말고 여러분만의 흥미로운 소재를 찾아보자.
음…
예제 소스 어디에서 구해요…
저녁에 정리해서 올리겠습니다.
링크를 고쳐놨습니다.