oop 객체 지향 프로그래밍(Object Oriented Programming, OOP)

|
모듈과 객체

Teodor Zlatanov
프로그래머, Gold Software Systems
2002년 1월

보다 나은 펄(Perl) 프로그래밍을 위한 완벽한 가이드를 제공하고 있다. 이번에는 객체 지향 프로그래밍(OOP, Object Oriented Programming)에 대해 설명한다. 객체 지향 프로그래밍이 무엇이고, 언제 쓰이며, 펄에서는 어떻게 작용하는지를 설명할 것이다.

객체 지향 프로그래밍(Object Oriented Programming, OOP)
OOP는 프로그래밍 방식 또는 문제를 해결에 쓰이는 일반적인 접근방식이다. 절차적 프로그래밍과 함수 프로그래밍 방식과는 연관성이 적고, 그들과 잘 맞지 않는다.

이 글에서는 펄에서 OOP와 함수적/절차적 프로그래밍의 기초를 다루겠다. 그리고 펄 프로그램과 모듈에서 OOP를 사용하는 방법을 설명하겠다. 이 글은 요약에 그칠것이라는 것을 명심하라. 펄 OOP의 모든 부분을 설명하는 것은 무리이다. 그와 같은 것을 다루는 책이 많이 나와있으며 이미 여러차례 다루어졌다.(참고자료).

OOP가 정확히 무엇인가?
OOP는 객체를 사용하여 문제를 해결하는 기술이다. 프로그래밍 용어에서 객체(object)란 속성(property)과 동작(behavior)이 문제 해결에 주요하게 작용하는 엔터티(entity)이다. 정의를 좀 더 자세하게 내려야 하겠지만, 오늘날의 컴퓨터 산업에서 OOP 접근방식의 엄청난 다양성 때문에 불가능하다.

펄 프로그래밍에서, OOP는 필수적인 것은 아니다. Perl version 5 이상 버전은 OOP를 장려하지만 필요한 것은 아니다. 모든 펄 라이브러리는 모듈이다. 적어도 OOP의 기초를 사용한다는 것을 의미한다. 게다가 대부분의 펄 라이브러리는 객체로서 구현된다. 모듈을 사용하는 사람은 특정 작동과 속성을 지닌 OOP 엔터티로서 그들을 사용해야 한다.

기본적인 OO 프로그래밍 언어 특징
일반적으로, OO 프로그래밍 언어에 있어서 기본적인 세 가지의 특징이 있다. 상속(inheritance), 다형성(polymorphism), 캡슐화(encapsulation)가 바로 그것이다.

펄은 상속을 지원한다. 상속(Inheritance) 하나의 객체(child)가 시작 포인트(parent)로서 다른 것을 사용하고 그런다음 필요할 경우에 속성과 동작을 변경할 때 적용된다. 이러한 자식-부모 관계는 OOP에 있어서 필수적이다. 재사용(reuse)은 OOP의 이점 중 하나이며 프로그래머들도 이러한 특징에 만족한다.

상속에는 두 가지 유형이 있다. 단일 상속(Single inheritance)은 자식이 단 하나의 부모를 가져야 한다. 반면 다중 상속(multiple inheritance) 은 좀 더 자유롭다. 실제로 둘 이상의 부모를 보게 될 경우는 드물지만 펄은 다중 상속을 지원한다.

다형성(Polymorphism)은 한 객체를 다른 것처럼 보이도록 만드는 기술이다. 펄에서, 다형성은 완벽히 지원된다. 펄 프로그래머들은 상속된 동작을 변경하는 것보다는 객체 속성을 가진 일반적인 작동을 변경하는 것을 더욱 선호하기 때문에 빈번하게 사용되는 것은 아니다. 234 포트의 UDP 패킷 리셉션과 트랜스미션에, 80 포트의 TCP 패킷 리셉션에, 1024 포트의 TCP 패킷 트랜스미션을 위해 세 개의 IO::Socket::INET 객체를 만드는 코드를 기대한다는 것을 의미한다. 첫 번째 경우에는 IO::Socket::INET::UDPTransceiver 를, 두 번째의 경우 IO::Socket::INET::TCPReceiver를, 세 번째의 경우 IO::Socket::TCPTransmitter를 사용하는 코드를 기대하기 보다는 말이다.

OOP 순수주의자들은 모든것이 적절하게 구별되어 한다고 느끼지만, 펄 프로그래머들은 순수주의자가 결코 아니다. 그들은 OOP 규칙에 대해 융통성을 발휘하고 있다.

캡슐화(Encapsulation)는 객체 작성자가 액세스를 허용하기를 원치 않는다면 사용자에게 액세스를 불가능하게 하는 방식으로 객체 작동과 속성을 인클로징한다는 것을 의미한다. 그러한 방식으로 객체 사용자들은 하지 못하도록 정해진 일을 수행할 수 없으며, 액세스 할 수 없도록 지정된 것에 액세스 할 수 없다(Listing 1).

왜 OOP가 강력한 방식인가?
어떻게 OOP가 강력한 방식이 될 수 있었는지 생각해보자. OOP에는 절차적/함수 프로그래밍 (PP/FP)에 적용하기에 까다로운 여러 주요 개념들이 있다는 것을 알았다. 우선, 무엇보다도, PP나 FP 모두 상속이나 클래스 다형성 개념이 없다. PP와 FP에는 클래스가 없기 때문이다. PP와 FP에서 캡슐화는 존재한다. 하지만 단지 절차적 레벨에서 존재하는 것 뿐이다. 결코 클래스나 객체 속성으로서 존재하는 것은 아니다. 따라서 프로그래머는 호환되지 않는 방식들을 섞는 것보다 전체 프로젝트에 OOP를 고수하는 것이 낫다.

OOP는 절차적 프로그래밍 방식과 함께 잘 작동하지 않는다. 왜냐하면 OOP는 객체에 집중하고 절차 프로그래밍은 프로시져(procedure)에 기반하기 때문이다. 마치 메소드(method)와 같은 프로시져는 사용자에 의해 호출되는 함수이지만 그 둘 사이에는 차이점이 없다.

프로시져는 객체 데이터를 가지고 있지 않다. 그들은 매개변수 리스트에 있는 데이터로 전달되거나 범위내에서 데이터를 사용해야 한다. 프로시져는 호출 시 이것으로 전달된 모든 데이터나 글로벌 데이터에 액세스 할 수 있다. 메소드는 오직 그들의 객체 데이터에만 액세스 해야한다. 실제로, 메소드용 함수 범위는 메소드를 포함하고 있는 객체이다.

프로시져는 글로벌 데이터를 사용하여 찾는다. 단, 절대적으로 필요할 때만 수행되어야 한다. 글로벌 데이터를 사용하는 메소드는 가능하면 빨리 다시 작성되어야 한다. 프로시져는 다른 매개변수들을 가진 다른 프로시져들을 호출한다. 메소드는 단지 몇 개의 매개변수만을 가지고 있어야 하며 다른 프로시져들 보다 자주 다른 메소드들을 호출한다.

함수 프로그래밍(FP)은 여러가지 이유로 OOP와 잘 섞이지 않는다. 가장 중요한 이유는 FP는 문제를 해결하는 데 있어서 세부적인 함수 접근방식에 기반을 두고 있고, OOP는 개념을 표시하기 위해 객체를 사용한다. FP 프로시져들은 어느 곳에서나 사용될 수 있지만, OOP 메소드는 그것을 보유하고 있는 객체내에서만 사용될 수 있다.

이제는 펄이 OOP, FP, PP 방식들을 섞는 최상의 언어인지를 설명할 수 있다.

펄에서 OOP를 절차적/함수 프로그래밍과 섞는 방법
펄은 프로그래머가 원하는 무엇이든 할 수 있도록 하기위해 길이를 한없이 늘일 수 있다. 이는 Java와 C++ 같은 언어들과는 극명하게 대조된다. 예를 들어, 펄에서 프로그래머는 이전에 선언되지 않는 변수를 자동으로 만들수 있다.

따라서 펄은 여러 방법들을 남용하기에 적합한 언어이다. 내부 객체 데이터에 액세스하고, 클래스를 변경하고, 메소드를 재 정의하는 것이 모두 가능하다. 펄의 규칙은 프로그래머들이 코딩, 디버깅, 실행 효율성에 맞게 규칙을 파괴할 수 있다. 작업을 수행하는데 도움이 된다면 괜찮다. 따라서, 펄 그 자체는 프로그래머에게 베스트 프랜드가 될 수도 최악의 적이 될 수도 있다.

규칙을 위반하는 것임에도 왜 사람들은 OOP, FP, PP를 섞기를 원하는가? 다시 근본적인 문제로 되돌아가 보자. OOP, FP, PP는 무엇인가? OOP, FP, PP는 툴이고 모든 프로그래머들이 하는 첫 번째 작업은 그들의 툴을 이해하는 것이다. 만일 프로그래머가 해시를 소팅할 때 FP Schwartzian 변형을 사용하지는 못하는데 Sort::Hashtable을 작성할 수 있거나 또는 Sys::Hostname 모듈의 재사용을 실패했지만 대신 시스템의 호스트네임을 얻기 위해 절차 코드를 작성한다면, 그 프로그래머는 시간, 노력, 돈을 낭비한 것이 되고, 코드 품질과 신뢰성도 깎인다.

프로그래밍 팀은 최상이라고 믿고 있는 툴에 만족할 수 있다. 이것은 그들에게 일어날 수 있는 최악의 일이다. 프로그래머는 효율성을 증대시키고, 더 나은 코드를 만들 수 있으며, 팀을 더욱 혁신적으로 만들수 있는 방법이 무엇이든지 그 방법을 섞을 수 있어야 한다. 펄은 이러한 태도를 인정하고 장려한다.

OOP의 효용성
OOP의 효용성을 이 글에서 설명하기에는 너무나 많다. 또한 앞서 언급했듯이 이 주제를 다루는 책들이 많이 있다.

OOP는 기본적인 클래스와 객체에 의존하기 때문에 OO 코드를 재사용한다는 것은 필요할 때 클래스를 임포팅한다는 것을 의미한다. 코드 재사용은 OOP를 사용하기 위한 가장 중요한 이유이고, OOP가 오늘날의 산업에서 중요성과 대중성을 얻을 수 있는 이유이다.

단점도 있다. 예를 들어, 이전 문제에 대한 솔루션이 현재 상황에도 이상적으로 적용되지 않을 수도 있고 형편없이 문서화 된 라이브러리는 이해하고 사용하기가 힘들어 오히려 다시쓰는 편이 더 나을 때도 있다. 시스템 아키텍트의 임무는 이러한 단점을 보완하는 것이다.

코드 품질은 OOP를 이용하여 향상된다. 캡슐화는 데이터 오염문제를 없애기 때문이다. 상속과 다형성은 새로 작성되어야 하는 코드의 복잡성을 줄인다. 코드 품질과 프로그래밍 혁신 사이에는 미묘한 균형이 있고 각 팀은 팀의 목적에 맞게 이것을 활용해야 한다.

OOP의 상속과 재사용은 코드의 일관성있는 인터페이스를 가능하게 한다. 모든 OO 코드가 일관성있는 인터페이스를 갖추어야 한다는 의미는 아니다. 프로그래머는 일반적인 아키텍쳐를 고수해야 한다. 예를들어, 확장이 가능하고 사용하기 매우 편리한 모듈식의 인터페이스를 통해 에러 기록을 위한 포맷과 인터페이스에 팀은 합의를 해야한다. 그때야 비로소 모든 프로그래머는 에러가 많은프린트 문장 대신 인터페이스를 사용한다.

적응성(adaptability)은 프로그래밍을 할 때 다소 막연한 개념이다. 개인적으로는 이것을 환경과 사용 변화의 수락과 예견으로 정의하고 싶다. 모든 소프트웨어는 진화하기 때문에 적응성은 중요하다. 잘 작성된 소프트웨어는 발전해야 한다. OOP는 모듈식의 디자인, 향상된 코드 품질, 일관성있는 인터페이스를 이용하여 새로운 오퍼레이팅 시스템이나 새로운 리포트 포맷이 핵심 아키텍쳐에 급진적 변화를 주어야하는 필요성을 감소시킨다.

펄에서 OOP 사용하기
믿거나 말거나 지만 :) 펄에서 OOP는 초급 이나 중급 레벨이 어렵지가 않다. 고급 레벨의 사용도 복잡하지 않다. 펄은 프로그래머들에게 몇 가지 제한을 둔다.

첫 번째 단계는 펄 패키지를 이해하는 것이다. 패키지는 C++ 과 Java 라이브러리에 있는 네임스페이스(namespace) 같은 것이다. 펄 패키지는 프로그래머에게 자문역할을 한다. 기본적으로 펄은 패키지들 간에 데이터 교환을 제한하지 않는다.

Listing 1. 패키지 이름, 패키지 바꾸기, 패키지들 사이에 데이터 공유, 패키지 변수

#!/usr/bin/perl
# note: the following code will generate warnings with the -w switch,
# and won't even compile with "use strict". It is meant to demonstrate
# package and lexical variables. You should always "use strict".
# pay attention to every line!
# this is a global package variable; you shouldn't have any with "use strict"
# it is implicitly in the package called "main"
$global_sound = "
";
package Cow; # the Cow package starts here
# this is a package variable, accessible from any other package as $Cow::sound
$sound = "moo";
# this is a lexical variable, accessible anywhere in this file
my $extra_sound = "stampede";
package Pig; # the Pig package starts, Cow ends
# this is a package variable, accessible from any other package as $Pig::sound
$Pig::sound = "oink";
$::global_sound = "pigs do it better"; # another "main" package variable
# we're back to the default (main) package
package main;
print "Cows go: ", $Cow::sound; # prints "moo"
print "\nPigs go: ", $Pig::sound; # prints "oink"
print "\nExtra sound: ", $extra_sound; # prints "stampede"
print "\nWhat's this I hear: ", $sound; # $main::sound is undefined!
print "\nEveryone says: ", $global_sound; # prints "pigs do it better"

파일 범위의 어휘 변수인 $extra_sound 는 세 패키지 ("main", "Pig", "Cow")액세스 할 수 있다. 이 예제에서 같은 파일 안에서 모두 정의되었기 때문이다. 일반적으로 각 패키지는 각자의 파일 안에서 정의되고 어휘 변수들은 패키지에 속하게 된다. 따라서 캡슐화가 가능하다. ( "perldoc perlmod"를 실행해보라.)

다음에는 패키지들을 클래스에 결부시켜야 한다. 펄에서의 클래스는 단지 형식적인 패키지이다. (그와는 반대로 객체는 bless() 함수로 특별하게 만들어졌다). 펄은 OOP 규칙을 융통성있게 적용하여 프로그래머는 규칙에 제약을 받지 않는다.

new() 메소드는 클래스 컨스트럭터용 이름이다. 클래스가 객체로 인스턴스화 될 때마다 호출된다.

Listing 2. barebones class

#!/usr/bin/perl -w
package Barebones;
use strict;
# this class takes no constructor parameters
sub new
{
my $classname = shift; # we know our class name
bless {}, $classname; # and bless an anonymous hash
}
1;

Listing 2에 있는 코드를 Barebones.pm 파일에 놓고 디렉토리에서 다음을 실행시켜서 테스트 할 수 있다:


perl -I. -MBarebones -e 'my $b = Barebones->new()'

예를 들어, print 문을 new() 메소드 안에 두어 $classname 변수가 무엇을 포함하고 있는지를 볼 수 있다.

Barebones->new()대신 Barebones::new()을 호출하면 클래스 이름은 new()로 전달되지 않을 것이다. 다시말해서, new() 는 컨스트럭터가 아닌 평범한 함수로서 작동한다.

Y$classname가 왜 전달되어야 하는지 궁금할 것이다. bless {}, "Barebones";라고 부르지 않는가? 상속이라는 것 때문에 이 컨스트럭터는 Barebones에서 상속되는 클래스에 의해 호출되지만 Barebones라고 하지는 않는다.

모든 클래스는 멤버 데이터와 new()와는 다른 메소드를 필요로 한다. 이를 정의하는 것은 몇 개의 프로시져를 작성하는 것 만큼 쉽다.

Listing 3. 멤버 데이터와 메소드를 가진 클래스

#!/usr/bin/perl -w
package Barebones;
use strict;
my $count = 0;
# this class takes no constructor parameters
sub new
{
my $classname = shift; # we know our class name
$count++; # remember how many objects
bless {}, $classname; # and bless an anonymous hash
}
sub count
{
my $self = shift; # this is the object itself
return $count;
}
1;

이 코드를 다음을 이용하여 테스트 할 수 있다:


perl -I. -MBarebones -e 'my $b = Barebones->new(); Barebones->new(); print $b->count'

그러면 결과 '2'를 얻는다. 컨스트럭터는 두 번 호출되고 Barebones 객체 범위가 아닌 Barebones 패키지 범위로 한정된 어휘 변수($count)를 수정한다. 객체 데이터는 객체 그 자체로 저장되어야 한다. 메소드가 호출될 때 마다 객체에 액세스 할 수 있는 이유는 객체로의 레퍼런스는 그러한 메소드에 전달된 첫 번째 매개변수이기 때문이다.

DESTROY()AUTOLOAD()같은 특정 메소드가 있는데 이것은 특정 조건에서 펄에 의해 자동으로 호출된다. AUTOLOAD()는 동적 메소드 이름을 허용하는데 사용되는 메소드이다. DESTROY()는 객체 소멸자(destructor) 이지만, 정말로 필요한 경우가 아니라면 사용하지 말아야 한다. 펄에서 소멸자를 사용하는 것은 C/C++ 프로그래머로서 자신을 생각하고 있다는 의미가 된다.

상속에 대해 살펴보자. 펄에서 이것은 @ISA 변수를 변경하여 수행된다. 클래스 이름 리스트를 그 변수에 할당하면 된다. 그것으로 충분하다. @ISA 에 무엇이든 둘 수 있다. 클래스를 Satan의 자식으로 만들 수 있다. 펄은 상관하지 않는다.

Listing 4. 상속

#!/usr/bin/perl -w
package Barebones;
# add these lines to your module's beginning, before other code or
# variable declarations
require Animal; # the parent class
@ISA = qw(Animal); # announce we're a child of Animal
# note that @ISA was left as a global default variable, and "use
# strict" comes after its declaration. That's the easiest way to do it.
use strict;
use Carp;
# make your new() method look like this:
sub new
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(); # use the parent's new() method
bless ($self, $class); # but bless $self (an Animal) as Barebones
}
1;

이것은 펄 OOP의 기본이다. 다른 많은 언어들도 연구해야 한다. 이를 주제로 다룬 책들도 많다. 참고자료 리스트도 꼼꼼히 살펴보기 바란다.

h2xs
펄 클래스를 작성하고 문서 (POD) 스켈레톤(skeleton)을 작성할 툴이 있다면 더욱 행복해질 것 같지 않은가? 펄에는 이러한 종류의 툴이 있다: h2xs.

"-A -n Module" 플래그는 중요하다. 기억하기 바란다. 이것을 이용하여, h2xs는 유용한 파일로 가득찬 "Module" 이라는 스켈레톤 디렉토리를 만든다:

  • Module.pm, 이미 작성된 스켈레톤 문서를 가진 모듈.
  • Module.xs, 모듈을 C 코드로 연결하는데 사용.
  • MANIFEST, 패키지용 파일 리스트.
  • test.pl, 스크립트를 테스트하는 스켈레톤.
  • Changes, 모듈의 변경 기록.
  • Makefile.PL, makefile 생성자(generator).

이 모든 파일을 사용할 필요는 없지만 이와 같은 것이 존재하고 있다는 것을 알아두는 것이 낫다.

참고자료

필자소개
Teodor Zlatanov는 보스턴 대학에서 컴퓨터 공학을 전공했다. 졸업 후 Perl, Java, C, C++를 사용하여 프로그램을 개발하였다.

[출처] oop 개념|작성자 보헤


And