domain과 infra를 나누며 백엔드 리팩토링을 시작했다
운영 중인 레거시 서비스를 AI와 함께 점진적으로 리팩토링 하기
- 01운영 중인 서비스를 왜 지금 리팩토링하기로 했나
- 02왜 Spring Boot, Gradle, React Router v7로 방향을 정했나
- 03리팩토링 전에 Playwright 테스트부터 만든 이유
- 04AI가 잘 이해할 수 있는 구조를 먼저 만들었다
- 05Gradle 전환과 미사용 코드 정리부터 시작한 이유
- 06domain과 infra를 나누며 백엔드 리팩토링을 시작했다← 현재
domain과 infra를 나누며 백엔드 리팩토링을 시작했다
Gradle 전환과 미사용 코드 정리를 어느 정도 마치고 나서야, 비로소 "원래 하고 싶었던 일"에 가까운 작업을 시작할 수 있었다. controller와 service에 몰린 책임을 나누고, persistence 의존을 격리하고, 앞으로 /api/v1를 붙일 수 있는 구조를 준비하는 일이다.
그런데 여기서도 처음부터 모든 걸 분해하려고 하지 않았다. 운영 중인 서비스에서 거대한 service를 한 번에 없애겠다고 들어가면, 결국 리팩토링이 아니라 큰 폭발이 된다. 그래서 이번 단계도 기준은 같았다.
한 번에 다 바꾸지 말고, 가장 작은 축부터 실제로 떼어보자.
이번 글은 왜 posting 도메인을 첫 번째 분리 대상으로 잡았는지, 그리고 왜 Spring Data MongoDB를 "전면 도입"이 아니라 "첫 슬라이스에 먼저 적용"하는 방식으로 가져갔는지를 정리한 글이다.
왜 PostDataService부터 손봐야 했나
이 프로젝트에서 가장 눈에 띄는 거대한 service 중 하나가 PostDataService였다. 이름만 보면 게시글 데이터 접근을 담당하는 서비스처럼 보이지만, 실제로는 그보다 훨씬 많은 책임을 갖고 있었다.
- 게시글 최신본 조회
- revision 조회
- board 관련 처리
- comment 관련 처리
- file 관련 처리
- subscribe / consent 처리
- reader / link / code 처리
즉, "게시글 데이터 서비스"라기보다 여러 도메인과 저장소를 한 손에 쥐고 있는 거대한 집합 서비스에 가까웠다.
이런 구조에서는 두 가지 문제가 생긴다.
첫째, 어느 기능을 바꾸더라도 이 service 전체를 계속 의식해야 한다.
둘째, persistence 전략을 바꾸기가 어렵다. 예를 들어 MongoTemplate을 줄이고 Spring Data MongoDB repository로 옮기고 싶어도, service 안에 너무 많은 쿼리가 섞여 있으면 이동 단위를 잡기 어렵다.
그래서 이 service를 한 번에 없애는 대신, service가 들고 있는 책임 중 일부를 먼저 외부로 빼내는 것을 목표로 잡았다.
왜 posting을 첫 번째 슬라이스로 골랐나
도메인을 나누기 시작할 때는 무엇부터 자를지가 중요하다. 잘못 자르면 리팩토링 난이도만 올라가고, 구조는 더 복잡해진다.
이번에는 posting을 첫 번째 슬라이스로 골랐다. 이유는 비교적 단순했다.
- 서비스의 핵심 흐름 대부분이 게시글을 중심으로 움직인다
post_last는 현재 서비스의 최신 상태를 담는 핵심 컬렉션이다- 검색, 상세, 목록, 작성, 관리자 화면 대부분이 결국 게시글 최신본을 참조한다
- 댓글, 첨부, 구독, 동의는 나중에 별도 도메인으로 나누더라도 우선 중심 축은
posting이다
즉 posting을 먼저 분리하면 나머지 도메인을 자르는 기준도 같이 생긴다. 반대로 처음부터 comment, attachment, board를 다 같이 나누려 하면 서비스가 너무 많이 흔들린다.
그래서 첫 번째 목표는 이렇게 정했다.
post_last를 담당하는 저장소를 domain / infra 경계 밖으로 빼고, 레거시 service는 그 저장소를 호출하는 쪽으로만 바꾸자.
이건 작아 보이지만 꽤 중요한 기준점이었다.
왜 Spring Data MongoDB를 여기서 도입했나
기존 코드는 MongoTemplate 중심으로 작성돼 있었다. 이 방식이 무조건 나쁘다고 생각하지는 않는다. 복잡한 쿼리나 aggregation을 다룰 때는 지금도 충분히 유효한 도구다. 하지만 지금 프로젝트에서는 MongoTemplate이 너무 넓은 범위에 직접 퍼져 있었다.
문제는 단순히 기술 취향이 아니라 경계였다.
비즈니스 로직에 가까운 service가 직접 MongoTemplate을 쓰기 시작하면, 저장소 전략이 service 안으로 침투한다. 쿼리 방식과 컬렉션 이름, 정렬, skip/limit 같은 저장소 세부 사항이 비즈니스 코드와 섞인다. 이렇게 되면 나중에 DB 전략을 바꾸거나, MariaDB 이관을 준비할 때 더 어렵다.
그래서 이번 리팩토링의 목표 중 하나는 "Mongo를 없애는 것"이 아니라, Mongo 의존을 비즈니스 로직 밖으로 밀어내는 것이었다. 그리고 그 출발점으로 Spring Data MongoDB를 도입했다.
여기서 중요한 건 Spring Data를 "전부 다" 한 번에 도입하지 않았다는 점이다.
우선 posting의 최신본 조회처럼 비교적 명확한 CRUD / 단건 조회 / 간단한 목록 조회부터 repository로 옮겼다. 복잡한 aggregation이나 넓은 검색은 그대로 MongoTemplate에 남겨두더라도, 가장 중심이 되는 저장소 경계부터 분리할 수 있었다.
이건 결국 기술 도입이라기보다 경계 이동에 가까웠다.
실제로는 "새 구조"와 "레거시 구조"가 같이 있었다
이 단계에서 중요했던 또 하나의 원칙은, 기존 코드를 당장 다 지우지 않는 것이었다.
예를 들어 PostDataService를 보더라도, 처음부터 모든 메서드를 새 repository로 옮기지 않았다. 대신 getPost, getPostById, getNextPost, getPreviousPost, insertPost의 일부처럼 비교적 명확한 흐름부터 새 repository를 거치도록 바꿨다.
그리고 레거시 Post 객체와 새 domain model 사이에는 mapper를 하나 두었다. 처음엔 이게 조금 우회적으로 느껴질 수도 있다. 하지만 운영 중인 서비스에서는 이 정도 완충지대가 오히려 필요하다. 기존 controller와 view는 당장 Post를 기대하고 있는데, 내부에서 새 domain model을 도입하려면 중간 브리지가 있어야 한다.
즉, 이 단계의 구조는 이렇게 요약할 수 있다.
- 레거시 controller / service는 아직 살아 있다
- 하지만 일부 저장소 의존은 새 domain / infra로 옮겨간다
- 중간에는 mapper가 있다
- 조금씩 범위를 넓혀간다
이 방식의 장점은 명확했다. 서비스는 계속 돌아가고, 새로운 구조는 조금씩 실제 역할을 갖기 시작한다.
모듈은 뼈대가 아니라 실제 역할을 가져야 했다
처음 멀티모듈 뼈대를 만들었을 때는 core, support 같은 폴더가 사실상 빈 껍데기에 가까웠다. 그건 어쩔 수 없는 출발점이었다. 하지만 그 상태에 오래 머무르면 오히려 "겉으로만 나뉜 단일 프로젝트"가 된다.
그래서 이번 단계의 중요한 기준은, 적어도 하나의 도메인이라도 실제로 그 모듈 안에서 역할을 하게 만드는 것이었다.
예를 들어:
core/domain에는Posting,PostingRepositorycore/infra/database/mongodb에는PostingLastDocument,PostingLastMongoRepository,SpringDataPostingRepositoryapplication에는 레거시에서 새 구조를 호출하는 mapper와 service 조정 코드
이렇게 실제 역할이 들어가야, 이후 comment, board, attachment도 같은 패턴으로 분리할 수 있다.
결국 첫 슬라이스의 목적은 기능 완성이 아니라 패턴 만들기였다.
이 작업을 하면서 더 확신하게 된 것
이 단계를 지나면서 몇 가지는 더 분명해졌다.
첫째, 거대한 service를 한 번에 없애려 하면 실패한다.
둘째, persistence 기술 교체보다 먼저 경계 이동이 필요하다.
셋째, 멀티모듈은 폴더를 나누는 일이 아니라 책임을 옮기는 일이다.
넷째, 레거시와 신규 구조의 공존은 부끄러운 게 아니라 운영 서비스를 위한 전략이다.
특히 네 번째는 이번 리팩토링 전체에서 중요한 태도라고 생각한다. 많은 경우 새 구조를 만들기 시작하면 기존 구조를 빨리 지우고 싶어진다. 하지만 운영 중인 서비스에서는 그 조급함이 오히려 더 위험하다. 지금 필요한 건 "새 구조의 순도"보다 "전환 가능한 흐름"이다.
앞으로 남은 작업
물론 이 단계가 끝났다고 해서 리팩토링이 끝난 것은 아니다. 오히려 이제 시작에 가깝다.
앞으로 해야 할 일은 훨씬 많다.
- comment, attachment, board, subscription, consent 분리
/api/v1병행 추가- Thymeleaf controller를 점점 thin adapter로 축소
- 프론트 React Router v7 SSR 연결
- 인증은 세션 유지 후 나중에 JWT 전환
- Mongo 유지 구조 위에서 나중에 MariaDB + Flyway 이관
하지만 중요한 건, 이제 이 작업들을 할 수 있는 구조적 출발점은 만들어졌다는 점이다. 이전에는 모든 것이 PostDataService 안에 섞여 있었지만, 이제는 적어도 "어떻게 잘라나갈 것인가"에 대한 기준이 생겼다.
마무리
domain과 infra를 나누기 시작한 이번 단계는, 겉으로 보면 아주 작은 변화처럼 보일 수 있다. 실제 서비스는 여전히 돌아가고 있고, controller도 남아 있고, 레거시 service도 완전히 사라지지 않았다. 하지만 이 작은 분리가 이후 리팩토링 전체의 기준점이 된다고 생각한다.
리팩토링은 종종 "크게 갈아엎는 일"처럼 보이지만, 실제 운영 서비스에서는 오히려 이렇게 작고 반복 가능한 패턴을 만드는 일이 더 중요하다. posting부터 domain과 infra를 나누기 시작한 것도, 결국 그 패턴을 만드는 첫 단계였다.
이 시리즈는 여기서 끝나지 않는다. 다음 단계에서는 /api/v1를 본격적으로 준비하고, 백엔드가 React Router v7 프론트와 자연스럽게 연결될 수 있는 구조를 어떻게 만들어갈지 계속 정리해보려고 한다.