목요일

AWS Lambda Cold Start 완벽 해결 가이드 — 지연 원인과 최적화 전략

🔍 검색 키워드: AWS Lambda cold start 해결, Lambda 콜드 스타트 최적화, Lambda 응답 지연 원인, Provisioned Concurrency 설정, Lambda 함수 초기화 시간 단축

AWS Lambda Cold Start 완벽 해결 가이드 — 지연 원인과 최적화 전략


증상: 첫 요청만 유독 느리다

Lambda 기반 API에서 이런 현상이 보인다.

일반 응답: 50~100ms
첫 번째 요청 / 트래픽 없다가 재요청: 1500~3000ms

CloudWatch 로그를 보면:

REPORT RequestId: abc-123  Duration: 1823.45 ms  Billed Duration: 1824 ms
       Memory Size: 512 MB  Max Memory Used: 128 MB
       Init Duration: 1654.23 ms

Init Duration이 실제 함수 실행 시간보다 훨씬 길다. 이것이 콜드 스타트(Cold Start)다.


원인 분석: Lambda 실행 생명주기

Lambda는 요청이 없을 때 컨테이너를 종료한다. 새 요청이 들어오면 다음 단계를 거친다.

  1. 컨테이너 생성 — Lambda 실행 환경(마이크로VM) 할당
  2. 런타임 초기화 — Node.js / Python / JVM 등 런타임 부팅
  3. 코드 초기화 — 핸들러 외부 코드 실행 (import, DB 연결, 설정 로드)
  4. 핸들러 실행 — 실제 요청 처리

1~3 단계가 "콜드 스타트"다. 이후 일정 시간 안에 다시 요청이 오면 4단계만 실행하는 "웜 스타트(Warm Start)"가 된다.

런타임별 콜드 스타트 시간

런타임 평균 콜드 스타트
Python 3.x~200ms
Node.js 18.x~250ms
Go~50ms
Java 17 (JVM)~1000~3000ms
Java 17 (GraalVM Native)~150ms
.NET 6~500ms

JVM 기반이 압도적으로 느리다. Spring Boot on Lambda는 이 문제의 대표적인 사례다.


해결방법

해결 1 — 핸들러 외부에서 초기화 (재사용 최대화)

# ❌ 잘못된 패턴: 매 요청마다 DB 연결
def handler(event, context):
    db = pymysql.connect(host=os.environ['DB_HOST'], ...)  # 매 요청마다 연결
    result = db.execute("SELECT ...")
    db.close()
    return result

# ✅ 올바른 패키-��팭: 요청마 항됐 ₔ DB 연결
import pymysql
import os

# 콜드 스타트 웄 해 벰헐 실행l 이후 재사용볠
db = pymysql.connect(
    host=os.environ['DB_HOST'],
    user=os.environ['DB_USER'],
    password=os.environ['DB_PASSWORD'],
    database=os.environ['DB_NAME'],
    cursorclass=pymysql.cursors.DictCursor
)

def handler(event, context):
    with db.cursor() as cursor:
        cursor.execute("SELECT ...")
        return cursor.fetchall()
// Node.js — SDK 클흼스이 언트도 외부초기화
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');

// 콜드 스타트 시 읐 번 생성, 이후 재사용트 시 한 번 생성, 이후 재사용
const dynamo = new DynamoDBClient({ region: 'ap-northeast-2' });

exports.handler = async (event) => {
    const result = await dynamo.send(new GetItemCommand({ ... }));
    return result;
};

해결 2 — 배포 패키지 최소화

# Node.js — esbuild로 번들링 (tree-shaking)
esbuild src/handler.ts --bundle --platform=node --target=node18 \
  --outfile=dist/handler.js --minify

# 패키지 크기 확인 (목표: 압축 기준 5MB 이하)
du -sh ./dist

해결 3 — 메모리 최적화 (CPU와 비례)

Lambda는 메모리 설정에 비례해 vCPU를 할당한다. 메모리를 늘리면 초기화도 빨라진다.

# AWS CLI로 메모리 설정 변경
aws lambda update-function-configuration \
  --function-name my-function \
  --memory-size 1024

# 콜드 스타트 비교 (동일 코드)
# 128 MB → Init: 1200ms
# 512 MB → Init: 400ms
# 1024 MB → Init: 180ms

해결 4 — Provisioned Concurrency 설정

콜드 스타트를 완전히 없애는 방법. 미리 초기화된 컨테이너를 대기시킨다.

# Provisioned Concurrency 설정
aws lambda put-provisioned-concurrency-config \
  --function-name my-function \
  --qualifier prod \
  --provisioned-concurrent-executions 5

# Auto Scaling으로 트래픽에 따라 자동 조절
aws application-autoscaling register-scalable-target \
  --service-namespace lambda \
  --resource-id function:my-function:prod \
  --scalable-dimension lambda:function:ProvisionedConcurrency \
  --min-capacity 2 \
  --max-capacity 20

해결 5 — Warm-up 스케줄러 (저비용 대안)

# 핸들러에서 warmup 요청 처리
def handler(event, context):
    if event.get('source') == 'serverless-plugin-warmup':
        print('WarmUp - Lambda is warm!')
        return {'statusCode': 200, 'body': 'warm'}

    # 실제 처리
    return process(event)

정리표

방법 효과 비용 적합한 상황
핸들러 외부 초기화 재사용 극대화 무료 항상 적용
패키지 최소화 초기화 시간 단축 무료 항상 적용
메모리 증가 초기화 속도 향상 미미 128~512MB 구간
Provisioned Concurrency 콜드 스타트 제거 높음 상시 트래픽 API
Warm-up 스케줄러 웜 상태 유지 낮음 간헐적 트래픽
VPC 제거/RDS Proxy ENI 지연 제거 RDS Proxy 비용 DB 접근 Lambda

모니터링

# CloudWatch에서 콜드 스타트 추적
# INIT_DURATION이 있는 로그  = 콜드 스타트 콜드 스타트 발생
aws logs filter-log-events \
  --log-group-name /aws/lambda/my-function \
  --filter-pattern "INIT_DURATION" \
  --start-time $(date -d '1 hour ago' +%s000)

콜드 스타트 최적화는 "없애기"보다 "허용 가능한 수준으로 관리하기"가 현실적 목표다. 핸들러 외부 초기화와 패키지 최소화는 무조건 적용하고, SLA 요구사항에 따라 Provisioned Concurrency 여부를 결정하면 �

MySQL Deadlock 완벽 해결 가이드 — 원인 분석과 쿼리 최적화

React useEffect 무한루프 완벽 해결 가이드 — 의존성 배열 원인과 해결방법

🔍 검색 키워드: React useEffect 무한루프 해결, useEffect 의존성 배열 오류, React 렌더링 무한 반복, useEffect deps 객체 함수 참조, React hooks 무한루프 원인

React useEffect 무한루프 완벽 해결 가이드 — 의존성 배열 원인과 해결방법


증상: 브라우저가 멈추거나 콘솔이 폭발한다

React 앱을 개발하다 보면 어느 순간 브라우저 탭이 멈추거나, 콘솔에 같은 로그가 수천 번 찍히거나, "Maximum update depth exceeded" 에러를 마주치게 된다.

Warning: Maximum update depth exceeded. This can happen when a component calls
setState inside useEffect, but useEffect either doesn't have a dependency array,
or one of the dependencies changes on every render.

혹은 Network 탭에서 API 요청이 무한 반복되는 걸 목격하게 된다. 이 현상의 공통 원인은 하나다: useEffect가 실행될 때마다 의존성이 새로 만들어진다.


원인 분석: 왜 무한루프가 발생하는가

원인 1 — 의존성 배열 생략

// ❌ 잘못된 코드
useEffect(() => {
  fetchData().then(res => setData(res));
}); // 의존성 배열 없음 → 매 렌더마다 실행

의존성 배열이 없으면 컴포넌트가 렌더링될 때마다 effect가 실행된다. setData가 상태를 바꾸고 → 렌더링 발생 → effect 재실행 → 무한반복.

원인 2 — 객체/배열을 의존성으로 사용

// ❌ 잘못된 코드
const filters = { page: 1, size: 10 }; // 렌더마다 새 객체 생성

useEffect(() => {
  fetchList(filters);
}, [filters]); // filters는 매 렌더마다 새 참조 → 매번 실행

JavaScript에서 객체와 배열은 참조 비교다. { page: 1 } === { page: 1 }false다. 컴포넌트가 렌더링될 때마다 filters는 메모리상 새 객체로 생성되므로 의존성이 항상 "바뀐 것"으로 판단된다.

원인 3 — 함수를 의존성으로 사용

// ❌ 잘못된 코드
const fetchData = async () => {
  const res = await api.get('/data');
  setData(res);
};

useEffect(() => {
  fetchData();
}, [fetchData]); // fetchData도 매 렌더마다 새 함수 참조

컴포넌트 본문에서 선언된 함수는 렌더링마다 새로 생성된다. useCallback 없이 함수를 의존성에 넣으면 역시 무한루프다.

원인 4 — state를 읽고 다시 set하는 패턴

// ❌ 잘못된 코드
useEffect(() => {
  setCount(count + 1); // count 읽기 → setCount → 렌더 → count 변경 → 재실행
}, [count]);

해결방법

해결 1 — 마운트 시 한 번만 실행: 빈 배열 []

// ✅ 올바른 코드
useEffect(() => {
  fetchData().then(res => setData(res));
}, []); // 컴포넌트 마운트 시 한 번만 실행

해결 2 — 객체 대신 원시값을 의존성으로

// ✅ 올바른 코드
const [page, setPage] = useState(1);
const [size] = useState(10);

useEffect(() => {
  fetchList({ page, size });
}, [page, size]); // 원시값은 값 비교 → 실제 변경 시에만 실행

해결 3 — useMemo로 객체 메모이제이션

// ✅ 올바른 코드
const filters = useMemo(() => ({ page, size, sort }), [page, size, sort]);

useEffect(() => {
  fetchList(filters);
}, [filters]); // filters 참조가 내부 값이 바뀔 때만 새로 생성됨

해결 4 — useCallback으로 함수 참조 안정화

// ✅ 올바른 코드
const fetchData = useCallback(async () => {
  const res = await api.get('/data');
  setData(res);
}, []); // 의존성이 없으면 마운트 시 한 번만 생성

useEffect(() => {
  fetchData();
}, [fetchData]); // fetchData 참조가 안정적이므로 무한루프 없음

해결 5 — 함수형 업데이트로 state 읽기 제거

// ✅ 올바른 코드
useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1); // prev를 통해 읽으므로 count를 deps에 넣을 필요 없음
  }, 1000);
  return () => clearInterval(timer);
}, []); // count 의존성 제거 → 무한루프 없음

정리표

원인 증상 해결
의존성 배열 생략 매 렌더마다 실행 [] 또는 올바른 deps 추가
객체/배열을 deps로 사용 항상 "바뀐" 것으로 판단 원시값 분리 또는 useMemo
함수를 deps로 사용 매 렌더마다 새 참조 useCallback으로 메모이제이션
state 읽고 다시 set 상태 변경 → 재실행 루프 함수형 업데이트 사용
부모 컴포넌트 렌더 자식 deps 객체 재생성 React.memo + useMemo 조합

ESLint 경고를 무시하지 말 것

eslint-plugin-react-hooksexhaustive-deps 규칙이 경고를 띄우면 반드시 이유를 파악하라. // eslint-disable-next-line으로 무시하면 대부분 나중에 무한루프나 stale closure 버그로 돌아온다.

무한루프 문제는 결국 "이 effect는 언제 실행되어야 하는가"라는 설계 질문이다. 의존성 배열은 React에게 그 의도를 전달하는 계약서다. 참조 동일성과 값 동일성의 차이를 이해하면 대부분의 무한루프는 자연스럽게 해결된다.