SideImpact 개선기 2편: 무거운 Router 덜어내기

L-cloud·2025년 9월 16일
0

sideimpact

목록 보기
2/2
post-thumbnail

이전 글에서는 개발 환경 배포 자동화를 공유했습니다. 이번 글은 코드 리팩토링 여정을 다룹니다.

sideimpact.io 도메인 알아보기

이야기를 시작하기 전에, 서비스를 간단히 소개해야 할 것 같습니다. sideimpact.io커뮤니티 기반의 공모 사업 플랫폼입니다.

전체적인 흐름은 다음과 같습니다.

  1. 지원(Apply): 지원가 자신의 아이디어를 프로젝트로 만들어 특정 라운드에 지원합니다.
  2. 발표(Announce): 지원 자격을 갖춘 프로젝트들이 커뮤니티에 공개됩니다.
  3. 리뷰(Review): 커뮤니티 멤버들이 발표된 프로젝트에 대한 리뷰를 작성합니다.
  4. 투표(Vote): 리뷰 기간이 끝나면, 멤버들은 마음에 드는 프로젝트에 투표합니다.
  5. 완료(Complete): 최종 결과가 발표되고 라운드가 종료됩니다.

단순해 보이는 흐름이지만, 실제로는 사용자(User), 라운드(Round), 리뷰(Review), 투표(Vote) 등 다양한 도메인이 Stage별로 복잡하게 얽혀 있습니다. 프로젝트 하나가 완료되기까지, 이 모든 도메인이 서로를 참조하며 동작해야 합니다.

간결한 구조와 간결하지 않은 Router

그렇다면 이 흐름을 기존 코드는 어떻게 표현하고 있었을까요? 기존 코드는 각 도메인을 Model, Service, Repository가 1:1:1로 대응하는, 간결한 구조를 채택했습니다.

  • Project 모델 → ProjectService → ProjectRepository
  • Review 모델 → ReviewService → ReviewRepository
  • Vote 모델 → VoteService → VoteRepository

하지만 이 구조는 지저분한 코드를 양산 했습니다.
예를 들어, '사용자가 프로젝트에 리뷰를 남기는' 간단해 보이는 기능 하나를 처리하려면, 코드는 수많은 질문에 답해야 합니다.

  • "지금은 리뷰 기간이 맞나?" (RoundService에 물어봐야 함)
  • "이 사용자는 커뮤니티 멤버인가?" (UserService에 물어봐야 함)
  • "혹시 자기 자신의 프로젝트에 리뷰를 남기려는 건 아닌가?" (ProjectService에 물어봐야 함)

1:1:1 구조에서는 각 Service가 자신의 도메인에 대한 정보밖에 모릅니다. 결국 이 모든 질문에 답하고, 흐름을 지휘하고, 최종적으로 리뷰를 저장하는 책임은 전부 Router 계층에 떠넘겨졌습니다.

그 결과, '리뷰 작성 API'의 Router와 '투표 진행 API'의 Router는 서로 다른 기능임에도 불구하고, '지금 라운드 단계가 맞는지?', '사용자가 커뮤니티 멤버인지?' 같은 비슷한 로직을 중복해서 가질 수밖에 없었습니다. 알림을 보내는 로직 또한 여러 Router에 흩어져 있었습니다.

Facade Service: 비즈니스 흐름을 지휘하는 새로운 계층

물론 다시 처음부터 구조를 그릴 수도 있었겠지만, 몇 가지 현실적인 이유를 고려해야 했습니다.
우선, 기존 코드가 이미 안정적으로 동작하고 있었기에 핵심 로직은 최대한 유지하며 변화를 최소화하고 싶었습니다. 또한, 모든 코드를 완벽히 이해하고 갈아엎기엔 시간과 리소스가 한정적이었습니다. 기존 구조의 핵심은 유지하되, 문제점만 개선할 방법을 찾고 싶어서 Facade pattern 도입을 하였습니다.

이를 적용해 Facade Service라는 새로운 계층을 Router와 기존 Domain Service 사이에 도입했습니다.

새로운 구조에서 각 계층의 책임은 재정의되었습니다.

  • Router: 오직 HTTP 요청과 응답이라는 창구 역할만 수행
  • Facade Service: 여러 Domain Service를 지휘하여 실제 비즈니스 로직을 처리
  • Domain Service: 1:1 원칙을 지키며 단일 도메인의 데이터 처리만 담당

이 변화로, 여러 Router에 중복되어 있던 권한 확인, 상태 체크, 알림 발송 로직은 각각의 Facade Service라는 단 한 곳으로 응집되었습니다.

테스트 가능한 구조로의 전환

이 구조가 가져다준 하나의 큰 선물은 바로 '테스트 용이성'의 향상이었습니다.

이전 구조를 다시 떠올려 볼까요? 모든 비즈니스 로직의 조합이 Router에 있었습니다. 즉, '리뷰 작성'이라는 하나의 비즈니스 흐름을 검증하려면 반드시 API 요청부터 시작하는 통합 테스트를 수행해야만 했습니다.

하지만 Facade Service의 도입으로 아래와 같은 테스트 코드를 작성할 수 있게 되었습니다.

  • Domain Service : 여전히 자신의 도메인에만 집중하므로, 간단한 단위 테스트로 안정성을 확보할 수 있습니다.

  • Facade Service: Router 계층에 흩어져 있던 비즈니스 로직을 품게 되면서, 테스트의 범위를 넓혔습니다. "리뷰 기간이 아닐 때 리뷰를 작성하려 하면 막아내는가?", "자신의 프로젝트에 리뷰/투표가 불가능한가?" 와 같은 복잡한 시나리오를 API 호출 없이도 명확하고 빠르게 검증할 수 있게 되었고, 덕분에 서비스 레벨 테스트 커버리지를 대략 10%p 향상시켰습니다.

느낌점

이번 작업을 통해 도메인 이해도를 높일 수 있었고, 코드도 미약하나 개선할 수 있었습니다. 또한 다른 사람이 작성한 코드를 오랜 시간 들여다보며 수정한 경험은 처음이었는데, 코드에서 작성자의 성격과 고민을 느낄 수 있었습니다. 코드를 통해 이와 같은 것을 알 수 있다는 것이 참 재미있었습니다.

물론 제가 도입한 구조도 완벽하지는 않습니다. Facade Service가 너무 비대해질 위험도 있고, 계층이 하나 더 늘어난 만큼 복잡도가 증가한 측면도 있습니다. 앞으로 서비스를 운영하며 또 다른 개선점을 찾아 나아가야 할 것입니다.

작지만 의미 있는 변화들

1편과 2편에서 다루지 않은 소소한 변화도 있습니다. Docker 이미지 크기를 60% 이상 줄였고, Poetry에서 UV로 패키지 매니저를 전환했으며, 알림 발송 추적 시스템도 도입했습니다. 이런 이야기들도 기회가 되면 하나씩 공유해보겠습니다.

긴 글 읽어주셔서 감사합니다 :)

profile
내가 배운 것 정리

0개의 댓글