프로그래밍 PROGRAMMING/아키텍쳐

Cell-based Architecture 적용하기

매운할라피뇽 2026. 3. 17. 08:30
반응형
Cell-based Architecture 적용하기


Cell-based Architecture는 대규모 분산 시스템에서 장애 전파를 차단하고 확장성을 확보하기 위한 아키텍처 패턴입니다. 전체 시스템을 독립적인 "셀(Cell)" 단위로 분리함으로써, 특정 셀에서 발생한 장애가 다른 셀로 번지지 않도록 격리합니다. Amazon, Netflix, Slack 등 대형 서비스가 수천만 사용자를 안정적으로 운영하는 데 이 패턴을 활용하고 있으며, 이 글에서는 Cell-based Architecture의 핵심 개념과 실제 프로젝트에 적용하는 방법을 다룹니다.


Cell-based Architecture란 무엇인가

Cell-based Architecture는 시스템 전체를 동일한 기능을 갖는 셀(Cell) 이라는 독립 단위로 수평 분할하는 설계 패턴입니다. 각 셀은 자체적인 컴퓨팅, 스토리지, 네트워크 리소스를 보유하며, 다른 셀과 직접 통신하지 않습니다.
기존 마이크로서비스 아키텍처(MSA)는 기능(Function) 단위로 서비스를 분리하지만, Cell-based Architecture는 테넌트(Tenant) 또는 사용자 그룹 단위로 분리합니다. 예를 들어 1,000만 명의 사용자가 있다면 이를 10개의 셀로 나누어 각 셀이 100만 명씩 처리하는 방식입니다.
이 패턴의 핵심 가치는 폭발 반경(Blast Radius) 최소화입니다. 하나의 셀에 장애가 발생하더라도 전체 사용자의 10%만 영향을 받으며, 나머지 90%는 정상 서비스를 유지할 수 있습니다.

MSA와의 비교

분리 기준기능(Function)사용자/테넌트 그룹
장애 범위해당 서비스 전체특정 셀 내 사용자만
데이터 공유서비스 간 DB 분리셀 간 DB 완전 격리
배포 단위서비스 단위셀 단위

셀 라우팅 설계 및 구현

Cell-based Architecture에서 가장 중요한 컴포넌트는 셀 라우터(Cell Router)입니다. 모든 요청은 셀 라우터를 통해 올바른 셀로 전달됩니다. 라우팅 전략으로는 사용자 ID 해시 기반, 지역 기반, 테넌트 기반 방식이 주로 사용됩니다.
아래는 Spring Boot를 사용한 사용자 ID 해시 기반 셀 라우터 구현 예시입니다.

@Component
public class CellRouter {

    private static final int CELL_COUNT = 10; // 총 셀 수
    private final List<String> cellEndpoints;

    public CellRouter(@Value("${cell.endpoints}") List<String> cellEndpoints) {
        this.cellEndpoints = cellEndpoints;
    }

    /**
     * 사용자 ID를 기반으로 라우팅할 셀 인덱스를 결정합니다.
     * MurmurHash를 사용해 균등 분산을 보장합니다.
     */
    public String resolveCell(String userId) {
        int cellIndex = Math.abs(userId.hashCode()) % CELL_COUNT;
        return cellEndpoints.get(cellIndex);
    }
}
@RestController
@RequiredArgsConstructor
public class GatewayController {

    private final CellRouter cellRouter;
    private final RestTemplate restTemplate;

    @GetMapping("/api/**")
    public ResponseEntity<String> route(
            HttpServletRequest request,
            @RequestHeader("X-User-Id") String userId) {

        String cellEndpoint = cellRouter.resolveCell(userId);
        String targetUrl = cellEndpoint + request.getRequestURI();

        // 요청을 해당 셀로 프록시
        return restTemplate.getForEntity(targetUrl, String.class);
    }
}

실행 결과 예시:

userId=user-001  → cell-03 (https://cell-03.internal/api/orders)
userId=user-002  → cell-07 (https://cell-07.internal/api/orders)
userId=user-003  → cell-03 (https://cell-03.internal/api/orders)

각 사용자는 자신에게 할당된 셀로 일관되게 라우팅되므로, 데이터 로컬리티(Data Locality)가 확보되고 캐시 효율이 높아집니다.


셀 격리 전략과 장애 대응

셀 간 완전한 격리를 위해서는 다음 세 가지 레이어에서 격리를 구현해야 합니다.

1. 컴퓨팅 격리

각 셀은 독립적인 Kubernetes 네임스페이스 또는 별도의 클러스터로 운영합니다. 네임스페이스 격리만으로도 리소스 쿼터(Resource Quota)와 네트워크 폴리시(NetworkPolicy)를 통해 상당한 수준의 격리가 가능합니다.

# cell-03 네임스페이스 리소스 쿼터 예시
apiVersion: v1
kind: ResourceQuota
metadata:
  name: cell-03-quota
  namespace: cell-03
spec:
  hard:
    requests.cpu: "16"       # CPU 요청 상한
    requests.memory: "32Gi"  # 메모리 요청 상한
    pods: "50"               # 최대 파드 수
---
# 셀 간 네트워크 통신 차단
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-cross-cell
  namespace: cell-03
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              cell: "cell-03"  # 동일 셀 내 트래픽만 허용

2. 데이터 격리

각 셀은 전용 데이터베이스 인스턴스를 사용합니다. 공유 DB를 사용하면 특정 셀의 쿼리 폭주가 전체 셀에 영향을 미치므로, 셀별 독립 DB는 이 아키텍처의 핵심 요건입니다.

3. 서킷 브레이커 패턴 적용

셀 라우터 레벨에서 Resilience4j를 사용해 특정 셀 장애 시 트래픽을 다른 셀로 임시 우회(Failover)하는 로직을 구현할 수 있습니다.

@Component
public class FaultTolerantCellRouter {

    private final CircuitBreakerRegistry circuitBreakerRegistry;
    private final CellRouter primaryRouter;

    public String resolveCell(String userId) {
        String primaryCell = primaryRouter.resolveCell(userId);
        CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker(primaryCell);

        // 해당 셀 장애 시 대체 셀로 페일오버
        return cb.executeSupplier(() -> primaryCell,
            throwable -> getFailoverCell(userId));
    }

    private String getFailoverCell(String userId) {
        // 장애 셀 제외하고 재해싱
        // 실제 구현에서는 헬스체크 상태를 참조합니다
        return "https://failover-cell.internal";
    }
}

장애 시나리오 흐름:

[사용자 요청] → [셀 라우터]
  → cell-03 장애 감지 (CircuitBreaker OPEN)
  → failover-cell로 우회
  → 영향 범위: cell-03 할당 사용자만 (전체의 10%)
  → 나머지 90% 사용자는 정상 서비스 유지

운영 환경에서의 셀 관리

운영 환경에서 Cell-based Architecture를 도입할 때 고려해야 할 실용적인 사항들이 있습니다.
셀 크기 결정: 셀이 너무 크면 장애 범위가 넓어지고, 너무 작으면 운영 복잡도가 증가합니다. 일반적으로 전체 트래픽의 5~15%를 처리하는 크기가 적절하며, 단일 셀 장애 시 허용 가능한 최대 서비스 영향도를 기준으로 결정합니다.
셀 재균형(Rebalancing): 사용자 성장에 따라 셀을 추가하거나 분할해야 할 경우가 생깁니다. 이때 사용자-셀 매핑 정보를 외부 라우팅 테이블(Redis, DynamoDB 등)에 저장하면, 하드코딩된 해시 함수보다 유연하게 재균형 작업이 가능합니다.
관찰 가능성(Observability): 각 셀의 지표를 개별적으로 수집하되, 셀 ID를 메트릭 레이블로 추가하여 셀별 SLI/SLO 현황을 한눈에 파악할 수 있도록 구성하는 것이 중요합니다.


맺음말

Cell-based Architecture는 수백만 명 이상의 사용자를 안정적으로 운영해야 하는 서비스에서 특히 효과적인 패턴입니다. 장애 격리와 확장성이라는 두 가지 목표를 동시에 달성할 수 있으며, 마이크로서비스 아키텍처와 상호 배타적이지 않고 함께 적용할 수 있습니다.
도입을 고려할 때는 다음 기준을 참고하시기 바랍니다.

  1. 적합한 경우: 멀티테넌트 SaaS, 대규모 소비자 서비스, 99.99% 이상의 가용성이 요구되는 시스템
  2. 신중히 검토해야 하는 경우: 소규모 서비스, 셀 간 데이터 집계가 잦은 시스템 (크로스-셀 쿼리는 별도 집계 파이프라인 필요)

관련 심화 주제로는 Bulkhead 패턴, Shuffle Sharding, AWS의 셀 기반 아키텍처 접근법(AWS Well-Architected)을 참고하시면 이해를 넓히는 데 도움이 됩니다.

반응형