내가 정말정말 하고 싶은 말이 있었습니다.


그래서, 머릿속에 있는 것을 줄줄이 적어 보려고 합니다.


그런 사람 없겠지만, 흥미가 있으신 분은 순서대로 읽어 주세요.


[컴퓨터 프로그램]


우리 일상에서 프로그램이라는 말은, 계획이라는 의미로 쓰입니다.


텔레비전 방송의 편성에서 쓰는 TV 프로그램이라는 말처럼요.


컴퓨터 프로그래머가 하는 일은, TV 프로그램의 시청자인 사람 대신, 소위 운영체제라고 부르는 컴퓨터 시스템이 독해해서 자동으로 어떤 일을 처리할 수 있도록, 컴퓨터 언어로 지시하는 명령 프로그램을 작성하는 것입니다.


그 컴퓨터 프로그래머의 작업의 결과물을, 컴퓨터 프로그램이라고 부릅니다.


[전통적인 프로그램의 빌드 과정]


자바스크립트, 파이썬, 매트랩, 베이직 등.


런타임에 구문이 한 줄 한 줄 해석되는 프로그램들이 있죠.


그런 것들은 전통적이고 전형적이며 정석적인 프로그램이 아닙니다.


인터프리터 언어로 작성된 프로그램은, 컴퓨터의 시각이 아닌 철저하게 사람의 시각에서 작성된 의사 코드에 가깝죠.


진정한 프로그래머는 모든 정의가 공리계에서 절대적이며 정적으로 결정되는 컴파일 언어를 이용해서 프로그램을 작성합니다.


그런 프로그램은 어떻게 빌드가 되나요.


1) 빌드 버튼을 누른다.


2) 그런 뒤 어떤 마법이 벌어진다.


3) 짠 하고 실행 파일이 만들어진다.


네, 전혀 아닙니다.


두 번째로 언급하는 표현입니다만, <진정한 프로그래머>라고 한다면, 컴파일과 링킹에 대한 인식은 필수입니다.


[1단계: 컴파일]


C언어로 작성된 프로그램이라면, 빌드는 파일 단위로 각각이 구분되는 소스 파일목적 파일로 컴파일하는 것이 첫 단계입니다.


(참고할 수 있게 덧붙이자면, 관례적으로, 소스 파일의 확장자는 .c고, 목적 파일의 확장자는 .o입니다.)


컴파일러가 생성한 각각의 목적 파일은 저수준의 명령어(코드; 인스트럭션)와, 소위 상수라고 부르는 정적 데이터가 기록된 바이너리입니다.


[2단계: 런타임의 고려 - 프로세스]


만약 이 바이너리를 리눅스나, 윈도우즈 같은 운영체제가 실행(execute)한다면, 운영체제는 그 프로그램의 수행에 필요한 물리적 자원들을 준비합니다.


그 물리적 자원이란 내가 알기로, CPU, 메모리, 기타 입출력이라고 하는 딱 세 종류밖에 없습니다.


이 때 현대적인 보통의 시스템(운영체제)들은 그 자원들의 시간이던지 공간이던지를 일부 쪼개서 하나의 프로그램에 할애하는 동작을 하는데, 이를 가리켜서 자원을 할당(allocate)한다는 표현을 씁니다.


그리하여 운영체제는 그 프로그램의 수행을 위해서 할당한 자원들을 프로그램별로 묶어서, 프로세스라는 별도의 명칭으로 구분합니다.


프로그램을 컴파일하기까지 프로그래머가 작성해서 모든 것을 결정해 둔, 변치 않는 그 계획을 프로그램이라고 하고, 프로그램이 수행되면서 운영체제가 그 프로그램을 위해서 할당하는 자원을 프로세스라고 부릅니다.


그리고 그 프로세스의 자원을 나누어서 들여다보면, 컴파일된 바이너리 안에는 들어있지 않던 힙이나 스택 따위의 아주 중요한 데이터 영역들이 가상 메모리 안에 둥지를 틀고 있습니다.


컴퓨터 프로그래머가 프로그램을 컴파일하기 전까지 작성하여, 컴파일되는 순간(컴파일 타임)에 최종 결정하고 그 뒤로 변하지 않는 프로그램의 여러가지 요소를 정적(static)이라고 합니다.


그 반대로, 레지스터나 힙이나 스택의 상태 따위를 내포하는 프로세스처럼, 그 프로그램의 수행시간(런타임)이 되지 않고는 알 수 없는 것들을 동적(dynamic)이라고 합니다.


그리고 정적인 영역동적인 영역의 구분이란, 프로그래머에게는 떼놓을 수 없는 맥락(배경 지식)입니다.


왜냐하면 컴퓨터 프로그래머는 그들을 고려하지 않고는 제대로 프로그램을 만들 수 없기 때문입니다.


[3단계: 링킹]


리눅스의 Bash로 이런 명령어를 실행하는 경우를 상정해 봅시다.


$ gcc in.c -o out.o
$ ./out.o


이제 중요한 이야기입니다.


한 단위의 소스 파일을 컴파일하여 생성된 바이너리는 컴파일된 즉시 그 자리에서 그대로 바로 실행할 수 있는 경우도 있지만, 그 파일 자기 자신 내부의 의존관계 이외에 외부 의존관계가 있다면, 그 외부 의존관계의 주소를 알지 못한 채로는 실행을 할 수가 없습니다.


의존관계(dependency)라는 것은 역시 코드와 데이터라는 한 세트가 기록된 다른 바이너리인데, 한 바이너리가 다른 바이너리의 코드나 데이터를 빌려 와서 쓰고 있는 경우에 그것을 의존관계라고 합니다.


그래서 의존관계의 주소 정보를 바이너리에게 제공하는 작업이 필요하고, 마치 둘 사이에 중매쟁이처럼 다리를 놓고 연결시켜 주는 것 같다고 해서 그것을 (의존관계의) 링킹이라고 부릅니다.


그리고 의존관계 역시 정적인 것과 동적인 것이 있어서, 그 대상에 따라 링킹도 정적 링킹과 동적 링킹으로 구분됩니다.


[동적 링킹]


동적인 의존관계는 운영체제의 메모리 안에 들어있기 때문에, 그 주소를 획득하기 위해서는 해당 의존관계를 로드하는 API를 런타임에 호출하는 방법을 씁니다.


동적 의존관계를 로드하는 API라면, Win32 APILoadLibrary()가 그 예시입니다.


바이너리 형태의 동적 의존관계는, 윈도우즈에서는 .DLL(동적 링킹 라이브러리 형식)이라고 하고, 리눅스에서는 .SO(Shared Object) 확장자로 통합니다.


단순하고 간편한 기법이어서, 동적 링킹에 대해서는 그만큼 이야기가 짧고 간단하네요.


여담으로, 이런 상반된 태도가 있는데, 일반 유저들에게는 DLL이라고 하면 전혀 쓸모도 없고 갖가지 런타임 에러를 연상시켜서 치가 떨리는 골칫덩이라는 인식이 흔하지만, 반대로 프로그래머들은 동적 라이브러리라고 하면 어쩐지 반갑고 환영하는 기분이 된다고 합니다.


[정적 링킹]


동적 링킹이나 정적 링킹이나 바이너리에게 어떤 의존관계의 주소를 가르쳐 주는 작업이라는 점은 다르지 않지만, 그 링킹의 시점에 차이가 있습니다.


동적 링킹이 런타임에 주소를 얻어오는 방법이라면, 정적 링킹은 바이너리 파일 그 자체의 내부에 미리 주소를 새겨 넣어 주는 기법입니다.


서로 다른 바이너리 파일을 하나로 합쳐 버리면 런타임이 되지 않고도 파일 그 자체에서, 의존관계의 주소를 계산해 얻어 낼 수 있겠죠.


그렇게 주소를 알 수 있도록 바이너리 파일을 하나로 엮는 것이 정적 링킹입니다.


정적 링킹의 대상이 되는 바이너리들은 .o 확장자 형식의 보통 목적 파일이거나, 윈도우즈에서 .lib이며 리눅스에서 .a로 통용되는 정적 라이브러리 형식일 것입니다.


의존관계가 컴파일 타임에 정해지니까, 프로그래밍 언어를 통해서도 이것들을 정의(표현)할 수 있습니다.


정적 링킹된 의존관계를 함수나 변수 따위의 데이터 덩어리 단위로 불러와서 쓰기 위한 C언어식 표현법으로 '선언 구문'이 있습니다.


그리고 그 링킹의 편의를 위해서 선언문을 따로 적어 넣는 파일이 바로 .h 확장자가 붙는 헤더 파일입니다.


보통 헤더에서 프로토타입을 선언해 주면 파일 외부의 의존관계가 구체화되면서, 언제든지 접근할 수 있는 것이 되잖아요.


C에서는 externstatic이라는 키워드를 통해서, 각각의 함수(인스트럭션 모음집)데이터(변수/상수)가 링크의 대상인지 아닌지 여부도 구분할 수 있습니다.


물론 따로 명시하지 않으면 extern이 되어요.


예)

void foo() {}
extern void foo() {}
static void foo() {}

int bar;
extern int bar;
static int bar;


내가 정적 링킹에 대해서 알고 있는 것은 이 정도입니다.


그밖에 링킹만을 전문으로 해 주는 도구들을 링커라고 부르는데, 보통은 정적 링킹 도구를 가리키는 것 같습니다. (로더라고 하면 정적 링킹보다는 어딘가 동적인 역할을 수행할 것 같은 느낌이 강합니다.)


유닉스 계열 시스템(운영체제) 기반의 make 같은 툴들은 이런 컴파일과 링크의 규칙을 스크립트에 명시적으로 정의함으로써 빌드 과정을 자동화하고 간편하게 할 수 있도록 도와 줍니다.


[객체지향]


위에 적은 컴파일과 정적 링킹의 개념을 동적인 것으로 바꾼 것이 객체지향(OOP)입니다.


어느날 우연히, 스스로 떠올린 것입니다.


자세하고도 구체적인 예를 들면 다음과 같습니다.


1) C의 extern/static 개념이 진화하면, 객체지향의 접근지정자인 public/private가 됩니다.


2) C의 소스파일 하위에서 extern/static 키워드가 붙는 변수나 함수 각각은 객체지향의 클래스 하위에 위치하는 멤버 개념에 대응합니다.


3) C에서 한 단위의 소스파일은, 객체지향에서 클래스와 같습니다.


4) 소스파일의 컴파일 결과물인 목적파일(Object)은 객체지향 프로그램이 힙에 할당하는 객체(Object)에 대응합니다.


5) 프로그램의 인스턴스가 프로세스가 되듯이, 클래스의 인스턴스는 객체가 됩니다.


6) C의 헤더파일은 객체지향의 인터페이스와 같은 역할입니다.


저는 이것을, 간지나게, "런타임에 발현하는 실체"라고 이름 짓고 싶네요.


모든 분야의 모든 학계에서는, 정적인 것보다 동적인 것이 한층 더 고급스러운 기법이라는 것이 정설입니다.


그렇기에, 객체지향 개념의 도입은 확실히 프로그래밍 패러다임이 한 단계 도약하는 계기였다고 믿고 있습니다.



Top