목차
결제 화면의 문구 하나를 고쳐서 배포했다. 그 몇 분 동안 주문도, 검색도, 로그인도 같이 멈췄다.
한 줄 고쳤는데 전부 멈춘다
문구 하나 바꾸는 데 위험을 감수한 것처럼 보인다. 그래서 자연스럽게 이런 생각이 든다. “결제만 따로 떼어놓으면 되잖아.”
그 생각이 이 시리즈의 출발점이다. 다만 시작하기 전에 하나를 못 박아야 한다. 지금 이 상태, 그러니까 서비스 전체가 한 덩어리로 빌드되고 한 번에 배포되는 상태는 고장난 게 아니다. 대부분의 서비스가 여기서 시작하고, 상당수는 끝까지 여기 머무는 게 맞다.
나누는 이야기를 하려면 나누기 전 상태를 먼저 정확히 알아야 한다. 무엇이 쉬웠는지 모르면, 나눈 뒤에 무엇을 잃었는지도 모른다.
한 덩어리로 빌드되고 한 번에 배포된다
모놀리스는 애플리케이션 전체가 하나의 실행 단위로 빌드·배포되는 형태다. 주문·결제·회원·상품이 전부 한 프로젝트 안에 있고, 빌드하면 결과물이 하나 나오고, 그걸 서버에 올린다.
여기서 중요한 건 코드가 한 폴더에 있다는 게 아니다. 같이 빌드되고, 같이 올라가고, 같은 프로세스 안에서 돈다는 것이다. 그래서 주문 코드가 결제 코드를 부를 때 그냥 메서드를 호출한다. 네트워크를 타지 않는다.
그리고 대개 데이터베이스도 하나다. 주문 테이블과 상품 테이블이 같은 DB에 있어서, 조인 한 번으로 “이 주문에 들어간 상품 이름”을 가져올 수 있다.
한 덩어리라서 공짜로 얻는 것들
이 구조가 주는 편의는 너무 당연해서 잘 안 보인다. 나눠보기 전까지는.
호출은 실패하지 않는다. paymentService.pay(order)를 부르면 그 메서드는 실행되거나 예외를 던진다. “요청이 갔는지 안 갔는지 모르는” 상태가 없다.
한 트랜잭션으로 묶인다. 주문을 저장하고 재고를 줄이는 일을 @Transactional 하나로 감싸면, 둘 다 되거나 둘 다 안 된다. 반쪽만 남는 상태를 DB가 막아준다.
조인할 수 있다. “지난달 주문 중 특정 등급 회원의 것”을 쿼리 한 줄로 뽑는다.
디버깅이 한 곳에서 끝난다. 에러가 나면 스택 트레이스 하나에 원인부터 결과까지 다 들어 있다. 로그도 한 파일이다.
바꾸기 쉽다. 주문과 결제에 걸친 규칙을 바꿔야 하면 한 커밋으로 고치고 한 번에 배포한다.
이 다섯 가지가 이 시리즈의 나머지 편에서 하나씩 사라진다. 뒤 편들이 다루는 문제는 대부분 여기서 공짜로 받던 것을 잃은 자리를 메우는 이야기다. 그래서 지금 이 목록을 기억해 두는 게 좋다.
시작은 여기가 맞다
만들려는 게 무엇인지 아직 정확히 모르는 단계에서는, 경계를 잘못 그을 확률이 매우 높다. 그리고 경계를 잘못 그었을 때 되돌리는 비용이 양쪽에서 완전히 다르다.
한 덩어리 안에서 그은 경계가 틀렸으면 클래스를 옮기고 패키지를 바꾸면 된다. 컴파일러가 도와준다. 하지만 서비스로 갈라놓고 나서 경계가 틀렸다는 걸 알면, 배포 단위 둘과 데이터베이스 둘과 그 사이를 오가는 API를 다시 합쳐야 한다.
그래서 순서가 있다. 모르는 걸 나누는 게 아니라, 알게 된 다음에 나눈다.
나쁜 건 한 덩어리인 게 아니라 뒤엉킨 것이다
여기서 자주 섞이는 두 가지를 갈라야 한다. 모놀리스가 욕을 먹는 이유는 대개 “한 덩어리”여서가 아니라 **“안이 뒤엉켜서”**다.
왼쪽은 아무나 아무 클래스를 부르고, 참조가 양방향으로 얽혀 있다. 하나를 고치면 어디가 같이 바뀌는지 아무도 답을 못 한다. 오른쪽은 같은 한 덩어리인데 누가 누구를 부를 수 있는지가 정해져 있다.
문제는 왼쪽 상태다. 그리고 왼쪽 상태를 그대로 잘라 서비스로 나누면, 네트워크로 얽힌 왼쪽이 된다. 얽힘은 그대로인데 호출만 느려지고 실패할 수 있게 된 것이다. 나누기가 실패하는 가장 흔한 방식이다.
안에서 먼저 나눈다
그래서 먼저 할 일은 서비스를 나누는 게 아니라 한 덩어리 안에서 경계를 긋는 것이다.
- 모듈 단위로 나눈다. 주문 모듈, 결제 모듈처럼 업무 단위로 묶는다. 컨트롤러 폴더·서비스 폴더로만 나뉘어 있으면 업무 경계는 안 보인다.
- 바깥에 열어줄 것만 공개한다. 모듈 안쪽 클래스는 밖에서 못 부르게 한다. 언어가 지원하면 언어에 맡긴다.
- 의존 방향을 한쪽으로 정한다. 주문이 결제를 알아도 결제는 주문을 모르게 한다. 양방향이 되는 순간 둘은 한 덩어리다.
- 테이블도 주인을 정한다. 회원 테이블은 회원 모듈만 쓰게 한다. 아무 모듈이나 아무 테이블을 읽으면 코드 경계는 장식이다.
이렇게 해두면 두 가지를 얻는다. 지금 당장 코드가 이해 가능해지고, 나중에 정말 나눠야 할 때 자를 선이 이미 그어져 있다. 나누기 좋은 모놀리스가 나누기 전에 해야 할 일의 전부다.
경계를 어디에 그을지는 “함께 바뀌는 것끼리 묶는다”로 판단한다. 요구사항 하나가 들어왔을 때 늘 같이 고쳐지는 코드들은 한 모듈이다. 반대로 몇 달째 서로 안 건드리는 것들은 다른 모듈이다.
그래도 아파지는 지점
경계를 잘 그어도 한 덩어리라서 어쩔 수 없는 게 남는다. 여기가 나누는 이야기가 시작되는 자리다.
배포가 묶인다. 맨 앞 장면이다. 문구 하나를 고쳐도 전체를 다시 올린다. 그리고 관계없는 모듈의 버그가 배포에 딸려 나간다.
장애가 번진다. 한 프로세스라 메모리가 마르거나 스레드가 다 잡히면 전부 같이 죽는다. 통계 화면의 무거운 쿼리 하나가 결제까지 못 하게 만드는 일이 실제로 벌어진다.
확장 단위가 통째다. 상품 조회만 바쁘고 나머지는 한가해도, 늘리려면 전부 들어 있는 덩어리를 통째로 복제해야 한다.
속도가 느려진다. 코드가 커질수록 빌드와 테스트가 오래 걸린다. 배포 한 번이 부담스러워지면 배포 주기가 길어지고, 한 번에 나가는 변경 덩어리가 커지고, 그래서 더 위험해진다. 악순환이 돈다.
기술이 고정된다. 한 프로세스라 언어도 런타임도 하나다. 특정 기능에 다른 도구가 훨씬 유리해도 못 바꾼다.
통증의 정체는 크기가 아니다
주의할 게 있다. 위 다섯 개 중 어느 것도 “코드가 크다” 자체가 원인이 아니다. 원인은 셋으로 압축된다.
| 통증 | 진짜 원인 |
|---|---|
| 배포가 묶인다 | 배포 단위가 하나 |
| 장애가 번진다 | 실행 단위가 하나 |
| 확장이 통째다 | 자원 단위가 하나 |
나눈다는 건 이 셋을 갈라놓는 것이지, 코드를 여러 폴더에 흩어놓는 게 아니다. 그래서 코드가 지저분해서 아픈 거라면 나눠도 안 낫는다. 그건 리팩터링으로 고칠 문제다.
그래서 언제 나누나
실무에서는 대개 아래 신호가 여러 개 겹칠 때 나눈다. 하나만으로는 근거가 약하다.
- 배포가 서로를 막는다. 한 팀이 배포를 기다리느라 다른 팀 일정이 밀리는 일이 반복된다.
- 한 부분의 부하 특성이 확연히 다르다. 특정 기능만 자원을 몇 배로 먹거나, 트래픽 모양이 다른 기능과 전혀 다르다.
- 터졌을 때 같이 죽으면 안 되는 것이 있다. 결제가 통계 화면 때문에 멈추면 안 된다는 게 업무 요구로 존재한다.
- 경계가 이미 안정적이다. 그 모듈이 몇 달간 다른 모듈과 거의 안 얽혔다.
반대로 이런 상태면 나누지 않는 쪽이 낫다.
- 경계가 아직 자주 바뀐다. 요구사항이 들어올 때마다 여러 모듈을 같이 고친다
- 배포·모니터링·장애 대응을 자동화해 둔 게 별로 없다. 서비스가 늘면 운영해야 할 대상이 배로 늘어난다
- 지금 아픈 이유가 “코드가 뒤엉켜서”다
나누는 걸 승급으로 보면 판단이 망가진다. 나누기는 배포·장애·확장 단위를 갈라놓는 대신 네트워크·정합성·추적이라는 비용을 새로 지는 거래다. 거래는 얻는 게 치르는 것보다 클 때만 이득이다. 그 계산을 안 하고 “요즘은 다 그렇게 한다”로 결정하면, 아픈 데는 그대로인 채 새 통증만 늘어난다.
정리
| 모놀리스란 | 전체가 한 단위로 빌드·배포되고 한 프로세스에서 도는 형태 |
| 공짜로 얻는 것 | 실패 없는 호출, 한 트랜잭션, 조인, 한 곳의 로그, 한 번의 배포 |
| 문제인 건 | 한 덩어리인 것이 아니라 안이 뒤엉킨 것 |
| 먼저 할 일 | 나누기 전에 안에서 모듈 경계·의존 방향·테이블 주인을 정한다 |
| 아픈 자리 | 배포 단위·실행 단위·자원 단위가 하나로 묶여 있다 |
| 나눌 때 | 그 신호들이 겹치고, 경계가 이미 안정적일 때 |
한 덩어리는 출발점이지 실패가 아니다. 다음 글에서는 그 덩어리를 실제로 갈랐을 때 무엇이 달라지는지를 본다. 얻는 것과, 그 대가로 새로 생기는 것을 나란히 놓는다.