메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

객체지향 펄

한빛미디어

|

2002-05-07

|

by HANBIT

9,247

저자: Simon Cozens, 역 정직한

필자는 최근에 바둑을 배우기 시작했다. 그러면서 알게 된 것이 바둑과 펄은 공통점이 많다는 것이었다. 즉 펄이든 바둑이든 이 둘을 구성하고 있는 법칙들은 상대적으로 간단하지만 항상 표면 아래에는 놀랄만한 복잡성을 감추어져 있기 때문이다. 하지만 내가 찾아낸 가장 재미있는 공통점은 배우는 사람마다 각자가 발전해 나가는 단계가 서로 다르다는 것이다. 필자가 보기에 펄을 배우는 데는 대여섯 가지의 다른 차원이 있는 것 같이 보이며 각각의 경지에 오르기 위해서는 반드시 정복해야 할 높은 언덕들이 있다.

예를 들어 바둑을 두는 사람은 아주 단순하게 바둑을 두면서 고상하게 행동할 수도 있다. 하지만 초보 티를 벗고 정말로 게임에 빠져들려고 한다면 어떻게 경제적으로 공격하고 방어할 수 있는지부터 배워야 한다. 그러고 나면 다음 단계에서 "패(ko)" 라고 하는 반복적인 일련의 전투방법을 배워야 한다. 필자의 경우, 이런 식으로 실력이 늘어 가면서 더 좋은 플레이어가 되려면 반드시 숙지하고 넘어가야 할 어려운 전략들이 계속 있을 것이라고 기대하게 되었다.

펄 역시 지식의 경지가 있기 마련이다. 필자의 경험상 하수와 중수를 구분하는 것은 객체지향 프로그래밍의 이해수준이다. 객체지향 펄을 어떻게 쓰는 것인지 알게 되기만 하면 재미있고 유용한 CPAN 모듈들의 거대한 세계, 새로운 프로그래밍 기술, 그리고 펄 프로그래밍의 더 높은 경지로 들어가는 문이 열린 것이다.

그래서 객체지향 프로그래밍이 도대체 뭔데요?

객체지향 프로그래밍은 입만 살아있는 관리자들이 내뱉는 말 중의 하나이긴 하지만 다른 말들과는 달리 실제로 뭔가 의미있는 말이다. 지극히 평범한 절차중심적 펄 코드를 한번 살펴보자. 이 코드는 대부분의 초보 프로그래머들이 일상적으로 작성하는 코드다.
my $request = accept_request($client);
my $answer = process_request($request);
answer_request($client, $answer);
$new_request = redirect_request($client, $request, $new_url);
여기에 든 예는 웹 서버의 일종으로 보인다. 클라이언트로부터 요청을 받고, 답을 얻기 위해 특정 방법으로 그 요청을 처리하고, 답을 클라이언트에게 보내준다. 게다가 그 요청을 다른 URL로 바꿀 수도 있다.

똑같은 코드이지만 이를 객체지향 스타일로 작성하면 좀 다르게 보인다.
my $request = $client->accept();
$request->process();
$client->answer($request);
$new_request = $request->redirect($new_url);
도대체 이게 뭐지? 저 우습게 생긴 화살표들은 또 뭐고? 객체지향 프로그래밍에서 기억해야 할 것은 더 이상 서브루틴들에게 자료를 넘겨주고 서브루틴이 일을 해주도록 만들지 않는다는 것이다. 이제는 자료에게 스스로 일을 하라고 시키는 것이다. 여기서 화살표(공식적으로 -> 는 "메소드 호출 연산자"라고 함)는 자료에게 주는 명령이라고 생각하면 된다. 첫번째 줄에서는 클라이언트를 표현하는 자료에게 요청을 받아들이고 뭔가를 되돌려 달라고 이야기하는 것을 볼 수 있다.

그렇다면 여기서 "클라이언트를 표현하는 자료"란 무엇이고, 또 무엇을 되돌려 준다는 말인가? 객체지향 프로그래밍이기 때문에 답은 간단하다. 그 둘은 모두 객체들이다. 객체들은 모두 평범한 펄 스칼라 변수처럼 보인다. 사실 객체들은 평범한 스칼라 변수와 거의 같기 때문에 그렇게 보인다.

각 예제에서 $client$request 간의 유일한 차이는 객체지향 버전에서 스칼라들은 자신들이 호출할 수 있는 서브루틴을 어디에서 찾아보아야 할지 알고 있다는 것 뿐이다. (객체지향적으로 말하자면 "서브루틴"이 아니라 "메소드" 라고 부른다)

바로 이런 이유로 인해 객체지향 버전에서는 process_request라는 서브루틴을 만들 필요가 없다. 자신이 request 라는 것을 알고 있는 것으로부터 process 메소드를 호출한다면 이는 자신이 request를 처리해야 한다는 것을 알고 있다는 말이다. 정말 간단하지 않은가? 객체지향 스타일로 말하자면 $request 객체는 Request "클래스"에 속한다고 말할 수 있다. 클래스는 그 객체가 속한 "종류의 사물"이며 클래스를 통해 객체들은 메소드를 찾는다. 따라서 $request$mail이 서로 다른 클래스에 속해있다면 $request->redirect$mail->redirect는 완전히 다른 메소드를 호출하게 된다. 다시 말해 Request 객체를 redirect 하는 것과 Mail 객체를 redirect 하는 것은 완전히 다른 일이라는 것이다.

메소드를 호출하면 실제로 어떤 일이 일어나는지 궁금할 것이다. 메소드가 단지 객체지향 형태의 서브루틴이라는 것을 이미 이야기했기 때문에 펄에서의 메소드가 실제로는 단순히 서브루틴들일 뿐이라는 것을 알게 되어도 그리 놀랍지 않을 것이다. 그렇다면 클래스는 어떻게 되는가? 클래스의 목적은 한 묶음의 메소드들과 다른 것들을 구별하기 위한 것이다. 펄에서 한 묶음의 서브루틴들을 다른 것들과 구별하기 위한 자연스러운 방법은 무엇일까? 짐작했겠지만, 펄에서 클래스는 패키지일 뿐이다. 따라서 Request 클래스에서 $request라고 불리는 객체를 가지고 redirect 메소드를 호출했다면 실제로는 아래와 같은 일이 발생할 것이다.
# $request->redirect($new_url)

Request::redirect($request, $new_url)
그렇다, 단지 알맞은 객체 안의 redirect 서브루틴을 호출하고 거기에 객체와 함께 다른 파라미터들을 넘겨주는 것 뿐이다. 그렇다면 여기서 왜 객체를 넘겨주느냐는 질문을 할 것이다. 이유는 지금 어떤 객체를 가지고 작업을 하는지 redirect가 알아야 하기 때문이다.

아주 기본적인 수준에서 이상에서 설명한 것이 객체지향 펄의 전부라고 할 수 있다. 자료에 대해서 어떤 행동을 취하는 것으로 보이도록 서브루틴 호출을 작성하는 또하나의 방법일 뿐이다. 객체지향 펄 모듈들을 사용하는 대부분의 사용자들이 알아야 할 것은 그게 전부다.

그렇다면 객체지향에는 무슨 이득이 있는가?

만일 위에서 말했던 사항이 전부라면 도대체 왜 모두들 객체지향 펄이 빵과 버터 이후로 최고의 물건이라고 말하는 것일까? 여러분들이 약간의 노력만 기울인다면 객체지향 기법에 기반한 재미있고 유용한 모듈들은 엄청나게 많이 찾을 수 있다. 이에 대해 모든 사람들이 알고있는 것들을 알아보기 위해 잠시 절차중심적 코드로 돌아가 보자. 아래 보이는 코드는 메일 메시지에서 송신자와 제목을 뽑아내는 코드이다.
sub mail_subject {
    my $mail = shift;
    my @lines = split /\n/, $mail;
    for (@lines) {
        return $1 if /^Subject: (.*)/;
        return if /^$/; # Blank line ends headers
    }
}

sub mail_sender {
    my $mail = shift;
    my @lines = split /\n/, $mail;
    for (@lines) {
        return $1 if /^From: (.*)/;
        return if /^$/;
    }
}

my $subject = mail_subject($mail);
my $from    = mail_sender($mail);
이 코드는 다 잘 작성되었다. 하지만 메일에 대한 새로운 정보를 뽑아내려면 매번 전체 메일을 다 훑어보아야 한다는 점에 주목해야 한다. 물론 이 두 서브루틴을 꽤 복잡한 정규식으로 대체해 버릴 수도 있지만 여기서 중요한 것은 그 점이 아니다. 설사 그렇게 한다 하더라도 이 예제에서 실제로 해야만 하는 일보다는 훨씬 많은 양의 작업을 하고 있다는 것이 중요한 점이다.

이와 같은 일을 하는 객체지향 예제를 만들기 위해 Mail::Header라는 CPAN 모듈을 사용해 보자. 이 모듈은 라인의 배열에 대한 참조를 넘겨받아 메일 헤더 객체를 넘겨주기 때문에 객체를 가지고 원하는 작업을 할 수 있다.
my @lines = split /\n/, $mail;
my $header = Mail::Header->new(\@lines);

my $subject = $header->get("subject");
my $from    = $header->get("from");
여기에서는 "헤더를 가지고 뭔가를 하는"관점에서 문제를 바라보는 것뿐 아니라, 모듈이 작업을 더 효과적으로 처리할 수 있는 기회도 주고 있는 것이다. 어떻게 그럴 수 있는지 궁금한가?

CPAN 모듈의 주된 장점 중 하나는 이 모듈에 우리가 호출할 수 있는 여러 함수들이 있으며, 그 함수들이 실제로 어떻게 구현되었는지 신경쓰지 않아도 된다는 점이다. 객체지향 프로그래밍에서는 이것을 "추상화"라고 부른다. 구현은 사용자의 관점에서 볼 때는 추상화 되어있다는 것이다. 마찬가지로 $mail_obj이 실제로 무엇인지 신경 쓸 필요도 없다. $mail_obj를 문자열의 배열에 대한 참조라고 생각할 수도 있겠지만, 중요한 것은 Mail::Header$mail_obj를 가지고 뭔가 똑똑한 방식으로 작업한다는 점이다.

실제로 $header는 감춰져 있는 해시의 참조다. 다시 말하지만 그것이 해시 참조인지, 아니면 배열 참조인지, 그도 아니면 그와는 전혀 다른 어떤 것인지는 전혀 신경 쓸 필요가 없다. 다만 해시 참조인 것처럼 생각하고 new라는 생성자(여기서 생성자라는 것은 새로운 객체를 만들어 주는 메소드일 뿐임)가 문자열의 배열과 관련된 모든 전처리(pre-processing)를 해준 후 제목, 보낸이, 그리고 그 외의 여러 필드들을 어떤 해시의 키와 연결시켜 저장해 준다는 것만 알면 된다. get 메소드가 하는 일이라고는 기실, 해시에서 적절한 값을 끌어내는 것 뿐이다. 메시지 전체를 매번 훑어보는 것보다 분명히 훨씬 효율적이다.

객체란 바로 이런 것이다. 객체는 사용자의 자료를 모듈이 원하는 형태로 재정리해서 나중에 조작할 때 가장 효율적으로 할 수 있도록 만들어 놓는 것이다. 사용하는 사람은 똑똑하게 구현되어 있는(물론, 그 모듈을 작성한 사람이 똑똑하다는 전제 하에...) 객체의 장점을 누리면 되고, 그 밑에서 뭐가 어떻게 돌아가고 있는지 볼 필요도 또한 신경 쓸 필요도 없다.

사용해보기

Mail::Header 모듈을 사용하여 간단한 객체지향 기법의 사용법을 살펴보았다. 그렇다면 이제는 심화학습을 위해 조금 더 깊이 들어가는 프로그램을 한 번 살펴보도록 하겠다. 이 예제는 유닉스 머신에서 사용할 아주 간단한 시스템 정보 서버다. (겁먹지 말길 - 이 프로그램들은 유닉스가 아닌 시스템에서도 잘 돌아감.) 유닉스에는 "핑거(finger)"라고 하는 클라이언트/서버 프로토콜이 있다. 이 핑거를 이용하면 서버에 접속하여 사용자들에 대한 정보를 요청할 수 있다. 필자가 사용하는 유닉스 머신에 사용자이름으로 "finger"를 실행시키면 아래와 같은 결과를 얻게 된다.
% finger simon
Login name: simon       (messages off)  In real life: Simon Cozens
Office: Computing S
Directory: /v0/xzdg/simon               Shell: /usr/local/bin/bash
On since Nov  6 10:03:46                5 minutes 38 seconds Idle Time
   on pts/166 from riot-act.somewhere
On since Nov  6 12:28:08
   on pts/197 from riot-act.somewhere
Project: Hacking Perl for Sugalski
Plan:

Insert amusing anecdote here.
여기서 우리가 시도할 작업은 현재 시스템 정보를 제공해 주는 핑거 클라이언트와 서버를 만드는 일이고, 이 작업을 하는 데 객체지향 IO::Socket 모듈을 이용하려고 한다. 물론, 이 작업을 Socket.pm을 이용하여 완전히 절차중심적으로 할 수도 있겠지만, 이렇게 하는 것이 실제로는 그보다 훨씬 쉬울 것이다.

먼저 클라이언트를 보자. 핑거(finger) 프로토콜은 우리가 보려고 하는 수준에서는 정말 간단하다. 클라이언트가 연결한 후 텍스트(일반적으로 사용자이름 하나) 한 줄을 보낸다. 서버는 텍스트를 보낸 후 연결을 종료한다.

IO::Socket을 이용해 연결을 관리하면 아마 다음과 같은 코드가 나올 것이다.
#!/usr/bin/perl
use IO::Socket::INET;

my ($host, $username) = @ARGV;

my $socket = IO::Socket::INET->new(
                        PeerAddr => $host,
                        PeerPort => "finger"
                      ) or die $!;

$socket->print($username."\n");

while ($_ = $socket->getline) {
    print;
}
위 코드는 아주 직설적이다. IO::Socket::INET 생성자인 new는 피어 어드레스(peer address) $hostfinger 포트에 대한 연결을 나타낸다. 그리고는 이 연결과 관련된 자료를 주고받기 위해 printgetline 메소드를 사용할 수 있다. 객체지향이 아닌 연결과 한 번 비교해 보면 왜 사람들이 객체지향을 선호할 수 밖에 없는지 깨닫게 될 것이다.
#!/usr/bin/perl -w
use strict;
use Socket;
my ($remote,$port, $iaddr, $paddr, $proto, $user);

($remote, $user) = @ARGV; 

$port    = getservbyname("finger", "tcp")   || die "no port";
$iaddr   = inet_aton($remote)               || die "no host: $remote";
$paddr   = sockaddr_in($port, $iaddr);

$proto   = getprotobyname("tcp");
socket(SOCK, PF_INET, SOCK_STREAM, $proto)  || die "socket: $!";
connect(SOCK, $paddr)                       || die "connect: $!";
print SOCK "$user\n";
while ()) {
   print;
}

close (SOCK)            || die "close: $!";
이제 서버로 되돌아가보자. 또하나의 객체지향 모듈인 Net::Hostent를 사용할 것이며 이 모듈은 gethostbyaddr의 결과를 값들의 목록이 아닌 객체로 취급할 수 있게 해준다. 이 말이 무슨 의미냐 하면 목록 중의 어느 요소가 우리가 원하는 값인지 기억하려고 고민할 필요가 없다는 뜻이다.
#!/usr/bin/perl -w
use IO::Socket;
use Net::hostent;

my $server = IO::Socket::INET->new( Proto     => "tcp",
                                    LocalPort => "finger",
                                    Listen    => SOMAXCONN,
                                    Reuse     => 1);
die "can"t setup server" unless $server;

while ($client = $server->accept()) {
  $client->autoflush(1);
  $hostinfo = gethostbyaddr($client->peeraddr);
  printf "[Connect from %s]\n", $hostinfo->name || $client->peerhost;
  my $command = client->getline();
  if    ($command =~ /^uptime/) { $client->print(`uptime`); }
  elsif ($command =~ /^date/)   { $client->print(scalar localtime, "\n"); }
  else  { $client->print("Unknown command\n");
  $client->close;
}
객체지향 펄을 최대한 사용한 코드이다(보는 바와 같이 거의 매 줄마다 메소드 호출이 있음). 클라이언트를 작성할 때 사용했던 방법처럼 IO::Socket::INET->new를 생성자로 사용해서 시작했다. 여기서 이상한 점이라도 발견했는가? 여기서 IO::Socket;:INET은 패키지 이름으로 오브젝트가 아니라 클래스이다. 클래스들에도 메소드 호출을 할 수 있으며(당연하게도 보통 이것은 "클래스 메소드"라고 불림) 사실 대부분의 객체들은 이런 방법으로 인스턴스화 된다. 클래스는 new라고 불리는 메소드를 제공하며 이것을 통해 객체를 만들어 사용자가 조작할 수 있게 넘겨준다.

커다란 while 루프는 클라이언트가 연결할 때까지 기다리는 accept 메소드를 호출하며 클라이언트가 연결해 오면 클라이언트와의 연결을 표현(represent)하는 또하나의 IO::Socket::INET 객체를 되돌려준다. 이때 클라이언트의 autoflush 메소드를 호출할 수 있는데 이 메소드는 그 핸들의 $|를 셋팅하는 것과 동등하다. peeraddr 메소드는 클라이언트의 주소를 반환해주며 gethostbyaddr에 매개변수로 넘겨줄 수 있다.

앞서 언급했듯이 이것은 보통 펄에서 사용하는 gethostbyadd가 아니라 Net::Hostent가 제공하는 것으로 객체를 되돌려준다. 이 객체의 name 메소드를 이용해 주어진 호스트의 정보, 즉 호스트의 이름을 찾아내는 것이다.

나머지는 새로운 것이 아니다. 클라이언트 프로그램을 다시 생각해 본다면 이 프로그램은 한 줄을 송신하고 응답을 기다린다. 따라서 서버 프로그램은 한 줄을 읽어들이고 응답을 보내게 된다. 서버 프로그램에 응답하는 기능들을 더 추가하면 보너스 점수를 얻는 것이다.

결론

이제 끝났다. 지금까지 객체지향 모듈들을 사용하는 예제 몇 개를 살펴보았다. 그럭저럭 괜찮다는 생각이 들지 않는가? 이 기사를 읽은 독자들이 CPAN에 있는 많은 객체지향 모듈들을 사용할 준비가 되었다면 필자는 더 바랄 것이 없겠다.

만일 독자가 객체지향 펄에 대해 더 깊이 알고 싶다면 펄 문서들 중 perlboot, perltoot, perltootc 등을 살펴보기 바란다. 『펄 쿡북』은 펄 프로그래머라면 누구나 가지고 있어야 할 가치있는 책이며 객체지향 기법들을 알기 쉽고 따라하기 쉽게 설명하고 있다. 마지막으로, 이 모든 것을 가장 깊이있게 다루고 있는 책은 다미안 콘웨이(Damian Donway)의 『Object-Oriented Perl』로 완전 초보에서부터 Perl 4 나 Perl 5 사용자들도 모두 볼 수 있는 책이다.
TAG :
댓글 입력
자료실

최근 본 책0