k3s 기반 MSA 구조로 전환하기까지
이번 글에서는 프로젝트를 진행하면서 모놀리식 구조의 한계를 체감하게 된 과정과, 이를 해결하기 위해 k3s 기반 MSA 구조로 전환한 경험을 정리합니다.
초기에는 빠른 개발과 단순한 운영을 위해 모놀리식 구조를 선택했습니다. 하지만 서비스가 커지고 요구사항이 늘어나면서, 특정 기능의 장애가 전체 서비스로 확산되거나 작은 변경에도 전체 배포가 필요한 문제를 겪게 되었습니다. 또한 일부 기능에 집중된 부하가 애플리케이션 전체에 영향을 주는 상황도 확인할 수 있었습니다.
이 글에서는 단순히 “MSA를 도입했다”는 결과보다, 어떤 문제를 겪었고 왜 기존 구조로는 해결이 어려웠는지, 그리고 그 과정에서 아키텍처를 어떤 기준으로 다시 설계했는지에 초점을 맞춰 이야기해 보겠습니다.
모놀리식 방식에서의 고충
초기 서비스는 빠른 개발을 위해 Docker Compose 기반 모놀리식 구조로 시작했습니다.
하지만 서비스에 대한 개발이 진행되면서 다음과 같은 두 가지 문제가 발생했습니다.
배포 단위가 너무 큼에 따른 영향도
모놀리식 구조에서는 배포 단위가 애플리케이션 전체이기 때문에, 일부 기능만 수정했더라도 전체를 다시 빌드하고 배포해야 했습니다. 그만큼 하나의 작은 오류도 특정 기능에만 그치지 않고 전체 배포 실패로 이어질 수 있었고, 실제로 프로젝트 진행 중에도 한 모듈의 문제로 인해 정상 배포에 실패하면서 로컬 환경으로 시연을 대체해야 했습니다.
이처럼 모든 기능이 하나의 실행 단위에 강하게 결합되어 있다 보니, 변경의 영향 범위를 빠르게 파악하기 어려웠고 배포에 대한 심리적 부담도 클 수밖에 없었습니다. 특히 사소한 수정조차 전체 서비스 안정성과 연결되었기 때문에, 배포는 점점 더 신중하고 무거운 작업이 되었고, 결국 서비스별로 배포와 장애 영향을 분리할 수 있는 구조의 필요성을 크게 느끼게 되었습니다.
장애 전파 문제
부하 테스트 과정에서 주문 생성 API에 RPS 200 수준의 요청을 인가하자, CPU 1 Core로 제한된 환경에서 서버 전체가 다운되는 상황이 발생했습니다. 주문 기능에 순간적으로 부하가 집중되었지만, 영향은 해당 API에만 머무르지 않았습니다.

- 주문 생성 API에 RPS 200 부하를 인가했을 때, CPU 1 Core로 제한된 환경에서 전체 서버가 일정 기간 다운
모놀리식 구조에서는 모든 기능이 하나의 애플리케이션 프로세스 안에서 함께 동작하기 때문에, 특정 모듈이 과도하게 CPU와 메모리를 점유하면 다른 기능도 동일한 자원을 공유한 채 영향을 받을 수밖에 없습니다. 결국 일부 기능의 과부하가 전체 서비스의 응답 지연과 장애로 확산되는 구조적 한계를 직접 확인하게 되었습니다.
이 문제를 계기로, 기능별 트래픽과 장애를 서로 분리해서 다룰 수 있는 구조가 필요하다고 판단했습니다. 특정 서비스의 부하가 전체 애플리케이션 장애로 번지지 않도록 배포 단위와 실행 환경, 리소스 관리 범위를 서비스별로 나누는 방향을 본격적으로 검토하게 되었습니다.
왜 MSA를 선택했는가?
문제들을 해결하기 위해 모놀리식을 유지하며 개선하는 방향과 MSA로 전환하는 방향, 두 가지를 검토했습니다.
모놀리식 구조 내에서의 개선 가능성
배포 문제의 경우, Feature Flag나 블루-그린 배포 전략을 통해 다운타임을 줄이고 배포 리스크를 일부 완화할 수 있습니다. 그러나 배포 단위 자체는 여전히 애플리케이션 전체입니다. 한 모듈의 빌드 오류가 전체 배포를 막는 구조적 문제는 파이프라인을 아무리 개선해도 해소되지 않습니다.
장애 전파 문제의 경우, Circuit Breaker나 Rate Limiting으로 특정 기능의 부하를 일부 차단할 수 있습니다. 하지만 단일 프로세스 안에서 CPU와 메모리를 모든 기능이 공유하는 이상, 자원 고갈 자체를 막을 수는 없습니다. 방어 코드는 증상을 늦출 뿐, 근본 원인을 제거하지 못합니다.
MSA 전환을 선택한 이유
두 문제 모두 코드나 운영 수준에서 해결할 수 없는, 구조 자체의 한계에서 비롯됩니다.
MSA로 전환하면 각 서비스가 독립된 배포 단위와 실행 환경을 갖게 됩니다. 특정 서비스의 장애나 부하가 다른 서비스로 번지지 않고, 변경이 발생한 서비스만 독립적으로 빌드하고 배포할 수 있습니다.
이는 앞서 경험한 두 문제를 구조적으로 해결할 수 있는 방향이었고, 결국 MSA 전환을 결정하게 되었습니다.
도입 과정에서의 고충
MSA 전환을 결정한 후, 오케스트레이션 플랫폼으로 k3s를 선택했습니다. 경량 쿠버네티스인 k3s는 온프레미스 환경에서도 무리 없이 운영할 수 있고, 쿠버네티스의 핵심 기능을 그대로 활용할 수 있다는 점에서 적합한 선택이었습니다.
기존 Docker Compose 기반의 모놀리식 구조를 서비스 단위로 분리하고, 각 서비스를 독립적인 Pod로 운영할 수 있도록 전환 작업을 진행했습니다. 인프라 구성 자체는 계획대로 마무리되었습니다.
예상치 못한 팀 전환 비용
문제는 그 다음이었습니다.
팀원들에게도 k3s로의 전환을 요청했지만, 기존 환경과의 설정 차이로 인해 예상보다 많은 시간이 들었습니다.
하나는 OS 환경의 차이입니다. k3s는 Linux 기반으로 동작하기 때문에, Windows를 사용하는 팀원에 대한 고려가 필요했습니다.
다른 하나는 환경 변수 설정의 불일치입니다. Docker Compose 환경에서는 mysql, redis, elasticsearch처럼 컨테이너 이름을 호스트로 사용하지만, k3s 환경에서는 modeunsa-infra-mysql, modeunsa-infra-redis처럼 클러스터 내부 서비스 이름을 사용해야 합니다. 이 차이를 인지하지 못한 채 기존 설정을 그대로 사용하면 연결 자체가 실패했고, 원인을 추적하는 과정에서 반복적으로 시간이 낭비되었습니다.
환경을 분리하고, 절차를 문서로 남기다
이 문제를 해결하기 위해 환경 설정 체계 분리와 운영 절차 문서화를 병행했습니다.
실행 환경별 설정 분리
로컬 Docker Compose 개발 환경은 application-dev.yml + .env.dev, 로컬 k3s 환경은 application-k3s-dev.yml + .env.k3s-dev를 사용하도록 나누고, 실행 시 dev 또는 prod를 인자로 넘기면 해당 환경의 .env 파일이 자동으로 로드되도록 구성했습니다.
get_env_file() {
local env="${1:-dev}"
case "$env" in
dev|prod)
echo "$ROOT_DIR/.env.k3s-$env"
;;
esac
}
- 개발자가 환경마다 설정 파일을 직접 찾아 수정할 필요 없이, 인자 하나로 실행 환경이 결정되는 구조입니다.
스크립트로 실행 흐름 정리
인프라와 애플리케이션 실행 흐름은 infra.sh와 app.sh로 분리했습니다.
# 인프라 관리
./k8s/infra.sh up [dev|prod] # 인프라 시작
./k8s/infra.sh down # 중지 (데이터 유지)
./k8s/infra.sh clean # 중지 + 데이터 삭제
# 애플리케이션 배포
./k8s/app.sh up [dev|prod] # 전체 배포
./k8s/app.sh rollout [dev|prod] [module] # 특정 모듈만 롤링 업데이트
./k8s/app.sh logs [module] # 로그 확인
인프라는 한 번 띄우면 유지되지만, 애플리케이션은 서비스별로 독립 재배포가 가능해야 했기 때문에 두 스크립트를 명확히 구분했습니다.
운영 명령어 공유
k3s 환경에서 실제로 자주 맞닥뜨리는 상황들을 명령어 모음으로 정리해 팀에 공유했습니다.
특히 rollout restart처럼 쿠버네티스 입문자가 헤매기 쉬운 부분은 왜 이 명령어가 필요한지 맥락과 함께 문서로 남겨두었습니다.

전환 후 달라진 것들
k3s 기반 MSA 구조로 전환한 이후, 초기에 겪었던 두 가지 문제가 구조적으로 해소되었습니다.
배포 측면에서는 변경이 발생한 서비스만 독립적으로 빌드하고 배포할 수 있게 되었습니다. 한 모듈의 문제가 전체 배포를 막는 상황이 사라졌고, 배포 범위가 명확해지면서 변경의 영향을 빠르게 파악할 수 있게 되었습니다.
장애 격리 측면에서는 각 서비스가 독립된 Pod로 분리되어 실행되기 때문에, 특정 서비스에 부하가 집중되더라도 다른 서비스는 영향을 받지 않습니다. 더 나아가 부하가 집중되는 서비스만 독립적으로 수평 확장 할 수 있게 되었습니다. 모놀리식에서는 불가능했던 방식입니다.
아래 영상은 k6로 Product 서비스에 부하를 인가했을 때, 해당 Pod만 수평적으로 확장되는 과정을 실제로 확인한 것입니다.
MSA 전환의 트레이드오프
전환 이후 긍정적인 변화가 있었던 만큼, 새롭게 생겨난 부담도 있었습니다.
운영 복잡도 증가
모놀리식에서는 하나의 애플리케이션을 관리하면 됐지만, 서비스를 분리하면서 관리해야 할 Helm 차트와 설정 파일이 서비스 수만큼 늘어났습니다.
특히 서버 사양이 높지 않은 환경이었기 때문에, 각 서비스의 메모리 한도를 빡빡하게 설정해야 했고 OOM이 발생할 때마다 해당 서비스의 JVM 옵션과 리소스 설정을 직접 수정하고 재배포하는 작업이 반복되었습니다. Docker Compose 환경에서는 리소스 관리를 크게 신경 쓰지 않아도 됐던 부분이, k3s에서는 서비스별로 하나씩 직접 튜닝해야 하는 작업으로 바뀐 셈이었습니다.
로컬 개발 환경의 리소스 부담
k3s 환경은 여러 서비스를 동시에 Pod로 띄워야 하기 때문에, 로컬 머신의 리소스 요구 사항이 높아졌습니다. 실제로 일부 팀원은 사양 문제로 k3s 환경을 로컬에서 구동하지 못하는 상황이 발생했습니다.
이를 우회하기 위해 해당 팀원의 브랜치를 직접 내 환경에서 체크아웃해 테스트해주는 방식으로 대응했지만, 이 과정이 반복되면서 전체 업무 흐름에 불필요한 병목이 생겼습니다. 근본적인 해결책이 아닌 임시방편이었고, 팀원 개개인의 개발 환경 격차가 협업 효율에 직접적인 영향을 준다는 점을 체감했습니다.
마치며
MSA 전환은 단순히 기술 스택을 바꾸는 작업이 아니었습니다. 구조를 바꾸면 배포 방식이 바뀌고, 배포 방식이 바뀌면 팀의 협업 방식도 함께 바뀌어야 했습니다. 기술적인 전환보다 팀 전체가 같은 기준으로 움직일 수 있는 환경을 만드는 것이 더 어렵고 중요한 과제였습니다.
운영 복잡도 증가나 로컬 환경의 리소스 부담처럼 아직 완전히 해결하지 못한 부분도 남아 있습니다. 리소스 튜닝 자동화, 개발 환경 표준화 등은 앞으로 개선해 나가야 할 숙제입니다.
그럼에도 이번 전환을 통해 장애가 전체로 번지지 않는 구조, 변경이 발생한 서비스만 독립적으로 배포할 수 있는 환경을 직접 만들고 운영해본 경험은 다음 아키텍처 결정을 내릴 때 도움이 될 것으로 생각합니다.
'Backend Programming' 카테고리의 다른 글
| 정말 HTTPS에 대해서 알고 있는가? (1) | 2025.10.28 |
|---|---|
| JPA에서 ID는 언제 만들어지고, 언제 DB에 저장될까? (0) | 2025.10.24 |
| 데이터베이스 구조 리팩토링 및 마이그레이션 경험 공유 (3) | 2025.07.30 |
| 빌드 실패 디버깅 (1) | 2025.03.07 |
| CI/CD? 배포 자동화를 해보자 (0) | 2024.12.13 |