동적 프로그래밍(DP) | 코딩 테스트 필수 알고리즘 완벽 정리
이 글의 핵심
동적 프로그래밍(DP): 코딩 테스트 필수 알고리즘 DP 기본 개념·Top-Down (메모이제이션).
시리즈 안내
들어가며
”같은 계산을 반복하지 마세요”
동적 프로그래밍(DP)은 중복 계산을 저장해서 O(2ⁿ) → O(n)으로 최적화하는 기법입니다.
코딩 테스트 준비하며 깨달은 것
알고리즘 문제를 풀다 보면 “이게 실무에 무슨 도움이 될까?” 하는 의문이 들 때가 있습니다. 저도 그랬습니다. 하지만 실제 프로젝트에서 성능 문제에 부딪히면, 알고리즘 지식이 얼마나 중요한지 깨닫게 됩니다. 예를 들어, 사용자 검색 기능이 느려서 고민하다가 해시 테이블을 적용하니 응답 시간이 10초에서 0.1초로 줄어든 경험이 있습니다. 코딩 테스트는 단순히 취업을 위한 관문이 아니라, 문제 해결 능력을 키우는 훈련장입니다. 처음엔 브루트 포스로 풀다가, 시간 복잡도를 개선하는 과정에서 “아, 이렇게 생각하면 되는구나” 하는 깨달음을 얻을 때의 쾌감은 말로 표현하기 어렵습니다. 이 글에서는 단순히 정답 코드만 제시하는 게 아니라, 문제를 어떻게 접근하고 최적화하는지 사고 과정을 함께 공유하겠습니다.
사전 지식 (초보자를 위한 기초)
1. 재귀(Recursion)란?
재귀는 함수가 자기 자신을 호출하는 것입니다. 마치 거울 앞에 거울을 놓으면 무한히 반복되는 것처럼요.
# 간단한 재귀 예시: 팩토리얼
# 5! = 5 × 4 × 3 × 2 × 1 = 120
def factorial(n):
# 기저 조건 (재귀를 멈추는 조건)
if n <= 1:
return 1
# 재귀 호출 (자기 자신을 호출)
return n * factorial(n - 1)
print(factorial(5)) # 120
# 실행 과정:
# factorial(5) = 5 × factorial(4)
# = 5 × (4 × factorial(3))
# = 5 × (4 × (3 × factorial(2)))
# = 5 × (4 × (3 × (2 × factorial(1))))
# = 5 × (4 × (3 × (2 × 1)))
# = 5 × 4 × 3 × 2 × 1
# = 120
재귀의 핵심:
- 기저 조건: 재귀를 멈추는 조건 (없으면 무한 반복!)
- 재귀 호출: 문제를 작은 문제로 쪼개서 해결
2. 중복 계산 문제
재귀를 사용하면 같은 계산을 여러 번 하게 됩니다.
# 피보나치 수열: 1, 1, 2, 3, 5, 8, 13, ...
# fib(n) = fib(n-1) + fib(n-2)
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
# fib(5)를 계산하면:
#
# fib(5)
# / \
# fib(4) fib(3)
# / \ / \
# fib(3) fib(2) fib(2) fib(1)
# / \ / \ / \
# fib(2) fib(1) f(1) f(0) f(1) f(0)
# / \
# f(1) f(0)
#
# fib(3)이 2번 계산됨! (중복!)
# fib(2)가 3번 계산됨! (중복!)
# fib(1)이 5번 계산됨! (중복!)
#
# 너무 비효율적입니다!
문제점:
fib(40)을 계산하면 10억 번 이상 계산- 컴퓨터가 수십 초 걸림
3. 동적 프로그래밍(DP)이란?
DP는 한 번 계산한 결과를 저장해두고, 다음에 필요할 때 저장된 값을 재사용하는 기법입니다. 비유:
- 재귀: 전화번호를 매번 계산해서 찾기
- DP: 전화번호를 메모장에 적어두고 필요할 때 메모장 보기
# DP를 사용한 피보나치
def fib_dp(n, memo={}):
# 이미 계산했으면 저장된 값 반환
if n in memo:
return memo[n]
# 기저 조건
if n <= 1:
return n
# 계산 후 저장
memo[n] = fib_dp(n-1, memo) + fib_dp(n-2, memo)
return memo[n]
print(fib_dp(40)) # 즉시 출력! (0.001초)
# fib(40) 비교:
# 일반 재귀: 10억 번 계산 (수십 초)
# DP: 40번만 계산 (즉시)
4. DP를 사용하는 문제의 특징
DP는 다음 2가지 조건이 있을 때 사용합니다: 1) 최적 부분 구조 (Optimal Substructure)
- 큰 문제의 답이 작은 문제의 답으로 구할 수 있음
- 예:
fib(5) = fib(4) + fib(3)2) 중복되는 부분 문제 (Overlapping Subproblems) - 같은 작은 문제를 여러 번 계산함
- 예:
fib(3)을 여러 번 계산 DP 문제 키워드: - “최대/최소 값 구하기”
- “경우의 수 구하기”
- “가능한지 판단하기”
- 피보나치, 계단 오르기, 배낭 문제 등
5. DP의 2가지 방법
Top-Down (하향식 - 재귀)
# 큰 문제부터 시작 → 작은 문제로 쪼갬
# fib(5) → fib(4) → fib(3) → ...
def fib_topdown(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib_topdown(n-1, memo) + fib_topdown(n-2, memo)
return memo[n]
Bottom-Up (상향식 - 반복문)
# 작은 문제부터 시작 → 큰 문제로 쌓아감
# fib(0), fib(1), fib(2), ....→ fib(5)
def fib_bottomup(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[0] = 0
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
비교:
- Top-Down: 이해하기 쉬움, 필요한 것만 계산
- Bottom-Up: 더 빠름, 메모리 효율적
1. DP 기본 개념
피보나치 수열로 이해하기
문제: n번째 피보나치 수 구하기
# ❌ 단순 재귀 (O(2ⁿ) - 매우 느림!)
def fib_naive(n):
if n <= 1:
return n
return fib_naive(n-1) + fib_naive(n-2)
print(fib_naive(40)) # 수십 초 걸림!
문제점: 같은 값을 여러 번 계산
fib(5)
├─ fib(4)
│ ├─ fib(3)
│ │ ├─ fib(2) ← 중복!
│ │ └─ fib(1)
│ └─ fib(2) ← 중복!
└─ fib(3) ← 중복!
├─ fib(2)
└─ fib(1)
2. Top-Down (메모이제이션)
재귀 + 캐싱 (메모이제이션)
메모이제이션은 한 번 구한 fib(k) 값을 딕셔너리에 저장해 두었다가, 같은 k가 다시 필요할 때 재귀 호출 대신 저장된 값을 꺼내 씁니다. 전화번호를 외운 뒤에는 매번 계산하지 않고 메모장만 보는 것과 같습니다.
# ✅ 메모이제이션 (O(n))
def fib_memo(n, memo={}):
# 이미 계산한 값이면 바로 반환 (O(1))
if n in memo:
return memo[n]
# 기저 조건
if n <= 1:
return n
# 재귀 호출 + 결과 저장
# fib(n) = fib(n-1) + fib(n-2)
memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
return memo[n]
print(fib_memo(40)) # 즉시 출력! (0.001초 미만)
# 단순 재귀 vs 메모이제이션 비교:
# fib_naive(40): 2^40 = 약 1조 번 계산 (수십 초)
# fib_memo(40): 40번만 계산 (즉시)
#
# 탐색 과정 (n=5):
# fib_memo(5) 호출
# → fib_memo(4) 호출
# → fib_memo(3) 호출
# → fib_memo(2) 호출
# → fib_memo(1) = 1 (기저)
# → fib_memo(0) = 0 (기저)
# → memo[2] = 1
# → fib_memo(1) = 1 (기저)
# → memo[3] = 2
# → fib_memo(2) = 1 (memo에서 가져옴, 재계산 안 함!)
# → memo[4] = 3
# → fib_memo(3) = 2 (memo에서 가져옴)
# → memo[5] = 5
#
# 각 값은 딱 한 번만 계산됨!
메모이제이션 주의사항:
# ❌ 잘못된 사용: memo를 기본 인자로 사용
def fib_bad(n, memo={}):
# 기본 인자는 함수 정의 시 한 번만 생성됨
# 여러 번 호출 시 같은 dict를 공유
pass
# 첫 호출
fib_bad(10) # memo에 0~10 저장
# 두 번째 호출
fib_bad(5) # 이미 저장된 값 사용 (의도치 않은 공유)
# ✅ 올바른 사용: memo를 None으로 초기화
def fib_good(n, memo=None):
if memo is None:
memo = {} # 매 호출마다 새로운 dict 생성
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib_good(n-1, memo) + fib_good(n-2, memo)
return memo[n]
Python 데코레이터
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_cached(n):
if n <= 1:
return n
return fib_cached(n-1) + fib_cached(n-2)
print(fib_cached(100)) # 매우 빠름
3. Bottom-Up (타뷸레이션)
반복문 + 테이블 (Bottom-Up)
Bottom-Up은 작은 문제부터 순서대로 풀어 테이블을 채웁니다:
# ✅ Bottom-Up (O(n), 재귀보다 빠름)
def fib_dp(n):
# 기저 조건
if n <= 1:
return n
# DP 테이블 생성
# dp[i]: i번째 피보나치 수
dp = [0] * (n + 1)
# 초기값 설정
dp[0] = 0 # fib(0) = 0
dp[1] = 1 # fib(1) = 1
# 작은 문제부터 순서대로 해결
for i in range(2, n + 1):
# 점화식: fib(i) = fib(i-1) + fib(i-2)
# 이미 계산된 값(dp[i-1], dp[i-2])을 사용
dp[i] = dp[i-1] + dp[i-2]
# 최종 답 반환
return dp[n]
print(fib_dp(100)) # 즉시 출력
# 계산 과정 (n=5):
# dp = [0, 0, 0, 0, 0, 0]
#
# 초기화:
# dp = [0, 1, 0, 0, 0, 0]
#
# i=2: dp[2] = dp[1] + dp[0] = 1 + 0 = 1
# dp = [0, 1, 1, 0, 0, 0]
#
# i=3: dp[3] = dp[2] + dp[1] = 1 + 1 = 2
# dp = [0, 1, 1, 2, 0, 0]
#
# i=4: dp[4] = dp[3] + dp[2] = 2 + 1 = 3
# dp = [0, 1, 1, 2, 3, 0]
#
# i=5: dp[5] = dp[4] + dp[3] = 3 + 2 = 5
# dp = [0, 1, 1, 2, 3, 5]
#
# return dp[5] = 5
Top-Down vs Bottom-Up 비교:
# Top-Down (메모이제이션)
# 장점:
# - 재귀로 직관적
# - 필요한 부분만 계산 (일부 문제에서 유리)
# 단점:
# - 재귀 호출 오버헤드
# - 스택 오버플로우 위험 (깊이 제한)
# - 상대적으로 느림
# Bottom-Up (타뷸레이션)
# 장점:
# - 반복문으로 빠름
# - 스택 오버플로우 없음
# - 공간 최적화 쉬움
# 단점:
# - 모든 부분 문제를 계산 (불필요한 계산 가능)
# - 점화식을 찾기 어려울 수 있음
# 실전에서는 Bottom-Up을 더 많이 사용
공간 최적화
# ✅ O(1) 공간
def fib_optimized(n):
if n <= 1:
return n
prev2, prev1 = 0, 1
for _ in range(2, n + 1):
current = prev1 + prev2
prev2, prev1 = prev1, current
return prev1
print(fib_optimized(100))
4. DP 문제 풀이 패턴
패턴 1: 1차원 DP
예제: 계단 오르기
def climb_stairs(n):
"""
1칸 또는 2칸씩 올라갈 수 있을 때 경우의 수
문제 이해:
- n=1: 1가지 (1칸)
- n=2: 2가지 (1+1칸, 2칸)
- n=3: 3가지 (1+1+1칸, 1+2칸, 2+1칸)
- n=4: 5가지
- n=5: 8가지
"""
# 기저 조건
if n <= 2:
return n
# DP 테이블 생성
# dp[i]: i번째 계단에 도달하는 경우의 수
dp = [0] * (n + 1)
# 초기값 설정
dp[1] = 1 # 1번 계단: 1가지 (1칸)
dp[2] = 2 # 2번 계단: 2가지 (1+1칸, 2칸)
# 점화식: dp[i] = dp[i-1] + dp[i-2]
# i번째 계단에 도달하는 방법:
# 1. (i-1)번째 계단에서 1칸 올라오기: dp[i-1]가지
# 2. (i-2)번째 계단에서 2칸 올라오기: dp[i-2]가지
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
print(climb_stairs(5)) # 8
# 계산 과정 (n=5):
# dp = [0, 1, 2, 0, 0, 0]
#
# i=3: dp[3] = dp[2] + dp[1] = 2 + 1 = 3
# (2번에서 1칸) + (1번에서 2칸)
# dp = [0, 1, 2, 3, 0, 0]
#
# i=4: dp[4] = dp[3] + dp[2] = 3 + 2 = 5
# (3번에서 1칸) + (2번에서 2칸)
# dp = [0, 1, 2, 3, 5, 0]
#
# i=5: dp[5] = dp[4] + dp[3] = 5 + 3 = 8
# (4번에서 1칸) + (3번에서 2칸)
# dp = [0, 1, 2, 3, 5, 8]
#
# return 8
공간 최적화 버전:
def climb_stairs_optimized(n):
if n <= 2:
return n
# 이전 두 값만 저장 (O(1) 공간)
prev2, prev1 = 1, 2 # dp[1], dp[2]
for i in range(3, n + 1):
current = prev1 + prev2
prev2, prev1 = prev1, current # 값 이동
return prev1
# 공간복잡도: O(n) → O(1)
# dp[i]는 dp[i-1]과 dp[i-2]만 필요하므로
# 전체 배열을 유지할 필요 없음
패턴 2: 2차원 DP
예제: 최소 경로 합
def min_path_sum(grid):
"""
(0,0)에서 (n-1,m-1)까지 최소 합
"""
n, m = len(grid), len(grid[0])
dp = [[0] * m for _ in range(n)]
# 초기값
dp[0][0] = grid[0][0]
# 첫 행
for j in range(1, m):
dp[0][j] = dp[0][j-1] + grid[0][j]
# 첫 열
for i in range(1, n):
dp[i][0] = dp[i-1][0] + grid[i][0]
# 나머지
for i in range(1, n):
for j in range(1, m):
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
return dp[n-1][m-1]
# 테스트
grid = [
[1, 3, 1],
[1, 5, 1],
[4, 2, 1]
]
print(min_path_sum(grid)) # 7 (1→3→1→1→1)
패턴 3: 배낭 문제 (Knapsack)
0-1 배낭 문제는 각 물건을 넣거나 넣지 않는 선택을 최적화합니다:
def knapsack(weights, values, capacity):
"""
0-1 배낭 문제
문제: 배낭 용량 capacity 이하로 물건을 담아 가치 최대화
제약: 각 물건은 0개 또는 1개만 선택 가능 (0-1)
입력:
- weights: 각 물건의 무게
- values: 각 물건의 가치
- capacity: 배낭 용량
"""
n = len(weights)
# DP 테이블 생성
# dp[i][w]: i번째 물건까지 고려했을 때, 용량 w에서의 최대 가치
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
# i=0: 물건이 없으면 가치 0 (이미 초기화됨)
# w=0: 용량이 0이면 가치 0 (이미 초기화됨)
for i in range(1, n + 1):
for w in range(capacity + 1):
# 선택 1: i번째 물건을 넣지 않는 경우
# 이전 상태(i-1번째까지)의 가치를 그대로 가져옴
dp[i][w] = dp[i-1][w]
# 선택 2: i번째 물건을 넣는 경우
# 조건: 현재 물건의 무게가 용량 이하여야 함
if weights[i-1] <= w:
# i번째 물건을 넣으면:
# - 남은 용량: w - weights[i-1]
# - 가치: dp[i-1][w - weights[i-1]] + values[i-1]
# 두 선택 중 최대값 선택
dp[i][w] = max(
dp[i][w], # 넣지 않는 경우
dp[i-1][w - weights[i-1]] + values[i-1] # 넣는 경우
)
# 모든 물건을 고려했을 때, 전체 용량에서의 최대 가치
return dp[n][capacity]
# 테스트
weights = [2, 3, 4, 5] # 무게
values = [3, 4, 5, 6] # 가치
capacity = 8 # 배낭 용량
print(knapsack(weights, values, capacity)) # 10
# DP 테이블 계산 (일부):
# dp[i][w]: i번째까지, 용량 w
#
# w: 0 1 2 3 4 5 6 7 8
# i=0: 0 0 0 0 0 0 0 0 0 (물건 없음)
# i=1: 0 0 3 3 3 3 3 3 3 (무게2, 가치3)
# i=2: 0 0 3 4 4 7 7 7 7 (무게3, 가치4)
# i=3: 0 0 3 4 5 7 8 9 9 (무게4, 가치5)
# i=4: 0 0 3 4 5 7 8 9 10 (무게5, 가치6)
#
# 최종 답: dp[4][8] = 10
# 선택된 물건: 무게3(가치4) + 무게5(가치6) = 총 무게8, 총 가치10
공간 최적화 (1차원 배열):
def knapsack_optimized(weights, values, capacity):
n = len(weights)
dp = [0] * (capacity + 1)
for i in range(n):
# 뒤에서부터 업데이트 (덮어쓰기 방지)
for w in range(capacity, weights[i] - 1, -1):
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
return dp[capacity]
# 공간복잡도: O(n*capacity) → O(capacity)
5. DP 문제 접근법
1단계: DP인지 판단
✅ DP 신호:
- "최대/최소 값"
- "경우의 수"
- "가능 여부"
- 중복되는 부분 문제
❌ DP 아님:
- 그리디로 풀림
- 단순 시뮬레이션
2단계: 점화식 세우기
# 예: 계단 오르기
# dp[i] = i번째 계단까지 가는 경우의 수
# dp[i] = dp[i-1] + dp[i-2]
3단계: 초기값 설정
# 예: 계단 오르기
dp[1] = 1 # 1칸: 1가지
dp[2] = 2 # 2칸: 2가지 (1+1, 2)
4단계: 구현 선택
# Top-Down: 재귀가 자연스러울 때
# Bottom-Up: 반복문이 명확할 때 (더 빠름)
실전 팁
디버깅 팁
# DP 테이블 출력
def print_dp_table(dp):
for row in dp:
print(row)
# 중간 과정 확인
print(f"dp[{i}] = {dp[i]}")
최적화 팁
# 1. 공간 최적화: 이전 행만 필요하면 1차원 배열
# 2. 시간 최적화: 불필요한 계산 스킵
# 3. 메모 초기화: defaultdict 활용
정리
핵심 요약
- DP: 중복 계산 저장, O(2ⁿ) → O(n)
- Top-Down: 재귀 + 메모이제이션
- Bottom-Up: 반복문 + 테이블 (더 빠름)
- 점화식: dp[i] = f(dp[i-1], dp[i-2], …)
추천 문제
백준:
관련 글
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「동적 프로그래밍(DP) | 코딩 테스트 필수 알고리즘 완벽 정리」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「동적 프로그래밍(DP) | 코딩 테스트 필수 알고리즘 완벽 정리」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 동적 프로그래밍(DP): 코딩 테스트 필수 알고리즘 완벽 정리. DP 기본 개념·Top-Down (메모이제이션)로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다. 알고리즘·DP·동적프로그래밍 중심으로 설명합니… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
이 글에서 다루는 키워드 (관련 검색어)
알고리즘, DP, 동적프로그래밍, 메모이제이션, 코딩테스트 등으로 검색하시면 이 글이 도움이 됩니다.