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