Stored Programming Computer

폰노이만의 저장식 개념과 프로그래밍 함수(Function)

by Younggi Seo



포인터 개념을 완전히 이해하기 위해서 언급한 '폰 노이만'의 저장식 프로그램 개념에 대한 글이다.






그전에 WBC 대회에서 한국과 도미니카 공화국과의 8강 전이 있었는데, 류현진이 3점 먹고 마운드를 내려가는 장면을 잠깐 보고, 아침을 먹자마자 바로 스터디 카페로 향했다. (도미니카의) 난타전이 예상되어, 금일 스터디 목표로 C언어 기본기를 환기(포인터와 배열, 배열을 처리하는 함수의 개념) 하기 위해 몇 가지 코딩 예제들을 복기하러 말이다.


대략 3시간 정도 답안을 확인하지 않고 오롯이 본인의 머릿속 개념을 다시 끄집어내기 위해 코딩에만 열중하니, 계획한 단원('혼자서 공부하는 C언어'의 9강에서 10강)까지 마무리했다.


정오가 넘어 집에 도착하자, 0:10이라는 콜드게임으로 진 야구 경기 결과가 기다리고 있었다. 씁쓸했지만, 오전에 코딩하는데 시간 투자하기를 잘한 거라고 위안 삼았다.







예전(정말 오래전)에 필자가 학교 대표로 프로그래밍 경진 대회에 나간 적이 있었다. 당시에 컴퓨터 학원에 다니는 이가 학교에서 별로 없는지라, 학원에서 국가 컴퓨터 급수 시험을 5급부터 1급까지 모조리 딴 경험을 인정해 줘 담임선생님이 추천해 주신 거 같았다. 좀 큰 대회였는지, 교감 선생님이 따로 불러서 잘해라고 언질까지 해주셨다(놀러 가는 분위기 유지;).


당시에 주로 출제되었던 문제를 어렴풋이 복기하자면 순서도를 보고 빈칸의 사각형이나 마름모에 코딩해야 할 구문을 맞추거나, 아니면 반복문의 흐름을 보고 최종 출력될 값을 알아맞히는 거였던 걸로 기억한다. 그 당시 퍼스널 컴퓨터에서 기본으로 구동되는 GW-BASIC 문제 유형들의 코딩 문제들을 토대로 어렵다면 베이식의 배열(dim) 문법까지 나와서 연산 과정이나 결과를 기입하는 문제들인데, 지금의 정처기 실기 코딩 문제도 이와 같은 유형의 손코딩 문제들이다.


그러니깐 30년 전이나(필자 초등학생시절) 지금이나, 코딩 문제의 수준은 별반 다르지 않다. 단지 컴퓨터 언어만 바뀐 채 그 언어의 문법이나 구문에 익숙하다면 규칙적인 순서(멘사 IQ 테스트 문제 같은)에 따라 최종값을 예상할 줄 알면 된다. 그러니 수학(수열이나 행렬 정도?)이 어느 정도 기반이 되면 편하다. 다만, 프로그램이 어떻게 작동하는가에 대해서는 등한 시 한채, 당시에 학원에서 가르쳐줬던 선생님은 채점만 할 뿐이었다. 혼자서 잡지('마소*')에서 국내 전문가들이 논하는 C++(최초의 객체지향 개념을 보고 그 이후 구독하지 않았다.)의 소스 코드 이면에서의 프로그램 작동방식의 설명은 너무 어려워서 '코알못'이었다.



*마소 : IT 프로그래밍 잡지, '마이크로 소프트웨어'의 준말로 2017년에 재창간했다.



두서없이 필자의 어린 시절부터 막 적어 내려갔는데, 결국 프로그래밍 언어 자체의 문법(Syntax)을 각 언어마다 파는 게 중요한 게 아니라, 폴리글랏*(Polyglot) 프로그래밍을 할 줄 알려면 코딩 이면의 프로그램이 어떻게 돌아가는지를 잘 알아야 한다. 이 개념은 어떤 프로그래밍 언어라도 매한가지일 수도 있기 때문이다.



*폴리글랏 프로그래밍 : 여러 가지 프로그래밍 언어를 한 번에 다루줄 아는 능력으로, 서비스 개발에서 1개의 언어만으로는 복잡한 요구사항을 맞추기 어려울 때, 여러 프로그래밍 언어(예: Java, Python, Node.js)를 조합하여 높은 퀄리티와 빠른 개발 속도를 달성하는 방식.

장점: 다양한 프로그래밍 패러다임을 접함으로써 개발자의 관점을 넓히고, 최적의 도구를 선택하여 문제를 해결할 수 있음

주의점: 언어의 표면적인 이해를 넘어 깊이 있는 지식이 없으면 오히려 성능 저하나 메모리 누수 등의 문제를 유발할 수 있음



예를 들어, C언어에서 함수를 호출하는 기본 구조다.

void func(int *pa, int *pb); // 함수 선언
(...)
int main(void){
void func(&a, &b); // 함수 호출(매개변수에 대입시킬 인자(변수 주솟값) 대입)
}
void func(int *pa, int *pb) // 함수의 매개변수(포인터 변수)를 통해 인자 패싱
{
...
}


저걸 보고 프로그램이 돌아갈 때 하드웨어 단(메모리)에서 무엇이 어떻게 흐르는지 대강 감을 잡을 수 있다면, 포인터 배열이나 힙의 메모리 영역 확보(malloc() 함수)할 시, 좀 더 주의 깊게 신경 쓸 수 있다. 이게 아마 중고급 레벨의 개발자가 되기 위한 프로그래머의 기본기인 걸로 안다. 하지만 이런 개념을 일깨우고 코딩을 전문적으로 가르치는 교육은 필자가 일전에 추천한 POCU 아카데미 외에는 거의 없다고 봐도 무방하다. 베이식 언어가 대중화되고 어연 30여 년이 지난 지금 이때도 애플리케이션 레벨의 수없는 추상화 레이어에 휩싸여 이런 저수준 프로세싱(프로시져, 루틴, 함수 다 같은 말이다.)을 누가 언질해준다 말인가?


다시 위의 소스 코드로 돌아가서 필자가 생각나는 수순 내에서 몇 가지 정리하자면, 함수에서 포인터 변수를 선언하는 까닭은 매개변수의 주소값을 찾아가서 그 주소에 있는 인자를 가져오기 위해서다. 이것을 함수 호출이라고 한다. 이때 보통의 퍼스널 컴퓨터 아키텍처, 즉 폰 노이만 저장식 컴퓨터에서는 메모리의 스택과 힙을 사용한다.


스택이라 함은 정처기 이론에서 LIFO로 유명한 메모리고, 힙이라 함은 최댓값/최솟값을 찾아내는 완전 이진트리 자료구조로, 주로 우선순위 큐 구현(선입선출(FIFO) 큐와 다름)에 사용한 메모리인 거 시험공부 좀 한 사람이면 다 안다. 그런데 왜 Last In First Out이고, 왜 우선수위 큐 구현 알고리즘 순서가 필요한 지는 개론서에서도 없다. 보통의 스택 메모리에는 함수의 주소값이 저장되기 때문에 함수가 한 번 호출(함수 내부의 프로세스가 완료)된 이후에 다시 원점(엔트리 포인트)으로 되돌아가기 위해서는 그 본래 소스코드의 순서를 기억하고 있는 마지막에 들어간 값(스택 프레임)이 맨 처음에 호출되어야 한다. 그래서 스택의 LIFO(Last In First Out) 알고리즘을 사용하는 메모리 구조가 필요하고, 지역변수도 주소값에 이어 저장된다.


힙이라 함은 필요한 데이터, 즉 먼저 들어간 데이터(전역 변수)가 루틴이 끝난 이후, 먼저 나가는 구조다. 이를 테면, 메시징 시스템은 저장해야 할 메시지 개수나 각 메시지의 크기를 미리 알 수 없다. 아래 그림처럼 동적 데이터(앞서 말한 프로그램이 실행하기 전에는 알 수 없는 값)는 주로 정적 데이터(미리 정해진 로컬 인자와 프로세스 상태)가 차지하는 영역의 바로 위 영역에 쌓이며(2차원으로 바라보자), 이를 힙(heap)이라고 부른다.


스택과 힙이 충돌하지 않게 하는 게 중요한데, 그 역할을 인터럽트 벡터가 한다.


그러면 여기서 포인터는 무엇인가? 바로 메모리의 스택영역에서 메모리 주소값을 가리키는(담는) 참조 변수다. 이 포인터를 함부로 남용하지 하지 않는 것도 중요하지만, 썼으면 동적 메모리(heap영역)에서 가비지 컬렉터도 할 줄 알아야 안다(free() 함수). LISP 프로그래밍을 개발한 미국의 존 매카시가 가비지 컬렉션(메모리 찌꺼기 수거)을 발명했는데, 부분적으로는 잘못된 포인터 사용에 대한 후회로 인해 자바나 C#에 있는 가비지 컬렉션이 떠오르기 시작했다(Steinhart, 2019).



근래에 들어 폰노이만 저장식(Von Nohiman stored computer)*의 아키텍처에서의 데이터 버스의 양방향성으로 인한 병목현상을 해결하기 위해 하바드 아키텍처*의 장점을 수용해서 만든 하이브리드 아키텍처가 아래와 같이 아키텍처의 변화가 생겼다. 여기서 폰노이만 설계 구조와 하바드 설계 구조의 유일한 차이는 메모리 배열뿐이다(Steinhart, 2019).


* 폰노이만 저장식(Von Neumann stored computer)

헝가리계 미국인이며 과학자인 존 폰 노이만(1903~1957)의 이름을 본 뜻 아키텍처.

* 하바드 아키텍처(Havard)

하버드 마크 I 컴퓨터의 이름을 본뜬 아키텍처.




개발자가 되려면 결국엔 반드시 짚고 넘어가야 할, 프로그램 작동방식에 대해서 많이 할애해 봤는데, C를 근래 정처기 실기를 대비한다고 배우면서 느낀 점이 컸다. 필자에게는 이 메모리가 어떻게 쓰이고 해제되는지의 포인터 역할을 그림으로 그려가면서(책 속의 그림) 배열값들과 변숫값을 추적하는 맛이 재밌긴 해서 지면을 많이 할애하더라도 환기했다. 틀린 부분이 있으면 댓글로 언제든지 지적해 주시면 감사하겠다.



References

1) Steinhart., J. E. (2021). The Secret Life of Programs: Understand Computers - Craft Better Code (1st ed., Vol. 1). No Starch Press.


2)

3)


1. 폰노이만 구조의 계승 (기본 원칙)

프로그램 내장 방식: 폰노이만 저장방식의 핵심인 '메모리에 명령어와 데이터를 함께 저장한다'는 원칙은 여전히 유지됨. 이 덕분에 우리는 하드웨어를 교체하지 않고도 소프트웨어만 설치해서 문서 작성, 게임, 코딩 등 다양한 작업을 수행할 수 있음.

2. 현대 컴퓨터의 진화: 하이브리드 구조

현대 CPU는 폰노이만 구조의 고질적 문제인 '폰노이만 병목현상'(CPU와 메모리 사이의 전송 속도 지연)을 해결하기 위해 하버드 아키텍처(Harvard Architecture)의 장점을 결합함.

변형된 하버드 아키텍처 (Modified Harvard Architecture): 외부(메모리)와 메인 메모리(RAM)는 여전히 하나로 합쳐져 있어 폰노이만 방식을 따름. 단, 내부(CPU 캐시) 같은 경우 CPU 내부의 L1 캐시 단계에서는 명령어(Instruction) 전용 캐시와 데이터(Data) 전용 캐시를 분리하여 동시에 읽어올 수 있게 함.

이를 통해 겉으로는 폰노이만 방식의 유연성을 유지하면서, 내부적으로는 하버드 방식의 속도를 챙기는 '하이브리드 형태'를 띠게 됨.


4)