TimescaleDB Continuous Aggregates: 실시간 vs Materialized-Only

발행: (2026년 3월 8일 PM 07:00 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

위의 링크에 포함된 전체 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다.
(코드 블록, URL 및 마크다운 형식은 그대로 유지됩니다.)

연속 집계란 무엇인가?

연속 집계는 자체 숨겨진 하이퍼테이블을 기반으로 하는 물리화된 뷰입니다. TimescaleDB는 사전에 계산된 집계 결과를 이 물리화 하이퍼테이블에 저장하므로, 쿼리는 수백만 개의 원시 레코드를 스캔하는 대신 압축된 요약 행을 읽습니다.

CREATE MATERIALIZED VIEW hourly_device_metrics
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 hour', event_timestamp_utc) AS bucket_hour_utc,
    device_id,
    AVG(metric_value)      AS avg_metric_value,
    MAX(metric_value)      AS max_metric_value,
    COUNT(*)               AS event_count
FROM sensor_events
GROUP BY bucket_hour_utc, device_id;

CAGG를 생성하면 구조가 정의되지만 데이터가 채워지지는 않습니다. 물리화 하이퍼테이블은 새로 고침 정책을 추가하거나 수동으로 새로 고침을 실행할 때까지 비어 있습니다.

새로 고침 정책

새로 고침 정책은 TimescaleDB에게 슬라이딩 시간 창에 대해 주기적으로 집계를 다시 계산하도록 지시합니다:

SELECT add_continuous_aggregate_policy(
    'hourly_device_metrics',
    start_offset      => INTERVAL '3 hours',
    end_offset        => INTERVAL '1 hour',
    schedule_interval => INTERVAL '30 minutes'
);

세 매개변수의 상호 작용 방식

매개변수의미
start_offsetnow() 기준으로 새로 고침 창이 시작되는 시점이 얼마나 과거인지. 3 시간이면 정책은 3시간 전부터 데이터를 재구성합니다.
end_offsetnow() 기준으로 새로 고침 창이 끝나는 시점이 얼마나 과거인지. 1 시간이면 1시간 이내의 최신 데이터는 정책에 의해 절대 물리화되지 않습니다.
schedule_interval정책이 실행되는 주기.

end_offset은 핵심 매개변수입니다. 이것은 의도적인 공백을 만듭니다: 정책이 고의적으로 건너뛰는 최신 데이터의 창입니다. 이는 버그가 아닙니다. 가장 최신의 시간 버킷은 여전히 데이터를 축적하고 있습니다. 부분적으로 채워진 버킷을 물리화한 뒤 몇 분 뒤에 다시 물리화하는 것은 불필요한 연산입니다. end_offset은 이러한 반복을 방지합니다.

실제 결과: 위와 같은 설정에서는 물리화된 데이터가 항상 최소 1시간 이상 오래된 것이며, 최대 1.5시간까지 오래될 수 있습니다 (end_offsetschedule_interval 한 사이클을 더한 값).

실시간 모드 (기본)

실시간 CAGG를 쿼리하면 TimescaleDB가 두 데이터 소스를 투명하게 결합합니다:

  1. 역사적 범위에 대한 물리화된 하이퍼테이블에서 사전 계산된 결과.
  2. 소스 하이퍼테이블에 대해 물리화 워터마크보다 최신인 데이터에 대한 실시간 집계.

두 결과 집합은 자동으로 합쳐집니다. 애플리케이션은 모든 데이터가 물리화된 것처럼 단일하고 완전한 결과 집합을 보게 됩니다.

쿼리 계획에서 확인할 수 있습니다: 두 자식을 가진 Append 노드 — 하나는 물리화된 하이퍼테이블을 스캔(빠름)하고, 다른 하나는 소스 하이퍼테이블을 스캔(느림, 원시 데이터에 대해 전체 집계를 수행)합니다.

트레이드‑오프

항목효과
역사적 부분빠름 (사전 계산된 행을 읽음)
최신 부분느림 (실시간으로 원시 데이터를 집계)
24시간 대시보드 전체 지연 시간처음 23시간은 빠르고, 마지막 1시간은 느림
end_offset가 크거나 마지막 새로 고침 이후 시간이 길수록더 많은 데이터가 느린 실시간 경로를 통과

Materialized‑Only Mode

실시간 union을 완전히 비활성화할 수 있습니다:

ALTER MATERIALIZED VIEW hourly_device_metrics
SET (timescaledb.materialized_only = true);

materialized‑only 모드에서는 CAGG가 사전 계산된 데이터만 반환합니다. 쿼리는 소스 하이퍼테이블에 절대 접근하지 않습니다. 가장 최신 데이터(end_offset 윈도우 내의 모든 데이터)는 표시되지 않습니다.

장점

  • 쿼리가 더 빠르고 예측 가능합니다; 실시간 집계 경로가 없고, 누적된 비물리화 데이터 양에 따라 성능이 변동하지 않습니다.
  • 쿼리 계획에 단일 스캔이 표시되어 물리화된 하이퍼테이블만 읽습니다.

데이터 손실 없이 언제든지 모드 간 전환이 가능합니다:

-- materialized‑only 로 전환 (쿼리는 더 빠르지만 최신 윈도우는 오래된 데이터)
ALTER MATERIALIZED VIEW hourly_device_metrics
SET (timescaledb.materialized_only = true);

-- real‑time 로 다시 전환 (최근 윈도우는 느리지만 전체 데이터 제공)
ALTER MATERIALIZED VIEW hourly_device_metrics
SET (timescaledb.materialized_only = false);

데이터가 삭제되거나 재계산되지 않으며, 토글은 쿼리 실행기가 실시간 union을 포함할지 여부만 변경합니다.

비교 표

항목실시간 (기본)Materialized‑Only
데이터 신선도현재 (지금까지)end_offset + schedule_interval에 따라 오래됨
최근 쿼리 성능느림 (실시간 집계)빠름 (materialization만)
과거 쿼리 성능동일동일
소스 하이퍼테이블에 접근예, 비물질화된 범위에 대해없음
최적 용도대시보드, 알림, 운영 모니터링보고서, 청구, 분석, 배치 파이프라인

Source:

언제 어떤 모드를 사용할까

  • 실시간 모드 (Real‑Time Mode) – 소비자가 최신 데이터를 기대하고 최근 윈도우에서 약간 높은 지연을 허용할 수 있을 때 사용합니다. 운영 대시보드와 알림에 이상적입니다.
  • Materialized‑Only 모드 – 신선도 요구사항이 완화되고 쿼리 일관성이 더 중요할 때 사용합니다. 청구 계산, 일일 보고서, 일정에 따라 실행되는 분석 파이프라인 등 실시간 집계 오버헤드가 필요 없는 경우에 이상적입니다.

무음 실패 모드

연속 집계의 무음 실패 모드는 staleness(오래됨) 입니다. Materialized‑Only 모드에서는 CAGG가 end_offsetschedule_interval 크기만큼 오래된 결과를 반환합니다. 애플리케이션의 신선도 요구사항에 맞는 모드를 선택하십시오.

-- 물리화 워터마크를 확인하고 오래됨을 감지
SELECT
    view_name,
    materialization_hypertable_name,
    (SELECT max(bucket_hour_utc) FROM hourly_device_metrics) AS latest_materialized,
    now() - (SELECT max(bucket_hour_utc) FROM hourly_device_metrics) AS staleness
FROM timescaledb_information.continuous_aggregates
WHERE view_name = 'hourly_device_metrics';

stalenessend_offsetschedule_interval을 크게 초과한다면, 새로 고침 정책이 제대로 실행되고 있지 않은 것입니다. timescaledb_information.job_stats에서 새로 고침 작업의 last_successful_finishtotal_failures를 확인하십시오.

Note:
새로 고침 정책에 있는 end_offset은 버그나 잘못된 설정이 아닙니다. 부분적으로 채워진 버킷의 불필요한 재계산을 방지하기 위한 의도된 설계 선택입니다.

  • 실시간 모드는 차이를 투명하게 메워 최신 결과를 보장합니다.
  • Materialized‑Only 모드는 속도가 신선도보다 더 중요할 때 유용합니다.

권장 사항

  1. 실시간 모드부터 시작하세요.
  2. 라이브 집계 오버헤드가 불필요한 특정 쿼리나 사용 사례를 식별한 후에만 materialized‑only 모드로 전환하세요.

모드 간 토글은 즉시이며 되돌릴 수 있습니다. 각 연속 집계가 어떤 모드에서 실행되고 있는지, 그리고 그 이유를 아는 것이 집계된 데이터의 신뢰성을 유지합니다.

0 조회
Back to Blog

관련 글

더 보기 »

Archify 소개: 아키텍처 아이디어에서 Spring Boot 코드까지

문제 모든 백엔드 개발자는 이 순간을 경험해 본 적이 있다: 새로운 프로젝트를 시작하면서 이미 아키텍처를 염두에 두고—아마도 간단한 REST 서비스와 몇 개의 엔드포인트만을 생각하고—시작한다. 하지만 프로젝트가 진행될수록 요구사항이 늘어나고, 복잡도가 급격히 증가한다. ### 원인 1. **초기 설계 부족** - 프로젝트 초기에 충분한 도메인 모델링과 데이터 흐름 설계를 하지 않으면, 나중에 구조를 바꾸는 데 큰 비용이 든다. 2. **기능 폭발** - 초기에는 “읽기 전용” API만 필요했지만, 곧 쓰기, 인증, 권한, 실시간 알림 등 다양한 기능이 추가된다. 3. **기술 스택 변화** - 새로운 라이브러리나 프레임워크가 등장하면서 기존 코드를 교체하거나 통합해야 하는 상황이 발생한다. ### 해결책 #### 1. 도메인 중심 설계(Domain‑Driven Design) 적용 - **Bounded Context**를 명확히 정의하고, 각 컨텍스트마다 독립적인 모델과 서비스 계층을 만든다. - **Entity**, **Value Object**, **Aggregate** 등을 활용해 비즈니스 로직을 캡슐화한다. #### 2. 레이어드 아키텍처 도입 - **Presentation Layer** (Controller / Handler) – HTTP 요청을 받아 DTO 로 변환하고, 서비스에 위임한다. - **Application Layer** – 트랜잭션 경계와 워크플로우를 정의한다. - **Domain Layer** – 핵심 비즈니스 로직과 도메인 규칙을 구현한다. - **Infrastructure Layer** – DB, 메시지 브로커, 외부 API 등 기술적 세부 사항을 담당한다. #### 3. 인터페이스와 의존성 역전(Dependency Inversion) - 상위 레이어가 하위 레이어에 의존하지 않도록 **Repository Interface**, **Service Interface** 등을 정의하고, 구현은 인프라 레이어에 둔다. - 스프링에서는 `@Autowired` 대신 **Constructor Injection**을 사용하고, 테스트 시에는 **Mock** 구현체를 주입한다. #### 4. 모듈화와 마이크로서비스 전략 - 시스템이 일정 규모를 넘어가면 **Domain‑Driven Microservices** 혹은 **Modular Monolith** 로 전환한다. - 각 모듈(또는 서비스)은 독립적인 데이터베이스 스키마와 배포 파이프라인을 갖는다. #### 5. 자동화된 테스트와 CI/CD 파이프라인 - **Unit Test**, **Integration Test**, **Contract Test**(예: Spring Cloud Contract) 를 레이어별로 작성한다. - GitHub Actions, GitLab CI 등으로 **빌드 → 테스트 → 배포** 흐름을 자동화한다. #### 6. 문서화와 코드 규칙 - OpenAPI/Swagger 로 API 스펙을 정의하고, **API‑First** 접근법을 채택한다. - 코드 스타일은 **Checkstyle**, **Spotless**, **EditorConfig** 로 일관성을 유지한다. ### 예시 코드 (Spring Boot) ```java // Domain Layer @Entity public class Order { @Id @GeneratedValue private Long id; private LocalDateTime orderDate; @Embedded private Money totalAmount; // 비즈니스 메서드 public void addItem(Product product, int quantity) { … } } // Application Layer @Service @RequiredArgsConstructor public class OrderService { private final OrderRepository orderRepo; private final PaymentGateway paymentGateway; @Transactional public OrderDto placeOrder(CreateOrderCommand cmd) { Order order = new Order(); // 도메인 로직 수행 orderRepo.save(order); paymentGateway.charge(order.getTotalAmount()); return OrderMapper.toDto(order); } } // Infrastructure Layer @Repository public interface OrderRepository extends JpaRepository<Order, Long> {} // Presentation Layer @RestController @RequestMapping('/api/orders') @RequiredArgsConstructor public class OrderController { private final OrderService orderService; @PostMapping public ResponseEntity<OrderDto> create(@RequestBody @Valid CreateOrderCommand cmd) { OrderDto result = orderService.placeOrder(cmd); return ResponseEntity.status(HttpStatus.CREATED).body(result); } } ``` ### 마무리 프로젝트 초기에 **아키텍처 설계**와 **도메인 모델링**에 충분한 시간을 투자하면, 이후 기능 추가나 기술 교체가 훨씬 수월해진다. 레이어드 아키텍처와 DDD 원칙을 적용하고, 의존성 역전 및 자동화된 테스트를 기반으로 하면, 복잡도가 증가해도 유지보수 가능한 코드를 유지할 수 있다. **핵심 포인트** - 초기 설계에 투자 → 장기 비용 절감 - 레이어와 인터페이스로 책임 분리 - 테스트와 CI/CD 로 변경 위험 최소화 - 필요 시 마이크로서비스 혹은 모듈형 모노리식으로 확장