Showing Posts From
개발
- 03 Dec, 2025
Redis TTL 설정 실수로 생긴 버그의 추억
Redis TTL 설정 실수로 생긴 버그의 추억 시작은 평범했다 월요일 오전 10시. 커피 두 잔 마시고 PR 하나 머지했다. 기분 좋았다. 점심 먹기 전에 간단한 캐시 로직 하나 추가하는 작업. "30분이면 되겠네." Redis에 상품 정보 캐싱하는 거였다. DB 부하 줄이려고. 간단하다. SET 하고 EXPIRE 설정하면 끝. TTL은 1시간으로. 자주 바뀌는 데이터 아니니까. 코드 짰다. 로컬에서 돌려봤다. 잘 된다. PR 올렸다. 팀장이 Approve. 배포했다. 11시 반. 점심 먹으러 갔다. 김치찌개 먹으면서 슬랙 확인했다. 조용하다. 좋다.3일이 지났다 수요일 오후 4시. 평화로웠다. 다음 스프린트 계획 세우는 중이었다. 슬랙에 빨간 점. CS팀에서 멘션. "개발님, 상품 가격이 업데이트가 안 되는 것 같은데요?" 심장이 쿵. 했다. 아니 근데 가격? 우리 가격 업데이트 로직 건드린 적 없는데. "언제부터요?" "월요일부터 고객 신고가 몇 건 들어왔어요. 근데 저희가 확인해보니까 관리자 페이지에선 가격이 바뀌는데, 실제 상품 페이지에선 안 바뀌더라고요." 월요일. 월요일. 내가 월요일에 뭐 했지. Redis. 캐시. 아. 씨발.코드를 열었다 손이 떨렸다. 파일 열었다. 내가 짠 코드. redisTemplate.opsForValue().set(key, productInfo); redisTemplate.expire(key, 3600, TimeUnit.SECONDS);3600초. 1시간. 맞는데? 아니 잠깐. 상품 정보 업데이트하는 로직 봤다. 기존 코드. DB만 업데이트한다. 캐시 무효화 로직이 없다. 당연하다. 원래 캐시가 없었으니까. 내가 캐시를 추가했으면. 업데이트 로직에도. 캐시 삭제를 추가했어야 했다. 근데 난 추가 안 했다. 상품 정보는 한 번 캐싱되면. 1시간 동안 죽어도 안 바뀐다. 아니 1시간도 아니다. 누군가 그 상품을 조회할 때마다. 캐시 히트. TTL 다시 1시간. 월요일 오전에 캐싱된 데이터가. 수요일 오후까지 살아있었다. 72시간. 고객들은 3일 동안. 구 가격을 봤다.패닉 팀 단톡에 쳤다. "긴급. 상품 캐시 버그. 지금 핫픽스 준비합니다." 팀장이 전화했다. "뭔데?" 설명했다. 3분 동안. 팀장이 한숨 쉬는 소리 들렸다. "일단 캐시 전부 날려. 그리고 핫픽스 PR 올려." Redis CLI 켰다. FLUSHDB. 엔터. 캐시 전부 날아갔다. DB 쿼리가 폭증할 거다. 근데 어쩔 수 없다. 모니터링 대시보드 봤다. DB CPU 사용률 급등. 60%. 70%. 80%. 멈췄다. 85%에서. 다행이다. 죽진 않았다. 코드 수정했다. 상품 업데이트 로직에 캐시 삭제 추가. 아니 더 정확하게는 캐시 키 패턴으로 관련된 거 전부 삭제. 상품 상세, 리스트, 카테고리별. 다. 30분 걸렸다. PR 올렸다. 팀장 리뷰. 1분 만에 Approve. "빨리 배포해." 배포했다. 6시 10분. 사후 처리 배포 끝났다. 테스트했다. 가격 업데이트. 캐시 삭제. 잘 된다. CS팀에 보고했다. "수정 완료했습니다. 현재는 정상 작동합니다." "고객들한테 어떻게 안내드리죠?" 어떻게. 어떻게 하긴. "죄송합니다. 시스템 오류로 가격 정보가 지연 반영되었습니다. 현재는 정상화되었습니다." 이렇게 쓰는 거지. 근데 문제가 하나 더 있었다. 구 가격으로 주문한 사람들. 있을까? 봤다. 있었다. 12건. 신규 가격보다 저렴한 구 가격으로. 회계팀 전화 왔다. "이거 손실 처리 어떻게 해요?" "...제가 결정할 사항은 아닌 것 같은데요." "개발팀장님이랑 통화 연결해드릴게요." 통화 30분. 결론. 12건은 그냥 구 가격으로 인정. 손실 처리. 약 24만원. 내 잘못으로 회사가 24만원 날렸다. 퇴근은 9시였다. 회고 미팅 다음날 오전. 팀 회고. 나 혼자 하는 반성문 낭독회. "캐시 추가할 때 쓰기 경로를 체크 안 했습니다." "TTL 설정만 하면 된다고 생각했습니다." "캐시 무효화 전략을 설계 안 했습니다." 팀장이 말했다. "이번 건으로 배운 게 뭐야?" "캐시는 읽기만 빠르게 하는 게 아니라, 쓰기도 고려해야 한다는 거요." "TTL은 만능이 아니다." "캐시 도입하려면 설계 단계부터 쓰기 경로 파악해야 한다." 맞는 말만 했다. 근데 의미 없다. 이미 사고 쳤는데. 후배가 물었다. "그럼 앞으로 캐시 추가할 땐 어떻게 해야 해요?" "체크리스트 만들어야겠지. 읽기 경로 분석. 쓰기 경로 분석. TTL 전략. 무효화 전략. 모니터링. 롤백 시나리오." 팀장이 끄덕였다. "그거 문서로 만들어. 다음 주까지." 일이 늘었다. 그 후 체크리스트 만들었다. 캐시 도입 가이드. 5페이지짜리. 예시 코드 포함. Confluence에 올렸다. 후배들이 읽었다. "오 좋은데요?" 댓글 달았다. 근데 솔직히. 이거 안 만들어도. 다들 내 실수 알고 있으니까. 안 그러려고 할 거다. 한 달 지났다. 다른 팀에서 나한테 물었다. "Redis 캐시 도입하려는데, 조언 좀 해주실 수 있어요?" "제가요? 제가 조언을?" "네, 개발님이 관련 문서 작성하셨다고 들었어요." 아. 내 삽질이 레퍼런스가 됐다. 이상한 기분이었다. 좋은 건지 나쁜 건지. 남은 것들 지금도 상품 업데이트 로직 볼 때마다. 캐시 삭제 코드 확인한다. 있나 없나. 두 번 본다. Redis 모니터링 대시보드. 즐겨찾기에 있다. 하루에 세 번 연다. 캐시 히트율. TTL 분포. Key 개수. 다 본다. 후배가 캐시 관련 PR 올리면. 꼼꼼히 본다. "쓰기 경로는 확인했어?" 댓글 단다. 매번. 팀장이 말했다. "네가 너무 신경 쓰는 거 아니야?" "한 번 당했으니까요." "실수는 누구나 해. 중요한 건 반복 안 하는 거고." 맞는 말이다. 근데 나한테는. 그 3일이. 72시간이. 아직도 생생하다. 고객들이 틀린 가격 보고 있을 때. 나는 커피 마시면서 다음 작업 계획 세우고 있었다. 그게 제일 씁쓸하다. 결국 버그는 고쳤다. 문서는 만들었다. 프로세스는 개선했다. 근데 그보다. 배운 건. "간단해 보이는 것도 간단하지 않다." 캐시 하나 추가하는 거. 30분 작업. 그렇게 생각했다. 근데 시스템은 연결돼 있다. 읽기만 빠르게 하면 끝이 아니다. 쓰기도 있다. 업데이트도 있다. 삭제도 있다. TTL 설정했다고 끝이 아니다. 데이터는 변한다. 변하면 캐시도 바뀌어야 한다. 안 바뀌면 거짓말을 하는 거다. 고객한테. 7년 차다. 이런 실수는 안 해야 한다. 근데 했다. 그래도. 다음엔 안 한다. 이번엔 확실히 배웠으니까. Redis 띄울 때마다. 생각한다. "TTL은 보험이지 전략이 아니다." 쓰기 경로부터 확인한다. 무효화 로직부터 짠다. 그 다음에 캐시를 추가한다. 좀 느려졌다. 예전보다. 신중해졌다. 그게 나쁘진 않다.실수는 또 할 거다. 근데 같은 실수는 아닐 거다. 그걸로 됐다.
- 03 Dec, 2025
MySQL 슬로우 쿼리 로그와의 밤샘 전쟁
MySQL 슬로우 쿼리 로그와의 밤샘 전쟁 새벽 3시의 슬랙 알림 진동이 울렸다. 아내가 뒤척였다. 폰을 집어들었다. 슬랙이다. 프로덕션 알림 채널. [CRITICAL] Slow Query Detected Response Time: 8.3s Endpoint: /api/users/activity8초. 망했다. 잠옷 바람으로 거실로 나왔다. 노트북을 켰다. 부팅되는 동안 물을 마셨다. 손이 떨렸다. 커피 때문인지 떨림 때문인지 모르겠다. VPN 연결. 프로덕션 DB 접속. 슬로우 쿼리 로그를 열었다. SELECT u.*, ua.last_active, ua.page_views FROM users u LEFT JOIN user_activities ua ON u.id = ua.user_id WHERE u.created_at >= '2024-01-01' AND u.status = 'active' ORDER BY ua.last_active DESC LIMIT 100;평범해 보였다. 그게 더 문제다.EXPLAIN은 거짓말을 하지 않는다 EXPLAIN을 돌렸다. +----+-------------+-------+------+---------------+------+---------+------+--------+----------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+--------+----------+ | 1 | SIMPLE | u | ALL | NULL | NULL | NULL | NULL | 450000 | Using... | | 1 | SIMPLE | ua | ALL | NULL | NULL | NULL | NULL | 890000 | Using... | +----+-------------+-------+------+---------------+------+---------+------+--------+----------+type이 ALL. 둘 다. 45만 로우와 89만 로우를 풀스캔. 카르테시안 곱. 400억 건을 뒤진다는 소리다. "씨발." 소리가 나왔다. 작게. user_activities 테이블을 확인했다. 인덱스가 하나도 없다. user_id에도. last_active에도. 누가 만든 건지 git blame을 돌렸다. 6개월 전. 커밋 메시지 "Add user activity tracking". 커밋한 사람은 나. "아 씨." 이번엔 크게 나왔다. 6개월 전엔 데이터가 얼마 없었다. 몇천 건. 그때는 문제없었다. 근데 지금은 89만 건. 매일 3천 건씩 쌓인다. 프로덕션에서 터지기 전까진 아무도 몰랐다. 스테이징에 데이터가 없으니까.인덱스를 추가할 수 없는 이유들 인덱스 추가하면 된다. 간단하다. 근데 프로덕션에서 ALTER TABLE은 간단하지 않다. 89만 건 테이블에 인덱스 추가하면 락이 걸린다. InnoDB라도 시간이 걸린다. 10분? 20분? 그 동안 서비스는 멈춘다. 새벽 3시에 배포는 못 한다. 승인이 필요하다. CTO 결재. 근데 CTO는 자고 있다. 당연히. 옵션을 생각했다.아침까지 기다린다 - 유저들은 8초 쿼리를 계속 겪는다 지금 긴급 배포 - CTO 깨워서 승인받기 임시방편으로 쿼리 수정 - 근본 해결 아님2번은 무섭다. 3번은 찝찝하다. 1번은 책임이 무섭다. 슬랙을 켰다. CTO에게 DM. "프로덕션 슬로우 쿼리 발생했습니다. 인덱스 추가 필요합니다. 긴급 배포 승인 부탁드립니다." 읽음 표시가 안 뜬다. 10분을 기다렸다. 20분을 기다렸다. 답이 없다.임시방편이라는 이름의 선택 기다릴 수 없었다. 쿼리를 수정하기로 했다. 인덱스 없이 빠르게 만드는 방법. -- 기존 WHERE u.created_at >= '2024-01-01'-- 수정 WHERE u.created_at >= DATE_SUB(NOW(), INTERVAL 90 DAY)범위를 줄였다. 1년치가 아니라 90일치만. -- ORDER BY 제거 -- LIMIT 100만 남김정렬을 포기했다. 어차피 프론트에서 다시 정렬한다. 코드를 수정했다. 커밋. 푸시. 젠킨스에서 빌드. 배포는 못 한다. 승인이 없으니까. 근데 DB 쿼리는 바로 테스트할 수 있다. 프로덕션 DB에 직접. EXPLAIN SELECT u.*, ua.last_active FROM users u LEFT JOIN user_activities ua ON u.id = ua.user_id WHERE u.created_at >= DATE_SUB(NOW(), INTERVAL 90 DAY) AND u.status = 'active' LIMIT 100;0.3초. 됐다. 근데 이건 임시방편이다. 근본 해결이 아니다. 데이터는 계속 쌓인다. 90일 범위도 언젠가 느려진다. 새벽 4시. CTO한테 전화했다. "여보세요?" 목소리가 잠에 절어있다. "죄송합니다. 프로덕션 이슈입니다." 상황을 설명했다. 쿼리 수정으로 임시 해결했다고. 인덱스 추가가 필요하다고. "아침에 회의하자. 6시까지는 자야겠다." 끊겼다. 나는 못 잔다. 아침 9시의 포스트모텀 출근했다. 4시간 잤다. CTO실에 들어갔다. 기획팀장도 있다. 왜 기획팀장까지. "상황 설명해봐." 화면을 공유했다. EXPLAIN 결과. 슬로우 쿼리 로그. 응답시간 그래프. "인덱스가 없었어요. user_activities 테이블에." "왜 없었는데?" "제가... 6개월 전에 만들 때 안 넣었습니다." "왜?" "데이터가 적어서 문제 될 줄 몰랐습니다." 침묵. 기획팀장이 물었다. "유저들 피해는?" "어젯밤 3시부터 8시까지. 약 5시간. 해당 API를 쓰는 유저들이 8초씩 기다렸습니다." "몇 명?" "로그 확인 결과 430명 정도." "우리 서비스 쓸 때 8초씩 기다렸다는 거네?" "...네." 또 침묵. CTO가 말했다. "인덱스 추가 언제 할 건데?" "오늘 점심시간에 하겠습니다. 유저 적을 때." "락 걸리는 시간은?" "15분 정도 예상합니다." "공지는?" "11시 50분에 올리겠습니다." "좋아. 근데 다음부터는 테이블 설계할 때 인덱스 미리 생각해." "네." 회의가 끝났다. 자리로 돌아왔다. 후배가 물었다. "형 괜찮아요?" "응." 괜찮을 리 없다. 점심시간의 배포 11시 50분. 공지를 올렸다. "점심시간 동안 DB 작업으로 서비스 일시 중단됩니다. 12:00 ~ 12:20 예정" 슬랙에 난리가 났다. "갑자기요?" "고객사 미팅 있는데" "점심시간이라 괜찮을 거예요" "아 진짜" 무시했다. 할 건 해야 한다. 12시 정각. 프로덕션 DB 접속. ALTER TABLE user_activities ADD INDEX idx_user_id (user_id), ADD INDEX idx_last_active (last_active);엔터를 눌렀다. 쿼리가 실행됐다. 진행률은 안 보인다. 그냥 기다려야 한다. 1분. 2분. 5분. 슬랙 알림이 울렸다. "아직 안 돼요?" "언제 돼요?" 10분. 12분. "Query OK, 890000 rows affected (13 min 47 sec)" 됐다. 서비스를 켰다. API 테스트. 응답시간 0.2초. 공지를 올렸다. "작업 완료. 서비스 정상화" 아무도 반응 안 했다. 다들 점심 먹으러 갔다. 나도 밥 먹으러 갔다. 김치찌개집. 사장님이 물었다. "오늘 안색이 안 좋네?" "어제 못 잤어요." "야근?" "비슷한 거요." 밥을 먹었다. 맛이 없었다. 배는 고픈데 넘어가지가 않았다. 그 후 일주일 인덱스 추가 후 일주일. 모니터링했다. 슬로우 쿼리 0건. 평균 응답시간 0.18초. 완벽하다. 근데 찝찝하다. user_activities 말고 다른 테이블은? 확인했다. orders 테이블 - 인덱스 3개뿐. user_id, created_at, status. payments 테이블 - 인덱스 2개. order_id, created_at. notifications 테이블 - 인덱스 1개. user_id만. 전부 시한폭탄이다. 데이터 쌓이면 터진다. 금요일 오후. 팀 회의에서 말했다. "모든 테이블 인덱스 점검이 필요합니다." 팀장이 물었다. "얼마나 걸려?" "일주일?" "다음주는 스프린트 마감인데." "그럼 그 다음 주에." "그때는 신규 기능 개발 들어가는데." 결국 없던 일이 됐다. 인덱스 점검은 우선순위에서 밀렸다. 또. 터지기 전까진 아무도 신경 안 쓴다. 나도 마찬가지다. 일이 많으니까. 다음번엔 어느 테이블이 터질까. orders? payments? 모르겠다. 터지면 그때 고치지 뭐. 새벽 3시에 또 깰 것 같다는 생각을 했다. 배운 것과 못 배우는 것 이번 일로 배운 거.테이블 만들 때 인덱스부터 생각하기 데이터 적을 때 빠르다고 안심하지 말기 스테이징에도 프로덕션급 데이터 넣어두기 슬로우 쿼리 알림은 진동 말고 무음으로근데 못 배우는 것도 있다. 기술 부채는 계속 쌓인다. 치울 시간은 없다. 우선순위는 항상 밀린다. 터지기 전까진 아무도 신경 안 쓴다. 개발자도. 기획자도. CTO도. 터지면 그제야 급하게 고친다. 새벽에. 혼자서. 그러고 일주일 지나면 다들 잊는다. 나만 기억한다. 다음번엔 어디가 터질지. 이게 7년차 개발자의 일상이다. 화려하지 않다. 그냥 버티는 거다. 터지면 고치고. 또 터지면 또 고치고. 가끔 생각한다. 이직해야 하나. 근데 다른 회사도 똑같을 거다. 기술 부채는 어디에나 있다. 레거시 코드도. 밤샘 배포도. 그냥 이렇게 사는 거다. 커피 한 잔 더 마셨다. 네 번째다. 오늘도 야근이다.다음번 슬로우 쿼리는 언제 터질까. 폰은 무음으로 해뒀다.
- 02 Dec, 2025
주말 치킨 배달이 유일한 낙인 남자
주말의 유일한 낙, 치킨 배달이 오는 그 순간 금요일 퇴근 시간, 나는 벌써 내일 주말 계획을 생각한다. 아니, 계획이라고 하기도 뭐한데, 그냥 루틴이다. 정해진 루틴. 마치 프로덕션 배포 절차처럼 변하지 않는, 그리고 변할 수 없는 루틴 말이다. 회사에서 나오면서 슬랙은 무음으로 돌린다. 월요일 아침까지는 거의 안 봐도 된다는 걸 이제 알았다. 진짜 급한 일이면 전화를 하겠지. 지금까지 그런 일은 없었고, 앞으로도 없을 거다. 아무튼 그렇게 자유로워진다. 금요일 저녁, 차를 몰고 집으로 향하면서 나는 이미 누군가를 기다리고 있다. 아내다. 아니다. 아내가 아니다. 아내는 주말도 일이 많다. UI 디자이너라는 게 그런 건지, 아니면 그냥 업계가 그런 건지 모르겠지만, 토요일 오후쯤이면 출근한다. "기획이 밀렸어"라는 메시지와 함께. 나는 이미 여러 번 봤다. "금요일에 더 할 수 있었잖아"라고 말하고 싶지만, 남편으로서의 기본 소양이 아직 남아있어서 그냥 "알았어, 늦지 말고 와"라고 답한다. 얄미운 건, 내가 그렇게 말하면 정말 늦다는 것이다. 그럼 나는?토요일 아침 10시. 늦잠이 내 주말의 시작이다. 평일에는 7시에 일어난다. 아내도 7시 30분쯤 일어난다. 양치질하고 세안하고 옷 입고 정신없이 집을 나간다. 하지만 토요일은 다르다. 토요일 아침 7시 알람을 설정해놨지만 나는 멍을 때린다. "한 시간 더"라고 생각한다. 그 "한 시간 더"가 얼마나 행복한가. 알람이 또 울린다. 8시 30분. 이제 일어나야 한다는 생각이 들지만, 여전히 침대에서 나갈 이유가 없다. 핸드폰을 집어 들고 회사 단톡방을 본다. 아, 어제 배포한 버그에 대한 메시지가 5개 있다. "나중에 본다"라고 중얼거리고 다시 눈을 감는다. 그렇게 또 한 시간이 지난다. 10시. 정신을 차린다. 아내는 벌써 나갔다. 샤워실에서 나오면서 "점심까지만 와, 저녁은 늦을 것 같아"라고 했다. 고개를 끄덕였던 것 같은데 정확히 기억나지 않는다. 어차피 내 계획에는 영향을 주지 않는다. 일어나서 화장실 가고, 세수하고, 냉장고를 연다. 지난주에 샀던 두유, 계란, 상한 것 같은 김치. 밥? 없다. 빵? 없다. 그럼 뭐 먹지? 아, 치킨이다. 아직 이른 시간이지만 벌써 배달 앱을 든다 스마트폰을 집어 들고 배달 앱을 킨다. 10시 35분. 한 시간쯤 있으면 배달받을 수 있는 시간이다. 나는 이미 이 시간들을 백분율로 계산한다. 조리 시간 20분, 배달 시간 15분, 기다리는 시간의 여유 20분. 11시 25분쯤이면 초인종이 울린다. 화면을 넘긴다. 어제 먹던 그 닭다리 세트? 아니면 오늘은 순살? 두 가지를 섞어주는 혼합 세트도 있다. 나는 항상 같은 것을 본다. 그 가게. 별점 4.8. 리뷰 7천 개. "역시 여기지"라고 중얼거린다. 사이드 시스템 구축할 때처럼 결정을 내린다. 순살+다리 조합, 소스는 간장, 치즈는 추가. 콜라 2리터. 이걸 본다고 해서 뭔가 건강해지는 건 아니지만, 나는 마치 영양소 계산을 하는 것처럼 "단백질... 칼슘..."이라고 중얼거린다. 수량을 선택하고 주소를 확인한다. 집 주소. 당연히 집 주소다. 결제한다. 버튼을 누르는 그 순간, 약간의 신비로운 설렘이 생긴다. "주문이 접수되었습니다. 가게에서 준비 중입니다." 알림 메시지. 이제 시작이다. 나는 화면을 계속 본다. "요리 중 70%"... "요리 중 90%"... 그리고 마침내 "배달원이 픽업했습니다."11시 10분. 이제부터는 기다림의 순간이다. 게임을 할까? 유튜브를 볼까? 아니다. 나는 배달원의 위치를 추적한다. 지도에 빨간 마크가 움직인다. 우리 동네 골목을 누비고 다닌다. "벌써 여기까지 왔어?"라고 중얼거린다. 마치 실시간 로그를 모니터링하는 것처럼. "배달 예상 시간 5분" 알림이 뜬다. 그 5분이 길다. 진짜로. 세상에 가장 긴 5분은 커피를 기다리는 시간이고, 그 다음이 치킨 배달을 기다리는 시간이다. 나는 현관 문 앞으로 간다. 신발을 신는다. 빼낸다. 다시 신는다. 이게 정상적인 행동일까? 모르겠다. 하지만 이건 루틴이다. 그리고 초인종이 울린다. 나만의 극장에 오신 것을 환영합니다 배달원은 손에 따뜻한 포장을 들고 서 있다. "감사합니다"라고 말하고, 그는 "감사합니다"라고 답한다. 문을 닫는다. 현관에서 거실로 온다. 콘크리트 바닥이 아닌, 정말로 따뜻한 집의 공간이 거기 있다. 소파에 앉는다. 박스를 열지 않는다. 아직 천천히 먹고 싶다. 영화를 킨다. 넷플릭스다. 뭘 볼까? 이미 본 시리즈 목록을 스크롤한다. "가디언즈 오브 갤럭시"는 벌써 10번 이상 봤다. "쇼생크 탈출"도 마찬가지다. 결국 나는 다큐멘터리를 켠다. "동물의 세계" 같은 거. 음소거 하고 자막만 켜놓아도 되는 종류의 콘텐츠다. 배경음이 필요할 뿐이다.이제 박스를 연다. 김이 모락모락 나온다. 그 냄새. 프로젝트를 성공적으로 마무리했을 때의 그 쾌감과는 다르지만, 어떤 의미에서는 비슷한 만족감이 있다. 정말이다. 나는 닭다리를 집는다. 한입 물어뜯는다. 바삭하다. 바삭한데 속은 말랑하다. 간장 소스가 입에 퍼진다. 소금기와 단맛의 균형. 이거다. 이게 내가 원하던 거다. 누군가는 나를 보고 뭐라고 할까? "주말에 또 치킨이야?", "항상 같은 거 먹네", "이게 웰빙이냐"... 하지만 지금 이 순간에는 그런 목소리들이 들리지 않는다. 음소거된 다큐멘터리에서 수사자가 얼룩말을 사냥한다. 이건 자연의 법칙이다. 나도 이 법칙을 따르고 있을 뿐이다. 내 먹이 사슬에서, 치킨은 정점이다. 두 시간이 흐른다. 박스는 비워진다. 콜라는 반쯤 마셔진다. 나는 소파에서 움직이지 않는다. 움직일 이유가 없다. 냉장고에서 나온 아이스크림을 집어 들고, 침대까지 걸어가는 거 말고는. 폰을 집는다. 단톡방을 본다. "개발이 뭐 어려운데, 이거 간단하게 할 수 있잖아?"라는 기획자의 메시지가 보인다. 월요일에는 이걸 보고 분노해야 하는데, 지금은... 그냥 "내일이 아니니까 괜찮아"라고 생각한다. 사이드 프로젝트를 한 번 시작해볼까 하는 생각도 든다. 하지만 이건 아까 봤던 생각이고, 앞으로도 끝까지 생각으로만 남을 것 같다. 완벽한 루틴의 또 다른 이름 누군가는 주말을 낭만적으로 보낸다. 산에 오르고, 영화관에 가고, 카페에서 새 책을 읽는다. 하지만 나는? 나는 집에 있다. 소파에 누워있다. 가끔은 이게 정상일까 생각한다. 34살 남자가 주말에 할 일이 치킨 배달뿐일까? 하지만 지금은 그렇게 생각하지 않는다. 평일에는 남편이고, 직장인이고, 테크 리드고, 누군가의 코드 리뷰어다. 점심시간 30분이 내 자유다. 연속으로 15분 화장실 들어가는 게 반항이다. 회의 중에 유튜브를 보면서 "아 그거요"라고 답하는 게 내 자존감 유지 방법이다. 하지만 토요일은? 토요일은 내 것이다. 아내가 없고, 업무 메시지도 없고, 기획자의 황당한 요청도 없다. 그냥... 나. 그리고 따뜻한 치킨. 그리고 누군가의 방해받지 않는 자유로움. 이게 뭐 대단한 걸까? 다른 사람들 보기에는 그냥 평범한 토요일일지도 모른다. 하지만 나한테는? 이건 지켜내야 할 가장 소중한 루틴이다. 월요일이 오면 다시 시작된다. 시스템은 작동하고, 코드는 배포되고, 회의는 계속되고, 기획자는 또 "이거 간단하죠?"라고 묻는다. 하지만 그 때도 상관없다. 왜냐하면 나는 이미 알기 때문이다. 다음 토요일이 오면, 또 이 루틴이 반복된다는 것을. 10시 늦잠, 11시 25분 초인종, 2시간 소파. 변하지 않는 프로덕션 절차처럼. 그리고 이게 나를 일요일 밤 일찍 자는 죄책감도, 월요일 아침의 기진맥진함도 잠시 잊게 해준다. 아내는 10시 반쯤 된다고 했다. 나는 "알았어"라고 답한다. 아직 치킨 냄새가 살짝 남아있다. 냉장고에 콜라 반병이 남아있다. 내일도 이 자유로움이 계속되길 바란다.주말의 진정한 사치는 비싼 것이 아니라, 누구의 방해도 받지 않는 그 한두 시간이 아닐까.
- 02 Dec, 2025
퇴근 후 넷플릭스 탭: 왜 항상 같은 것만 보나
퇴근 후 넷플릭스 탭: 왜 항상 같은 것만 보나 그 루틴이 시작된 지 언제쯤일까 저는 매일 퇴근한다. 6시에 정각 같은 건 아니지만, 대충 그즈음 노트북을 덮고 슬랙을 뮤트 한다. 아니, 주말에도 뮤트 풀 일 없도록 아예 알림을 꺼두고 있다는 게 맞다. 배포일은 예외지만, 그래도 최대한 빨리 끝내고 집에 가려고 한다. 집에 가면 뭘 할까? 처음엔 진지했다. 요즘 유명하다는 드라마 본다고 했고, 시즌 1부터 차근차근 봐야 한다고 생각했다. 동료들이 얘기하는 명작들을 소화해야 한다고 마음먹었다. 《오징어 게임》, 《달콤한 집》, 《종이의 집》... 리스트는 길었고 결심은 강했다. 그런데 지금? 지금은 그냥 넷플릭스 앱을 열고, 이미 시작한 드라마를 찾아 재생 버튼을 누른다. 같은 거다. 항상 같은 거다.폰 스크롤의 늪으로 빠져가며 넷플릭스 화면은 켜져 있다. 오프닝 영상이 흘러나온다. 그런데 손에 폰이 있다. 슬랙을 확인한다. 아무도 나한테 메시지를 보내지 않았을 텐데, 왜 자꾸 확인할까. 직업병이다. 팀장이 갑자기 배포를 요청할 수도 있고, 인턴이 PR 리뷰를 재촉할 수도 있으니까. 퇴근했는데도 머리에서는 계속 일이 돈다. 인스타그램을 본다. 피드에서 봤던 사진들이 또 보인다. 릴스 가면 한 곡이 끝나고 다음 곡이 나온다. 화면을 아래로 밀어내린다. 또 밀어낸다. 계단을 내려가는 것처럼 계속 내려간다. 디바운싱(debouncing)이 없는 무한 스크롤이다. 이렇게 10분이 가고, 30분이 간다. 넷플릭스는 여전히 재생 중이다. 나는 화면을 안 본 지 30분이 됐다. 스트리밍 서비스에서 배운 진짜 비즈니스 모델이 뭔지 아나? 드라마를 보여주는 게 아니라, 폰을 손에서 떨어지게 못 하는 거다. 한 번 시작하면 멈출 수 없도록 설계된 시스템. 내가 매일 밤 그 먹이사슬 맨 아래에 있다. 그리고 놀랍게도, 이게 편하다. 뭔가 새로운 걸 시작할 생각을 할 필요가 없다. 의사결정 피로를 덜 수 있다. "오늘 뭘 볼까?" 같은 질문은 이미 과거형이다. 이미 시작한 거 계속 보면 된다. 같은 드라마만 계속 재생되는 이유 여기서 재미있는 패턴을 발견했다. 내가 "새로운 걸 봐야지"라고 다짐한 적이 몇 번인가. 마음먹고 드라마 목록을 돌아다닌 적도 있다. 흥미로워 보이는 타이틀을 클릭했다. 그리고... 포기했다. 왜냐하면 피로하기 때문이다. 새로운 작품은 새로운 스토리, 새로운 캐릭터, 새로운 감정을 요구한다. 퇴근한 뇌는 거기까진 못 간다. 우리 뇌는 최소 저항 경로를 선택하도록 진화했고, 이미 알고 있는 것을 반복하는 게 가장 편하다. 내가 이미 시작한 드라마는 다르다. 1화는 이미 봤으니까 내용을 조금 안다. 2화도 본 지 얼마 안 됐으니까 줄거리가 흐릿하지 않다. 다시 보는 순간, 기억이 돌아온다. "아, 이 장면 있었지." 이 정도의 자극이면 충분하다. 나는 사실 드라마를 보고 있지 않다. 소음을 틀어놓고 있는 거다. 거실에 정적만 있으면 불안하니까. 폰을 들지 않을까봐서. 뭔가 하고 있다는 착각을 하려고.슬랙은 여전히 울리지 않지만 퇴근한 지 1시간이 됐다. 넷플릭스는 아직도 재생 중이다. 드라마 에피소드가 끝나고 자동 재생으로 다음 편이 시작된다. 나는 여전히 폰을 보고 있다. 아내가 들어올 시간이 되면 좀 정신을 차린다. "오늘 하루 어땠어?"라고 물으면 "음, 뭐 별로네. 너는?"이라고 대답한다. 이미 외출했던 옷은 벗고 집에서만 입는 편한 옷으로 갈아입었다. 거실 조명도 꺼뒀다. TV 화면의 빛이 조명 역할을 한다. 저녁을 먹는다. 아내가 집에 있으면 뭔가 함께하는 시간이 생긴다. 그 사이에 드라마는 계속 재생된다. 밥을 다 먹고 나면 다시 소파에 누운다. 손 닿는 곳에 폰이 있다. 아내가 옆에 있어도 폰을 본다. 이미 익숙해졌다. 서로 같은 공간에 있으면서 다른 화면을 보는 일. 이게 이 세대의 부부 문화인가 싶기도 한다. 자정이 다 돼 가면 눈이 무거워진다. 그래도 한 편만 더 본다고 다짐한다. 한 편이 끝나면 또 한 편만 더. 결국 아내가 먼저 자러 간다. "너 먼저 자. 나도 곧 간다."라고 한다. 근데 안 간다. 폰 손에서 떨어지지 않는다. 어느 순간 폰이 떨어진다. 잠들었다. 넷플릭스는 아직 재생 중이다. 배터리 25%. 자동 잠금까지 3분 남았다. 화면은 계속 빛난다. 읽지 못한 슬랙 메시지들 휴일 아침, 눈을 뜬다. 폰을 집어 든다. 첫 번째로 하는 일은 슬랙을 확인하는 것이다. 자동으로 튼다. 손가락이 알아서 한다. "으... 뭐가 이렇게 많아?" 팀 채널에 메시지 3개, 개인 채널에 멘션 2개, 시스템 알림 7개. 대부분은 별거 없다. 동료가 공유한 아티클, 팀 회의 내용, 배포 결과 보고. 그런데 몇 개는 어제 저녁에 온 거다. 내가 자고 있던 시간에. "아, 이거 답장해야 하나..." 생각만 하고 창을 닫는다. 주말이니까 나중에 하자. 나중에란 보통 월요일 아침 출근할 때인데, 정확히는 커피를 마신 후다. 첫 커피는 목을 헹구는 용도다. 이게 반복되다 보니 나도 모르게 습관이 됐다. 퇴근하면 슬랙을 보지 않는다. 아니, 본다. 하지만 답을 안 한다. 회신할 에너지가 없다. 그리고 내일도, 모레도 같으니까 지금 할 필요가 없다고 자기기만한다. 그런데 이게 정말 건강한 건가? 나도 감시당한다는 느낌은 안 받지만, 뭔가 도망치고 있는 건 같다. 넷플릭스에서 도망치고, 폰에서 도망치고, 결국 자기 자신에게서 도망치는 거 같다.이게 정상이라는 게 더 무서운 이유 주말에 게임을 했다. 진짜 게임, 즉 폰 게임 말고. 실제로 콘솔에서 하는 그런 거. 아내가 권했다. "너 요즘 폰만 본다. 너 원래 이런 사람 아니었잖아." 맞다. 나는 원래 코딩이 좋았다. 집에 와서도 사이드 프로젝트 같은 거 생각했었다. 새로운 라이브러리 시도해보고, 재미있는 알고리즘 문제 풀고. 그래서 처음에 신입 때는 계속 배워야겠다고 다짐했었다. 그런데 지금은? 지금은 폰을 본다. 매일 밤. 내가 이상한 게 아니라, 이게 정상이라는 게 무서워서다. 직장 동료들한테 이 얘기를 했다. "너 퇴근 후에 뭐 해?"라고. 대충 다 똑같은 답이다. "그냥... 쉰다." "쉬는 방법이 뭔데?" "폰 본다. 드라마 본다. 별 거 없어." 우리는 모두 같은 늪에 빠져 있다. 그리고 그게 정상처럼 느껴진다. 아무도 이상하다고 말하지 않는다. 왜냐하면 모두가 그렇게 하니까. 반공학적인 표현을 하자면, 이건 디자인 문제다. 넷플릭스와 폰은 우리가 계속 붙어있도록 설계되어 있다. 알고리즘은 우리의 취향을 학습하고, 자동 재생은 우리의 마음을 읽는다. "계속 보시겠습니까?" 같은 짜증나는 팝업은 없다. 그냥 다음 에피소드가 자동으로 켜진다. 우리는 편함을 추구한다. 그리고 그 편함은 점점 깊어진다. 그럼 어떻게 할 건데? 여기가 참 어려운 부분이다. 나는 답을 모른다. "내일부터는 달라질 거야"라는 다짐은 더 이상 안 한다. 너무 많이 했으니까. 아침에 일어나서 "오늘은 퇴근하면 독서를 해야지"라고 생각한다. 근데 퇴근해서 소파에 누우면, 손이 자동으로 폰을 집어 든다. 뇌가 이기는 거다. 그렇다고 무포기하는 건 아니다. 겨우 그 정도 의지력도 못 발휘냐고 스스로를 채찍질할 수도 있겠지만, 그건 별로 생산적이지 않다. 나는 이미 업무 시간에 충분히 스트레스를 받고 있다. 퇴근해서까지 자신한테 엄격할 이유가 뭐 있나. 대신 좀 더 현실적으로 생각한다. 넷플릭스 재생을 멈추는 건 못 해도, 조금 다른 환경을 만들 수는 있다. 예를 들어, 거실 조명을 끄지 말기. 아니면 폰을 손 닿지 않는 곳에 두기. 아니면 아내와 함께 보기. 사실 제일 좋은 방법은 피곤하지 않은 것 같다. 지금 내가 퇴근 후 에너지가 없는 이유는, 퇴근 전까지 이미 다 소진했기 때문이다. 후배 PR 리뷰하고, 기획자 요구사항 해석하고, 레거시 코드와 싸우고, 배포 스트레스 받고. 남은 건 빈 깡통뿐이다. 그러니까 문제는 퇴근 후가 아니라, 근무 중일 수도 있다. 저 화면을 끄는 날이 올까 최근에 좋은 일이 있었다. 회사에서 토이 프로젝트를 해보자는 제안이 나왔다. 우리 팀이 뭔가 새로운 기술을 시도해보는 거다. 최근 핫한 프레임워크 같은 거. 처음엔 별로 관심이 없었다. 또 일을 더 하라는 거 아닌가 싶었다. 그런데 일주일 정도 지나니까 마음이 바뀌었다. 이건 업무가 아니라 공부인 거고, 재미있을 것 같았다. 내 손이 다시 키보드로 움직였다. 집에 가서도 그걸 생각했다. 새로운 라이브러리는 뭐가 다를까? 이걸 어떻게 적용해볼까? 넷플릭스를 틀어놨지만, 5분이 채 안 돼서 껐다. 딱 하나의 이유로. 흥미로웠기 때문이다. 그 밤에 나는 폰을 안 들었다. 노트북을 켰다. 간단한 프로토타입을 만들어 봤다. 코드를 짰다. 에러가 났다. 스택오버플로우에서 답을 찾았다. 다시 시도했다. 작동했다. 자정을 넘었을 때, 문득 시간이 얼마나 지났는지 몰랐다. 그건 오랜만에 느끼는 감정이었다. "플로우 상태"라고 심리학에서 부르는 그 감각. 시간이 멈춘 것처럼 느껴지는 상태. 분명히 이게 내가 좋아하던 일이었다. 내가 일이란 걸 선택한 이유가 이것 때문이었다. 그런데 요즘은 언제쯤 마지막으로 이 감정을 느꼈지? 넷플릭스 자동 재생 화면을 넘어, 폰 무한 스크롤을 벗어나면, 내가 다시 돌아올 수 있을까? 그 생각이 들었다. 아직 희망은 남아 있다는 뜻이다. 내일 퇴근하면 뭘 할까? 아마 넷플릭스를 틀 것 같다. 습관이니까. 폰도 들 거고. 그런데 한 가지는 다를 거다. 한 번쯤은, 그냥 한 번쯤만 화면을 끄고, 노트북을 켜 볼 생각을 해야겠다는 것.결국 우리가 봐야 할 건 화면이 아니라 미래다.
- 01 Dec, 2025
MySQL 인덱스 설계: 경험치로는 모자란 부분
MySQL 인덱스 설계: 경험치로는 모자란 부분 느려진다는 신호, 그 다음부터가 지옥문 "김개발님, 대시보드 조회가 자꾸 타임아웃이 나네요. 인덱스를 좀 봐주실 수 있을까요?" 팀원 신입이 슬랙에 메시지를 보냈고, 나는 "아 그거요, 금방 봐드리죠"라고 답했다. 자신감 가득한 대답이었다. 그런데 이게 내 인생의 3시간을 빼앗을 줄은 몰랐다. 일반적으로 백엔드 개발자에게 "쿼리가 느리다"는 보고는 고혈압 약을 먹고도 혈압을 올릴 수 있는 마법의 말이다. 왜냐하면 가능한 원인이 너무 많기 때문이다. N+1 쿼리? 인덱스 미스? 테이블 잠금? MySQL 캐시? 네트워크 레이턴시? 아니면 서버 리소스 부족? 경력 7년차라는 타이틀이 나를 자신감 넘치게 만든다. 7년이면 충분하지 않을까? 인덱스 정도야 내가 다 알지. 나는 빠르게 쿼리 플랜을 뽑아내기로 했다. EXPLAIN EXTENDED SELECT * FROM user_logs WHERE user_id = 123 AND created_at BETWEEN '2024-01-01' AND '2024-12-31' AND status IN ('active', 'pending') ORDER BY created_at DESC LIMIT 100;플랜을 봤다. rows: 2,847,392. 풀 테이블 스캔이다. 뭐가 이렇게 많이 나오는 거야? 나는 즉각 인덱스를 설계했다. "네, 여기요. (user_id, created_at, status) 복합 인덱스를 추가하면 될 것 같습니다. 아 그거요, status도 같이 포함해야 필터링이 먹으니까요." 신입이 감탄했다. "오, 역시 시니어는 다르네요!" 그 칭찬이 나를 망쳤다. 나는 인덱스를 추가했고, 자리로 돌아가 커피를 마셨다. 그리고 30분 뒤... "어... 김개발님? 아직 느린데요?"'EXPLAIN' 다시 읽기, 내가 놓친 것들 다시 쿼리 플랜을 봤다. 이번엔 좀 더 찬찬히. 아니, 이전엔 정말 제대로 본 건가? mysql> EXPLAIN EXTENDED SELECT * FROM user_logs WHERE user_id = 123 AND created_at BETWEEN '2024-01-01' AND '2024-12-31' AND status IN ('active', 'pending') ORDER BY created_at DESC LIMIT 100\G결과를 보니:Type: ALL - 풀 테이블 스캔 Key: NULL - 인덱스를 사용하지 않음 Key_len: NULL - 인덱스 길이도 없음 Rows: 2,847,392 - 약 284만 건을 스캔 Using where; Using filesort - 디스크 정렬 사용어? 근데 내가 인덱스 추가했는데? 나는 실시간으로 테이블을 다시 확인했다. SHOW INDEXES FROM user_logs;인덱스가 추가되어 있었다. 그럼 왜 안 쓰는 건데? 그 순간이었다. 뇌가 깨어났다. 아, 쿼리 자체가 이상한데? 다시 원래 쿼리를 봤다. SELECT * FROM user_logs WHERE user_id = 123 AND created_at BETWEEN '2024-01-01' AND '2024-12-31' AND status IN ('active', 'pending') ORDER BY created_at DESC LIMIT 100;뭔가 이상하다. WHERE절에는 user_id가 먼저 오고, 그 다음에 created_at과 status가 있다. 그런데 내가 만든 인덱스는... (user_id, created_at, status)? 아 그거요. 잠깐. 인덱스가 있어도 MySQL 옵티마이저가 판단해서 안 쓸 수도 있다. 특히 카디널리티(Cardinality) 문제가 있으면 말이다. ANALYZE TABLE user_logs; SHOW INDEXES FROM user_logs\G카디널리티를 확인했다. user_id는 5개. status는 3개. 근데 전체 행은 284만 건. 오... 아. 그 순간 깨달음이 왔다. Status 컬럼의 카디널리티가 너무 낮다는 것이다. 'active'와 'pending'만 있으면, 결국 거의 모든 행이 조건을 만족한다는 뜻이다. 그럼 MySQL은 "인덱스를 타서 284만 건을 다 읽은 다음 정렬할까? 아니면 차라리 풀 스캔 한 번 하고 나중에 정렬할까?" 이렇게 판단하고, 풀 스캔이 더 빠르다고 생각한 것이다. 그런데 내가 뭘 했냐고? 인덱스를 추가하고 "야, 이제 빠르겠지?" 했다. 기초를 무시하고 경험만 믿었다. EXPLAIN EXTENDED SELECT * FROM user_logs WHERE user_id = 123 AND created_at BETWEEN '2024-01-01' AND '2024-12-31' ORDER BY created_at DESC LIMIT 100;status 조건을 빼서 실행해봤다. Type: range Key: idx_user_created_status Key_len: 12 Rows: 1,247 Using index condition어? 이제 인덱스를 쓴다? 그렇다. 내가 놓친 문제는 인덱스가 없는 게 아니었다. 쿼리 자체가 문제였고, 옵티마이저의 선택이 합리적이었고, 내가 상황을 제대로 분석하지 않았다는 것이다.쿼리 다시 쓰기, 그리고 배운 것 결국 문제는 쿼리였다. 신입이 준 쿼리를 그대로 실행시키고 있었다. SELECT * FROM user_logs WHERE user_id = 123 AND created_at BETWEEN '2024-01-01' AND '2024-12-31' AND status IN ('active', 'pending') ORDER BY created_at DESC LIMIT 100;나는 신입을 불렀다. "어, 이 쿼리를 왜 이렇게 짰어요? status를 꼭 WHERE절에 포함해야 하나요?" 신입 왈: "어... 기획팀에서 'active나 pending 상태인 로그만 보고 싶다'고 했는데..." "근데 실제로 active나 pending이 아닌 상태의 로그가 많나요?" 신입이 생각해봤다. "아, 거의 없네요. 아마 전체의 98% 정도가 active나 pending일 것 같은데요?" 그 순간이었다. 카디널리티가 높지 않은 컬럼은 WHERE절의 앞에 오면 안 된다. 특히 복합 인덱스에서는 더욱 그렇다. 인덱스 컬럼의 순서가 중요하다는 건 알았지만, 이렇게까지 직접 경험할 줄은... 나는 쿼리를 다시 설계했다. SELECT * FROM user_logs WHERE user_id = 123 AND created_at BETWEEN '2024-01-01' AND '2024-12-31' LIMIT 100;그리고 애플리케이션 단에서 status를 필터링했다. 실행 시간: 150ms → 8ms. 18배 빨라졌다. 인덱스를 하나 추가해서가 아니라, 쿼리를 바꿔서. 그 다음부터 나는 매번 "아 그거요"라고 하기 전에 EXPLAIN을 3번은 본다. 아 그거요. 근데 일단 쿼리 플랜부터 봐야 할 것 같은데요.이렇게 말한다. 조금 더 신중해진 나. 인덱스는 은탄환이 아니다 사실 이 경험이 나한테 준 가장 큰 교훈은 간단하다. 인덱스를 추가하는 것보다, 쿼리를 제대로 짜는 게 100배 중요하다. 7년차니까 알겠지 하면서 기초를 무시했다. 느린 쿼리가 나오면 인덱스를 먼저 생각한다. 하지만:EXPLAIN을 제일 먼저 봐야 한다. Type, Key, Key_len, Rows, Extra 모두. 카디널리티를 확인해야 한다. 특히 복합 인덱스를 만들 때. 인덱스 컬럼의 순서가 결정적이다. Equality, Range, Sort 순서로. LIMIT절이 있으면 MySQL은 풀 스캔도 고려한다. 특히 작은 LIMIT일 때. 데이터 분포를 알아야 한다. 같은 인덱스도 데이터가 어떻게 분포하는지에 따라 쓰일 수도, 안 쓰일 수도 있다.그날 이후로 나는 슬랙 메시지에 "쿼리가 느려요"라는 보고가 오면, 먼저 쿼리를 본다. 그 다음에 EXPLAIN을 본다. 그리고 나서야 인덱스를 생각한다. 신입이 또 다른 느린 쿼리 건을 줬을 때, 나는 EXPLAIN을 봤다. N+1이었다. 관계 데이터베이스인데 Loop Join으로 100만 번의 쿼리를 날리고 있었다. 인덱스와는 아무 상관이 없었다. "아 그거요. LEFT JOIN으로 한 번에 가져가시면 될 것 같습니다." 이번엔 자신감이 없었다. EXPLAIN을 5번 봤고, 쿼리를 다시 썼고, 인덱스 필요성을 판단했다. 그 결과 실행 시간이 47초에서 2초로 줄었다. 신입이 감탄했고, 나도 뿌듯했다. 하지만 그 뿌듯함은 다르다. 이번엔 "내가 똑똑해서"가 아니라 "내가 제대로 분석해서"다.당신이 다음부터 할 수 있는 것 이 글을 읽는 개발자에게 당부하고 싶다. 특히 "경력이 좀 쌓인" 개발자에게. EXPLAIN을 무시하지 말자. 쿼리가 느려졌다고 해서 인덱스를 먼저 생각하지 말자. 그리고 자신감으로 기초를 뛰어넘지 말자. 진짜로 해야 할 체크리스트:쿼리를 읽는다. JOIN 수, WHERE 조건, GROUP BY, ORDER BY. 이상한 부분이 있나? EXPLAIN을 본다. Type이 ref가 아닌가? Rows가 너무 많진 않은가? Using filesort가 있진 않은가? 실제 실행 시간을 본다. EXPLAIN은 추정값이다. 100% 정확하지 않다. 쿼리를 수정한다. WHERE절 순서, JOIN 방식, 서브쿼리 활용. 그래도 느리면 인덱스를 생각한다.내가 처음에 인덱스를 추가했을 때, 아무 효과가 없었던 이유는 인덱스가 문제가 아니었기 때문이다. 쿼리 자체가 비효율적이었다. 지금도 팀에서 느린 쿼리가 나오면 내가 본다. 그런데 거의 절반 이상이 인덱스 문제가 아니다. 대부분은:**SELECT *** 하는데 실제로 10개 컬럼만 필요한 경우 Left Join에서 WHERE절에 Right 테이블 조건이 있어서 LEFT가 INNER가 되는 경우 GROUP BY 후 HAVING으로 필터링 하는데, WHERE에서 미리 필터링할 수 있는 경우 Subquery로 일일이 조회하는 경우이런 것들은 인덱스로 안 된다. 쿼리를 다시 써야 한다. 나는 이제 느린 쿼리를 보면 한숨이 아니라 '어, 이게 뭔가 재미있는 문제네?' 이런 생각이 든다. 왜냐하면 대부분 쿼리 최적화로 해결할 수 있기 때문이다. 그리고 그게 훨씬 더 근본적인 솔루션이기 때문이다. 경력이 늘수록, 기초가 더 중요하다는 걸 깨닫는다. 그리고 "아 그거요"라는 대답이 가장 위험한 말이라는 것도.인덱스는 옷깃이 아니라, 기초 설계가 먼저인 법이다.
- 01 Dec, 2025
아침 9시 출근이 지옥인 이유: 커피 3잔의 진실
아침 9시 출근이 지옥인 이유: 커피 3잔의 진실 아침 7시. 알람이 울린다. 나는 눈을 뜬다. 하지만 뇌는 아직 잠들어 있다. 7시 30분. 샤워를 한다. 따뜻한 물이 얼굴을 적신다. 아직도 아무것도 아니다. 8시. 출근길. 버스에 앉아 창밖을 본다. 세상이 흐릿하다. 이건 창밖이 아니라 내 눈이 문제다. 8시 50분. 회사 건물 1층 카페. 여기서부터 내 하루가 시작된다. 아니, 내 카페인 의존성이 시작된다고 해야 맞다. 첫 번째 커피, 아메리카노(Small): 의식 깨우기첫 아메리카노는 의식을 깨우는 과정이다. 편의점에서 산 아메리카노 한 잔, 350ml. 마시면서 생각한다. '오늘도 버텨야 한다.' 이 시점에서 나는 깨어났다고 생각하지만, 실은 깨어나는 중이다. 이메일을 확인한다. 야간에 들어온 긴급 배포 관련 슬랙 메시지 5개. 아, 이미 스트레스 호르몬이 분비되고 있다. 카페인이 혈액에 흡수되는 데 15분이 걸린다고 한다. 그래서 나는 9시 정각 5분 전에 첫 잔을 마신다. 과학이다. 생존 전략이다. 9시. 자리에 앉는다. 첫 아메리카노의 효과가 나타나기 시작한다. 뇌가 작동한다. 메일을 읽을 수 있다. 슬랙 메시지의 의미를 이해할 수 있다. 이것이 정상인 상태다. 두 번째 커피, 아메리카노(Large): 집중력 유지11시. 두 번째 아메리카노를 마신다. Large 사이즈. 이 시간이 되면 후배들의 PR 리뷰 요청이 들어온다. 어제 올린 4개의 PR, 각각 300줄 이상의 코드를 읽고 의견을 남겨야 한다. 구조적 결함, 네이밍 컨벤션, 잠재적 버그... 이 모든 것을 찾아내려면 뇌가 풀 파워로 돌아야 한다. 두 번째 커피가 없으면 이건 불가능하다. 진짜다. 심지어 한두 번 해봤다. 첫 커피만으로 버티려다가 후배 코드에 "괜찮습니다" 같은 무책임한 리뷰를 달고 나중에 버그가 터져서 야근하는 악순환을 겪었다. 지금은 과학적으로 접근한다. 11시 정각, 두 번째 커피. 오후의 집중력은 이 한 잔에 달려 있다. 세 번째 커피, 핫아메리카노: 일몰 신드롬 극복3시 30분. 여기서 오후 슬럼프가 온다. 오후 3시부터 5시까지는 개발자에게 죽음의 시간이다. 생체 리듬이 오후 커피 이후로 점진적으로 떨어진다. 의학적으로는 '포스트 런치 디프(post-lunch dip)'라고 부른다. 이 시간대에 버그가 터진다. 왜냐하면 우리 모두 집중력이 떨어져 있기 때문이다. 그래서 세 번째 커피가 필요하다. 이번엔 핫아메리카노다. 따뜻한 한 잔이 심리적 위안을 준다. 맛도 좀 더 부드럽고, 마시는 시간도 더 오래 걸려서 정신 차리는 데 도움이 된다. 기획팀에서 '이거 간단하지 않을까요? 오늘 안에 가능할 것 같은데' 같은 메시지가 들어오는 시간도 대략 이 무렵이다. 핫아메리카노를 한 모금 마시고, 깊은 숨을 쉬고, 정중한 톤으로 '검토해보겠습니다'라고 답한다. 커피가 없었다면 아마 폭발했을 것이다. 커피 없이는 불가능한 것들나는 이 회사에 7년을 있으면서 배운 게 있다. 커피는 단순한 음료가 아니라는 것이다. 아메리카노는 내 생산성 매니저다. 회사는 나를 그 역할로 본다(사실 직책도 없이). 커피는 그 일을 가능하게 하는 연료다. 후배들 PR 리뷰, 레거시 코드 분석, 기획팀과의 일정 협상... 이 모든 것이 커피에 의존한다. 문제는 이게 지속 불가능하다는 것이다. 5년 전엔 첫 커피 한 잔으로도 충분했다. 지금은 3잔이 필수다. 내년엔 4잔이 필요할까? 커피 중독이라고 부르면 너무 거창한가? 아니다. 이건 직업병이다. 가끔 주말에 휴무로 하루 종일 집에 있으면 커피 생각이 안 난다. 넷플릭스 보고, 치킨 시켜먹고, 폰 보다가 잔다. 그때는 커피가 필요 없다. 왜냐하면 내가 필요 없는 사람이기 때문이다. 그냥 평범한 34세 남자일 뿐이다. 하지만 월요일 아침이 오면 다시 돌아간다. 알람, 샤워, 출근길. 그리고 편의점. 첫 번째 아메리카노. 이것이 내 일상이다. 9시 정각을 맞추기 위해 매일 같은 것을 반복한다. 생존을 위해서.결국 개발자의 하루는 커피 사이의 공백을 채우는 일이다.