백엔드 데이터 인프라 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"테이블처럼" 사용 가능.

SELECT * FROM active_users WHERE city = 'Seoul';

내부적으로는 — PG가 "뷰 정의 + 우리 쿼리" 합쳐 한 SQL로 실행. 저장된 쿼리 + 별명.

왜 뷰가 필요한가 — 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 트리거로 가능.

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 확장 사용 시
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가 뷰를 "읽기 전용 엔티티" 로 매핑.

@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 는 컬럼 추가만 OK. 변경·삭제 = 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 무조건

시리즈 다른 편

시리즈 다음 글

다음 글(17편)에서는 트랜잭션 — BEGIN·COMMIT·ROLLBACK — ACID의 직접 표현.

공식 문서: PostgreSQL 18 — Tutorial: Views에서 더 자세한 사양을 확인할 수 있어요.

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

답글 남기기

error: Content is protected !!