Showing Posts From
개발
- 22 Dec, 2025
면접 볼 회사를 찾아보는 늦은 밤의 고민
면접 볼 회사를 찾아보는 늦은 밤의 고민 11시 45분 노트북을 켰다. 넷플릭스 대신 잡코리아를. 아내는 벌써 잤다. 나는 거실 소파에 앉아서 채용공고를 본다. 스크롤을 내린다. 올린다. 또 내린다. "백엔드 개발자 5년 이상, Spring 필수" 자격은 된다. 연봉은 7천에서 협의. 지금보다 500은 오를 거다. 그런데 손이 안 간다. 지원하기 버튼이.창은 5개가 떠 있다. 잡코리아, 원티드, 로켓펀치, 프로그래머스, 링크드인. 매일 밤 11시쯤 이러고 있다. 벌써 3주째다. 이력서는 아직도 2년 전 버전이다. 네카라쿠배는 글렀고 네이버는 어렵다. 카카오도 어렵다. 라인 쿠팡 배민 다 어렵다. 알고리즘 문제를 3개월은 풀어야 한다. 코테 통과율이 10%라던데. 나는 프로그래머스 레벨 2도 버겁다. 지난 주말에 DFS 문제 하나 풀었다. 2시간 걸렸다. 결국 답 봤다. "이 정도는 기본이죠" 면접관이 그럴 거다. 나는 땀 흘리며 화이트보드에 서 있을 거고. 그래서 대기업을 본다. 삼성SDS, LG CNS, 현대오토에버. SK C&C도 있다. 연봉은 네카라쿠배보단 낮다. 그래도 지금보단 높다. 7천은 준다.복지가 좋다는 말이 많다. 야근이 적다는 말도. "칼퇴 문화 정착" 공고에 다 그렇게 써 있다. 믿어도 되나. 마우스를 올렸다. 지원하기 버튼에. 그리고 뗐다. 이력서가 문제다 경력기술서를 써야 한다. "주요 성과를 정량적으로 기술하세요" API 응답속도 30% 개선. DB 쿼리 최적화로 트래픽 처리량 2배 증가. 쓸 건 있다. 그런데 어떻게 쓰지. "Elasticsearch 도입으로 검색 성능 향상" 이게 맞나. 너무 뻔한가. 다들 이렇게 쓰나. GitHub 링크를 넣어야 한다는 글을 봤다. 내 GitHub은 초록색이 없다. 잔디가 말랐다. 마지막 커밋이 8개월 전이다. 회사 일로 바빴다고 쓸 순 없다.사이드 프로젝트를 만들까. 토이 프로젝트 하나 있으면 어필된다던데. 뭘 만들지. To-do 앱은 너무 흔하고. 게시판도 그렇고. "ChatGPT API 활용한 뭔가..." 생각해봤다. 아이디어가 안 나온다. 그리고 시간이 없다. 퇴근하면 9시다. 밥 먹으면 10시. 주말엔 쉬고 싶다. 자격요건을 본다 "우대사항: AWS, Docker, Kubernetes 경험자" AWS는 쓴다. Docker도 쓴다. Kubernetes는 안 써봤다. 강의를 들어야 하나. 인프런에 강의가 많다. 20만원짜리도 있다. 사면 또 안 볼 거다. 작년에 산 강의가 3개 있다. 진도율 15%, 8%, 0%. "Spring Boot 3.x, Java 17 이상" 우린 아직 Java 11이다. Spring Boot 2.7. 레거시 건드리면 팀장님이 싫어한다. "되는 걸 왜 바꿔" 기술스택이 뒤처지는 거 같다. 불안하다. 면접에서 물어보면 뭐라고 답하지. "최신 기술 학습에 관심이 많습니다" 거짓말은 아니다. 관심은 있다. 실행을 안 했을 뿐. 연봉 계산기를 켠다 7천만원 실수령액. 세후 580만원. 지금은 540만원. 40만원 차이. 1년이면 480만원이다. 대출 이자가 월 35만원이다. 40 빼면 5만원 남는다. 5만원으로 뭘 하지. "연봉 협상 가능" 공고에 그렇게 써 있다. 7천에서 시작하면 7500까지 받을 수 있나. 아니면 8천도. 계산기를 두드린다. 8천만원 실수령액. 현실적이지 않다. 경력 7년에 8천은 네카라쿠배나 받는다. 7500도 어렵다. 7200 정도. 그래도 지금보단 낫다. 면접 후기를 본다 블라인드에 들어갔다. "삼성SDS 면접 후기" 검색. "1차 코테 + 2차 직무면접 + 3차 임원면접" 3번이나 본다. 한 달은 걸린다. 코테는 프로그래머스 레벨 2~3 수준이라는데. 레벨 3은 못 푼다. 지금 실력으론. "직무면접에서 설계 질문 많이 나옴. MSA, DB 정규화, 트랜잭션 격리수준" MSA는 해본 적 없다. 팀장님한테 제안했다가 까였다. DB 정규화는 안다. 트랜잭션도 안다. 설명하라면 버벅일 거다. "임원면접은 인성 위주. 근데 기술 질문도 섞여 나옴" 3차까지 가면 붙는다던데. 3차까지 가는 게 문제다. 댓글을 본다. "저도 7년차인데 1차 코테 떨어짐 ㅋㅋ" 웃을 수가 없다. 회사 복지를 비교한다 현재 회사: 연차 15일, 자율복장, 야근수당 없음, 점심 7천원 지원. 대기업: 연차 20일, 복지포인트 연 200만원, 기숙사, 카페테리아, 헬스장. 복지포인트 200만원이 크다. 연 단위로 쌓인다. 헬스장도 좋다. PT 끊으면 월 30만원인데. 기숙사는 필요 없다. 출퇴근 1시간이면 괜찮다. 그런데 야근은 어떨까. "프로젝트 일정 타이트할 때 야근 있음" 블라인드 댓글에 그렇게 써 있다. "그래도 중소기업보단 덜함" 지금도 배포일엔 10시까지 남는다. 한 달에 2번. 대기업은 어떨까. 더할까 덜할까. 동기한테 물어볼까 대학 동기 중에 삼성에 간 놈이 있다. 4년 전에 이직했다. 단톡방에서 본 게 작년이다. 카톡을 열었다. 1:1 대화방. "야 너 요즘 어때" 쳤다가 지웠다. 뭐라고 물어보지. "이직 어땠어?" 너무 직접적이다. "거기 야근 많아?" 이것도 이상하다. "면접 준비 뭐했어?" 이건 괜찮은데. 4년 만에 연락해서 이직 물어보면 어떻게 생각할까. '얘 이직하려나보네.' 맞긴 하다. 그래도 부담스럽다. 나중에 안 되면 창피하다. 카톡을 껐다. 포기할까 시계를 봤다. 12시 50분. 1시간 동안 채용공고만 봤다. 지원은 0건. 이력서는 안 고쳤다. 알고리즘 문제도 안 풀었다. 내일도 출근이다. 9시 회의가 있다. 기획자가 새 기능을 제안한다. "이거 이번 주에 되죠?" 또 안 된다고 말해야 한다. 노트북을 덮었다. 이직은 다음 주에 생각하자. 다음 주에도 이러고 있을 거다. 안다. 그래도 오늘은 자야 한다. 침대로 갔다. 아내가 돌아누웠다. 나도 누웠다. 눈을 감았다. '7500은 받아야 하는데.' 잠이 안 온다. 결국 토요일에 주말이다. 오전 11시에 일어났다. 아내는 친구 만나러 나갔다. 커피를 내렸다. 노트북을 켰다. 이력서를 열었다. 고치기 시작했다. "API 응답속도 30% 개선..." 지웠다. 다시 썼다. "Redis 캐싱 도입으로 평균 응답시간 2.3초 → 0.7초 단축" 구체적으로 썼다. 숫자를 넣었다. 더 쓴다. "레거시 시스템 리팩토링으로 월 서버 비용 40만원 절감" 이것도 성과다. 쓸 만하다. 2시간 썼다. 경력기술서 80% 완성. GitHub을 켰다. 커밋을 하나 만들었다. README 수정. "Update project description" 초록색이 하나 생겼다. 별로다. 프로그래머스를 켰다. 레벨 2 문제 하나. 30분 걸렸다. 통과했다. 기분이 좀 낫다.내일 또 미룰 거다. 그래도 오늘은 뭔가 했다.
- 11 Dec, 2025
기술 부채를 상환해야 하는데 새로운 기능을 요청받다
월요일 아침, 슬랙 한 방 출근하자마자 슬랙이 울렸다. 기획팀 이수진 님: "개발님, 이번 주 스프린트에 소셜 로그인 추가 가능할까요? 사용자 요청이 많아서요 🙏" 커피도 안 마셨다. 마우스 커서가 멈췄다. 지난주 금요일에 작성한 리팩토링 계획서를 열었다. "레거시 인증 모듈 개선 - 예상 기간 2주" 파일명 끝에 (최종)(진짜최종)(이번엔진짜).md 3개월 전에도 이랬다. 6개월 전에도.기술 부채 장부 우리 서비스 인증 모듈은 4년 전 코드다. 당시 주니어가 급하게 만들었다. 그 주니어가 나다. UserAuthService.java 파일을 열면 숨이 막힌다. 1,247줄. 메서드 하나가 350줄. if 문이 7단 중첩. 주석은 "// 나중에 고치기" 이걸 고치려면 최소 2주다. 테스트 코드 작성 3일. 리팩토링 5일. QA 3일. 버퍼 3일. 근데 새 기능은 3일이면 된다. 소셜 로그인 API 붙이고, 기존 로직 우회하면 끝. 기획자 입장에선 당연히 새 기능이다. 사용자는 리팩토링을 모른다. 경영진도 모른다. 속도만 안다.회의실, 30분 PM이 물었다. "둘 다는 안 되나요?" 테크 리드인 나는 설명했다. "기술 부채 상환하면서 새 기능 개발하면, 둘 다 늦어집니다." "그럼 리팩토링은 나중에 하면 안 돼요?" "6개월째 나중입니다." PM이 한숨 쉬었다. 나도 한숨 쉬었다. 결국 타협했다. 소셜 로그인 먼저. 리팩토링은 다음 스프린트. 다음 스프린트엔 또 급한 게 생긴다. 알고 있다. 다들 알고 있다. 회의실 나오면서 후배 개발자가 물었다. "형, 리팩토링 언제 하는 거예요?" "다음에." "지난번에도 다음에라고..." 말을 잇지 못했다.빚은 눈덩이처럼 기술 부채는 이자가 붙는다. 소셜 로그인을 기존 인증 모듈에 덕지덕지 붙였다. UserAuthService.java가 1,247줄에서 1,489줄이 됐다. if 문이 8단 중첩. 새로운 주석이 추가됐다. "// TODO: 전체 리팩토링 필요. 2024.03.11" 2주 뒤, 기획팀에서 또 연락 왔다. "애플 로그인도 추가해야 해요. iOS 필수래요." 1,489줄이 1,672줄 됐다. if 문이 9단 중첩. 주석이 하나 더 늘었다. "// FIXME: 이거 진짜 개판임. 누가 좀..." 그게 나다. 한 달 뒤, 버그 리포트가 올라왔다. "특정 조건에서 로그인 안 됨" 디버깅하는데 4시간 걸렸다. if 문 6단계 안쪽에 숨어 있는 버그. 원래 코드 짠 사람도 모를 거다. 나도 모르겠다. 내가 짰는데. 다른 팀은 어떨까 점심시간에 옆 팀 시니어 개발자를 만났다. 김치찌개집 단골. "너네 팀은 리팩토링 시간 받냐?" "우린 스프린트마다 20% 할당해." 부럽다. "어떻게 설득했어?" "CTO가 개발자 출신이라." 우리 CTO는 영업 출신이다. "빠르게 만들면 되지, 왜 고쳐?" 설명해도 이해 못 한다. '작동하는데 왜 고쳐'가 그분 철학이다. 다른 회사 동기한테 물어봤다. 카톡으로. "너네는?" "우린 장애 나야 리팩토링 시간 받음." "장애 안 나면?" "계속 쌓이지 뭐." 다들 비슷하다. 위안이 안 된다. 설득의 기술 기술 부채를 설명하는 건 어렵다. 비개발자에게 "레거시 코드가..."라고 하면 "그래서 영업일 기준 며칠?"이라고 묻는다. "속도가 느려져요"라고 하면 "지금 느린가요?"라고 묻는다. "나중에 큰 문제가..."라고 하면 "나중이 언제죠?"라고 묻는다. 대답할 수 없다. 언제 터질지 모른다. 터지기 전까진 멀쩡해 보인다. 그래서 나는 다르게 설명한다. "새 기능 개발 속도가 점점 느려집니다." 구체적 숫자를 댄다. "3개월 전엔 3일 걸리던 게 지금은 5일 걸립니다." "다음 분기엔 7일 걸릴 겁니다." "리팩토링하면 다시 3일로 줄어듭니다." 그래도 안 통한다. 결국 장애가 나야 한다. 장애가 나면 모두가 심각함을 안다. 그때 리팩토링 시간을 받는다. 소 잃고 외양간 고친다. 결국 타협 이번 스프린트 계획을 다시 짰다. 소셜 로그인 개발: 3일 애플 로그인 추가: 2일 버그 수정: 1일 회의, 코드 리뷰: 2일 예상 못 한 일: 2일 리팩토링: 0일 스프린트 회고 때 말했다. "기술 부채 관리 시간이 필요합니다." PM이 고개를 끄덕였다. "다음 분기 로드맵에 반영할게요." 이게 세 번째다. 다음 분기에도 급한 게 생긴다. 그래서 나는 포기하지 않는다. 매번 말한다. 회고 때마다. 스프린트 계획 때마다. "리팩토링 시간이 필요합니다." 언젠가는 될 거다. 장애가 나거나. 내가 이직하거나. 농담이다. 반은. 그래도 일은 한다 월요일 오전 11시. 소셜 로그인 개발 시작했다. 일단 기존 코드를 분석한다. 1,672줄을 스크롤한다. 한숨이 나온다. if 문 미로를 헤맨다. "이게 뭐지?" 주석도 없다. git blame으로 작성자를 확인한다. 나다. 2년 전 나다. 당시엔 급했을 거다. 지금 나도 급하다. 그래서 또 덕지덕지 붙인다. 새로운 if 문을 추가한다. 주석을 쓴다. "// TODO: 리팩토링 필요" 언젠가 미래의 내가 볼 거다. 또 한숨 쉴 거다. 그래도 일은 한다. 동작하게는 만든다. 테스트도 돌린다. PR도 올린다. 후배가 코멘트를 단다. "이거 나중에 리팩토링하죠?" 답글을 쓴다. "응, 나중에." approve를 누른다. 퇴근길 생각 6시 30분. 모니터를 끈다. 소셜 로그인 개발 완료. 예상 3일이었는데 1일 만에 끝났다. 기존 코드 건드리지 않고 우회했다. 빠르긴 하다. 근데 찝찝하다. 파일을 닫기 전에 다시 봤다. UserAuthService.java 1,847줄이 됐다. 리팩토링 계획서를 열었다. 예상 기간을 수정했다. 2주에서 3주로. 파일명을 바꿨다. (최종)(진짜최종)(이번엔진짜)(제발).md 저장하고 닫았다. 슬랙 알림이 울렸다. 기획팀 이수진 님: "내일 회의 때 카카오 로그인도 논의해요!" 웃음이 나왔다. 쓴웃음. 가방을 챙겼다. 불을 껐다. 엘리베이터 안에서 생각했다. '다음 스프린트엔 정말 리팩토링 해야지.' 거짓말이다. 다음 스프린트에도 급한 게 있을 거다. 그래도 괜찮다. 모두가 그렇게 일하니까. 위안이 안 된다. 금요일 회고 일주일이 지났다. 소셜 로그인 배포했다. 카카오 로그인도 추가했다. 네이버 로그인 요청도 들어왔다. UserAuthService.java는 2,034줄이 됐다. 처음으로 2천 줄을 넘었다. 스프린트 회고 시간. "이번 주 잘한 점?" "빠르게 배포했어요." "개선할 점?" 침묵. 내가 말했다. "기술 부채 관리가 필요합니다." PM이 물었다. "구체적으로 얼마나 필요해요?" "3주 정도요." "3주 동안 새 기능 개발이 멈추는 거죠?" "네." "다음 다음 스프린트에 넣을게요." 이게 네 번째다. 회고가 끝났다. 다들 자리로 돌아갔다. 나는 리팩토링 계획서를 열었다. 날짜를 수정했다. 다음 다음 스프린트로. 어차피 또 밀릴 거다. 알고 있다.오늘도 빚을 쌓았다. 내일도 쌓을 거다.
- 08 Dec, 2025
API 응답 시간 최적화로 밤새 격렬한 논쟁
화요일 밤 10시 배포가 끝났다. 그런데 퇴근을 못 한다. 민수가 슬랙에 글을 올렸다. "API 응답 속도 개선안 공유합니다." PDF 파일 하나. 23페이지다. 10시에 이걸 올리나. 열어봤다. 캐싱 전략을 Redis에서 로컬 메모리로 바꾸자는 내용이다. 응답 시간을 300ms에서 50ms로 줄일 수 있다고 한다. 벤치마크 결과까지 첨부했다. 성실하긴 하다. 문제는 이게 아니다. "민수야, 잠깐." "네 형님." "이거 좋긴 한데, 메모리 동기화 이슈는?" "Caffeine 쓰면 괜찮을 것 같은데요." 시작됐다.250ms의 의미 우리 API는 평균 300ms다. 나쁘지 않다. 대부분의 유저는 느끼지 못한다. 그런데 민수는 이게 문제라고 한다. 모바일에서 체감된다고. 특히 3G 환경에서. "형님, 요즘 누가 3G 써요." "통계 봐. 아직 12%야." "그 12%를 위해 아키텍처를 바꿔요?" 할 말은 없다. 그래도 12%는 유저다. 민수의 안은 이렇다. Redis 대신 로컬 메모리 캐시를 쓴다. Caffeine 라이브러리로 TTL 5분. 서버 3대 각각 독립적으로 캐시를 가진다. 장점은 명확하다. 네트워크 홉이 사라진다. Redis까지 왕복 50ms가 없어진다. 데이터 직렬화도 필요 없다. 객체를 그냥 메모리에 둔다. 단점도 명확하다. 캐시 불일치. 서버 A에서 업데이트해도 서버 B는 모른다. 최대 5분간 다른 데이터를 준다. "그 정도는 괜찮지 않아요? Eventually Consistent면 되는데." "민수야, 우리 결제 API도 이거 쓰는데." "아." 침묵. 밤 11시, 화이트보드 회의실로 갔다. 화이트보드를 꺼냈다. 민수가 왼쪽에 그렸다. 현재 아키텍처. WAS 3대, Redis 1대. 모든 요청이 Redis를 거친다. 단순하다. 데이터 일관성도 보장된다. 내가 오른쪽에 그렸다. 민수의 안. WAS 3대, 각각 로컬 캐시. Redis는 그대로 두되 폴백용. 복잡하다. "형님, Redis는 왜 남겨둬요?" "세션이랑 Rate Limiting은 어떻게 할 건데." "아, 맞다." 우리는 30분간 화이트보드를 채웠다. 장단점을 표로 만들었다. 로컬 캐시 장점:응답 속도 250ms 단축 Redis 부하 70% 감소 네트워크 장애 영향 최소화로컬 캐시 단점:캐시 불일치 최대 5분 메모리 사용량 서버당 2GB 증가 배포 시 캐시 워밍업 필요 모니터링 복잡도 증가민수가 펜을 내려놨다. "그래도 해볼 만한데요." 나도 펜을 내려놨다. "어떤 API부터?"데이터를 보자 감이 아니라 숫자로 얘기하기로 했다. Grafana를 켰다. 지난 한 달 API 통계. 상품 조회 API일 호출 500만 건 평균 응답 320ms 데이터 변경 주기: 10분에 1번 캐시 히트율: 89%장바구니 API일 호출 120만 건 평균 응답 280ms 데이터 변경 주기: 실시간 캐시 히트율: 45%주문 내역 API일 호출 80만 건 평균 응답 350ms 데이터 변경 주기: 하루 1~2번 캐시 히트율: 92%민수가 화면을 가리켰다. "이거 보세요. 상품 조회는 10분에 한 번만 바뀌잖아요. 5분 불일치도 괜찮아요." 맞는 말이다. 상품 가격이나 재고가 5분 늦게 반영돼도 큰 문제는 없다. "주문 내역도요. 하루에 한두 번만 바뀌는데 5분이 뭐가 중요해요." 이것도 맞다. 이미 완료된 주문은 거의 안 바뀐다. "그럼 장바구니는?" "이건 Redis 그대로 쓰죠." 합리적이다. 자정 넘어서 계산을 해봤다. 현재 비용:EC2 t3.large 3대: 월 50만원 ElastiCache r6g.large 1대: 월 35만원 총 85만원로컬 캐시 적용 시:EC2 t3.xlarge 3대 필요 (메모리 증가): 월 95만원 ElastiCache 그대로: 월 35만원 총 130만원월 45만원 증가. 연 540만원. "형님, 근데 Redis 스케일 아웃 안 해도 되잖아요." 민수 말대로 현재 Redis CPU가 60%다. 트래픽 더 늘면 스케일 아웃 해야 한다. r6g.xlarge로 올리면 월 70만원. 35만원 증가. 결국 비슷하다. "그럼 속도 개선만 남네." "네. 250ms요." "250ms에 얼마를 지불할 건데." 민수가 계산기를 두드렸다. "개발 2주, 테스트 1주, 모니터링 구축 1주. 한 달이요." 한 달이면 내 인건비 500만원, 민수 인건비 400만원. 총 900만원. "첫 해에 1500만원 쓰는 거네." "네." "250ms를 위해." 침묵이 길어졌다.기획팀 메일 그때 메일이 왔다. 기획팀장이다. "개발팀 귀띔 하나 해드려요. 다음 분기에 모바일 앱 리뉴얼 들어갑니다. API 속도 개선 필요하면 지금 하세요. 예산 나와요." 민수가 웃었다. "타이밍 좋네요." 나도 웃었다. "그러게." 예산이 나온다. 그럼 얘기가 다르다. "민수야, 그럼 이렇게 하자." "네." "1단계는 상품 조회만. 리스크 낮으니까." "좋아요." "2단계는 주문 내역. 한 달 모니터링 후에." "네." "장바구니는 그대로." "알겠습니다." 계획이 나왔다. 단계적 적용. 리스크 분산. 새벽 1시 민수가 일어났다. "형님, 그럼 내일부터 시작할게요." "아니, 모레부터. 내일은 쉬어." "괜찮은데요." "민수야, 지금 몇 시야." "아, 맞다." 민수가 가방을 챙겼다. 나도 컴퓨터를 껐다. 엘리베이터에서 민수가 물었다. "형님, 제 안이 맞았어요?" "반은." "반만요?" "기술적으론 맞았어. 근데 비용 계산을 안 했잖아." 민수가 고개를 끄덕였다. "그게 시니어의 차이군요." "뭔 시니어. 나도 3년 전엔 너처럼 했어." "진짜요?" "응. 캐시를 Memcached에서 Redis로 바꾸자고 했다가 팀장한테 혼났지." 엘리베이터가 1층에 도착했다. 최적화의 함정 집에 가는 택시 안에서 생각했다. 개발자는 최적화를 좋아한다. 300ms를 50ms로 만드는 게 즐겁다. 코드가 빨라지면 기분이 좋다. 근데 회사는 다르다. 비용을 본다. 1500만원으로 250ms를 사는 게 합리적인지 묻는다. 답은 없다. 상황마다 다르다. 트래픽이 많으면 최적화가 돈이 된다. 서버 한 대를 줄일 수 있으면 연 1200만원이 남는다. 그럼 해야 한다. 근데 우리는 아직 그 단계가 아니다. 트래픽은 안정적이다. 서버를 줄일 수준도 아니다. 그래도 하기로 했다. 예산이 나오니까. 결국 타이밍이다. 수요일 아침 출근했다. 민수가 먼저 와 있다. "형님, 어제 생각해봤는데요." "응." "1단계 전에 PoC 먼저 돌려보면 어때요?" "좋지." 민수가 노트북을 열었다. 이미 로컬 환경에 구축했다. Caffeine 설정, TTL 5분, 최대 10000개 캐시. "JMeter로 부하 테스트 돌렸어요." "결과는?" "300ms에서 48ms로 떨어졌어요." 오. 벤치마크보다 좋다. "근데요." "뭔데." "CPU가 15% 올라갔어요. GC 때문에." 역시. 로컬 메모리는 GC 압력을 높인다. 객체가 많아지면 Stop-the-World 시간이 늘어난다. "JVM 옵션 튜닝 필요하겠네." "G1GC를 ZGC로 바꿔볼까요?" "Java 버전은?" "11이요." "그럼 안 돼. 17로 올려야 하는데." 또 일이 늘었다. 결국 민수와 나는 계획을 다시 짰다. 1차: 준비 (2주)Java 17 업그레이드 JVM 옵션 튜닝 PoC 환경 구축 모니터링 대시보드2차: 적용 (2주)상품 조회 API 로컬 캐시 적용 카나리 배포 (트래픽 10%부터) 에러율, 응답시간 모니터링3차: 확장 (4주)주문 내역 API 적용 전체 트래픽 전환 성능 리포트 작성총 8주. 2개월이다. 민수가 Jira 티켓을 만들었다. 나는 팀장에게 보고 메일을 썼다. 기술 부채와 개선의 경계 점심을 먹으면서 민수에게 물었다. "어제 힘들었지?" "괜찮았어요. 재밌었어요." "뭐가?" "형님이랑 같이 고민하는 거요." 민수는 솔직하다. "민수야, 근데 하나 물어볼게." "네." "왜 이 최적화를 하고 싶었어?" 민수가 잠깐 생각했다. "느린 게 싫었어요. 우리가 만든 API가 300ms씩 걸리는 게." "유저는 못 느끼는데?" "그래도요. 더 빠르게 할 수 있잖아요." 이해한다. 나도 7년 전엔 그랬다. "근데 형님은 왜 반대하셨어요?" "반대한 게 아니라, 계산을 하자는 거였어." "비용이요?" "응. 시간, 돈, 리스크." 민수가 고개를 끄덕였다. "시니어가 되면 그런 거 자동으로 생각돼요?" "아니. 실수 많이 해봐야 돼." 나는 김치찌개를 한 숟가락 떴다. 논쟁의 가치 어제 밤은 소모적이지 않았다. 민수는 비용 개념을 배웠다. 나는 새로운 기술을 배웠다. Caffeine 라이브러리는 몰랐다. ZGC 얘기도 민수가 꺼냈다. 좋은 논쟁이었다. 나쁜 논쟁도 있다. 자존심 싸움. 누가 옳은지 증명하려는 싸움. 그런 건 시간 낭비다. 좋은 논쟁은 다르다. 서로 배운다. 더 나은 답을 찾는다. 완벽한 답은 없어도 합리적인 답은 나온다. 어제 우리는 합리적인 답을 찾았다. 로컬 캐시를 쓰되, 단계적으로. 비용을 쓰되, 예산 범위 안에서. 리스크를 지되, 최소화하면서. 목요일 오후 민수가 PR을 올렸다. Java 17 업그레이드 작업. 리뷰했다. Gradle 버전도 올렸다. 라이브러리 호환성도 체크했다. 꼼꼼하다. "Approve." 민수가 머지했다. CI/CD가 돌아간다. 테스트 통과. 빌드 성공. "형님, 개발 서버에 배포할게요." "ㄱㄱ." 10분 후. 슬랙 알람. "개발 서버 배포 완료. Java 17 정상 동작." 시작이다. 2주 후 PoC가 끝났다. 결과는 좋았다. 상품 조회 API (로컬 캐시 적용)평균 응답: 45ms (기존 320ms) CPU 증가: 12% (예상 15%) 메모리 증가: 1.8GB (예상 2GB) 캐시 히트율: 94% (기존 89%) 에러율: 변화 없음민수가 보고서를 만들었다. 그래프도 예뻤다. 기획팀에 보냈다. 답장이 왔다. "좋습니다. 프로덕션 일정 잡아주세요." 금요일 저녁 회식이다. 민수 제안으로 고깃집. 고기를 굽는다. 민수가 소주를 땄다. "형님, 건배." "응." 잔을 부딪쳤다. "형님, 고마워요." "뭐가." "어제 밤에요. 같이 고민해줘서." 나는 소주를 마셨다. "민수야, 나도 고마워." "네?" "좋은 아이디어였어. 덕분에 나도 배웠어." 민수가 웃었다. 우리는 고기를 먹었다. 야근 얘기, 레거시 얘기, 신입 얘기. 개발자들이 모이면 하는 얘기. 편했다. 최적화의 끝 최적화는 끝이 없다. 45ms를 20ms로 만들 수도 있다. CDN을 쓰면 된다. 10ms로 만들 수도 있다. Edge Computing을 쓰면 된다. 근데 안 한다. 비용 때문이다. 어디선가 멈춰야 한다. 합리적인 지점에서. 45ms면 충분하다. 유저도 못 느낀다. 비용도 적당하다. 더 빠르게 할 수 있지만, 하지 않는다. 그게 어른이 되는 거다. 민수도 곧 알게 될 거다. 월요일 이번 주에 프로덕션 배포다. 월요일 밤 10시. 카나리 배포. 트래픽 10%. 화요일 새벽 2시. 모니터링 체크. 수요일 오후. 50%로 확대. 목요일 오전. 100% 전환. 계획은 완벽하다. 근데 계획대로 안 될 거다. 항상 그랬으니까. 괜찮다. 우리는 준비했다. 롤백 플랜도 있다. 모니터링도 빡빡하다. 민수가 물었다. "형님, 떨려요." "나도." "진짜요?" "응. 매번 떨려." 배포는 항상 떨린다. 7년 차도 마찬가지다. 근데 해야 한다. 안 하면 아무것도 안 바뀐다.결국 둘 다 옳았다. 민수의 기술안도, 내 비용 계산도. 그래서 절충했다. 개발은 원래 그런 거다. 완벽한 답은 없고 합리적인 타협만 있다. 어제 밤 논쟁은 시간 낭비가 아니었다. 더 나은 방향을 찾았으니까. 다음 주엔 배포다. 또 떨리겠지.
- 07 Dec, 2025
PR 리뷰 요청이 계속 들어오는 금요일 오후의 진실
PR 리뷰 요청이 계속 들어오는 금요일 오후의 진실 금요일 2시 슬랙 알림 소리가 울렸다. 뭔가 불길하다. "김개발님, PR 올렸습니다! 봐주세요~" 시계를 본다. 금요일 오후 2시 17분. 배포는 5시까지다. PR 링크를 클릭했다. 변경된 파일 23개. 추가 라인 +847, 삭제 라인 -124. 한숨이 나왔다. 이게 뭔데 금요일 2시에 올리는 건데.제목을 읽는다. "feat: 사용자 권한 체크 로직 개선". 개선이라는 단어가 들어가면 십중팔구 리팩토링이다. 그리고 리팩토링은 십중팔구 기존 로직을 다 뜯어고친 것이다. 커밋 히스토리를 본다. 커밋 하나. "권한 체크 수정". 설명이 전혀 없다. "이거 왜 바꾼 거예요?" 답장이 왔다. "기존 코드가 복잡해서요." 복잡하다는 건 네가 이해 못 했다는 얘기구나. 첫 번째 리뷰 코드를 읽기 시작했다. public boolean hasPermission(User user, Resource resource) { return user.getRoles().stream() .filter(role -> role.isActive()) .anyMatch(role -> resource.getAllowedRoles().contains(role)); }기존 코드다. 그렇게 복잡하지 않다. public boolean hasPermission(User user, Resource resource) { if (user == null) return false; if (resource == null) return false; List<Role> userRoles = user.getRoles(); if (userRoles == null || userRoles.isEmpty()) return false; List<Role> allowedRoles = resource.getAllowedRoles(); if (allowedRoles == null || allowedRoles.isEmpty()) return false; for (Role userRole : userRoles) { if (!userRole.isActive()) continue; if (allowedRoles.contains(userRole)) return true; } return false; }바뀐 코드다. 3배로 길어졌다. null 체크를 추가했다고 한다. 방어 코드래. 문제는 user랑 resource가 이미 @NonNull 어노테이션이 붙어 있다는 거다. "user랑 resource는 null일 수 없는데요." "혹시 모르잖아요." 혹시 모르면 에러가 터져야 정상이다. null을 숨기면 나중에 더 이상한 데서 터진다.코멘트를 달기 시작했다. "1. null 체크 불필요 (이미 @NonNull)" "2. Stream API 제거한 이유?" "3. 테스트 케이스 추가 필요" 그리고 핵심 질문. "4. 기존 로직이랑 뭐가 달라진 건가요?" 사실 아무것도 안 달라졌다. 그냥 코드 스타일만 바꿨다. 두 번째 PR 리뷰를 마치고 자리에서 일어섰다. 커피가 필요하다. 자리로 돌아오는데 슬랙이 또 울렸다. "개발님, PR 올렸어요!" 또다른 후배다. 시계를 본다. 3시 12분. 링크를 클릭했다. 변경 파일 7개. +342, -89. 제목이 "fix: 버그 수정"이다. 어떤 버그인지 안 써놨다. 커밋 메시지를 본다. "버그 수정", "오류 해결", "코드 정리". 도대체 뭘 고친 건지 알 수가 없다. PR 설명란을 본다. 비어있다. "어떤 버그 수정한 거예요?" "아 네트워크 에러요." 어떤 네트워크 에러인데. "어떤 상황에서 발생하는 에러죠?" "음... 가끔요." 가끔이 제일 무섭다. 금요일의 법칙 금요일 오후에는 법칙이 있다.큰 PR은 금요일 2시 이후에 온다. 설명은 없고 "급해요"만 있다. 테스트는 "로컬에서 돌려봤어요"가 전부다. "월요일에 보면 안 될까요?"는 통하지 않는다.왜냐고? "월요일에 배포해야 해서요." 그걸 지금 말하나.세 번째 알림이 왔다. "개발님 이것도 봐주세요 ㅠㅠ" 변경 파일 31개. 시계를 본다. 4시 03분. 배포까지 한 시간. 커피를 한 모금 마셨다. 식었다. 리뷰 전쟁 세 개의 PR을 번갈아 본다. 첫 번째 PR. 후배가 수정을 올렸다. null 체크는 지웠는데 테스트는 안 올렸다. "테스트요?" "아 깜빡했어요. 지금 올릴게요!" 두 번째 PR. 코드를 읽어봤는데 버그 수정이 아니라 기능 변경이다. "이거 기획 확인은 했어요?" "...해야 하나요?" 당연하지. 세 번째 PR. 코드는 괜찮은데 마이그레이션 스크립트가 없다. "DB 스키마 바꿨는데 마이그레이션은요?" "아 그거요. 제가 직접 실행하려고요." 금요일 저녁에 수동으로 SQL 돌린다고? "롤백 계획은?" "...?" 없구나. 4시 47분 배포까지 13분 남았다. 첫 번째 PR은 테스트 추가해서 approve 했다. 두 번째 PR은 기획 확인하고 월요일에 다시 올리래. 세 번째 PR은 마이그레이션 스크립트 만들고 있다. 슬랙에서 CTO가 물었다. "오늘 배포 건들 뭐죠?" 목록을 보낸다. "셋 다 급한가요?" 하나도 안 급하다. 그런데 후배들은 급하다고 한다. "첫 번째 것만 하죠." 후배한테 말한다. "나머지 두 개는 월요일에." "네... 죄송해요." 죄송하면 다음 주에도 금요일 2시에 올릴 거 아니야. 배포가 시작됐다. 5시 03분. 배포 후 배포는 무사히 끝났다. 로그를 확인한다. 에러 없다. 모니터링 대시보드를 본다. 정상이다. 후배한테 메시지를 보낸다. "배포 완료. 다음엔 미리 올려요." "네! 감사합니다!" 감사는 무슨. 내 금요일 저녁을 돌려달라고. 자리에서 일어났다. 6시 15분. 다른 팀원들은 벌써 퇴근했다. 사무실이 조용하다. 모니터를 끈다. 가방을 챙긴다. 엘리베이터를 기다리는데 핸드폰이 울렸다. 슬랙 알림. "개발님, 내일 출근하시나요? 급한 게 생겼어요..." 토요일에는 안 한다. 알림을 끈다. 월요일이 되면 월요일 아침에 출근한다. 후배가 물어본다. "저번 주 금요일 PR 어땠어요?" "금요일 오후엔 올리지 마." "아 네... 근데 이번 주도 금요일에 배포인데요." 그럼 목요일에 올려. "목요일은 회의가 많아서요." 그럼 수요일에. "수요일은 개발하기 바빠서요." 그럼 화요일에. "화요일은... 생각을 정리하는 날이라서요." 결국 금요일 오후에 올린다는 얘기구나. 커피를 마신다. 아아. 차갑다. 다음 금요일도 똑같을 거다. 후배들은 금요일 2시에 PR을 올릴 거고. 나는 그걸 리뷰할 거고. 배포는 6시 넘어서 끝날 거고. 주말은 짧아질 거다. 그게 시니어 개발자의 금요일이다.다음 금요일에도 슬랙 알림은 2시에 울릴 거다. 그리고 나는 또 리뷰할 거다. 뭐.
- 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시 정각을 맞추기 위해 매일 같은 것을 반복한다. 생존을 위해서.결국 개발자의 하루는 커피 사이의 공백을 채우는 일이다.