3부에서 마틴은 벽돌 한 장을 어떻게 다듬는지를 이야기했다. SOLID는 클래스와 그 사이의 관계, 그러니까 작은 방 하나를 짓는 규율이었다. 4부는 시선을 한 단계 끌어올린다 — 그 벽돌들을 모아 만든 방들을 이제 어떻게 하나의 덩어리로 묶어 배포할 것인가. 그 덩어리의 이름이 컴포넌트다. 그리고 12장은 컴포넌트가 무엇인지 정의하기 전에, 먼저 이상한 질문을 던진다 — 우리가 오늘 아무렇지 않게 라이브러리를 내려받아 시스템에 끼워 넣는 이 행위가, 사실은 반세기에 걸쳐 서서히 획득한 능력이라는 것을.
컴포넌트란 배포의 단위다. 시스템의 일부로서 배포될 수 있는 가장 작은 단위. 자바에서는 jar 파일, 루비에서는 gem, 닷넷에서는 DLL이다. 컴파일된 언어에서는 바이너리 파일들의 묶음이고, 인터프리터 언어에서는 소스 파일들의 묶음이다. 형태가 무엇이든 공통점은 하나다 — 다시 컴파일하지 않고도 시스템에 끼우고 뺄 수 있는 조각. 이 “끼우고 뺄 수 있음”이 오늘의 우리에게는 당연해 보이지만, 마틴은 이 당연함이 언제부터 당연했는지를 되짚는다.
링킹과 로딩의 역사가 곧 컴포넌트의 역사다
이 장의 몸통은 정의가 아니라 역사다. 마틴은 컴포넌트가 무엇인지를 설명하기 위해, 프로그램을 메모리에 올려 실행하는 방식이 어떻게 변해 왔는지를 이야기로 풀어낸다. 왜 굳이 옛날이야기인가. 지금 우리가 누리는 “플러그인처럼 갈아 끼우는 자유”가 공짜로 주어진 게 아니라, 로딩과 링킹이라는 두 문제를 프로그래머들이 오래 씨름해 얻어낸 결과이기 때문이다. 그 씨름의 궤적을 알아야 컴포넌트가 왜 지금의 모습인지를 이해한다.
가장 초창기, 프로그래머는 프로그램이 메모리의 어느 주소에 올라갈지를 직접 알아야 했다. 소스 코드의 첫머리에 이 프로그램이 몇 번지에서 시작한다고 명시했고, 그러면 컴파일된 바이너리의 모든 함수 주소와 점프 목적지가 그 시작점을 기준으로 확정되었다. 프로그램은 정해진 자기 자리를 갖고 태어났다. 문제는, 이 시절에는 프로그램이 통째로 하나였다는 것이다. 라이브러리라고 부를 만한 것도 프로그램의 소스에 함께 얹혀 한 몸으로 컴파일되었다. 모든 것이 절대 주소에 못 박혀 있었고, 조각을 따로 떼어 재사용한다는 발상 자체가 성립하기 어려웠다.
판단 기준: 어떤 조각을 독립적으로 다루고 싶다면, 먼저 그 조각이 자신의 위치에 대한 가정을 얼마나 품고 있는지를 물어라 — 절대 주소에 못 박힌 코드는 결코 떼어낼 수 없다. 함정: “위치를 미리 정해 두면 단순하다”는 편의는 조각의 이동 가능성을 통째로 저당 잡는다. 초기 단순함의 대가는 언제나 나중의 경직이다.
재배치 가능한 바이너리 — 위치의 저주를 푼 첫 열쇠
프로그램이 커지자 이 방식은 견딜 수 없어졌다. 소스가 길어져 컴파일에 몇 시간이 걸리는 지경이 되자, 프로그래머들은 자주 쓰는 함수를 미리 컴파일해 두고 그 컴파일된 조각을 여러 프로그램이 나눠 쓰기를 원했다. 그런데 조각을 재사용하려면 그 조각이 메모리의 어디에 올라가든 상관없어야 한다. 자기 자리를 고집하는 코드로는 불가능하다.
그래서 나온 것이 **재배치 가능한 바이너리(relocatable binary)**다. 컴파일러의 출력이 달라졌다. 컴파일러는 코드를 메모리 어디에나 올릴 수 있는 형태로 뽑아내되, 그 안에서 주소를 참조하는 모든 지점에 표시를 남겼다 — “여기는 나중에 실제 적재 위치가 정해지면 그만큼 더해 주어야 하는 자리”라고. 프로그램을 실행할 때, 로더는 이 재배치 가능 코드를 원하는 주소에 올린 다음, 표시된 자리마다 실제 시작 주소를 더해 주소를 확정했다. 코드가 자신의 위치에 대한 결정을 적재 시점까지 미룰 수 있게 된 것이다. 위치의 저주가 처음으로 풀렸다.
이 대목에서 마틴이 앞 장들과 잇는 실이 하나 보인다. 재배치란 결국 결정을 미루는 기술이다. 15장에서 아키텍처의 목적이 선택지를 열어두는 것이라 말할 때, 그 열어둠의 가장 원초적인 형태가 바로 여기 있다. 코드가 자기 주소를 몰라도 되게 만든 순간, 그 코드는 비로소 여러 맥락에 재사용될 자유를 얻었다.
링킹 로더 — 조각들이 서로의 이름을 부르다
재배치만으로는 부족했다. 조각 A가 조각 B 안의 함수를 호출하려면, A는 B의 그 함수가 결국 어느 주소에 자리 잡을지를 알아야 한다. 하지만 각 조각은 따로 컴파일되었으니, 컴파일 시점에는 남의 함수 주소를 알 도리가 없다. 그래서 컴파일러는 또 하나의 표시를 남겼다 — 내가 부르는 이 함수는 **외부의 어떤 이름(external reference)**이고, 내가 제공하는 이 함수는 남이 부를 수 있는 **이름(external definition)**이라고.
**링킹 로더(linking loader)**가 이 이름들을 서로 이어 주는 중매쟁이였다. 링킹 로더는 여러 재배치 가능 조각을 메모리에 올리면서, 한 조각이 부르는 외부 이름을 다른 조각이 내건 정의와 맞춰 실제 주소로 연결했다. 흩어진 이름들이 서로를 찾아 손을 맞잡는 이 과정이 링킹이다. 이제 프로그래머는 독립적으로 컴파일된 여러 모듈을 두고, 로더에게 “이것들을 함께 올려 서로 잇게 하라”고 지시할 수 있었다. 분할 컴파일과 재사용이 처음으로 실용의 궤도에 올랐다. 이것이 컴포넌트라는 개념의 진짜 조상이다 — 따로 만들어져 실행 순간에 하나로 엮이는 조각들.
하지만 이 편리에는 대가가 있었다. 프로그램이 더 커지고 참조가 수천 개로 불어나자, 링킹 로더가 매 실행마다 이 모든 이름을 맞추는 데 걸리는 시간이 감당할 수 없이 길어졌다. 조각을 읽어들이는 로딩은 느렸고, 이름을 잇는 링킹도 느렸다. 실행 한 번에 링킹만 한 시간이 걸린다면, 재사용의 자유는 좋았으나 값이 너무 비쌌다.
판단 기준: 어떤 유연성을 도입할 때는 그 유연성이 매번 지불되는지 한 번만 지불되는지를 구분하라 — 실행마다 링킹 비용을 무는 구조는 재사용의 자유를 가졌으나 그 자유가 곧 병목이 된다. 함정: “느슨하게 이어 두면 유연하다”는 말은 절반만 옳다. 이음새가 많을수록 그 이음새를 잇는 비용도 어딘가에서 반드시 청구되며, 그 청구서가 실행 경로 위에 놓이면 유연성은 성능을 갉아먹는다.
로드타임 링킹과 런타임 링킹으로 갈라지다
느린 링킹 로더를 두고, 사람들은 두 개의 일을 갈라내는 것으로 답했다. 로딩과 링킹을 두 단계로 분리한 것이다. 시간이 오래 걸리는 링킹은 미리 한 번만 해두고, 그 결과를 실행 가능한 완성된 이미지로 저장해 두었다. 이것이 링커(linker)다. 링커가 느린 링킹을 도맡아 실행 파일을 만들어 두면, 실제 실행 때 로더는 이미 다 이어진 그 파일을 그냥 빠르게 메모리에 올리기만 하면 되었다. 링킹은 빌드 시점으로 옮겨 가 한 번만 치르는 값이 되었고, 로딩은 다시 빨라졌다.
무어의 법칙이 이 이야기에 개입한다. 1980년대까지도 링킹은 몇 시간짜리 작업이라 이렇게 미리 이어 두는 방식이 지배적이었다. 그러나 기계가 빨라지고 메모리가 커지면서, 한때 몇 시간이던 링킹이 몇 초로 줄었다. 그러자 다시 선택지가 열렸다 — 이렇게 빠르다면, 굳이 미리 이어 두지 않고 실행하는 바로 그 순간에 이어도 되지 않을까.
그래서 링킹의 시점이 다시 뒤로 밀려났다. 로드타임에 조각을 올리며 잇거나(로드타임에 동적으로 링크되는 라이브러리), 심지어 실행 도중 필요한 순간에야 조각을 찾아 잇는(런타임 링킹) 방식이 가능해졌다. 이 흐름의 종착점에 우리가 매일 쓰는 것들이 있다. 자바의 jar를 클래스로더가 실행 중에 불러들이는 것, 운영체제가 공유 라이브러리를 프로그램이 요청하는 순간 메모리에 매핑해 주는 것 — 모두 링킹을 실행의 마지막 순간까지 미룬 결과다. 컴포넌트는 이렇게 링킹의 시점이 컴파일에서 로드타임으로, 다시 런타임으로 계속 뒤로 밀려난 역사의 끝에서 태어났다.
판단 기준: 두 관심사가 한데 묶여 비싸다면, 무겁고 드물게 바뀌는 쪽과 가볍고 자주 반복되는 쪽으로 갈라 각각을 알맞은 시점으로 보내라 — 링킹을 빌드로, 로딩을 실행으로 나눈 분리가 바로 그 판단이다. 함정: 분리 자체를 목적으로 삼지 말 것. 링킹을 로드타임·런타임으로 미룬 것은 기계가 그만큼 빨라졌다는 전제 위에서만 옳았다. 전제가 바뀌면 최적의 시점도 바뀌므로, “늦게 잇는 것이 항상 낫다”는 규칙은 없다.
늦은 링킹이 낳은 진짜 선물 — 플러그인
이 반세기의 궤적에서 마틴이 길어 올리는 결론은 성능 이야기가 아니다. 링킹을 실행의 순간까지 미룰 수 있게 되자, 전에는 상상할 수 없던 일이 가능해졌다는 것이다 — 프로그램을 다시 컴파일하지 않고도, 실행 시점에 새로운 조각을 끼워 넣는 일.
런타임에 동적으로 링크되는 컴포넌트가 곧 우리가 아는 플러그인이다. 본체 프로그램은 어떤 이름의 조각이 있으리라 약속만 해두고, 그 조각의 실체는 실행 순간에 찾아 잇는다. 그래서 본체를 손대지 않고도 조각만 새로 만들어 자리에 꽂으면 시스템의 행위가 달라진다. 낡은 조각을 빼고 새 조각을 넣는 일이, 큰 기계를 멈추고 뜯어고치는 일이 아니라 부품 하나를 교체하는 일이 된 것이다. 재배치 가능 바이너리가 위치의 결정을 미룬 데서 시작해, 링킹의 시점을 실행 순간까지 밀어붙인 끝에, 컴포넌트는 마침내 독립적으로 배포되고 독립적으로 개발될 수 있는 조각이 되었다.
이 두 단어 — 독립적 배포와 독립적 개발 — 가 이 장이 4부 전체에 심는 씨앗이다. 독립적으로 배포된다는 것은, 한 컴포넌트를 새로 빌드해 갈아 끼울 때 시스템 전체를 다시 빌드하고 배포하지 않아도 된다는 뜻이다. 독립적으로 개발된다는 것은, 서로 다른 팀이 각자의 컴포넌트를 따로 만들 수 있다는 뜻이다. 이 두 자유가 아키텍처의 목표인 “적은 인력으로 시스템을 만들고 유지한다”에 직결된다. 컴포넌트로 잘 쪼갠 시스템은 팀들이 서로를 기다리지 않고 나란히 일할 수 있고, 한 부분의 변경이 다른 부분의 재빌드를 강요하지 않는다.
판단 기준: 어떤 조각을 컴포넌트로 볼지 판단할 때는 “이것을 남을 건드리지 않고 따로 배포하고 따로 개발할 수 있는가”를 물어라 — 그 두 독립성이 서지 않으면 파일을 나눠 놓았어도 아직 컴포넌트가 아니다. 함정: 물리적으로 jar나 DLL로 분리했다는 사실이 곧 독립성을 보장하지는 않는다. 이름만 조각으로 나뉜 채 서로의 내부에 얽혀 있으면, 하나를 바꿀 때마다 나머지를 함께 다시 빌드해야 한다 — 배포 단위는 여럿인데 개발 단위는 여전히 하나인, 이름뿐인 분리다.
우리가 물려받은 것을 잊지 않기
마틴이 이 역사를 굳이 다 짚는 데는 이유가 있다. 오늘의 프로그래머는 라이브러리를 한 줄로 가져다 쓰고, 플러그인을 별생각 없이 꽂아 넣는다. 이 매끄러움이 너무 당연해서, 그 아래에 재배치와 링킹과 로딩을 둘러싼 수십 년의 씨름이 깔려 있다는 걸 잊는다. 컴포넌트를 자유롭게 갈아 끼우는 능력은 언어가 원래부터 준 선물이 아니라, 결정을 점점 더 뒤로 미룰 수 있게 만든 오랜 노력의 열매다.
그리고 이 관점은 다음 장들로 곧장 이어진다. 컴포넌트가 독립적으로 배포·개발될 수 있는 조각이라면, 이제 남는 질문은 두 가지다 — 무엇을 한 컴포넌트 안에 모을 것인가(응집도), 그리고 컴포넌트들을 어떤 방향으로 이을 것인가(결합). 12장은 컴포넌트라는 그릇을 정의했을 뿐이고, 그 그릇에 무엇을 어떻게 담아야 독립성이라는 선물이 실제로 지켜지는지는 13장과 14장의 몫이다.
- 컴포넌트는 배포의 단위다 — jar, DLL, gem처럼 다시 컴파일하지 않고 시스템에 끼우고 뺄 수 있는 가장 작은 조각.
- 그 능력은 링킹·로딩의 역사가 준 것이다: 절대 주소에 못 박힌 코드 → 위치 결정을 미루는 재배치 가능 바이너리 → 이름을 잇는 링킹 로더 → 링킹 시점을 빌드로, 다시 로드타임·런타임으로 미룬 동적 링킹.
- 링킹을 실행 순간까지 미룰 수 있게 되자 플러그인이 태어났다 — 본체를 다시 빌드하지 않고 조각만 교체하는 자유.
- 그래서 컴포넌트의 본질은 독립적 배포와 독립적 개발이다. 파일을 나눈 것만으로는 부족하고, 서로를 건드리지 않고 따로 배포·개발될 때만 진짜 컴포넌트다.
다음장으로 13장