Showing Posts From
배포
- 04 Dec, 2025
배포 전 마지막 체크리스트: 놓친 게 뭐지?
오후 4시의 슬랙 메시지 "오늘 배포 맞죠?" 기획자 메시지다. 알고 있다. 3일 전부터 알고 있었다. 손이 떨린다. 커피를 너무 마신 건지, 아니면 그냥 배포 전이라서 그런 건지 모르겠다.체크리스트를 연다. 노션에 만들어둔 거다. 지난번 배포 때 새벽 2시에 롤백하면서 "다음엔 이런 일 없게 하자"고 다짐하며 만든 것. 근데 이게 문제다. 체크리스트를 볼 때마다 항목이 늘어난다. 처음엔 10개였다. 지금은 37개다. 체크리스트라는 이름의 불안 데이터베이스 백업. 체크. 스테이징 환경 테스트. 체크. API 문서 업데이트. 체크. 슬랙 배포 공지. 체크. 여기까진 기본이다. 문제는 여기서부터다. "혹시 인덱스 추가한 거 확인했나?" 확인했다. 근데 다시 확인한다. EXPLAIN 결과를 또 본다. 쿼리 플랜이 이상하진 않다. 근데 프로덕션 데이터로도 테스트했나? 스테이징이랑 데이터 양이 다른데? 다시 확인한다."Redis 캐시 워밍업은?" 아. 맞다. 배포하고 첫 요청 때 DB 커넥션 폭증하는 거. 지난번에 그거 때문에 30초간 타임아웃 났었지. 체크리스트에 추가한다. 38개. "로그 레벨 확인했나?" 디버그 모드로 돌린 적 있었나? 없다. 근데 확인한다. 혹시 모르니까. "헬스체크 엔드포인트는?" 당연히 있다. 근데 실제로 동작하나? 5분 전에 확인했는데 또 확인한다. 이게 무한루프다. 후배의 PR "형, PR 올렸어요." 오후 5시. 배포 3시간 전. 제목을 본다. "hotfix: 결제 금액 소수점 처리". 소수점? 결제에? 심장이 뛴다. "이거 급한 거야?" "아뇨, 발견해서 고쳤어요." 발견? 언제? 어디서? 코드를 본다. Math.round() 하나 추가한 거다. 테스트 코드도 있다. 근데 불안하다. 결제는 민감한 부분이다. 이거 지금 넣어야 하나? 다음 배포로 미뤄야 하나? "스테이징에서 테스트했어?" "네." "얼마로?" "1000원이요." 1000원. 실제 결제는 십만원대도 있는데."십만원으로도 해봐." "네." 기다린다. 10분. 답이 온다. "됐어요." 됐다고? 뭐가? 테스트가 통과했다는 건가, 아니면 문제없다는 건가? 전화한다. 직접 확인하는 게 빠르다. 통화 15분. 괜찮은 것 같다. 근데 '것 같다'가 문제다. "일단 빼자. 다음 배포 때 넣어." "네..." 후배 목소리가 풀이 죽는다. 미안하다. 근데 배포는 조심해야 한다. 체크리스트에 항목 추가. "긴급 수정사항 재확인". 39개. 오후 6시의 회의 "배포 전 최종 확인 회의 시작하겠습니다." 누가 시작하자고 했나. 기억이 안 난다. 근데 매번 한다. 참석자: 나, 후배 둘, 기획자, QA, DevOps 형. 기획자가 묻는다. "이번 배포 범위가 어떻게 되죠?" 말한다. 회원가입 플로우 개선, 상품 상세페이지 성능 개선, 관리자 대시보드 차트 추가. "회원가입 플로우에서 소셜로그인도 테스트했죠?" 했다. 구글, 카카오, 네이버. 전부 했다. "탈퇴한 회원이 재가입하는 케이스는?" 아. 안 했다. "확인해볼게요." 회의 중간에 노트북 연다. 스테이징 접속. 테스트 계정 하나 탈퇴시킨다. 재가입 시도. 에러. "unique 제약조건 위반". 이메일 컬럼에 unique 인덱스가 걸려있는데, soft delete 방식이라 탈퇴해도 데이터가 남는다. "이거 고쳐야겠는데요." 기획자가 묻는다. "시간 얼마나 걸려요?" 모른다. 30분? 1시간? DB 마이그레이션까지 하면 2시간? "배포 연기할까요?" 연기. 그 단어만 들어도 한숨 나온다. 기획자도, 대표님도, 전부 오늘 배포 알고 있다. "아니요. 고치겠습니다." 회의 끝. 오후 6시 40분. 긴급 수정 손이 빠르게 움직인다. 이럴 때는 생각하지 말고 코딩한다. deleted_at 컬럼 추가. unique 인덱스를 partial index로 변경. WHERE deleted_at IS NULL. MySQL은 partial index를 지원하지 않는다. 망했다. 다른 방법. unique 제약조건 제거하고, 애플리케이션 레벨에서 체크. 안 된다. 동시성 이슈. 같은 이메일로 동시 가입하면? 세 번째 방법. email + deleted_at 복합 unique. 이것도 안 된다. deleted_at이 NULL이면 중복 허용된다. 네 번째 방법. 탈퇴할 때 이메일을 수정한다. "deleted_{timestamp}_{original_email}". 이건 된다. 근데 더럽다. 정말 더럽다. 시간이 없다. 일단 이걸로 간다. 코드 수정. 10분. 테스트 코드 작성. 15분. 스테이징 배포. 5분. 테스트. 10분. 동작한다. 더럽지만 동작한다. PR 올린다. 제목: "hotfix: 탈퇴 회원 재가입 이슈 수정". DevOps 형한테 메시지. "긴급 수정 하나 더 들어갑니다." "알았어. 근데 배포 시간 좀 늦어질 것 같은데." "얼마나요?" "30분?" 오후 7시 30분. 원래 배포 시간. 지금 8시로 밀린다. 체크리스트를 다시 본다. 39개 항목. 하나하나 다시 확인한다. 배포 30분 전 체크리스트 완료. 전부 체크했다. 근데 불안하다. 뭔가 놓친 것 같다. 모니터링 대시보드를 연다. 그라파나. 지난 일주일 트래픽 패턴을 본다. 평일 저녁 8시. 동접 2000명. 초당 요청 150개. DB 커넥션 풀 사용률 60%. 괜찮다. 여유롭다. 근데 혹시 모른다. 배포하면 캐시가 날아간다. 첫 요청들이 몰린다. DB 커넥션이 순간적으로 튄다. "혹시 캐시 워밍업 스크립트 준비했나?" 준비 안 했다. 체크리스트엔 있는데 실제론 안 했다. 지금 만들까? 30분 안에? 불가능하다. "캐시 없이 버틸 수 있나?" 계산한다. 캐시 미스율 100%라고 가정. 모든 요청이 DB로. 초당 150개. DB는 초당 500개까지 처리 가능하다고 했다. AWS 스펙 문서에서 봤다. 근데 그건 단순 SELECT 기준이다. JOIN 많은 쿼리는? 집계 쿼리는? 모른다. "일단 가자." 결정했다. 캐시 워밍업 없이 간다. 문제 생기면 그때 대응한다. 체크리스트에 항목 하나 더 추가. "배포 후 캐시 모니터링". 40개. 배포 시작 오후 8시. DevOps 형이 메시지 올린다. "배포 시작합니다." 슬랙 배포 채널에 공지 올라간다. "@channel 배포 시작. 약 15분 소요 예정." 그라파나를 본다. 초당 요청 수. DB 커넥션. CPU 사용률. 메모리. 디스크 IO. 전부 정상이다. 아직은. Jenkins 로그가 흐른다. [INFO] Building application... [INFO] Running tests... [INFO] All tests passed. [INFO] Creating Docker image... [INFO] Pushing to registry...손에 땀이 난다. 마우스가 미끄럽다. 후배가 옆에서 묻는다. "형, 괜찮을까요?" "모르지." 솔직한 대답이다. 배포는 매번 도박이다. 아무리 체크해도 변수는 있다. [INFO] Deploying to production... [INFO] Updating service... [INFO] Waiting for health check...헬스체크. 30초 걸린다. 가장 긴 30초다. [INFO] Health check passed. [INFO] Deployment completed.완료. 그라파나를 본다. 트래픽이 들어온다. 초당 요청 수가 튄다. 150에서 200으로. DB 커넥션. 60%에서 85%로. 응답 시간. 평균 200ms에서 800ms로. 에러율. 0%에서 0.3%로. 심장이 뛴다. 배포 후 10분 슬랙에 메시지가 터진다. "로그인이 느려요." "상품 페이지 로딩 중..." "관리자 페이지 접속 안 돼요." 패닉이다. 응답 시간을 본다. 계속 오른다. 800ms, 1200ms, 1500ms. 에러율. 0.3%, 0.5%, 1.2%. 뭐가 문제지? 로그를 연다. Elasticsearch. 에러 로그를 검색한다. java.sql.SQLException: Timeout: Pool empty. Unable to fetch a connection in 30 seconds.커넥션 풀. 비었다. 예상했던 거다. 캐시가 없어서 DB 쿼리가 몰렸다. "커넥션 풀 늘려야겠어요." DevOps 형한테 메시지. "DB 커넥션 풀 100에서 200으로 늘려주세요." "지금요?" "네, 지금요." 설정 변경. 재시작. 5분. 다시 본다. 응답 시간 떨어진다. 1500ms에서 1000ms로. 800ms. 600ms. 에러율. 1.2%에서 0.8%. 0.5%. 0.2%. 안정화된다. 슬랙 메시지. "다시 괜찮아진 것 같아요." 한숨 쉰다. 등에 땀이 찬다. 배포 후 1시간 오후 9시. 모니터링 지표 전부 정상이다. 트래픽: 초당 요청 160개 응답 시간: 평균 250ms 에러율: 0.05% DB 커넥션: 70% 배포 전보다 약간 느리다. 캐시가 아직 다 안 차서 그렇다. 체크리스트를 다시 본다. 40개 항목. 결국 놓쳤다. 커넥션 풀 증설. 체크리스트엔 없었다. 추가한다. 41개. "다들 수고했어요." 팀원들한테 메시지 보낸다. 답장 온다. "수고하셨습니다." "고생했어요 형." "역시 형이에요." 역시가 아니다. 운이 좋았을 뿐이다. 퇴근 오후 10시. 사무실을 나선다. 혼자다. 다른 팀원들은 9시에 나갔다. "먼저 갈게요." "네, 조심히 가세요." 혼자 남아서 1시간 더 모니터링했다. 안정화 확인. 엘리베이터를 탄다. 거울에 비친 내 얼굴. 피곤하다. 폰을 본다. 아내 메시지. "저녁 먹었어?" "아직." "편의점에 뭐 있어. 데워먹어." 고맙다. 집에 도착한다. 10시 40분. 편의점 도시락을 데운다. 전자레인지 돌아가는 소리. 3분. 소파에 앉는다. 도시락을 먹는다. 맛은 모르겠다. 넷플릭스를 켠다. 뭔가 틀어놓는다. 뭔지 모른다. 폰을 본다. 슬랙. 알림 없다. 다행이다. 그라파나 앱을 연다. 습관이다. 지표 확인. 전부 정상. 앱을 닫는다. 폰을 내려놓는다. 눈을 감는다. 그래도 다음날 출근한다. 팀원이 말한다. "어제 배포 잘됐죠?" "응. 좀 삐걱거렸는데." "그래도 잘 넘어갔잖아요." 맞다. 잘 넘어갔다. 기획자가 메시지 보낸다. "어제 배포 건 잘 적용됐어요. 고객 반응 좋네요." 고객 반응. 좋다고 한다. CS 채널을 확인한다. 어제 배포 관련 문의. 없다. 회원가입 전환율 대시보드. 전일 대비 12% 상승. 상품 페이지 체류시간. 평균 23초 증가. 숫자로 보니 실감난다. 체크리스트를 연다. 41개 항목. 다음 배포 때도 이걸 볼 것이다. 또 항목이 늘어날 것이다. 또 불안할 것이다. 그래도 배포는 한다. 이게 개발자다.체크리스트는 늘어나고, 불안은 줄지 않지만, 배포는 계속된다. 그게 우리 일이니까.