기술

미리보기는 정상인데 결과물만 틀렸습니다 — 쌍둥이 코드 한쪽만 고친 탓이었습니다

2026.10.069분 읽기

장애 알림이 안 오는 결함이 가장 오래 남습니다. 화면도 뜨고 산출물도 나오고 테스트도 통과하는데, 결과만 조금 다른 종류가 그렇습니다. 사용자가 보고하는 말은 늘 같습니다. '비슷한데 뭔가 다릅니다.'

자사 영상 콘텐츠 프로젝트에서 자막을 얹은 영상을 만듭니다. 최종 산출물을 만드는 쪽은 서버가 담당하고, 작업 중에 미리 보여주는 쪽은 화면이 담당합니다. 두 쪽의 실행 환경이 다릅니다. 줄을 어디서 접을지, 자막 띠를 어떻게 쌓을지 같은 규칙이 양쪽에 각각 구현돼 있었습니다. 이틀 사이에 같은 뿌리에서 결함이 두 번 났습니다.


미리보기가 정상으로 보이는 것이 함정이었습니다

사건은 두 번 다 같은 모양으로 흘렀습니다. 순서를 적어 두면 어디서 잘못 짚었는지가 보입니다.

규칙을 한쪽에서 고칩니다. 대개 서버 쪽입니다. 최종 산출물의 원천이니까 그쪽을 먼저 봅니다

테스트를 돌립니다. 통과합니다

미리보기를 엽니다. 뜹니다. 깨진 데가 없습니다

결과물을 봅니다. 비슷한데 뭔가 다릅니다

로직을 의심하며 서버 쪽 코드를 다시 읽습니다. 맞게 짜여 있습니다

그제서야 화면 쪽에 같은 일을 하는 함수가 따로 있다는 것을 떠올립니다

3단계가 가장 나빴습니다. 미리보기가 정상으로 보이면 '화면 쪽은 문제없다'는 결론으로 바로 넘어갑니다. 그런데 실제로는 화면 쪽이 옛 규칙으로 정확하게 동작하고 있었습니다. 고장이 아니라 다른 규칙이었으니 깨진 데가 보일 이유가 없었습니다. 정상으로 보이는 화면 한 장이 범위를 절반으로 잘못 좁혀 준 셈입니다.

테스트가 통과한 이유도 같습니다. 서버 쪽 단위 테스트는 서버 쪽 규칙에 대해 통과하고, 화면 쪽 단위 테스트는 화면 쪽 규칙에 대해 통과합니다. 양쪽이 서로 다른 규칙을 각각 정확하게 지키고 있는 상태를, 각자를 따로 보는 테스트는 잡지 못합니다.

같은 줄바꿈 규칙이 서버와 화면에 따로 구현돼 한쪽만 고쳐진 쌍둥이 코드 구조도


두 번 중 한 번은 함수가 아예 안 물려 있었습니다

첫 번째는 줄이 접히는 자리가 달랐습니다. 서버 쪽 줄바꿈 규칙을 '길이를 고르게 나누기'로 고쳤는데, 화면 쪽에서 같은 일을 하는 함수를 안 고쳤습니다. 미리보기와 결과물의 접힘 위치가 문장마다 한두 어절씩 엇갈렸습니다. 어느 쪽도 틀린 모양은 아니라서 나란히 놓고 봐야만 차이가 보였습니다.

두 번째는 자막 띠가 결과물에서만 겹쳤습니다. 미리보기는 줄마다 띠가 따로 쌓여 안 겹쳤고, 결과물은 겹쳐서 그 부분만 두 번 어두웠습니다. 겹침을 막으려고 써 둔 함수가 조립 경로에 안 물려 있었습니다. 함수는 있는데 부르는 자리가 없었습니다. 그 함수는 만든 지 이틀 뒤에야 물렸습니다.

'만들었다'와 '물렸다'는 다른 사건입니다. 저는 정의를 보고 작업이 끝났다고 판단했고, 실제로 이틀이 벌어졌습니다. 죽은 코드는 문법 오류처럼 생기지 않았습니다. 편집기도 테스트도 아무 말을 하지 않았고, 그 함수를 부르는 자리가 없다는 사실만 남았습니다.


비슷한데 뭔가 다르다는 재라는 신호였습니다

겹침 여부는 눈으로 판정하지 않고 숫자로 쟀습니다. 흰 바탕에 자막만 얹어 렌더한 뒤 행 밝기 분포를 봤습니다. 띠가 겹치면 그 행만 어두워지므로 겹침이 1차원 값으로 내려옵니다. 줄 간격을 10px, 14, 21, 33 으로 바꿔 가며 분포를 본 다음 값을 정했습니다.

'화면이 넘친다'도 같은 식으로 쟀습니다. 문서 전체 너비 대 보이는 너비가 2630 대 1440 이었습니다. 두 값을 나란히 적는 것만으로 '넘친다'가 '얼마나 넘친다'로 바뀝니다.

그 전에 눈대중으로 정한 줄 간격 배수 1.42 는 한 번에 '너무 벌어졌다'가 됐습니다. 재고 나서 정한 값은 한 번에 맞았습니다. 사람 눈은 차이를 감지하지만 크기를 알려주지 않습니다. 밝기·너비·좌표처럼 숫자로 뽑을 수 있는 대리값을 하나 찾으면, 원인 후보가 줄고 조정할 값도 같이 정해집니다.

눈대중으로 정한 줄 간격과 밝기 분포를 재서 정한 줄 간격의 BEFORE AFTER 비교


세 가지로 정리했습니다

같은 결함이 두 번 난 뒤에 처방을 세 줄로 적어 뒀습니다. 순서대로 봅니다.

원천을 하나로 둘 수 있으면 둡니다. 서버가 계산해서 내려주고 화면은 그리기만 합니다. 같은 프로젝트의 '지금 만들고 있나' 상태는 이미 그렇게 돼 있습니다. 양쪽이 잡 큐 하나를 보기 때문에 그쪽은 이런 일이 안 났습니다

쌍둥이가 불가피하면 같은 예문으로 양쪽을 대조하는 검사를 붙입니다. 각자를 따로 보는 테스트 두 개 대신, 양쪽을 같은 입력으로 나란히 세우는 검사 한 개가 필요합니다

고쳤으면 부르는 자리를 찾아 확인합니다. 정의만 보고 작업이 끝났다고 판단하지 않고 호출부를 찾습니다

두 번째에 조건이 하나 붙습니다. 기대값은 원본을 실제로 돌려서 받아 적어야 합니다. 손으로 쓰면 그 기대값 자체가 세 번째 진실이 됩니다. 원본이 바뀌어도 검사는 옛 값을 지키며 통과하고, 그 상태는 쌍둥이가 둘일 때보다 나쁩니다. 정답표는 원본 실행 결과를 받아 적는 자리이고, 사람이 생각하는 정답을 적는 자리가 아닙니다.

같은 것을 두 곳이 다르게 판정하는 구조는 렌더링 밖에서도 같은 모양으로 나타납니다.

정산 쪽에서는 같은 계좌를 두 화면이 다르게 분류한 일이 있었습니다. 그때도 양쪽 각각은 자기 기준대로 정확하게 동작하고 있었습니다.


미리보기가 붙은 기능은 전부 쌍둥이 후보입니다

이 구조는 영상 자막에만 있는 것이 아닙니다. 보여주는 쪽과 만드는 쪽의 실행 환경이 다르면 같은 규칙이 두 벌 있을 가능성이 높습니다. 아래는 같은 자리에 서 있는 기능들입니다.

기능

보여주는 쪽

만드는 쪽

에디터 프리뷰

브라우저 렌더러

서버 변환기

메일 템플릿 미리보기

관리 화면

발송 엔진

리포트 PDF 미리보기

화면 컴포넌트

PDF 생성기

영수증·라벨 출력

출력 전 화면

프린터 드라이버 쪽 조립

의사결정 쪽에서 보면 이 결함의 비용은 발견 시점에 걸려 있습니다. 에러가 안 나므로 모니터링에 안 걸리고, 테스트가 통과하므로 배포도 막히지 않습니다. 결국 사용자가 산출물을 보고 '뭔가 다르다'고 말할 때 알게 됩니다. 산출물이 이미 밖으로 나간 뒤면 그 말을 듣는 자리가 고객 쪽입니다.

다만 쌍둥이가 늘 나쁜 선택은 아닙니다. 미리보기를 서버 왕복 없이 즉시 보여주는 것이 제품 가치인 경우가 있습니다. 그때는 '없애기'가 아니라 '대조 검사 붙이기'가 답입니다. 원천을 하나로 모으는 쪽에도 값이 듭니다. 화면이 서버 응답을 기다려야 하면 미리보기가 느려지고, 연결이 끊긴 상황에서는 아무것도 못 보여줍니다.

대조 검사의 한계도 같이 적어 둡니다. 예문 세 줄로 검사를 만들면 그 세 줄 바깥의 분기는 여전히 갈립니다. 예문은 실제로 틀렸던 입력을 넣어 늘려 가는 쪽이 맞습니다. 결과가 이미지나 영상인 도메인은 자동 대조 자체가 비쌉니다. 여기서는 밝기 같은 1차원 대리값으로 줄여 쟀는데, 모든 시각 결함이 그렇게 줄여지지는 않습니다.

보여주는 쪽과 만드는 쪽이 갈리는 네 가지 기능 목록과 대조 검사 한 개를 붙이는 처방


점검 항목

미리보기나 출력 기능을 들여다볼 때 다음 다섯 가지를 봅니다.

같은 규칙이 몇 곳에 구현돼 있는지 센다. 두 곳 이상이면 그 목록을 적어 둔다

한쪽을 고칠 때 다른 쪽을 같이 고치는 것이 작업 단위인지, 아니면 사람 기억에 맡겨져 있는지 확인한다

양쪽을 같은 입력으로 나란히 세우는 검사가 있는지 본다. 없으면 한 개를 만든다

그 검사의 기대값이 원본을 돌려서 받아 적은 값인지, 사람이 손으로 쓴 값인지 확인한다

새로 만든 함수나 고친 동작의 호출부를 찾아 실제로 물려 있는지 본다

조용히 틀리는 결함은 기록으로도 남지 않습니다. 사유 없이 사라진 판정을 쫓은 적이 있는데, 그때도 에러는 한 건도 없었습니다.

같은 성격의 다른 사례를 정리해 둔 글이 있습니다.

미리보기와 산출물이 어긋나면 로직을 다시 읽기 전에 '같은 규칙이 두 곳에 있나'를 먼저 봅니다. 그 확인은 몇 분이면 끝나고, 아니라는 답이 나와도 범위가 줄어듭니다. 그리고 눈이 '비슷한데 뭔가 다르다'고 말하면 그 자리에서 재는 쪽으로 넘어갑니다. 숫자로 뽑을 대리값을 하나 찾는 쪽이, 눈대중으로 값을 몇 번 고치는 쪽보다 짧을 것으로 봤습니다.

#쌍둥이코드#단일원천#미리보기#렌더링#골든테스트#죽은코드