🧩 대규모 시스템
동시 접속자와 초당 요청량
- 사용자수: 하루 접속량 파악보다 더 중요한 게 동시 접속자의 요청 수임
- TPS (Transactions Per Second): 초당 처리되는 트랜잭션의 수
- 시스템이 얼마나 많은 요청을 동시에 처리할 수 있는지를 나타내는 지표
- 시스템이 많은 TPS를 견딜 수 있기 위해서는 특정 시간대에 TPS가 가장 많은 시간을 파악해야 함
- 예상치 못한 이벤트로 인해 설계 예상 이상의 요청이 몰린다면 시스템이 멈출 수 있음. 이러한 상황을 대비하기 위해 애플리케이션의 수를 늘리거나, 사용자 대기열을 설정하거나, 자동 스케일링을 통해 시스템의 자원을 동적으로 할당해 부하를 분산하는 방법이 있음.
요청 종류에 따른 개발
- 캐시를 사용해 읽기 요청을 최적화할 수 있음
- 모든 사용자가 같은 데이터를 요청하는 경우 이를 해소하기 위해 DB의 데이터를 Redis 같은 캐시에 올려두면 요청에 대한 응답 속도를 빠르게 할 수 있음
- 캐시를 사용하면 응답속도를 높이고 DB의 부하를 줄일 수 있음
- 캐시에 데이터가 없을 경우에만 DB를 조회하고 이후 결과를 캐시에 저장해 재사용함
- Redis와 같은 인메모리 캐시는 DB보다 속도가 월등히 빠르고 애플리케이션 메모리 내 캐싱은 속도를 더 높일 수 있지만, 데이터 유실 등의 이슈를 함께 고려해야 함
- 개인화되지 않은 데이터의 경우 캐시를 적극 활용하면 전체적인 응답 속도 향상과 DB 자원 절약이 가능하며, 만약 캐시에 데이터가 없을 땐 DB에서 읽어와 다시 캐시에 저장하면 됨
- DB 데이터가 변경될 때 캐시에 담긴 데이터도 동일하게 무효화 또는 갱신되어야 데이터 일관성을 확보할 수 있음
- 대용량 JSON 데이터의 경우 필요한 항목만 필터링해서 애플리케이션 내에서 처리하면 성능상 이점이 있음 (DB 필터링이 항상 만능을 아님)
- 시스템이 읽기 전용인지, 쓰기 및 업데이트 전용인지도 중요함
- DB 읽기/쓰기에서 대부분의 시간이 소요되므로 성능 최적화의 주요 대상이 됨
- DB에 인덱스를 걸면 검색은 빨라지지만 인덱스가 많아질수록 데이터 생성(CREATE) 작업과 트랜잭션 처리 시간이 길어져 자원을 더 소모하게 됨
- DB에서 필터링된 데이터를 가져오는 것보다 Redis나 애플리케이션 레벨에서 필터링을 수행하는 것이 성능 상 더 이점이 있음
- 데이터의 구조가 단순할 경우 Redis에서 모든 데이터를 불러온 뒤 필요한 데이터만 애플리케이션 함수에서 추출하는 방식도 있음
- 데이터가 중첩된 JSON 구조라면 전체 데이터를 통으로 가져와 함수 내에서 처리하는 것이 효율적임
- 인덱싱이 잘 되어 있으면 쿼리로 필요한 데이터만 가져오는 게 빠르기는 하지만 만능은 아님
- 데이터가 작을 때는 통째로 가져와 메모리에서 처리하는 방법도 충분히 사용 가능함
- 엣지 캐싱은 사용자와 가까운 곳에서 데이터를 제공하므로 네트워크 지연을 최소화하고 적은 수의 애플리케이션으로도 요청을 처리할 수 있음
- 캐시 갱신 정책도 중요함 (자주 변경되지 않는 경우에는 유효기간을 길게, 변경이 잦은 경우에는 짧게)
- 데이터 소실의 위험을 줄이기 위해 데이터의 유효성을 지속적으로 검증하고, 데이터가 손실되지 않았는지 확인해야 함
- 캐시 계층에서 데이터 소실 시 재요청을 통해 DB에서 데이터를 다시 가져올 수 있도록 해야 함
- Redis에 데이터를 무제한 저장하기보다는 TTL(Time-To-Live)을 설정해 강제적으로 퍼지가 일어나도록 하고, 이후에는 다시 DB에서 데이터를 가져와 갱신하는 방식을 사용하기도 함
대규모 시스템에서 Redis 캐시 사용 시 주의사항
- Redis 캐시에 불필요한 데이터까지 객체 전체를 직렬화해서 저장하면 페이징 데이터 등으로 인해 사용량이 급격히 늘어 시스템이 다운될 수 있음
- 커넥션 타임아웃 설정이 없을 경우, Redis 장애 시 애플리케이션이 응답 대기 상태에 빠지고 대량의 동시 요청이 한꺼번에 DB로 몰려 전체 장애가 발생 가능함
- 커넥션 타임아웃을 3초나 4초 등으로 설정하여 문제가 발생했을 때 DB로의 과도한 접근을 방지할 수 있음
- 캐시 오버플로나 타임아웃이 발생하면 프론트엔드 쪽에서 사용자에게 목데이터를 반환해 에러 상황을 감추고 시스템의 가용성을 유지하는 방법도 있음
- Spring에서 어노테이션으로 객체 전체 캐싱 시 불필요한 정보까지 포함되는 경우가 많으므로, 반드시 필요한 데이터만 가볍게 캐싱하도록 최적화해야 함
- 캐시 용량 모니터링 및 알람 설정을 통한 사전 조치가 중요함
🧩 DB 최적화
읽기 요청 최적화
- DB 인덱싱
- 조회 성능을 크게 향상시킬 수 있는 방법
- 인덱스를 사용하면 데이터베이스는 데이터를 빠르게 검색 가능함
- 읽기 성능을 최적화하고 쿼리 응답 시간을 줄일 수 있지만, 인덱스가 너무 많으면 쓰기 성능이 저하될 수 있음
- DB 샤딩
- DB를 여러 shard로 분할해 각각의 shard가 독립적으로 쿼리를 처리하게 하는 방법
- 단일 DB에 대한 부하 분산 가능, 읽기 요청 응답 속도 요청 향상 가능
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_id INT,
order_date DATE,
amount DECIMAL
) PARTITION BY RANGE (order_date);
-- 테이블 파티셔닝 예시
CREATE TABLE orders_2023_01 PARTITION OF orders FOR VALUES FROM ('2023-01-01') TO ('2023-02-01');
CREATE TABLE orders_2023_02 PARTITION OF orders FOR VALUES FROM ('2023-02-01') TO ('2023-03-01');
- 읽기 전용 DB
- 읽기 요청을 주로 처리하는 DB 인스턴스
- 데이터를 주기적으로 동기화해서 최신 상태를 유지하며 읽기 요청을 처리함
- 쿼리 최적화
- SQL 쿼리를 효율적으로 작성하여 데이터베이스의 읽기 성능을 향상시키는 방법
쓰기 요청 최적화
- 비동기 처리
- 쓰기 요청을 비동기 방식으로 처리하면 DB에 직접 접근하지 않고도 빠르게 응답을 반환할 수 있음
- 예: 메시지를 바로 DB에 쓰지 않고 큐에 넣었다가 나중에 처리
- 비동기 처리는 특히 높은 트래픽을 처리할 때 유용하며, 사용자는 요청을 보낸 즉시 응답을 받을 수 있음
- 비동기 처리 시 데이터 소실이나 오류를 방지하기 위해 큐에 데이터를 넣을 때 적절한 검증을 수행하고, 큐에 쌓인 데이터를 지속적으로 모니터링하여 실패한 요청을 재시도할 수 있는 메커니즘을 마련해야 함
- 또한 데이터의 순서를 보장하고 중복 처리를 방지하기 위해 고유 ID를 사용해야 함
- 배치 처리
- 실시간 처리할 필요 없는 쓰기 요청은 Batch 처리를 통해 한번에 처리 가능
- 예: 일정 시간마다 큐에 쌓인 메시지를 DB에 저장하는 방식
- 배치 처리 시에도 데이터 소실을 방지하기 위해 배치 작업 중 오류가 발생한 경우 이를 기록하고, 재시도할 수 있는 메커니즘을 마련해야 함
- 또한 배치 작업의 상태를 모니터링하고, 배치 작업이 완료되었는지 확인하는 프로세스를 마련해야 함
- 분산 DB
- 단일 DB로 모든 쓰기 요청을 처리하기 어렵다면 분산 DB를 사용해 부하를 분산시킬 수 있음
- 여러 개의 DB인스턴스를 사용해 각 인스턴스가 특정 사용자 그룹의 데이터를 처리하도록 할 수 있음
- 데이터를 여러개의 노드에 분산 저장해 고가용성과 확장성을 제공함
- Sharding 기법을 통해 DB를 수평 분할해 각 샤드가 독립적으로 쓰기 작업을 처리하도록 할 수 있음
- 샤딩의 성능은 설계에 따라 달라지며, 시스템에 맞지 않게 적용하면 오히려 퍼포먼스가 저하될 수 있으므로 유의해야 함
- 분산 DB 사용 시 데이터 일관성을 유지하기 위해 트랜잭션 관리와 데이터 동기화에 신경써야 함
🧩 데이터 일관성 유지
분산 트랜잭션
- 트랜잭션(Transaction): DB의 상태를 변환하는 작업의 단위
- 원자성(Atomicity): 트랜잭션은 전부 성공하거나 전부 실패해야 함
- 일관성(Consistency): 트랜잭션이 완료된 후에도 DB는 모든 무결성 제약 조건을 유지해야 함
- 격리성(Isolation): 동시에 실행되는 트랜잭션이 서로 간섭하지 않도록 보장함
- 지속성(Durability): 트랜잭션이 성공적으로 완료된 후의 결과는 시스템 장애가 발생해도 영구적으로 유지되어야 함
- 분산 트랜잭션(Distributed Transaction)
- 여러 개의 독립된 시스템이나 DB에서 동시에 일어나는 트랜잭션을 일관되게 관리하는 방법
- MSA에서 여러 서비스가 독립적으로 실행되고 DB도 각각 연결됨. 여러 개의 애플리케이션을 거쳐 가는 도중 에러가 발생하면 전체가 롤백되어야 함
- 단일 트랜잭션이 여러 시스템에 걸쳐 발생할 때 모든 시스템이 해당 트랜잭션을 성공적으로 완료하거나 모든 시스템이 트랜잭션을 실패로 처리하도록 보장함
- 2PC(Two-Phase Commit)
- 준비 단계(Prepare Phase): 각 참여 노드는 트랜잭션 준비 상태를 확인하고 준비 완료를 마스터 노드에 알림
- 커밋 단계(Commit Phase): 마스터 노드는 모든 참여 노드가 준비되었음을 확인하고 트랜잭션을 커밋하도록 지시함. 준비되지 않은 노드가 있다면 트랜잭션을 롤백함
- 사가 패턴(Saga Pattern)
- 트랜잭션을 여러 단계로 나누어 처리하고, 각 단계가 독립적으로 커밋됨
- 실패 시 보상 트랜잭션을 실행하여 상태를 롤백함
- 주문 → 결제 → 재고감소 각 단계가 성공적으로 완료되면 다음 단계로 넘어가고 실패하면 이전단계에서 수행된 작업을 취소함
- 이벤트 소싱(Event Sourcing)
- 상태 변화를 이벤트로 기록하고 해당 이벤트를 재생하여 현재 상태를 유지함
분산 트랜잭션의 장점
- 데이터 일관성 보장
- 확장성
- 신뢰성
- 복구 가능 → 안정성 향상
분산 트랜잭션의 단점
- 복잡성 증가: 여러 시스템 간의 트랜잭션 동기화와 데이터 일관성을 유지해야 함
- 성능 저하: 2PC와 같은 프로토콜을 사용할 경우 트랜잭션의 준비와 커밋 단계에서 지연이 발생 가능
- 네트워크 오버헤드: 여러 시스템 간 통신 필요
- 복구의 어려움: 분산 트랜잭션 실패 시, 모든 시스템에서 일관된 상태로 롤백하는 것이 어려울 수 있음
분산 트랜잭션의 예시
- 2PC를 사용한 주문 생성과 결제 처리
- 주문을 생성하고 결제를 진행할 때 주문 서비스와 결제 서비스가 각각 독립된 DB를 사용한다면, 분산 트랜잭션을 통해 두 서비스가 일관되게 주문을 처리해야 함
- 준비 단계: 주문 서비스와 결제 서비스가 트랜잭션을 준비 → 두 서비스가 모두 준비 완료 상태를 마스터 노드에 알림
- 커밋 단계: 마스터 노드는 두 서비스가 모두 준비되었음을 확인하고 트랜잭션을 커밋하도록 지시함. 하나라도 준비되지 않았으면 트랜잭션을 롤백함