운영 중인 서비스를 왜 지금 리팩토링하기로 했나
운영 중인 레거시 서비스를 AI와 함께 점진적으로 리팩토링 하기
- 01운영 중인 서비스를 왜 지금 리팩토링하기로 했나← 현재
- 02왜 Spring Boot, Gradle, React Router v7로 방향을 정했나
- 03리팩토링 전에 Playwright 테스트부터 만든 이유
- 04AI가 잘 이해할 수 있는 구조를 먼저 만들었다
- 05Gradle 전환과 미사용 코드 정리부터 시작한 이유
- 06domain과 infra를 나누며 백엔드 리팩토링을 시작했다
운영 중인 서비스를 왜 지금 리팩토링하기로 했나
새 프로젝트를 시작할 때와 운영 중인 서비스를 다시 손볼 때의 감정은 꽤 다르다. 새 프로젝트를 시작할 때는 "이번엔 제대로 해보자"는 기대가 먼저 앞선다. 반면 운영 중인 서비스를 리팩토링할 때는 기대보다 걱정이 먼저 든다. 지금도 돌아가고 있는 기능을 건드려야 하고, 이미 사용 중인 사용자 흐름이 있고, 요구사항은 기다려주지 않는다. 이 글은 바로 그런 상황에서, 왜 내가 이 서비스를 지금 리팩토링하기로 했는지, 그리고 왜 처음부터 다시 만드는 대신 점진적으로 바꾸는 방식을 선택했는지를 정리한 기록이다.
문제는 코드가 아니라 속도였다
리팩토링을 결심한 이유를 한 문장으로 줄이면 이렇다.
기능은 계속 붙여야 하는데, 코드는 점점 건드리기 어려워지고 있었다.
처음에는 익숙한 불편함 정도로 생각했다. 오래된 프로젝트라면 어느 정도 구조적 부채가 있는 것이 자연스럽다고 여겼고, 조금 불편해도 필요한 기능만 잘 추가하면 된다고 생각했다. 그런데 시간이 지날수록 문제는 단순한 불편함이 아니었다.
새 요구사항이 들어올 때마다 먼저 드는 생각이 "어디를 고쳐야 하지?"가 아니라 "이거 건드리면 어디가 같이 깨질까?"가 됐다. 기능을 추가하는 시간이 오래 걸리는 것보다 더 큰 문제는, 변경의 영향 범위를 빠르게 설명하기 어려웠다는 점이다. 코드를 수정하는 사람 입장에서도 부담이 컸고, 나중에 다시 같은 코드를 보는 미래의 나에게도 친절하지 않은 상태였다.
이건 결국 생산성의 문제였다. 개발 속도는 기능 수가 아니라 변경 비용에 의해 결정된다고 생각하는데, 그 변경 비용이 점점 올라가고 있었다.
기존 구조가 힘들었던 이유
코드를 조금만 들여다보면 이유는 분명했다.
첫째, 비즈니스 로직이 controller와 service에 과하게 몰려 있었다. 단순히 요청을 받고 응답을 만드는 수준이 아니라, 여러 도메인 규칙과 후처리, 저장 로직, 예외 흐름이 한 곳에 뭉쳐 있었다. 한 메서드 안에서 해야 할 일이 너무 많아지면, 코드는 당장은 돌아가도 나중에 바꾸기 어려워진다.
둘째, setter 중심의 변경 방식이 많았다. 객체를 만들어서 한 번에 완성하는 방식보다, 여러 곳에서 조금씩 값을 바꾸는 코드가 많다 보면 "이 객체가 최종적으로 어떤 상태가 되는지"를 머릿속으로 계속 추적해야 한다. 이건 작은 프로젝트에서는 버틸 수 있지만, 운영 중인 서비스에서는 피로도가 굉장히 높다.
셋째, 오래전에 fork 떠온 코드가 그대로 남아 있었다. 당시에는 빠르게 가져와 기능을 붙이는 것이 중요했겠지만, 시간이 지나면서 실제로 쓰지 않는 코드도 많이 남아 있게 됐다. 문제는 이 코드가 단순히 "안 쓰는 파일"로만 남아 있는 게 아니라, 지금 서비스에 필요한 코드와 섞여 있다는 점이었다. 즉, 바꾸기 전에 먼저 "무엇이 살아 있고 무엇이 죽어 있는지"부터 구분해야 했다.
넷째, 지금 구조로는 앞으로 들어올 요구사항을 감당하기 어렵다고 판단했다. 이건 매우 현실적인 이유다. 리팩토링은 종종 "코드를 예쁘게 만들고 싶어서" 하는 일처럼 보이지만, 실제로는 앞으로의 개발 비용을 줄이기 위한 선택에 가깝다. 계속 기능을 붙여야 하는 서비스라면 더더욱 그렇다.
그런데 왜 한 번에 갈아엎지 않았나
이쯤 되면 자연스럽게 이런 생각이 든다.
그냥 새로 만드는 게 낫지 않나?
나도 그 생각을 안 한 것은 아니다. 오히려 처음에는 새로 만드는 쪽이 더 깔끔해 보였다. 기존 구조를 억지로 살리기보다, 새로운 구조를 제대로 설계해서 새 프로젝트처럼 밀어붙이는 편이 훨씬 시원해 보였다.
그런데 운영 중인 서비스에서는 "깔끔함"보다 "전환 가능성"이 더 중요하다.
이미 사용자들이 쓰고 있는 서비스였고, 운영 중이었고, 중간에 요구사항도 계속 들어오는 상황이었다. 이런 상태에서 전면 재작성은 설계적으로는 멋질 수 있어도, 일정과 위험도 측면에서는 부담이 컸다. 새 구조를 다 만든 뒤 한 번에 바꾸는 방식은, 성공하면 좋지만 실패 비용이 너무 크다. 무엇보다 기존 서비스의 현재 동작을 정확히 모르는데 새로 만드는 방식은 더 위험하다.
그래서 이 프로젝트는 처음부터 점진적 리팩토링을 전제로 다시 보기 시작했다.
내가 필요했던 것은 "완벽한 새 시스템"이 아니라, "기존 서비스를 유지하면서 조금씩 구조를 바꿔갈 수 있는 방식"이었다.
이 판단이 이번 리팩토링의 출발점이었다.
기술 선택은 개인 취향보다 팀의 기준이 중요했다
리팩토링 방향을 정하면서 기술 스택도 같이 다시 봤다.
백엔드는 Spring Boot와 Gradle로 가기로 했다. 프론트는 React Router v7 기반으로 다시 가져가기로 했다. 이 선택은 "요즘 이게 좋아 보여서"라기보다, 회사에서 실제로 사용하는 기술 스택에 맞추는 것이 중요하다고 판단했기 때문이다.
운영 중인 서비스는 혼자 소유하는 코드가 아니다. 지금 내가 개발하더라도, 나중에는 다른 사람이 보고 유지해야 한다. 회사에서 이미 익숙한 스택을 중심으로 정리해두면 협업도 쉬워지고, 문제를 공유하기도 쉽고, 새로운 기능을 붙일 때도 공통 기반을 활용할 수 있다. 반대로 개인적으로 선호하는 도구를 가져와도 팀이 같이 유지하지 못하면 결국 더 큰 비용이 된다.
그래서 백엔드는 Spring Boot + Gradle, 프론트는 React Router v7 SSR을 기준으로 잡았다. 다만 한 가지는 분명히 했다. 프론트는 바로 갈아타지 않고, 백엔드를 먼저 정리하기로 했다. 지금 시점에서 가장 급한 건 렌더링 기술의 교체가 아니라, 구조적으로 변경 가능한 백엔드를 만드는 일이었기 때문이다.
프론트를 잘 모르기 때문에 더 구조가 중요했다
이번 작업에서 AI를 적극적으로 같이 쓰기로 한 것도 같은 맥락이다.
솔직히 말하면, 나는 프론트엔드를 깊게 공부해본 경험이 많지 않다. 화면을 고치고 동작을 맞추는 정도는 가능하지만, React Router v7 기반으로 구조를 새로 잡고 SSR 관점에서 설계를 정리하는 건 혼자 밀어붙이기엔 자신 있는 영역이 아니었다.
그래서 사내 프론트엔드 개발자의 도움을 받아 큰 방향을 먼저 잡고, 실제 구현 과정에서는 AI를 적극적으로 활용하기로 했다. 그런데 여기서 금방 깨달은 점이 있었다. AI가 잘 도와주려면, 먼저 프로젝트 구조가 AI가 이해하기 쉬운 형태여야 한다는 점이다.
파일이 뒤섞여 있고, 책임 경계가 불분명하고, 어디가 진입점이고 어디가 공통 모듈인지도 헷갈리는 상태에서는 사람도 힘들고 AI도 힘들다. 오히려 AI를 쓰기 때문에 더 명확한 구조가 필요했다.
그래서 /backend, /frontend로 최상단을 먼저 나누고, 백엔드는 멀티모듈 구조로 정리하고, 도메인과 인프라를 점진적으로 분리하는 방향을 잡았다. 이건 단순히 "예쁜 구조를 만들기 위해서"가 아니라, 사람과 AI가 같은 그림을 보면서 협업할 수 있게 하기 위한 선택이었다.
리팩토링의 첫 단계는 코드 개선이 아니었다
흥미로웠던 점은, 실제로 가장 먼저 한 일이 코드 리팩토링이 아니었다는 점이다.
처음부터 PostDataService를 쪼개거나 controller를 정리하지 않았다. 그 전에 먼저 해야 할 일이 있었다.
첫 번째는 현재 서비스의 동작을 고정하는 일이었다. 운영 중인 서비스에서는 "지금이 정상인지"를 먼저 알아야 한다. 그래서 Playwright로 현재 화면 동작을 먼저 테스트로 만들기 시작했다. 홈, 검색, 상세, 작성 화면, 관리자 페이지 같은 주요 흐름을 기준선으로 잡았다.
두 번째는 실제로 사용되는 코드와 사용되지 않는 코드를 구분하는 일이었다. fork 떠온 코드에는 생각보다 많은 dead code가 남아 있었고, 이걸 정리하지 않으면 리팩토링 범위 자체가 계속 흔들린다. 어떤 화면과 어떤 JS, 어떤 controller가 실제로 살아 있는지부터 추적해야 했다.
세 번째는 실행 가능한 상태를 유지한 채 구조를 옮기는 일이었다. Maven에서 Gradle로 옮기고, 백엔드와 프론트를 나누고, 기존 앱을 일단 실행 가능한 모듈 안에 수용하는 작업을 먼저 했다. 즉, 리팩토링의 초반은 "구조 개선"이라기보다 "안전하게 이동할 수 있는 기반 만들기"에 가까웠다.
이 과정을 지나고 나서야 비로소 도메인 분리, Spring Data MongoDB 도입, /api/v1 정리 같은 실제 리팩토링 작업을 시작할 수 있었다.
이번 시리즈에서 다루고 싶은 것
이 시리즈는 단순히 "레거시 코드를 이렇게 바꿨다"는 결과 정리보다는, 왜 그런 순서로 판단했고 어떤 기준으로 움직였는지를 기록하려고 한다. 구체적으로는 이런 흐름을 다룰 생각이다.
- 왜 이 시점에 리팩토링이 필요했는가
- 왜 이 기술 스택과 구조를 선택했는가
- 왜 테스트를 먼저 만들었는가
- 왜 AI가 이해하기 쉬운 구조를 먼저 만들었는가
- Gradle 전환과 미사용 코드 정리를 어떻게 시작했는가
- 백엔드 리팩토링을 어떤 순서로 진행했는가
지금의 관심사는 "완성된 아키텍처를 자랑하는 것"이 아니라, 운영 중인 서비스를 실제로 어떻게 점진적으로 바꾸고 있는지를 솔직하게 남기는 것이다.
마무리
정리하면, 이번 리팩토링의 출발점은 기술 욕심이 아니었다. 새로운 구조를 만들어보고 싶어서가 아니라, 지금 구조로는 더 이상 기능 추가 비용을 감당하기 어렵다는 현실적인 문제 때문에 시작한 일이었다. 그리고 그 문제를 풀기 위한 방식으로, 나는 전면 재작성보다 점진적 리팩토링을 선택했다.
운영 중인 서비스는 멈출 수 없고, 요구사항은 기다려주지 않는다. 그렇기 때문에 더더욱, 지금 당장 필요한 것은 "완벽한 새 시스템"이 아니라 "깨지지 않게 바꿔가는 방법"이라고 생각했다.
다음 글에서는 왜 이번 리팩토링의 기준 스택을 Spring Boot, Gradle, React Router v7로 정했는지, 그리고 왜 /backend, /frontend 구조를 먼저 나누는 판단을 했는지 정리해보려고 한다.