Spring Boot 4 MSA 1일차 — 모놀리스에서 마이크로서비스로
이 시리즈는 르무엘 아카데미(
lemuel-academy) 를 만들면서 정리한 노트입니다. 모놀리스 한 덩어리를 4개 마이크로서비스로 쪼갠 실 사례 중심.
오늘 1일차는 왜 쪼개나 + 첫 서비스 부트스트랩 까지.
이 글에서 다루는 것
- 모놀리스 vs 마이크로서비스 — 언제 쪼개야 하나
- lemuel-academy 의 4개 서비스 분해 사례
- Spring Boot 4 + Kotlin 2.0 첫 프로젝트 5분 부트스트랩
- Hexagonal architecture 디렉토리 구조
1. 언제 쪼개야 할까
단일 모놀리스가 좋은 경우
- 팀 5명 이하
- 트래픽 작음 (RPS < 100)
- 도메인 경계가 흐릿함
- 배포 사이클이 일주일 이상
이런 경우 모놀리스 한 통이 압도적으로 빠릅니다. 마이크로서비스의 인프라 비용을 감당할 가치가 없어요.
쪼개야 할 신호
- 한 도메인이 다른 도메인 배포 때문에 막힌다
- 한 팀이 다른 팀의 코드를 너무 많이 건드린다
- 트래픽 패턴이 도메인별로 크게 다르다 (예: 영상은 자주 / 결제는 드물게)
- 리소스 요구가 다르다 (예: 영상은 GPU, 결제는 메모리)
2. lemuel-academy 의 4개 서비스 분해
[학생/크리에이터/관리자]
│
▼
[api-gateway :8080] ← Spring Cloud Gateway, JWT 인증
│
┌────┼─────────────┐
▼ ▼ ▼
[user] [catalog] [media]
:8081 :8082 :8083
│ │
┌────┴────┐ ┌────┴────┐
▼ ▼ ▼ ▼ ▼ ▼
회원 강의 진도 영상 R2 ffmpeg
upload worker
| 서비스 | 책임 | 데이터 |
|---|---|---|
| api-gateway | JWT 검증, 라우팅, X-User-Id 주입 | 없음 |
| user-service | 회원, 진도, 즐겨찾기, OAuth | users 스키마 |
| catalog-service | 강의/챕터/레슨/리뷰 | catalog 스키마 |
| media-service | 업로드/HLS/R2 + ffmpeg-worker | media 스키마 |
핵심은 데이터를 공유하지 않는 것 — 각 서비스가 자기 스키마만 만짐. 서비스 간 통신은 webhook 또는 메시지큐.
3. Spring Boot 4 + Kotlin 첫 프로젝트
5분 부트스트랩
# IntelliJ → New Project → Spring Initializr (또는 https://start.spring.io)
# - Project: Gradle (Kotlin DSL)
# - Language: Kotlin
# - Spring Boot: 4.0.x
# - Java: 21
# - Group: co.lemuel.academy
# - Artifact: user-service
# - Dependencies: Web, Data JPA, Validation, Flyway, PostgreSQL, Lombok 빼고
필수 + 선택 의존성
// build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.flywaydb:flyway-database-postgresql")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
Hexagonal architecture 디렉토리
user-service/
└── src/main/kotlin/co/lemuel/academy/user/
├── UserServiceApplication.kt
├── domain/ ← 도메인 모델 (순수, 의존성 없음)
│ └── User.kt
├── application/
│ ├── port/in/ ← UseCase 인터페이스 (Controller 가 호출)
│ │ └── SignUpUseCase.kt
│ ├── port/out/ ← Repository 인터페이스 (DB 가 구현)
│ │ └── UserRepositoryPort.kt
│ └── service/ ← UseCase 구현
│ └── SignUpService.kt
└── adapter/
├── in/web/ ← HTTP 컨트롤러
│ └── UserController.kt
└── out/persistence/ ← JPA 어댑터
├── UserJpaEntity.kt
└── UserJpaRepository.kt
핵심: domain 은 어떤 의존성도 없어야 함. Spring annotation 도 X. 테스트가 매우 빨라집니다.
4. 첫 엔티티 + 컨트롤러 한 통
// domain/User.kt
data class User(
val id: UUID? = null,
val email: String,
val nickname: String,
val role: UserRole,
val createdAt: LocalDateTime = LocalDateTime.now(),
)
enum class UserRole { STUDENT, CREATOR, ADMIN }
// application/port/in/SignUpUseCase.kt
interface SignUpUseCase {
fun signUp(cmd: SignUpCommand): User
}
data class SignUpCommand(val email: String, val nickname: String, val password: String)
// adapter/in/web/UserController.kt
@RestController
@RequestMapping("/api/users")
class UserController(private val signUp: SignUpUseCase) {
@PostMapping("/signup")
fun signUp(@RequestBody @Valid req: SignUpRequest): UserDto {
val user = signUp.signUp(SignUpCommand(req.email, req.nickname, req.password))
return UserDto.from(user)
}
}
./gradlew bootRun
curl -X POST http://localhost:8081/api/users/signup \
-H 'Content-Type: application/json' \
-d '{"email":"a@b.com","nickname":"alice","password":"qwerty1234"}'
다음 학습 (7일 코스)
| Day | 주제 |
|---|---|
| 1 | 모놀리스 → MSA 분해 + 첫 서비스 (오늘) |
| 2 | API Gateway + JWT 인증 |
| 3 | JPA + Flyway + Hexagonal |
| 4 | 서비스 간 통신 (REST / Webhook / Redis Streams) |
| 5 | Testcontainers 통합 테스트 |
| 6 | 관측성 (Micrometer + Prometheus) |
| 7 | 배포 (Docker Compose → K8s) |
시리즈를 따라가시려면
lemuel-academy레포가 살아있는 표본입니다 — 코드를 보면서 따라오세요.