‘Simple’ 문제들이 단순하지 않을 때: PHP 숨은 보석에서 얻은 힘든 교훈
Source: Dev.to
원래 Medium에 게시되었습니다 – 전체 이야기를 여기서 읽어보세요
새벽 3시의 호출
Picture this: It’s 3 AM. Your phone won’t stop buzzing. The batch process has failed again, corrupting records before crashing. Your CTO wants to know why a “basic CSV upload” has caused three production incidents in two weeks.
이것이 제 현실이었습니다. “이틀짜리 작업”이 3개월짜리 악몽이 되었습니다. 문제는? 저는 CSV를 이미 해결된 것으로 여기고 있었지만, 실제로는 함정이 가득했습니다.
제가 힘들게 배운 교훈:
- Excel은 숨겨진 바이트 순서 표시(BOM)를 추가합니다.
- Google Sheets는 LibreOffice와 다르게 따옴표를 이스케이프합니다.
- macOS와 Windows는 서로 다른 줄 끝 문자를 사용합니다.
- 여러 프로그램에서 편집된 파일은 혼란스러워집니다.
우리는 fgetcsv()와 맞춤형 검증으로 모든 것을 파싱하려고 시도했습니다. 마치 포켓 사전으로 다섯 언어를 번역하는 것과 같았으며—기술적으로는 가능하지만 중요한 뉘앙스를 놓치게 됩니다.
내가 거의 저질렀던 UUID 실수
마이크로서비스를 설계하면서 서비스 간에 충돌하지 않는 ID가 필요했습니다. 내 해결책? bin2hex(random_bytes(16)) 로 32‑character 문자열을 무작위로 생성했습니다. 배포했고, 똑똑하다고 생각했죠.
2주 뒤: 운영 환경에서 중복 ID가 나타났습니다. 드물게 발생했지만, 금융 시스템에서는 “드물게”가 용납되지 않습니다.
생일 역설에 걸렸던 겁니다. 충돌은 예상보다 빨리 통계적으로 가능해집니다. 더 안 좋은 점은, 나는 UUID 버전이 일곱 가지가 있고 각각 특정 시나리오를 위해 설계되었다는 사실을 전혀 몰랐다는 것입니다:
- Version 1: 타임스탬프 + MAC 주소 (정렬 가능, 프라이버시 우려)
- Version 4: 순수 랜덤 (프라이버시 문제 없음, 정렬 불가)
- Version 7: 타임스탬프 순서와 랜덤 요소 결합 (데이터베이스 인덱스에 최적)
나는 수 주 동안 반쯤 만든 해결책에 시간을 쏟아 부어, 수년간 미묘한 버그를 초래할 수도 있었습니다.
우리가 방어할 수 없었던 소송
고객이 $15,000 거래에 대해 이의를 제기했습니다. 우리 데이터베이스에는 해당 거래가 존재한다는 기록이 있었지만, 어떻게 발생했는지는 증명할 수 없었습니다—오직 최종 상태만 알 수 있었습니다.
전통적인 데이터베이스는 현재 상태만 저장합니다: 사용자 잔액, 주문 상태, 구독 등급 등. 그 상태에 이르게 된 과정을 저장하지 않죠. 누군가 이의를 제기하면, 우리는 방어할 방법이 없습니다.
우리가 답변할 수 없었던 질문들:
- 그들이 주문을 시작한 시점은 언제인가?
- 화면에 무엇이 표시되었는가?
- 그들이 명시적으로 확인했는가?
- IP 주소는 무엇인가?
이벤트 소싱은 이를 근본적으로 바꿉니다. 상태가 아니라 이벤트를 저장합니다: account_created, deposited $150, withdrew $50. 모든 상태 변화는 불변합니다.
하지만 처음부터 구현한다면? 저는 개발 환경에서는 완벽히 동작했지만, 실제 운영 부하에서는 무너지는 이벤트 스토어를 만드는 데 3주를 보냈습니다.
내가 결국 알아낸 패턴
모든 실수는 같은 흐름을 따랐습니다:
- “이거 간단해 보인다”
- 명백한 해결책 구현
- 개발 환경에서는 동작
- 프로덕션에서는 예상치 못한 방식으로 붕괴
- 특수 케이스 추가
- 더 많은 특수 케이스
- 유지보수 악몽
내가 마주한 실제 문제들:
- 인코딩이 뒤섞인 CSV 파일, 형식이 잘못된 따옴표, 포함된 개행 문자
- 생일 역설에 의한 UUID 충돌
- 동시 쓰기를 처리하지 못하는 이벤트 스토어
- 2,000줄이 넘는 콘솔 애플리케이션 괴물
- 47개의 파일에 흩어져 있는 검증 로직
- 회로 차단기 없이 동작하는 HTTP 클라이언트가 초래한 연쇄 실패
- DST 전환 시 발생한 날짜 버그로 인한 비용 손실
각 경우마다 이미 특화된 패키지들이 이러한 문제들을 포괄적으로 해결하고 있었습니다.
내 관점을 바꾼 계기
최고의 엔지니어는 모든 것을 처음부터 만드는 사람이 아니다. 그들은 패턴을 인식하고, 트레이드‑오프를 이해하며, 적절한 도구를 선택한다.
새벽 3시에 CSV 인코딩을 디버깅하고 있다면, 문자 집합에 대해 배우는 것이 아니라 이미 해결된 문제에 시간을 낭비하고 있는 것이다. UUID 충돌 감지를 구현하고 있다면, 알고리즘 지식을 강화하는 것이 아니라 수십 년 전의 분산 시스템 연구를 다시 발견하고 있는 것이다.
진정한 비용은 다음과 같다: 이미 해결된 문제를 푸는 데 소비한 1시간은 실제 비즈니스 로직—도메인 고유의 문제이며 경쟁 우위를 제공하는—에 투자되지 않은 시간이다.
Source:
레슨
전문 라이브러리로 전환한 뒤:
- 새벽 3시 CSV 가져오기 호출이 사라짐
- UUID 충돌이 없음
- 분쟁을 위한 완전한 감사 로그
- 유지 보수가 쉬운 콘솔 애플리케이션
- 깔끔하고 테스트 가능한 검증 로직
- 탄탄한 서드파티 통합
- 시간대 전반에 걸친 정확한 날짜 처리
시간을 절약하는 것 이상의 차이가 있습니다. 다른 사람들의 실수에서 배우는 것이죠. 프로덕션 수준의 패키지 하나마다 수백 시간의 개발, 수천 시간의 테스트, 그리고 실제 현장에서 얻은 경험이 담겨 있습니다.
다음에 복잡한 문제에 직면했을 때, 코딩하기 전에 잠시 멈추세요. 다음 질문을 해보세요:
- 누군가 이미 해결했는가?
- 그들의 접근 방식에서 무엇을 배울 수 있는가?
- 겉보기엔 간단하지만 내부는 복잡하지 않은가?
이것이 주니어와 시니어 엔지니어를 구분 짓는 요소입니다—문법 지식이 아니라, 어떤 문제는 맞춤형 솔루션이 필요하고, 어떤 문제는 거인의 어깨 위에 서는 것이 더 유리한지를 판단하는 능력이죠.
전체 상세 내용을 보고 싶다면, Medium에서 전체 기사 읽기에서 각 문제, 실패 사례, 그리고 해결 방안을 깊이 있게 살펴보세요.