2002년부터 저는 제가 마주친 모든 까다로운 버그를 추적해왔습니다. 9년 전, 그때까지의 버그에서 얻은 교훈을 담아 블로그 글을 작성했습니다. 그 이후로 기록해온 버그들을 이번에 전부 다시 돌아봤습니다. 첫 번째 회고에서 정리했던 교훈들을 실제로 잘 실천해왔는지 확인하고 싶었고, 그사이 어떤 유형의 버그들을 마주쳤는지도 살펴보고 싶었습니다. 이전과 마찬가지로 교훈을 코딩, 테스팅, 디버깅의 카테고리로 나눠 정리했습니다.

코딩
1. 빈 케이스. 다섯 개의 버그가 빈 줄, 빈 파일, 공백, 또는 값이 0인 경우와 관련이 있었습니다. 예를 들어, 공백 한 칸(0이 아닌)이 있는 줄은 비어있는 것으로 건너뛰어야 했지만 그렇지 않았습니다. 다른 경우에는 csv 파일의 빈 헤더가 문제를 일으켰습니다. 최근 예시에서는 수정이 필요한 누락된 매핑이 0개임에도 불구하고 알림 메일이 발송되었습니다. 이전 글에서 0과 null 케이스를 고려하지 못했다는 것을 알게 되었습니다. 분명히 저는 이런 종류의 오류를 발견하는 데 더욱 주의를 기울여야 합니다.
2. 날짜. 네 개의 버그가 어떤 식으로든 날짜와 관련이 있었습니다. 예를 들어, 이전 날짜를 확인하는 로직은 이전 날이 주말인 경우 어떻게 해야 하는지를 고려해야 합니다. 연속 공휴일이 며칠까지 이어질 수 있는지 가정할 때는 일본의 골든위크(Golden Week)를 기억하세요. 또한, 종료 날짜가 오늘 이후인지 확인하는 것만으로는 계약이 활성화되어 있는지 알기에 충분하지 않습니다. 시작 날짜도 오늘 이후일 수 있습니다.
3. 오래된 데이터 형식. 변경된 데이터 형식을 사용하도록 로직을 업그레이드하는 것은 항상 까다롭습니다. 데이터베이스의 오래된 데이터를 새 형식으로 변환해야 할 수도 있다는 것을 고려해야 합니다. 또한, 새 로직이 배포되었더라도 진행 중인 작업이 여전히 이전 형식을 사용하는 과도기가 있을 수 있습니다. 계약 이름 끝의 공백을 제거했지만, 4-eye 승인 로직(역자 주: 두 명의 독립적인 검토자가 승인해야 하는 원칙)이 오래된 이름에서 실패한 경우도 있었습니다.
4. 별칭이 생긴 딕셔너리/해시맵. 한 번 이상, 저는 실수로 이미 존재하는 딕셔너리에 대한 별칭에 불과한 두 번째 딕셔너리를 만들었습니다. 이는 한쪽의 변경 사항이 다른 쪽에도 나타난다는 것을 의미했습니다. 이로 인해 코드 실행 결과가 매우 혼란스러워졌습니다.
5. 로컬 변경사항. 때때로 푸시를 잊어버린 로컬 변경사항이 있어서, 로컬에서 테스트한 것이 배포된 것과 달랐습니다. 이상적으로는 CI 테스트에서 잡아야 했지만, 이러한 특정 케이스에 대한 테스트가 없었습니다. 비슷한 경우도 있습니다. 로컬에서 작업하는 동안 일부 코드를 주석 처리하고 몇 가지 변경을 수행한 다음 코드의 주석을 다시 해제했습니다. 하지만 그 사이에 다른 로직이 변경되어 (코드가 주석 처리되어 있는 동안) 버그가 발생했습니다.
테스팅
6. 탐색적 테스팅. 기능을 완료하기 전에 탐색적 테스팅을 수행할 때 많은 버그를 발견했습니다. 종종 다양한 기능의 활성화/비활성화 상태에 따른 기능 간 상호 작용과 관련이 있었고, 이로 인해 버그가 드러났습니다. 다른 경우에는 고객이 특정 방식으로 기능을 사용하고 있다고 생각했습니다. 하지만 그것이 작동하지 않자 그들에게 물어봤고, 그들은 완전히 다른 방식으로 기능을 사용하고 있다고 말했습니다. 또한, GUI에서 보면 명백해지는 것들도 있습니다. 예를 들어, 제가 만든 한 변경사항은 실수로 표시된 모든 레코드에 *"hasApiKey=false"를 추가했지만, 의도는 *"false"로 설정된 것은 숨기는 것이었습니다.
7. 테스트에서 더 작은 설정. 일반적으로 테스트 시스템은 프로덕션(prod) 시스템보다 여러 면에서 더 작습니다. 예를 들어, 테스트 시스템에는 이벤트 핸들러가 하나만 있을 수 있지만 프로덕션 시스템에는 두 개가 있습니다. 이로 인해 순서대로 처리되어야 하는 두 개의 이벤트가 프로덕션에서 병렬로 처리되는 버그가 발생했습니다. 이벤트들은 두 개의 다른 이벤트 핸들러로 갔지만, 테스트에서는 (이벤트 핸들러가 하나만 있어서) 항상 순차적으로 처리되었습니다. 이런 종류의 버그는 당연히 테스트에서 발견하기 매우 어렵습니다(불가능할 수도 있습니다).
8. 접근 권한. 때때로 저는 너무 많은 접근 권한을 가진 사용자로 기능을 테스트했습니다. 이로 인해 기능이 작동하는 것처럼 보였지만, 실제로는 사용자가 특정 권한을 가진 경우에만 작동했습니다.
디버깅
9. 좋은 로깅. 많은 버그의 경우, 문제를 해결하는 핵심은 로그를 보고 무슨 일이 일어났는지 파악하는 것이었습니다. 예를 들어, 동일해야 하는 세 개의 캘린더 서비스 중 하나가 잘못된 답을 제공했을 때, 로그에서 결함이 있는 서비스가 시작 시점에 데이터의 일부만 받았다는 것을 볼 수 있었습니다(오류 표시도 없었습니다). 로그와 오류 메시지를 주의 깊게 읽는 것도 중요합니다. 종종 저는 로그를 꼼꼼히 확인하지 않고 무슨 일이 일어났는지 안다고 가정하곤 했습니다. 타임스탬프도 매우 도움이 됩니다. 여러 버그의 "발견 방법" 섹션에서, 저는 다음과 같은 내용을 작성했습니다. "그런 다음 데드 레터가 발생한 시점 즈음에 Kibana에서 검색했습니다."
10. 동료와의 논의. 이전과 마찬가지로, 동료와 논의하는 것은 어려운 버그를 해결하는 매우 효과적인 방법입니다. 최근의 한 경우에, 문제를 해결하는 동안 우리 모두 사무실에 함께 있었습니다. 보통 우리는 일주일에 3일은 원격으로 일하지만, 물리적으로 가까이 있으면 협력이 훨씬 더 효과적입니다.
11. 알림. 알림이 없었다면 일부 오류는 아예 발견되지 않았거나 늦게 발견됐을 것입니다. 좋은 알림을 설정하는 것은 정말로 효과가 있습니다.
12. 최소한의 케이스로 재현. 많은 경우에, 저는 작동하는 케이스와 실패하는 케이스가 있었습니다 (아마도 메인 브랜치와 기능 브랜치에서). 코드를 주석 처리하거나 기능을 축소하는 방법으로 문제의 원인을 좁혀 가는 것이 핵심이었습니다.
회고
이 모든 버그의 노트를 검토하는 것은 꽤 재미있었습니다. 일부 버그는 노트 없이도 기억했을 것입니다. 많은 버그는 노트를 읽었을 때 기억났고, 일부는 노트를 읽은 후에도 전혀 기억이 나지 않았습니다. 함께 일했던 동료들과 우리가 함께 작업했던 시스템들을 (다양한 프로그래밍 언어로) 기억하는 것은 꽤 향수를 불러일으켰습니다. 정말로 인상 깊었던 것은 각 시스템을 구성하는 세부 사항의 양이었습니다. 이는 (다시) 소프트웨어 엔지니어링의 얼마나 많은 부분이 실제로 도메인에 대해 학습하는 것인지에 대해 생각하게 만들었습니다.
9년 전의 제 글을 되돌아보면, 제가 그곳에서 강조했던 문제들을 피했을까요? 대부분의 경우에는 그렇습니다. 하지만 저는 여전히 빈 값, 0 또는 null 케이스를 처리하는 데 여러 번 실패했습니다. 빈 값, 0, null 케이스는 제가 더욱 주의를 기울여야 하는 부분입니다. 잘못된 if 문으로 인해 발생한 잠재적으로 정말로 심각한 버그도 하나 있었습니다. 다행히도, 탐색적 테스팅을 수행하면서 이를 발견했고, 로그에서 이상한 것을 발견했습니다. 로그를 읽는 것에 관해서는, "세심하게 주의를 기울이라"는 제 오래된 조언을 더 자주 따라야 합니다. 하지만 전반적으로, 저는 과거에 일으켰던 많은 유형의 버그를 피하는 데 성공했습니다.
분석
아래 다이어그램은 시작 이후로 매년 제가 기록한 버그의 수를 보여줍니다. 지난 9년 동안 평균적으로 두 달에 한 번 까다로운 버그를 마주했습니다.

모든 버그가 제가 일으킨 것은 아닙니다. 때때로 다른 사람들이 일으킨 버그가 너무 흥미로워서 저도 포함시킵니다. 지난 9년 동안 약 70%의 버그가 제가 일으킨 것이었습니다. 저는 버그를 수정하는 데 걸린 시간도 기록합니다. 여기에는 문제 해결, 수정 및 수정 사항 테스트가 포함됩니다. 아래는 소요 시간에 대한 다이어그램입니다. 8시간 이상은 여러 날을 의미한다는 점에 유의하세요. 따라서 24시간은 3일이지, 24시간 쉬지 않고 작업한 것이 아닙니다.

결론
많은 오류는 원인을 파악하기 전까지는 도저히 이해할 수 없어 보입니다. 예를 들어, 우리 중 누구도 어떻게 발생할 수 있는지 이해할 수 없었던 SQL 오류가 있었습니다. 결국, (데이터베이스 쿼리를 수행하는) 한 노드가 재시작되지 않아서 이전 버전의 소프트웨어를 실행하고 있었다는 것이 밝혀졌습니다. 다른 경우에는, 일단 보게 되면 파악하기 어렵지 않지만 그럼에도 흥미롭습니다. 몇 년 전에 Cassandra에서 오버플로우가 있었습니다. 문제의 변수는 파이썬과 카산드라 모두에서 int였지만, 파이썬에서는 정수가 임의로 클 수 있는 반면 카산드라에서는 int가 32비트입니다.
원인이 무엇이든, 무슨 일이 일어났는지 파악하는 것은 항상 만족스럽습니다. 버그는 학습을 위한 훌륭한 소스이며, 가장 까다로운 버그를 추적함으로써 저는 각각의 버그로부터 최대한 많이 배우려고 노력하고 있습니다.
'Computer Sci.' 카테고리의 다른 글
| [요즘IT] AI 에이전트 정복 : 규칙, 스킬, 커맨드, 서브 에이전트 활용 전략 (0) | 2026.03.11 |
|---|---|
| [동시성 프로그래밍] Ch4. 동시성 프로그래밍 특유의 버그와 문제점 (0) | 2023.12.13 |
| [한.권.컴.구] Ch1. 컴퓨터 내부의 언어 체계 (1) | 2022.09.25 |
| [동시성 프로그래밍] Ch3. 동기 처리 1 (하) (1) | 2022.09.06 |
| [동시성 프로그래밍] Ch3. 동기 처리 1 (상) (2) | 2022.07.07 |