Skip to content
게으른 엔지니어의 기술 블로그
Go back

설계까지 다 거치고도, 실행해봐야 보였다 5편 — 되돌리기 버튼 하나에, 버그가 세 겹으로 숨어 있었다

설계까지 다 거치고도, 실행해봐야 보였다 · 5/6편

태그 병합 기능은 AI가 비슷한 태그 두 개를 하나로 합칠지 판단해준다. 그런데 가끔 합치는 방향을 거꾸로 고른다 — 예를 들어 금융업계에서 관용적으로 대문자로 쓰는 약어를, “일반 개념 태그는 소문자로”라는 규칙 쪽에 끌려가 소문자로 정규화해버리는 식이다. 문제는 이미 실행된 병합을 되돌릴 방법이 전혀 없었다는 것. 되돌리기 버튼을 만들기로 했다.

대칭이라 엔진은 손댈 게 없었다

병합을 실제로 실행하는 함수를 다시 들여다보니, 두 태그 이름을 받아서 하나를 다른 하나로 바꾸는 것뿐인 완전히 대칭적인 함수였다. 그러니 이 함수를 인자만 뒤집어서 다시 부르면 그게 그대로 되돌리기가 된다. 새로운 엔진 로직은 필요 없고, 병합 완료 메시지에 “되돌리기” 버튼만 하나 붙이면 됐다.

내가 직접 적어준 방법이 버그였다 — 되돌리기의 되돌리기 식별자 충돌

구현 계획서를 쓰면서, 완료 메시지의 각 항목마다 버튼을 렌더링하고 “실행이 끝나면 그 항목을 화면에서 다시 찾아서 완료 표시로 바꾼다”는 방법을 코드까지 구체적으로 적었다. 항목을 찾는 방법으로는, 태그A → 태그B라는 식별자를 버튼에 붙여두고 되돌리기가 끝난 뒤 그 식별자로 화면 전체를 다시 뒤지는 방식을 제안했다.

이 계획대로 구현한 태스크가 리뷰에 올라갔는데, 리뷰어가 걸린 부분을 잡아냈다. 되돌리기 결과도 같은 형식의 완료 메시지로 다시 표시되고, 그 메시지에도 되돌리기 버튼이 또 달린다 — 즉 “되돌리기의 되돌리기”가 가능하다. 그러면 같은 태그A → 태그B라는 식별자가 한 채팅 세션 안에 두 번, 세 번 나타날 수 있다. 식별자로 화면을 다시 뒤지는 방식은 이 경우 어떤 줄을 완료 표시로 바꿔야 할지 구분하지 못한다 — 잘못된 줄이 갱신되거나, 여러 줄이 한꺼번에 바뀔 수 있었다.

내가 계획서에 직접 써준 방법 자체가 이 시나리오를 놓치고 있었던 것이다. 고친 방법은 식별자로 나중에 다시 찾는 대신, 버튼을 클릭한 바로 그 순간 이미 화면에 잡혀 있는 그 버튼 자신을 가리키는 함수 하나를 만들어서, 그 함수를 실행 로직에 그대로 넘겨주는 것이었다. 나중에 누군가 다시 찾을 필요가 없어졌다 — 애초에 어떤 버튼인지 헷갈릴 자리 자체가 사라진 셈이다.

버그를 고치자 계획서가 낡았다

이 수정으로 함수 몇 개의 신호(인자 개수)가 바뀌었다. 다음 태스크는 이 함수들을 그대로 가져다 쓰는 배선 작업이었는데, 그 태스크를 준비하려고 계획서를 다시 열어보니 아직 옛날 신호 그대로 적혀 있었다. 방금 고친 내용이 계획서에는 반영이 안 된 상태였던 것.

이걸 다음 태스크를 맡길 사람에게 그대로 넘겼다면, 계획서에 적힌 옛 방식으로 구현됐을 거고, 그러면 방금 고친 함수와 신호가 안 맞아 코드가 아예 안 돌아가거나, 더 나쁘게는 어중간하게 맞아떨어져서 방금 잡은 버그가 조용히 되살아났을 수도 있었다. 다음 단계를 준비하는 과정에서 계획서부터 다시 읽어본 덕에, 넘기기 전에 직접 잡아서 고쳤다.

두 태스크 다 리뷰를 통과한 뒤에 — 실패 시 버튼 멈춤·동시 실행 덮어쓰기

이렇게 두 태스크 모두 각자의 리뷰를 통과했다. 각 태스크가 맡은 범위 안에서는 스펙도 맞고 코드 품질도 문제없다는 판정이었다.

그런데 이 프로젝트의 개발 흐름은 여기서 끝나지 않는다. 개별 태스크 리뷰 다음에, 전체 변경을 처음부터 끝까지 다시 통으로 훑는 마지막 리뷰가 한 번 더 있다. 그 리뷰에서 버그가 두 개 더 나왔다.

첫 번째는 되돌리기가 실패했을 때였다. 버튼은 클릭하자마자 “되돌리는 중…”으로 바뀌고 비활성화되는데, 성공했을 때만 그 상태를 풀어주는 코드가 있었다. 파일을 읽거나 쓰는 도중 오류가 나면, 버튼은 영원히 “되돌리는 중…”에 멈춘 채로 남는다 — 같은 메시지에서 다시 시도할 방법이 없어지는 것이다.

두 번째는 동시 실행이었다. 서로 다른 두 되돌리기 버튼을 연달아 누르면, 둘 다 동시에 파일을 읽고 쓰는 작업을 시작할 수 있었다. 둘 중 하나가 다른 하나보다 늦게 파일을 읽어서 늦게 저장하면, 먼저 저장된 내용이 통째로 덮어씌워질 수 있다 — 조용히, 파일 하나가 사라지는 방식으로.

왜 이 둘은 개별 리뷰에서 안 보였나

두 버그 다 “성공 경로”만 보면 완벽하게 동작한다. 실패 처리와 동시 실행은 각 태스크의 코드를 따로따로 읽을 때는 딱히 틀린 데가 없어 보인다 — 실패하면 오류 메시지를 보여주고, 버튼 하나는 그 버튼 나름대로 자기 상태를 관리하고 있으니까. 문제는 그 버튼이 페이지 전체에서 유일한 되돌리기 버튼이 아니라는 사실이, 딱 하나의 태스크 범위 안에서는 눈에 안 들어온다는 데 있었다.

이번에도 결국 실행이, 그리고 그 실행 전체를 한 번에 놓고 보는 시선이 문서와 개별 리뷰로는 안 걸리던 자리를 잡아냈다.

남는 생각

이번 사이클은 세 겹으로 걸렸다. 내가 계획서에 직접 써준 방법 자체가 버그였고, 그걸 고치자 계획서가 낡아버린 걸 다음 단계를 준비하다가 스스로 알아챘고, 두 태스크 모두 통과한 뒤에도 전체를 다시 훑는 리뷰가 새로운 버그 두 개를 더 찾았다.

가장 인상 깊었던 건 첫 번째 겹이다 — 지금까지는 대개 “구현이 계획을 잘못 따랐다”는 식의 어긋남이었는데, 이번엔 계획 자체가 틀려 있었다. 리뷰가 지켜야 하는 건 구현이 문서와 일치하는지만이 아니라, 그 문서가 애초에 맞는 말을 하고 있는지도 포함된다는 걸 다시 확인한 셈이다. 그리고 그 문서를 쓴 사람도 나였다는 점에서, “설계자가 항상 옳다”는 가정이 제일 먼저 버려야 할 가정이라는 것도.


Share this post:

설계까지 다 거치고도, 실행해봐야 보였다

  1. 1. 설계까지 다 거치고도, 실행해봐야 보였다 1편 — 테스트가 원천적으로 못 잡는 자리에 버그를 심어놓고 있었다
  2. 2. 설계까지 다 거치고도, 실행해봐야 보였다 2편 — 병합을 빼자, 분할이 살아났다
  3. 3. 설계까지 다 거치고도, 실행해봐야 보였다 3편 — 레이스를 막을 방법이 아니라, 없앨 이유를 찾았다
  4. 4. 설계까지 다 거치고도, 실행해봐야 보였다 4편 — 없는 줄도 몰랐던 안전망을 찾아냈다
  5. 5. 설계까지 다 거치고도, 실행해봐야 보였다 5편 — 되돌리기 버튼 하나에, 버그가 세 겹으로 숨어 있었다
  6. 6. 설계까지 다 거치고도, 실행해봐야 보였다 6편 — 자정을 넘긴 순간에만 걸리는 레이스였다