컴포넌트는 시스템의 구성 요소로 배포할 수 있는 가장 작은 단위이다.
예를 들어, 자바에서는 jar 파일, 루비에서는 gem 파일, 닷넷에서는 DLL 이다.
컴파일형 언어 에서 컴포넌트는 바이너리 파일의 결합체 이다. 인터프리터형 언어의 경우 소스 파일의 결합체 이다.
컴파일형 언어는 전체 소스 코드를 한 번에 기계어로 변환 후 실행한다. C, C++, Go, Rust 등이 있다. 인터프리터형 언어는 소스 코드를 한 줄씩 해석하며 즉시 실행한다. Python, Javascript, Ruby, R 등이 있다.
여러 컴포넌트를 서로 링크하여 실행 가능한 단일 파일로 생성할 수 있다. 혹은 여러 컴포넌트를 서로 묶어 단일 아카이브로 만들거나, 컴포넌트 각각을 동적으로 로드할 수 있는 플러그인이나 .exe 파일로 만들어 독립적으로 배포할 수 있다.
컴포넌트가 마지막에 어떤 형태로 배포되든, 잘 설계된 컴포넌트라면 반드시 독립적으로 배포 가능해야한다.
오늘날은 프로그래머는 프로그램을 메모리의 어느 위치에 로드할 지 고민할 필요가 거의 없다. 하지만, 소프트웨어 개발 초창기에는 프로그램을 로드할 메모리의 위치를 정해야했다.
즉, 프로그래머는 메모리에서의 프로그램 위치와 레이아웃을 프로그래머가 직접 제어해야했다. 따라서 프로그램의 시작부에는 로드될 주소를 선언하는 오리진(origin) 구문이 나와야했다.
예를 들어, 다음의 간단한 PDP-9 프로그램을 살펴보자.
*200
TSL
START, CLA
TAD BUFR
JMS GETSTR
. . .
프로그램 시작부에 있는 *200 명령어는 메모리 주소 200(8진수)에 로드할 코드를 생성하라고 컴파일러에 알려준다.
이러한 구시대에는 라이브러리 함수에 어떻게 접근했을까? 위의 코드가 그 방식을 보여준다.
프로그래머가 라이브러리 함수의 소스 코드를 애플리케이션 코드에 직접 포함시켜 단일 프로그램으로 컴파일했다. 즉 라이브러리는 바이너리 형태가 아니라 소스 코드로 유지되었다.
하지만 이 시대에 장치는 느리고 메모리는 너무 비싸 자원이 한정적이었기에, 이러한 접근법은 문제가 있었다. 메모리가 너무 작아 소스 코드 전체를 메모리에 상주시킬 수 없었다. 결국 컴파일러는 소스 코드를 여러 차례에 걸쳐 읽어야 했는 데 이는 너무 시간을 오래걸리게 했다.
그래서 컴파일 시간을 단축시키기 위해 함수 라이브러리 소스 코드를 애플리케이션 코드로부터 분리했다. 라이브러리를 개별적으로 컴파일하고, 컴파일된 바이너리를 메모리의 특정 위치에 로드했다.
그림 12.1 초기의 메모리 배치 (출처 : https://agorism.dev/book/software-architecture/(Robert%20C.%20Martin%20Series)%20Robert%20C.%20Martin%20-%20Clean%20Architecture_%20A%20Craftsman%E2%80%99s%20Guide%20to%20Software%20Structure%20and%20Design-Prentice%20Hall%20(2017).pdf)
하지만, 애플리케이션은 점점 커졌고 결국 할당된 공간을 넘어서게 되었다. 이 시점이 되면, 애플리케이션을 두 개의 주소 세그먼트로 분리하여 함수 라이브러리 공간을 사이에 두고 오가며 동작하게 배치해야한다.
그림 12.2 애플리케이션을 두 개의 주소 세그먼트로 분리 (출처 : https://agorism.dev/book/software-architecture/(Robert%20C.%20Martin%20Series)%20Robert%20C.%20Martin%20-%20Clean%20Architecture_%20A%20Craftsman%E2%80%99s%20Guide%20to%20Software%20Structure%20and%20Design-Prentice%20Hall%20(2017).pdf)
이러한 상황은 분명 지속 가능하지 않았다. 프로그램과 함수 라이브러리가 사용하는 메모리가 늘어날 수록 이와 같은 단편화는 계속 될 수 밖에 없었다.
해결책은 재배치가 가능한 바이너리였다. 지능적인 로더를 사용해 메모리에 재배치할 수 있는 형태의 바이너리를 생성하도록 컴파일러를 수정하는 방식이었다. 프로그래머는 함수 라이브러리를 로드할 위치와 애플리케이션을 로드할 위치를 로더에게 지시할 수 있다.
- 재베치 코드에는 로드한 데이터에서 어느 부분을 수정해야 정해진 주소에 로드할 수 있는지를 알려주는 플래그가 삽입되었다. 대체로 플래그는 바이너리에서 참조하는 메모리의 시작 주소였다.
- 로더는 재배치 코드의 위치 정보를 전달받는다. 여러 개의 바이너리를 입력받은 후, 단순히 하나씩 차례로 메모리에 로드하며 재배치하는 작업을 수행한다.
또한, 컴파일러는 재배치 가능한 바이너리 안의 함수 이름을 메타데이터 형태로 생성하도록 수정되었다.
만약 프로그램이 라이브러리 함수를 호출한다면 컴파일러는 라이브러리 함수 이름을 외부 참조로 생성했다. 반면 라이브러리 함수를 정의하는 프로그램이라면 컴파일러는 해당 이름을 외부 정의로 생성했다.
즉, 이렇게하면 외부 정의를 로드할 위치가 정해지기만 하면 로더가 외부 참조를 외부 정의에 링크시킬수 있게 된다. 이렇게 링킹 로더가 탄생했다.
링킹 로더의 등장으로 프로그래머는 프로그램을 개별적으로 컴파일하고 로드할 수 있는 단위로 분할할 수 있게 되었다. 하지만 1960, 1970년대가 되자 프로그램은 더욱 커지게 되었고 링킹 로더는 너무 느린것으로 되어버렸다.
마침내 로드와 링크가 두 단계로 분리되었다. 프로그래머가 느린 부분, 즉 링크 과정을 맡았는데, 링커라는 별도의 애플리케이션으로 이 작업을 처리하도록 만들었다.
링커는 링크가 완료된 재배치 코드를 만들어 주었고, 그 덕분에 로더의 로딩 과정이 아주 빨라졌다.
그리고 1980년대가 되었다. 역시 프로그램은 더 커지고 프로그래머는 고수준 언어를 사용하기 시작했다. 코드가 수만, 수십만 라인을 넘어서는 게 별일이 아니게 되면서 전체 모듈을 컴파일하는 시간은 많이 소요되었다.
결국 컴파일-링크 시간은 줄일 수 없었고 병목 구간이었다.
하지만 디스크는 점점 작아지고, 빨라졌다. 또한, 비용이 비쌌던 컴퓨터 메모리는 매우 저렴해졌다. 1990년대 후반이 되자, 프로그램을 성장시키는 속도보다 링크 시간이 줄어드는 속도가 더 빨라지기 시작했다.
이렇게 액티브 X와 공유 라이브러리 시대가 열렸고, .jar 파일도 등장하기 시작했다. 컴퓨터와 장치가 빨리진 덕에 또다시 로드와 링크를 동시에 할 수 있게 되었다.
다수의 .jar 파일 또는 다수의 공유 라이브러리를 순식간에 서로 링크한 후, 링크가 끝난 프로그램을 실행할 수 있게 되었다. 이렇게 컴포넌트 플러그인 아키텍처가 탄생했다.
런타임에 플러그인 형태로 결합할 수 있는 동적 링크 파일이 이 책에서 말하는 소프트웨어 컴포넌트에 해당한다.