Showing Posts From

충돌

버전 관리 충돌: 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시에 잤다. 내일 또 회의다.충돌은 일상이다. 해결하면 된다.