API 문서화를 미뤘다가 생기는 혼란

API 문서화를 미뤘다가 생기는 혼란

문서화는 나중에 API 개발 끝났다. 배포도 했다. 근데 문서는 안 썼다. "나중에 쓰지 뭐." 그게 3주 전이다. 이제 슬랙이 난리다. "김개발님 이 API 파라미터가 뭐예요?" "응답 형식이 어떻게 되죠?" "에러코드 정리된 거 있어요?" 코드 보면 되는데. 근데 그것도 민망하다.처음엔 괜찮았다 처음 2주는 괜찮았다. 나만 쓰는 API였으니까. 인증 토큰은 헤더에 Authorization: Bearer {token}. 페이징은 page랑 size. 정렬은 sort. 당연한 거 아닌가. 근데 프론트팀 신입이 들어왔다. 김신입. "김개발님, API 명세서 어디 있어요?" "아... 그게... 코드 보면 돼요." "네? 코드요?" 말하면서도 개같다는 걸 알았다. 질문이 쌓인다 김신입은 착하다. 슬랙으로 물어본다. 하나씩. "User API의 role 필드 값이 뭐가 올 수 있나요?" "ADMIN, USER, GUEST 세 개요." 10분 후. "null일 수도 있나요?" "아뇨. 필수값이에요." 또 10분. "그럼 기본값은 뭐예요?" "기본값 없어요. 회원가입 때 지정해야 해요." 한 필드에 세 번 물었다. 나는 코드 짜다 말고 세 번 답했다.그냥 코드 보라고 할까 "코드 보면 다 나와요." 이렇게 말하고 싶다. 근데 그게 개발자 오만이라는 걸 안다. 코드가 문서는 아니니까. 프론트 개발자가 스프링 코드 뒤적이면서 @RequestBody가 뭔지 찾아보고, @Valid 어노테이션 의미 파악하고, 유효성 검증 로직 추적하고... 그건 폭력이다. 근데 문서 쓰는 것도 폭력이다. 나한테. 회의에서 터졌다 기획자 박기획이 말했다. "이번 기능, API 호출 시나리오 정리 좀 해주세요." "시나리오요?" "네. 어떤 순서로 어떤 API 호출하는지요. 플로우차트 같은 거." "그건... QA팀이 테스트하면서 보면 되는 거 아닌가요?" "QA도 모르는데요. 문서가 없어서." 회의실이 조용해졌다. 다들 나를 봤다. 나는 모니터 보면서 펜 돌렸다. "...알겠습니다."일단 미뤘던 이유 문서화를 안 한 게 게을러서만은 아니다. 변명이긴 한데, 진짜다. API가 계속 바뀌었다. 초기 개발 단계라서. 처음엔 User 조회가 GET /user/{id}. 근데 기획 바뀌어서 GET /users/{id}로 수정. 복수형. 응답 형식도 바뀌었다. 처음엔 유저 객체 그대로 내려줬는데, 나중에 래핑해서 { data: {...}, meta: {...} } 형태로. 에러 코드는 3번 바뀌었다. USER_NOT_FOUND가 USER_001이 됐다가 다시 ERR_USER_NOT_FOUND로. 그때마다 문서 고치려면 개빡친다. 그래서 "일단 완성되면 쓰지" 했는데. 완성은 안 된다. 계속 수정된다. 코드로 자동 생성? "Swagger 쓰세요." 후배 이후배가 말했다. "Swagger요?" "네. 어노테이션 달면 문서 자동 생성돼요." 알긴 아는데. 도입이 귀찮다. 어노테이션 달려면 컨트롤러마다 @ApiOperation, @ApiParam 다 붙여야 한다. 우리 API 엔드포인트가 47개다. 기존 코드 47개 파일 열어서 어노테이션 추가. 그것도 일이다. "시간 나면 해볼게요." 이후배는 더 안 물었다. "시간 나면"이 뭔 뜻인지 아니까. 문서 쓰기 시작했다 결국 썼다. Notion 페이지 만들었다. "API 문서" 제목 달고. 목차 만들고.인증 User API Post API Comment API ...47개 엔드포인트를 정리하기 시작했다. 각 API마다:메서드, URL 요청 파라미터 요청 바디 예시 응답 예시 에러 케이스첫 번째 API 정리하는 데 30분 걸렸다. "30분 곱하기 47... 2350분... 39시간?" 계산기 두드리다가 한숨 나왔다. 누가 봐주긴 할까 4시간 정도 쓰고 12개 API 문서화했다. 진도는 25%. 슬랙에 공유했다. "API 문서 작성 시작했습니다. 피드백 주세요." 5분 후 김신입이 답장했다. "오오 감사합니다!" 그게 끝이다. 다른 반응은 없다. "이거 진짜 다 쓸 가치가 있나?" 혼잣말이 나왔다. 근데 김신입이 질문이 확 줄었다. 슬랙 알림이 조용하다. 그거면 됐다. 유지보수가 문제다 이틀 뒤 기획 변경됐다. User API에 nickname 필드 추가. 필수값. 2~10자. 코드는 10분 만에 고쳤다. 근데 문서는? Notion 열어서 User API 섹션 찾아서. 요청 바디 예시 수정하고. 응답 예시 수정하고. 유효성 검증 규칙 추가하고. 15분 걸렸다. "이래서 안 쓰는 건데." 또 혼잣말. 그리고 1주일 뒤 또 기획 변경. 이번엔 문서 업데이트 안 했다. "나중에 몰아서 하지." 3주 전 그 마인드가 돌아왔다. 문서가 틀렸다 김신입이 또 물었다. "문서랑 실제 API 응답이 달라요." "어디요?" "nickname 필드가 안 내려와요." "아, 그거 선택값으로 바뀌었어요." "문서에는 필수라고 돼있는데..." "...업데이트 깜빡했네요." 이게 더 최악이다. 문서가 없는 것보다 틀린 문서가 더 위험하다. 김신입은 틀린 문서 믿고 개발했다. 필수값이니까 예외처리 안 했다. 버그 났다. 내 잘못이다. Swagger를 결국 도입했다 주말에 했다. 집에서. Spring REST Docs도 고민했는데, 테스트 코드 짜야 해서 패스. Swagger가 빠르다. 의존성 추가. 설정 파일 작성. 컨트롤러에 어노테이션 추가. @ApiOperation(value = "유저 조회", notes = "ID로 유저 조회") @ApiResponses({ @ApiResponse(code = 200, message = "성공"), @ApiResponse(code = 404, message = "유저 없음") }) @GetMapping("/users/{id}") public UserResponse getUser(@PathVariable Long id) { ... }이런 식으로 47개 엔드포인트 다 달았다. 토요일 오후 내내. 근데 뭔가 뿌듯하다. 코드 정리하니까 보기 좋다. /swagger-ui.html 접속해봤다. 문서가 예쁘게 나온다. "이거 괜찮네." 팀원들 반응 월요일에 공유했다. "Swagger 도입했습니다. /swagger-ui.html 확인해보세요." 김신입 답장이 빨랐다. "헐 대박이에요!" 이후배도 반응했다. "오 형 주말에 이거 하신 거예요?" "그냥 심심해서요." 사실 김신입이 계속 물어볼까봐 불안해서 한 거다. 기획자 박기획도 봤다. "이거 좋네요. 이제 기획 전달할 때 이거 보여드리면 되겠어요." 개발 안 하는 사람도 보기 쉽다는 게 Swagger 장점이다. 그래도 완벽하진 않다 Swagger도 한계는 있다. 복잡한 비즈니스 로직 설명은 어렵다. 어노테이션만으로는. "결제 API는 먼저 주문 생성하고, 그 다음 결제 요청하고..." 이런 플로우 설명은 못한다. 에러 케이스 설명도 부족하다. 400 에러가 나는 이유가 10가지인데, 그걸 다 @ApiResponse에 못 쓴다. 인증 토큰 발급 과정, 리프레시 토큰 로직... 이런 건 별도 문서 필요하다. 결국 Swagger + Notion 병행하게 됐다. Swagger는 API 레퍼런스. Notion은 가이드 문서. 둘 다 관리하는 건 귀찮은데. 그래도 없는 것보단 낫다. 유지보수는 여전히 숙제 한 달 지났다. Swagger 문서도 낡아간다. 신규 API 추가할 때 어노테이션 빼먹는다. 급하니까. "나중에 추가하지." 그러고 까먹는다. 김신입이 또 물어본다. "새로 추가된 알림 API 문서는 어디 있어요?" "아... 어노테이션 추가 안 했네요. 코드 보면..." 또 "코드 보면"이 나왔다. 문서화는 일회성이 아니다. 계속 관리해야 한다. 그게 어렵다. PR 체크리스트에 추가 이후배가 제안했다. "PR 템플릿에 문서 업데이트 항목 추가하는 거 어때요?" "PR 템플릿에요?" "네. API 변경 시 Swagger 어노테이션 업데이트했는지 체크하는 거요." ## Checklist - [ ] 테스트 코드 작성 - [ ] API 변경 시 Swagger 문서 업데이트 - [ ] 코드 리뷰 반영괜찮은 아이디어다. 실제로 추가했다. 다들 체크는 한다. 안 하면 내가 코멘트 달아준다. "Swagger 문서 업데이트 부탁드려요." 처음엔 귀찮아했는데, 한 달 지나니 습관 됐다. 신규 API 만들 때 자동으로 어노테이션 단다. 이제는. 문서화 문화 결국 문화 문제다. "문서화는 나중에" 이 마인드가 문제였다. 개발하면서 동시에 문서 작성. 이게 기본이 돼야 한다. 근데 그게 쉽지 않다. 특히 일정 촉박할 때. "기능 개발이 먼저고 문서는 나중에" 이 생각이 자동으로 나온다. 팀장이 "문서화도 개발의 일부"라고 강조한다. 맞는 말인데. 실천이 어렵다. 지금도 완벽하진 않다. 그래도 3달 전보단 낫다. 배운 것들 API 개발하고 문서 안 쓰면:팀원이 계속 물어본다. 슬랙 알림 지옥. 신입이 헤맨다. 온보딩 시간 3배. 기획/QA가 답답해한다. "이거 어떻게 테스트해요?" 나도 까먹는다. 3개월 전 API 뭐였는지 기억 안 남.문서화 방법:Swagger/OpenAPI: API 레퍼런스용, 자동 생성 가능 Notion/Wiki: 가이드, 플로우, 비즈니스 로직 설명 README: 빠른 시작 가이드 코드 주석: 복잡한 로직만중요한 건 도구가 아니라 습관이다. 개발 끝나고 "나중에 쓰지"가 아니라, 개발하면서 바로 쓰기. PR 체크리스트에 넣기. 리뷰 때 확인하기. 완벽하지 않아도 된다. 없는 것보단 낫다. 3개월 후 효과 문서화 시작하고 3개월 지났다. 김신입 질문이 80% 줄었다. 슬랙 조용하다. 신규 입사자 온보딩 시간 절반 됐다. Swagger 보고 바로 개발 시작. QA팀이 좋아한다. "이제 테스트 케이스 짜기 쉬워요." 나도 편하다. "코드 보면 돼요" 안 해도 된다. 시간 투자는 했다. 주말 하루, 그 뒤로 일주일에 1시간씩. 근데 그거 하고 절약되는 시간이 더 크다. 질문 답하는 시간, 설명하는 시간, 디버깅 도와주는 시간. "진작 할 걸" 이 생각 든다. 여전히 귀찮다 그래도 솔직히 말하면. 여전히 귀찮다. API 하나 추가할 때마다 Swagger 어노테이션 5줄 쓰는 거. 귀찮다. Notion 가이드 문서 업데이트하는 거. 귀찮다. 기획 변경될 때마다 문서 수정하는 거. 귀찮다. 근데 한다. 안 하면 더 귀찮아지니까. 김신입이 또 물어보는 게 더 귀찮다. 틀린 문서 때문에 버그 나는 게 더 귀찮다. 문서화는 미래의 나를 위한 투자다. 그리고 팀을 위한 배려다. 개발자가 코드만 짜면 된다는 생각. 학생 때 끝났어야 했다. 실무에서는 커뮤니케이션도 개발이다. 문서도 개발이다."나중에"는 영원히 안 온다. 지금 쓰거나, 계속 물어봐 주거나.

테스트 코드 작성: 해야 하는데 시간이 없어

테스트 코드 작성: 해야 하는데 시간이 없어

테스트 코드, 또 미뤘다 오늘도 PR 올렸다. 테스트 코드 없이. 리뷰어가 물어볼 거다. "테스트는요?" 그럼 나는 답한다. "다음 스프린트에 추가하겠습니다." 이게 벌써 세 번째다. 다음 스프린트는 안 온다. 우리 모두 알고 있다. 아침 스탠드업에서 PM이 말했다. "이번 주 배포 일정 타이트합니다." 타이트하다는 건 야근한다는 뜻이다. 테스트 코드 쓸 시간은 당연히 없다는 뜻이다. 모니터를 보며 펜을 돌린다. 막 짠 코드가 화면에 있다. 200줄짜리 서비스 로직. 분기 처리가 5개. 예외 케이스가 3개. 테스트 짜려면 최소 10개는 필요하다. 시계를 본다. 오후 4시. 퇴근까지 2시간. 코드 리뷰 반영하고, 문서 업데이트하고, 내일 회의 자료 준비해야 한다. 테스트는 무리다.악순환의 시작 테스트 없는 코드는 빠르다. 짜는 동안만. 이번 주에 3개 기능 만들었다. 테스트 코드 없이. 다음 주에 버그 리포트 5개 받았다. 엣지 케이스였다. 내가 놓친 분기들. 버그 수정하는 데 하루 반 걸렸다. 테스트 짰으면 2시간이면 찾았을 버그다. 근데 수정할 때도 테스트 안 짰다. 시간이 없어서. 후배가 물어봤다. "형, 이 함수 어떻게 동작하는 거예요?" 내가 3달 전에 짠 코드다. 주석도 없다. 테스트도 없다. 나도 기억 안 난다. 코드를 다시 읽었다. 10분 걸렸다. 테스트 코드가 있었으면 3분이면 이해했을 것이다. 테스트는 문서다. 살아있는 문서. 레거시 코드가 쌓인다. 테스트 없는 코드가 레거시가 되는 건 빠르다. 6개월이면 충분하다. 리팩토링 회의에서 팀장이 말했다. "이 부분 개선이 필요한데요." 나는 고개를 저었다. "건드리면 어디가 터질지 모릅니다." 테스트가 없어서. 결국 안 건드린다. 기술 부채가 쌓인다. 이자는 복리다.시간이라는 핑계 "시간 없어서 테스트 못 짰다"는 거짓말이다. 나한테 하는. 진짜는 이거다. 테스트 짜는 게 귀찮다. 당장 눈에 보이는 결과가 없다. 기획자는 테스트 코드 커버리지에 관심 없다. PM은 기능이 돌아가는지만 본다. 테스트는 미래를 위한 투자다. 근데 나는 오늘이 바쁘다. 내일도 바쁠 거다. 투자할 여유가 없다. 이게 7년째 반복이다. 신입 때는 달랐다. TDD 책도 샀다. JUnit 강의도 들었다. 첫 회사에서 테스트 커버리지 80% 채웠다. 그땐 시간이 있어서가 아니다. 배우고 싶어서였다. 지금은 다르다. 연차가 쌓이면서 핑계만 늘었다. "레거시라 테스트 짜기 힘들어요." "의존성이 너무 많아요." "모킹하기 복잡해요." 다 맞는 말이다. 근데 변명이다. 진짜 이유는 이거다. 테스트 없이도 어떻게든 돌아간다. 버그 나면 그때 고친다. 장애 나면 야근한다. 이게 익숙하다. 편한 길이다. 당장은.돌고 도는 이야기 목요일 저녁. 슬랙에 메시지가 떴다. "결제 API 오류 났어요." 심장이 뛴다. 내가 화요일에 수정한 부분이다. 테스트 없이 배포했다. 프로덕션 로그를 연다. NullPointerException. 예상 못 한 케이스였다. 테스트 짰으면 잡았을 거다. 핫픽스 시작. 코드 고치는 건 10분. 검증하는 데 1시간. 배포하고 모니터링하느라 2시간. 퇴근은 10시. 집에 가는 지하철에서 생각했다. "다음엔 테스트 꼭 짜야지." 이게 몇 번째 다짐인지 모르겠다. 금요일 아침. 새 기능 티켓이 떨어졌다. 월요일까지 완료. 타이트하다. 테스트 짤 시간은 또 없다. 악순환이다. 회사는 테스트를 강제하지 않는다. 커버리지 목표도 없다. PR에 테스트 없어도 머지된다. "바쁘니까 다음에"가 통한다. 처음엔 편했다. 이제는 무섭다. 내가 짠 코드가 시한폭탄처럼 느껴진다. 언제 터질지 모른다. 터지면 내가 고쳐야 한다. 주말에도 전화 온다. 변명들의 목록 "레거시라 테스트 짜기 힘들어요." 맞다. 의존성 주입 안 된 코드. 싱글톤 지옥. static 메서드 천지. 테스트하기 어렵게 설계됐다. 근데 새 코드는? 내가 어제 짠 코드도 테스트하기 어렵다. 내가 그렇게 짰다. 습관이다. "모킹하기 너무 복잡해요." 외부 API 6개 호출하는 함수. 데이터베이스 4개 테이블 조인. 레디스 캐시 체크. 모킹할 게 너무 많다. 근데 이건 테스트의 문제가 아니다. 설계의 문제다. 함수가 너무 많은 일을 한다. 책임이 분리 안 됐다. 테스트가 어려운 코드는 나쁜 코드다. 이건 학교에서 배웠다. 실무에선 잊었다. "시간 대비 효율이 안 나와요." 코드 짜는 데 1시간. 테스트 짜는 데 30분. 50% 오버헤드다. 비효율적으로 보인다. 근데 나중에 버그 고치는 시간은? 어제 핫픽스에 3시간 썼다. 테스트 있었으면 버그 자체가 없었다. 장기적으로 보면 테스트가 더 빠르다. 근데 나는 단기밖에 못 본다. 다음 주 배포만 보인다. "우리 팀은 빠른 개발이 중요해요." 스타트업 마인드. 일단 만들고 본다. 시장 검증이 먼저다. 테스트는 나중에. 근데 나중은 안 온다. 기능이 쌓인다. 유저가 늘어난다. 버그가 폭발한다. 그때는 더 못 짠다. 더 바쁘니까. 빠르게 가려면 제대로 가야 한다. 이것도 학교에서 배웠다. 실무에선 반대로 한다. 테스트 짜는 사람들 팀에 한 명 있다. 테스트 빡빡하게 짜는 후배. PR 올리면 테스트가 같이 온다. 유닛 테스트. 통합 테스트. 엣지 케이스까지. 코드보다 테스트가 더 길다. 처음엔 답답했다. "왜 이렇게 오래 걸려?" 3일이면 끝날 걸 5일 걸린다. 근데 그 코드는 안 터진다. 버그 리포트에 그 후배 이름이 없다. 리팩토링도 빠르다. 테스트가 보호하니까. 나는 빨리 짠다. 3일이면 끝낸다. 그리고 다음 주에 2일 동안 버그 고친다. 결국 5일이다. 야근 포함하면 더 길다. 누가 더 빠른 걸까. 오픈소스 코드를 볼 때가 있다. 유명한 라이브러리들. 테스트 커버리지 90% 넘는다. 그래서 믿고 쓴다. 내 코드는 어떨까. 커버리지 15%. 나도 내 코드 못 믿는다. 수정할 때마다 떨린다. 어디가 터질지 몰라서. 테스트는 자신감이다. 없으면 항상 불안하다. 시작하지 못하는 이유 테스트를 시작하기 어려운 건 맞다. 지금 프로젝트는 테스트가 없다. 시작하려면 환경부터 세팅해야 한다. JUnit 5? Mockito? AssertJ? 뭘 쓸지부터 정해야 한다. 기존 코드는 테스트하기 어렵게 짜여 있다. 리팩토링부터 해야 한다. 그러면 기능 개발은 언제 하나. 완벽하게 하려면 못 한다. 이게 함정이다. 테스트는 0 아니면 100이 아니다. 50도 괜찮다. 30도 없는 것보단 낫다. 어제 짠 핵심 로직 하나만 테스트 짜도 된다. 버그 나기 쉬운 부분만. 전체 커버리지 90% 목표 안 세워도 된다. 근데 나는 완벽주의자다. 시작하면 제대로 하고 싶다. 그래서 시작을 안 한다. 완벽한 테스트를 기다리다가 아무 테스트도 못 짠다. 이게 7년째다. 시간의 역설 테스트 짤 시간이 없어서 테스트를 안 짠다. 그래서 버그가 난다. 버그 고치느라 시간이 더 없어진다. 그래서 테스트를 더 못 짠다. 역설이다. 시간을 아끼려고 테스트를 건너뛴다. 결국 더 많은 시간을 쓴다. 이걸 알면서도 반복한다. 이번 달 야근 시간을 계산했다. 40시간. 대부분 버그 수정과 핫픽스였다. 테스트가 있었다면? 10시간이면 충분했을 것이다. 30시간을 아낄 수 있었다. 그 시간이면 테스트 100개는 짰다. 근데 다음 달도 똑같을 것이다. 테스트 짤 시간이 없어서. "바빠서 테스트 못 짠다"는 말은 틀렸다. "테스트 안 짜서 바쁜" 게 맞다. 원인과 결과가 뒤집혔다. 이걸 깨닫는 데 7년 걸렸다. 바꾸는 데는 얼마나 걸릴까. 작은 시작 오늘 결심했다. 하나만 짜보기로. 새 기능 티켓을 받았다. 쿠폰 발급 API. 간단해 보인다. 근데 분기가 많다. 중복 발급 체크. 기간 검증. 재고 확인. 유저 등급별 차등 지급. 예전 같으면 그냥 짰다. 포스트맨으로 몇 번 찔러보고 PR 올렸을 것이다. 오늘은 다르게 해봤다. 함수 하나 짜고, 테스트 하나 짰다. 정상 케이스. 통과했다. 다음 분기 추가하고, 테스트 추가했다. 중복 발급 시도. 예외 발생 확인. 통과했다. 이렇게 하나씩. 30분 더 걸렸다. 근데 확신이 생겼다. 이 코드는 안 터진다. 리팩토링도 해봤다. 함수 이름 바꾸고, 파라미터 순서 바꿨다. 테스트가 깨졌다. 고쳤다. 다시 통과했다. 테스트가 없었으면 못 했을 리팩토링이다. 어디가 망가질지 몰라서. PR 올렸다. 테스트 코드가 같이 올라갔다. 처음이다. 리뷰어가 댓글 달았다. "오 테스트 있네요. 좋습니다." 기분이 묘했다. 이게 당연한 건데. 변화는 느리다 일주일에 하나씩 짜기로 했다. 새 기능 하나. 리팩토링 하나. 버그 수정 하나. 뭐든 하나만. 완벽하지 않다. 커버리지는 여전히 낮다. 레거시는 그대로다. 근데 조금씩 늘고 있다. 한 달 뒤. 테스트 30개가 생겼다. 작은 숫자다. 근데 없는 것보다 훨씬 낫다. 버그가 줄었다. 체감된다. 이번 달 핫픽스가 2개였다. 저번 달은 5개였다. 리팩토링도 조금씩 한다. 테스트가 있는 부분만. 조심스럽지만 할 수 있다. 후배가 물어봤다. "형, 갑자기 왜 테스트 짜요?" 뭐라 답해야 할까. "이제야 정신 차렸어" 이렇게 말했다. 변화는 극적이지 않다. 어느 날 갑자기 좋아지지 않는다. 조금씩, 천천히. 근데 방향은 맞다. 처음으로 맞는 방향으로 가는 기분이다. 여전히 바쁘다 테스트 짜기 시작했지만 여전히 시간은 없다. 어제도 야근했다. 기획 변경으로 코드 다 갈아엎었다. 테스트도 같이 갈아엎었다. 시간 두 배 걸렸다. 짜증 났다. "테스트 없었으면 진작 끝났을 텐데." 이런 생각도 들었다. 근데 배포는 안심하고 했다. 터지진 않을 거다. 테스트가 통과했으니까. 다음 날 아침. 슬랙 알림 없었다. 버그 리포트 없었다. 조용한 아침이다. 이게 얼마 만인지. 시간은 여전히 부족하다. 근데 쓰는 방식이 달라졌다. 예전엔 빨리 짜고, 나중에 고치느라 시간 썼다. 지금은 천천히 짜고, 나중이 편하다. 같은 시간인데 결과가 다르다. 불안한 빠름보다 확실한 느림이 낫다. 후회와 다짐 7년을 돌아본다. 처음부터 테스트 짰으면 어땠을까. 지금쯤 시니어답게 코드 짰을 것이다. 레거시 만들지 않았을 것이다. 근데 후회해도 소용없다. 과거는 못 바꾼다. 바꿀 수 있는 건 오늘뿐이다. 오늘 하나 짰다. 내일도 하나 짤 거다. 이게 계획의 전부다. 완벽한 계획은 없다. 거창한 목표도 없다. 그냥 하나씩. 테스트는 시간이 없어서 못 짜는 게 아니다. 안 짜는 거다. 선택의 문제다. 나는 이제 다르게 선택한다. 느리지만 확실하게. 불안하지 않게.테스트는 사치가 아니라 기본이다. 이제야 안다.

버전 관리 충돌: Git merge의 악몽

버전 관리 충돌: Git merge의 악몽

버전 관리 충돌: Git merge의 악몽 월요일 오전 10시 출근했다. 슬랙을 켰다. 빨간 알림 37개. "김개발님, UserService.java 머지 부탁드립니다 ㅎㅎ" 후배 박주니어가 올린 PR이다. 코드 확인했다. 내가 금요일에 수정한 파일이랑 겹친다. 아뿔싸. 금요일 저녁 7시. 급하게 hotfix 커밋하고 튀었다. push는 했는데 PR은 안 올렸다. 주말 내내 까먹었다. 박주니어는 금요일 오후에 브랜치 땄다. 내 hotfix 이전 버전에서. "충돌 날 거 같은데요." 답장을 보냈다. 3분 후. "어? 진짜네요. 어떻게 하죠?"충돌 파일 열어보기 <<<<<<< HEAD public void updateUser(Long userId, UserDto dto) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException()); user.update(dto); cacheManager.evict("users", userId); // 내가 추가한 캐시 처리 ======= public void updateUser(Long userId, UserDto dto) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException()); validateUserStatus(user); // 박주니어가 추가한 검증 로직 user.update(dto); >>>>>>> feature/user-validation둘 다 필요하다. 내 코드는 캐시 버그 수정이다. 프로덕션에서 유저 정보가 안 갱신되던 문제. 급해서 금요일에 핫픽스했다. 박주니어 코드는 기획에서 요청한 기능이다. 탈퇴 유저 업데이트 방지. 월요일 배포 예정이었다. 둘 다 놓치면 안 된다. 전화했다. "주니어님, 잠깐 통화 가능하세요?" "네 형님!" 형님 소리는 별로다. 나이 차이 4살인데. 협상 시작 "제 코드랑 주니어님 코드 둘 다 살려야 할 것 같은데요." "아 네! 그럼 제가 다시 짤까요?" 착한 후배다. 근데 시간이 아깝다. "아니요. 제가 해볼게요. 근데 validateUserStatus 메서드 어디 있어요?" "아 그거요. UserValidator 클래스 새로 만들었어요. src/main/java/validators 폴더에요." validators 폴더? 처음 듣는다. "그 폴더 언제 만드셨어요?" "이번 주에요! 검증 로직 분리하려고요. 클린 코드 책에서 봤는데..." 좋은 의도다. 근데 팀원들한테 얘기했어야지. 폴더 구조 바꾸는 건 함께 논의한다. 우리 팀 룰이다. 주니어는 아직 모른다. "아 그거 좋은데요. 근데 다음엔 먼저 얘기해주세요. 폴더 구조 바뀌면 다른 분들도 헷갈려하거든요." "아... 죄송해요." 목소리가 풀 죽었다. 기분 상한 것 같다. "아니 괜찮아요. 좋은 시도예요. 나중에 팀 회의 때 같이 정리해봅시다." "네!" 목소리가 다시 밝아졌다. 다행이다.코드 합치기 시작 일단 pull 받는다. git pull origin develop예상대로 conflict 발생. CONFLICT (content): Merge conflict in src/main/java/service/UserService.java CONFLICT (content): Merge conflict in src/test/java/service/UserServiceTest.java테스트 코드도 충돌이다. 예상 못 했다. 테스트 파일 열었다. <<<<<<< HEAD @Test void updateUser_캐시_삭제_확인() { // given Long userId = 1L; UserDto dto = createUserDto(); // when userService.updateUser(userId, dto); // then verify(cacheManager).evict("users", userId); ======= @Test void updateUser_탈퇴유저_검증() { // given Long userId = 1L; User withdrawnUser = createWithdrawnUser(); when(userRepository.findById(userId)).thenReturn(Optional.of(withdrawnUser)); // then assertThrows(UserStatusException.class, () -> userService.updateUser(userId, createUserDto())); >>>>>>> feature/user-validation둘 다 다른 테스트다. 근데 같은 위치에 넣었다. 이건 둘 다 남겨야 한다. 순서만 정리하면 된다. 합치는 중 UserService.java부터 정리했다. public void updateUser(Long userId, UserDto dto) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException()); // 박주니어 코드 validateUserStatus(user); // 내 코드 user.update(dto); cacheManager.evict("users", userId); }깔끔하다. 검증 먼저, 업데이트 다음, 캐시 삭제 마지막. 근데 생각해보니 validateUserStatus는 private 메서드로 만들어야 한다. 외부에서 부를 필요 없다. 박주니어 코드 확인했다. public으로 되어 있다. 고쳤다. private void validateUserStatus(User user) { if (user.isWithdrawn()) { throw new UserStatusException("탈퇴한 회원입니다."); } }테스트는 더 간단했다. 두 테스트 메서드 순서만 정리. @Test void updateUser_정상동작() { // 기존 테스트 }@Test void updateUser_탈퇴유저_검증() { // 박주니어 테스트 }@Test void updateUser_캐시_삭제_확인() { // 내 테스트 }저장했다. 빌드 돌려보기 ./gradlew clean build기도했다. 제발 한 번에 성공해라. 30초 후. BUILD SUCCESSFUL in 28s됐다! 근데 테스트 결과 확인해야 한다. ./gradlew test --tests UserServiceTest전부 통과했다. 124개 테스트 모두 초록불. 안심이다.푸시하기 전 확인 박주니어한테 메시지 보냈다. "주니어님, 코드 합쳤어요. 제가 좀 수정한 게 있는데 괜찮을까요?" "뭐 수정하셨어요?" "validateUserStatus를 private으로 바꿨어요. 이 메서드는 UserService 내부에서만 쓰니까요." "아 맞네요! 그게 맞는 것 같아요." "그리고 validators 패키지는 일단 안 만들었어요. 나중에 검증 로직이 더 많아지면 그때 분리하는 게 좋을 것 같아서요." 잠시 답이 없었다. 3분쯤 지났다. "네 알겠습니다. 그게 나을 것 같네요." 목소리가 조금 아쉬운 것 같다. 그래도 이해는 하는 것 같다. "주니어님 코드 아이디어는 좋았어요. 검증 로직 분리하는 거. 나중에 꼭 해봅시다. 지금은 검증이 하나뿐이라 오버 엔지니어링일 수 있어서요." "아 네! 감사합니다 형님!" 다시 밝아졌다. 푸시와 PR git add . git commit -m "Merge: user validation + cache eviction" git push origin develop푸시했다. 슬랙에 알림이 떴다. GitHub Actions 돌아간다. CI 파이프라인이 돌아간다. 빌드, 테스트, 린트 체크. 2분 후. 초록불. 박주니어 PR에 코멘트 남겼다. "LGTM. 제 hotfix랑 합쳤습니다. 확인 부탁드려요." 1분 후. 박주니어가 approve 했다. "확인했습니다! 감사합니다 형님!" 머지 버튼 눌렀다. develop 브랜치에 합쳐졌다. 끝났다. 오후 3시, 후속 작업 근데 문제가 또 있다. 이찬민 팀장이 슬랙 메시지 보냈다. "김개발님, user 관련 코드 왜 두 번 커밋됐어요? 금요일이랑 오늘이랑." 아차. PR 안 올리고 바로 푸시한 거 들켰다. "금요일에 긴급 버그 수정이었습니다. PR 올릴 시간이 없어서..." "그래도 PR은 올려야죠. 팀 룰이잖아요." 맞는 말이다. "죄송합니다. 다음부터 조심하겠습니다." "알겠습니다. 근데 주니어님이랑 충돌 안 났어요?" "났습니다. 제가 합쳤어요." "수고하셨어요." 휴. 팀장은 착하다. 다른 회사 동기들 얘기 들으면 우리 팀장은 천사다. 회고 merge conflict는 피할 수 없다. 팀 프로젝트니까. 중요한 건 충돌을 어떻게 해결하느냐다. 오늘 배운 것들:hotfix도 PR 올리기: 급해도 기록은 남겨야 한다. 나중에 누가 왜 고쳤는지 알아야 한다.폴더 구조 변경은 함께: 좋은 의도라도 혼자 결정하면 안 된다. 팀원들이 헷갈린다.conflict 해결은 소통으로: 내 코드가 맞다고 우기지 말기. 상대방 코드 의도 먼저 이해하기.테스트는 꼭 돌리기: 합쳤다고 끝이 아니다. 빌드 성공해도 테스트 실패할 수 있다.후배 기분 챙기기: 코드 리뷰는 코드만 보는 게 아니다. 사람도 봐야 한다.박주니어는 좋은 개발자가 될 것 같다. 의욕도 있고 배우려고 한다. 나도 저랬다. 7년 전에. 그때 선배들이 잘 가르쳐줬다. 지금 내가 그 역할이다. 책임감이 든다. 무겁다. 퇴근길 6시 15분에 나왔다. 평소보다 늦었다. conflict 해결하느라. 지하철에서 폰 봤다. 슬랙 알림 3개. "김개발님 내일 회의 때 validators 패키지 건 같이 얘기해봐요!" 박주니어다. "네 좋습니다." 답장 보냈다. 집에 도착했다. 7시 10분. 아내가 저녁 만들고 있다. 김치찌개 냄새가 난다. "오빠 늦었네?" "응. merge conflict." "뭐 그게 뭐야?" 설명 안 했다. 길다. "그냥 코드 충돌." "힘들었겠다." "응." 밥 먹었다. 맛있다. 넷플릭스 틀었다. 아무 생각 없이 봤다. 10시에 잤다. 내일 또 회의다.충돌은 일상이다. 해결하면 된다.

기술 부채를 상환해야 하는데 새로운 기능을 요청받다

기술 부채를 상환해야 하는데 새로운 기능을 요청받다

월요일 아침, 슬랙 한 방 출근하자마자 슬랙이 울렸다. 기획팀 이수진 님: "개발님, 이번 주 스프린트에 소셜 로그인 추가 가능할까요? 사용자 요청이 많아서요 🙏" 커피도 안 마셨다. 마우스 커서가 멈췄다. 지난주 금요일에 작성한 리팩토링 계획서를 열었다. "레거시 인증 모듈 개선 - 예상 기간 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주 동안 새 기능 개발이 멈추는 거죠?" "네." "다음 다음 스프린트에 넣을게요." 이게 네 번째다. 회고가 끝났다. 다들 자리로 돌아갔다. 나는 리팩토링 계획서를 열었다. 날짜를 수정했다. 다음 다음 스프린트로. 어차피 또 밀릴 거다. 알고 있다.오늘도 빚을 쌓았다. 내일도 쌓을 거다.

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

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

프로젝트 데드라인 일주일 앞두고 하는 생각들 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년째 이 일 하는데, 아직도 적응 안 된다. 적응하면 안 되는 건가. 적응했다는 건 포기했다는 거니까. 눈 감았다. 내일 또 생각하자. 아니, 모레. 일요일은 쉬어야지. 근데 월요일 킥오프 미팅 자료 준비는 해야 하나. 일요일 저녁에 할까. 또 그렇게 된다. 항상 그렇다.다음 주 월요일, 또 '될 것 같은' 착각으로 시작할 거다. 그게 개발자다.