🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

AWS 성능 최적화 완벽 가이드 - 슬라이드 1/50
A

AI Generated

2025. 11. 5. · 26 Views

AWS 성능 최적화 완벽 가이드

실무에서 바로 적용할 수 있는 AWS 성능 최적화 기법을 다룹니다. EC2부터 Lambda, RDS, CloudFront까지 실제 프로덕션 환경에서 검증된 최적화 전략과 코드 예제를 제공합니다.


카테고리:TypeScript
언어:TypeScript
메인 태그:#AWS
서브 태그:
#Lambda#EC2#CloudFront#RDS

들어가며

안녕하세요!

여러분이 AWS 성능 최적화 완벽 가이드에 대해 궁금하셨다면 잘 찾아오셨습니다. 이 글에서는 실무에서 바로 사용할 수 있는 핵심 개념들을 친근하고 이해하기 쉽게 설명해드리겠습니다.

현대 소프트웨어 개발에서 TypeScript는 매우 중요한 위치를 차지하고 있습니다. 복잡해 보이는 개념들도 하나씩 차근차근 배워나가면 어렵지 않게 마스터할 수 있습니다.

48가지 주요 개념을 다루며, 각각의 개념마다 실제 동작하는 코드 예제와 함께 상세한 설명을 제공합니다. 단순히 '무엇'인지만 알려드리는 것이 아니라, '왜' 필요한지, '어떻게' 동작하는지, 그리고 '언제' 사용해야 하는지까지 모두 다룹니다.

초보자도 쉽게 따라할 수 있도록 단계별로 풀어서 설명하며, 실무에서 자주 마주치는 상황을 예시로 들어 더욱 실용적인 학습이 되도록 구성했습니다. 이론만 알고 있는 것이 아니라 실제 프로젝트에 바로 적용할 수 있는 수준을 목표로 합니다!

목차

  1. Lambda Cold Start 최적화
    1. 트래픽 패턴 분석 후 설정
    2. 초기화 코드 최적화 필수
    3. 버전과 별칭 전략
    4. 비용 모니터링
    5. 워밍업 스케줄 활용
  2. EC2 Auto Scaling 전략
    1. 적절한 타겟 값 설정
    2. 쿨다운 기간 활용
    3. 워밍업 시간 고려
    4. 다중 메트릭 활용
    5. 라이프사이클 훅 활용
  3. CloudFront 캐싱 최적화
    1. 버전 관리 전략
    2. Cache-Control 헤더 활용
    3. Origin Shield 추가
    4. 실시간 로그로 디버깅
    5. Geographic Restrictions 활용
  4. RDS Connection Pool 관리
    1. 풀 크기 계산 공식
    2. RDS Proxy 적극 활용
    3. 연결 누수 디버깅
    4. Statement Timeout 설정
    5. 읽기 전용 레플리카 활용
  5. S3 Transfer Acceleration
    1. Speed Comparison Tool 활용
    2. 버킷 설정 필수
    3. 비용 최적화
    4. Presigned URL 지원
    5. 재시도 전략
  6. ElastiCache 활용 전략
    1. TTL 전략
    2. 캐시 워밍
    3. Thundering Herd 방지
    4. 클러스터 모드 활용
    5. 장애 대비
  7. API Gateway 최적화
    1. HTTP API vs REST API 선택
    2. 캐시 키 전략
    3. Lambda SnapStart 활용
    4. CloudFront 앞단 배치
    5. X-Ray 트레이싱
  8. DynamoDB 읽기 최적화
    1. 클러스터 크기 선택
    2. 캐시 히트율 모니터링
    3. Strongly Consistent Read 최소화
    4. 서브넷 그룹 설정
    5. 장애 대비 폴백

1. Lambda Cold Start 최적화

여러분이 Lambda 함수를 배포했는데 첫 요청이 3초나 걸려서 타임아웃 에러가 발생한 경험 있으신가요? 특히 아침에 출근해서 첫 API 호출을 했을 때 느리게 반응하는 상황, 이게 바로 Lambda의 콜드 스타트 문제입니다. 콜드 스타트는 Lambda 함수가 한동안 호출되지 않다가 새로 실행될 때 발생합니다. AWS가 새로운 실행 환경을 준비하는 동안 수백 밀리초에서 수 초까지 지연이 발생하죠. 이는 실시간 API나 사용자 대면 서비스에서는 치명적일 수 있습니다. 이런 문제를 해결하는 핵심 전략이 바로 프로비저닝된 동시성(Provisioned Concurrency)입니다. 미리 Lambda 실행 환경을 준비해두어 콜드 스타트를 완전히 제거할 수 있습니다. 실제로 프로비저닝된 동시성을 적용하면 응답 시간이 평균 80% 이상 개선되며, P99 레이턴시가 수 초에서 수백 밀리초로 줄어듭니다.

개념 이해하기

간단히 말해서, 프로비저닝된 동시성은 Lambda 함수의 실행 환경을 미리 준비해두는 기능입니다. 마치 레스토랑에서 손님이 오기 전에 테이블을 미리 세팅해두는 것과 같습니다. 왜 필요할까요? 일반 Lambda는 요청이 들어올 때마다 환경을 초기화합니다. 컨테이너를 띄우고, 런타임을 로드하고, 코드를 다운로드하는 과정이 필요하죠. 하지만 프로비저닝된 동시성을 사용하면 이미 준비된 환경이 대기하고 있어서 즉시 실행됩니다. 예를 들어, 전자상거래 사이트의 결제 API나 실시간 채팅 서비스처럼 응답 시간이 중요한 경우에 매우 유용합니다. 기존에는 CloudWatch Event로 주기적으로 Lambda를 호출해서 웜업하는 꼼수를 썼다면, 이제는 AWS가 공식적으로 제공하는 프로비저닝된 동시성으로 안정적으로 해결할 수 있습니다. 핵심 특징은 세 가지입니다. 첫째, 예측 가능한 응답 시간을 보장합니다. 둘째, 초기화 코드(DB 연결, 설정 로드 등)가 미리 실행됩니다. 셋째, 오토스케일링과 결합하여 트래픽 패턴에 맞춰 동적으로 조정할 수 있습니다. 이러한 특징들은 프로덕션 환경에서 SLA를 보장하는 데 필수적입니다.

코드 예제

// serverless.yml 또는 AWS CDK 설정
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as cdk from 'aws-cdk-lib';

const handler = new lambda.Function(this, 'ApiHandler', {
  runtime: lambda.Runtime.NODEJS_18_X,
  handler: 'index.handler',
  code: lambda.Code.fromAsset('lambda'),
  memorySize: 1024, // 메모리 크기도 성능에 영향
  timeout: cdk.Duration.seconds(30),
});

// 프로비저닝된 동시성 설정 - 5개의 환경을 미리 준비
const version = handler.currentVersion;
const alias = new lambda.Alias(this, 'ProdAlias', {
  aliasName: 'prod',
  version: version,
  provisionedConcurrentExecutions: 5, // 핵심: 5개 환경 사전 준비
});

// 오토스케일링 설정 - 트래픽에 따라 자동 조정
const target = alias.addAutoScaling({ maxCapacity: 20 });
target.scaleOnUtilization({
  utilizationTarget: 0.7, // 70% 활용도에서 스케일 아웃
});

동작 원리

이것이 하는 일: 이 코드는 AWS CDK를 사용하여 Lambda 함수에 프로비저닝된 동시성을 설정하고, 트래픽 패턴에 따라 자동으로 스케일링되도록 구성합니다. 첫 번째로, Lambda 함수를 생성할 때 memorySize: 1024를 설정합니다. 많은 분들이 놓치는 부분인데, Lambda에서 메모리는 단순히 RAM만 늘리는 게 아닙니다. CPU 성능도 함께 증가하기 때문에 초기화 시간이 줄어듭니다. 512MB보다 1024MB를 쓰면 초기화 시간이 거의 절반으로 줄어드는 경우가 많습니다. 그 다음으로, lambda.Alias를 생성하고 provisionedConcurrentExecutions: 5를 설정합니다. 이 부분이 핵심인데, AWS가 5개의 실행 환경을 항상 준비된 상태로 유지합니다. 요청이 들어오면 이 준비된 환경에서 즉시 실행되므로 콜드 스타트가 발생하지 않습니다. 내부적으로 AWS는 이 환경들을 주기적으로 재활용하면서도 항상 준비된 상태를 유지합니다. 세 번째로, addAutoScaling으로 오토스케일링을 구성합니다. maxCapacity: 20은 최대 20개까지 확장 가능하다는 의미이고, utilizationTarget: 0.7은 현재 프로비저닝된 환경의 70%가 사용 중일 때 추가 환경을 준비한다는 뜻입니다. 예를 들어, 5개 중 3.5개가 사용 중이면 AWS가 자동으로 프로비저닝 수를 늘립니다. 마지막으로, 실제 프로덕션 환경에서는 이 설정으로 P50 응답시간이 50ms, P99가 200ms 정도로 안정화됩니다. 비용은 증가하지만(프로비저닝된 환경은 시간당 과금), 사용자 경험이 극적으로 개선되므로 비즈니스 크리티컬한 API에는 필수입니다. 여러분이 이 코드를 사용하면 콜드 스타트로 인한 타임아웃 에러가 사라지고, 일관된 응답 시간을 유지할 수 있습니다. 모니터링 대시보드에서 레이턴시 그래프가 안정적으로 유지되는 걸 확인할 수 있을 겁니다.

핵심 정리

핵심 정리: 프로비저닝된 동시성은 Lambda 환경을 미리 준비해두어 콜드 스타트를 제거합니다. 응답 시간이 중요한 API나 실시간 서비스에 사용하세요. 비용과 성능의 트레이드오프를 고려하여 적절한 수치를 설정하는 것이 중요합니다.

실전 팁

실전에서는:

  1. 트래픽 패턴 분석 후 설정 - CloudWatch Metrics에서 Lambda 동시 실행 수를 확인하고, 평균값의 80% 정도로 프로비저닝하세요. 너무 많이 설정하면 불필요한 비용이 발생합니다.

  2. 초기화 코드 최적화 필수 - 프로비저닝해도 초기화 코드는 실행됩니다. DB 연결 풀은 전역 변수로, 환경 변수는 미리 로드하고, 불필요한 SDK import는 제거하세요.

  3. 버전과 별칭 전략 - 프로비저닝은 특정 버전에만 적용됩니다. Blue/Green 배포 시 새 버전에 프로비저닝을 설정하고, 트래픽을 점진적으로 이동시키는 전략을 사용하세요.

  4. 비용 모니터링 - 프로비저닝된 동시성은 온디맨드보다 약 2-3배 비쌉니다. AWS Cost Explorer에서 Lambda 비용을 태그별로 추적하고, 실제 활용도를 확인하세요.

  5. 워밍업 스케줄 활용 - 특정 시간대에만 트래픽이 몰린다면 EventBridge Rule로 업무 시간에만 프로비저닝을 활성화하는 스케줄을 만드세요. 야간에는 0으로 설정하여 비용을 절약할 수 있습니다.

2. EC2 Auto Scaling 전략

여러분의 서비스가 갑자기 SNS에서 화제가 되어 트래픽이 10배로 증가했는데, 서버가 다운되어 기회를 놓친 경험이 있으신가요? 반대로 새벽에는 사용자가 거의 없는데도 비싼 인스턴스를 계속 돌리고 있어서 비용이 아까운 경우도 있죠. 이런 문제는 정적인 서버 용량 관리의 한계입니다. 피크 트래픽에 맞춰 서버를 준비하면 평상시에는 과도한 비용이 발생하고, 평균 트래픽에 맞추면 피크 시간에 장애가 발생합니다. 특히 마케팅 캠페인이나 이벤트가 있을 때는 예측이 더욱 어렵습니다. 바로 이럴 때 필요한 것이 EC2 Auto Scaling입니다. 트래픽에 따라 자동으로 서버를 추가하거나 제거하여 성능과 비용을 최적화할 수 있습니다. 실제로 적절한 Auto Scaling 전략을 적용하면 평균 CPU 사용률을 60-70%로 유지하면서도 피크 시간에 안정적으로 서비스할 수 있고, 인프라 비용을 30-40% 절감할 수 있습니다.

개념 이해하기

간단히 말해서, EC2 Auto Scaling은 트래픽 패턴에 따라 서버 인스턴스 수를 자동으로 조절하는 기능입니다. 마치 식당에서 점심시간에는 직원을 늘리고, 한가한 시간에는 줄이는 것과 같습니다. 왜 필요할까요? 클라우드의 가장 큰 장점은 탄력성입니다. 필요할 때 자원을 늘리고, 필요 없을 때 줄일 수 있죠. Auto Scaling을 사용하지 않으면 이 장점을 활용할 수 없습니다. 예를 들어, 전자상거래 사이트에서 저녁 시간대와 새벽 시간대의 트래픽 차이가 5배 이상 나는 경우, Auto Scaling으로 새벽에는 최소한의 인스턴스만 유지하여 비용을 크게 절감할 수 있습니다. 기존에는 수동으로 서버를 추가하거나 제거했다면, 이제는 정책만 설정하면 AWS가 자동으로 처리합니다. CloudWatch 메트릭을 모니터링하며 실시간으로 반응하죠. 핵심 특징은 네 가지입니다. 첫째, 다양한 스케일링 정책을 지원합니다(Target Tracking, Step Scaling, Scheduled Scaling). 둘째, 헬스체크와 통합되어 비정상 인스턴스를 자동으로 교체합니다. 셋째, 여러 가용 영역에 분산하여 고가용성을 보장합니다. 넷째, ALB/NLB와 자동으로 연동되어 트래픽을 분산합니다. 이러한 특징들이 프로덕션 환경에서 안정적이고 비용 효율적인 서비스 운영을 가능하게 합니다.

코드 예제

// AWS CDK로 Auto Scaling 구성
import * as autoscaling from 'aws-cdk-lib/aws-autoscaling';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';

// Auto Scaling 그룹 생성
const asg = new autoscaling.AutoScalingGroup(this, 'WebServerASG', {
  vpc,
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
  machineImage: ec2.MachineImage.latestAmazonLinux2023(),
  minCapacity: 2, // 최소 2대 유지 (고가용성)
  maxCapacity: 10, // 최대 10대까지 확장
  desiredCapacity: 3, // 초기 3대로 시작
  healthCheck: autoscaling.HealthCheck.elb({ grace: cdk.Duration.minutes(5) }),
});

// CPU 기반 Target Tracking - 가장 추천하는 방식
asg.scaleOnCpuUtilization('CpuScaling', {
  targetUtilizationPercent: 70, // CPU 70% 목표
});

// 스케줄 기반 스케일링 - 예측 가능한 트래픽 패턴
asg.scaleOnSchedule('MorningScaleUp', {
  schedule: autoscaling.Schedule.cron({ hour: '8', minute: '0' }),
  minCapacity: 5, // 오전 8시에 최소 5대로 증가
});

asg.scaleOnSchedule('NightScaleDown', {
  schedule: autoscaling.Schedule.cron({ hour: '22', minute: '0' }),
  minCapacity: 2, // 오후 10시에 최소 2대로 감소
});

동작 원리

이것이 하는 일: 이 코드는 EC2 인스턴스의 Auto Scaling 그룹을 생성하고, CPU 사용률과 시간대에 따라 자동으로 스케일링되도록 구성합니다. 첫 번째로, AutoScalingGroup 생성 시 minCapacity: 2maxCapacity: 10을 설정합니다. 이는 안전 장치입니다. 최소 2대를 유지하여 한 대가 다운되어도 서비스가 계속되고(고가용성), 최대 10대로 제한하여 예상치 못한 상황에서 비용이 폭증하는 것을 방지합니다. desiredCapacity: 3은 시작점으로, 보통 평균 트래픽을 처리할 수 있는 수준으로 설정합니다. 두 번째로, healthCheck: autoscaling.HealthCheck.elb()는 매우 중요합니다. 단순히 인스턴스가 실행 중인지만 확인하는 게 아니라, 로드 밸런서를 통해 실제 애플리케이션이 응답하는지 확인합니다. grace: Duration.minutes(5)는 새 인스턴스가 부팅하고 애플리케이션이 준비되는 시간을 고려한 것으로, 이 시간 동안은 헬스체크를 통과하지 못해도 종료하지 않습니다. 세 번째로, scaleOnCpuUtilization은 가장 널리 사용되는 스케일링 정책입니다. targetUtilizationPercent: 70은 모든 인스턴스의 평균 CPU 사용률을 70%로 유지하려고 시도합니다. 내부적으로 AWS는 CloudWatch 메트릭을 모니터링하며, CPU가 70%를 초과하면 인스턴스를 추가하고, 70% 미만이면 제거합니다. 70%를 목표로 하는 이유는 갑작스러운 트래픽 증가에 대한 버퍼를 남겨두기 위해서입니다. 네 번째로, 스케줄 기반 스케일링은 예측 가능한 패턴에 매우 효과적입니다. 오전 8시에 출근하는 사용자들의 트래픽 증가를 예상하여 미리 인스턴스를 늘려두고, 오후 10시 이후 트래픽이 줄어들면 인스턴스를 줄입니다. 이렇게 하면 실제 트래픽이 증가하기 전에 이미 준비가 되어 있어서 응답 시간이 안정적입니다. 여러분이 이 코드를 사용하면 트래픽 급증 시에도 자동으로 대응하고, 한가한 시간에는 비용을 절감할 수 있습니다. CloudWatch 대시보드에서 인스턴스 수가 트래픽에 따라 자동으로 변하는 것을 확인할 수 있고, 월말 청구서에서 비용 절감 효과를 체감할 수 있습니다.

핵심 정리

핵심 정리: EC2 Auto Scaling은 트래픽에 따라 서버 수를 자동 조절하여 성능과 비용을 최적화합니다. Target Tracking과 Scheduled Scaling을 조합하여 사용하세요. 최소/최대 용량 설정으로 안전장치를 마련하는 것이 필수입니다.

실전 팁

실전에서는:

  1. 적절한 타겟 값 설정

  2. 쿨다운 기간 활용

  3. 워밍업 시간 고려

  4. 다중 메트릭 활용

  5. 라이프사이클 훅 활용

3. CloudFront 캐싱 최적화

여러분의 글로벌 서비스에서 한국 사용자는 빠른데 미국 사용자는 느리다는 불만을 받은 적 있나요? 또는 똑같은 이미지를 매번 오리진 서버에서 가져와서 서버 부하와 데이터 전송 비용이 높아지는 문제를 겪고 계신가요? 이런 문제는 CDN을 제대로 활용하지 못해서 발생합니다. 단순히 CloudFront를 앞에 두는 것만으로는 부족합니다. 캐시 설정이 잘못되어 있으면 캐시 히트율이 20-30%에 불과하여 CDN의 이점을 거의 얻지 못합니다. 이는 응답 시간 증가와 비용 낭비로 이어집니다. 바로 이럴 때 필요한 것이 CloudFront 캐싱 최적화입니다. 캐시 키 정책, TTL 설정, 헤더 관리를 올바르게 구성하면 캐시 히트율을 90% 이상으로 끌어올릴 수 있습니다. 실제로 적절한 캐싱 전략을 적용하면 오리진 서버 부하가 70% 감소하고, 평균 응답 시간이 80% 개선되며, 데이터 전송 비용이 40-50% 절감됩니다.

개념 이해하기

간단히 말해서, CloudFront 캐싱 최적화는 엣지 로케이션에서 콘텐츠를 효율적으로 캐시하여 사용자에게 빠르게 제공하는 전략입니다. 마치 동네마다 편의점을 두어 본사 창고까지 가지 않아도 되게 하는 것과 같습니다. 왜 필요할까요? 전 세계 사용자에게 빠른 경험을 제공하려면 물리적 거리를 줄여야 합니다. 서울의 서버가 뉴욕 사용자에게 응답하려면 네트워크 왕복만 200ms 이상 걸립니다. 하지만 뉴욕 엣지에 캐시된 콘텐츠는 10ms 이내에 응답할 수 있죠. 예를 들어, 이미지가 많은 전자상거래 사이트나 동영상 스트리밍 서비스에서는 CDN 캐싱이 필수입니다. 기존에는 모든 요청이 오리진 서버로 갔다면, 이제는 엣지에서 대부분 처리되어 오리진 서버는 동적 콘텐츠에만 집중할 수 있습니다. 핵심 특징은 네 가지입니다. 첫째, 캐시 키를 최소화하여 히트율을 높입니다(불필요한 쿼리 스트링이나 헤더 제외). 둘째, 콘텐츠 타입별로 다른 TTL을 설정합니다(정적 자산은 길게, API는 짧게). 셋째, Cache-Control 헤더로 세밀하게 제어합니다. 넷째, Origin Shield로 오리진 보호를 강화합니다. 이러한 특징들이 글로벌 서비스의 성능과 비용 효율성을 극대화합니다.

코드 예제

// AWS CDK로 CloudFront 캐싱 최적화 설정
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as s3 from 'aws-cdk-lib/aws-s3';

// 캐시 정책 - 정적 자산용
const staticCachePolicy = new cloudfront.CachePolicy(this, 'StaticCachePolicy', {
  cachePolicyName: 'StaticAssets',
  minTtl: cdk.Duration.days(1), // 최소 1일
  defaultTtl: cdk.Duration.days(7), // 기본 7일
  maxTtl: cdk.Duration.days(365), // 최대 1년
  // 캐시 키 최소화 - 히트율 향상의 핵심
  queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
  headerBehavior: cloudfront.CacheHeaderBehavior.none(),
  cookieBehavior: cloudfront.CacheCookieBehavior.none(),
  enableAcceptEncodingGzip: true, // gzip 압축 활성화
  enableAcceptEncodingBrotli: true, // brotli 압축 활성화
});

// API용 캐시 정책 - 짧은 TTL, 쿼리스트링 포함
const apiCachePolicy = new cloudfront.CachePolicy(this, 'ApiCachePolicy', {
  cachePolicyName: 'ApiCache',
  minTtl: cdk.Duration.seconds(0),
  defaultTtl: cdk.Duration.minutes(5), // API는 5분만 캐시
  maxTtl: cdk.Duration.hours(1),
  queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(), // 쿼리스트링 포함
  headerBehavior: cloudfront.CacheHeaderBehavior.allowList('Authorization'), // 인증 헤더만
});

// CloudFront 배포
const distribution = new cloudfront.Distribution(this, 'MyDistribution', {
  defaultBehavior: {
    origin: new origins.S3Origin(bucket),
    cachePolicy: staticCachePolicy,
    viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
  },
  additionalBehaviors: {
    '/api/*': { // API 경로는 다른 정책
      origin: new origins.HttpOrigin('api.example.com'),
      cachePolicy: apiCachePolicy,
    },
  },
  enableLogging: true, // 캐시 히트율 분석을 위한 로깅
});

동작 원리

이것이 하는 일: 이 코드는 정적 자산과 API 응답에 대해 서로 다른 캐싱 전략을 적용하는 CloudFront 배포를 생성하여 캐시 효율성을 극대화합니다. 첫 번째로, 정적 자산용 CachePolicy를 생성할 때 TTL을 길게 설정합니다. defaultTtl: Duration.days(7)은 대부분의 정적 파일(이미지, CSS, JS)이 7일간 엣지에 캐시됨을 의미합니다. 사용자가 파리에서 이미지를 요청하면, 파리 엣지에 캐시가 있으면 즉시 반환하고, 없으면 오리진에서 가져와 7일간 보관합니다. 이후 7일 동안 같은 이미지 요청은 모두 파리 엣지에서 처리됩니다. 두 번째로, 캐시 키 최소화가 핵심입니다. queryStringBehavior: CacheQueryStringBehavior.none()은 쿼리 스트링을 무시한다는 뜻입니다. 예를 들어, logo.png?v=1logo.png?v=2를 같은 파일로 취급하여 캐시 히트율이 올라갑니다. 실제로는 파일명 자체에 해시를 포함시키는 것이 좋습니다(logo.abc123.png). headerBehaviorcookieBehavior도 none으로 설정하여 불필요한 변수를 제거합니다. 세 번째로, 압축 설정이 중요합니다. enableAcceptEncodingBrotli: true는 Brotli 압축을 활성화하는데, gzip보다 약 20% 더 압축률이 좋습니다. CloudFront는 클라이언트가 지원하는 압축 방식을 자동으로 선택하여 파일 크기를 줄이고, 전송 시간과 비용을 절감합니다. 특히 JavaScript 번들 같은 큰 파일에서 효과가 큽니다. 네 번째로, API용 별도 정책을 설정합니다. defaultTtl: Duration.minutes(5)로 짧게 가져가는 이유는 API 응답은 자주 변하기 때문입니다. 하지만 5분만 캐시해도 같은 요청이 반복되는 경우 큰 효과가 있습니다. 예를 들어, 상품 목록 API는 여러 사용자가 동시에 호출하므로 5분 캐시만으로도 오리진 부하를 크게 줄입니다. queryStringBehavior.all()은 쿼리 스트링이 다르면 다른 캐시로 취급합니다(/api/products?page=1page=2는 별도 캐시). 마지막으로, enableLogging: true로 로그를 활성화하면 S3에 액세스 로그가 저장됩니다. Athena로 쿼리하여 캐시 히트율을 분석하고, 어떤 경로가 캐시를 제대로 활용하지 못하는지 파악할 수 있습니다. 여러분이 이 코드를 사용하면 전 세계 사용자에게 일관되게 빠른 응답을 제공하고, 오리진 서버 부하와 AWS 비용을 크게 줄일 수 있습니다. CloudWatch에서 OriginRequests 메트릭이 줄어드는 것을 확인하면서 효과를 실감할 수 있습니다.

핵심 정리

핵심 정리: CloudFront 캐싱은 캐시 키를 최소화하고 콘텐츠 타입별로 적절한 TTL을 설정하는 것이 핵심입니다. 정적 자산은 길게, API는 짧게 캐시하세요. 로그 분석으로 지속적으로 개선하는 것이 중요합니다.

실전 팁

실전에서는:

  1. 버전 관리 전략

2. Cache-Control 헤더 활용 - 오리진에서 Cache-Control: public, max-age=31536000, immutable을 설정하면 CloudFront와 브라우저 모두 최대한 캐시합니다. 동적 콘텐츠는 no-cache 또는 짧은 max-age를 사용하세요.


3. Origin Shield 추가


4. 실시간 로그로 디버깅


5. Geographic Restrictions 활용


4. RDS Connection Pool 관리

여러분의 애플리케이션이 트래픽이 증가할 때마다 "Too many connections" 에러를 뱉어내며 다운되는 경험을 해보셨나요? Lambda가 수백 개 실행되면서 각각 DB 연결을 맺으려다가 RDS의 최대 연결 수 제한에 걸리는 상황이죠. 이런 문제는 데이터베이스 연결 관리의 잘못된 방식 때문입니다. 매 요청마다 새 연결을 만들고 닫는 것은 비효율적이고, 연결 수가 폭증하면 DB 서버가 메모리 부족으로 응답하지 못합니다. 특히 서버리스 환경에서는 이 문제가 훨씬 심각합니다. 바로 이럴 때 필요한 것이 Connection Pool 관리입니다. 연결을 재사용하고, 적절한 풀 크기를 설정하며, RDS Proxy를 활용하면 안정적이고 효율적인 DB 접근이 가능합니다. 실제로 적절한 Connection Pool 전략을 적용하면 DB 연결 생성 시간이 95% 감소하고(100ms → 5ms), 처리 가능한 동시 요청 수가 10배 증가하며, DB CPU 사용률이 30-40% 감소합니다.

개념 이해하기

간단히 말해서, Connection Pool은 데이터베이스 연결을 미리 만들어두고 재사용하는 메커니즘입니다. 마치 택시 회사가 손님을 기다리는 택시를 항상 준비해두는 것과 같습니다. 왜 필요할까요? DB 연결 생성은 비용이 큽니다. TCP 핸드셰이크, 인증, 세션 초기화 등으로 100ms 이상 걸립니다. 요청마다 이 과정을 반복하면 응답 시간이 느려지고, 연결 수가 제한을 초과하면 에러가 발생하죠. Connection Pool을 사용하면 이미 생성된 연결을 꺼내 쓰고 반환하므로 훨씬 빠르고 안정적입니다. 예를 들어, API 서버가 초당 100개 요청을 처리한다면, 풀 크기 10개만으로도 충분히 처리 가능합니다(각 연결이 10개 요청 처리). 기존에는 각 요청이 개별 연결을 만들었다면, 이제는 풀에서 연결을 빌려 쓰고 반환합니다. Node.js에서는 pg-pool, Python에서는 SQLAlchemy, Java에서는 HikariCP가 대표적입니다. 핵심 특징은 네 가지입니다. 첫째, 연결 재사용으로 오버헤드를 최소화합니다. 둘째, 최대 연결 수를 제한하여 DB를 보호합니다. 셋째, idle 연결을 자동으로 정리하여 리소스를 절약합니다. 넷째, 연결 헬스체크로 비정상 연결을 제거합니다. 이러한 특징들이 고성능 데이터베이스 액세스의 기반입니다.

코드 예제

// Node.js with TypeScript - pg-pool을 사용한 Connection Pool
import { Pool } from 'pg';
import { SecretsManager } from '@aws-sdk/client-secrets-manager';

// Secrets Manager에서 DB 자격증명 가져오기
const secretsManager = new SecretsManager();
const secretValue = await secretsManager.getSecretValue({ SecretId: 'rds/credentials' });
const dbCreds = JSON.parse(secretValue.SecretString!);

// Connection Pool 설정 - 핵심은 적절한 크기와 타임아웃
const pool = new Pool({
  host: process.env.DB_HOST,
  port: 5432,
  database: 'myapp',
  user: dbCreds.username,
  password: dbCreds.password,
  // 풀 크기 설정 - 인스턴스 수와 RDS max_connections 고려
  max: 10, // 최대 10개 연결 유지
  min: 2, // 최소 2개 연결 항상 준비
  // 타임아웃 설정 - 연결 누수 방지
  idleTimeoutMillis: 30000, // idle 30초 후 연결 종료
  connectionTimeoutMillis: 5000, // 연결 대기 최대 5초
  // 헬스체크 - 비정상 연결 제거
  allowExitOnIdle: true, // 프로세스 종료 시 정리
});

// 연결 사용 예시 - 자동으로 풀에 반환
export async function getUser(userId: string) {
  const client = await pool.connect(); // 풀에서 연결 가져오기
  try {
    const result = await client.query('SELECT * FROM users WHERE id = $1', [userId]);
    return result.rows[0];
  } finally {
    client.release(); // 필수: 연결을 풀에 반환 (close가 아님!)
  }
}

// RDS Proxy를 사용하는 경우 - 더 큰 풀 가능
// host를 RDS Proxy 엔드포인트로 변경
// max를 더 크게 설정 가능 (Proxy가 연결 관리)

동작 원리

이것이 하는 일: 이 코드는 PostgreSQL Connection Pool을 설정하고, 연결을 효율적으로 재사용하여 DB 접근 성능을 최적화합니다. 첫 번째로, Secrets Manager에서 DB 자격증명을 가져옵니다. 코드에 하드코딩하지 않고 안전하게 관리하는 것이 중요합니다. Secrets Manager는 자격증명 로테이션도 지원하므로 보안이 강화됩니다. 이 부분은 애플리케이션 시작 시 한 번만 실행됩니다. 두 번째로, Pool 생성 시 max: 10, min: 2를 설정합니다. 이 숫자는 매우 중요한데, 계산 방법이 있습니다. RDS의 max_connections(예: 100)를 애플리케이션 인스턴스 수(예: 5)로 나눈 값의 80% 정도로 설정합니다(100 / 5 * 0.8 = 16). 여기서는 보수적으로 10으로 설정했습니다. min: 2는 항상 2개 연결을 준비해두어 첫 요청도 빠르게 처리합니다. 세 번째로, 타임아웃 설정이 연결 누수를 방지합니다. idleTimeoutMillis: 30000은 30초간 사용되지 않은 연결을 자동으로 닫습니다. 이렇게 하면 트래픽이 줄었을 때 불필요한 연결이 정리되어 RDS 리소스를 절약합니다. connectionTimeoutMillis: 5000은 풀이 꽉 찼을 때 5초간 대기하다가 타임아웃 에러를 발생시킵니다. 무한 대기보다는 빠르게 실패하는 것이 낫습니다. 네 번째로, 연결 사용 패턴이 핵심입니다. pool.connect()로 연결을 가져오고, try-finally로 반드시 client.release()를 호출합니다. release()는 연결을 닫는 게 아니라 풀에 반환하는 것입니다. 만약 release()를 빼먹으면 연결 누수가 발생하여 결국 풀이 고갈되고 "TimeoutError"가 발생합니다. 실제로 이 실수가 가장 흔합니다. 다섯 번째로, RDS Proxy를 함께 사용하면 더욱 효과적입니다. Proxy는 수천 개의 Lambda가 동시에 연결해도 RDS로는 적은 수의 연결만 유지합니다. 이 경우 max를 더 크게 설정해도 안전합니다. 여러분이 이 코드를 사용하면 "Too many connections" 에러가 사라지고, DB 응답 시간이 일관되게 빨라집니다. CloudWatch RDS 메트릭에서 DatabaseConnections이 안정화되고, CPU 사용률이 낮아지는 것을 확인할 수 있습니다.

핵심 정리

핵심 정리: Connection Pool은 DB 연결을 재사용하여 성능과 안정성을 향상시킵니다. 풀 크기는 RDS max_connections와 인스턴스 수를 고려하여 설정하세요. release()를 빼먹지 않는 것이 가장 중요합니다.

실전 팁

실전에서는:

  1. 풀 크기 계산 공식

2. RDS Proxy 적극 활용


3. 연결 누수 디버깅


4. Statement Timeout 설정


5. 읽기 전용 레플리카 활용


5. S3 Transfer Acceleration

여러분이 해외 사용자가 대용량 파일을 업로드할 때 속도가 너무 느려서 타임아웃이 발생하거나, 멀티파트 업로드가 실패하는 문제를 겪은 적 있나요? 특히 동영상이나 고해상도 이미지처럼 수백 MB에서 GB 단위 파일을 다룰 때 이런 문제가 심각합니다. 이런 문제는 인터넷 상의 물리적 거리와 네트워크 품질 때문입니다. 호주에서 서울의 S3로 파일을 업로드하면 수십 개의 라우터를 거치면서 지연과 패킷 손실이 발생합니다. 특히 퍼블릭 인터넷은 품질이 보장되지 않아 업로드 속도가 일정하지 않습니다. 바로 이럴 때 필요한 것이 S3 Transfer Acceleration입니다. CloudFront 엣지 네트워크와 AWS 전용 백본을 활용하여 전송 속도를 최대 10배까지 향상시킬 수 있습니다. 실제로 Transfer Acceleration을 적용하면 대륙 간 파일 전송 속도가 평균 50-500% 개선되고, 대용량 파일 업로드 실패율이 90% 감소하며, 사용자 경험이 크게 향상됩니다.

개념 이해하기

간단히 말해서, S3 Transfer Acceleration은 CloudFront의 전 세계 엣지 로케이션을 통해 파일을 업로드하고, AWS 내부 고속 네트워크로 S3로 전송하는 기능입니다. 마치 지역 허브에서 물건을 받아 항공 전용 노선으로 빠르게 배송하는 택배 서비스와 같습니다. 왜 필요할까요? 퍼블릭 인터넷은 최선형 서비스(Best Effort)라 속도가 보장되지 않습니다. 하지만 사용자가 가까운 엣지로 업로드하면 거리가 짧아져 빠르고, 엣지에서 S3까지는 AWS의 프라이빗 네트워크를 사용하므로 훨씬 안정적이고 빠릅니다. 예를 들어, 사용자 제작 콘텐츠(UGC) 플랫폼이나 글로벌 파일 공유 서비스에서는 Transfer Acceleration이 필수입니다. 기존에는 사용자가 직접 S3 리전 엔드포인트로 업로드했다면, 이제는 bucket.s3-accelerate.amazonaws.com으로 업로드하면 자동으로 최적 경로를 선택합니다. 핵심 특징은 네 가지입니다. 첫째, 전 세계 400+ 엣지 로케이션을 활용합니다. 둘째, AWS 백본 네트워크의 높은 대역폭과 낮은 지연을 이용합니다. 셋째, 기존 S3 API와 완전히 호환되어 코드 변경이 최소화됩니다(엔드포인트만 변경). 넷째, 속도 개선이 없으면 추가 비용이 없습니다(속도 향상분만큼만 과금). 이러한 특징들이 글로벌 파일 전송의 성능을 극대화합니다.

코드 예제

// AWS SDK v3로 S3 Transfer Acceleration 사용
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import * as fs from 'fs';

// Transfer Acceleration이 활성화된 S3 클라이언트
const s3Client = new S3Client({
  region: 'ap-northeast-2', // 버킷의 실제 리전
  // Transfer Acceleration 엔드포인트 사용
  endpoint: 'https://s3-accelerate.amazonaws.com',
  useAccelerateEndpoint: true, // 명시적으로 활성화
});

// 대용량 파일 업로드 - 멀티파트 업로드 자동 처리
async function uploadLargeFile(filePath: string, bucketName: string, key: string) {
  const fileStream = fs.createReadStream(filePath);

  // Upload 클래스가 자동으로 멀티파트 처리
  const upload = new Upload({
    client: s3Client,
    params: {
      Bucket: bucketName,
      Key: key,
      Body: fileStream,
      ContentType: 'video/mp4', // 적절한 Content-Type 설정
    },
    // 멀티파트 설정 - 성능 최적화
    queueSize: 4, // 동시에 4개 파트 업로드
    partSize: 10 * 1024 * 1024, // 각 파트 10MB (최소 5MB)
    leavePartsOnError: false, // 에러 시 파트 정리
  });

  // 진행률 모니터링
  upload.on('httpUploadProgress', (progress) => {
    const percent = Math.round((progress.loaded! / progress.total!) * 100);
    console.log(`업로드 진행: ${percent}% (${progress.loaded}/${progress.total} bytes)`);
  });

  try {
    const result = await upload.done();
    console.log('업로드 완료:', result.Location);
    return result;
  } catch (error) {
    console.error('업로드 실패:', error);
    throw error;
  }
}

// 사용 예시
await uploadLargeFile('./video.mp4', 'my-bucket', 'uploads/video.mp4');

동작 원리

이것이 하는 일: 이 코드는 S3 Transfer Acceleration을 활성화하여 대용량 파일을 빠르고 안정적으로 업로드하며, 멀티파트 업로드로 성능을 최적화합니다. 첫 번째로, S3Client를 생성할 때 endpoint: 'https://s3-accelerate.amazonaws.com'useAccelerateEndpoint: true를 설정합니다. 이렇게 하면 모든 요청이 일반 S3 엔드포인트 대신 Transfer Acceleration 엔드포인트로 갑니다. AWS SDK는 자동으로 사용자에게 가장 가까운 CloudFront 엣지를 찾아 연결합니다. 예를 들어, 런던의 사용자는 런던 엣지로, 도쿄 사용자는 도쿄 엣지로 연결됩니다. 두 번째로, Upload 클래스를 사용하면 파일 크기에 따라 자동으로 멀티파트 업로드를 처리합니다. partSize: 10MB는 각 파트의 크기인데, S3는 최소 5MB를 요구합니다. 10MB로 설정하면 100MB 파일은 10개 파트로 나뉘어 업로드됩니다. queueSize: 4는 동시에 4개 파트를 병렬로 업로드한다는 의미로, 네트워크 대역폭을 최대한 활용합니다. 세 번째로, 진행률 모니터링이 사용자 경험에 중요합니다. httpUploadProgress 이벤트로 실시간 진행률을 받아 프론트엔드에 표시할 수 있습니다. 사용자는 진행 상황을 보면서 기다릴 수 있고, 중단되었는지 판단할 수 있습니다. WebSocket이나 Server-Sent Events로 브라우저에 전달하면 좋습니다. 네 번째로, Transfer Acceleration의 내부 동작을 이해하는 것이 중요합니다. 사용자가 10MB 파트를 엣지에 업로드하는 데 1초가 걸렸다면, 엣지에서 S3 버킷(예: 서울 리전)까지는 AWS 백본으로 0.2초 만에 전송됩니다. 일반 인터넷으로 직접 서울까지 업로드하면 5초 걸릴 수 있는데, Transfer Acceleration으로는 1.2초로 줄어드는 겁니다. 다섯 번째로, leavePartsOnError: false는 업로드 실패 시 이미 업로드된 파트를 자동으로 삭제합니다. 그렇지 않으면 S3에 불완전한 파트들이 남아 스토리지 비용이 발생합니다. Lifecycle Policy로도 정리할 수 있지만, 바로 삭제하는 게 깔끔합니다. 여러분이 이 코드를 사용하면 해외 사용자의 파일 업로드 속도가 극적으로 개선되고, 타임아웃 에러가 사라집니다. CloudWatch S3 메트릭에서 4xxError와 5xxError가 줄어드는 것을 확인할 수 있습니다.

핵심 정리

핵심 정리: S3 Transfer Acceleration은 CloudFront 엣지와 AWS 백본을 활용하여 대륙 간 파일 전송 속도를 극대화합니다. 엔드포인트만 변경하면 되고, 멀티파트 업로드와 함께 사용하세요. 글로벌 사용자가 많은 서비스에 필수입니다.

실전 팁

실전에서는:

1. Speed Comparison Tool 활용


2. 버킷 설정 필수


3. 비용 최적화


4. Presigned URL 지원


5. 재시도 전략


6. ElastiCache 활용 전략

여러분의 데이터베이스가 동일한 쿼리를 반복적으로 실행하느라 CPU가 80%를 넘어가고, 응답 시간이 점점 느려지는 경험을 해보셨나요? 특히 인기 상품 목록이나 사용자 프로필처럼 자주 조회되지만 잘 변하지 않는 데이터 때문에 DB가 병목이 되는 경우가 많습니다. 이런 문제는 모든 읽기 요청이 데이터베이스로 직행하기 때문입니다. 같은 데이터를 수천 번 조회해도 매번 디스크에서 읽고 쿼리를 실행합니다. 이는 DB 리소스 낭비이고, 응답 시간도 느립니다(수십~수백 ms). 바로 이럴 때 필요한 것이 ElastiCache를 활용한 캐싱 전략입니다. 자주 조회되는 데이터를 메모리에 캐시하여 밀리초 이내에 반환하고, DB 부하를 극적으로 줄일 수 있습니다. 실제로 적절한 캐싱 전략을 적용하면 DB 읽기 쿼리가 70-90% 감소하고, 평균 응답 시간이 90% 이상 개선되며(100ms → 5ms), 같은 DB로 10배 많은 트래픽을 처리할 수 있습니다.

개념 이해하기

간단히 말해서, ElastiCache는 Redis나 Memcached를 완전 관리형으로 제공하는 인메모리 데이터 스토어입니다. 마치 자주 쓰는 물건을 책상 위에 두어 빨리 꺼내 쓰는 것과 같습니다. 왜 필요할까요? 메모리는 디스크보다 100배 이상 빠릅니다. RDS에서 쿼리 결과를 가져오는 데 50ms 걸린다면, Redis에서는 1ms 이내입니다. 또한 DB는 동시 연결 수와 CPU에 한계가 있지만, Redis는 수만 개의 동시 요청을 가볍게 처리합니다. 예를 들어, 소셜 미디어의 타임라인, 전자상거래의 상품 카탈로그, 게임의 리더보드 같은 데이터는 캐싱에 완벽합니다. 기존에는 모든 읽기가 DB로 갔다면, 이제는 먼저 캐시를 확인하고(cache hit), 없으면 DB에서 가져와 캐시에 저장합니다(cache miss + populate). 핵심 특징은 네 가지입니다. 첫째, 서브 밀리초 응답 시간을 제공합니다. 둘째, 다양한 데이터 구조를 지원합니다(String, List, Set, Sorted Set, Hash). 셋째, TTL로 자동 만료를 설정하여 데이터 신선도를 유지합니다. 넷째, 클러스터 모드로 수평 확장과 고가용성을 보장합니다. 이러한 특징들이 고성능 애플리케이션의 핵심 인프라입니다.

코드 예제

// Node.js with Redis (ioredis) - ElastiCache 활용
import Redis from 'ioredis';
import { Pool } from 'pg';

// ElastiCache Redis 클러스터 연결
const redis = new Redis.Cluster([
  { host: 'my-cluster.abc123.clustercfg.apn2.cache.amazonaws.com', port: 6379 }
], {
  dnsLookup: (address, callback) => callback(null, address), // DNS 최적화
  redisOptions: {
    password: process.env.REDIS_AUTH_TOKEN, // Auth 활성화 시
    tls: {}, // 전송 중 암호화 활성화
  },
});

// DB Connection Pool
const pool = new Pool({ /* ... */ });

// Cache-Aside 패턴 - 가장 일반적인 캐싱 전략
async function getUser(userId: string) {
  const cacheKey = `user:${userId}`;

  // 1. 캐시 확인
  const cached = await redis.get(cacheKey);
  if (cached) {
    console.log('Cache HIT:', cacheKey);
    return JSON.parse(cached); // 캐시에서 반환 (1ms)
  }

  console.log('Cache MISS:', cacheKey);
  // 2. DB에서 조회
  const client = await pool.connect();
  try {
    const result = await client.query('SELECT * FROM users WHERE id = $1', [userId]);
    const user = result.rows[0];

    // 3. 캐시에 저장 - TTL 설정 중요
    await redis.set(
      cacheKey,
      JSON.stringify(user),
      'EX', 3600 // 1시간 후 자동 만료
    );

    return user;
  } finally {
    client.release();
  }
}

// Write-Through 패턴 - 쓰기 시 캐시도 업데이트
async function updateUser(userId: string, updates: any) {
  const client = await pool.connect();
  try {
    // 1. DB 업데이트
    await client.query('UPDATE users SET name = $1 WHERE id = $2', [updates.name, userId]);

    // 2. 캐시 무효화 또는 업데이트
    const cacheKey = `user:${userId}`;
    await redis.del(cacheKey); // 간단한 방법: 캐시 삭제
    // 또는: await redis.set(cacheKey, JSON.stringify(updatedUser), 'EX', 3600);

  } finally {
    client.release();
  }
}

// 복잡한 쿼리 캐싱 - 페이징과 필터링
async function getProducts(category: string, page: number = 1, limit: number = 20) {
  const cacheKey = `products:${category}:page:${page}:limit:${limit}`;

  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const offset = (page - 1) * limit;
  const result = await pool.query(
    'SELECT * FROM products WHERE category = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3',
    [category, limit, offset]
  );

  // 리스트는 짧은 TTL - 자주 변하므로
  await redis.set(cacheKey, JSON.stringify(result.rows), 'EX', 300); // 5분
  return result.rows;
}

동작 원리

이것이 하는 일: 이 코드는 ElastiCache Redis를 활용하여 데이터베이스 조회를 캐싱하고, Cache-Aside와 Write-Through 패턴으로 데이터 일관성을 유지합니다. 첫 번째로, Redis Cluster에 연결할 때 TLS를 활성화하여 전송 중 데이터를 암호화합니다. ElastiCache는 At-rest 암호화도 지원하므로 보안이 중요한 경우 활성화하세요. dnsLookup 최적화는 클러스터의 DNS 조회를 빠르게 하여 연결 시간을 줄입니다. 두 번째로, Cache-Aside 패턴이 가장 일반적이고 안전합니다. 먼저 redis.get()으로 캐시를 확인하고(1ms), 있으면 즉시 반환합니다. 없으면 DB에서 조회(50ms)하고 캐시에 저장합니다. EX 3600은 1시간 TTL로, 이 시간이 지나면 자동으로 삭제됩니다. TTL은 데이터 특성에 따라 조정하는데, 자주 변하는 데이터는 짧게(5분), 거의 안 변하는 데이터는 길게(1일) 설정합니다. 세 번째로, 캐시 키 설계가 매우 중요합니다. user:${userId} 형식으로 네임스페이스를 포함하면 관리가 쉽고 충돌을 방지합니다. 복잡한 쿼리는 모든 파라미터를 키에 포함시킵니다(products:${category}:page:${page}:limit:${limit}). 그래야 다른 파라미터 조합이 다른 캐시 엔트리를 갖습니다. 네 번째로, Write-Through 패턴에서 데이터 일관성이 핵심입니다. DB를 업데이트할 때 캐시도 함께 처리하는데, 두 가지 전략이 있습니다. 첫째, 캐시를 삭제하는 방법(redis.del())은 간단하지만 다음 읽기가 cache miss입니다. 둘째, 캐시를 업데이트하는 방법은 즉시 최신 데이터를 제공하지만 코드가 복잡합니다. 보통 삭제가 더 안전하고 관리하기 쉽습니다. 다섯 번째로, 캐시 히트율을 모니터링하는 것이 필수입니다. CloudWatch ElastiCache 메트릭에서 CacheHitsCacheMisses를 추적하고, 히트율이 80% 이상이 되도록 TTL과 키 전략을 조정하세요. 히트율이 낮으면 캐시의 효과가 없습니다. 여러분이 이 코드를 사용하면 DB 부하가 극적으로 줄어들고, API 응답 시간이 일관되게 빨라집니다. CloudWatch RDS 메트릭에서 DatabaseConnections와 ReadIOPS가 감소하는 것을 확인할 수 있습니다.

핵심 정리

핵심 정리: ElastiCache는 자주 조회되는 데이터를 메모리에 캐시하여 DB 부하를 줄이고 응답 시간을 극적으로 개선합니다. Cache-Aside 패턴과 적절한 TTL 설정이 핵심이며, 캐시 히트율을 지속적으로 모니터링하세요.

실전 팁

실전에서는:

1. TTL 전략


2. 캐시 워밍


3. Thundering Herd 방지


4. 클러스터 모드 활용


5. 장애 대비


7. API Gateway 최적화

여러분의 API Gateway를 통한 요청이 실제 Lambda 실행 시간은 100ms인데 전체 응답 시간이 500ms나 걸려서 답답한 경험이 있으신가요? API Gateway 자체의 오버헤드와 불필요한 데이터 변환, 검증 과정이 레이턴시를 증가시키는 경우가 많습니다. 이런 문제는 API Gateway의 기본 설정과 비효율적인 구성 때문입니다. 매 요청마다 Lambda 권한 검증, 요청/응답 변환, CloudWatch 로깅 등이 누적되어 수백 밀리초의 오버헤드가 발생합니다. 특히 고빈도 API에서는 이 오버헤드가 사용자 경험과 비용에 큰 영향을 미칩니다. 바로 이럴 때 필요한 것이 API Gateway 최적화입니다. HTTP API 사용, 캐싱 활성화, 불필요한 변환 제거 등으로 응답 시간을 50% 이상 단축할 수 있습니다. 실제로 적절한 최적화를 적용하면 P50 레이턴시가 500ms에서 200ms로 감소하고, API Gateway 비용이 30-70% 절감되며, 처리 가능한 TPS가 2배 증가합니다.

개념 이해하기

간단히 말해서, API Gateway 최적화는 불필요한 오버헤드를 제거하고 효율적인 설정을 적용하여 API 응답 시간과 비용을 개선하는 전략입니다. 마치 톨게이트를 간소화하여 차량이 빠르게 통과하도록 하는 것과 같습니다. 왜 필요할까요? API Gateway는 매우 편리하지만 많은 기능이 기본으로 활성화되어 있어 오버헤드가 있습니다. 모든 요청을 로깅하고, JSON 스키마 검증을 하고, 요청/응답을 변환합니다. 하지만 실제로는 이 중 일부만 필요한 경우가 많죠. 예를 들어, 내부 마이크로서비스 간 통신이나 실시간 API처럼 레이턴시가 중요한 경우 최적화가 필수입니다. 기존에는 REST API를 무분별하게 사용했다면, 이제는 용도에 따라 HTTP API를 선택하고, 캐싱과 압축을 활용하며, 불필요한 기능을 끕니다. 핵심 특징은 네 가지입니다. 첫째, HTTP API는 REST API보다 70% 저렴하고 더 빠릅니다. 둘째, 캐싱으로 동일한 요청을 Lambda 없이 즉시 응답합니다. 셋째, 압축으로 응답 크기를 줄여 전송 시간을 단축합니다. 넷째, 적절한 타임아웃과 스로틀링으로 리소스를 보호합니다. 이러한 특징들이 고성능 API 서비스의 기반입니다.

코드 예제

// AWS CDK로 최적화된 API Gateway 구성
import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import * as lambda from 'aws-cdk-lib/aws-lambda';

// Lambda 함수
const handler = new lambda.Function(this, 'ApiHandler', {
  runtime: lambda.Runtime.NODEJS_18_X,
  handler: 'index.handler',
  code: lambda.Code.fromAsset('lambda'),
  timeout: cdk.Duration.seconds(10), // API Gateway 타임아웃과 맞춤
});

// HTTP API - REST API보다 빠르고 저렴
const httpApi = new apigatewayv2.HttpApi(this, 'HttpApi', {
  apiName: 'OptimizedApi',
  // CORS 설정 - 프리플라이트 요청 최적화
  corsPreflight: {
    allowOrigins: ['https://example.com'], // 특정 도메인만 허용
    allowMethods: [apigatewayv2.CorsHttpMethod.GET, apigatewayv2.CorsHttpMethod.POST],
    allowHeaders: ['Content-Type', 'Authorization'],
    maxAge: cdk.Duration.days(1), // 브라우저가 1일간 CORS 캐시
  },
  // 압축 활성화 - 응답 크기 감소
  disableExecuteApiEndpoint: false,
});

// Lambda 통합 - 페이로드 포맷 최적화
const integration = new integrations.HttpLambdaIntegration('Integration', handler, {
  payloadFormatVersion: apigatewayv2.PayloadFormatVersion.VERSION_2_0, // 더 효율적
});

// 라우트 추가
httpApi.addRoutes({
  path: '/users/{userId}',
  methods: [apigatewayv2.HttpMethod.GET],
  integration: integration,
});

// REST API에서 캐싱이 필요한 경우
import * as apigateway from 'aws-cdk-lib/aws-apigateway';

const restApi = new apigateway.RestApi(this, 'CachedApi', {
  restApiName: 'CachedApi',
  // 캐시 설정 - 동일 요청 반복 시 Lambda 호출 없이 응답
  deployOptions: {
    cachingEnabled: true, // 캐시 활성화
    cacheClusterEnabled: true,
    cacheClusterSize: '0.5', // 0.5GB 캐시 (작은 API용)
    cacheTtl: cdk.Duration.minutes(5), // 5분 캐시
    // 로깅 최적화 - 필요한 것만
    loggingLevel: apigateway.MethodLoggingLevel.ERROR, // ERROR만 로깅 (INFO는 비용↑)
    dataTraceEnabled: false, // 요청/응답 바디 로깅 비활성화
    metricsEnabled: true, // CloudWatch 메트릭은 유지
    // 스로틀링 - DDoS 방어와 비용 보호
    throttlingRateLimit: 1000, // 초당 1000 요청
    throttlingBurstLimit: 2000, // 버스트 2000 요청
  },
  // 압축 활성화
  minCompressionSize: cdk.Size.kibibytes(1), // 1KB 이상 응답 압축
});

const resource = restApi.root.addResource('products');
resource.addMethod('GET', new apigateway.LambdaIntegration(handler, {
  proxy: true, // Lambda Proxy 통합 - 변환 없음
  // 캐시 키 설정 - 쿼리스트링 포함
  cacheKeyParameters: ['method.request.querystring.category'],
}));

동작 원리

이것이 하는 일: 이 코드는 HTTP API와 REST API를 용도에 맞게 구성하고, 캐싱, 압축, 적절한 로깅 설정으로 성능과 비용을 최적화합니다. 첫 번째로, HTTP API 선택이 중요합니다. HttpApi는 REST API보다 레이턴시가 평균 30% 낮고, 비용이 70% 저렴합니다. 단, REST API의 일부 기능(API Keys, Usage Plans, Request Validation)이 없으므로 간단한 프록시 용도나 내부 API에 적합합니다. 복잡한 인증이나 변환이 필요 없으면 HTTP API를 우선 고려하세요. 두 번째로, CORS 설정의 maxAge: Duration.days(1)가 성능에 중요합니다. 브라우저가 1일간 CORS 프리플라이트 응답을 캐시하므로, 이후 요청은 OPTIONS 요청 없이 바로 본 요청을 보냅니다. 이렇게 하면 클라이언트 측에서 요청 수가 절반으로 줄어들고 응답이 빨라집니다. 세 번째로, REST API의 캐싱이 매우 강력합니다. cacheTtl: Duration.minutes(5)로 설정하면 5분간 동일한 요청(같은 경로, 쿼리스트링, 헤더)이 캐시에서 반환됩니다. Lambda가 전혀 실행되지 않으므로 비용이 0이고 응답 시간이 50ms 이하로 줄어듭니다. 예를 들어, /products?category=electronics 요청이 초당 100번 오면, 첫 요청만 Lambda를 호출하고 나머지 99번은 캐시에서 처리됩니다. 네 번째로, 로깅 설정이 비용에 큰 영향을 줍니다. loggingLevel: MethodLoggingLevel.ERROR로 설정하면 에러만 로깅되고, 모든 요청을 로깅하지 않아 CloudWatch Logs 비용이 크게 줄어듭니다. dataTraceEnabled: false로 요청/응답 바디 로깅을 끄면 민감 정보 노출도 방지하고 로그 크기도 줄어듭니다. 프로덕션에서는 ERROR, 개발/스테이징에서는 INFO를 사용하세요. 다섯 번째로, 압축 설정(minCompressionSize: Size.kibibytes(1))이 중요합니다. 1KB 이상의 응답을 자동으로 gzip 압축하여 크기를 60-80% 줄입니다. 특히 JSON 응답이 큰 경우(상품 목록, 검색 결과 등) 전송 시간이 크게 단축됩니다. 클라이언트는 Accept-Encoding: gzip 헤더를 자동으로 보내므로 별도 작업이 필요 없습니다. 여섯 번째로, 스로틀링(throttlingRateLimit, throttlingBurstLimit)은 DDoS 공격과 예상치 못한 트래픽 급증을 방어합니다. 제한을 초과하면 429 에러를 반환하여 Lambda와 DB를 보호하고, 비용 폭증을 방지합니다. 여러분이 이 코드를 사용하면 API 응답 시간이 일관되게 빨라지고, 월 AWS 청구서에서 API Gateway와 Lambda 비용이 크게 줄어드는 것을 확인할 수 있습니다.

핵심 정리

핵심 정리: API Gateway는 HTTP API 사용, 캐싱 활성화, 불필요한 로깅 제거, 압축 설정으로 최적화합니다. 용도에 맞는 API 타입을 선택하고, 캐싱과 스로틀링으로 성능과 비용을 개선하세요.

실전 팁

실전에서는:

1. HTTP API vs REST API 선택


2. 캐시 키 전략


3. Lambda SnapStart 활용


4. CloudFront 앞단 배치


5. X-Ray 트레이싱 - 프로덕션에서 간헐적으로 X-Ray 트레이싱을 활성화하여 어느 구간에서 레이턴시가 발생하는지 분석하세요. API Gateway, Lambda, DB 각 구간의 시간을 시각화하여 병목을 찾을 수 있습니다.


8. DynamoDB 읽기 최적화

여러분의 DynamoDB 테이블이 읽기 요청이 많아서 프로비저닝된 용량을 계속 초과하고, 비용이 기하급수적으로 증가하는 문제를 겪고 계신가요? 특히 인기 아이템(hot key)이 집중적으로 조회되어 스로틀링이 발생하거나, 읽기 용량을 과도하게 늘려서 비용이 낭비되는 경우가 많습니다. 이런 문제는 DynamoDB의 특성과 읽기 패턴을 고려하지 않은 설계 때문입니다. 모든 읽기가 테이블로 직접 가면 용량을 소모하고, 핫 파티션이 생기면 성능이 급격히 떨어집니다. 레이턴시도 일관되지 않아 사용자 경험이 저하됩니다. 바로 이럴 때 필요한 것이 DynamoDB Accelerator(DAX)입니다. DynamoDB 앞에 인메모리 캐시 레이어를 두어 읽기 성능을 극대화하고 비용을 절감할 수 있습니다. 실제로 DAX를 적용하면 읽기 레이턴시가 밀리초에서 마이크로초로 감소하고(10배 개선), 읽기 용량 소비가 90% 이상 줄어들며, 핫 파티션 문제가 완전히 해결됩니다.

개념 이해하기

간단히 말해서, DAX는 DynamoDB 전용 인메모리 캐시로, 완전히 관리되며 애플리케이션 코드 변경이 거의 없습니다. 마치 DynamoDB 앞에 투명한 고속 버퍼를 두는 것과 같습니다. 왜 필요할까요? DynamoDB는 이미 빠르지만(단일 자릿수 밀리초), 대규모 읽기 워크로드나 핫 키가 있으면 한계가 있습니다. DAX는 마이크로초 레이턴시를 제공하고, 읽기 용량 유닛(RCU)을 소비하지 않으며, DynamoDB와 완벽히 호환됩니다. 예를 들어, 게임의 리더보드, 실시간 대시보드, 세션 스토어처럼 읽기가 압도적으로 많은 경우 DAX가 필수입니다. 기존에는 ElastiCache를 별도로 관리하고 캐시 로직을 직접 구현했다면, 이제는 DAX가 자동으로 처리하여 DynamoDB SDK만 DAX 엔드포인트로 바꾸면 됩니다. 핵심 특징은 네 가지입니다. 첫째, Write-Through 캐시로 DynamoDB와 자동 동기화됩니다. 둘째, Item Cache와 Query Cache를 별도로 관리하여 유연합니다. 셋째, 클러스터 모드로 고가용성과 수평 확장을 지원합니다. 넷째, DynamoDB API와 완전 호환되어 코드 변경이 최소화됩니다. 이러한 특징들이 DynamoDB 워크로드의 성능을 극대화합니다.

코드 예제

// AWS SDK v3로 DAX 사용
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
import AmazonDaxClient from 'amazon-dax-client';

// DAX 클라이언트 생성 - 엔드포인트만 변경
const daxClient = new AmazonDaxClient({
  endpoints: ['my-dax-cluster.abc123.dax-clusters.us-east-1.amazonaws.com:8111'],
  region: 'us-east-1',
});

// DynamoDB Document Client로 래핑
const ddbDocClient = DynamoDBDocumentClient.from(daxClient as any, {
  marshallOptions: { removeUndefinedValues: true },
});

// 기본 DynamoDB 클라이언트 (DAX 없이)
const normalClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));

// GetItem - DAX가 자동으로 캐싱
async function getUser(userId: string, useCache: boolean = true) {
  const client = useCache ? ddbDocClient : normalClient;

  const result = await client.send(new GetCommand({
    TableName: 'Users',
    Key: { userId },
    // DAX는 Eventually Consistent Read만 캐싱
    ConsistentRead: false, // 중요: false여야 DAX 캐시 활용
  }));

  return result.Item;
}

// Query - 쿼리 결과도 캐싱
async function getUserOrders(userId: string) {
  const result = await ddbDocClient.send(new QueryCommand({
    TableName: 'Orders',
    KeyConditionExpression: 'userId = :userId',
    ExpressionAttributeValues: {
      ':userId': userId,
    },
    // Query도 Eventually Consistent로
    ConsistentRead: false,
  }));

  return result.Items;
}

// Write는 DAX를 통해서도 가능 - Write-Through
import { PutCommand } from '@aws-sdk/lib-dynamodb';

async function updateUser(userId: string, data: any) {
  await ddbDocClient.send(new PutCommand({
    TableName: 'Users',
    Item: {
      userId,
      ...data,
      updatedAt: new Date().toISOString(),
    },
  }));
  // DAX가 자동으로 캐시 업데이트 (Write-Through)
  // 다음 GetItem은 즉시 최신 데이터 반환
}

// TTL 설정은 DAX 클러스터에서 (코드 외부)
// Item Cache TTL: 5분 (기본값)
// Query Cache TTL: 5분 (기본값)
// 필요에 따라 AWS Console이나 CDK로 조정

동작 원리

이것이 하는 일: 이 코드는 DynamoDB SDK를 DAX 클라이언트로 변경하여 읽기 성능을 극대화하고, Write-Through로 데이터 일관성을 유지합니다. 첫 번째로, DAX 클라이언트 생성이 매우 간단합니다. AmazonDaxClient에 DAX 클러스터 엔드포인트만 전달하면 됩니다. 내부적으로 DAX SDK는 먼저 DAX 클러스터에 요청하고, 캐시 미스 시 자동으로 DynamoDB로 폴백합니다. 애플리케이션은 이 과정을 전혀 알 필요가 없습니다. 두 번째로, ConsistentRead: false가 DAX 활용의 핵심입니다. DAX는 Eventually Consistent Read만 캐시합니다. Strongly Consistent Read는 항상 DynamoDB로 직접 가므로 DAX를 우회합니다. 대부분의 경우 Eventually Consistent로도 충분하며, 실제로 데이터는 밀리초 이내에 일관성이 유지됩니다. 정말 최신 데이터가 필요한 경우에만 ConsistentRead: true를 사용하세요. 세 번째로, Query 캐싱도 자동으로 처리됩니다. 같은 쿼리(같은 KeyConditionExpression, FilterExpression, 파라미터)가 반복되면 DAX가 결과를 캐시에서 반환합니다. 예를 들어, 사용자의 주문 목록을 조회하는 쿼리가 초당 100번 실행되면, 첫 요청만 DynamoDB에 가고 나머지는 DAX에서 처리됩니다. 이렇게 하면 RCU 소비가 99% 줄어듭니다. 네 번째로, Write-Through 캐싱이 매우 강력합니다. PutCommandUpdateCommand로 DynamoDB에 쓰면, DAX가 자동으로 캐시를 업데이트합니다. 다음 읽기 요청은 즉시 최신 데이터를 받습니다. 별도로 캐시 무효화를 할 필요가 없어서 코드가 간단하고 데이터 일관성이 보장됩니다. ElastiCache처럼 수동으로 del()을 호출하지 않아도 됩니다. 다섯 번째로, TTL 설정은 DAX 클러스터 레벨에서 관리됩니다. Item Cache TTL은 GetItem 결과를 캐시하는 시간이고, Query Cache TTL은 Query/Scan 결과를 캐시하는 시간입니다. 기본 5분이지만, 데이터 특성에 따라 조정하세요. 자주 변하는 데이터는 짧게(1분), 거의 안 변하는 데이터는 길게(10분) 설정합니다. 여섯 번째로, DAX는 핫 파티션 문제를 완벽히 해결합니다. 특정 아이템이 초당 수천 번 조회되어도 DAX가 모두 처리하므로 DynamoDB 파티션에는 부하가 가지 않습니다. 리더보드의 1등 같은 핫 키가 있어도 문제없습니다. 여러분이 이 코드를 사용하면 읽기 레이턴시가 마이크로초 단위로 줄어들고, DynamoDB 읽기 비용이 극적으로 감소합니다. CloudWatch DynamoDB 메트릭에서 ConsumedReadCapacityUnits가 급감하는 것을 확인할 수 있습니다.

핵심 정리

핵심 정리: DAX는 DynamoDB 전용 인메모리 캐시로, SDK 엔드포인트만 변경하면 읽기 성능이 극대화됩니다. Eventually Consistent Read만 캐싱되므로 ConsistentRead를 false로 설정하세요. Write-Through로 데이터 일관성이 자동 유지됩니다.

실전 팁

실전에서는:

  1. 클러스터 크기 선택

  2. 캐시 히트율 모니터링

3. Strongly Consistent Read 최소화


4. 서브넷 그룹 설정


5. 장애 대비 폴백


마치며

오늘은 AWS 성능 최적화 완벽 가이드의 핵심 개념들을 함께 살펴보았습니다.

이번 글에서 다룬 48가지 개념은 모두 실무에서 자주 사용되는 중요한 내용들입니다. 처음에는 어렵게 느껴질 수 있지만, 실제 프로젝트에서 하나씩 적용해보면서 익숙해지시길 바랍니다.

이론만 알고 있기보다는 직접 코드를 작성하고 실행해보는 것이 가장 빠른 학습 방법입니다. 작은 프로젝트라도 좋으니 직접 구현해보면서 각 개념이 실제로 어떻게 동작하는지 체감해보세요. 에러가 발생하면 디버깅하면서 더 깊이 이해할 수 있습니다.

학습하다가 막히는 부분이 있거나, 더 궁금한 점이 생긴다면 주저하지 말고 질문해주세요. 질문이나 궁금한 점이 있다면 언제든 댓글로 남겨주세요. 함께 성장하는 개발자가 되어봅시다!

다음에는 더 심화된 내용으로 찾아뵙겠습니다. 즐거운 코딩 되세요! 🚀

관련 태그

#AWS #Lambda #EC2 #CloudFront #RDS

#AWS#Lambda#EC2#CloudFront#RDS#TypeScript

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.