Redis 키스페이스 알림과 임대를 이용한 GPU 워커 풀 로드 밸런싱
2026년 7월 23일 · 7분 읽기
MongoDB 변경 스트림을 사용한 Cron 스케줄 핫스와핑
2026년 7월 23일 · 6분 읽기
단일 실행기 작업을 위한 CAS 및 하트비트를 사용한 임대 잠금
2026년 7월 22일 · 7분 읽기
종량제 데이터베이스에서 커서 페이지네이션이 Skip보다 나은 이유
2026년 7월 21일 · 6분 읽기
Firestore의 MongoDB 호환성에서 `retryWrites=false`의 함정
================================================================
Firestore를 MongoDB 호환 모드에서 사용하고 있다면 쓰기 작업이 때때로 실패하고 온라인에서 찾을 수 있는 유일한 "해결책"은 연결 문자열에 `retryWrites=false`를 추가하는 것뿐인 특이한 문제를 겪었을 수 있습니다. 이것이 문제를 해결하는 것처럼 보일 수 있지만, 데이터 손실 및 기타 문제로 이어질 수 있는 함정입니다. 이러한 현상이 발생하는 이유와 올바른 해결책이 무엇인지 알아보겠습니다.
## 쓰기 작업이 실패하는 이유
Firestore는 MongoDB를 에뮬레이션할 때 MongoDB의 재시도 가능 쓰기(retryable writes) 기능을 완벽하게 지원하지 않습니다. 쓰기 작업이 Firestore로 전송될 때 일시적인 네트워크 오류나 서버 측의 일시적인 문제가 발생하면 MongoDB 드라이버는 자동으로 쓰기 재전송을 시도할 수 있습니다. 하지만 Firestore의 에뮬레이션 레이어는 이 두 번째 시도를 올바르게 처리하지 못할 수 있으며, 특히 첫 번째 시도가 이미 처리 중인 경우 더욱 그렇습니다. 이로 인해 드라이버는 쓰기 작업이 실패했다고 생각하는 오류 상태가 발생할 수 있으며, 실제로는 결국 성공할 수도 있습니다.
## `retryWrites=false` "해결책"
연결 문자열에 `retryWrites=false`를 추가하면 MongoDB 드라이버에게 첫 번째 시도 후에 바로 포기하라고 지시합니다. 일시적인 오류가 발생하면 쓰기 작업은 즉시 실패하고 애플리케이션은 오류를 보게 됩니다. 더 이상 혼란스러운 재시도 관련 오류가 발생하지 않기 때문에 이것이 문제를 해결하는 것처럼 *보입니다*. 쓰기 작업은 첫 시도에 성공하거나 깔끔하게 실패합니다.
## 진짜 문제: 데이터 손실
함정은 재시도를 비활성화함으로써 이제 일시적인 실패를 직접 처리해야 한다는 점입니다. 그렇게 하지 않으면, 애플리케이션은 순간적인 네트워크 문제만으로도 중요한 데이터를 저장하지 못할 수 있습니다. 사용자는 데이터가 저장되었다고 생각할 수 있지만, 실제로는 쓰기 작업이 조용히 실패했을 수 있습니다(오류를 처리하지 않는다면 그다지 조용하지 않을 수도 있습니다). 이는 데이터 신뢰성 측면에서 중대한 후퇴입니다.
## 올바른 해결책
중요한 신뢰성 기능을 비활성화하는 대신, 올바른 접근 방식은 Firestore의 MongoDB 호환성을 위해 환경이 올바르게 설정되었는지 확인하는 것입니다.
* **1. 공식적으로 지원되는 드라이버 및 버전 사용:** Google Cloud의 문서를 확인하여 Firestore에서 완전히 테스트되고 지원되는 MongoDB 드라이버 및 버전 목록을 확인하십시오. 지원되지 않는 버전을 사용하면 예측할 수 없는 동작이 발생할 수 있습니다.
* **2. 사용자 지정 재시도 로직 구현:** 문제를 일으키는 드라이버 구성을 꼭 사용해야 하는 경우, 재시도를 전역적으로 비활성화하지 마십시오. 대신, 쓰기 실패와 관련된 특정 오류 코드를 포착하여 애플리케이션 코드에 자체 재시도 로직을 구현하십시오. 이를 통해 재시도 횟수와 백오프 전략(예: 지수 백오프)을 제어할 수 있습니다.
* **3. 문제 보고:** Firestore의 MongoDB 호환성 레이어에서 버그를 발견했다고 생각되면 Google Cloud 지원팀에 보고하십시오. 드라이버 버전, 연결 문자열(자격 증명 제외), 정확한 오류 메시지에 대한 자세한 정보를 제공하십시오. 이는 모든 사용자를 위해 서비스를 개선하는 데 도움이 됩니다.
요약하자면, `retryWrites=false`는 해결책이 아닙니다. 이는 더 깊은 문제를 숨기고 데이터 손실의 위험을 초래하는 임시방편입니다. 항상 지원되는 구성을 사용하고 애플리케이션에 강력한 오류 처리 및 재시도 메커니즘을 구축하는 것을 우선시해야 합니다.
2026년 7월 19일 · 5분 읽기
백엔드를 Rust에서 Go로 이전한 이유, 그리고 빌드 시간이 결정적이었던 까닭
================================================================
저는 Rust를 사랑합니다. 정말입니다. 훌륭한 커뮤니티를 가진 환상적인 언어이며, 대부분의 개인 프로젝트에서 제가 선택하는 언어입니다. 하지만 최근 한 백엔드 프로젝트에서는 Rust에서 Go로 이전하는 어려운 결정을 내렸습니다. 주된 이유는요? 바로 빌드 시간이었습니다.
## Rust 경험: 강력하지만 컴파일이 느림
저희 백엔드 서비스는 Rust로 작성된 작은 모놀리스로 시작했습니다. 초기 개발은 순조로웠습니다. Rust의 타입 시스템과 소유권 모델은 컴파일 시간에 많은 버그를 잡아주어 저희에게 큰 자신감을 주었습니다. `serde`, `tokio`, `actix-web`과 같은 크레이트가 있는 생태계는 함께 작업하기에 즐거웠습니다.
하지만 프로젝트가 커지면서 컴파일 시간도 늘어났습니다. 개발자 머신에서 클린 빌드를 하는 데 몇 분이 걸렸습니다. 증분 빌드는 더 빠르긴 했지만, 점차 느리게 느껴지기 시작했습니다. 이것은 사소한 불편함을 넘어, 개발 속도를 실질적으로 저해하고 있었습니다.
## CI/CD 병목 현상
하지만 진짜 고통스러운 지점은 CI/CD 파이프라인에 있었습니다. 모든 풀 리퀘스트는 전체 릴리스 빌드와 포괄적인 테스트 스위트 실행을 포함한 일련의 검사를 트리거했습니다. 다음은 CI에서의 빌드 시간을 간략하게 살펴본 것입니다:
| 단계 | Rust 빌드 시간 |
| --- | --- |
| 의존성 캐싱 | ~1분 |
| 디버그 빌드 (테스트용) | ~5-7분 |
| 릴리스 빌드 (배포용) | ~10-15분 |
| **총 파이프라인 시간** | **~20-25분** |
간단한 변경에 대한 25분의 피드백 루프는 받아들일 수 없었습니다. 저희는 `sccache`, Docker 레이어 캐싱, 빌드와 테스트 작업 분리 등 다양한 최적화를 시도했습니다. 이러한 방법들이 도움이 되긴 했지만, 몇 분을 단축하는 데 그쳤습니다. 핵심 문제는 그대로였습니다. Rust 컴파일러는 많은 작업을 수행하고(안전성 측면에서는 훌륭하지만!), 시간이 걸린다는 점입니다.
## Go의 등장: 실용적인 선택
저희는 실험을 해보기로 결정했습니다. 서비스의 핵심적이고 독립적인 부분을 가져와 Go로 다시 작성했습니다. 그 경험은... 달랐습니다.
* **단순성:** Go는 훨씬 더 단순한 언어입니다. Rust의 강력한 추상화(트레이트나 고급 열거형 등)는 잃었지만, 코드는 놀라울 정도로 작성하고 읽기 쉬웠습니다.
* **동시성:** Go의 고루틴과 채널은 동시성 처리를 쉽게 만들어 주어, 저희 백엔드 요구사항에 아주 적합했습니다.
* **컴파일러:** 이것이 바로 게임 체인저였습니다. Go 컴파일러는 *빠릅니다*. 눈부시게 빠릅니다.
서비스의 상당 부분을 다시 작성한 후, 동일한 CI 파이프라인을 실행했습니다. 결과는 놀라웠습니다.
| 단계 | Go 빌드 시간 |
| --- | --- |
| 의존성 캐싱 | ~30초 |
| 빌드 및 테스트 | ~1-2분 |
| **총 파이프라인 시간** | **~2-3분** |
저희는 25분 걸리던 파이프라인을 3분짜리로 단축했습니다. 이것은 단지 점진적인 개선이 아니라, 개발 워크플로우에 대한 근본적인 변화였습니다. 개발자들은 거의 즉시 피드백을 받을 수 있게 되었습니다. 코드 병합이 더 빨라졌습니다. 핫픽스 배포가 사소한 일이 되었습니다.
## 트레이드오프와 결론
이전 과정이 순탄했냐고요? 아닙니다. 저희는 Rust의 컴파일 타임 보장을 포기했고, 이는 테스트와 코드 리뷰에 더 부지런해야 한다는 것을 의미했습니다. 제네릭의 부재(초기 재작성 시점)는 일부 코드를 더 장황하게 만들었습니다. Go의 오류 처리는 단순하지만, Rust의 `Result`와 `?` 연산자보다 덜 견고하게 느껴집니다.
하지만 개발 속도와 빠른 피드백 루프가 중요한 웹 백엔드라는 저희의 특정 사용 사례에서는 그 트레이드오프가 가치가 있었습니다. 빌드 시간의 극적인 감소는 개발자 생산성과 행복도 증가로 바로 이어졌습니다.
저는 여전히 Rust를 사랑하며, 성능과 안전성 보장이 가장 중요하고 컴파일 시간이 덜 중요한 프로젝트에서는 계속해서 사용할 것입니다. 하지만 이 경험은 때로는 작업에 가장 '최고의' 도구는 방해하지 않고 결과물을 내놓게 해주는 도구라는 것을 강력하게 상기시켜 주었습니다. 저희에게 그 도구는 Go였습니다.
2026년 7월 18일 · 7분 읽기