MSA로 SNS 처음부터 끝까지 만들어보자!

Raha·2026년 6월 18일

Social Network Project

목록 보기
1/3

MSA + 헥사고날 + DDD, 왜 이걸 선택했나 — 설계 전 고민 기록

시작은 "Springboot으로 된 프로젝트 한 번 만들어보자"였다

이 글은 Spring Boot로 SNS 플랫폼을 만들면서 남기는 학습 일지입니다.
이런 저런 설계 과정부터 개발, 배포까지 꾸준히 글을 남겨보려고 합니다!

Spring 생태계로 돌아온 이유

경력은 4.X년 차지만, 솔직히 Spring은 입사 전 포트폴리오를 만들면서 사용해 본 이후 거의 다뤄보지 못했다. 현재 회사에서는 Java와 자체 프레임워크를 사용하고 있어 Spring Boot, JPA, Gradle 멀티모듈 같은 Spring 생태계를 제대로 경험할 기회가 많지 않았다.

그렇다고 백엔드 개발 경험이 부족했던 건 아니다. 최근에는 현업에서 Hexagonal Architecture를 직접 도입하며 프로젝트 구조를 설계하고 개발을 주도했다. LLM과 LangChain4j를 활용해 기존 제품에 AI 기능을 접목하는 업무도 진행하고 있다.

다만 설계 역량과 Spring 생태계 경험은 서로 대체할 수 있는 게 아니었다. 아는 것과 직접 구현해 보는 것은 분명 다르기 때문이다. Spring Boot, JPA, 멀티모듈 프로젝트, 테스트 전략, 트랜잭션 설계, 아키텍처까지 직접 고민하며 구현해 보고 싶었다. 이번 프로젝트는 그 공백을 메우기 위해 시작했다.

프로젝트의 목표

주제는 텍스트와 사진 기반 SNS 플랫폼이다. Threads처럼 단순한 글을 작성하는 서비스지만, 사용자 관계, 피드 조회, 좋아요, 댓글, 인증 등 실제 서비스에서 고민해야 할 다양한 요구사항을 자연스럽게 녹여낼 수 있는 도메인이라고 생각했다.

이 프로젝트의 목표는 단순히 CRUD를 구현하는 것이 아니다. Spring Boot와 JPA, 멀티모듈 구조, 테스트, 아키텍처를 직접 설계하고 구현하며 Spring 생태계를 깊이 있게 경험하는 것이 첫 번째 목표다.

여기에 한 가지를 더하고 싶었다. 현재 실무에서 LLM과 LangChain4j로 AI 기능을 개발하고 있는 만큼, 이번 프로젝트에서도 AI를 자연스럽게 녹여보고자 한다. 단순히 "AI를 붙였다"는 수준이 아니라 추천 피드, 관심사 기반 콘텐츠 추천, 검색 품질 개선처럼 실제 사용자 경험을 높이는 기능을 직접 설계하고 구현해 볼 예정이다. 궁극적으로는 Spring 기반 백엔드와 AI를 함께 활용해 서비스를 설계하고 개발할 수 있는 개발자가 되는 것, 그것이 이번 프로젝트의 목표다.


왜 MSA인가

처음엔 "그냥 모놀리식으로 빠르게 만들면 안 되나?" 싶었다.
근데 SNS를 뜯어보니까 자연스럽게 서비스가 쪼개진다.

  • 게시글을 쓰는 것과
  • 피드를 읽는 것과
  • 알림을 받는 것과
  • 사용자 정보를 관리하는 것은

사실 성격이 다른 일이다. 피드 조회가 폭발적으로 늘어난다고 해서 알림 서비스를 같이 스케일 아웃할 필요는 없다.

MSA를 선택한 이유를 정리하면:

  • 서비스마다 독립적으로 배포하고 스케일할 수 있다
  • 장애가 한 서비스에 격리된다 (알림 서버 다운돼도 게시글 작성은 된다)
  • 각 서비스를 다른 기술 스택으로 최적화할 수 있다 (나중에)

물론 단점도 있다. 서비스 간 통신 복잡도, 분산 트랜잭션, 데이터 일관성 문제. 하지만 이걸 직접 부딪혀보는 게 목적이었다.


왜 헥사고날 아키텍처인가

일반적인 레이어드 아키텍처(Controller → Service → Repository)의 문제를 먼저 이해했다.

[레이어드 아키텍처의 흔한 문제]

Controller
    ↓
Service ──── 여기서 JPA Entity를 직접 쓰기 시작함
    ↓
Repository (JPA)

→ 비즈니스 로직이 DB 기술에 종속됨
→ DB를 바꾸면 서비스 로직도 손대야 함
→ 테스트할 때 DB 없으면 못 돌림

헥사고날(포트 & 어댑터) 아키텍처는 이 문제를 의존성 역전으로 푼다.

핵심 규칙은 딱 하나다:

의존성은 항상 안쪽으로. 도메인은 아무것도 모른다.

도메인 모델(User)은 JPA도, Spring도, HTTP도 모른다. 순수한 자바 객체다.
덕분에 DB를 H2에서 MySQL로 바꿔도, HTTP를 gRPC로 바꿔도 도메인은 건드리지 않아도 된다.


왜 DDD인가

DDD(도메인 주도 설계)를 전술 패턴 수준에서 적용하기로 했다.
전략 패턴(바운디드 컨텍스트, 유비쿼터스 언어 등)까지 하면 범위가 너무 넓어지니까.

전술 패턴에서 챙긴 것들:

패턴내가 적용한 방식
EntityUser — 순수 자바, 식별자로 동일성 판단
Repository (포트)UserRepository — 인터페이스로 추상화
Use CaseRegisterUseCase, GetUserUseCase — 행동을 인터페이스로 선언
CommandRegisterCommand — 상태 변경 요청을 데이터 객체로 표현

DDD를 적용하면서 가장 크게 달라진 건 "어디에 뭘 둬야 하는가"가 명확해진다는 거다.
비즈니스 규칙은 도메인에, 흐름 조율은 서비스에, 기술 연결은 어댑터에.


설계 전 핵심 고민들

피드 시스템: Fan-out on Write vs Read

SNS에서 피드를 어떻게 만들지가 가장 큰 설계 결정이었다.

Fan-out on Write (쓰기 시 분산)
  게시글 작성 → 팔로워 모두의 타임라인에 미리 복사
  장점: 읽기 빠름
  단점: 팔로워 100만 명인 계정이 글 쓰면 100만 번 INSERT

Fan-out on Read (읽기 시 조합)  
  피드 요청 → 그때그때 팔로잉 목록의 최신 글 조합
  장점: 쓰기 가벼움
  단점: 읽기 느림, 팔로잉 많으면 복잡

현실적인 답: 하이브리드
  일반 유저 → Fan-out on Write
  팔로워 10만 이상 셀럽 → Fan-out on Read

지금 단계에선 단순하게 가고, 나중에 개선하는 방향으로 결정했다.

ID 전략: UUID → ULID

UUID를 쓰면 완전 랜덤값이라 DB B-Tree 인덱스에서 Page Split이 빈번하게 발생한다.
ULID는 앞 48비트가 타임스탬프라 시간순으로 정렬된 값이 들어온다.

UUID: 550e8400-e29b-41d4-a716-446655440000  (완전 랜덤)
ULID: 01HQ2MXYZ0000000000000AAAA            (시간 + 랜덤)

실제 코드에서는 getMonotonicUlid()를 쓴다.

UlidCreator.getMonotonicUlid().toString();
// 같은 밀리초 안에도 단조 증가 보장 (항상 이전 것보다 큼)

일반 getUlid()는 같은 밀리초 안에서 랜덤값이 들어갈 수 있어서 순서가 역전될 수 있다. Monotonic은 같은 밀리초 안에서도 직전 값보다 크다는 걸 보장해줘서 정렬 안정성이 더 높다.

ULID를 쓰면 커서 페이지네이션도 단순해진다.

-- UUID였으면: 복합 커서 필요
WHERE (created_at, id) < (:cursorTime, :cursorId)
 
-- ULID는: id 하나로 시간 순서 + 유일성 모두 보장
WHERE id < :cursorId ORDER BY id DESC

이력 관리: 얼마나 복잡하게 할 것인가

실제 SNS들은 어떻게 하는지 찾아봤다.

  • Threads/Instagram: 수정 시 '수정됨' 라벨만 표시, 이전 내용은 비공개. 삭제된 게시글은 30일간 Soft Delete 상태로 보관 후 Batch로 영구 삭제.
  • Twitter/X: 수정 이력 전면 공개. '수정됨' 버튼을 누르면 원본부터 수정본까지 시간순으로 모두 조회 가능. 리트윗된 글의 맥락을 유지하기 위한 투명성 정책.
  • 국내 서비스: 통신비밀보호법에 따라 접속 로그를 3개월 의무 보관. 명예훼손 등 법적 분쟁 대응을 위해 사용자가 삭제한 게시글도 일정 기간 Soft Delete 상태로 유지.
    내가 선택한 방향:
  • 기본은 Soft Delete (deleted_at 컬럼)
  • 게시글 수정만 edit_history JSONB로 이력 보관
  • 좋아요 취소해도 알림은 회수 안 함 (실제 서비스 표준)

최종 아키텍처 결정

각 서비스는 독립된 헥사고날 구조를 갖는다.
서비스 간 통신은 나중에 다루려고 한다.


이 단계에서 배운 것

아키텍처를 선택하는 건 "뭐가 더 좋아 보이냐"가 아니라 트레이드오프를 이해하고 결정하는 것이라는 걸 알게 됐다.

MSA는 복잡도가 올라가는 대신 독립성을 얻는다.
헥사고날은 파일이 많아지는 대신 변경에 강해진다.
DDD는 코드량이 늘어나는 대신 의도가 명확해진다.

세 가지를 함께 쓰면 배울 게 많아진다. 그게 목적이었으니까 맞는 선택이었다.


다음 글에서는 실제로 Spring Boot 프로젝트 스켈레톤을 잡고, 헥사고날 폴더 구조를 어떻게 잡았는지 기록할 예정이다.


profile
Backend Developer | AI Integration & Product Minded

0개의 댓글