한번 세상에 내놓은 서비스는 그 뒤로 나 혼자만의 것이 아니게 된다. 그것을 불러 쓰는 클라이언트들이 곳곳에서 자라나고, 그들은 내가 정한 요청과 응답의 모양에 기대어 동작한다. 그런데 시간이 지나면 그 모양을 바꿔야 할 일이 생긴다. 나는 이 변화의 순간마다, 이미 내 서비스에 의지하고 있는 수많은 클라이언트를 어떻게 배신하지 않고 앞으로 나아갈지를 고민해야 했다. 버전 관리는 그 고민에 대한 답이다.


이번 편에서는 서비스가 시간이 지나며 바뀔 때 그 변화를 어떻게 다스릴지를 다룬다. 어떤 변화가 기존 클라이언트를 깨뜨리고 어떤 변화가 무해한지, 깨뜨리는 변화가 불가피할 때 어떻게 옛것과 새것을 나란히 두는지, 그 버전을 어디에 표시하며 옛 버전을 어떻게 거두어들이는지를 살펴본다. 버전 관리를 소홀히 하면, 개선을 하려던 손길이 이미 잘 돌아가던 남의 시스템을 한순간에 멈춰 세우게 된다.

바꾸는 순간 남과의 약속이 흔들린다

버전 관리를 이해하려면 먼저 서비스가 곧 약속이라는 사실을 받아들여야 한다. 내가 어떤 요청에 어떤 모양의 응답을 준다고 정해두면, 그것을 불러 쓰는 클라이언트는 그 모양을 믿고 자기 코드를 짠다. 응답의 어느 자리에 어떤 값이 있으리라 기대하고 그 값을 꺼내 쓴다. 이 기대가 곧 우리 사이의 약속이고, 나는 서비스를 내놓는 순간 이 약속에 묶인다.


문제는 이 약속을 내가 일방적으로 바꿀 수 없다는 데 있다. 응답의 어떤 자리를 없애거나 그 자리의 값을 다른 모양으로 바꾸면, 그 자리를 믿고 있던 클라이언트의 코드가 어긋난다. 나는 그저 개선을 했을 뿐인데, 상대는 어느 날 갑자기 잘 돌아가던 것이 멈추는 일을 겪는다. 서비스를 바꾼다는 것은 이렇게 남과 맺은 약속을 건드리는 일이라, 함부로 할 수 없다.


그렇다고 한번 정한 모양을 영원히 못 바꾸는 것도 아니다. 서비스는 살아 있는 한 자라야 하고, 자라려면 바뀌어야 한다. 잘못 설계된 부분을 바로잡고, 새 요구를 받아들이고, 낡은 방식을 걷어내야 한다. 그러니 변화를 아예 막는 것은 답이 아니다. 변화를 허용하되 그 변화가 기존의 약속을 배신하지 않게 다스리는 것, 바로 그것이 버전 관리가 풀려는 문제다.


여기서 핵심이 되는 구분이 약속을 깨는 변화와 깨지 않는 변화의 갈림이다. 어떤 변화는 기존 클라이언트에 아무 해가 없지만, 어떤 변화는 그들의 코드를 곧바로 무너뜨린다. 이 둘을 분명히 나누는 것이 버전 관리의 출발점이다. 모든 변화를 똑같이 위험하게 여기면 서비스가 굳어버리고, 모든 변화를 똑같이 안전하게 여기면 남의 시스템이 깨진다. 나는 이 경계를 늘 먼저 가늠한다.


약속을 깨지 않는 변화의 대표는 무언가를 더하는 것이다. 응답에 새로운 자리를 하나 덧붙이는 변화는, 그 자리를 모르는 기존 클라이언트에게 아무 영향을 주지 않는다. 그들은 여전히 자기가 아는 자리만 꺼내 쓰고, 새로 생긴 자리는 그저 못 본 척 지나갈 뿐이다. 그래서 기존의 것을 건드리지 않고 새것을 더하는 방향의 변화는 대체로 안전하다. 나는 가능하면 변화를 이 더하는 방향으로 끌고 가려 한다.


반대로 약속을 깨는 변화는 기존의 것을 없애거나 그 의미를 바꾸는 것이다. 있던 자리를 없애면 그것을 꺼내 쓰던 코드가 빈손을 쥐고, 값의 모양이나 뜻을 바꾸면 그것을 해석하던 코드가 헛다리를 짚는다. 이런 변화는 기존 클라이언트를 그 자리에서 넘어뜨린다. 나는 이런 변화를 마주할 때, 이것이 정말 피할 수 없는지부터 되묻고, 피할 수 없다면 버전을 가르는 쪽으로 방향을 잡는다.

깨지 않는 변화와 깨는 변화

깨지 않는 변화를 더 자세히 들여다보면, 그 안전함이 어디서 오는지 보인다. 관용의 원칙이라는 것이 있다. 받는 쪽은 자신이 모르는 것을 만나도 너그럽게 넘겨야 한다는 태도다. 잘 만든 클라이언트는 응답에서 자기가 아는 자리만 꺼내 쓰고, 모르는 자리는 조용히 무시한다. 이 관용이 지켜지는 한, 응답에 새 자리를 더하는 변화는 아무도 깨뜨리지 않는다.


그래서 나는 클라이언트를 만들 때부터 이 관용을 심어둔다. 응답에 예상 못한 자리가 있다고 실패로 처리하지 않고, 필요한 것만 꺼내 쓰고 나머지는 흘려보내게 한다. 이렇게 관용적으로 만들어진 클라이언트는 서버가 응답을 조금씩 넓혀가도 흔들리지 않는다. 관용은 서버가 안전하게 자랄 여지를 클라이언트 쪽에서 미리 열어두는 배려인 셈이다. 이 태도가 없으면 사소한 추가조차 사고가 된다.


새 항목을 요청에 더할 때도 마찬가지 원칙이 선다. 새로 생긴 요청 항목을 반드시 채워야만 하는 것으로 두면, 그것을 모르는 기존 클라이언트의 요청은 항목이 빠졌다는 이유로 거절된다. 그래서 새 요청 항목은 채우지 않아도 되도록 열어두고, 빠졌을 때 쓸 적당한 기본값을 정해두어야 한다. 이렇게 하면 옛 클라이언트는 그 항목을 모른 채로도 여전히 받아들여진다. 더하되 강요하지 않는 것이다.


반면 깨는 변화는 그 성질상 관용으로 감쌀 수 없다. 있던 자리를 없애면 그것에 기대던 코드는 관용을 발휘할 대상조차 잃는다. 값의 뜻을 바꾸면, 예컨대 같은 자리의 숫자가 뜻하던 바가 달라지면, 그것을 옛 뜻으로 해석하던 코드는 조용히 틀린 결과를 낸다. 이런 변화는 눈에 띄는 실패보다 더 고약한, 소리 없이 어긋나는 오류를 낳기도 한다. 나는 값의 의미를 바꾸는 변화를 특히 경계한다.


요청 쪽에서 깨는 변화의 예는, 없던 항목을 갑자기 반드시 채우게 만드는 것이다. 어제까지 그 항목 없이도 받아들여지던 요청이 오늘부터 그 항목이 없다고 거절되면, 기존 클라이언트는 자기가 아무것도 바꾸지 않았는데 갑자기 막힌다. 규칙을 조이는 방향의 변화는 이렇게 늘 위험하다. 나는 검증 규칙을 강화할 때, 그것이 기존의 정상적인 요청을 걸러내지 않는지를 반드시 확인한다.


이 둘을 가르는 감각이 잡히면, 변화를 대하는 태도가 달라진다. 나는 어떤 변경을 앞두고 늘 자문한다. 이 변화가 지금 내 서비스를 쓰고 있는 어느 클라이언트라도 깨뜨리는가. 답이 아니오라면 버전을 가르지 않고 조용히 반영하고, 답이 예라면 옛것을 지키면서 새것을 따로 두는 방법을 찾는다. 이 물음 하나가 버전을 새로 갈라야 할지 말지를 대부분 결정해준다.

버전을 나누고 표시하는 방법

깨는 변화가 정말 불가피할 때, 옛 약속과 새 약속을 나란히 살려두는 것이 버전 관리의 요체다. 옛 방식에 기대던 클라이언트는 옛 버전을 계속 쓰게 두고, 새 방식을 원하는 클라이언트는 새 버전으로 옮겨가게 한다. 한쪽을 위해 다른 쪽을 희생하지 않고, 둘을 한동안 함께 두어 옮겨갈 시간을 버는 것이다. 나는 이 나란히 둠을 버전 관리의 핵심 장치로 본다.


버전을 어디에 표시하느냐에는 몇 갈래가 있다. 가장 눈에 잘 띄는 방식은 주소 경로에 버전을 드러내는 것이다. 주소의 앞부분에 버전을 나타내는 표지를 넣어, 같은 자원이라도 옛 버전과 새 버전을 다른 주소로 가른다. 이 방식은 보기에 분명하고 다루기 쉬워 널리 쓰인다. 클라이언트도 자신이 어느 버전을 부르는지 주소만 봐도 알 수 있어, 혼동이 적다.


다른 방식은 버전을 주소가 아니라 요청의 헤더에 담는 것이다. 자원의 주소는 하나로 두되, 어떤 버전의 표현을 원하는지를 헤더로 알려 서버가 그에 맞는 버전을 골라주게 한다. 이 방식은 앞선 편에서 다룬 콘텐츠 협상의 연장선에 있다. 같은 주소가 요청자의 뜻에 따라 다른 표현을 내주는 그 원리를, 버전을 고르는 데 적용하는 것이다. 주소를 버전마다 늘리지 않아도 된다는 이점이 있다.


주소에 드러내는 방식과 헤더에 담는 방식은 저마다 장단이 갈린다. 주소에 드러내면 명료하고 눈에 잘 띄지만, 같은 자원이 버전마다 다른 주소를 갖게 되어 자원과 주소가 하나로 맞물린다는 원칙과는 살짝 어긋난다. 헤더에 담으면 그 원칙에는 더 충실하지만, 버전이 눈에 잘 띄지 않아 다루기가 덜 직관적이다. 나는 이 둘을 놓고 명료함과 원칙 가운데 무엇을 더 중히 여길지를 저울질한다.


어느 방식을 택하든 지켜야 할 것은 버전을 잘게 쪼개지 않는 것이다. 사소한 변화마다 버전을 새로 가르면, 유지해야 할 버전이 순식간에 불어나 관리가 무너진다. 나는 버전을 오직 약속을 깨는 변화가 쌓였을 때만 올리고, 깨지 않는 변화는 같은 버전 안에서 흡수한다. 버전을 가르는 것은 비용이 큰 결정이므로, 정말 필요할 때만 신중히 내려야 한다. 버전이 많다고 좋은 것이 결코 아니다.


버전을 나눈 뒤에는 그 여러 버전이 각각 어떻게 다른지를 문서로 분명히 남겨야 한다. 어느 버전에서 무엇이 어떻게 바뀌었는지가 기록되어야, 클라이언트를 만드는 쪽이 자신이 옮겨갈 때 무엇을 고쳐야 할지 안다. 나는 버전을 가를 때마다 그 변화의 내역을 함께 적는다. 버전 표지만 올려두고 무엇이 달라졌는지 밝히지 않으면, 옮겨가려는 쪽은 어둠 속에서 더듬게 된다.

옛 버전을 거두어들이는 절차

버전을 나란히 두는 것은 영원히가 아니라 한동안이다. 옛 버전을 언제까지나 살려두면 유지할 짐이 끝없이 쌓이므로, 언젠가는 옛것을 거두어들여야 한다. 그러나 이 거둠은 갑작스러워서는 안 된다. 어느 날 예고 없이 옛 버전을 닫으면, 미처 옮겨가지 못한 클라이언트가 한순간에 멈춘다. 나는 옛 버전을 거두는 일을 단칼이 아니라 단계를 밟는 절차로 다룬다.


그 절차의 첫 단계가 이 버전은 이제 낡았으니 새것으로 옮기라는 예고다. 옛 버전은 여전히 동작하되, 이것이 곧 거두어질 것임을 클라이언트에 알린다. 응답에 이 버전이 낡았다는 표시를 담아, 그것을 부르는 쪽이 옮겨갈 준비를 하도록 미리 신호를 준다. 이 예고가 있어야 클라이언트는 아직 여유가 있을 때 새 버전으로 옮겨갈 계획을 세울 수 있다.


예고와 함께 중요한 것이 옮겨갈 넉넉한 시간이다. 낡았다고 알린 다음 곧바로 닫아버리면 예고의 뜻이 없다. 클라이언트마다 사정이 다르고, 옮겨가는 데는 시간이 걸린다. 그래서 낡음을 알린 시점과 실제로 닫는 시점 사이에 충분한 유예를 두어, 그 사이에 옮겨갈 수 있게 한다. 나는 이 유예를 상대에 대한 배려이자, 갑작스러운 중단이 부를 혼란을 막는 안전장치로 여긴다.


옮겨감을 도우려면 무엇이 어떻게 달라지는지를 친절히 안내해야 한다. 옛 버전의 어느 부분이 새 버전에서 어떻게 바뀌었고, 옮겨가려면 무엇을 고쳐야 하는지를 안내하는 것이다. 이 안내가 부실하면 클라이언트는 옮겨가고 싶어도 길을 몰라 옛 버전에 머문다. 나는 옛것을 거두려면 새것으로 가는 길을 함께 닦아주어야 한다고 본다. 닫기만 하고 길을 열어두지 않으면 상대는 갈 곳이 없다.


유예가 끝나 실제로 옛 버전을 닫을 때도, 그 닫힘이 명료해야 한다. 옛 버전으로 온 요청에는 그것이 이제 거두어졌음을 뜻하는 분명한 응답을 주어, 아직 남아 있던 클라이언트가 무슨 일이 벌어졌는지 알게 한다. 소리 없이 이상하게 동작하게 두는 것보다, 이 버전은 닫혔다고 또렷이 알리는 편이 낫다. 앞 편에서 실패를 정직하게 알려야 한다고 한 원칙이 여기서도 그대로 선다.


정리하면 버전 관리는 서비스가 남과 맺은 약속을 배신하지 않으면서 변화를 허용하는 규율이다. 깨지 않는 변화는 같은 버전에서 흡수하고, 깨는 변화가 불가피할 때만 버전을 갈라 옛것과 새것을 한동안 나란히 둔다. 옛 버전은 예고와 유예와 안내를 거쳐 거두어들인다. 이 규율을 지키면 서비스는 굳지 않고도 남을 넘어뜨리지 않는다. 다음 편에서는 이런 서비스를 지나친 요청과 악의로부터 지키는 요청 제한과 보안을 다뤄 보려 한다.