본문 바로가기

IT

절차적 프로그래밍 vs 객체지향프로그래밍

해당 포스팅은 OOP 이해를 돕기위해 작성되었습니다.


1. 절차적 프로그래밍


절차적 프로그래밍의 정의를 보면, "루틴", "서브루틴", "메소드", "함수" 등 "프로시저"를 이용한 프로그래밍 패러다임을 뜻한다고 되어있습니다.


일단 절차적 프로그래밍을 쉽게 이야기하면,  특정 작업을 수행하기 위한 프로그램을 작성할 때, "루틴", "프로시저"으로 구성되게끔 프로그래밍


하는 프로그래밍 패러다임이라는 이야기입니다.


그럼 여기서 루틴이 무엇인가가 중요해집니다. 루틴은 무엇인가?


루틴(Routine)의 단어의 정의는 "틀에 박힌 일"입니다. 말 그래도 우리가 할 일에 대한 순서가 정해져있고, 그것이 변하지 않는다는 거죠.


프로시저(Procedure)의 단어 정의는 "순서"를 의미합니다.


이러한 루틴과 프로시저란 단어의 뜻을 숙지하고 다음 C언어 예제 코드를 봅시다. C언어를 모르셔도 상관없습니다.



1
2
3
4
5
int main(void){
 
 
    return 0;
}
cs



위의 코드는 C언어를 하시는 분은 항상 보시는 것일 거고, 


다른 언어를 쓰시는 분들도 main이라는 써져있는 어떤 것 혹은 다르게 정의되어있는 프로그램 진입점에서 프로그램을 주로 작성하실 것입니다.


해당 코드를 컴파일하면, 실행이 가능한 파일이 생성될 것이고, 해당 프로그래밍을 실행시켜본다면 실행이 되자마자 프로그램이 종료되는 것을 


확인할 수 있습니다. 


그 이유는 해당 코드의 논리 흐름이 프로그래밍을 실행시켰을 때, 그 프로그램 덩어리는 main이라는 하나의 코드 블럭(block)과 같기 때문입니다.


코드 내부 블럭 "{ }" 안을 보면, return 0;이라는 것이 보입니다. 이 return 0;이라는 구문은 프로그램이 정상적으로 종료되었음을 알리는 


0이라는 숫자를 반환하기 때문입니다.


코드 실행 순서를 보면, 


  • int main(void)의 진입점으로 진입한다.
  • 0 이라는 숫자를 반환한다.


즉, 시작하자마자 종료하겠다는 프로그램을 짠 것이라고 할 수 있지요.


이렇게 위와 같은 예제에서 main과 같이 프로그래머가 정의한 "틀에 박힌 일 역활"을 위에서 아래로 순차적으로 실행하는 것을 


"루틴", "프로시저"라고 합니다.


다시 돌아와서, 절차적 프로그래밍이란


특정 작업을 수행하기 위한 프로그램을 작성할 때, "프로시저", "루틴"으로 구성이 되게 프로그래밍하는 것을 절차적 프로그래밍이라고 합니다.


이제 조금 이해가 되시나요?



절차적 프로그래밍에는 2가지 종류의 패러다임이 있습니다.


* 제가 학습하면서 느껴지기에 이런형태의 분류가 맞는거 같아서  분류는 하지만 아직 해당부분은 개념이 정확하지 않으니 참고수준에서만 

어주시고 틀렸다고 생각되시면 언제든지 알려주시면 감사하겠습니다.


  • 구조적 프로그래밍
  • 비구조적 프로그래밍

1). 비구조적 프로그래밍


비구조적 프로그래밍의 위키피디아 정의를 보면, 하나의 연속된 덩어리에 모든 코드를 넣는 프로그래밍 패러다임이라고 정의하고 있습니다.

절차적인 프로그래밍에서 비구조적 프로그래밍이라고 한다면, 하나의 프로시저 혹은 루틴에 모든 코드를 넣는 프로그래밍이라고 이해하면 

될 것 같습니다.

비구조적인 프로그래밍에서 코드의 특정 부분으로 건너뛰는 흐름제어는 GOTO문에 의존합니다.

장점으로는 속도적인 측면에서 약간 유리하며 단점으로는 디버그가 굉장히 어려워집니다. 장점은 조금 있다가 루틴, 서브 루틴, 함수를


설명할 때, 자세하게 설명하도록 하고, 단점 부분에서 왜 디버그가 어려워지냐 하면,


이유는 흐름 제어시 GOTO문에 의존하게 되는데, GOTO문을 남발하게 되면, 코드의 로직 순서가 GOTO 인해 엉망이 되어 


스파게티코드가 됩니다. 이런 스파게티 코드는 로직의 흐름이 엉망이 되기 때문에 디버그시 로직의 흐름과 값의 변화를 비교해가면서 점검할 ,


매우 어려워지는 문제때문이지 않나 생각하고있습니다.


비구적인 프로그래밍의 예를 보겠습니다.


10 i = 0
20 i = i + 1
30 PRINT i; " squared = "; i * i
40 IF i >= 10 THEN GOTO 60
50 GOTO 20
60 PRINT "Program Completed."
70 END


위 코드의 실행 순서는 다음과 같습니다.


  • 10번: 변수 i를 0으로 초기화합니다.
  • 20번: 변수 i를 기존 변수 i의 값과 1을 더한 값으로 갱신합니다.
  • 30번: 변수 i의 값과 i의 제곱된 값을 출력합니다.
  • 40번: 만약 변수 i의 값이 10보다 크거나 같다면 60번으로 이동합니다. 변수 i의 값이 10보다 작다면 그냥 지나칩니다.
  • 50번: 20번으로 이동합니다.
  • 60번: "Program Completed"를 출력합니다.
  • 프로그램이 종료됩니다.

위의 코드를 위에서 아래로 읽다보면 도중에 순서(위에서 아래)를 역행해 다시 20번으로 돌아가는 모습을 확인할 수 있습니다.

코드가 커지고, 이러한 형태의 GOTO가 많아지면 많아질수록 프로그래머는 해당 프로그램을 이해하기 위해서 코드를 위아래로 왔다갔다

하면서 해당 프로그램을 이해하기 위한 시간을 많이 쓰게 됩니다. 이를 가독성이 나쁘다라고 이야기합니다.

디버그 시에도 변수값을 확인해가면서 코드의 흐름을 읽을 때, 같은 방식으로 코드를 읽어 디버그가 힘들게 됩니다.


2). 구조적 프로그래밍


이런 비구조적 프로그래밍의 단점을 보완하고자 구조적 프로그래밍 패러다임이 등장하게 되었습니다.


구조적 프로그래밍 이론은 순차, 분기, 반복만으로 계산 가능한 함수를 표현할 있다 구조적 프로그래밍 정리가 토대입니다.


구조적 프로그래밍은 구조화 기법이랑 방법론에 따라서 세가지 종류로 나뉩니다.


  • 잭슨의 구조적 프로그래밍
  • 데이크스트라의 구조적 프로그래밍
  • 데이크스트라의 관점에서 파생된 관점


일반적으로 사람들이 구조적 프로그래밍이라고 이야기하면 "잭슨의 구조적 프로그래밍" "데이크스트라의 구조적 프로그래밍" 의미합니다.


여기서는 일반적인 구조적 프로그래밍에 대해서 언급하겠습니다.


구조적 프로그래밍은 "저수준 관점" "고수준 관점"으로 나뉩니다.


2)-1 저수준 관점


저수준 관점에서 구조적 프로그래밍은 간단하고, 계층적인 프로그램 제어 구조로 구성됩니다.


이러한 제어 구조는 다음과 같은 종류가있습니다.


  • 순차(concatenation)

: 구문 순서에 따라서 순서대로 실행하는 것을 의미합니다

  • 선택(selection)

: if, else, if else, switch, case 같은 조건문을 의미합니다.

  • 반복(repetition)

: while, for, do while 등과 같은 반복문을 의미합니다.



2)-2. 고수준 관점


코드 작성자는 조각의 코드를 이해하기 쉬운 작은 하부 프로그램 (함수, 메소드, 블록) 등으로 나누어야 합니다.


일반적으로 전역 변수는 전역 변수를 거의 사용하지 않아야 하며, 하부 프로그램들은 지역 변수나, 값이나, 참조에 의한 인자를 받아야 합니다.


이러한 기법은 전체 프로그램을 이해하지 않고, 분리된 작은 코드 조각을 쉽게 이해하는데 도움이 됩니다.




이러한 구조적 프로그래밍의 설계는 하향식 설계(Top-Down) 관련이 있습니다. 설계자는  프로그램을 작은 프로그램으로 나누고


이를 개별적으로 구현, 테스트 , 합치는 방식을 택합니다.


이러한 구조적 프로그래밍은 다음과 같은 장점을 갖습니다.


  • 함수를 이용한 코드의 재사용성 증가
  • 메인 프로시저 뿐만 아니라 함수 내의 호출을 이용한 여러 구현부 생략으로 인한 프로그램 흐름 간략화, 가독성 증가
  • 모듈화, 구조화가 더 용이해져 대규모 프로그래밍에서 이점을 갖음

그리고 다음과 같은 단점이 있습니다.


  • 프로시저 호출 시, 직접 메인 프로시저에서 코드를 쓰는 것(인라인)보다 느리다.

3). 서브루틴, 함수


자 이제 맨 위에서 언급했던, 서브루틴이 무엇인지, 그리고 함수가 무엇인지를 정리할 시간이 되었습니다.


서두에 이야기했던 다음과 같은 예제에서 main은 메인 루틴이라고 했습니다.



1
2
3
4
int main(void){
 
    return 0;
}
cs



메인 루틴(Main Routine)과 서브 루틴(Sub Routine). 뭔가 비슷하지 않나요?


루틴이라는 용어를 같이 쓰고, 메인과 서브만 차이 있을 뿐입니다.


다음 예제를 봅시다.



1
2
3
4
5
6
7
8
int function (int x, int y){
    return x + y;
}
 
int main(void){
 
    return 0;
}
cs



main이라는 것과 function이라는 것이 보일 겁니다. 여기서 main은 메인 루틴이, function은 서브 루틴이 됩니다. 


서브 루틴은 메인 루틴과 마찬가지로 진입점에서 return 까지 혹은 블록 끝까지 순차적으로 실행되는 것에 있어서 같은 성격을 갖습니다.


서브 루틴은 구조적 프로그래밍 고차원 관점에서 큰 프로그램(하나의 프로시저)에서 작은 프로그램(여러개의 프로시저)로 나누는데


사용됩니다. 다음 예제를 봅시다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
int function (int x, int y){
    return x + y;
}
 
int main(void){
 
    int tmp = 0;
 
    tmp = function(35);
 
    print("value : %d\n", tmp);
 
    return 0;
}
cs



프로그램이 시작되면 main이라는 메인 루틴 진입점에서 시작되서 위에서 아래로 코드가 순차적으로 실행됩니다.


예제의 실행 순서는 다음과 같습니다.


  • 메인 루틴 진입점 진입
  • int형 변수 tmp를 생성하고 tmp의 값으로 0을 대입함
  • 인자 3과 5을 전달하면서 서브 루틴 function 진입점으로 진입
  • 인자 3과 5를 더하고 그 결과를 반환함
  • 다시 메인 루틴으로 돌아와서 서브 루틴으로부터 반환된 값을 tmp에 대입함
  • tmp의 값을 출력함
  • 성공적으로 프로그램이 실행되고 종료되었다는 의미로 0을 반환하고 프로그램(메인 루틴)을 종료함

이러한 로직의 흐름을 메인 루틴에서 모두 표현할 수 있지만, 이를 서브루틴 function을 이용하여 두개의 프로시저로 나누어서 실행하고 있음을

확인할 수 있습니다.

이런 형태로 서브루틴은 사용됩니다.


C언어에서 서브루틴과 함수는 같은 의미로 사용되나, 실제로 포트란이나 파스칼에서는 서브 루틴과 함수의 의미는 반환값이 있다면 함수로

반환값이 없다면 서브 루틴으로 정의하고 있습니다.


자 그럼, 메인 루틴을 주축으로 서브루틴의 조합으로 구조적인 프로그래밍을 하는 건 알겠고, 이러한 구조적인 프로그래밍이 가독성 및 

작은 프로그램으로 쪼개는 것을 통해서 프로그램 전체를 이해하지 않고 일부만 보더라도 부분적인 코드를 이해할 수 있고, 이로 인해서 

대규모 프로그램을 작성할 때, 많은 사람들이 각자 자신이 맡은 부분에 대한 프로시저를 작성해서 합치면 되는 건 직관적으로 알겠는데, 

속도는 왜 느려지는가? 에 대해서 의문이 생길 수 있습니다.


그 이유는 서브 루틴을 사용하는 방식과, 메인 루틴에서 서브 루틴으로 넘어갈 때, 실제 일어나는 일때문입니다.

일단 코드를 메인 루틴에 작성하게 된다면, 다음과 같이 작성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
 
int main(void){
 
    int tmp = 0;
 
    tmp = 3+5;
 
    print("value : %d\n", tmp);
 
    return 0;
}
cs


코드의 실행 순서는 다음과 같습니다.
* 실은 이보다 더 복잡하겠지만, 이해를 돕고자 간략화하였습니다.
  • 메인 루틴 진입점을 생성합니다.
  • 코드를 읽어들여서 명령어 대기열에 명령어를 6가지를 넣습니다. 
: int형 데이터 타입 tmp 선언
: 0을 tmp에 대입
: 3+5를 계산
: 계산 결과를 tmp에 대입
: 출력
: 0을 반환
  • int형 변수 tmp를 생성하고 tmp의 값으로 0을 대입함
  • 인자 3과 5를 더하고 그 결과를 tmp에 대입함
  • tmp의 값을 출력함
  • 성공적으로 프로그램이 실행되고 종료되었다는 의미로 0을 반환하고 프로그램(메인 루틴)을 종료함


그리고 메인 루틴과 서브 루틴으로 된 예제 코드와 실행 순서를 자세하게 다시 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int function (int x, int y){
    return x + y;
}
 
int main(void){
 
    int tmp = 0;
 
    tmp = function(35);
 
    print("value : %d\n", tmp);
 
    return 0;
}
cs


  • 메인 루틴 진입점을 생성
  • 서브 루틴 진입점을 생성
  • 코드를 읽어들여, 명령어 대기열에 8가지를 넣습니다.
: int형 변수 tmp를 선언
: tmp에 0을 대입
: 현태 메인 루틴의 상태를 저장하고, 메인 루틴의 상태, 돌아올 메인 루틴의 주소, 인자 3과 5를 저장된 서브 루틴 진입점에 전달
: 전달 받은 인자를 계산
: 계산된 결과를 돌아가야하는 메인 루틴의 주소로 전달
: 서브 루틴으로 반환된 값을 tmp에 대입
: tmp의 값을 출력
: 0을 반환
  • int형 변수 tmp를 생성하고 tmp의 값으로 0을 대입함
  • 인자 3과 5을 전달하면서 서브 루틴 function 진입점으로 진입
  • 인자 3과 5를 더하고 그 결과를 반환함
  • 다시 메인 루틴으로 돌아와서 서브 루틴으로부터 반환된 값을 tmp에 대입함
  • tmp의 값을 출력함
  • 성공적으로 프로그램이 실행되고 종료되었다는 의미로 0을 반환하고 프로그램(메인 루틴)을 종료함

이렇듯, 서브루틴을 사용하냐 아니냐의 차이는 처리해야하는 명령의 양이 절대적으로 늘어나기 때문에, 속도적인 측면에서 차이가 

날 수 밖에 없습니다.


그러면 왜 이러한 서브 루틴은 어떤 이점을 갖는가 설명을 드리자면, 구조적은 프로그래밍의 장점에서, 코드의 가독성, 추상화, 재사용이 

가능해지게 돕는 역활 그리고 그 외에도 중요한 한가지가 더 있습니다.


그것은 바로 실행 컨텍스트(Execution Context)에 대한 것입니다. 실행 컨텍스트가 무엇이냐면, 아까 제가 코드의 순서를 설명할 때,

명령어 대기열이라는 용어를 쓴 것을 확인할 수 있을 겁니다. 명령이 실행되기 전에 명령을 위한 대기소가 있는데, 이를 명령어 대기열이라고 하고

콜스택(Call Stack)이라고 부릅니다. 콜스택은 하나의 메모리 자료구조이고, 콜스택의 갯수와 하나당 담을 수 있은 명령어의 집합에 대해서 

크기 제한을 가지고 있습니다. 이 때, 콜스택에 들어가는 명령어 집합들 덩어리 하나를 실행 컨텍스트라고 합니다.


즉, 한 프로시저를 실행하기 위한 프로시저의 주소와 해당 프로시저가 포함하고 있는 명령어들의 집합을 하나의 덩어리(실행 컨텍스트)로 만들어

콜스택에 저장하게 되고, 자신이 실행해야되는 순서가 오면 해당 프로시저는 실행됩니다.

하지만 앞에서 이야기했듯이 실행 컨텍스트를 담는 메모리는 크기의 제한이 있습니다. 이 말은 하나의 프로시저에서 실행되는 명령어 집합들의 

갯수에 대한 제한이 있다는 의미가 됩니다.

이렇게 한 프로시저에 담을 수 있는 명령어들의 집합에 대한 제한이 있다는 의미는 하나의 프로시저에서 대단위의 프로그래밍을 작성하게 되면

이를 수행할 수 없다는 의미입니다.


이러한 문제를 서브 루틴을 이용해서 분산시킴으로 해결할 수 있습니다. 그래서 서브 루틴은 가독성, 추상화, 재사용 외에도 실행 컨텍스트 측면

에서 엄청난 이득이 있습니다.


[Reference]













자 이것으로 제가 이번 포스팅에서 설명하고자 했던, 절차적 프로그래밍과 그를 설명하기 위한 용어들에 대한 내용 정리를 마쳤습니다.

조금이나마 이해가 되실지 잘 모르겠습니다.


이것으로 이번 포스팅은 마치고 다음 포스팅은 절차지향 프로그래밍과는 반대되는 객체 지향 프로그래밍에 대해서 이야기해보겠습니다.