AI 요약
서비스는 처음엔 한 대의 서버와 DB로 시작하지만, 트래픽과 요구가 커지며 세션 공유, 캐시 일관성, 읽기/쓰기 분산, 트랜잭션 일관성, 서비스 간 결합도 같은 현실적 문제를 해결하려다 보니 Redis, Replica, Sharding, Kafka 같은 구성으로 자연스럽게 복잡해진다. 이런 변화들은 기술 이름을 외우기보다 "무슨 문제가 생겼고 그걸 왜 이렇게 풀었나"를 이해하는 게 핵심이라는, 약간 골치 아프지만 당연한 결론으로 이어진다.
처음부터 Redis, Kafka, Replica, Sharding을 넣는 서비스는 드물다.
대부분의 시스템은 단순하게 시작하고, 트래픽과 요구사항이 커지면서 생기는 문제를 해결하다 보니 분산 시스템이 되는것이다.
처음에는 이것만 있어도 된다.
Spring 서버 한 대가 요청을 받고 DB 한 대에 데이터를 저장한다.
구조가 단순하고 트랜잭션도 관리하기 쉽다.
문제는 트래픽이 늘면서 시작된다.
Spring 서버의 CPU의 코어를 늘리거나 메모리를 늘려 더 좋은 서버로 바꿀 수 있다.
이게 Scale Up이다.
가장 단순하지만 서버 한 대를 계속해서 키울 수는 없는 노릇이다.
그래서 다음 선택을 하게 된다.
서버를 여러 대 띄우면 안될까?
동일한 Spring 애플리케이션을 여러 서버에 실행한다.
01 Spring A02 ↗03사용자 → Load Balancer → Spring B04 ↘05 Spring CA, B, C에는 기본적으로 같은 프로젝트가 배포되어 있다.
로드 밸런서가 요청을 나눠준다.
01요청 1 → A02요청 2 → B03요청 3 → C한 서버가 죽어도 다른 서버가 요청을 받을 수 있다는 장점도 생긴다.
그런데 새로운 문제가 나타난다.
A와 B는 서로 다른 JVM이다.
A 서버에 로그인 세션을 저장했다고 해보자.
01첫 번째 요청 → A02두 번째 요청 → BB는 A 메모리에 저장된 세션을 알 수 없다.
그래서 방법 중 하나가 공유 저장소를 사용하는 것이다.
01Spring A ─┐02Spring B ─┼→ Redis03Spring C ─┘또는 JWT처럼 서버가 로그인 상태를 가지지 않는 Stateless 구조를 사용할 수도 있다.
Spring 서버는 계속 늘릴 수 있는데 모든 서버가 결국 같은 DB를 바라본다.
01Spring A ─┐02Spring B ─┼→ DB03Spring C ─┘특히 같은 데이터를 계속 조회하면 DB 입장에서는 낭비가 심하다.
그래서 Redis 같은 캐시를 둔다.
첫 요청은 DB까지 가지만 이후에는 Redis에서 바로 반환할 수 있다.
대신 새로운 문제가 생긴다.
DB와 Redis의 데이터가 달라질 수 있다는 점이다.
즉 캐시 정합성을 관리해야 한다.
캐시만으로 모든 조회를 해결할 수는 없다.
그래서 DB 자체도 복사한다.
01 Primary02 / \03 Replica A Replica B보통
01INSERT / UPDATE / DELETE → Primary02 03SELECT → Replica이렇게 읽기 부하를 분산할 수 있다.
하지만 복제에는 시간이 걸릴 수 있다.
Primary에는 최신값이지만 Replica에는 아직 이전값이 들어갈 수 있는 것이다.
이게 Replication Lag이다.
여기서부터 '여러 서버의 데이터가 항상 같은가?' 라는 일관성 문제가 등장한다.
Replica를 늘려도 쓰기는 여전히 Primary 한 곳으로 몰릴 수 있다.
그러면 데이터를 여러 DB에 나누는 방법을 생각한다.
01Shard A → 사용자 A02Shard B → 사용자 B03Shard C → 사용자 C이게 Sharding이다.
키를 나누는 방법은 여러가지가 있다.
Replication과 차이는 중요하다.
01Replication02- 같은 데이터의 복사본03 04Sharding05- 서로 다른 데이터를 나눠 저장쓰기 부하까지 분산할 수 있지만 대가가 크다.
예전에는 하나의 DB 트랜잭션이면 됐던 일이
01Shard A 성공02Shard B 실패이처럼 여러 DB의 일관성 문제가 된다.
처음에는 하나의 프로젝트가 전부 담당할 수 있다.
서비스가 커지면서 필요하다면
01Order Service02Payment Service03Inventory Service04Member Service서버별로 이렇게 나눌 수 있다.
이게 마이크로서비스다.
문제는 예전의 단순한 메서드 호출이
이 흐름처럼 된다는 것이다.
그래서 네트워크 장애를 고려해야 한다.
주문이 생성될 때
01Order02→ Email03→ Point04→ Notification전부 동기 Http 호출하면 Email 장애가 주문까지 영향을 줄 수 있다.
그래서
처럼 이벤트를 중간에 둔다.
Order는 '주문이 생성됐다' 만 Kafka에 남기고 끝낸다.
그 다음은 Email, Point 등의 Consumer가 각자 처리한다.
이렇게 서비스 간의 결합도를 낮추고 장애 전파를 줄일 수 있다.
이번에는 이런 문제가 생긴다.
그래서 주문과 함께 outbox를 같은 DB 트랜잭션으로 처리한다.
그리고 별도의 Publisher가 Outbox를 읽어 Kafka로 전송한다.
자세한 내용은 전 글에 있다.
처음에는 하나의 서버와 하나의 DB로 시작해서,
이렇게 발전하는 것이다.
중요한 건 기술 이름을 외우는게 아니라 '어떤 문제가 생겼고, 그 문제를 해결하기 위해 왜 이 기술을 사용했는가?' 를 이해하는 것이다.
분산 시스템은 처음부터 복잡하게 설계하는 것이 아니라, 단일 시스템을 확장하면서 생기는 문제를 해결하다 보니 자연스럽게 만들어지는 구조라고 생각하자.
하;; 왜 이렇게 어렵냐??