백엔드 데이터 인프라 16편 — 뷰 VIEW와 MATERIALIZED VIEW

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 16편. 뷰(VIEW)로 복잡한 쿼리를 테이블처럼 다루는 패턴과 MATERIALIZED VIEW의 캐시 동작 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 16편 — 뷰 VIEW와 MATERIALIZED VIEW

이 글은 백엔드 데이터 인프라 시리즈 70편 중 16편이에요. 11~15편 까지 SQL 5대 동사와 JOIN·외래 키를 다뤘으니, 이번 16편은 "이 복잡한 쿼리를 매번 다시 쓰기 싫을 때"뷰(VIEW).

뷰란 — 쿼리에 이름을 박은 것

CREATE VIEW active_users AS
SELECT id, name, email, created_at
FROM users
WHERE deleted_at IS NULL;

이제 active_users"테이블처럼" 쓸 수 있어요. PG(PostgreSQL)는 내부적으로 "뷰 정의 + 우리 쿼리" 를 합쳐 한 SQL로 실행해요. 저장된 쿼리 + 별명이라고 보면 됩니다.

SELECT * FROM active_users WHERE city = 'Seoul';

왜 뷰가 필요한가 — 4가지

(1) 복잡한 쿼리 추상화

CREATE VIEW order_summary AS
SELECT
    u.id        AS user_id,
    u.name      AS user_name,
    COUNT(o.id) AS order_count,
    SUM(o.amount) AS total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.deleted_at IS NULL
GROUP BY u.id, u.name;

이후 매번 — SELECT * FROM order_summary WHERE total_amount > 100000.

(2) 권한 분리

-- 일반 사용자에겐 users 직접 접근 X
REVOKE SELECT ON users FROM analyst_role;

-- 민감 컬럼(이메일·전화) 뺀 뷰만 노출
CREATE VIEW users_public AS
SELECT id, name, city FROM users WHERE deleted_at IS NULL;
GRANT SELECT ON users_public TO analyst_role;

리포팅·분석 부서에는 "필요한 정보만" 노출하는 식입니다.

(3) Soft Delete(논리 삭제 — 행을 지우지 않고 플래그로 표시) 일관성

14편 DELETE 의 Soft Delete 패턴은 매번 WHERE deleted_at IS NULL 박는 게 부담이죠.

CREATE VIEW active_users AS
SELECT * FROM users WHERE deleted_at IS NULL;

이후 모든 코드는 FROM active_users 만 쓰면 됩니다. 실수 0.

(4) 호환성·마이그레이션

테이블 구조를 바꿀 때 옛 인터페이스를 유지하려면 뷰를 박아두면 됩니다.

-- 기존 코드는 user_name 컬럼 의존
-- 테이블에서 user_name → name 변경 후

CREATE VIEW legacy_users AS
SELECT id, name AS user_name, email FROM users;

옛 코드를 건드리지 않아도 그대로 동작.

뷰 갱신 — CREATE OR REPLACE VIEW

CREATE OR REPLACE VIEW active_users AS
SELECT id, name, email, city FROM users WHERE deleted_at IS NULL;

기존 뷰를 덮어쓰는 명령. 다만 컬럼 순서·이름 추가만 됩니다 — 기존 컬럼을 삭제하거나 이름을 바꾸려면 DROP 후 재생성해야 해요.

뷰 삭제

DROP VIEW IF EXISTS active_users;

-- 의존하는 다른 뷰도 함께
DROP VIEW active_users CASCADE;

뷰로 INSERT·UPDATE·DELETE — 가능한 경우

-- 단순 뷰 (JOIN·GROUP BY 없음)는 INSERT·UPDATE 가능
CREATE VIEW active_users AS
SELECT id, name, email FROM users WHERE deleted_at IS NULL;

INSERT INTO active_users (name, email) VALUES ('Alice', '...');
UPDATE active_users SET name = 'New' WHERE id = 1;

PG가 자동으로 뷰 INSERT 를 실제 테이블 INSERT 로 변환해 줍니다.

복잡한 뷰(JOIN·GROUP BY·집계 함수)는 INSERT·UPDATE 자동 불가예요. 트리거나 INSTEAD OF 트리거(뷰에 직접 INSERT/UPDATE 가 왔을 때 가로채는 트리거)를 따로 박아야 합니다.

MATERIALIZED VIEW — 캐시된 뷰

CREATE MATERIALIZED VIEW daily_stats AS
SELECT
    DATE(created_at)  AS day,
    COUNT(*)          AS orders,
    SUM(amount)       AS revenue
FROM orders
GROUP BY DATE(created_at);

일반 뷰 = 매 조회마다 원본 쿼리 다시 실행. MATERIALIZED VIEW = "쿼리 결과를 실제 테이블처럼 저장". 조회는 매우 빠름, 단 갱신은 수동.

갱신

REFRESH MATERIALIZED VIEW daily_stats;

-- 다른 쿼리 막지 않고 갱신 (UNIQUE 인덱스 필요)
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_stats;

CONCURRENTLY = 갱신 중에도 SELECT 가능. 운영 환경 표준입니다.

자동 갱신 — cron + REFRESH

PG 자체에는 cron(시간 기반 작업 스케줄러) 기능이 없어요. 외부 cron, 자바 백엔드 입문 55편 @Scheduled, 또는 PG 확장 pg_cron(PG 안에서 직접 cron 잡 등록) 중 하나로 갱신합니다.

-- pg_cron 확장 사용 시
SELECT cron.schedule('refresh-daily-stats', '0 3 * * *',
    'REFRESH MATERIALIZED VIEW CONCURRENTLY daily_stats');

매일 새벽 3시 자동 갱신.

일반 VIEW vs MATERIALIZED VIEW

VIEW MATERIALIZED VIEW
저장 정의만 데이터까지
조회 속도 원본 쿼리 속도 매우 빠름
갱신 자동 (실시간) 수동 (REFRESH)
데이터 신선도 실시간 갱신 시점 기준
인덱스 불가 가능
사용 추상화·권한·일관성 무거운 통계·대시보드

: - 자주 변하는 데이터·실시간성 = VIEW - 무거운 집계·대시보드·로그 분석 = MATERIALIZED VIEW

인덱스 — MATERIALIZED VIEW 만

CREATE INDEX idx_daily_stats_day ON daily_stats(day);
CREATE UNIQUE INDEX idx_daily_stats_day_unique ON daily_stats(day);

CONCURRENTLY REFRESH는 UNIQUE 인덱스가 있어야 동작합니다.

실전 예 — 대시보드

-- 1. 일별 매출 (MATERIALIZED)
CREATE MATERIALIZED VIEW dashboard_daily_revenue AS
SELECT
    DATE(o.created_at)    AS day,
    p.category            AS category,
    COUNT(*)              AS orders,
    SUM(o.amount)         AS revenue
FROM orders o
JOIN products p ON o.product_id = p.id
WHERE o.status = 'PAID'
GROUP BY DATE(o.created_at), p.category;

CREATE UNIQUE INDEX idx_dashboard_day_cat
    ON dashboard_daily_revenue(day, category);

-- 2. 사용자별 활성 상태 (일반 VIEW)
CREATE VIEW user_activity AS
SELECT
    u.id, u.name,
    u.last_login_at,
    CASE
        WHEN u.last_login_at >= NOW() - INTERVAL '7 days' THEN 'ACTIVE'
        WHEN u.last_login_at >= NOW() - INTERVAL '30 days' THEN 'IDLE'
        ELSE 'DORMANT'
    END AS activity_status
FROM users u
WHERE u.deleted_at IS NULL;

-- 3. 매일 새벽 3시 대시보드 갱신
SELECT cron.schedule('refresh-dashboard', '0 3 * * *',
    'REFRESH MATERIALIZED VIEW CONCURRENTLY dashboard_daily_revenue');

Spring JPA 의 뷰

JPA(Java Persistence API — 자바 ORM 표준)는 뷰를 "읽기 전용 엔티티" 로 매핑합니다.

@Entity
@Immutable
@Table(name = "order_summary")
public class OrderSummary {
    @Id
    private Long userId;
    private String userName;
    private Long orderCount;
    private BigDecimal totalAmount;
}

@Immutable(불변 엔티티 표시 어노테이션) 을 붙이면 JPA에게 "이건 수정 안 함" 이라고 알리는 셈이라 1차 캐시·Dirty Checking(엔티티 변경 자동 감지) 도 생략됩니다.

함정 5가지

(1) 복잡한 뷰 위에 뷰 위에 뷰

뷰 안에서 다른 뷰를 참조하는 건 가능합니다. 다만 3단계 이상 중첩되면 디버깅이 매우 어렵고 성능도 떨어져요.

(2) MATERIALIZED VIEW 갱신 안 됨

REFRESH 를 빼먹으면 데이터가 "옛날 그대로" 남습니다. 자동 갱신을 꼭 걸어두세요.

(3) REFRESH 락

기본 REFRESH"독점 락" 이라 갱신 중에 SELECT 가 막혀요. CONCURRENTLY 를 무조건 붙입니다.

(4) 뷰에 INSERT·UPDATE 시도

복잡한 뷰는 자동으로 안 됩니다. "뷰는 SELECT 전용" 마인드로 인터페이스를 명확히 갈라두세요.

(5) 뷰의 컬럼 변경

CREATE OR REPLACE VIEW 는 컬럼 추가만 가능. 변경·삭제는 DROP 후 재생성이고, 이때 의존 객체가 깨질 수 있습니다.

🎯 뷰 4가지 활용

(1) 복잡한 쿼리 추상화. (2) 권한 분리 (민감 컬럼 가림). (3) Soft Delete 일관성. (4) 무거운 통계 = MATERIALIZED + 매일 REFRESH. 4가지가 한국 회사 백엔드 90% 시나리오.

한 줄 정리 — VIEW = 쿼리에 이름 박은 것. 추상화·권한 분리·일관성 효과. MATERIALIZED VIEW = 캐시된 뷰 (REFRESH 필요). CONCURRENTLY 갱신 + UNIQUE 인덱스 표준. JPA = @Immutable 매핑.

시험 직전 한 번 더 — VIEW 입문자가 매번 헷갈리는 것

  • VIEW = 저장된 쿼리 + 이름
  • CREATE VIEW name AS SELECT ...
  • 실시간 — 매 조회 시 원본 쿼리 실행
  • CREATE OR REPLACE VIEW = 갱신 (컬럼 추가만)
  • DROP VIEW [CASCADE]
  • 단순 뷰 = INSERT·UPDATE 자동 가능
  • 복잡한 뷰 = SELECT 전용
  • INSTEAD OF 트리거 = 복잡 뷰에 쓰기 가능
  • MATERIALIZED VIEW = 캐시된 뷰
  • 조회 매우 빠름
  • REFRESH MATERIALIZED VIEW = 수동 갱신
  • CONCURRENTLY = 갱신 중 SELECT 가능 (UNIQUE 인덱스 필요)
  • 자동 갱신 = pg_cron 또는 외부 스케줄러
  • MV 인덱스 가능 (일반 VIEW 불가)
  • 활용 1 = 복잡한 쿼리 추상화
  • 활용 2 = 권한 분리 (민감 컬럼)
  • 활용 3 = Soft Delete 일관성
  • 활용 4 = 대시보드·통계 (MATERIALIZED)
  • 3단계+ 뷰 중첩 = 안티패턴
  • JPA = @Immutable 읽기 전용 엔티티
  • REFRESH 락 = CONCURRENTLY 박기
  • legacy_table_name 패턴 = 마이그레이션
  • 뷰는 인덱스 못 가짐 (MV는 가능)
  • 한국 회사 = 대시보드 = MATERIALIZED VIEW 무조건

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!