모놀리스
고친 사람 github-actions[bot]
모놀리스는 애플리케이션의 모든 기능을 한 프로그램으로 묶어 한 번에 배포합니다. 주문 코드와 결제 코드가 같은 프로그램 안에서 서로를 함수로 부릅니다. 대부분의 서비스가 이 모양으로 시작합니다. 코드와 팀이 커지면 배포가 서로 묶이는 불편이 생깁니다.
쉽고 빠른 이해
모놀리스는 애플리케이션 전체를 결과물 하나로 만들어 한 번에 올립니다. 쇼핑몰이라면 주문·결제·배송 코드가 결과물 하나로 묶여 프로그램 하나로 뜹니다. 새 서비스는 대개 이 모양으로 시작합니다.
이렇게 두면 다른 기능을 쓰는 일이 쉽습니다. 함수 호출 한 번이면 됩니다. 중간에 네트워크가 끼지 않습니다. 주문 저장과 결제 기록도 한 묶음으로 처리할 수 있습니다.
- 모든 코드를 한 번에 빌드합니다
- 결과물 하나를 서버에 올려 띄웁니다
- 요청이 몰리면 같은 결과물을 여러 대에 띄웁니다
대가는 전부가 함께 움직인다는 것입니다. 한 줄을 고쳐도 전체를 다시 배포합니다. 여러 팀이 같은 덩어리를 고치면 서로의 배포를 기다립니다. 그때가 되면 기능 사이의 나눔이 오래 안 바뀐 기능부터 따로 떼어 냅니다.
상세
이 절은 모놀리스가 무엇을 한 덩어리로 묶는지, 왜 대부분 이 모양으로 시작하는지, 커지면 무엇이 곤란해지는지를 다룹니다. 주문·결제·배송을 처리하는 쇼핑몰 하나를 줄곧 예로 씁니다.
한 권으로 제본한 책을 떠올리면 가깝습니다. 한 쪽에 오타가 있어도 고친 판을 내려면 책 전체를 다시 찍습니다. 대신 앞 장에서 뒤 장을 찾을 때는 쪽을 넘기기만 하면 됩니다.
한 덩어리로 묶이는 단위
먼저 무엇을 한 덩어리라고 부르는지 정합니다. 기준은 코드의 양이 아니라 배포 단위입니다. 배포는 고친 프로그램을 서버에 올려 사용자가 쓰게 하는 일입니다. 한 번에 함께 올라가는 묶음이 배포 단위입니다.
모놀리스에서는 배포 단위가 하나입니다. 빌드는 소스 코드를 실행할 수 있는 파일로 바꾸는 일입니다. 모놀리스는 주문·결제·배송 코드를 한 번에 빌드해 결과물 하나를 만듭니다.
결과물의 모양은 언어마다 다릅니다. Java 로 짰다면 코드 전체가 JAR(Java ARchive, 자바 아카이브) 파일 하나로 묶여 나옵니다. 이 파일 하나가 곧 배포 단위입니다.
그 결과물을 띄우면 프로세스 하나가 뜹니다. 프로세스는 운영체제가 돌리는 프로그램 하나입니다. 모든 기능이 이 프로세스 안에서 돕니다. 데이터도 대개 데이터베이스 하나에 모아 둡니다.
flowchart TD
subgraph 배포단위["한 프로세스 안"]
O["주문 코드"] --> P["결제 코드"]
O --> S["배송 코드"]
end
배포단위 --> DB[("데이터베이스 하나")]
주문 코드가 결제와 배송을 부르는 화살표는 전부 프로세스 안쪽에 있습니다. 바깥으로 나가는 선은 데이터베이스로 가는 하나뿐입니다.
요청이 늘면 같은 결과물을 서버 여러 대에 띄웁니다. 같은 것을 여러 대로 늘려 일을 나누는 방식을 수평 확장이라고 합니다. 서버가 열 대여도 똑같은 결과물의 복사본이라 배포 단위는 여전히 하나입니다. 복사본 하나하나 안에는 모든 기능이 함께 삽니다.
여러 대에 요청을 나눠 주려면 앞에 로드 밸런서를 둡니다. 로드 밸런서는 들어온 요청을 뒤의 서버들에 고르게 보내는 장치입니다. 어느 서버가 받든 같은 코드가 요청을 처리합니다.
프로세스 안에서 부르는 호출
한 프로세스 안에 있으면 다른 기능을 쓰는 방법이 단순합니다. 주문을 저장하면서 결제 기록을 남기는 코드 한 조각이 그 모습을 보여 줍니다.
그 전에 트랜잭션을 짚습니다. 트랜잭션은 여러 변경을 한 묶음으로 처리합니다. 묶음 안의 변경은 전부 반영되거나 전부 취소됩니다. 주문은 저장됐는데 결제 기록이 없는 상태를 막으려고 씁니다.
tx.begin();
orders.save(order); // 주문 저장
payments.add(pay); // 결제 저장
tx.commit(); // 둘이 함께 반영
두 호출은 같은 프로세스 안의 함수 호출입니다. 중간에 네트워크가 끼지 않으므로 호출 자체가 실패하는 일이 거의 없습니다. 두 테이블이 같은 데이터베이스에 있어서 한 트랜잭션으로 묶입니다.
결제 기능을 다른 서버로 떼어 내면 두 가지가 달라집니다. 결제를 부르는 일이 네트워크 요청이 되어 상대가 응답하지 않을 수 있습니다. 결제 데이터가 다른 데이터베이스로 가면 두 변경을 한 트랜잭션에 넣을 수 없습니다.
기능마다 따로 배포하는 서비스로 쪼갠 구조를 마이크로서비스라고 합니다. 모놀리스라는 이름은 흔히 이 구조의 맞은편을 가리킬 때 쓰입니다.
한 덩어리가 주는 편함
모놀리스는 누가 일부러 고르지 않아도 생기는 모양입니다. 새 애플리케이션을 만들 때 코드를 한 곳에 쓰고 한 번에 띄우는 것이 가장 짧은 길이기 때문입니다. 그 짧은 길이 주는 편함은 네 가지 일에서 드러납니다.
| 하는 일 | 모놀리스에서는 |
|---|---|
| 배포 | 결과물 하나를 올린다 |
| 오류 추적 | 프로세스 하나의 로그만 본다 |
| 함수 이름·인자 바꾸기 | 개발 도구가 그 함수를 부르는 곳을 전부 찾아 준다. 한 번에 함께 고친다 |
| 테스트 | 프로세스 하나를 띄우고 요청을 보내면 기능 전체를 거친다 |
네 줄 모두 「한 곳」이라서 쉬워지는 일입니다. 팀이 하나이고 코드가 작을 때 이 편함이 가장 큽니다.
커지면 생기는 불편
편함은 「전부 한 곳」에서 나옵니다. 코드와 사람이 늘면 같은 「한 곳」이 기능들을 한데 묶어 움직이게 만듭니다. 이 소절은 그 묶임을 결제 팀과 주문 팀 두 팀의 예로 다룹니다.
두 팀이 한 덩어리를 고친다고 해 봅시다. 배포 단위가 하나라서 두 팀의 변경이 한 번에 나갑니다. 결제 팀의 수정에서 버그가 나오면 그 배포가 멈춥니다. 주문 팀의 수정도 같이 기다립니다.
flowchart TD
A["결제 팀의 수정"] --> C["둘을 합쳐 전체를 다시 빌드"]
B["주문 팀의 수정"] --> C
C --> D{"전체 테스트"}
D -->|통과| E["전체를 다시 배포"]
D -->|"결제 쪽 실패"| F["배포 멈춤 · 주문 팀 수정도 대기"]
고친 곳이 한 줄이어도 빌드와 테스트는 전체를 거칩니다. 코드가 커질수록 이 시간이 길어집니다. 팀이 늘수록 한 번의 실패에 붙잡히는 수정도 늘어납니다.
배포 말고도 묶이는 것이 셋 더 있습니다.
| 묶이는 것 | 일어나는 일 |
|---|---|
| 늘리기 | 조회만 몰려도 전체를 복사해 띄운다. 안 바쁜 기능도 서버마다 메모리를 차지한다 |
| 장애 | 한 기능이 메모리를 다 써서 프로세스가 죽으면 모든 기능이 함께 멈춘다 |
| 기술 선택 | 한 프로세스라 언어와 주요 라이브러리를 하나로 맞춘다 |
셋 다 「기능이 전부 한 프로세스 안에 산다」에서 나옵니다. 수평 확장으로 복사본을 열 대 띄워도 복사본마다 모든 기능이 함께 삽니다. 그래서 코드를 아무리 잘 나눠도 이 묶임은 남습니다.
안쪽 경계를 지킨 모놀리스
한 덩어리라고 안쪽까지 뒤섞여 있어야 하는 것은 아닙니다.
코드를 기능마다 나눠 이름 붙인 덩이를 모듈이라고 합니다. 주문 모듈, 결제 모듈처럼 기능마다 하나씩 둡니다. 모듈마다 따로 이해하고 따로 고치려고 나눕니다.
모듈끼리 서로에게 기대는 정도를 결합도라고 합니다. 한 모듈을 고칠 때 다른 모듈까지 따라 고쳐야 하는 정도로 잽니다.
경계를 안 지키면 주문 코드가 결제 테이블을 직접 읽습니다. 배송 코드도 같은 테이블을 읽게 됩니다. 그러면 결제 테이블 칼럼 하나를 바꿀 때 세 모듈을 따라 고쳐야 합니다. 결합도가 높다는 말이 이 상태를 가리킵니다.
모듈이 수십 개로 늘면 이렇게 직접 읽는 줄이 얽혀 작은 수정이 어디를 깨뜨릴지 아무도 모르게 됩니다. 코드를 고치는 일 자체가 어려워집니다. 경계가 무너진 이런 덩어리의 이름이 큰 진흙 공(big ball of mud)입니다.
반대로 모듈마다 공개 메서드로만 서로를 부르게 할 수 있습니다. 결제 테이블은 결제 모듈만 만집니다. 배포는 하나지만 안쪽 경계는 또렷합니다. 이 모양의 이름이 모듈러 모놀리스입니다.
flowchart TD
subgraph 진흙["큰 진흙 공"]
O1["주문"] --> T1[("결제 테이블")]
S1["배송"] --> T1
P1["결제"] --> T1
end
subgraph 모듈러["모듈러 모놀리스"]
O2["주문"] --> P2["결제 · 공개 메서드"]
S2["배송"] --> P2
P2 --> T2[("결제 테이블")]
end
위쪽은 결제 테이블에 들어가는 길이 셋입니다. 아래쪽은 결제 모듈을 거치는 길 하나뿐입니다. 결제 테이블을 바꿀 때 아래쪽은 결제 모듈만 고치면 됩니다. 결합도가 낮은 쪽이 아래입니다.
그래서 커진 모놀리스의 불편은 두 갈래입니다. 배포와 늘리기가 묶이는 것은 기능이 전부 한 프로세스 안에 살아서 생깁니다. 코드를 고치기 어려운 것은 안쪽 경계가 무너져서 생깁니다. 원인이 다르므로 고치는 방법도 다릅니다.
떼어 내는 순서
배포 대기가 길어지면 일부 기능을 따로 도는 서비스로 떼어 냅니다. 한꺼번에 다시 짜지는 않습니다.
기능을 하나씩 새 서비스로 옮깁니다. 옛 코드는 옮긴 만큼 지웁니다. 이 방식을 스트랭글러 무화과 패턴이라고 부릅니다.
경계가 흐린 채로 떼면 다른 문제가 생깁니다. 서비스는 나뉘었습니다. 그런데 한 서비스를 고칠 때마다 다른 서비스도 함께 배포해야 합니다.
이런 구조를 분산 모놀리스라고 합니다. 배포 묶임은 그대로 남습니다. 네트워크 비용만 더 치르게 됩니다.
그래서 떼기 전에 모듈러 모놀리스로 안쪽 경계부터 세우는 순서를 많이 씁니다. 모듈 경계가 오래 흔들리지 않으면 그 경계가 서비스 경계의 후보가 됩니다.
고를 때와 떼어 낼 때
모놀리스로 둘지 떼어 낼지는 팀과 경계의 상황이 정합니다. 흔한 상황 넷을 표에 모았습니다.
| 상황 | 흔한 선택 |
|---|---|
| 팀이 하나이고 기능이 아직 자주 바뀐다 | 모놀리스로 시작한다 |
| 모듈 경계는 또렷하고 배포는 한 팀이 감당한다 | 모듈러 모놀리스로 둔다 |
| 여러 팀이 서로의 배포를 기다린다 | 경계가 오래 안 바뀐 모듈부터 떼어 낸다 |
| 한 기능만 부하가 유독 크다 | 그 기능만 떼어 따로 늘린다 |
어느 줄이든 떼어 내는 때는 같습니다. 배포나 늘리기의 묶임이 네트워크 비용보다 커졌을 때입니다. 묶임이 크지 않을 때 먼저 떼면 운영할 서버와 실패할 수 있는 호출만 늘어납니다.
이름이 비슷한 다른 개념
모놀리스와 이름이나 모양이 비슷해 헷갈리는 말이 둘 있습니다.
모노레포는 여러 프로젝트의 코드를 저장소 하나에 모아 두는 방식입니다. 저장소가 하나여도 서비스마다 따로 빌드하고 따로 배포할 수 있습니다. 모놀리스를 가르는 기준은 저장소 수가 아니라 배포 단위 수입니다.
모놀리식 커널은 운영체제 쪽 말입니다. 파일 시스템과 장치 드라이버 같은 기능을 커널 프로그램 하나에 모아 돌리는 설계를 가리킵니다. 「한 덩어리」라는 뜻은 같지만 이 편이 다루는 애플리케이션 구조와는 다른 대상입니다.
관련 항목
모놀리스와 맞세워지는 구성 방식
마이크로서비스 · 서비스 지향 아키텍처 · 분산 모놀리스 · 모듈러 모놀리스 · 큰 진흙 공 · 서버리스
모놀리스가 속하는 상위 분류
소프트웨어 아키텍처 · 아키텍처 양식 · 시스템 설계 · 애플리케이션
모놀리스 안쪽을 나누는 설계 양식
레이어드 · 헥사고날 · 클린 아키텍처 · 모듈 · 패키지 · 계층
모놀리스 안쪽 경계를 재는 성질
결합도 · 응집도 · 순환 의존 · 캡슐화 · 정보 은닉
모놀리스를 떼어 낼 때 쓰는 방법
스트랭글러 무화과 · 도메인 주도 설계 · 경계 컨텍스트 · 컨웨이의 법칙 · 모듈화 · 리팩터링
모놀리스를 배포하고 늘리는 수단
배포 · 빌드 · JAR · 프로세스 · 수평 확장 · 수직 확장 · 로드 밸런서 · 무중단 배포 · 롤백
한 프로세스 안에서 묶이는 데이터 처리
트랜잭션 · 원자성 · ACID · 데이터베이스 · 조인
한 덩어리라서 겪는 장애와 부채
단일 장애점 · 메모리 누수 · 기술 부채 · 스파게티 코드
모놀리스와 이름이 겹치는 개념
모노레포 · 모놀리식 커널 · 마이크로커널 · 폴리레포
다른 이름: monolith · 모놀리식 아키텍처 · monolithic architecture · 모놀리식 애플리케이션 · monolithic application