https://arin-nya.tistory.com/168
신규 프로젝트 [0] : 초기 아키텍쳐 설계
이번에 이직을 하면서 신규 프로젝트의 0 -> 1 설계부터 초기 구현까지 혼자 맡게 되었다.실제 내부 비즈니스 로직, 도메인 관련된 것은 제외하고 아키텍쳐만 서술한다. 우선 서버리스로 결정하
arin-nya.tistory.com
이전 글에서 언급한 아키텍쳐 위에
Spring Boot 기반 백엔드 설계와 구현 작업이 어느 정도 마무리됐다.
이번 작업에서 가장 오래 붙들고 있었던 건 “어떤 기능을 더 넣을까”가 아니었다.
오히려 반대였다. 지금 단계에서 무엇을 넣지 않을지, 그리고 초기 구조를 어디까지 무겁게 가져갈지를 계속 고민했다.
이번 스프린트에서 내가 만들고 싶었던 것은 단순히 돌아가는 백엔드가 아니었다.
미래의 나와 동료, 그리고 LLM까지 비교적 쉽게 이어서 작업할 수 있는 프로젝트에 더 가까웠다.
그래서 당연하게도 약 2주 동안 주말도 없이 작업했지만, 실제 UI/UX 레벨에서 보이는 서비스는 아직 윤곽만 나온 상태다.
하지만 이 부분에 대해서는 크게 후회하지 않는다. 초반에 구조를 제대로 잡아두면,
이후 작업 속도는 오히려 더 빨라질 가능성이 높다고 보기 때문이다.
그동안 여러 토이 프로젝트를 진행하면서 아쉬웠던 초기 설계의 문제들을 이번에는 다시 반복하고 싶지 않았다.
그래서 이번 설계에서는 계속 다섯 가지 질문을 머릿속에 두고 작업했다.
- 왜 Redis를 안 썼는가
- 왜 DB를 SoT로 잡았는가
- 왜 멀티모듈 + 헥사고날 구조를 선택했는가
- 왜 Cloud Run 분리 가능성까지 고려했는가
- 왜 LLM 협업 환경에서 경계 강제가 중요했는가
사실 나머지 결정들은 대부분 이 다섯 가지를 따라가다 보니 자연스럽게 붙은 것들에 가까웠다.
1. 왜 Redis를 안 썼는가
가장 먼저 한 결정 중 하나가, 현재 단계에서는 Redis를 선도입하지 않는 것이었다.
이건 Redis가 필요 없는 기술이라서가 아니다.
오히려 캐시, 세션, 분산 락, 멱등성 키 저장 같은 문제를 생각하면 너무 쉽게 손이 가는 도구다.
다만 지금 규모에서 먼저 도입했을 때 얻는 이점보다, 운영 복잡도와 비용 증가가 더 크게 느껴졌다.
그래서 현재 단계에서는 Redis 없이 가기로 했다.
대신 멱등성과 동시성 제어는 DB 쪽에서 훨씬 더 엄격하게 가져가야 했다.
유니크 키, 조건부 업데이트, 상태 전이 검증, 트랜잭션 경계 같은 것들을 API 보조 장치가 아니라 실제 보호 장치로 써야 했다.
요약하자면 Redis를 쓰지 않은 이유는 기술적 거부감 때문이 아니라,
현재 단계에서의 비용과 운영 복잡도를 고려한 보류에 가깝다.
Memorystore는 프로비저닝한 용량 기준으로 과금되는 관리형 저장소이기 때문에,
초기 단계에서는 먼저 DB 중심으로 정합성을 관리하고 정말 필요해지는 시점에 Redis를 도입하는 쪽이 더 현실적이라고 판단했다.
지금은 DB를 중심으로 멱등성과 동시성을 관리하되, 이후 캐시·세션·락 수요가 분명해지면 Redis, 비동기 이벤트 흐름이 커지면 Kafka, 배포 및 운영 단위가 더 복잡해지면 GKE 전환까지 충분히 고려하고 있다.
다시 말해 이번 결정은 Redis를 부정한 것이 아니라, 지금 필요한 문제와 나중에 생길 문제를 분리해서 본 결과에 더 가깝다.
2. 왜 DB를 SoT로 잡았는가
Redis를 빼기로 했으면, 상태 판단과 무결성 보장의 중심은 자연스럽게 DB로 모이게 된다.
이번 작업에서는 Spring과 DB를 전체 아키텍처의 축으로 두되, 최종적인 사실 판단은 DB가 한다는 쪽으로 정리했다.
즉, DB를 SoT로 본 것이다.
이유는 단순했다.
회원가입, 관리 객체, 청구 로직은 멱등성과 무결성이 정말 중요했기 때문이다.
이런 영역은 API 레벨 검증만으로 버티기 어렵다.
중복 요청, 재시도, 동시 수정, 상태 전이 꼬임은 결국 DB까지 내려가서 막아야 한다.
물론 대가도 컸다.
스키마 변경과 마이그레이션 관리 책임이 무거워졌고, Entity/DTO/Command/Query 모델도 복잡해졌다.
Spring 구현 난이도도 실제로 올라갔다.
단순 CRUD처럼 가볍게 짜기 어려워졌고, 어디서 상태를 바꾸고 어디서 검증하는지를 계속 의식해야 했다.
그래도 중요한 제약을 서비스 코드의 선의에만 맡기고 싶지는 않았다.
3. 왜 멀티모듈 + 헥사고날 구조를 선택했는가
이번 프로젝트는 인증, 비즈니스 로직, 향후 비동기 job, 인프라 모듈 등을 기준으로 경계를 나눴고, 비즈니스 로직도 다시 세분화해 총 10개의 모듈로 설계했다.
솔직히 초기 단계 MVP만 놓고 보면 과한 선택처럼 보일 수 있다.
실제로 이 구조를 택하면서 초기 구현 속도는 느려졌고, 디렉토리와 파일 수도 많이 늘어났다.
간단한 기능 하나 추가하는 데도 손이 더 갔다.
그럼에도 이 구조를 고른 이유는 분명했다.
처음부터 경계를 흐리게 만들고 싶지 않았기 때문이다.
전형적인 3단 구조는 시작은 빠르다.
하지만 기능이 늘어나면 서비스 레이어가 모든 걸 먹기 시작한다.
특히 조회, 쓰기, 검증, tenant 처리, 상태 전이 규칙 등 여러 기능이 한 서비스에 몰리기 시작하면 나중에는 설명이 안 된다.
멀티모듈과 헥사고날은 멋있어 보이기 위한 선택이 아니라, 경계를 강제로 지키기 위한 장치이다.
4. 왜 Cloud Run 분리 가능성까지 고려했는가
현재 배포는 크게 가져가고 있다.
지금은 사실상 단일 배포 단위에 가깝다.
그런데 구조는 그보다 조금 더 앞을 봤다.
모듈이 커졌을 때 특정 기능군만 따로 분리해 Cloud Run 인스턴스를 늘리거나,
이후 더 나아가면 Kubernetes 같은 운영 형태까지 고려할 수 있도록 경계를 먼저 잡아두었다.
이 선택도 분명히 무겁다.
현재 시점에서는 프로젝트 복잡도를 올리고, 빌드와 의존성 관리 부담도 키운다.
좋게 말하면 확장성을 본 설계고, 나쁘게 말하면 초기 단계에서 다소 과한 구조다.
그래도 초기에 경계를 흐리게 만들고 나중에 다시 찢는 비용이 더 크다고 판단했다.
5. 왜 LLM 협업 환경에서 경계 강제가 중요했는가
이번 작업에서는 Codex, Claude Code 같은 LLM 도구도 적극적으로 활용했다.
그런데 작업하다 보니 분명한 한계가 있었다.
프롬프트만으로는 구조 일관성을 끝까지 유지하기 어렵다는 점이다.
한 번의 수정은 그럴듯해 보여도, 작업이 반복되면 LLM은 쉽게 모듈 간 경계를 침범한다.
한 모듈 안에서도 레이어 책임을 무시하고 의존성을 섞어버리기 쉽다.
즉, 프롬프트 엔지니어링만으로는 한계가 있었다.
그래서 구조적 계약과 테스트 규칙이 더 필요했다.
멀티모듈, 패키지 경계, 아키텍처 테스트 같은 것들이 단순한 형식이 아니라 LLM 협업 환경에서의 안전장치가 됐다.
이번 구조는 사람 개발자뿐 아니라, LLM이 참여하는 환경에서도 경계가 쉽게 무너지지 않도록 하려는 목적이 컸다.
사실 작업이 끝나고 개인적으로 만족스럽지 않아 다시한번 뇌에 새길 겸 정리하고 글을 쓰고, 수정했는데
짧은 기간동안 참 많은걸 했다 싶다.
아무도 고생했다고 해주지 않았지만, 이렇게 다시 보니 진짜 고생했다는 생각이 든다.
잘한지는 모르겠다.
하지만 고생했다. 이번 주말은 푹 쉬었으면 좋겠다.