📖 구조 노트 목차 Chapter 23 읽는 중

23. 무엇을 끝까지 지켜야 하는가

구조 노트 Chapter 23 2026. 07. 18.

처음 패킷 직렬화 코드를 짤 때는 리플렉션을 썼다. 새 메시지 타입을 하나 추가하면, 그 타입의 필드를 일일이 나열해서 바이트로 바꾸는 코드를 손으로 쓰는 대신, 런타임에 그 타입의 필드 목록을 훑어서 자동으로 바이트로 바꿔 주는 함수 하나가 모든 타입을 처리했다. 편했다. 새 메시지 타입에 필드를 하나 추가해도 직렬화 코드는 건드릴 필요가 없었다. 컴파일도, 배포도, 아무 것도 신경 쓰지 않아도 다음 빌드부터 그 필드는 알아서 실려 나갔다.

문제는 속도였다. 트래픽이 늘면서 리플렉션이 매 호출마다 타입 정보를 뒤지는 비용이 눈에 띄게 부담스러워졌다. 그래서 코드 생성으로 갈아탔다. 빌드 시점에 각 메시지 타입을 분석해서, 그 타입 전용의 직렬화 함수를 미리 텍스트로 찍어 내는 방식이었다. 런타임에는 그 생성된 함수가 그대로 실행되니 리플렉션 비용은 사라졌다. 속도는 확실히 나아졌다.

그런데 얼마 지나지 않아 메시지 하나에 필드를 새로 추가했는데 그 값이 클라이언트에 도착하지 않는 사고가 났다. 원인은 단순했다. 코드 생성기를 다시 돌리는 것을 깜빡한 것이다. 리플렉션 시절에는 이런 실수가 애초에 일어날 수 없었다. 필드를 추가하는 순간 다음 실행부터 자동으로 반영됐으니까. 코드 생성으로 옮긴 뒤에는 “필드를 추가했으면 생성기를 다시 돌려야 한다”는 새로운 규칙이 조용히 생겨났고, 그 규칙을 깜빡하는 사람이 나오는 것도 시간 문제였다.

이 사고를 겪고 나서야 뒤늦게 알아차린 것이 있다. “필드를 추가하면 자동으로 직렬화에 반영된다”는 동작은, 그동안 이 시스템이 지켜야 할 본질적인 규칙이라고 은연중에 여겨져 왔다. 하지만 실제로 그것은 리플렉션이라는 구현이 우연히 얹어 준 부가 혜택이었을 뿐이다. 이 시스템이 정말로 지켜야 했던 약속은 그보다 한 단계 아래에 있었다. “이 타입의 값은 이 바이트 형식과 정확히 왕복 가능해야 한다”는 것, 즉 직렬화한 결과를 다시 역직렬화하면 원래 값으로 돌아온다는 약속이다. 이 약속은 리플렉션에서도 코드 생성에서도 똑같이 지켜졌다. 반면 “필드를 추가만 하면 아무것도 더 할 필요가 없다”는 편리함은 이 약속에 포함된 적이 없었다. 다만 리플렉션이라는 특정 구현이 그 편리함을 덤으로 얹어 주고 있었을 뿐이고, 아무도 그 덤을 계약과 구분해서 인식하지 못하고 있었다.

22장에서 프록시가 원본과 완전히 다른 구현이었으면서도 클라이언트 앞에서는 같은 서비스로 통했던 이유는, 겉으로 관찰되는 화살표, 즉 요청과 응답의 관계만 그대로였기 때문이라고 했다. 직렬화 코드도 마찬가지다. 바이트를 넣으면 원래 값이 나오고 값을 넣으면 정해진 바이트가 나온다는 화살표는 리플렉션에서 코드 생성으로 옮겨 가면서도 한 치도 어긋나지 않았다. 하지만 “필드 추가만으로 반영된다”는 화살표는 애초에 이 시스템이 약속한 화살표가 아니었다. 코드 위에서는 둘 다 똑같이 안정적으로 작동하는 것처럼 보였기 때문에, 그 차이는 구현을 갈아치우는 순간이 되어서야 드러났다.

13장에서 15장까지 살펴본 것들, 즉 관계가 대상보다 먼저라는 것과 두 구조가 같음을 판정하는 기준과 구조를 옮길 때 지켜야 할 규칙은 모두 이 구별의 재료였다. 다만 이 구별은 미리 알 수 있는 것이 아니다. 리플렉션을 채택했을 때는 그것이 주는 편리함 전부가 본질처럼 느껴졌다. 구현을 실제로 바꾸는 순간, 무엇이 본질이고 무엇이 우연이었는지가 드러난다.