Amazon Athena는 S3 데이터를 표준 SQL로 쿼리하는 서버리스 대화형 쿼리 서비스입니다. Presto/Trino 엔진 기반으로 페타바이트 규모 데이터를 분석할 수 있습니다.
┌─────────────────────────────────────────────────────────────────────┐
│ Amazon Athena Architecture │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Client │───▶│ Athena │───▶│ Glue Data Catalog │ │
│ │ (Console/ │ │ Engine │ │ (메타데이터) │ │
│ │ SDK/JDBC) │ │ (Presto) │ └─────────────────────────┘ │
│ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐│
│ │ Amazon S3 ││
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││
│ │ │ Parquet │ │ JSON │ │ CSV │ ││
│ │ │ (권장) │ │ │ │ │ ││
│ │ └─────────────┘ └─────────────┘ └─────────────┘ ││
│ └─────────────────────────────────────────────────────────────────┘│
│ │
│ 비용: 스캔한 데이터량 기준 ($5/TB) │
└─────────────────────────────────────────────────────────────────────┘컬럼형 포맷은 필요한 컬럼만 읽어 스캔량 대폭 감소
JSON 100GB → Parquet 10GB (90% 절감)WHERE 절에서 파티션 필터링으로 스캔 범위 제한
-- 파티션 프루닝 SELECT * FROM logs WHERE year='2024' AND month='01' -- 해당 파티션만 스캔
SELECT * 대신 필요한 컬럼만 명시
-- Bad: SELECT * FROM orders (전체 컬럼 스캔) -- Good: SELECT order_id, amount FROM orders
GZIP, Snappy, ZSTD 압축으로 파일 크기 감소 → 스캔 비용 감소
원본 JSON 1TB 쿼리: $5.00
Parquet 변환 후 (100GB): $0.50
+ 파티션 프루닝 (10GB): $0.05
총 절감: 99%
-- CTAS: 쿼리 결과를 새 테이블로 저장
CREATE TABLE curated.orders_summary
WITH (
format = 'PARQUET',
parquet_compression = 'SNAPPY',
partitioned_by = ARRAY['year', 'month']
) AS
SELECT
customer_id,
SUM(amount) as total_amount,
year, month
FROM raw.orders
GROUP BY customer_id, year, month;
-- Federated Query: 다른 데이터 소스 쿼리
SELECT * FROM awsdatacatalog.database.table -- Glue Catalog
UNION ALL
SELECT * FROM mysql_connector.database.table -- RDS MySQL
-- Workgroups: 비용 제어
-- 쿼리당 스캔 제한, 팀별 비용 분리Amazon QuickSight는 클라우드 네이티브 BI 서비스입니다. SPICE 인메모리 엔진으로 빠른 대시보드를 제공하고, ML 기반 인사이트를 자동 생성합니다.
┌─────────────────────────────────────────────────────────────────────┐
│ Amazon QuickSight │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐│
│ │ Data Sources ││
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││
│ │ │ Athena │ │Redshift │ │ RDS │ │ S3 │ │ SaaS │ ││
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ ││
│ └───────┼──────────┼──────────┼──────────┼──────────┼───────────┘│
│ └──────────┴──────────┴──────────┴──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐│
│ │ SPICE Engine ││
│ │ • 인메모리 캐시 (Super-fast, Parallel, In-memory) ││
│ │ • 자동 데이터 새로고침 ││
│ │ • 10GB/사용자 포함 ││
│ └─────────────────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐│
│ │ Dashboards & Analysis ││
│ │ • 대화형 대시보드 ││
│ │ • ML Insights (이상 탐지, 예측) ││
│ │ • 임베딩 (웹앱 통합) ││
│ └─────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────┘자연어로 질문하면 자동으로 시각화 생성
"지난 달 지역별 매출은?"
→ 자동으로 막대 차트 생성
QuickSight 대시보드를 웹 애플리케이션에 임베딩하여 고객에게 분석 기능을 제공할 수 있습니다.
// Lambda - 임베딩 URL 생성
const quicksight = new AWS.QuickSight();
const params = {
AwsAccountId: '123456789012',
DashboardId: 'dashboard-id',
IdentityType: 'ANONYMOUS', // 또는 IAM, QUICKSIGHT
SessionLifetimeInMinutes: 600,
Namespace: 'default'
};
const response = await quicksight.getDashboardEmbedUrl(params).promise();
// response.EmbedUrl을 iframe src로 사용Amazon OpenSearch Service는 실시간 검색, 로그 분석, 모니터링을 위한 관리형 서비스입니다. Elasticsearch 호환 API를 제공하며 OpenSearch Dashboards로 시각화합니다.
┌─────────────────────────────────────────────────────────────────────┐
│ 로그 분석 파이프라인 │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 로그 │───▶│ Kinesis │───▶│ OpenSearch │───▶│ Dashboards │ │
│ │ 소스 │ │Firehose │ │ Cluster │ │ (시각화) │ │
│ └─────────┘ └─────────┘ └─────────────┘ └─────────────┘ │
│ │
│ CloudWatch Logs → Subscription Filter → OpenSearch │
└─────────────────────────────────────────────────────────────────────┘| 특성 | Serverless | Provisioned |
|---|---|---|
| 관리 | 완전 자동 | 클러스터 관리 |
| 스케일링 | 자동 (OCU 기반) | 수동 |
| 비용 | 사용량 기반 | 인스턴스 시간 |
| 적합한 사용 | 간헐적, 예측 불가 | 지속적, 대규모 |
// OpenSearch 쿼리 예시
// 로그 검색
GET /logs-*/_search
{
"query": {
"bool": {
"must": [
{ "match": { "level": "ERROR" } },
{ "range": { "@timestamp": { "gte": "now-1h" } } }
]
}
},
"aggs": {
"errors_by_service": {
"terms": { "field": "service.keyword" }
}
}
}| 사용 사례 | 권장 서비스 |
|---|---|
| Ad-hoc SQL 쿼리 | Athena |
| BI 대시보드 | QuickSight |
| 로그 분석/검색 | OpenSearch |
| 복잡한 분석/ML | Redshift |
서버리스 SQL 쿼리. Parquet + 파티셔닝으로 비용 99% 절감 가능.
SPICE 인메모리 엔진으로 빠른 대시보드. ML Insights, 임베딩 지원.
실시간 로그 분석, 전문 검색. Serverless로 관리 부담 제거.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Athena Workgroups │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Workgroup: analytics-team ││
│ │ ├── Query Result Location: s3://athena-results/analytics/ ││
│ │ ├── Data Scanned Limit: 100 GB per query ││
│ │ ├── Encryption: SSE-KMS ││
│ │ ├── Enforce Workgroup Settings: true ││
│ │ └── CloudWatch Metrics: enabled ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Workgroup: data-science ││
│ │ ├── Query Result Location: s3://athena-results/data-science/ ││
│ │ ├── Data Scanned Limit: 1 TB per query ││
│ │ ├── Requester Pays: enabled ││
│ │ └── Query Execution Timeout: 30 minutes ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Workgroup: etl-jobs ││
│ │ ├── Engine Version: Athena engine version 3 ││
│ │ ├── Concurrent Query Limit: 20 ││
│ │ └── Per-Query Data Usage Control: 500 GB ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘// Terraform - Workgroup 생성
resource "aws_athena_workgroup" "analytics" {
name = "analytics-team"
configuration {
enforce_workgroup_configuration = true
publish_cloudwatch_metrics_enabled = true
result_configuration {
output_location = "s3://${aws_s3_bucket.athena_results.id}/analytics/"
encryption_configuration {
encryption_option = "SSE_KMS"
kms_key_arn = aws_kms_key.athena.arn
}
}
engine_version {
selected_engine_version = "Athena engine version 3"
}
bytes_scanned_cutoff_per_query = 107374182400 # 100 GB
}
tags = {
Team = "analytics"
}
}
# 워크그룹별 IAM 정책
resource "aws_iam_policy" "athena_analytics" {
name = "athena-analytics-workgroup"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"athena:StartQueryExecution",
"athena:GetQueryExecution",
"athena:GetQueryResults"
]
Resource = aws_athena_workgroup.analytics.arn
}
]
})
}-- Iceberg 테이블 생성
CREATE TABLE iceberg_db.orders (
order_id STRING,
customer_id STRING,
amount DECIMAL(10,2),
order_date DATE,
status STRING
)
PARTITIONED BY (order_date)
LOCATION 's3://data-lake/iceberg/orders/'
TBLPROPERTIES (
'table_type' = 'ICEBERG',
'format' = 'parquet',
'write_compression' = 'snappy'
);
-- INSERT (ACID 지원)
INSERT INTO iceberg_db.orders
VALUES ('ORD-001', 'CUST-123', 99.99, DATE '2024-01-15', 'completed');
-- UPDATE (ACID 지원)
UPDATE iceberg_db.orders
SET status = 'shipped'
WHERE order_id = 'ORD-001';
-- DELETE (ACID 지원)
DELETE FROM iceberg_db.orders
WHERE order_date < DATE '2023-01-01';
-- MERGE (Upsert)
MERGE INTO iceberg_db.orders t
USING staging.new_orders s
ON t.order_id = s.order_id
WHEN MATCHED THEN
UPDATE SET status = s.status, amount = s.amount
WHEN NOT MATCHED THEN
INSERT (order_id, customer_id, amount, order_date, status)
VALUES (s.order_id, s.customer_id, s.amount, s.order_date, s.status);
-- Time Travel (스냅샷 쿼리)
SELECT * FROM iceberg_db.orders
FOR TIMESTAMP AS OF TIMESTAMP '2024-01-14 10:00:00';
-- 특정 스냅샷 ID로 쿼리
SELECT * FROM iceberg_db.orders
FOR VERSION AS OF 1234567890;
-- 스냅샷 히스토리 조회
SELECT * FROM "iceberg_db"."orders$snapshots";Athena Federated Query를 사용하면 S3 외에도 다양한 데이터 소스를 쿼리할 수 있습니다.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Athena Federated Query │
│ │
│ ┌─────────────────┐ │
│ │ Athena │ │
│ │ │ │
│ │ SELECT * FROM │ │
│ │ catalog.db.tbl │ │
│ └────────┬────────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Lambda │ │ Lambda │ │ Lambda │ │
│ │ Connector │ │ Connector │ │ Connector │ │
│ │ (DynamoDB) │ │ (RDS/Aurora) │ │ (Redshift) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ DynamoDB │ │ RDS/Aurora │ │ Redshift │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘// Federated Query 예시
-- DynamoDB 테이블 쿼리
SELECT * FROM dynamodb_catalog.default.users
WHERE user_id = 'USER-123';
-- RDS MySQL 쿼리
SELECT * FROM mysql_catalog.mydb.orders
WHERE order_date >= '2024-01-01';
-- S3와 DynamoDB 조인
SELECT
s.order_id,
s.amount,
d.user_name,
d.email
FROM s3_catalog.analytics.orders s
JOIN dynamodb_catalog.default.users d
ON s.user_id = d.user_id
WHERE s.order_date = '2024-01-15';
-- Redshift와 S3 조인
SELECT
r.product_name,
r.category,
SUM(s.quantity) as total_sold
FROM redshift_catalog.sales.products r
JOIN s3_catalog.raw.transactions s
ON r.product_id = s.product_id
GROUP BY r.product_name, r.category;┌─────────────────────────────────────────────────────────────────────────────┐
│ Cost Optimization Strategies │
│ │
│ 1. 파일 포맷 최적화 │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ CSV (1 TB) ────────────────────────────────────────▶ $5.00 ││
│ │ JSON (1 TB) ───────────────────────────────────────▶ $5.00 ││
│ │ Parquet (100 GB, 압축) ────────────────────────────▶ $0.50 (90% 절감)││
│ │ ORC (100 GB, 압축) ────────────────────────────────▶ $0.50 (90% 절감)││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ 2. 파티셔닝 │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ 전체 스캔 (1 TB) ──────────────────────────────────▶ $5.00 ││
│ │ 파티션 필터 (10 GB) ───────────────────────────────▶ $0.05 (99% 절감)││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ 3. 컬럼 선택 │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ SELECT * (100 컬럼) ───────────────────────────────▶ $5.00 ││
│ │ SELECT col1, col2 (2 컬럼) ────────────────────────▶ $0.10 (98% 절감)││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ 4. 결과 재사용 │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ CTAS로 중간 결과 저장 → 반복 쿼리 비용 절감 ││
│ │ Query Result Reuse 활성화 (동일 쿼리 캐시) ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘// CTAS로 최적화된 테이블 생성
-- CSV를 Parquet으로 변환 + 파티셔닝
CREATE TABLE optimized_orders
WITH (
format = 'PARQUET',
parquet_compression = 'SNAPPY',
partitioned_by = ARRAY['year', 'month'],
external_location = 's3://data-lake/optimized/orders/'
) AS
SELECT
order_id,
customer_id,
amount,
order_date,
YEAR(order_date) as year,
MONTH(order_date) as month
FROM raw_orders;
-- 버킷팅 추가 (조인 최적화)
CREATE TABLE bucketed_orders
WITH (
format = 'PARQUET',
partitioned_by = ARRAY['order_date'],
bucketed_by = ARRAY['customer_id'],
bucket_count = 10
) AS
SELECT * FROM raw_orders;┌─────────────────────────────────────────────────────────────────────────────┐
│ SPICE (Super-fast, Parallel, In-memory │
│ Calculation Engine) │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Data Sources ││
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││
│ │ │ S3 │ │ Athena │ │Redshift │ │ RDS │ ││
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ ││
│ │ └────────────┴────────────┴────────────┘ ││
│ │ │ ││
│ │ ▼ ││
│ │ ┌─────────────────────────────────────────────────────────────────┐ ││
│ │ │ SPICE Engine │ ││
│ │ │ │ ││
│ │ │ ┌─────────────────────────────────────────────────────────┐ │ ││
│ │ │ │ In-Memory Columnar Storage │ │ ││
│ │ │ │ • 압축된 컬럼 데이터 │ │ ││
│ │ │ │ • 병렬 쿼리 처리 │ │ ││
│ │ │ │ • 자동 데이터 새로고침 │ │ ││
│ │ │ └─────────────────────────────────────────────────────────┘ │ ││
│ │ │ │ ││
│ │ │ Capacity: 10 GB (Standard) / 500 GB (Enterprise) │ ││
│ │ │ Refresh: 매시간, 매일, 매주 스케줄 │ ││
│ │ └─────────────────────────────────────────────────────────────────┘ ││
│ │ │ ││
│ │ ▼ ││
│ │ ┌─────────────────────────────────────────────────────────────────┐ ││
│ │ │ Dashboard │ ││
│ │ │ • 서브초 응답 시간 │ ││
│ │ │ • 동시 사용자 지원 │ ││
│ │ └─────────────────────────────────────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────────────┐
│ QuickSight Q │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Natural Language Query ││
│ │ ││
│ │ User: "지난 달 지역별 매출은?" ││
│ │ ││
│ │ ┌─────────────────────────────────────────────────────────────────┐ ││
│ │ │ Q Engine │ ││
│ │ │ 1. 자연어 파싱 │ ││
│ │ │ 2. 의도 파악 (매출, 지역, 시간) │ ││
│ │ │ 3. 데이터셋 매핑 │ ││
│ │ │ 4. 쿼리 생성 및 실행 │ ││
│ │ │ 5. 시각화 자동 선택 │ ││
│ │ └─────────────────────────────────────────────────────────────────┘ ││
│ │ ││
│ │ Result: ││
│ │ ┌─────────────────────────────────────────────────────────────────┐ ││
│ │ │ [Bar Chart] │ ││
│ │ │ 서울: $1.2M |████████████████████ │ ││
│ │ │ 부산: $800K |█████████████ │ ││
│ │ │ 대구: $500K |████████ │ ││
│ │ │ 인천: $450K |███████ │ ││
│ │ └─────────────────────────────────────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ Q Topics (데이터셋 준비) │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ • 필드 동의어 설정: "매출" = "revenue", "sales", "금액" ││
│ │ • 계산 필드 정의: "이익률" = (revenue - cost) / revenue ││
│ │ • 날짜 필드 지정: order_date → 시간 분석 활성화 ││
│ │ • 지역 필드 지정: region → 지도 시각화 활성화 ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘// Q Topic 설정 예시
Q Topic: Sales Analytics
Fields:
├── order_date (Date)
│ └── Synonyms: 주문일, 날짜, date
├── revenue (Measure)
│ └── Synonyms: 매출, 수익, sales, 금액
├── region (Dimension)
│ └── Synonyms: 지역, 지방, area
├── product_category (Dimension)
│ └── Synonyms: 카테고리, 제품군, category
└── customer_segment (Dimension)
└── Synonyms: 고객군, 세그먼트, segment
Calculated Fields:
├── profit_margin = (revenue - cost) / revenue
│ └── Synonyms: 이익률, 마진, margin
└── yoy_growth = (current_revenue - previous_revenue) / previous_revenue
└── Synonyms: 전년대비, YoY, 성장률
Sample Questions:
• "이번 분기 매출 추이는?"
• "지역별 이익률 비교"
• "상위 10개 제품 매출"
• "전년 대비 성장률"// 임베디드 대시보드 URL 생성
import boto3
def get_dashboard_embed_url(dashboard_id, user_arn):
"""QuickSight 대시보드 임베디드 URL 생성"""
client = boto3.client('quicksight', region_name='ap-northeast-2')
response = client.generate_embed_url_for_registered_user(
AwsAccountId='123456789012',
SessionLifetimeInMinutes=600,
UserArn=user_arn,
ExperienceConfiguration={
'Dashboard': {
'InitialDashboardId': dashboard_id
}
},
AllowedDomains=['https://myapp.example.com']
)
return response['EmbedUrl']
# 익명 사용자용 (Row-Level Security 적용)
def get_anonymous_embed_url(dashboard_id, session_tags):
"""익명 사용자용 임베디드 URL (RLS 적용)"""
client = boto3.client('quicksight', region_name='ap-northeast-2')
response = client.generate_embed_url_for_anonymous_user(
AwsAccountId='123456789012',
Namespace='default',
SessionLifetimeInMinutes=600,
AuthorizedResourceArns=[
f'arn:aws:quicksight:ap-northeast-2:123456789012:dashboard/{dashboard_id}'
],
ExperienceConfiguration={
'Dashboard': {
'InitialDashboardId': dashboard_id
}
},
SessionTags=[
{'Key': 'region', 'Value': session_tags['region']},
{'Key': 'department', 'Value': session_tags['department']}
],
AllowedDomains=['https://myapp.example.com']
)
return response['EmbedUrl']// React 컴포넌트에서 임베딩
import { useEffect, useState } from 'react';
import { createEmbeddingContext } from 'amazon-quicksight-embedding-sdk';
function EmbeddedDashboard({ dashboardId }) {
const [embedUrl, setEmbedUrl] = useState(null);
useEffect(() => {
// 백엔드에서 임베드 URL 가져오기
fetch(`/api/quicksight/embed-url?dashboardId=${dashboardId}`)
.then(res => res.json())
.then(data => setEmbedUrl(data.embedUrl));
}, [dashboardId]);
useEffect(() => {
if (!embedUrl) return;
const embedDashboard = async () => {
const embeddingContext = await createEmbeddingContext();
const dashboard = await embeddingContext.embedDashboard({
url: embedUrl,
container: '#dashboard-container',
height: '700px',
width: '100%',
locale: 'ko-KR',
footerPaddingEnabled: true,
undoRedoDisabled: false,
resetDisabled: false
});
// 이벤트 리스너
dashboard.on('error', (error) => {
console.error('Dashboard error:', error);
});
dashboard.on('load', () => {
console.log('Dashboard loaded');
});
};
embedDashboard();
}, [embedUrl]);
return <div id="dashboard-container" />;
}| 기능 | Standard | Enterprise |
|---|---|---|
| SPICE 용량 | 10 GB/사용자 | 500 GB/사용자 |
| Row-Level Security | ❌ | ✅ |
| QuickSight Q | ❌ | ✅ (추가 비용) |
| 임베디드 분석 | ❌ | ✅ |
| ML Insights | 기본 | 고급 (이상 탐지, 예측) |
| Active Directory 통합 | ❌ | ✅ |
Athena 비용 최적화
Parquet + 파티셔닝 = 최대 99% 비용 절감
SPICE vs Direct Query
SPICE = 빠른 응답, Direct = 실시간 데이터
Athena Iceberg
UPDATE/DELETE/MERGE + Time Travel 지원
OpenSearch Serverless
OCU 기반 과금, 자동 스케일링
QuickSight Q
자연어 → SQL 변환, Topic 설정 필요
Federated Query
Lambda 커넥터로 다양한 소스 쿼리
| 요구사항 | 권장 서비스 | 이유 |
|---|---|---|
| Ad-hoc S3 쿼리 | Athena | 서버리스, 사용량 기반 과금 |
| 복잡한 조인/집계 | Redshift | MPP, 최적화된 성능 |
| BI 대시보드 | QuickSight | SPICE, 임베디드 분석 |
| 로그 분석 | OpenSearch | 전문 검색, 실시간 분석 |
| 자연어 분석 | QuickSight Q | ML 기반 자연어 처리 |
| ACID 트랜잭션 | Athena + Iceberg | UPDATE/DELETE/MERGE 지원 |
┌─────────────────────────────────────────────────────────────────────────────┐
│ Unified Analytics Architecture │
│ │
│ Data Sources │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ S3 │ │ RDS │ │DynamoDB │ │ Logs │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │
│ └────────────┴────────────┴────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Glue Data Catalog ││
│ │ (Unified Metadata) ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Athena │ │ Redshift │ │ OpenSearch │ │
│ │ │ │ Spectrum │ │ │ │
│ │ • Ad-hoc │ │ • BI/Report │ │ • Log/Search│ │
│ │ • Federated │ │ • Complex │ │ • Real-time │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ QuickSight ││
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││
│ │ │ Dashboard │ │ Q (NLQ) │ │ Embedded │ ││
│ │ └─────────────┘ └─────────────┘ └─────────────┘ ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘// 인덱스 템플릿 생성
PUT _index_template/logs-template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s",
"index.codec": "best_compression",
"index.mapping.total_fields.limit": 2000
},
"mappings": {
"properties": {
"@timestamp": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis"
},
"message": {
"type": "text",
"analyzer": "standard",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"level": {
"type": "keyword"
},
"service": {
"type": "keyword"
},
"host": {
"type": "keyword"
},
"trace_id": {
"type": "keyword"
},
"duration_ms": {
"type": "long"
},
"request": {
"properties": {
"method": { "type": "keyword" },
"path": { "type": "keyword" },
"status_code": { "type": "integer" },
"user_agent": { "type": "text" }
}
},
"geo": {
"properties": {
"location": { "type": "geo_point" },
"country": { "type": "keyword" },
"city": { "type": "keyword" }
}
}
}
}
},
"priority": 100
}// Index Lifecycle Management (ILM)
PUT _plugins/_ism/policies/logs-policy
{
"policy": {
"description": "Log retention policy",
"default_state": "hot",
"states": [
{
"name": "hot",
"actions": [
{
"rollover": {
"min_size": "50gb",
"min_index_age": "1d"
}
}
],
"transitions": [
{
"state_name": "warm",
"conditions": {
"min_index_age": "7d"
}
}
]
},
{
"name": "warm",
"actions": [
{
"replica_count": {
"number_of_replicas": 0
}
},
{
"force_merge": {
"max_num_segments": 1
}
}
],
"transitions": [
{
"state_name": "cold",
"conditions": {
"min_index_age": "30d"
}
}
]
},
{
"name": "cold",
"actions": [
{
"read_only": {}
}
],
"transitions": [
{
"state_name": "delete",
"conditions": {
"min_index_age": "90d"
}
}
]
},
{
"name": "delete",
"actions": [
{
"delete": {}
}
]
}
],
"ism_template": {
"index_patterns": ["logs-*"],
"priority": 100
}
}
}// 복합 쿼리
GET logs-*/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"message": "error"
}
},
{
"range": {
"@timestamp": {
"gte": "now-1h",
"lte": "now"
}
}
}
],
"filter": [
{
"term": {
"service": "payment-service"
}
},
{
"terms": {
"level": ["ERROR", "FATAL"]
}
}
],
"must_not": [
{
"match": {
"message": "health check"
}
}
],
"should": [
{
"match": {
"message": "timeout"
}
},
{
"match": {
"message": "connection refused"
}
}
],
"minimum_should_match": 1
}
},
"sort": [
{ "@timestamp": "desc" }
],
"size": 100,
"highlight": {
"fields": {
"message": {}
}
}
}// 집계 쿼리
GET logs-*/_search
{
"size": 0,
"query": {
"range": {
"@timestamp": {
"gte": "now-24h"
}
}
},
"aggs": {
"errors_over_time": {
"date_histogram": {
"field": "@timestamp",
"fixed_interval": "1h"
},
"aggs": {
"by_service": {
"terms": {
"field": "service",
"size": 10
},
"aggs": {
"error_count": {
"filter": {
"term": {
"level": "ERROR"
}
}
},
"avg_duration": {
"avg": {
"field": "duration_ms"
}
},
"percentiles_duration": {
"percentiles": {
"field": "duration_ms",
"percents": [50, 90, 95, 99]
}
}
}
}
}
},
"top_errors": {
"terms": {
"field": "message.keyword",
"size": 10,
"order": {
"_count": "desc"
}
}
},
"status_codes": {
"terms": {
"field": "request.status_code",
"size": 20
}
}
}
}┌─────────────────────────────────────────────────────────────────────────────┐
│ OpenSearch Performance Tuning │
│ │
│ 샤드 설계 │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ • 샤드 크기: 10-50 GB 권장 ││
│ │ • 샤드 수 = 데이터 크기 / 30 GB ││
│ │ • 노드당 샤드: 20개 이하 권장 ││
│ │ • Primary + Replica = 총 샤드 수 ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ 인덱싱 최적화 │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ • refresh_interval: 30s (기본 1s) ││
│ │ • bulk API 사용 (개별 인덱싱 X) ││
│ │ • 적절한 매핑 (불필요한 필드 제외) ││
│ │ • _source 필드 최적화 ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ 검색 최적화 │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ • filter 컨텍스트 사용 (캐시 활용) ││
│ │ • 필요한 필드만 반환 (_source filtering) ││
│ │ • 페이지네이션: search_after 사용 ││
│ │ • 집계: doc_values 활용 ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────────────┐
│ Visualization Types │
│ │
│ 시계열 데이터 │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Line Chart: 시간에 따른 추세 ││
│ │ • 매출 추이, 사용자 증가, 트래픽 변화 ││
│ │ ││
│ │ Area Chart: 누적 또는 비율 변화 ││
│ │ • 카테고리별 매출 비중 변화 ││
│ │ ││
│ │ Bar Chart (시간축): 기간별 비교 ││
│ │ • 월별/분기별 실적 비교 ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ 비교 데이터 │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Bar Chart: 카테고리 간 비교 ││
│ │ • 지역별 매출, 제품별 판매량 ││
│ │ ││
│ │ Grouped Bar: 다중 측정값 비교 ││
│ │ • 지역별 매출 vs 비용 ││
│ │ ││
│ │ Stacked Bar: 구성 요소 비교 ││
│ │ • 지역별 제품 카테고리 구성 ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ 비율/구성 │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Pie Chart: 전체 대비 비율 (5개 이하) ││
│ │ • 시장 점유율, 비용 구성 ││
│ │ ││
│ │ Donut Chart: 중앙에 KPI 표시 ││
│ │ • 목표 달성률 ││
│ │ ││
│ │ Treemap: 계층적 비율 ││
│ │ • 카테고리 > 서브카테고리 매출 ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ 분포/관계 │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Scatter Plot: 두 변수 간 관계 ││
│ │ • 가격 vs 판매량, 광고비 vs 매출 ││
│ │ ││
│ │ Histogram: 데이터 분포 ││
│ │ • 주문 금액 분포, 응답 시간 분포 ││
│ │ ││
│ │ Box Plot: 분포 및 이상치 ││
│ │ • 지역별 주문 금액 분포 ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────────────┐
│ Dashboard Design Principles │
│ │
│ 레이아웃 구조 │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ ┌─────────────────────────────────────────────────────────────────┐ ││
│ │ │ Header: 제목, 필터, 날짜 범위 │ ││
│ │ └─────────────────────────────────────────────────────────────────┘ ││
│ │ ││
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││
│ │ │ KPI │ │ KPI │ │ KPI │ │ KPI │ ││
│ │ │ 매출 │ │ 주문수 │ │ 고객수 │ │ 전환율 │ ││
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ││
│ │ ││
│ │ ┌────────────────────────────────┐ ┌────────────────────────────────┐ ││
│ │ │ │ │ │ ││
│ │ │ 매출 추이 (Line Chart) │ │ 지역별 매출 (Bar Chart) │ ││
│ │ │ │ │ │ ││
│ │ └────────────────────────────────┘ └────────────────────────────────┘ ││
│ │ ││
│ │ ┌────────────────────────────────┐ ┌────────────────────────────────┐ ││
│ │ │ │ │ │ ││
│ │ │ 카테고리별 비중 (Pie) │ │ 상세 데이터 (Table) │ ││
│ │ │ │ │ │ ││
│ │ └────────────────────────────────┘ └────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ │
│ 설계 원칙: │
│ • 5초 규칙: 핵심 정보를 5초 내에 파악 │
│ • 위에서 아래로: 요약 → 상세 │
│ • 왼쪽에서 오른쪽으로: 중요도 순 │
│ • 일관된 색상 체계 │
│ • 적절한 여백 │
└─────────────────────────────────────────────────────────────────────────────┘// 자주 사용하는 계산 필드
// 1. 전년 대비 성장률
yoy_growth =
(sum(revenue) - sum(revenue, [{order_date}], PRE_FILTER))
/ sum(revenue, [{order_date}], PRE_FILTER) * 100
// 2. 이동 평균 (7일)
moving_avg_7d =
avgOver(sum(revenue), [{order_date}], [-6, 0], PRE_FILTER)
// 3. 누적 합계
cumulative_revenue =
runningSum(sum(revenue), [{order_date}], ASC)
// 4. 순위
sales_rank =
rank([sum(revenue) DESC], [{product_category}], PRE_FILTER)
// 5. 비율
category_percentage =
sum(revenue) / sumOver(sum(revenue), [], PRE_FILTER) * 100
// 6. 조건부 계산
profit_status =
ifelse(
sum(profit) > 0, "Profitable",
sum(profit) < 0, "Loss",
"Break-even"
)
// 7. 날짜 계산
days_since_last_order =
dateDiff(max(order_date), now(), "DD")
// 8. 문자열 처리
customer_segment =
concat(
toString(ntile(4, [sum(revenue) DESC], [])),
"-",
ifelse(count(order_id) > 10, "Frequent", "Occasional")
)
// 9. 전월 대비
mom_change =
sum(revenue) - lag(sum(revenue), [{truncDate("MM", order_date)}], 1, PRE_FILTER)
// 10. 목표 달성률
target_achievement =
sum(revenue) / sum(target) * 100| 데이터 유형 | 목적 | 권장 차트 |
|---|---|---|
| 시계열 | 추세 파악 | Line, Area |
| 카테고리 | 비교 | Bar, Column |
| 비율 | 구성 | Pie, Donut, Treemap |
| 분포 | 패턴 | Histogram, Box Plot |
| 관계 | 상관관계 | Scatter, Bubble |
| 지리 | 위치 기반 | Map, Filled Map |
Athena 최적화
파티셔닝 + 컬럼나 포맷 + 압축
SPICE
QuickSight 인메모리 캐시, 빠른 응답
OpenSearch UltraWarm
S3 기반 저비용 웜 스토리지
Federated Query
Athena에서 RDS, DynamoDB 등 직접 쿼리
| 요구사항 | 권장 서비스 |
|---|---|
| S3 데이터 Ad-hoc 쿼리 | Athena |
| 대시보드 + BI | QuickSight |
| 로그 검색 + 분석 | OpenSearch |
| 복잡한 분석 + ML | Redshift |
| 실시간 대시보드 | OpenSearch + Kinesis |