Image: AI generated
고친 자리에서 다시 올라온다
나는 드리프트를 닫는 도구를 지었다.
yongol의 테제는 단순하다. 결정은 권위 있는 단일 소스(SSOT)에 살지 않으면 표류한다. 그래서 결정을 SSOT에 넣고, 코드를 매 생성마다 다시 그려지는 일회용 투영(projection)으로 만든다. BIGINT로 정한 컬럼이 리팩터 한 번에 슬그머니 INT로 되돌아가는 비즈니스 로직 드리프트는, 이렇게 닫혔다.
그런데 얼마 전 yongol이 만든 코드에서 결함 묶음을 분석하다 이상한 것을 봤다. 결함들이 같은 문장 구조로 자기를 고백하고 있었다. “import 수집이 ‘핸들러가 실제로 time을 쓰는가’와 분리돼 있다.” “requiredness 추론이 ‘대상 API가 이 파라미터를 정말 required로 요구하는가’와 분리돼 있다.” 경로 파라미터는 항상 required, import는 토큰을 실제로 쓸 때만. 같은 구조적 결정들이 어떤 SSOT에도 적히지 않은 채, 생성기 코드 안에 편한 로컬 프록시로 박혀 있었다.
드리프트가 사라진 게 아니었다. 한 층 위로 올라가 있었다. 비즈니스 로직은 SSOT로 닫혔지만, 그 SSOT를 읽어 코드를 찍어내는 생성기 자신의 구조적 결정에는 SSOT가 없었다. yongol의 테제가 yongol 자신에게 그대로 돌아온 것이다. 이론이 예측한 정확히 그 자리에서.
그래서 질문이 바뀐다. 드리프트가 왜 발생하는가는 누구나 안다. 진짜 질문은 이것이다. 고쳐도 왜 다시 돌아오는가.
뿌리: 결정과 디테일은 다른 것이다
물리부터 다시 쌓자.
결정은 정보다. 그것도 낮은 엔트로피의 정보다. “이 컬럼은 64비트여야 한다"는, 가능한 수많은 상태 중 하나를 의도적으로 골라낸 것이다. 자연은 낮은 엔트로피를 싫어한다. 가만히 두면 정보는 주변의 노이즈로 번져 사라진다. 열역학 제2법칙이 결정에도 적용된다.
소프트웨어 공학은 이 붕괴를 오래 관찰해 왔다. 레만(Lehman)의 소프트웨어 진화 법칙은 E-type 시스템의 복잡도가 줄이려는 명시적 노력이 없으면 증가한다고 말한다(1980). 정보 물리학은 더 아래로 내려간다. 란다우어(Landauer)는 1비트를 지우는 데조차 최소한의 열역학 비용(kT ln 2)이 든다는 것을 1961년에 보였다. 정보를 바꾸고 붙잡아 두는 일은 원리적으로 공짜가 아니다. 결정을 제자리에 두려면 에너지를 계속 써야 한다.
정보가 살아남으려면 두 가지가 필요하다. 권위 있는 저장(authoritative store)과, 거기서 끊임없이 다시 그려내는 능동적 재투영(error correction). 우리 몸의 DNA가 그렇고, 디지털 저장의 패리티 비트가 그렇다. 원본을 따로 두고, 매번 그것을 기준으로 복원한다.
드리프트는 이 복원이 깨질 때 일어난다. 메커니즘은 하나다. 나는 그것을 프록시 바인딩이라 부른다. 매체가 결정과 디테일을 구분해 보존하지 못하면, 다음 사람(혹은 다음 에이전트)은 결정을 권위 있는 소스에서 읽지 못하고, 옆에 있는 편한 상관신호에서 재유도한다. “이 컬럼이 timestamptz네, 그럼 time을 import해야겠지” 같은 추측. 대개는 맞다. 그래서 위험하다. 가끔 틀리고, 틀릴 때 결정은 소리 없이 사라진다.
raw 코드가 바로 그런 매체다. 코드는 “이것은 결정이다"와 “이것은 이 자리에서 우연히 참이다"를 구분해 표시하지 않는다. 그래서 더 큰 모델로도 안 풀린다. 매체 자체가 결정을 담지 못하는데, 읽는 쪽이 똑똑해진들 읽을 게 없다.
이 현상에 이름이 없었던 건 아니다. 소프트웨어 아키텍처에서 페리와 울프는 원칙을 위반하는 것을 침식(erosion), 아키텍처에 무감각해지는 것을 드리프트(drift)라 갈랐고(1992), 커닝햄은 제대로 안 된 코드에 붙는 이자를 기술 부채라 불렀다(1992). 분야마다 증상은 잘 명명됐다. 내가 더하려는 것은 그 아래의 단일 메커니즘(프록시 바인딩)과, 그 메커니즘이 닫을수록 위층으로 재귀한다는 구조다. 이름이 아니라 인과를 묻는 것이다.
왜 위로 올라가는가
여기까지는 알려진 이야기다. 새로운 것은 그다음이다.
드리프트를 닫으려면 두 가지가 필요하다. 결정을 권위 있게 담을 저장소(SSOT)와, 그것을 읽어 산출물을 찍어내는 닫는 주체(생성기). 그런데 닫는 주체 자신도 결정을 내린다. “경로 파라미터는 required로 취급한다” 같은 구조적 결정을. 그 결정이 사는 매체—생성기 코드—역시 결정과 디테일을 구분해 담지 못한다.
같은 메커니즘이 한 층 위에서 반복된다. 닫는 행위 자체가 한 층 위에 닫히지 않은 매체를 만든다. 드리프트는 박멸된 게 아니라 이사한 것이다. 권위 없는 층으로.
이것을 끝까지 밀면 불편한 결론이 나온다. 생성기에 SSOT를 주면? 그 SSOT를 만드는 무언가가 또 자기 결정을 닫지 않은 매체에 담는다. 층을 올릴 때마다 표면적은 줄지만, 맨 위에는 언제나 권위 없는 층이 남는다. 사람이거나, 생성기의 생성기이거나. 드리프트는 점근적으로 박멸 불가능하다. (이건 증명이라기보다 강한 추측이다. 다만 지금까지 내가 닫은 모든 층은, 닫는 순간 위층을 열었다.)
이게 “고쳐도 왜 돌아오는가"의 답이다. 돌아오는 게 아니다. 우리가 한 층을 닫으면 그 닫는 도구가 다음 층을 연다. 같은 강물이 더 높은 둑에서 다시 새는 것이다.
처방의 비대칭: 선언할 수 있는 것과, 검증할 수밖에 없는 것
그렇다면 위층은 어떻게 닫나. 여기서 결정적인 비대칭이 드러난다.
비즈니스 로직의 결정은 대개 값이다. 컬럼은 64비트, 접근은 소유자만, 페이지네이션은 커서 방식. 값은 선언할 수 있다. DDL에, OpenAPI에, 명세 파일에 적어 두면 그게 SSOT가 된다. 선언으로 닫힌다.
생성기의 구조적 결정은 다르다. “경로 파라미터는 required다”, “import는 실제 토큰 참조에 묶인다”, “required(키가 있음)와 비어있지 않음은 다르다.” 이건 값이 아니라 모든 입력에 걸친 함수의 행동 속성이다. 행동 속성은 선언으로 열거할 수 없다. 입력이 무한하기 때문이다. YAML 한 칸에 “이 변환은 모든 경우에 이렇게 행동해야 한다"고 적을 방법은 없다.
그래서 이 층의 결정은 선언이 아니라 검증으로만 닫힌다. 타입 체커, 속성 테스트, 컴파일 게이트. 결정을 데이터로 박는 게 아니라, 위반을 기계가 매번 잡아내는 관문으로 박는 것이다.
내가 다른 글에서 “사람 검수를 코드화하라"고 쓴 것이 이 자리다. 어떤 약속은 선언할 수 있어서 SSOT가 지키고, 어떤 약속은 선언할 수 없어서 게이트가 지킨다. 생성기가 찍어낸 코드가 컴파일되는지는 어떤 SSOT에도 적을 수 없다. 오직 컴파일을 매번 돌려서만 확인된다. 그 관문이 없으면, “generate 성공 = 빌드 가능"이라는 약속은 아키텍처 밖에 둥둥 떠서, validate가 0/0으로 통과하는데도 산출물은 깨진다.
선언할 수 있는 드리프트는 SSOT로 닫고, 검증할 수밖에 없는 드리프트는 게이트로 닫는다. 둘을 혼동하면, 선언으로 막으려다 영원히 두더지를 잡는다.
같은 강, 다른 둑
이 구조는 코드 바깥에서도 반복된다.
지식에서 드리프트는 출처의 상실이다. 어떤 주장이 누가, 언제, 무슨 근거로 했는지를 잃으면, 그 주장은 “사실"이라는 노이즈로 번진다. 다음 사람은 권위(원 출처)에서 읽지 못하고 주변 정황에서 재유도한다. 내가 GEUL을 모든 정보에 출처·시점·신뢰도를 강제로 붙이는 언어로 설계한 이유가 이것이다. 사실은 없고 주장만 있다는 인식론은, 지식 층의 프록시 바인딩을 막으려는 안전장치다.
법에서 드리프트는 판례가 원래의 결정에서 벗어나는 것이다. 문명은 이것을 매 사건마다 판사의 양심에 맡기지 않고, 규칙을 성문화하고 위반을 정의하고 강제 메커니즘을 붙여 닫았다. 좋은 판사는 SSOT가 아니라 프록시다. 성문법이 SSOT다.
같은 강이다. 결정이 권위 있는 자리에 살지 않으면, 매체가 결정과 디테일을 구분하지 못하면, 그것은 표류한다. 코드든, 지식이든, 법이든.
결론: 박멸이 아니라 밀어올리기
드리프트와의 싸움은 박멸이 목표일 수 없다. 박멸은 불가능하다. 닫는 도구가 늘 다음 층을 열기 때문이다.
목표는 다른 것이다. 더 높은, 표면적이 더 작은 층으로 드리프트를 밀어올리고, 그 층을 기계적 검증으로 무장하는 것. 수만 줄의 raw 코드에 흩어져 있던 결정을 SSOT 한 곳으로 모으면, 표류할 수 있는 표면이 극적으로 줄어든다. 남은 표면—생성기의 행동 불변식—은 게이트로 막는다. 그래도 맨 위에는 더 이상 위임할 곳 없는 마지막 층, 사람의 판단이 남는다. 거기서 우리는 매번 새로 검증하고 약속을 다시 박는다.
이것이 래칫이다. 한 방향으로만 돈다. 한 번 올라간 톱니는 미끄러져 내려오지 않는다. 엔트로피는 결정을 끌어내리려 하고, 래칫은 그때마다 한 칸씩 다시 올린다. 평형은 없다. 멈추면 표류한다.
드리프트는 죽지 않는다. 그래서 우리는 멈추지 않는다. 엔트로피에 맞서 약속을 짓는 일은, 한 번의 승리가 아니라 영구적인 래칫이다.
관련 글
더 읽을거리 (외부)
- Lehman’s laws of software evolution — 소프트웨어가 손대지 않으면 복잡해진다는 경험 법칙의 개요.
- Landauer’s principle — 정보를 지우는 데 드는 열역학 비용.
출처
- Perry, D. E. & Wolf, A. L. (1992). Foundations for the Study of Software Architecture. ACM SIGSOFT Software Engineering Notes, 17(4), 40-52. ACM — 침식(erosion)과 드리프트(drift)의 구분.
- De Silva, L. & Balasubramaniam, D. (2012). Controlling software architecture erosion: A survey. Journal of Systems and Software, 85(1), 132-151. ScienceDirect
- Lehman, M. M. (1980). Programs, Life Cycles, and Laws of Software Evolution. Proceedings of the IEEE, 68(9), 1060-1076. IEEE — 복잡도 증가 법칙·지속 변화 법칙.
- Landauer, R. (1961). Irreversibility and Heat Generation in the Computing Process. IBM Journal of Research and Development, 5(3), 183-191. IBM — 정보 소거의 최소 열역학 비용.
- Shannon, C. E. (1948). A Mathematical Theory of Communication. Bell System Technical Journal, 27, 379-423. DOI — 정보·엔트로피·오류정정의 토대.
- Cunningham, W. (1992). The WyCash Portfolio Management System. OOPSLA ‘92 Experience Report. c2.com — 기술 부채와 “제대로 안 된 코드의 이자”.