기술

하나였던 Omnilude 백엔드를 AI와 함께 MSA로 전환한 과정

AAnonymous
12분 읽기

들어가며

이 글은 Omnilude를 만들기 위해, 이런 백엔드를 만들고 있습니다 다음에 이어지는 글입니다. 앞선 글에서 Omnilude 백엔드의 현재 구조와 책임 경계를 하나의 제품 백엔드처럼 소개했다면, 이번 글에서는 그 구조가 실제로 어떻게 하나의 큰 프로젝트에서 여러 서비스 경계로 나뉘기 시작했는지를 이야기해보려 합니다.

MSA 이야기는 자칫 추상적으로 들릴 때가 많습니다. 서비스를 나눴다, 독립 배포가 가능해졌다, 경계를 세웠다고 말은 쉬운데, 실제 프로젝트 안에서는 무엇이 어떻게 바뀌는지 잘 그려지지 않을 때가 많기 때문입니다.

Omnilude 백엔드는 그런 점에서 설명할 재료가 분명한 편입니다. 지금은 auth-service, backbone-service, storage-service, ai-service, game-service, blog-service, legacy-service처럼 여러 서비스 이름으로 나뉘어 있지만, 원래부터 이렇게 시작한 프로젝트는 아니었기 때문입니다. 처음에는 하나의 큰 Spring Boot 백엔드였습니다.

이번 글에서는 그 하나의 프로젝트가 왜 나뉘기 시작했는지, 왜 jspring(common)을 먼저 뽑았는지, 왜 legacy-service를 일부러 남겼는지, 그리고 AI가 이 전환에서 실제로 어떤 역할을 했는지를 가능한 쉽게 정리해보겠습니다.

처음에는 그냥 큰 백엔드 하나였습니다

전환 전의 Omnilude 백엔드는 아주 단순하게 말하면 한곳에 많은 것이 모여 있는 구조였습니다. 인증도 있고, AI도 있고, 게임도 있고, 파일 저장도 있고, 실시간 이벤트도 있고, 공통 유틸도 있었는데, 그 모든 것이 같은 프로젝트 안에 붙어 있었습니다.

처음에는 이런 구조가 편합니다. 기능을 붙일 때도 빠르고, 같은 코드베이스 안에서 바로 수정할 수 있기 때문입니다. 하지만 프로젝트가 커질수록 문제가 생깁니다.

  • 인증처럼 조심스럽게 다뤄야 하는 영역과
  • AI처럼 자주 실험하고 바뀌는 영역과
  • 실시간 이벤트처럼 안정성이 중요한 영역과
  • 게임처럼 도메인 로직이 빠르게 커지는 영역이

전부 같은 배포 압력을 받기 시작합니다.

쉽게 비유하면, 옷장 하나에 계절옷, 운동복, 정장, 이불, 공구함까지 다 넣어두는 것과 비슷합니다. 처음에는 편하지만, 어느 순간부터는 뭘 하나 바꾸는 일 자체가 점점 부담스러워집니다.

왜 굳이 나누려고 했나

저는 처음부터 MSA라는 형태 자체를 목표로 잡고 움직인 것은 아니었습니다. 오히려 더 현실적인 이유가 컸습니다.

가장 중요했던 이유는 연결을 오래 유지해야 하는 기능을 일반 배포 영향에서 떼어내고 싶었기 때문입니다. Omnilude 안에는 backbone-service처럼 WebSocket, SSE, 이벤트 스트림, 오래 걸리는 비동기 작업과 가까운 기능들이 있었습니다. 이런 영역은 다른 도메인의 수정 때문에 같이 흔들리면 운영 체감이 바로 나빠집니다. 블로그나 게임 기능 하나를 수정했는데도 연결성이 중요한 서비스까지 같이 재시작되거나 영향권에 들어가는 구조는 오래 끌고 가기 어려웠습니다.

두 번째는 배포 시간이었습니다. 작은 수정 하나를 해도 결국 큰 앱 전체를 빌드하고 이미지화하고 다시 올려야 했습니다. 프로젝트가 커질수록 이 시간은 점점 더 아깝게 느껴졌습니다. 자주 바뀌는 영역의 실험 속도를 높이고 싶어도, 배포 단위가 하나로 묶여 있으면 결국 전체가 같이 느려집니다.

그다음에는 책임 경계가 흐려졌습니다. 인증 규칙, 공통 예외, 내부 HTTP, 메시징, 파일 저장 같은 것들이 전체 코드베이스에 섞여 있으니, 무엇이 공통이고 무엇이 도메인인지 점점 모호해졌습니다.

마지막으로 더 큰 이유가 있었습니다. 앞으로 만들고 싶은 것이 분명했기 때문입니다. Omnilude에서는 AI 기능도 키우고 싶었고, 게임 서비스도 키우고 싶었고, 블로그나 도구 제품처럼 별도의 제품도 계속 붙이고 싶었습니다. 그런데 모든 것을 하나의 백엔드 안에서 계속 키우는 방식은 어느 순간부터 좋은 선택이 아니라고 판단했습니다.

즉, 이 전환은 멋있어 보여서 한 일이 아니라, 연결을 지켜야 하는 서비스는 보호하고, 배포 시간을 줄이고, 앞으로 만들고 싶은 것들을 계속 밀어 올리기 위해 구조를 다시 잡아야 했던 일에 더 가까웠습니다.

첫 단계는 서비스를 자르는 일이 아니라 공통 기준을 빼는 일이었습니다

여기서 중요한 점이 하나 있습니다. 이 전환의 첫 단계는 auth-service를 떼는 일도, ai-service를 떼는 일도 아니었습니다. 그 전에 먼저 common을 만들었습니다.

이 부분이 꽤 중요합니다. 서비스 분리는 겉으로 보면 도메인을 자르는 일처럼 보이지만, 실제로는 그 전에 무엇이 공통 규약인지 먼저 꺼내야 합니다.

Omnilude에서는 그 역할을 common이 맡았습니다.

  • 공통 예외 처리
  • JWT 규약
  • 공통 DTO
  • 웹 계층 유틸
  • JPA와 데이터 접근 기반
  • 내부 HTTP와 메시징 보조

이런 것들을 먼저 common으로 빼두니, 이후에 서비스를 나눌 때도 각 서비스가 제각각인 앱이 되지 않고 같은 언어를 쓰는 구조가 됐습니다.

쉽게 말하면, 방을 여러 개로 나누기 전에 각 방에서 공통으로 쓸 콘센트 규격과 문 손잡이 규격부터 맞춘 셈입니다. 그래야 나중에 방을 나눠도 생활이 가능해집니다.

분리는 아주 짧은 기간에 빠르게 진행됐습니다

실제 저장소 기록을 보면 전환은 놀랄 만큼 짧은 기간에 진행됐습니다. 2026년 1월 18일부터 22일 사이에 핵심 뼈대가 거의 잡혔습니다.

흐름을 단순하게 정리하면 이렇습니다.

  • common 추출
  • auth-service 분리
  • backbone-service 분리
  • storage-service 분리
  • ai-service 분리 진행
  • 남은 영역을 legacy-service로 격리
  • game-service 추출
  • 이후 blog-service를 새 서비스로 올림
  • Jenkins와 Kubernetes 기준의 선택 배포 구조 정리

이 순서에는 나름의 이유가 있었습니다.

auth-service는 경계가 비교적 명확했습니다. 인증, 계정, 권한은 다른 서비스가 공통으로 의존하기도 하고, 먼저 떼어두면 이후 구조를 잡는 기준점이 됩니다.

backbone-service는 실시간 이벤트, 메시징, DTE 같은 인프라 성격이 강했습니다. 자주 바뀌는 제품 로직과 분리해두는 편이 운영상 훨씬 낫다고 봤습니다.

storage-service는 여러 서비스가 공통으로 쓰는 파일 저장 계약을 담당하게 만들고 싶었습니다. 파일 저장을 한곳에 모아두면 나중에 블로그, 게임, AI 생성 결과가 모두 같은 흐름을 탈 수 있기 때문입니다.

그 다음이 ai-service였습니다. 이쪽은 규모도 크고 의존성도 많아서 난도가 높았습니다. 하지만 결국 Omnilude에서 가장 자주 실험하게 될 영역 중 하나였기 때문에 독립 서비스로 보는 편이 맞았습니다.

legacy-service를 남겨뒀나

처음 MSA 전환을 떠올리면 보통 전부 깔끔하게 분리해서 모놀리식을 없애는 그림을 상상하게 됩니다. 그런데 실제로 해보면 그렇게 한 번에 끝내는 방식은 생각보다 비현실적입니다.

그래서 Omnilude에서는 완전히 분리하지 못한 남은 도메인들을 legacy-service라는 이름으로 따로 묶었습니다.

이 결정은 후퇴라기보다 완충 장치에 가까웠습니다.

legacy-service가 있었기 때문에 가능한 일이 있었습니다.

  • 아직 안 옮긴 기능을 억지로 깨지 않아도 됐습니다.
  • 새 서비스와 옛 서비스가 같은 도메인 아래에서 공존할 수 있었습니다.
  • 분리 속도를 유지하면서도 운영을 계속할 수 있었습니다.
  • 더 어려운 도메인은 나중으로 미루고, 쉬운 것부터 먼저 독립시킬 수 있었습니다.

저는 이 부분이 오히려 현실적인 MSA 전환의 핵심이라고 생각합니다. 실제 전환은 완벽한 분리보다 운영 가능한 경계를 만드는 일에 더 가깝기 때문입니다.

게임, 블로그, AI는 그 위에서 더 명확해졌습니다

이후부터는 제품 성격이 더 강한 서비스들이 분리되기 시작합니다.

ai-service는 단순한 챗봇 API가 아니라, 에이전트 실행, 워크플로우, 생성 파이프라인, 미디어 생성, 역할극, 상품 에이전트처럼 꽤 많은 책임을 안고 있습니다. 이 정도가 되면 이미 하나의 독립 서비스로 보는 편이 훨씬 자연스럽습니다.

game-service는 Omnilude에서 결국 가장 크게 키우고 싶은 영역과 가까웠습니다. 스토리 퀴즈, 시나리오, 에셋 스튜디오 같은 기능이 붙으면서 AI와 스토리지, 인증을 함께 엮는 도메인 중심 서비스가 됐습니다.

blog-service는 조금 다릅니다. 이건 원래 모놀리식에서 바로 떼어낸 도메인이라기보다, MSA 구조 위에 새로 자리를 잡은 서비스에 가깝습니다. 이미 배포와 공통 규약이 분리된 상태였기 때문에, 블로그는 처음부터 독립 서비스처럼 설계하고 운영할 수 있었습니다.

즉, MSA 전환은 단순히 과거 것을 뜯어내는 일만이 아니었습니다. 그 이후 새로 만드는 제품들을 더 좋은 단위로 올릴 수 있게 만든 기반 작업이기도 했습니다.

진짜로 달라진 순간은 배포였습니다

코드만 보면 서비스가 나뉜 것처럼 보일 수 있습니다. 하지만 실제로 MSA가 체감되는 순간은 배포 구조가 바뀔 때입니다.

Omnilude에서는 이 부분이 Jenkinsfile, Jenkinsfile.prd, 그리고 kubernetes/services/**에 남아 있습니다.

지금 기준으로 Jenkins에서 선택 배포가 가능한 서비스는 아래 7개입니다.

  • auth-service
  • backbone-service
  • storage-service
  • ai-service
  • game-service
  • blog-service
  • legacy-service

즉, 같은 저장소 안에 있더라도 이제는 모든 것을 한 번에 배포하는 앱이 아니라, 필요한 서비스만 고를 수 있는 배포 구조가 된 것입니다.

Kubernetes도 같은 방향입니다. 각 서비스는 자기 DeploymentService를 가지고 있고, Ingressapi.omnilude.com 또는 dev-api.omnilude.com 아래에서 path prefix 기준으로 라우팅됩니다.

이 지점이 중요합니다. MSA는 폴더를 나누는 일이 아니라, 실제로 운영 단위가 갈라지는 일입니다. 저는 Omnilude에서 이 지점을 지나고 나서야 진짜로 분리됐다는 감각을 더 강하게 느꼈습니다.

AI는 여기서 어디까지 도움을 줬나

이 이야기를 하면서 빼놓을 수 없는 것이 AI입니다.

이 전환은 AI가 혼자 설계한 프로젝트는 아니었습니다. 어디를 먼저 떼어낼지, 무엇을 공통으로 볼지, 무엇을 legacy-service에 남길지 같은 판단은 결국 사람이 해야 했습니다.

하지만 AI가 속도를 크게 올려준 구간은 분명했습니다.

  • 새 모듈 뼈대 만들기
  • settings.gradle.kts, build.gradle.kts 초안 정리
  • 패키지 이동 보조
  • import와 참조 보정 반복
  • 설정 파일과 배포 매니페스트 초안 작성
  • 작업 문서와 체크리스트 정리

특히 이런 전환 작업은 한 번 멋지게 설계하는 일보다 수백 개의 작고 귀찮은 보정이 더 많은 편입니다. AI는 바로 그 부분에서 체감 효율이 컸습니다.

저장소에 남아 있는 fix-*.ps1 스크립트들이나 전환 커밋의 흔적을 보면, 이 작업이 얼마나 반복적이었는지도 드러납니다. 저는 이 경험을 통해 AI가 아키텍처 결정을 대신한다기보다, 사람이 정한 방향을 따라 대규모 이관 속도를 비정상적으로 끌어올리는 조수에 가깝다고 느꼈습니다.

지금 돌아보면 중요한 것은 서비스 수가 아니었습니다

전환을 하고 나면 자꾸 서비스를 몇 개로 나눴는가에 시선이 갑니다. 하지만 지금 돌아보면 더 중요한 것은 숫자가 아니었습니다.

중요했던 것은 다음 네 가지였습니다.

  • 공통 규약을 먼저 빼서 이후 분리를 가능하게 만들었는가
  • 자주 바뀌는 도메인과 덜 바뀌는 인프라를 구분했는가
  • 완벽하지 않아도 운영 가능한 경계를 만들었는가
  • 코드 분리와 배포 분리를 실제로 연결했는가

이 기준에서 보면 Omnilude의 전환은 꽤 현실적인 방식이었다고 생각합니다. 완벽하게 예쁜 그림은 아니어도, 다음 제품과 다음 실험을 계속 올릴 수 있는 구조를 먼저 만들었기 때문입니다.

마무리

Omnilude 백엔드의 MSA 전환은 처음부터 거창한 전략 문서가 있었던 일이라기보다, 더 큰 것을 만들기 위해 결국 지나가야 했던 구조 정리 작업에 가까웠습니다.

하나의 큰 프로젝트에서 시작해 common을 뽑고, auth, backbone, storage, ai, game, blog를 나누고, 남은 것은 legacy-service로 묶고, 마지막에는 Jenkins와 Kubernetes에서 실제 배포 단위까지 맞춰갔습니다.

저는 이 과정이 꽤 Omnilude답다고 생각합니다. Omnilude라는 이름에는 omni-가 주는 포괄성과 -lude가 주는 놀이의 감각이 함께 들어 있습니다. 여러 기능과 서비스가 따로 흩어지는 것이 아니라, 결국 하나의 더 큰 놀이 경험으로 모이게 하고 싶다는 뜻에 가깝습니다. 처음부터 정답을 알고 있었던 것이 아니라, 만들고 싶은 것이 분명했기 때문에 구조도 그에 맞춰 계속 바뀌어 갔다는 점에서 이 전환은 이름과도 잘 닮아 있다고 느꼈습니다.

다음 글에서는 구조 이야기에서 한 걸음 더 들어가, 실제로 게임을 어떻게 구현하고 있는지에 대한 이야기를 들고 찾아뵙겠습니다.