Abstraction 관점에서 보는 프로그래밍 패러다임

개요

1~3년차 개발자이거나 프로그래밍의 기초가 부족한 사람, 프로그래밍 패러다임이 무엇인지 모르는 사람이라면 이 글을 읽어달라.

코드 재사용이 무엇인지. 특히 객체지향 설계 패러다임에서의 코드 재사용이란 무엇인지 살펴보고자 한다.

 

왜 코드를 재사용하나

코드를 재사용하는 방법은 많다. 흔히들 사용하는 컨트롤CV 패턴이라던지 깃허브다운로드 패턴도 코드를 재사용하는 한 방법이다.

누군가가 만들어둔 코드를 어떤 방식으로든지 재사용하면 그게 바로 코드 재사용이다.

허나 남이 짜둔 코드 블럭을 복사 붙여넣기로 재사용하다 보면 언젠가는 반드시 큰 코를 다치게 된다.

만일 복붙한 코드에 문제가 있어 수정을 해야 한다면? 그런데 해당 코드블럭을 100번 정도 복붙했다면? 너가 이 생이 끝나기 전까지 제대로 수정할 수 있을까?

복붙의 갯수가 늘어날 수록 프로그램의 복잡도는 쓸데없이 높아진다. 코드 재사용에도 바람직한 방법과 그렇지 않은 방법이 있다는거고, 복붙은 그리 좋은 방법이 아니라는 거다.

묻는다. 우리는 왜 코드를 재사용하나? 너는 왜 스택오버플로우에서 검색한 코드를 복붙하여 쓰는거냐?

답은 “귀찮아서” 이다.

나와 같은 문제를 해결하기 위해 고민한 누군가의 결과물을 가져다 쓰기 위함일 수도 있고, 예전에 짜둔 코드가 여기저기서 사용될 필요가 있는데 새로 만들기는 귀찮아서이기도 하다.

과거의 언어 설계자들도 반복되는 작업이 귀찮았다. 다만 복붙보다는 고급진 방법으로 문제를 해결하려 하였다.

 

아래는 어셈블리 Macro 로 문자열을 출력하는 코드를 작성한 예시이다.

Assembly Macro
; A macro with two parameters
; Implements the write system call
   %macro write_string 2
      mov   eax, 4
      mov   ebx, 1
      mov   ecx, %1
      mov   edx, %2
      int   80h
   %endmacro
 
section .text
   global _start            ;must be declared for using gcc
    
_start:                     ;tell linker entry point
   write_string msg1, len1              
   write_string msg2, len2   
   write_string msg3, len3 
    
   mov eax,1                ;system call number (sys_exit)
   int 0x80                 ;call kernel
section .data
msg1 db 'Hello, programmers!',0xA,0xD  
len1 equ $ - msg1          
msg2 db 'Welcome to the world of,', 0xA,0xD
len2 equ $- msg2
msg3 db 'Linux assembly programming! '
len3 equ $- msg3

 

내가 수행하고자 하는 어떤 기능에 이름을 붙이고, 이 기능이 필요할 때 마다 이름을 불러 사용하겠다는 아이디어는

프로그램 언어에 따라 Macro / function / sub-program / sub-routine / method 등등등 여러 가지 형태로 구현되어 불필요한 반복작업을 대체하였다.

프로그램 내에 존재하는 프로그램(이후에는 그냥 function으로 부르도록 하겠다.)으로 반복되는 문제를 해결하기 위한 프로그래밍 사조가 Structured Programming Paradigm이다.

그렇다면, Structured Programming Paradigm은 반복되는 문제들을 일소에 해결하였을까? 아니면 새로운 단계의 복잡한 문제들이 생겨났을까?

 

No Silver Bullet

프레드릭 브룩스 께서 쓴 No Silver Bullet 이라는 에세이를 요약하면 이렇다.

No Silver Bullet

요구사항이 10개인 소프트웨어는 태생적으로 10개 만큼의 복잡한 문제가 존재하고

여기에 개발자 놈들이 싸지르는 문제들이 +α 가 되기 때문에

니들이 아무리 노력해 봐야 +α 만큼의 문제를 줄이는 것 밖에는 안되겠지만 그거라도 해라.

본질적으로 소프트웨어는 복잡하다. 거기에 개발자가 복잡도를 더하지만 않으면 그것만으로도 매우 잘 설계된 소프트웨어다.

function 의 발명은 분명히 반복되는 작업을 줄여주었고, 더욱 크고 복잡한 문제들을 다룰 수 있게 하였다.

허나 문제의 복잡한 수준이 어느 정도를 넘어가면서, 새로운 차원의 문제에 봉착하게 되었다.

 

다음과 같은 요구사항을 만족하는 소프트웨어를 개발한다고 가정해 보자.

문서 편집기 소프트웨어 요구사항

a)로컬 저장소 또는 b) 클라우드 를 통해

1)엑셀 또는 2)워드 또는 3)PPT 문서 파일을 열어서,

a)로컬 저장소 또는 b)클라우드 에 저장하는 소프트웨어

가 있다고 가정해보자.

사용자가 엑셀 파일을 열지, 워드파일을 열지, PPT 파일을 열지는 소프트웨어 실행 중에 결정되기 때문에 아무도 모른다. 그때 그때 달라지는 사용자 맘이다.

또한 사용자가 파일을 로컬 저장소에 저장할지, 클라우드에 저장할지 역시 소프트웨어 실행 중에 결정되기 때문에 마찬가지로 사용자 맘이다.

 

종래의 Structured Programming Paradigm으로 이 문제를 해결하려면

1) 다루고자 하는 시스템의 갯수 x 기능의 갯수 만큼 function을 생성하여

2) 조건에 따라 적절히 분기하여, 해당 시나리오에 맞는 function을 호출

하여야 한다.

 

위의 문제의 경우, system의 갯수는 a)로컬 저장소, b)클라우드 2 개이며

기능의 갯수는 1) 엑셀 열기, 2) 엑셀 닫기, 3) 워드 열기, 4) 워드 닫기, 5) PPT 열기, 6) PPT 닫기 총 6개이다.

따라서, 구현하여야 하는 function의 총 갯수는 2 x 6 = 12 개 이다.

 

예시의 경우 system과 기능의 갯수가 많지 않기 때문에 function의 갯수가 그리 많지 않지만,

현실에서 다루는 문제들은 보통 이것보다 크고 복잡하다. 아무튼 좋다. 까짓거 함 만들어보자.

 

실제로 이 function들을 조건에 맞게 적절히 분기하는 logic을 만든다고 생각해보자.

if (open == 파일저장소) {
    if(source == 엑셀) {
        open엑셀();
    } else if (source == 워드) {
        open워드();
    } else if (source == PPT) {
        openPPT();
    }
} else if (open == 클라우드) {
    ...
}

Structured Programming Paradigm 으로는 분기되는 조건의 갯수만큼 if else 를 발라야 한다.

오케이 좋다. 백번 양보해서 12번 if else 발라가면서 개발해보자.

 

고갱님께서 앞으로는 파일 목록에 에버노트를 추가하고, 에버노트 전용 클라우드에 저장하는 기능을 추가해 달란다.

물론 에버노트 클라우드에는 기존에 존재하던 엑셀 / 워드 / PPT 도 저장이 되어야 한다.

 

c)에버노트 전용 클라우드

7)에버노트 열기 8) 에버노트 닫기

라는 system 과 기능이 추가되었기 때문에 function은 24개를 만들어야 하고 분기로직도 수정을 하여야 한다.

이 시점에서 개발자는 절로 토가 쏠린다. 머리에 피가 쏠리며 대략 정신이 멍해진다.

 

여기서 우리가 줄일 수 없는 태생적인 복잡도는 24개의 function 이다.

허나 24개의 분기처리 로직은 줄일 수 있다.

어떻게?

Object Oriented Paradigm 으로 소프트웨어를 개발하면 된다.

 

Dynamic binding 과 run time 단계에서의 Abstraction

위 프로그램에서 각 경우의 수 만큼 분기처리를 하여야만 했던 이유는 사용자가 어느 시점에 무슨 행동을 할지 몰랐기 때문이다.

사용자가 어느 시점에 뭘할지 모르는 상태, 바로 이 상태를 표현할 수 있는 방법은 없을까?

 

다음과 같은 class 를 살펴보자.

static binding
class File {
    ...
}
 
class 워드File extends File {
    void open() {...};
}
 
워드File foo = new 워드File();
foo.open();

워드File 객체 foo의 메쏘드 foo.open() 은 언제 어느 때 호출하여도 항상 워드File을 open 할 것이다.

다시 말하면, open() 메쏘드의 동작은 컴파일 타임에 이미 결정되었으며, 바뀌지 않을 것이다.

 

그러면 다음과 같은 경우를 살펴보자.

dynamic binding
interface File {
    open();
}
 
class 워드File implements File {
    void open() {...};
}
 
class 엑셀File implements File {
    void open() {...};
}
 
File foo;
if (source == 워드)
    foo = new 워드File;
else if (source == 엑셀)
    foo = new 엑셀File;
 
foo.open();

위와는 다르게, File 인터페이스 객체 foo 의 메쏘드 open() 을 호출하면 실제로 호출되는 method 는 실행 시점의 조건에 따라 그때 그때 달라진다.

source가 워드면 워드File::open() 메쏘드가 호출되고, source가 엑셀이면 엑셀File::open() 메쏘드가 호출된다.

 

아까 전의 문제로 돌아가 보자면, 우리는 사용자가 어느 시점에 뭘할지 몰랐기 때문에 복잡하게 분기처리를 하여야만 했다.

헌데 객체지향 언어에서는 사용자가 뭘 할지 모르는 상태를 위와 같이 추상화할 수 있다.

 

위의 문제에서는 사용자가

1) 클라우드를 사용할지, 로컬저장소를 사용할지 모르고

2) 엑셀을 쓸지, 워드를 쓸지, PPT 를 쓸지 모른다.

 

이 실행 중에 사용자가 뭘 쓸지 모르는 상태를 추상화해보자.

 

Polymorphism, 객체지향 설계의 핵심

이제 개쩌는 매직쇼가 벌어질 시간이다. 최초의 프로그램에선 12가지 조건에 대한 분기 처리를 하여야만 했다.

객체지향의 가장 중요한 특징인 Polymorphism 으로 분기 처리가 어떻게 바뀌는지 살펴보자.

polymorphysm
interface File {
    open();
}
 
interface Cloud {
}
 
interface Local {
}
 
interface CloudFile extends Cloud, File {
    void open();
}
 
interface LocalFile extends Local, File {
    void open();
}
 
class Cloud워드File implements CloudFile {
    void open() {...};
}
 
class Local엑셀File implements LocalFile {
    void open() {...};
}
...
 
File foo;
AbstractFileFactory fooFactory;
if (open == 파일저장소)
    fooFactory = new LocalFileFactory();
else if (open == 클라우드)
    fooFactory = new CloudFileFactory();
    
if (source == 엑셀)
    foo = fooFactory.create(엑셀);
else if (source == 워드)
    foo = fooFileFactory.create(워드);
else if (source == PPT)
    foo = fooFileFactory,create(PPT);
... 
 
foo.open();

조건문에 따른 분기가 5개로 줄었다. 여기서 에버노트 및 에버노트 클라우드가 추가된다면 조건문은 단지 2개가 더 늘어날 뿐이다.

종전에 요구사항이 추가되었을 때 총 24개의 분기처리를 해야만 했던 것에 비하면 다루어야 할 복잡도가 훨씬 간소화되었다.

Abstraction 관점에서 보는 프로그래밍 패러다임”에 대한 2개의 생각

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

w

%s에 연결하는 중