Showing Posts From

회의

로그 레벨을 DEBUG로 두고 배포한 죄책감

로그 레벨을 DEBUG로 두고 배포한 죄책감

로그 레벨을 DEBUG로 두고 배포한 죄책감 새벽 3시의 슬랙 메시지 금요일 저녁 7시. 배포 완료. "수고하셨습니다~" 단톡에 올리고 퇴근. 치킨 시켜놓고 맥주 땄다. 새벽 3시 20분. 슬랙 알림. "서버 응답 속도 왜 이래요?" 인프라팀 최대리다. 심장이 멎었다. 노트북 켰다. 손이 떨렸다.로그 파일 30GB VPN 접속. 서버 들어갔다. 로그 파일 확인했다. -rw-r--r-- 1 app app 30G Jan 17 03:15 application.log30기가. 배포 전엔 200메가였다. 4시간 만에 150배. tail 명령어 쳤다. 화면이 미친 듯이 흘렀다. 2025-01-17 03:15:23.441 DEBUG [http-nio-8080-exec-234] - Request received: GET /api/users 2025-01-17 03:15:23.442 DEBUG [http-nio-8080-exec-234] - Parsing request parameters... 2025-01-17 03:15:23.443 DEBUG [http-nio-8080-exec-234] - DB connection pool size: 15 2025-01-17 03:15:23.444 DEBUG [http-nio-8080-exec-234] - Executing query: SELECT * FROM users WHERE...1초에 수백 줄. 모든 API 호출마다. 모든 DB 쿼리마다. 모든 Redis 접근마다. 다 찍히고 있었다. application.yml 열었다. logging: level: root: DEBUG com.company: DEBUG내가 그제 고친 거다. 로컬에서 버그 잡으려고. 그리고 까먹었다.디스크 사용률 97% 더 큰 문제가 있었다. 그라파나 대시보드 확인. 디스크 사용률: 97%. 1시간 전만 해도 65%였다. 로그 때문에 디스크가 차고 있었다. 100% 되면 서버가 죽는다. 쓰기 작업 전부 실패한다. 오전 9시까지 4시간. 이 속도면 2시간 안에 100%. 손가락 떨면서 타이핑. sudo rm -rf /var/log/app/application.log.* sudo truncate -s 0 /var/log/app/application.log구형 로그 지웠다. 현재 로그도 비웠다. 5분 뒤. 다시 1.2GB. yml 수정했다. logging: level: root: INFO com.company: WARN긴급 배포. Jenkins 띄우고 빌드. 12분 걸렸다. 평소엔 8분인데. 배포 완료. 서버 재시작. 로그 속도 정상화. 디스크 사용률 72%에서 멈췄다. 새벽 4시 10분. 50분 걸렸다.월요일 아침 회의 주말 내내 불안했다. 로그 모니터링만 10번 확인. 다행히 문제 없었다. 월요일 9시. 주간 회의. 팀장이 물었다. "금요일 배포 후 이슈 있었나요?" 나: "아... 로그 레벨 설정 실수가 있었습니다." 인프라팀 최대리가 옆에 있었다. 눈빛이 날카로웠다. 팀장: "어떤 실수?" 나: "DEBUG로 남겨서 로그가 폭발했습니다. 디스크 97%까지 갔었고요." 회의실이 조용해졌다. 팀장: "언제 알았어요?" 나: "토요일 새벽 3시요." 팀장: "고객 영향은?" 나: "없었습니다. 응답 속도 약간 느렸지만." 거짓말이었다. Latency가 평소 200ms에서 800ms까지 튀었다. 다만 timeout은 안 났다. 팀장: "재발 방지는?" 나: "배포 체크리스트에 추가하겠습니다." 인프라팀 최대리: "로그 볼륨 알림도 설정해야죠." 나: "예, 그것도 하겠습니다." 회의 끝나고. 팀장이 따로 불렀다. "다음부턴 미리 말해." 그게 다였다. 혼은 안 났다. 더 무서웠다. 체크리스트 추가 그날 오후. 배포 문서를 수정했다. 배포 전 필수 체크리스트:application.yml 로그 레벨 확인 (INFO/WARN) 프로파일 설정 확인 (prod) DB 커넥션 URL 확인 Redis 엔드포인트 확인 외부 API 키 확인빨간색으로 강조했다. "로그 레벨 반드시 INFO 이하" Jenkins에 훅도 추가했다. 빌드 전에 yml 파싱해서. DEBUG 있으면 빌드 실패. stage('Check Log Level') { steps { script { def config = readYaml file: 'src/main/resources/application.yml' if (config.logging.level.root == 'DEBUG') { error('로그 레벨이 DEBUG입니다. INFO로 변경하세요.') } } } }후배 김주니어가 물었다. "형, 이거 너무 빡빡한 거 아니에요?" 나: "실수 한 번 하면 알게 돼." 김주니어는 이해 못 하는 표정. 2년 차는 아직 모른다. 프로덕션 장애의 공포를. 로그의 무게 개발할 땐 로그가 친구다. println 박고. System.out 찍고. log.debug() 남기고. "여기 들어왔나?" "이 값이 뭐지?" "쿼리 제대로 나가나?" 로그 없으면 디버깅 못 한다. IntelliJ 디버거 써도. 결국 로그를 본다. 그런데 프로덕션은 다르다. 로그 한 줄이 디스크를 먹는다. 초당 천 건 API면. DEBUG로 10줄씩 찍으면. 초당 만 줄. 분당 60만 줄. 시간당 3600만 줄. 하루면 8억 줄이 넘는다. 그게 전부 디스크에 쌓인다. 파일 시스템이 버틴다. 로그 수집기가 버틴다. 엘라스틱서치가 버틴다. 근데 비용이 나간다. S3 저장 비용. 클라우드워치 비용. 엘라스틱서치 인덱싱 비용. 회사 돈이다. 그리고 성능이 떨어진다. 파일 I/O는 느리다. 로그 찍는 동안 스레드가 멈춘다. async logger 써도. 버퍼가 차면 대기한다. 사용자는 기다린다. 로그 한 줄의 무게. 신입 때는 몰랐다. 7년 차가 되니 안다. 그래도 실수는 반복된다 이번이 처음은 아니다. 3년 전에는. 하드코딩한 테스트 계정. 프로덕션에 그대로 배포. 2년 전에는. Redis 키 prefix 빼먹어서. dev 데이터랑 prod 데이터 섞임. 1년 전에는. SQL 쿼리에 limit 빠뜨려서. 10만 건 full scan. 매번 다짐한다. "다시는 안 그러겠다." 근데 또 한다. 사람이라서. 피곤해서. 급해서. 시스템으로 막는다. 체크리스트. 자동화. 코드 리뷰. 그래도 빠져나간다. 완벽한 시스템은 없다. 결국 새벽에 슬랙 온다. 심장 떨린다. 노트북 켠다. 7년 차도 이런데. 15년 차는 어떨까. 익숙해질까. 아니면 더 조심해질까. 금요일 배포의 저주 왜 하필 금요일이었을까. 금요일 배포는 저주다. 업계 불문율. "금요일엔 배포하지 마라." 근데 우리 회사는 한다. 스프린트가 2주. 매주 금요일이 배포일. 그래서 매주 금요일. 6시 퇴근이 9시가 된다. 배포하고. 모니터링하고. 괜찮으면 퇴근. 근데 이번엔 방심했다. 배포 후 30분만 보고. "괜찮네" 하고 나왔다. 문제는 4시간 뒤에 터졌다. 로그가 쌓이는 건 느리다. 처음엔 티가 안 난다. 새벽에 터진다. 트래픽 적을 때. 다들 자고 있을 때. 월요일 배포였으면. 당일에 발견했을 거다. 사람 많을 때. 금요일 배포의 저주. 맞는 말이었다. 인프라팀의 차가운 시선 월요일 점심. 구내식당에서 최대리 만났다. 나: "저번에 죄송했습니다." 최대리: "괜찮아요. 빨리 조치했잖아요." 말투는 괜찮은데. 표정이 안 괜찮았다. 인프라팀 입장에선. 개발팀이 원수다. 우리가 코드 잘못 짜면. 서버가 죽는다. 그럼 인프라팀이 깬다. 메모리 릭. CPU 100%. 디스크 풀. 네트워크 타임아웃. 전부 개발자 실수다. 인프라팀이 수습한다. 그래서 우리를 못 믿는다. 배포할 때마다 긴장한다. "또 뭐 터질까." 이번 건으로. 신뢰가 더 깎였다. 다음 배포 때. 더 까다롭게 체크할 거다. "로그 레벨 확인했어요?" "진짜 확인했어요?" "yml 파일 보여주세요." 귀찮아진다. 근데 내 잘못이다. 신뢰는 쌓기 어렵고. 무너지긴 쉽다. 로그 레벨의 철학 INFO는 뭐고. WARN은 뭐고. ERROR는 뭔가. 신입 때는 몰랐다. 그냥 아무거나 썼다. log.info("유저 정보 조회"); log.warn("유저가 없음"); log.error("DB 연결 실패");다 틀렸다. INFO는 비즈니스 이벤트다. "주문 생성됨" "결제 완료" "회원가입 성공" 운영에 필요한 정보. 흐름 파악용. WARN은 비정상인데 처리 가능. "재시도 했음" "캐시 미스" "외부 API 느림" 신경 쓸 건데 급하진 않음. ERROR는 장애다. "DB 죽음" "필수 API 실패" "데이터 정합성 깨짐" 당장 처리해야 함. 슬랙 알림 가는 레벨. DEBUG는 개발용이다. 프로덕션엔 절대 안 씀. TRACE는 더 심하다. 라이브러리 내부 로직. 쓸 일 없다. 이걸 7년 만에 깨달았다. 늦었다. 다른 사람들의 실수 우리 팀만 그런 게 아니다. 작년에 A사. 환경변수 잘못 설정. 개발 DB를 프로덕션으로 착각. 실서비스가 테스트 데이터로 돌아감. B사는. 배포 스크립트 오타. 서버 전체 재시작 대신. 서버 전체 삭제. 백업으로 복구하는데 4시간. C사는. 캐시 TTL 설정 실수. 1시간이 1초로. Redis 트래픽 폭발. AWS 비용 하루에 300만원. 다들 실수한다. 베테랑도. 대기업도. 카카오 불 났을 때. 데이터센터 하나 죽었다. 백업 센터 자동 전환 실패. 카톡 5시간 먹통. 페이스북은. 잘못된 설정 배포로. 글로벌 서비스 6시간 다운. 아마존은. 오타 하나로. S3 전체 다운. 인터넷 절반이 멈췄다. 우리가 약한 게 아니다. 모두가 약하다. 다만 시스템을 개선해. 같은 실수 반복을 막는다. 지금의 대책 일주일 지났다. 추가한 것들.Jenkins 빌드 훅 - 로그 레벨 자동 체크 Grafana 알림 - 디스크 80% 넘으면 슬랙 로그 볼륨 모니터링 - 시간당 1GB 넘으면 경고 PR 템플릿 수정 - 로그 레벨 확인 항목 추가 배포 문서 업데이트 - 로그 관련 섹션 강화김주니어한테 교육했다. "로그는 비용이다." "프로덕션은 전쟁터다." "금요일 배포는 조심해라." 주니어는 고개 끄덕였다. 근데 이해 못 한 것 같았다. 결국 직접 겪어야 안다. 언젠가 김주니어도. 새벽 3시에 깰 거다. 심장 떨리며 노트북 켤 거다. 그게 성장이다. 아프지만. 로그가 가르쳐준 것 7년 동안 배운 것. 코드는 혼자 안 돌아간다. 서버 위에서 돈다. 네트워크 타고 간다. 데이터베이스랑 얘기한다. 그 모든 게 비용이다. CPU. 메모리. 디스크. 네트워크. 로그도 비용이다. 공짜가 아니다. "일단 찍어보자"가 습관이었다. 이제는 "꼭 필요한가"를 묻는다. DEBUG 로그 남길 때. "프로덕션에서 보겠어?" 아니면 지운다. INFO 로그 남길 때. "운영에 필요한가?" 아니면 DEBUG로. WARN 로그 남길 때. "조치 필요한가?" 아니면 INFO로. ERROR 로그 남길 때. "당장 처리해야 하나?" 그럼 슬랙 알림도. 로그 한 줄을 신중하게. 프로덕션을 존중하게. 여전히 불안한 금요일 오늘도 금요일. 배포일이다. 어제 PR 3개 머지했다. 오늘 오후 6시 배포 예정. 벌써 불안하다. 점심도 안 넘어간다. 체크리스트 10번 확인. 로그 레벨 INFO 맞음. 환경변수 prod 맞음. DB URL 맞음. 그래도 불안하다. 뭐 빠뜨린 것 같다. 김주니어가 물었다. "형, 왜 이렇게 예민해요?" 나: "작년에 사고 쳐봐서." 김주니어: "큰 사고였어요?" 나: "새벽 3시에 깼어." 김주니어: "아..." 이제 알 것 같다는 표정. 커피 세 번째. 손 떨린다. 카페인 때문인지. 긴장 때문인지. 6시간 남았다.금요일 배포는 여전히 무섭다. 7년 차도 그렇다.

프로젝트 데드라인 일주일 앞두고 하는 생각들

프로젝트 데드라인 일주일 앞두고 하는 생각들

프로젝트 데드라인 일주일 앞두고 하는 생각들 D-7 월요일 아침. 슬랙 알림이 울렸다. "다들 일정 괜찮으시죠? 다음 주 금요일 배포 확정입니다~" 괜찮을 리가 없다. 하지만 "네~"라고 답했다. 다들 그렇게 답했다. 지라 보드를 켰다. 'To Do' 컬럼에 티켓 12개. 'In Progress' 4개. 'Done'은 딱 3개. 계산기 두드릴 필요도 없다. 안 된다. 하지만 이상하게 여유롭다. 일주일이나 남았으니까. 7일이면 168시간이다. 자고 먹는 시간 빼도 70시간은 된다. 될 것 같다. 아니, 되게 할 수 있다. 점심은 김치찌개였다. 사장님이 "요즘 야근 많아요?"라고 물었다. "곧 많아질 것 같아요"라고 답했다. 오후 3시. 후배가 물었다. "형, 이거 API 응답 구조 이렇게 해도 돼요?" 코드를 봤다. 안 된다. 다시 설명했다. 30분 갔다. 오후 5시. 기획자가 슬랙 DM을 보냈다. "개발님, 이 기능 UX 좀 바꾸면 안 될까요? 간단한 건데..." 간단할 리가 없다. 하지만 일단 "검토해보겠습니다"라고 답했다. 저녁 먹고 생각하자. 퇴근길. 편의점에서 레드불 2캔을 샀다. 아직 마실 건 아니다. 그냥 보험이다.D-5 수요일. 이제 좀 현실이 보인다. 어제 'Done' 티켓 2개 추가했다. 총 5개. 남은 건 14개. 어? 늘었네. 기획자가 어제 "작은 기능" 3개를 추가했다. 작을 리가 없다. 오전 회의. 프로젝트 매니저가 물었다. "일정 괜찮으신가요?" 다들 고개를 끄덕였다. 나도 끄덕였다. 거짓말이다. 점심 먹으면서 후배한테 말했다. "이번 주 금요일까지 네 거 다 끝내야 돼. 다음 주는 통합 테스트하고 버그 잡는 주간이야." 후배가 물었다. "형, 솔직히 가능해요?" "글쎄." 오후 2시. 코드 리뷰 요청 5건. 다 봐야 한다. 내 일은? 저녁에. 오후 6시. 팀원 다섯 명 중 아무도 퇴근 안 했다. 말은 안 하지만 다들 안다. 이번 주는 그런 주다. 저녁 8시. 치킨 시켜먹었다. 회사 카드로. PM이 쏜다고 했다. 고마운데, 차라리 일정을 현실적으로 잡아줬으면. 밤 11시. 집에 가는 버스 안. 아내한테 카톡 보냈다. "나 이번 주 좀 늦을 것 같아." "알았어. 밥은 챙겨먹고." 고맙다. 근데 미안하다.D-3 금요일 밤 10시. 사무실에 우리 팀만 남았다. 'Done' 티켓 9개. 남은 거 10개. 이론상으론 절반. 실제론 70%는 남았다. 어려운 것들만 남았으니까. 옆자리 후배가 한숨 쉰다. "형, 이거 안 되는 거 같은데요." 코드 봤다. 안 된다. 설계를 다시 해야 한다. "일단 퇴근해. 주말에 내가 볼게." "형이요?" "응. 어차피 잠도 안 올 거 같아." 거짓말 아니다. 데드라인 앞두면 항상 그렇다. 자려고 누워도 머릿속에서 코드가 돈다. 'NullPointerException'이 꿈에 나온다. 밤 12시. 편의점 갔다. 레드불 하나 땄다. 삼각김밥 2개. 바나나우유. 계산대 알바생이 물었다. "야근이세요?" "네." "힘내세요." 고맙다. 근데 안 힘내도 해야 한다. 새벽 2시. 커밋 푸시했다. 일단 오늘은 여기까지. 내일 다시. 택시 탔다. 기사님이 물었다. "회사원이세요?" "개발자요." "아, 요즘 그거 힘들다며요." 맞다. 힘들다.D-1 목요일 저녁 7시. 내일이 배포일이다. 'Done' 티켓 17개. 남은 거 2개. 보기엔 거의 다 한 것 같다. 실제론 지옥의 문 앞이다. QA팀에서 버그 리포트가 올라오기 시작했다. 지금까지 8건. "치명적" 등급 3개. 해야 한다. 오후 8시. 긴급 회의. PM, 개발팀, QA팀 전원 참석. "최악의 경우 배포 연기할 수도..." 누가 말했다. 다들 침묵했다. 연기하면? 이 지옥이 일주일 더 연장된다. 그리고 다음 프로젝트 일정도 밀린다. 그건 더 지옥이다. "일단 해봅시다." 내가 말했다. 다들 고개 끄덕였다. 밤 11시. 아내한테 전화했다. "나 오늘 집 못 갈 것 같아." "...응. 조심해." 끊고 나서 죄책감. 하지만 지금은 생각할 시간이 없다. 새벽 4시. 버그 3개 고쳤다. 5개 남았다. 눈이 감긴다. 커피 한 잔 더. 새벽 6시. 동료가 말했다. "형, 일단 좀 자요. 9시에 다시 시작하죠." "그래야겠다." 회의실 소파에 누웠다. 알람 8시 30분. 30분 뒤엔 샤워하러 헬스장 갈 거다. 회사 옆 24시간 헬스장. 이럴 때 쓰라고 끊어놨다. 잠들기 전에 생각했다. '이번에 끝나면 이직 준비 시작하자.' 근데 매번 그렇게 생각한다. 그리고 까먹는다. 다음 프로젝트 시작하면. D-Day 금요일. 배포일. 아침 9시. 샤워하고 왔다. 편의점 김밥으로 아침. 모두 제자리에 앉았다. "시작하죠." 최종 점검. 코드 리뷰. 테스트. 다시 테스트. QA 확인. PM 확인. 그리고 기다림. 오후 2시. 배포 시작. 손이 떨린다. 항상 그렇다. 몇 년째 해도 긴장된다. 30분 후. "배포 완료되었습니다." 모니터링. 에러 로그 체크. API 응답 속도. DB 쿼리. 메모리 사용량. 모든 게 정상. 오후 3시. PM이 말했다. "다들 고생하셨습니다." 박수 소리. 피곤한 웃음들. 안도감. 그리고 허무함. "오늘 저녁 회식할까요?" "저 그냥 집 가서 자고 싶은데요." "그럼 내일 점심 제가 쏠게요." "그게 낫겠네요." 오후 5시. 퇴근했다. 정시 퇴근. 일주일 만이다. 버스 안. 창밖을 봤다. 사람들이 웃으며 걷는다. 저녁 약속이라도 있나보다. 부럽다. 집 도착. 문 열었다. 아내가 웃으며 맞이했다. "고생했어." 소파에 앉았다. TV 켰다. 넷플릭스. 아무거나 틀었다. 5분 만에 잠들었다. 그리고 토요일 아침. 늦잠 잤다. 12시에 일어났다. 휴대폰 봤다. 슬랙 알림 37개. 겁났다. 뭐가 터졌나. 천천히 확인했다. 다행히 버그 아니다. 다음 프로젝트 얘기다. "다음 주 월요일 킥오프 미팅 있습니다~" 한숨 나왔다. 또 시작이다. 아내가 물었다. "뭐해? 밥 먹어야지." "응." 점심 먹으면서 생각했다. 이번엔 진짜 이직 준비 해볼까. 근데 귀찮다. 이력서 쓰는 것도 일이다. 면접 준비도 일이다. 그냥 여기서 버티는 게 편하다. 오후. 소파에 누워서 유튜브 봤다. "개발자 이직 브이로그" 영상. 신기하다. 저 사람들은 어떻게 저 에너지가 있을까. 저녁. 치킨 시켰다. 아내랑 둘이 먹었다. 맥주 한 캔. 피곤해서 맛도 모르겠다. 밤. 침대에 누웠다. 천장을 봤다. '이번 프로젝트도 이럴까?' 알고 있다. 이럴 거다. 항상 그랬으니까. 계획은 항상 좋다. 처음엔 여유롭다. 중간에 뭔가 추가된다. 막판에 죽어난다. 배포하고 안도한다. 그리고 반복한다. 7년째 이 일 하는데, 아직도 적응 안 된다. 적응하면 안 되는 건가. 적응했다는 건 포기했다는 거니까. 눈 감았다. 내일 또 생각하자. 아니, 모레. 일요일은 쉬어야지. 근데 월요일 킥오프 미팅 자료 준비는 해야 하나. 일요일 저녁에 할까. 또 그렇게 된다. 항상 그렇다.다음 주 월요일, 또 '될 것 같은' 착각으로 시작할 거다. 그게 개발자다.

회의 시간에 본 프로덕션 에러 로그의 공포

회의 시간에 본 프로덕션 에러 로그의 공포

회의실에서 울린 경보음 회의 시작 10분 전. 커피 들고 회의실 들어갔다. 기획자 둘, PM 한 명, 나. 분기 계획 회의래. 한 시간 예정. 노트북 펼쳤다. 슬랙 꺼놨다. 방해 금지 모드. 그래야 집중한다고 핑계 댄다. 사실 회의 중에 코드 보려고. 근데 핸드폰은 안 끈다. 혹시 몰라서. PM이 화면 공유 시작했다. 로드맵 어쩌고. 귀에 안 들어온다. 고개는 끄덕이는데 손은 펜 돌리고 있다. 15분 지났을 때. 핸드폰이 진동했다. 테이블 밑에서. 심장이 한 박자 뛴다. 이 시간에 알림이면 둘 중 하나다. 인사팀 공지 아니면 장애. 테이블 밑으로 슬쩍 핸드폰 꺼냈다. 화면 봤다. Sentry: [Production] NullPointerException 좆됐다.로그를 보는 순간 화면 밝기 최소로 줄였다. 슬랙 열었다. 알림 세 개. 아니, 다섯 개. 새로고침하니까 일곱 개. [ERROR] UserService.getProfile() - NPE at line 247 [ERROR] UserService.getProfile() - NPE at line 247 [ERROR] UserService.getProfile() - NPE at line 247같은 에러. 연속으로. 1분 사이에 23건. UserService. 내가 짠 코드다. 지난주 배포한 거. PM이 말하고 있다. "이번 분기 목표는 MAU 20% 증가인데요..." 한 귀로 듣고 한 귀로 흘린다. 눈은 핸드폰. 스크롤 내린다. 에러 로그 펼쳤다. 스택 트레이스 본다. at com.company.service.UserService.getProfile(UserService.java:247) at com.company.controller.UserController.getUser(UserController.java:89)247번 줄. 어디였지. 기억 안 난다. 떠올려본다. 아. 그거. 캐시 체크하는 부분. Redis에서 유저 정보 가져오는 로직. 근데 왜 NPE가. 널 체크 분명 했는데. 아니다. 안 했다. 그냥 redisTemplate.opsForValue().get(key).getName() 이렇게 박았다. 왜 그랬지. 리뷰도 통과했는데. 아 맞다. 그날 금요일이었다. 다들 빨리 퇴근하고 싶어 했다. 심장이 빨라진다. 손에 땀 난다.회의는 계속된다 "개발님 어떻게 생각하세요?" "네? 아 네." 뭘 물어봤는지 모른다. 그냥 긍정했다. 기획자가 웃는다. "그럼 이 기능 이번 달 안에 가능하다는 거죠?" 좆됐다. 뭐라고 대답해야 하지. "일단... 검토해보고 말씀드리겠습니다." 만능 답변. 검토해보겠습니다. 핸드폰 다시 본다. 알림 더 왔다. 12개. 20개. 숫자가 올라간다. Sentry 대시보드 연다. 에러율 그래프 본다. 빨간 선이 수직 상승. 2:47PM부터. 지금 3:02PM. 15분간 148건. 유저 몇 명이나 영향받았지. 대시보드 스크롤. Affected Users: 37. 37명. 아직 괜찮다. 아니, 괜찮지 않다. 37명이면 엄청 많은 거다. 검색해본다. "redis get null check java". 스택오버플로우. String value = redisTemplate.opsForValue().get(key); if (value != null) { return value.getName(); }맞다. 이렇게 했어야 했다. 병신같이 왜 안 했지. PM이 또 말한다. "그럼 다음 안건으로 넘어가겠습니다." 넘어가. 빨리. 나가게 해줘. 참을 수 없는 20분 노트북 열었다. 척 코드 보는 척. 사실 핫픽스 생각 중. UserService.java 파일 기억해낸다. 어떻게 고쳐야 하지. 간단하다. 널 체크 추가. if 문 하나. 3줄이면 끝. 근데 배포가 문제다. PR 올리고, 리뷰 받고, 머지하고, 빌드하고, 배포. 최소 30분. 급하면 15분. 근데 지금 회의 중. 회의는 아직 30분 남았다. 로드맵 다 안 끝났다. 핸드폰 진동. 또. DM 왔다. 운영팀. "개발님 유저 문의 들어오는데요, 프로필 조회가 안 된다고..." 답장 친다. "확인 중입니다." 확인 중. 맞다. 확인 중이다. 근데 고칠 수는 없다. 회의 중이니까. 손가락이 떨린다. 펜 더 빠르게 돌린다. 기획자가 화면 넘긴다. "다음 기능은 소셜 로그인인데요..." 소셜 로그인. 듣고 싶지 않다. 나가고 싶다. 노트북 들고 나가서 배포하고 싶다. 근데 나갈 수 없다. 이유가 필요하다. "화장실 갑니다" 하고 30분 안 들어오면 이상하다. 그냥 참는다. 20분만. 20분만 버티면 된다.회의 끝, 그리고 전쟁 시작 3:28PM. PM이 말한다. "오늘은 여기까지 하겠습니다." 노트북 닫는다. 아니, 안 닫는다. 들고 간다. "먼저 가보겠습니다." 뛰다시피 나간다. 자리 앉는다. 모니터 켠다. IntelliJ 연다. UserService.java 찾는다. 247번 줄. // 기존 코드 User user = redisTemplate.opsForValue().get(cacheKey); return user.getName(); // 여기서 NPE고친다. // 수정 코드 User user = redisTemplate.opsForValue().get(cacheKey); if (user == null) { return fetchFromDB(userId); // DB에서 다시 조회 } return user.getName();3분 걸렸다. 커밋한다. "[HOTFIX] Add null check for Redis cache". 푸시. PR 올린다. 제목 "[HOTFIX] Fix NPE in UserService". 설명 대충 쓴다. "Redis null 체크 누락". 슬랙에 쓴다. "#dev 채널 @channel 핫픽스 PR 올렸습니다. 리뷰 부탁드립니다." 시니어 개발자가 답한다. "확인했습니다. Approve." 머지한다. Jenkins 빌드 시작. 2분 걸린다. 화면 본다. 초록불. 배포 스크립트 실행. ./deploy.sh production. 엔터. Deploying to production... Building Docker image... Pushing to registry... Rolling update started...5분 기다린다. 핸드폰 본다. 손톱 물어뜯는다. Deployment successful Pods: 4/4 running완료. Sentry 새로고침한다. 에러 그래프 본다. 빨간 선이 멈췄다. 3:41PM부터 0건. 숨 쉰다. 제대로. 총 에러: 289건. 영향받은 유저: 73명. 운영팀한테 DM. "배포 완료했습니다. 확인 부탁드립니다." "넵 확인했어요! 감사합니다~" 자리에 앉아있다. 심장 진정된다. 손 떨림 멈춘다. 커피 마신다. 식었다. 그래도 마신다. 그날 저녁 퇴근길 지하철. 핸드폰 본다. Sentry. 에러 0건. 계속 0건. 집 도착. 현관문 열었다. 신발 벗는다. 아내가 묻는다. "오늘 어땠어?" "그냥... 회의 있었어." "힘들었어?" "응. 좀." 저녁 먹는다. 치킨. 시켜먹었다. 배달의민족. 넷플릭스 켠다. 아무거나 튼다. 화면 안 본다. 천장 본다. 오늘 배운 거. 회의 전엔 배포하지 말 것. 금요일엔 더더욱. 그리고 널 체크. 무조건 널 체크. Redis든 뭐든 외부에서 받은 건 다 의심. 믿지 말 것. IDE 켠다. 노트북. 개인 프로젝트 아니고 회사 코드. 검색한다. "redisTemplate.opsForValue().get". 프로젝트 전체. 결과 17개. 하나씩 연다. 널 체크 있는지 본다. 7개 없다. 다 고친다. 커밋한다. PR 올린다. 제목: "[REFACTOR] Add null checks for Redis operations". 내일 아침에 리뷰 요청하면 된다. 지금은 잔다. 근데 잠이 안 온다. 눈 감으면 빨간 알림이 보인다. [Production] NullPointerException 한숨 쉰다. 내일도 출근이다.회의 중 프로덕션 에러는 개발자의 심장을 가장 빠르게 뛰게 만드는 알림이다. 그리고 가장 확실하게 널 체크의 중요성을 각인시킨다.

에러 메시지 'null pointer exception': 다시는 싫다

에러 메시지 'null pointer exception': 다시는 싫다

에러 메시지 'null pointer exception': 다시는 싫다 오전 10시 32분 커피 마시다가 슬랙 알림이 울렸다. QA팀이다. "개발님, 회원가입 안 돼요." 손이 떨렸다. 어제 배포한 거다. 30분 전에 올라갔다. 벌써 터졌다. 로그를 켰다. 빨간 글씨가 가득하다. java.lang.NullPointerException at com.company.user.UserService.register(UserService.java:247)또 null이다. 247번째 줄이다. 코드를 열었다. String email = userDto.getEmail().toLowerCase();getEmail()이 null을 뱉었다. 당연히 toLowerCase()에서 터진다. "아..." 한숨이 나왔다. 어제 급하게 짠 코드다. null 체크를 안 했다. 3년 전 신입도 아니고, 7년 차가 이러면 안 되는데. 핫픽스 브랜치를 땄다. 손가락이 자동으로 움직인다. if (userDto.getEmail() == null) { throw new IllegalArgumentException("이메일은 필수입니다"); }5분 컷이다. 커밋, 푸시, PR, 머지. 배포는 10분. QA팀에 "해결했습니다" 보냈다. "감사합니다^^" 답장이 왔다. 고맙진 않다. 부끄럽다.점심시간, 김치찌개집 사장님이 물었다. "오늘 표정 안 좋네요?" "일이 좀..." "개발자는 힘들어요. 우리 아들도 그래요." 김치찌개를 떴다. 뜨거웠다. 혀가 데었다. null pointer exception. NPE. 개발자라면 누구나 안다. 제일 흔한 에러다. 제일 짜증 나는 에러다. 왜 짜증 나냐면, 내 잘못이기 때문이다. 문법 에러는 컴파일러가 잡아준다. 네트워크 에러는 인프라 탓을 할 수 있다. DB 에러는... 뭐, DBA한테 물어보면 된다. 근데 NPE는 다르다. 내가 null을 만들었다. 내가 null 체크를 안 했다. 내가 실수했다. 그래서 더 화난다. 나한테. 후배 민수가 작년에 물었다. "형, null 체크 매번 하는 게 맞아요? 코드가 너무 길어져요." "해야지. 안 하면 터져." "Optional 쓰면 안 돼요?" "그것도 결국 체크는 해야 해. 다른 방식일 뿐이지." 민수는 고개를 끄덕였다. 그러고 2주 뒤에 NPE로 배포 롤백했다. 내가 리뷰할 때 못 봤다. 둘 다 잘못이다.오후 3시, 회의실 장애 회고다. 팀장이 물었다. "원인이 뭐였어요?" "null 체크 누락이었습니다." "왜 누락됐죠?" 대답이 안 나왔다. '급했어요'라고 할 순 없다. 그건 핑계다. "제가 놓쳤습니다." 팀장이 한숨을 쉬었다. "다음부턴 조심하죠." 회의가 끝났다. 자리로 돌아왔다. 모니터를 봤다. 내 코드에 null 체크가 몇 개나 있을까. 세어봤다. 파일 하나에 12개다. if (user == null) return; if (user.getName() == null) return; if (request == null) throw new Exception(); if (request.getBody() == null) throw new Exception();지겹다. 정말 지겹다. 코틀린으로 짜는 팀이 부럽다. Null Safety가 언어 레벨에서 된다. 자바는 안 된다. Optional이 있긴 한데, 레거시 코드는 다 null 투성이다. "리팩토링 해야 하는데..." 혼잣말이 나왔다. 옆자리 수진이 물었다. "뭘요?" "아니, 아무것도." 리팩토링할 시간이 어딨나. 기획자는 매주 새 기능을 요구한다. PM은 일정을 당긴다. 기술 부채는 쌓인다. 그래서 또 null 체크를 빼먹는다. 그래서 또 터진다. 악순환이다.퇴근 직전, 6시 10분 민수가 PR을 올렸다. 리뷰 요청이 왔다. 코드를 열었다. 200줄짜리 서비스 로직이다. 쭉 읽었다. 50번째 줄에서 멈췄다. Payment payment = paymentRepository.findById(paymentId); payment.setStatus("COMPLETE");findById()는 null을 리턴할 수 있다. 민수는 체크 안 했다. 코멘트를 달았다. "findById null 체크 필요합니다." 5분 뒤에 민수가 수정했다. Payment payment = paymentRepository.findById(paymentId); if (payment == null) { throw new NotFoundException("결제 정보가 없습니다"); } payment.setStatus("COMPLETE");Approve 눌렀다. 그러고 내 코드를 열었다. 어제 짠 거다. 다시 읽었다. 3군데에서 null 체크가 없었다. 아직 배포 안 된 코드다. 고쳤다. "휴..." 7년 차다. 아직도 이런다. 언제까지 이럴 건가. 검색창에 쳤다. "how to avoid null pointer exception java" 스택오버플로우가 떴다. 똑같은 질문이 1만 개다. 똑같은 대답이다.null 체크 하세요 Optional 쓰세요 애초에 null 반환하지 마세요다 안다. 다 해봤다. 그래도 터진다. 왜냐면 사람은 실수하니까. 피곤하면 놓친다. 급하면 까먹는다. 리뷰어도 사람이라 못 본다. 결국 답은 하나다. 조심하는 수밖에. 저녁 9시, 집 아내가 물었다. "오늘 힘들었어?" "응. 장애 났었어." "또? 지난주에도 그랬잖아." "응... 내 실수야." 아내가 맥주를 건넸다. 땄다. 마셨다. NPE 이야기를 했다. 아내는 디자이너라 잘 모른다. 그래도 들어줬다. "그게 그렇게 자주 나는 거야?" "응. 진짜 많이 나." "그럼 자동으로 체크하는 프로그램 같은 거 없어?" "있어. SonarQube, SpotBugs... 다 있어. 근데 완벽하진 않아. 못 잡는 케이스도 많고." "흠..." 아내는 더 안 물었다. 나도 더 안 말했다. 맥주를 다 마셨다. 냉장고를 열었다. 한 캔 더 꺼냈다. 노트북을 켰다. 회사 코드를 열었다. 출퇴근 기록부는 6시 퇴근이다. 근데 지금 코드를 본다. UserService.java를 열었다. 쭉 읽었다. null 체크를 추가했다. 5군데. 커밋 메시지를 썼다. "add null checks for safety" 푸시했다. PR 올렸다. 내일 아침에 머지하면 된다. 시계를 봤다. 10시 반이다. "또 일해?" 아내 목소리다. 돌아봤다. 문 앞에 서 있다. "응... 좀." "그만 해. 내일 하면 안 돼?" "응. 내일 해야지." 노트북을 덮었다. 근데 머릿속은 아직 코드다. 247번째 줄. getEmail(). toLowerCase(). null. 또 떠올랐다. 자기 전까지 계속 떠오를 것이다. 그래도 해야 한다 NPE는 없앨 수 없다. 완벽한 코드는 없다. 사람은 실수한다. 근데 줄일 순 있다. null 체크를 습관화한다. PR 리뷰 때 집중한다. 단위 테스트를 꼼꼼히 짠다. 로그를 잘 남긴다. 그렇게 하면 조금 덜 터진다. 조금 덜 야근한다. 조금 덜 부끄럽다. 7년 차 개발자의 결론이다. "null은 적이다. 근데 없앨 순 없다. 그러니까 경계해야 한다." 이게 끝이다. 내일도 출근한다. 내일도 코드 짠다. 내일도 null 체크한다. 그러다 보면 또 놓친다. 또 터진다. 그럼 또 고친다. 또 배운다. 이게 개발자다.오늘도 null 체크 30개 추가. 내일은 40개.

Java 메모리 누수: 10시간을 날리다

Java 메모리 누수: 10시간을 날리다

Java 메모리 누수: 10시간을 날리다 금요일 오후 3시 슬랙이 울렸다. 운영팀이다. "개발님, 프로덕션 서버 메모리 사용량이 계속 올라가요." 금요일 오후 3시. 퇴근까지 3시간. 배포한 지 일주일 된 기능이다. "재시작하면 괜찮아지나요?" "네, 근데 2일 정도 지나면 다시 80% 넘어가요." 메모리 누수다. 확신했다.일단 재시작 "일단 재시작 한 번 해주세요. 제가 로그 확인해볼게요." 재시작했다. 메모리 사용량이 정상으로 돌아왔다. 시간을 벌었다. 하지만 원인을 찾아야 한다. 일주일 전 배포 내역을 확인했다. 커밋 6개. 파일 23개 변경. "이거 하나하나 다 봐야 하나." 커피를 마셨다. 네 번째다. 금요일 오후 4시. 퇴근까지 2시간. 로그를 뒤졌다 애플리케이션 로그. 특이사항 없다. GC 로그. 여기서 뭔가 보인다. [Full GC 3.2G->3.1G(4G), 2.3 secs] [Full GC 3.3G->3.2G(4G), 2.5 secs] [Full GC 3.5G->3.4G(4G), 2.8 secs]Full GC가 계속 돌아도 메모리가 안 줄어든다. 전형적인 메모리 누수. "문제는 어디서 새는가지." 힙 덤프를 떠야 한다. 운영 서버에서. "혹시 성능 영향 있을까요?" "잠깐일 거예요. 10초 정도?" 거짓말이다. 3GB 힙 덤프는 30초 걸린다. 하지만 말 안 했다. 금요일 오후 5시. 퇴근까지 1시간.힙 덤프 분석 덤프 파일을 받았다. 3.2GB. Eclipse MAT을 켰다. 로딩하는데 5분 걸렸다. "Leak Suspects Report"를 눌렀다. One instance of "java.util.HashMap" loaded by <system class loader> occupies 2.8 GB (87.5%) bytes.HashMap 하나가 2.8GB를 먹고 있다. "뭐야 이게." 해당 HashMap의 참조를 추적했다. CacheManager -> userSessionCache -> HashMap아, 세션 캐시구나. 일주일 전에 추가한 기능이다. 사용자 세션을 캐시에 담아서 조회 속도를 높이는. 근데 이게 왜 2.8GB? 금요일 오후 6시. 퇴근 시간이다. 아내한테 카톡 보냈다. "야근할 것 같아. 저녁 먼저 먹어." "또? 어제도 늦었잖아." "미안. 메모리 누수 잡아야 해." "무슨 말인지 모르겠지만 힘내." 코드를 봤다 해당 CacheManager 클래스를 열었다. @Service public class CacheManager { private Map<String, UserSession> userSessionCache = new HashMap<>(); public void addSession(String userId, UserSession session) { userSessionCache.put(userId, session); } public UserSession getSession(String userId) { return userSessionCache.get(userId); } }세션을 넣는 로직은 있다. 빼는 로직이 없다. "아..." 사용자가 로그아웃하거나 세션이 만료돼도 캐시에서 안 지워진다. 일주일 동안 쌓인 세션이 2.8GB. DAU 5만. 평균 세션 크기가 대충 60KB라고 치면. 5만 × 7일 × 60KB = 21GB. 아니다. 중복 로그인도 있으니까... 계산이 안 맞는다. 어쨌든 지워야 한다. 금요일 오후 7시. 배가 고프다.해결책을 찾았다 세 가지 옵션.세션 만료 시 캐시에서 제거 TTL 기반 자동 삭제 LRU 캐시로 교체1번은 로그아웃 로직 모두 수정해야 한다. 시간 걸린다. 3번은 라이브러리 교체. 테스트 범위가 크다. 2번이 답이다. Guava Cache로 교체. @Service public class CacheManager { private Cache<String, UserSession> userSessionCache = CacheBuilder.newBuilder() .expireAfterAccess(2, TimeUnit.HOURS) .maximumSize(10000) .build(); public void addSession(String userId, UserSession session) { userSessionCache.put(userId, session); } public UserSession getSession(String userId) { return userSessionCache.getIfPresent(userId); } }2시간 동안 접근 없으면 자동 삭제. 최대 1만 개 유지. 코드 수정 완료. 20분 걸렸다. 금요일 오후 8시. 치킨집 마감 시간이 10시다. 테스트 로컬에서 테스트했다. 세션 1만 개 생성. 메모리 확인. 정상. 2시간 대기는 못 한다. 시간을 1분으로 줄여서 테스트. 1분 후 GC 로그 확인. 메모리 회수됐다. "됐다." 개발 서버에 배포했다. 30분 모니터링. 문제없다. QA팀한테 슬랙 보냈다. "내일 아침에 테스트 부탁드려요. 급한 버그 픽스입니다." "네, 확인했습니다." 금요일 오후 9시. 프로덕션 배포 결정. 프로덕션 배포 배포 준비했다. 운영팀한테 연락했다. "10분 뒤 배포합니다. 모니터링 부탁드려요." "네, 대기하고 있겠습니다." Jenkins 빌드 시작. 3분 걸린다. 빌드 성공. 배포 시작. 서버 1대씩 순차 배포. Blue-Green 방식이다. 10분 후 배포 완료. 헬스체크 확인. 정상. 메모리 사용량 확인. 40%에서 유지. "일단 됐다." 하지만 확신은 못 한다. 2일 지나봐야 안다. 금요일 오후 10시. 치킨집 문 닫았다. 모니터링 집에 가지 않았다. 사무실에서 30분마다 메모리 그래프를 확인했다. 10시 30분. 메모리 42%. 11시. 메모리 43%. 11시 30분. 메모리 44%. "올라가는 거 아냐?" 아니다. 트래픽이 증가하는 시간대다. 정상 범위다. 12시. 메모리 45%. 안정됐다. 아내한테 카톡 보냈다. "이제 들어간다." "벌써 12시야. 택시 타." "응." 택시를 탔다. 요금 2만3천원. 금요일 자정. 10시간이 지났다. 토요일 아침 9시에 눈을 떴다. 제일 먼저 한 일. 폰으로 메모리 그래프 확인. 메모리 48%. 안정적이다. 슬랙 확인. 장애 알림 없다. "살았다." 이불을 다시 덮었다. 오후 2시에 일어났다. 월요일 오전 출근했다. 주말 동안 메모리 사용량을 확인했다. 최대 52%. 더 이상 안 올라갔다. 운영팀이 슬랙을 보냈다. "서버 안정적이에요. 감사합니다." "네." 팀 회의 때 공유했다. "금요일에 메모리 누수 있었는데 해결했습니다." "고생하셨어요." 끝이다. 아무도 자세히 안 물어본다. 10시간 날린 건 나만 안다. 배운 것캐시에는 항상 만료 정책이 있어야 한다. HashMap은 메모리 누수의 주범이다. 코드 리뷰 때 캐시 관련 코드는 집중해서 본다. 금요일 오후 3시 장애는 최악이다.그리고 하나 더. 힙 덤프 분석은 5분 안에 끝난다고 생각했다. 30분 걸렸다. 항상 예상의 6배가 걸린다. 일주일 후 CTO가 이메일을 보냈다. "지난주 메모리 이슈 빠르게 해결해줘서 감사합니다." 답장 안 했다. 감사하면 연봉 올려주든가.메모리 누수는 찾는 데 1시간, 고치는 데 20분이다. 나머지 8시간 40분은 불안이다.

스택오버플로우 없는 개발자는 존재할까

스택오버플로우 없는 개발자는 존재할까

스택오버플로우 없는 개발자는 존재할까 매일 아침 9시에 회사에 도착하면 커피를 한 잔 내려 마신다. 그리고 슬랙을 켠다. 어김없이 누군가는 물어본다. "개발님, 이거 어떻게 하죠?" 나는 그럼 "아 그거요"라고 한다. 그 순간 손가락은 자동으로 움직인다. 구글 탭을 열고, 스택오버플로우에 들어가고, 검색창에 키워드를 친다. 5초, 10초 지나면 누군가는 이미 같은 문제를 겪었고, 2015년에 누군가가 정답을 남겨놨다. 나는 그 링크를 복사해서 슬랙에 붙여넣는다. "여기 봐보세요." 마치 내가 방금 그 코드를 생각해낸 것처럼.개발자의 비밀, 스택오버플로우 7년 개발 경력이라고 하면 사람들은 내가 거의 모든 에러를 처리할 수 있다고 생각한다. 솔직히 말하자면, 틀렸다. 나는 거의 모든 에러를 검색할 수 있다. 차이가 크다. 후배가 처음 입사했을 때 보여준 표정을 아직도 기억한다. 내가 NullPointerException으로 한 시간을 고민하다가 결국 스택오버플로우를 켰을 때, 후배가 물었다. "어? 시니어 분도 검색하세요?" 그 순간의 당혹스러운 감정은 이루 말할 수 없었다. 내 신비로움이 한 순간에 무너져 내렸다. 하지만 생각해보니 이건 수치스러운 게 아니었다. 이건 효율이었다. 프로그래밍이 정말로 유명해진 건 스택오버플로우 같은 플랫폼이 생겨난 후부터다. 그 전에는 어떻게 했을까? 동료에게 물었을까? 책을 뒤졌을까? 아니면 그냥 에러를 품고 살았을까? 요점은 이거다. 모든 개발자는 스택오버플로우를 쓴다. 쓰지 않는 개발자는 거짓말쟁이다. 나는 이제 이 사실이 자랑스럽다. 내가 모르는 걸 빠르게 찾을 수 있는 능력, 그게 진짜 시니어의 실력이 아닐까? '아 그거요' 신드롬 회사에서 내 입버릇이 뭐냐고 물으면 '아 그거요'라고 답할 것 같다. 진짜 입버릇이 그거다. 기획자가 "김개발님, 이거 간단하게 수정 가능하죠?"라고 물을 때. 나는 "아 그거요"라고 한다. 속으로는 '간단한 게 뭔데 아무것도 간단한 게 없는데'라고 생각하면서. 근데 이 '아 그거요'라는 말이 나올 때, 사실 나는 이미 구글을 켠 상태다. 손가락은 스택오버플로우 URL을 치고 있다. 그리고 몇 분 뒤, 나는 마치 내가 방금 이 솔루션을 생각해낸 것처럼 설명한다. "네, 이렇게 하면 돼요." 동료들은 나를 신뢰한다. 나는 빠르기 때문이다. 빠른 것처럼 보이기 때문이다. 사실 나는 빠르게 검색할 뿐이다. 회의 중에 라포톱으로 딴 짓하는 척하면서 코드를 보는 것도 그래서다. 누군가가 기술적인 질문을 던질 때, 나는 회의실에서 나가지 않고도 답할 수 있어야 한다. 그래야 존재감이 있다. 슬랙 알림보다 회의 중 질문이 더 스트레스다. 왜냐하면 구글을 켤 시간이 없기 때문이다. 레거시 코드와 스택오버플로우의 한계 그런데 여기서 흥미로운 지점이 있다. 모든 게 스택오버플로우로 해결되는 건 아니라는 것이다. 작년에 우리 회사 시스템에 심각한 버그가 발생했다. Redis 캐시가 특정 상황에서 스테일(stale) 데이터를 반환하는 문제였다. 처음엔 간단한 TTL 설정 문제인 줄 알았다. 스택오버플로우에서 관련 답변들을 찾았다. 하지만 우리 상황과 맞지 않았다. 우리는 레거시 코드를 건드리고 있었기 때문이다.그 순간 나는 깨달았다. 스택오버플로우는 일반적인 문제를 푸는 거다. 하지만 개발자가 실제로 맞닥뜨리는 건 특수한 문제다. 우리 회사의 특수한 아키텍처, 우리 팀의 특수한 레거시 코드. 이건 누구도 미리 답해줄 수 없다. 그때서야 내가 진짜로 생각해야 했다. 검색하는 게 아니라, 논리를 조립해야 했다. 에러 로그를 읽고, 데이터 플로우를 추적하고, 테스트를 써야 했다. 그게 7년의 경력이 의미를 갖는 순간이었다. 스택오버플로우 없이도 문제를 풀 수 있다는 걸 증명해야 했다. 팀 채널에 링크 붙여넣는 나 근데 여기서 또 다른 문제가 생겼다. 바로 내가 아는 것처럼 보여야 한다는 압박이다. "김개발님, 이거 어떻게 하죠?"라는 질문이 나오면, 내가 "음... 잘 모르는데 한번 알아봐야겠네요"라고 답하면 어떨까? 솔직한 답변이지만, 팀의 신뢰도는 하락한다. 그래서 나는 재빨리 스택오버플로우를 켠다. 그리고 링크를 붙여넣는다. "여기 봐보세요. 이런 식으로 하면 돼요." 후배들은 고마워한다. 나는 영웅이 된다. 아무도 내가 지금 그 링크를 처음 읽고 있다는 걸 모른다. 이게 나쁜 건 아니다. 오히려 이게 효율적인 개발 문화를 만드는 방법이다. 스택오버플로우라는 거인의 어깨 위에 서서, 우리는 더 높이 본다. 우리는 개별 문제를 푸는 시간을 줄이고, 아키텍처와 설계를 고민할 시간을 얻는다. 하지만 한 가지 확실한 건, 그 링크 뒤의 진짜 지식을 이해해야 한다는 것이다. 링크를 붙여넣는 건 쉽다. 하지만 왜 그 솔루션이 작동하는지 설명할 수 있어야 한다. 그게 바로 시니어와 주니어의 차이다. 스택오버플로우를 넘어서지난 3년간 나는 사이드 프로젝트를 3번 시작했다. 그리고 3번 모두 접었다. 왜냐하면 사이드 프로젝트에서는 스택오버플로우가 덜 효과적이기 때문이다. 회사 일과 달리, 내가 정말로 무언가를 만들어야 하기 때문이다. 회사에선 기존 구조를 이해하고 거기에 기능을 추가하면 된다. 하지만 새 프로젝트를 시작하면 처음부터 아키텍처를 설계해야 한다. 데이터베이스 스키마를 그려야 한다. API 엔드포인트를 설계해야 한다. 이건 스택오버플로우로 해결되지 않는다. 그래서 나는 프로젝트를 접었다. 불편한 진실을 마주했기 때문이다. 내가 진짜로 무언가를 만들지는 못한다는 것을. 하지만 요즘 생각이 바뀌고 있다. 회사에서 시스템을 유지보수하고 개선하는 것도 충분히 가치 있는 일이라는 걸 깨달았기 때문이다. 누군가는 새로운 기술을 개척해야 하고, 누군가는 기존 시스템을 견고하게 유지해야 한다. 나는 후자를 잘 하는 개발자다. 그리고 그건 나쁜 게 아니다. 스택오버플로우는 내 도구다. 좋은 도구를 쓰는 게 효율적인 거다. 결국, 그래서 뭐어? 어제 기획자가 또 물었다. "이 기능, 어렵죠?" 나는 답했다. "아 그거요, 간단해요." 내 손가락은 이미 스택오버플로우 URL을 치고 있었다. 5분 뒤, 나는 솔루션을 제시했다. 기획자는 놀랐다. 동료들은 감탄했다. 나는 아무 말도 하지 않았다. 필요 없었다. 그들의 신뢰가 내 상이었다. 근데 오늘따라 한 가지 생각이 떠나지 않는다. 혹시 내가 7년간 같은 걸 반복하고 있는 건 아닐까? 검색 능력만 늘고, 실제 지식은 쌓이지 않고 있는 건 아닐까? 아니다. 그건 아니다. 나는 분명히 성장했다. 스택오버플로우 링크를 붙여넣을 때, 이제는 그게 왜 작동하는지 안다. 처음엔 몰랐지만. 경험이 쌓이면서 링크 뒤의 지식도 깊어졌다. 스택오버플로우는 내가 알고 있는 것들의 색인이 되었다. 나는 더 이상 순수하게 검색만 하지 않는다. 나는 확인한다. 그 차이가 전부다.스택오버플로우 없는 개발자는 존재할 수 있지만, 그런 개발자는 아마 매우 외로울 것 같다.

배포일 10시까지 야근하다 보니 생각해본 것들

배포일 10시까지 야근하다 보니 생각해본 것들

배포일 10시까지 야근하다 보니 생각해본 것들 6시가 뭐길래? 오늘은 배포일이다. 매달 한두 번 돌아오는 그 날. 아침에 일어날 때부터 이미 피곤했다. 왜냐하면 나는 이미 알고 있으니까. 원칙상 6시 퇴근, 하지만 배포일엔 그런 원칙은 없다. 그냥 일종의 전설일 뿐이다. 마치 "한 달에 딱 한 번만 야근"처럼 들리는 말 같은 거. 사무실 형광등 아래 앉아 있다 보니, 저 멀리 6시의 시간이 자꾸 떠오른다. 6시는 그냥 시간이 아니다. 그건 자유다. 퇴근의 신호음이고, 개인 시간의 시작이고, "이제 코드는 내 것이 아니다"라는 선언이다. 그런데 배포일엔 그게 안 된다. 6시가 되고 또 7시가 되고, 어느새 8시가 되는데 화면엔 여전히 빨간 에러 로그가 떠 있다.오늘 아침 기획자가 말했었다. "이번 배포에 새로운 결제 기능이랑 추천 알고리즘 들어가니까 좀 유의해주세요." 유의해주세요. 그 말이 뭔지 아는가? 그건 "뭔가 터질 가능성이 있다"는 뜻이다. 기획자 입에서 "유의해주세요"가 나오면 현업 개발자들은 절대 편할 수 없다. 그래서 오후 2시부터 배포 준비에 들어갔다. 데이터베이스 마이그레이션 스크립트 돌려보고, 로컬에서 엔드 투 엔드 테스트도 몇 번 해봤다. 쿠키도 먹었고, 물도 마셨다. 준비는 다 했다. 그런데 뭔가 늘 빠진 게 있다. 7시, 첫 번째 에러의 등장 배포 시간 5시 50분. 마지막 체크를 하고 있었다. 슬랙에 배포 시작 메시지를 남기고, 자동화 배포 스크립트를 실행했다. 진행 중... 진행 중... 그리고 BUILD_FAILED. 아, 맞다. 내가 지금까지 몇 번을 반복했는지 세어본 적도 없다. 이 느낌. 배포 과정 중에 예상치 못한 에러를 마주하는 그 짜증나는 감정. 뭔가 왠지 모르게 나를 탓하는 기분도 들고, 세상을 탓하는 기분도 든다. 빨간 글자를 읽어보니, 테스트 코드 하나가 실패했다고 한다. 분명 로컬에선 다 통과했는데. 문제는 로컬 환경과 배포 환경의 차이다. 그 차이가 정확히 뭔지는 거의 철학 문제 수준이다. 왜냐하면 내 옆 팀은 "우리는 Docker로 일관성을 유지하려고 했는데..."라고 말하니까. 그건 나중에 할 일이고, 지금은 이 에러를 고쳐야 한다. 코드를 뒤져본다. 7시 10분. 아내는 아직 회사에 있을 거다. UI 디자인팀은 저녁 늦게까지 일하는 쪽이니까. 좋아, 그럼 최소 1시간 20분은 있다. 충분하다. 아마 8시쯤엔 집에 들어갈 수 있을 거다. 8시, 현실의 무게 에러는 하나가 아니었다. 첫 번째 에러를 고쳤더니 두 번째 에러가 튀어나왔다. 이건 마치 고래게임 같다. 한 문제를 풀면 다음 문제가 나타난다. 보스전 같은 이 과정. 8시가 되니 사무실은 거의 비어있었다. 청소 아저씨가 쓰레기통을 비우고 가셨다. 그리고 나 혼자 남았다. 모니터 화면이 자꾸 흐릿하게 보인다. 피로의 신호다. 아이 드롭스를 짜서 눈에 떨어뜨리고, 커피를 다시 마신다. 오늘의 네 번째 커피다. 카페인은 내 혈액형이다. 이제. 아내한테 문자를 보낼까. 하지만 뭐라고 보내지? "배포 중, 늦겠다" 따위의 말? 이미 내 상황을 충분히 안다. 결혼 2년차다. 매달 한두 번 이런 일이 반복되니까. 그게 가장 서글픈 부분이다. 이제 아내도 배포일을 안다. 내 마음속 달력에 배포일이 표시되듯이, 아내의 마음속 달력에도 이미 표시되어 있을 거다.8시 45분. 마침내 에러를 찾았다. 데이터베이스 쿼리에서 타임존 처리를 잘못했다. 그런데 이건 단순한 쿼리 수정이 아니라, 일부 배포된 데이터까지 롤백해야 할 수도 있다는 뜻이다. 신경이 곤두선다. 나는 펜을 돌리며 생각한다. 내 버릇이다. 가만 생각만 하는 게 아니라, 손가락으로 펜을 돌려야 생각이 잘 된다. 그게 뭐하는 짓인지도 모르지만, 여하튼 그렇다. 옆 테이블에 앉아있는 똑똑한 신입 개발자 박준호 대리가 한 번 물었었다. "개발자님, 왜 펜을 계속 돌려요?" 나는 대답했다. "모르겠는데, 이렇게 하면 버그가 적게 나오는 것 같아." 그건 과학이 아니라 신앙이다. 9시, 아내를 생각하는 마음 배포 진행 상황은 좋아졌다. 데이터 불일치를 수정하고, 다시 배포를 시작했다. 진행 중... 진행 중... 이번엔 성공했다. 프로덕션 환경에 정상 배포됐다. 하지만 아직 끝이 아니다. 배포 후 체크리스트가 남아있다. 프로덕션 DB 접속해서 몇 가지 쿼리로 데이터 검증을 한다. 캐시는 초기화됐나? 로그 에러는 없나? 트래픽 메트릭은 정상인가? 요즘 시대엔 배포하고 "좋아, 끝" 이라고 할 수 없다. 그다음이 또 있다. 9시. 아내한테 연락할 시간이다. 나는 핸드폰을 들었다 놨다를 반복한다. 지금 전화하면 회의 중일 수도 있고, 프레젠테이션 준비 중일 수도 있다. 아니면 이미 집에 와 있을 수도 있다. 그렇다면 혼자 뭐하면서 기다리고 있을까. TV를 볼까. 아니면 또 다른 프로젝트를 할까. 솔직히 말하면, 나는 지금 가정을 미안해하고 있다. 회사 일이 중요한 건 맞다. 근데 가정도 있지 않은가. 배포일이 아니면 오늘 같은 날이 또 없을 텐데, 내가 점점 그 사실에 무뎌지는 것 같다. 작년에는 배포일에 대한 스트레스로 한 달에 며칠을 고민했는데, 올해는? 이제는 그냥 일어날 일이라고 생각한다. 마치 월급이 들어오는 것처럼. 9시 30분, 테크 리드의 책임감 후배 개발자들이 자꾸 내 슬랙을 울린다. 배포 진행 상황을 묻는 것이다. 공식적으로는 테크 리드가 따로 있지만, 실제론 내가 한다. 직책 없이. 급여도 없이. 그래서 다른 사람들은 "개발자님, 이거 마이너 버그인데 괜찮은가요?"라고 물어본다. 배포 중에는 모든 경고가 최우선이다. 마이너도 메이저도 없다. 그냥 모두가 신경 쓴다. 나는 기획자한테 "지금 배포 중이니까 급할 땐 아니지만, 곧 확인 가능합니다"라고 말한다. 기획자는 "아, 그렇구나요"라고 대답한다. 하지만 그 말은 "빨리"라는 뜻이다. 모든 "구나요"는 "빨리"를 포함하고 있다. 배포 후 검증이 끝나니 9시 50분. 마지막으로 배포 로그를 정리하고, 슬랙 채널에 배포 완료 메시지를 남긴다. "#devops-alerts" 채널에. 그 메시지를 보고 회사 전체가 안심한다. 그 메시지 한 줄 때문에 내가 여기까지 왔구나 싶으면서도, 또 한편으로는 "이제 끝이다"라는 생각이 든다.10시, 다음엔 더 안정적으로 10시에 나는 사무실을 나선다. 조명을 모두 끄고, 사무실 문을 닫는다. 밤의 도시는 여전히 깨어있다. 버스를 탄다. 버스 창 밖으로는 서울의 야경이 흐른다. 사람들은 술집에 가고, 당구장에서 놀고, 또 누군가는 나처럼 집으로 돌아간다. 그들도 뭔가 일이 있을까. 아니면 그냥 퇴근한 건가. 집에 가면 아내가 기다릴 거다. "배포 잘됐어?"라고 물을 거다. 나는 "응, 잘됐어. 조금만 쉬면 돼"라고 대답할 거다. 그러면 아내는 밥을 데워줄 거고, 나는 먹을 거다. 그리고 한참을 앉아만 있을 거다. 화면을 보다가, 또 아내를 보다가. 배포일 밤 10시 버스 안에서 나는 생각했다. "다음엔 더 안정적으로 배포하자." 이 생각은 이미 3번째다. 배포가 있을 때마다 나는 이 다짐을 한다. 도커 환경을 좀 더 정확히 구성하고, 자동화 테스트를 더 촘촘히 하고, 배포 전 체크리스트를 더 자세히 만들고... 그런데 왜 이 다짐이 매번 반복될까. 그건 사실 내 능력 문제가 아니라, 시간의 문제다. 다음 배포까지 시간이 있어야 이런 것들을 개선할 수 있는데, 매일 새로운 버그 리포트가 들어오고, 새로운 기능 요청이 들어온다. 그럼 언제 개선을 하나. 주말? 하지만 주말은 치킨 시키고 넷플릭스 보는 내 유일한 쉼표다. 결국 배포는 또 다음 달에 한다. 그때도 어디선가 뭔가 빠질 거다. 그때도 내가 8시에 에러를 발견하고, 9시에 아내를 생각하고, 10시에 버스를 탈 거다. 그리고 또 다시 다짐을 한다. "다음엔 더 안정적으로." 배포일 밤 10시. 나는 버스 의자에 기대어 눈을 감는다. 오늘도 이렇게 끝난다. 내일도 일어나서 코드를 본다. 그 다음도. 그리고 또 그 다음 배포일도. 사실 배포일은 직업 개발자의 숙명이다. 피할 수 없다. 그래서 나는 이 밤을 받아들인다. 아내도 알고 있다. 경영진도 안다. 우리 팀도 안다. 이게 우리 일이라는 걸. 배포가 성공하는 그 순간, 모든 스트레스가 보상받는 느낌이 든다. 어느 정도는. 나머지는 그냥... 직업 의식이다.결국 내일이 또 있으니까, 오늘 따위는 상관없다.