본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 18. · 46 Views
에러 처리와 폴백 완벽 가이드
AWS API 호출 시 발생하는 에러를 처리하고 폴백 전략을 구현하는 방법을 다룹니다. ThrottlingException부터 서킷 브레이커 패턴까지, 실전에서 바로 활용할 수 있는 안정적인 에러 처리 기법을 배웁니다.
목차
1. 일반적인 에러 유형
어느 날 김개발 씨가 AWS API를 호출하는 코드를 작성했습니다. 로컬에서는 완벽하게 작동했는데, 배포 후 갑자기 500 에러가 속출했습니다.
선배인 박시니어 씨가 물었습니다. "에러 처리는 해뒀어요?"
AWS API를 사용할 때는 다양한 에러가 발생할 수 있습니다. 네트워크 에러, 권한 에러, 서비스 제한 에러 등 각각의 에러 유형을 이해하고 적절히 처리하는 것이 안정적인 서비스의 첫걸음입니다.
에러를 제대로 분류하면 적절한 대응 전략을 수립할 수 있습니다.
다음 코드를 살펴봅시다.
// AWS API 호출 시 발생하는 일반적인 에러 처리
try {
const response = await bedrock.invokeModel({
modelId: 'anthropic.claude-3-sonnet',
body: JSON.stringify(payload)
});
return response;
} catch (error: any) {
// 에러 유형별로 분류하여 처리
if (error.name === 'ThrottlingException') {
console.error('API 호출 제한 초과');
} else if (error.name === 'ValidationException') {
console.error('잘못된 요청 파라미터');
} else if (error.name === 'AccessDeniedException') {
console.error('권한 부족');
} else {
console.error('알 수 없는 에러:', error.message);
}
throw error;
}
김개발 씨는 입사 6개월 차 백엔드 개발자입니다. 회사에서 AWS Bedrock을 활용한 AI 기능을 개발하고 있습니다.
로컬에서 테스트할 때는 모든 게 완벽했는데, 프로덕션 환경에 배포하자마자 문제가 터졌습니다. 새벽 2시, 온콜 담당이었던 김개발 씨의 전화기가 울렸습니다.
"서비스가 안 돼요!" 급하게 로그를 확인하니 수많은 에러 메시지가 쏟아지고 있었습니다. 에러 처리란 무엇일까요? 마치 운전할 때 안전벨트를 매는 것과 같습니다.
평소에는 필요 없어 보이지만, 사고가 났을 때 생명을 구하는 중요한 안전장치입니다. 코드도 마찬가지입니다.
정상적으로 작동할 때는 에러 처리 코드가 실행되지 않지만, 문제가 발생했을 때 서비스 전체가 멈추는 것을 막아줍니다. 에러 처리가 없던 시절에는 어땠을까요? 초기 개발자들은 모든 코드가 항상 성공한다고 가정하고 프로그래밍했습니다.
API 호출이 실패할 수도 있다는 생각 자체를 하지 않았습니다. 결과는 참담했습니다.
작은 네트워크 문제 하나로 전체 서비스가 다운되는 일이 비일비재했습니다. 더 큰 문제는 무엇이 잘못됐는지 파악조차 어려웠다는 점입니다.
에러 로그도 없고, 원인도 모르니 디버깅에 며칠씩 걸리곤 했습니다. 사용자들은 "서비스가 느려요", "안 돼요"라는 막연한 불만만 토로할 뿐이었습니다.
에러 유형별 분류의 등장 이런 문제를 해결하기 위해 에러를 체계적으로 분류하고 처리하는 방법이 발전했습니다. AWS에서 발생하는 에러는 크게 몇 가지 유형으로 나뉩니다.
첫째, ThrottlingException은 API 호출 횟수 제한을 초과했을 때 발생합니다. 둘째, ValidationException은 요청 파라미터가 잘못됐을 때 나타납니다.
셋째, AccessDeniedException은 IAM 권한이 부족할 때 발생합니다. 넷째, 네트워크 타임아웃이나 연결 실패 같은 인프라 문제도 있습니다.
코드를 단계별로 살펴봅시다 try-catch 블록으로 전체 API 호출을 감쌉니다. 이렇게 하면 어떤 에러가 발생하더라도 catch 블록에서 잡을 수 있습니다.
catch 블록 안에서는 error.name을 확인하여 에러 유형을 판단합니다. ThrottlingException이라면 "너무 많은 요청을 보냈구나"라고 이해할 수 있습니다.
ValidationException이라면 "파라미터를 잘못 보냈구나"라고 파악할 수 있습니다. 각 에러마다 적절한 로그를 남깁니다.
이 로그는 나중에 문제를 분석하는 데 귀중한 자료가 됩니다. 마지막으로 에러를 다시 throw하여 상위 레이어에서도 처리할 수 있게 합니다.
실무에서는 어떻게 활용할까요? 예를 들어 AI 챗봇 서비스를 운영한다고 가정해봅시다. 사용자가 질문을 입력하면 AWS Bedrock API를 호출하여 답변을 생성합니다.
이때 ThrottlingException이 발생하면 "잠시만 기다려주세요"라는 메시지를 보여줄 수 있습니다. ValidationException이면 "질문을 다시 입력해주세요"라고 안내할 수 있습니다.
네이버, 카카오 같은 대형 서비스에서는 이런 에러 처리를 더욱 정교하게 구현합니다. 에러 유형별로 다른 알림을 보내고, 심각도에 따라 온콜 담당자를 깨우기도 합니다.
흔한 실수는 무엇일까요? 초보 개발자들은 모든 에러를 동일하게 처리하는 실수를 범합니다. "에러가 났네, 그냥 로그만 찍고 넘어가자." 이렇게 하면 복구 가능한 에러도 그냥 실패로 끝나버립니다.
또 다른 실수는 에러 메시지를 사용자에게 그대로 보여주는 것입니다. "AccessDeniedException: IAM role lacks bedrock:InvokeModel permission"이라는 메시지를 일반 사용자가 보면 어떨까요?
혼란스러울 뿐입니다. 사용자에게는 "일시적인 문제가 발생했습니다"라는 친절한 메시지를 보여주고, 개발자에게만 상세한 에러를 전달해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다 박시니어 씨는 김개발 씨의 코드를 보며 말했습니다. "에러가 발생했을 때 어떻게 처리할지 미리 정해둬야 해요.
각 에러 유형마다 다른 전략이 필요합니다." 김개발 씨는 고개를 끄덕이며 코드를 수정하기 시작했습니다. 에러 유형별로 로그를 남기고, 재시도할 수 있는 에러와 즉시 실패해야 하는 에러를 구분했습니다.
그날 이후로 새벽 온콜 전화는 현저히 줄어들었습니다. 에러를 제대로 분류하고 처리하면 서비스의 안정성이 크게 향상됩니다.
문제가 생겼을 때 빠르게 원인을 파악하고 대응할 수 있습니다.
실전 팁
💡 - 에러마다 고유한 에러 코드를 부여하면 추적이 쉬워집니다
- 에러 로그에는 항상 타임스탬프와 요청 ID를 포함시키세요
- 사용자용 메시지와 개발자용 메시지를 분리하여 관리하세요
2. ThrottlingException 처리
김개발 씨가 만든 AI 챗봇이 인기를 끌면서 사용자가 급증했습니다. 그런데 오후 2시경부터 "ThrottlingException: Rate exceeded"라는 에러가 쏟아지기 시작했습니다.
박시니어 씨가 말했습니다. "AWS가 당신 요청을 막고 있네요."
ThrottlingException은 AWS API 호출 횟수가 허용된 제한을 초과했을 때 발생하는 에러입니다. AWS는 서비스 안정성을 위해 초당 요청 수를 제한하는데, 이를 넘으면 일시적으로 요청을 거부합니다.
이 에러는 복구 가능한 에러이므로, 적절한 지연 시간을 두고 재시도하면 대부분 해결됩니다.
다음 코드를 살펴봅시다.
// ThrottlingException 감지 및 처리
async function callBedrockWithThrottling(payload: any) {
try {
const response = await bedrock.invokeModel({
modelId: 'anthropic.claude-3-sonnet',
body: JSON.stringify(payload)
});
return response;
} catch (error: any) {
// Throttling 에러 확인
if (error.name === 'ThrottlingException') {
console.warn('API 호출 제한 초과, 재시도 필요');
// 재시도 로직은 다음 섹션에서 구현
throw new Error('RATE_LIMIT_EXCEEDED');
}
throw error;
}
}
김개발 씨가 만든 AI 요약 서비스가 회사 내부에서 입소문을 타기 시작했습니다. 처음에는 하루 100건 정도였던 요청이 어느새 1000건을 넘어섰습니다.
모두가 점심시간에 몰려서 사용하니 순간적으로 초당 50개의 API 요청이 발생했습니다. 그런데 AWS Bedrock의 기본 제한은 초당 10개였습니다.
결과는 예상대로였습니다. ThrottlingException이 폭발적으로 증가했고, 대부분의 사용자가 에러 화면만 보게 됐습니다.
ThrottlingException이란 무엇일까요? 마치 고속도로 톨게이트와 같습니다. 동시에 너무 많은 차량이 몰리면 통과 속도를 제한하여 혼잡을 방지합니다.
AWS도 마찬가지입니다. 한꺼번에 너무 많은 요청이 들어오면 "잠깐만요, 천천히 보내세요"라고 말하며 일부 요청을 거절합니다.
이는 AWS가 심술을 부리는 게 아닙니다. 오히려 서비스 전체의 안정성을 지키기 위한 안전장치입니다.
한 사용자의 폭주하는 요청 때문에 다른 사용자들까지 피해를 보는 일을 막기 위한 것입니다. 왜 이런 제한이 필요할까요? 제한이 없던 초창기 클라우드 서비스를 상상해봅시다.
어떤 개발자가 실수로 무한 루프에서 API를 호출하는 코드를 배포했습니다. 순식간에 수백만 개의 요청이 쏟아졌고, AWS 서버가 다운됐습니다.
그 서버를 사용하던 수천 개의 다른 서비스도 함께 멈췄습니다. 이런 대참사를 막기 위해 Rate Limiting이라는 개념이 도입됐습니다.
각 계정마다, 각 API마다 초당 최대 요청 수가 정해졌습니다. Bedrock의 경우 기본값은 초당 10개입니다.
이 숫자를 넘으면 ThrottlingException이 발생합니다. 어떻게 감지하고 처리할까요? 코드를 살펴보면, try-catch 블록에서 에러를 잡은 후 error.name을 확인합니다.
'ThrottlingException'이라는 문자열과 정확히 일치하는지 검사합니다. 일치한다면 이것은 일시적인 문제입니다.
서버가 고장 난 게 아니라 단순히 "너무 바빠요, 조금만 기다려주세요"라는 메시지입니다. 따라서 바로 실패로 처리하지 않고, 재시도할 수 있도록 특별한 에러로 변환합니다.
여기서는 'RATE_LIMIT_EXCEEDED'라는 커스텀 에러를 발생시킵니다. 이 에러를 받은 상위 레이어에서는 "아, 속도 제한에 걸렸구나.
잠깐 기다렸다가 다시 시도해야겠다"라고 판단할 수 있습니다. 실무에서는 어떻게 대응할까요? 쿠팡이나 배달의민족 같은 서비스를 생각해봅시다.
점심시간 12시부터 1시까지 주문이 폭증합니다. 이때 모든 요청을 즉시 처리하려고 하면 ThrottlingException이 속출할 겁니다.
대신 이렇게 대응합니다. 먼저 요청을 큐에 쌓습니다.
그리고 적절한 속도로 하나씩 꺼내서 처리합니다. 사용자에게는 "주문을 받았습니다.
곧 처리됩니다"라는 메시지를 보여줍니다. ThrottlingException이 발생하면 자동으로 재시도합니다.
AWS에서는 제한을 높여달라고 요청할 수도 있습니다. Service Quotas 콘솔에서 증가 신청을 하면 며칠 내로 승인됩니다.
하지만 무조건 높이는 것보다는 요청 속도를 조절하는 게 더 근본적인 해결책입니다. 주의할 점은 무엇일까요? ThrottlingException을 무시하고 계속 재시도하면 상황이 더 악화됩니다.
마치 막힌 톨게이트에서 계속 경적을 울리는 것과 같습니다. 소용없을 뿐 아니라 다른 사람에게 피해를 줍니다.
올바른 방법은 Exponential Backoff입니다. 첫 번째 재시도는 1초 후, 두 번째는 2초 후, 세 번째는 4초 후...
이런 식으로 대기 시간을 점점 늘려갑니다. 이렇게 하면 서버가 회복할 시간을 벌어줄 수 있습니다.
김개발 씨는 어떻게 해결했을까요? 박시니어 씨의 조언을 받은 김개발 씨는 두 가지 조치를 취했습니다. 첫째, ThrottlingException을 감지하는 코드를 추가했습니다.
둘째, AWS에 제한 증가를 요청했습니다. 초당 10개에서 50개로 늘려달라고 신청했고, 이틀 후 승인됐습니다.
하지만 더 중요한 것은 재시도 로직이었습니다. 다음 섹션에서 배울 내용이지만, 김개발 씨는 자동 재시도 시스템을 구축했습니다.
ThrottlingException이 발생하면 자동으로 2초 후에 재시도하고, 그래도 실패하면 4초 후에 다시 시도합니다. 결과는 놀라웠습니다.
점심시간에도 안정적으로 서비스가 작동했고, 에러율이 0.1% 미만으로 떨어졌습니다. 사용자들은 가끔 "처리 중입니다"라는 메시지를 보지만, 곧 정상적으로 결과를 받을 수 있었습니다.
실전 팁
💡 - AWS Service Quotas 콘솔에서 현재 제한과 사용량을 모니터링하세요
- CloudWatch 알람을 설정하여 ThrottlingException 발생률을 추적하세요
- 가능하면 요청을 시간대별로 분산시켜 피크를 줄이세요
3. 재시도 로직 구현
ThrottlingException을 감지하는 것만으로는 부족합니다. 김개발 씨는 에러를 잡았지만, 그다음에 무엇을 해야 할지 몰라서 박시니어 씨에게 물었습니다.
"이제 어떻게 하죠?" 박시니어 씨가 웃으며 답했습니다. "다시 시도하면 되죠.
하지만 똑똑하게요."
재시도 로직은 일시적인 에러가 발생했을 때 자동으로 요청을 다시 보내는 메커니즘입니다. 핵심은 Exponential Backoff와 Jitter를 활용하여 서버에 부담을 주지 않으면서도 성공 확률을 높이는 것입니다.
AWS SDK에는 기본 재시도 로직이 내장되어 있지만, 커스텀 로직을 구현하면 더 세밀한 제어가 가능합니다.
다음 코드를 살펴봅시다.
// Exponential Backoff와 Jitter를 사용한 재시도 로직
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
const isLastAttempt = attempt === maxRetries - 1;
const isRetryable = error.name === 'ThrottlingException' ||
error.name === 'ServiceUnavailable';
if (!isRetryable || isLastAttempt) {
throw error;
}
// Exponential Backoff: 2^attempt * 1000ms
const baseDelay = Math.pow(2, attempt) * 1000;
// Jitter: 랜덤성 추가 (0~500ms)
const jitter = Math.random() * 500;
const delay = baseDelay + jitter;
console.log(`재시도 ${attempt + 1}/${maxRetries}, ${delay}ms 대기`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
}
김개발 씨는 처음에 간단하게 생각했습니다. "에러가 나면 1초 기다렸다가 다시 시도하면 되겠네!" 코드를 작성하고 배포했습니다.
그런데 문제가 생겼습니다. 점심시간에 1000명의 사용자가 동시에 요청을 보냈습니다.
모두 ThrottlingException을 받았고, 정확히 1초 후에 모두 재시도했습니다. 결과는?
또다시 1000개의 요청이 동시에 AWS 서버를 강타했고, 전부 ThrottlingException을 받았습니다. 무한 루프에 빠진 것입니다.
Exponential Backoff란 무엇일까요? 마치 교통 체증을 피하는 방법과 같습니다. 고속도로가 막혔을 때, 모든 차가 똑같이 5분 후에 다시 진입하면 또 막힙니다.
대신 어떤 차는 2분 후, 어떤 차는 4분 후, 어떤 차는 8분 후에 진입하면 흐름이 분산됩니다. Exponential Backoff는 재시도할 때마다 대기 시간을 2배씩 늘립니다.
첫 번째 재시도는 1초 후, 두 번째는 2초 후, 세 번째는 4초 후, 네 번째는 8초 후... 이렇게 하면 서버에 주는 부담이 점진적으로 줄어들어 회복할 시간을 벌어줍니다.
Jitter는 왜 필요할까요? 하지만 여전히 문제가 있습니다. 1000명이 동시에 에러를 받으면, Exponential Backoff를 적용해도 모두 똑같은 시간에 재시도합니다.
첫 번째는 모두 1초 후, 두 번째는 모두 2초 후... 여기에 Jitter를 추가합니다.
Jitter는 무작위성을 의미합니다. 2초 후에 재시도하되, 정확히 2000ms가 아니라 2000~2500ms 사이의 랜덤한 시간을 선택합니다.
이렇게 하면 1000개의 요청이 시간적으로 흩어져서 들어오게 됩니다. 코드를 단계별로 분석해봅시다 retryWithBackoff 함수는 제네릭 타입 T를 받습니다.
어떤 종류의 비동기 함수든 래핑할 수 있습니다. for 루프를 사용하여 최대 재시도 횟수만큼 반복합니다.
각 시도마다 전달받은 함수 fn을 실행합니다. 성공하면 즉시 결과를 반환하고 루프를 빠져나옵니다.
에러가 발생하면 먼저 재시도 가능한 에러인지 확인합니다. ThrottlingException이나 ServiceUnavailable처럼 일시적인 에러만 재시도합니다.
ValidationException처럼 파라미터 문제는 재시도해도 소용없으므로 즉시 실패시킵니다. 마지막 시도였다면 더 이상 재시도하지 않고 에러를 던집니다.
그렇지 않다면 대기 시간을 계산합니다. Math.pow(2, attempt)는 2의 거듭제곱을 계산하여 1, 2, 4, 8...을 만들어냅니다.
여기에 1000을 곱하면 밀리초 단위가 됩니다. Math.random()으로 0에서 1 사이의 랜덤 값을 생성하고, 500을 곱하면 0~500ms의 Jitter가 만들어집니다.
최종 대기 시간은 baseDelay와 jitter를 합친 값입니다. setTimeout을 Promise로 감싸서 await할 수 있게 만듭니다.
지정된 시간만큼 대기한 후 다음 재시도를 진행합니다. 실무에서는 어떻게 활용할까요? 넷플릭스의 경우 전 세계 수억 명이 동시에 접속합니다.
서버 한 대가 일시적으로 응답하지 않으면 어떻게 될까요? 재시도 로직이 없다면 수백만 개의 요청이 실패할 것입니다.
넷플릭스는 정교한 재시도 시스템을 구축했습니다. Exponential Backoff와 Jitter는 기본이고, 요청의 중요도에 따라 재시도 횟수를 다르게 설정합니다.
메인 비디오 스트리밍은 최대 5회까지 재시도하지만, 부가적인 추천 목록은 1회만 시도합니다. AWS 공식 SDK도 내부적으로 비슷한 로직을 사용합니다.
하지만 기본 설정이 모든 상황에 최적인 것은 아닙니다. Bedrock처럼 응답 시간이 긴 API는 재시도 간격을 더 길게 설정하는 게 좋습니다.
주의해야 할 함정들 무한 재시도는 절대 금물입니다. 반드시 최대 재시도 횟수를 설정해야 합니다.
보통 3~5회가 적당합니다. 그 이상 실패하면 근본적인 문제가 있다는 신호입니다.
또 다른 실수는 모든 에러를 재시도하는 것입니다. AccessDeniedException은 재시도해도 절대 성공하지 않습니다.
권한 설정을 바꾸지 않는 한 말이죠. ValidationException도 마찬가지입니다.
파라미터가 잘못됐으면 코드를 고쳐야 합니다. 재시도 로그를 남기는 것도 중요합니다.
얼마나 자주 재시도가 발생하는지 모니터링해야 합니다. 재시도율이 10%를 넘으면 뭔가 잘못된 것입니다.
인프라를 점검하거나 AWS 제한을 높여야 할 시점입니다. 김개발 씨의 개선 사항 박시니어 씨의 조언대로 김개발 씨는 retryWithBackoff 함수를 구현했습니다.
그리고 모든 Bedrock API 호출을 이 함수로 감쌌습니다. 결과는 즉각적이었습니다.
ThrottlingException 발생률은 그대로였지만, 최종 실패율은 5%에서 0.5%로 떨어졌습니다. 대부분의 요청이 재시도를 통해 성공한 것입니다.
사용자 입장에서는 가끔 응답이 1~2초 늦어지는 정도였습니다. "처리 중..."이라는 로딩 메시지를 조금 더 보는 것뿐이었습니다.
에러 화면을 보는 것보다 훨씬 나은 경험이었습니다. 재시도 로직은 분산 시스템의 필수 요소입니다.
네트워크는 언제나 불안정하고, 서버는 때때로 과부하에 걸립니다. 이런 일시적인 문제를 자동으로 해결해주는 재시도 로직이 있으면 서비스 안정성이 비약적으로 향상됩니다.
실전 팁
💡 - AWS SDK의 기본 재시도 설정을 확인하고, 필요시 maxAttempts를 조정하세요
- 재시도 로그를 CloudWatch나 DataDog으로 수집하여 패턴을 분석하세요
- 사용자에게는 "재시도 중입니다"라는 명확한 피드백을 제공하세요
4. 폴백 모델 설정
어느 날 AWS가 Claude 3.5 Sonnet 모델의 일시적인 장애를 공지했습니다. 김개발 씨의 서비스는 완전히 멈췄습니다.
사용자들의 불만이 쏟아졌습니다. 박시니어 씨가 물었습니다.
"Plan B는 없었어요?"
폴백 모델은 주 모델이 실패했을 때 자동으로 대체 모델로 전환하는 전략입니다. AWS Bedrock에는 Claude 외에도 다양한 모델이 있으며, 성능과 가격이 다릅니다.
주 모델로 Claude 3.5 Sonnet을 사용하다가 실패하면 Claude 3 Haiku나 다른 모델로 자동 전환하여 서비스 연속성을 보장할 수 있습니다.
다음 코드를 살펴봅시다.
// 폴백 모델 체인 구성
const MODEL_CHAIN = [
{ id: 'anthropic.claude-3-5-sonnet-20241022-v2:0', name: 'Claude 3.5 Sonnet' },
{ id: 'anthropic.claude-3-sonnet-20240229-v1:0', name: 'Claude 3 Sonnet' },
{ id: 'anthropic.claude-3-haiku-20240307-v1:0', name: 'Claude 3 Haiku' }
];
async function invokeWithFallback(payload: any): Promise<any> {
let lastError: Error | null = null;
for (const model of MODEL_CHAIN) {
try {
console.log(`${model.name} 시도 중...`);
const response = await bedrock.invokeModel({
modelId: model.id,
body: JSON.stringify(payload)
});
console.log(`${model.name} 성공`);
return response;
} catch (error: any) {
console.warn(`${model.name} 실패: ${error.message}`);
lastError = error;
// 다음 모델로 자동 전환
continue;
}
}
// 모든 모델이 실패한 경우
throw new Error(`All models failed. Last error: ${lastError?.message}`);
}
토요일 오후 3시, 김개발 씨는 친구들과 카페에서 즐거운 시간을 보내고 있었습니다. 그런데 갑자기 핸드폰이 울렸습니다.
회사 슬랙에서 긴급 알림이 쏟아졌습니다. "AI 기능이 안 돼요!" "계속 에러만 나와요!" 급하게 로그를 확인하니 "ModelNotReadyException"이라는 생소한 에러가 보였습니다.
AWS 콘솔에 들어가 보니 공지가 떠 있었습니다. "Claude 3.5 Sonnet 모델에 일시적인 문제가 발생했습니다.
복구 중입니다." 김개발 씨는 속이 탔습니다. 언제 복구될지도 모르는데, 사용자들은 계속 기다리고 있습니다.
"다른 모델을 써볼까?" 하지만 코드는 하나의 모델만 사용하도록 하드코딩되어 있었습니다. 폴백이란 무엇일까요? 마치 여행 갈 때 플랜 B를 준비하는 것과 같습니다.
원래는 비행기를 타려고 했는데 결항되면? KTX로 바꿔 타면 됩니다.
KTX도 안 되면? 버스를 타면 됩니다.
목적지에 도착하는 것이 중요하지, 수단이 무엇인지는 덜 중요합니다. AI 모델도 마찬가지입니다.
Claude 3.5 Sonnet이 최고 성능을 내지만, 서비스가 멈추는 것보다는 Claude 3 Haiku라도 사용하는 게 낫습니다. 성능이 조금 떨어지더라도 사용자에게 결과를 제공할 수 있으니까요.
폴백 전략은 어떻게 설계할까요? 먼저 우선순위를 정합니다. 가장 좋은 모델을 1순위로, 그다음 좋은 모델을 2순위로, 가장 빠르고 저렴한 모델을 마지막 순위로 배치합니다.
MODEL_CHAIN 배열이 바로 이 우선순위를 나타냅니다. Claude 3.5 Sonnet이 1순위, Claude 3 Sonnet이 2순위, Claude 3 Haiku가 최종 폴백입니다.
각 모델의 ID와 이름을 객체로 저장합니다. 코드는 어떻게 작동할까요? invokeWithFallback 함수는 for 루프로 MODEL_CHAIN을 순회합니다.
첫 번째 모델부터 시도하며, 성공하면 즉시 결과를 반환합니다. 실패하면 에러를 로그로 남기고 continue로 다음 모델로 넘어갑니다.
두 번째 모델을 시도하고, 또 실패하면 세 번째 모델로 넘어갑니다. 모든 모델이 실패하면 lastError에 저장된 마지막 에러 정보를 포함한 에러를 던집니다.
이렇게 하면 디버깅할 때 무엇이 잘못됐는지 파악할 수 있습니다. 성공한 경우 console.log로 어떤 모델이 성공했는지 기록합니다.
이 정보는 나중에 모니터링할 때 유용합니다. "오늘은 Haiku를 많이 썼네?
Sonnet에 문제가 있었나?"라고 추론할 수 있습니다. 실무에서는 어떻게 활용할까요? OpenAI의 경우 GPT-4가 간헐적으로 과부하 상태가 됩니다.
큰 서비스들은 GPT-4 → GPT-3.5 → Claude → Llama 같은 폴백 체인을 구성합니다. 중요한 작업은 GPT-4로 처리하지만, 과부하 상태면 자동으로 GPT-3.5로 전환합니다.
사용자는 응답 품질이 미세하게 다른 것을 느낄 수 있지만, 에러 화면을 보는 것보다는 훨씬 낫습니다. 비용 최적화 측면에서도 유용합니다.
평소에는 저렴한 Haiku를 사용하다가, 복잡한 요청이 오면 Sonnet으로 자동 업그레이드하는 전략도 가능합니다. 이는 폴백의 역방향 개념인 "폴업(Fall-up)"이라고 부르기도 합니다.
주의할 점은 무엇일까요? 모델마다 입력 형식이 다를 수 있습니다. Claude는 특정 프롬프트 형식을 선호하고, GPT는 다른 형식을 선호합니다.
폴백할 때 프롬프트를 변환하는 로직이 필요할 수 있습니다. 또한 비용도 고려해야 합니다.
Haiku는 Sonnet보다 10배 저렴합니다. 무조건 Sonnet만 사용하다가 트래픽이 급증하면 비용 폭탄을 맞을 수 있습니다.
요청의 복잡도를 분석하여 적절한 모델을 선택하는 지능형 라우팅이 필요합니다. 폴백 로그를 반드시 모니터링해야 합니다.
Haiku 사용률이 50%를 넘는다면 Sonnet에 심각한 문제가 있다는 신호입니다. AWS 지원팀에 문의하거나 다른 대안을 찾아야 합니다.
김개발 씨의 해결책 박시니어 씨에게 급하게 전화를 걸었습니다. "어떡하죠?
Sonnet이 계속 안 돼요!" 박시니어 씨는 침착하게 말했습니다. "폴백 모델을 설정하세요.
Haiku로 임시 대응하면 됩니다." 김개발 씨는 서둘러 위 코드를 작성하고 배포했습니다. 15분 만에 서비스가 복구됐습니다.
Haiku를 사용하니 응답 품질이 조금 떨어졌지만, 사용자들은 최소한 결과를 받을 수 있었습니다. 3시간 후 AWS에서 Sonnet 복구 완료를 공지했습니다.
김개발 씨의 서비스는 자동으로 다시 Sonnet을 사용하기 시작했습니다. 폴백 덕분에 장애 시간을 3시간에서 15분으로 줄인 것입니다.
폴백 전략은 고가용성 시스템의 핵심입니다. 어떤 컴포넌트도 100% 신뢰할 수 없습니다.
항상 플랜 B, 플랜 C를 준비해두어야 합니다.
실전 팁
💡 - 폴백 모델 사용률을 CloudWatch 메트릭으로 추적하세요
- 주기적으로 폴백 경로를 테스트하여 실제로 작동하는지 확인하세요
- 사용자에게 "임시로 다른 모델을 사용 중입니다"라고 투명하게 알리는 것도 좋습니다
5. 서킷 브레이커 패턴
폴백 전략을 구현한 김개발 씨는 안심했습니다. 하지만 새로운 문제가 생겼습니다.
첫 번째 모델이 계속 실패하는데도 매번 시도하다가 타임아웃이 걸렸습니다. 5초씩 낭비하다가 결국 두 번째 모델로 넘어가는 것입니다.
박시니어 씨가 말했습니다. "서킷 브레이커가 필요하겠네요."
서킷 브레이커 패턴은 전기 회로의 차단기에서 영감을 받은 패턴입니다. 연속적으로 실패하는 서비스에 대한 요청을 일시적으로 차단하여, 불필요한 대기 시간을 줄이고 시스템 자원을 보호합니다.
실패율이 임계값을 넘으면 회로를 "열어서" 해당 서비스로의 요청을 즉시 차단하고, 일정 시간 후 다시 시도합니다.
다음 코드를 살펴봅시다.
// 서킷 브레이커 구현
class CircuitBreaker {
private failures = 0;
private lastFailTime = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
constructor(
private threshold: number = 3, // 연속 실패 임계값
private timeout: number = 60000 // 회복 대기 시간 (60초)
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
// OPEN 상태: 즉시 실패
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailTime > this.timeout) {
this.state = 'HALF_OPEN';
console.log('회로 HALF_OPEN: 재시도 시작');
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
private onFailure() {
this.failures++;
this.lastFailTime = Date.now();
if (this.failures >= this.threshold) {
this.state = 'OPEN';
console.log(`회로 OPEN: ${this.failures}회 연속 실패`);
}
}
}
김개발 씨는 폴백 전략을 구현하고 뿌듯해했습니다. 하지만 모니터링 대시보드를 보니 이상한 패턴이 보였습니다.
평균 응답 시간이 5초나 됐습니다. 자세히 들여다보니, Claude 3.5 Sonnet이 계속 실패하는데도 매번 5초 동안 타임아웃을 기다렸다가 폴백으로 넘어가고 있었습니다.
100개의 요청이 오면 100번 다 5초씩 낭비하는 것입니다. 총 500초, 8분 넘게 허비한 셈입니다.
"이미 Sonnet이 죽었다는 걸 알면, 굳이 계속 시도할 필요가 있을까?" 김개발 씨는 의문이 들었습니다. 서킷 브레이커란 무엇일까요? 집에 있는 두꺼비집(차단기)을 떠올려 보세요.
전기 과부하가 걸리면 자동으로 차단됩니다. 이는 화재를 막기 위한 안전장치입니다.
전기가 차단되면 문제를 해결한 후 다시 올려야 합니다. 소프트웨어의 서킷 브레이커도 같은 원리입니다.
어떤 서비스가 계속 실패하면 "회로를 열어서" 그 서비스로 가는 요청을 차단합니다. 시간을 낭비하지 않고 즉시 폴백으로 넘어갑니다.
일정 시간이 지나면 "회로를 반쯤 열어서" 다시 시도해보고, 성공하면 정상으로 돌아옵니다. 서킷 브레이커의 세 가지 상태 CLOSED 상태는 정상 상태입니다.
모든 요청이 정상적으로 통과합니다. 실패가 발생하면 카운터가 증가합니다.
OPEN 상태는 차단 상태입니다. 연속 실패 횟수가 임계값(예: 3회)을 넘으면 이 상태로 전환됩니다.
이 상태에서는 요청을 시도조차 하지 않고 즉시 에러를 반환합니다. 5초를 기다릴 필요가 없어집니다.
HALF_OPEN 상태는 복구 시도 상태입니다. OPEN 상태로 일정 시간(예: 60초)이 지나면 이 상태로 전환됩니다.
다시 한 번 시도해보고, 성공하면 CLOSED로 돌아가고, 실패하면 다시 OPEN으로 갑니다. 코드를 분석해봅시다 CircuitBreaker 클래스는 failures(실패 횟수), lastFailTime(마지막 실패 시각), state(현재 상태) 세 가지를 추적합니다.
execute 메서드가 핵심입니다. 먼저 현재 상태를 확인합니다.
OPEN 상태라면 timeout(60초)이 지났는지 확인합니다. 아직 안 지났으면 즉시 에러를 던집니다.
"지금은 안 돼요, 나중에 다시 오세요." timeout이 지났다면 HALF_OPEN으로 바꾸고 함수를 실행해봅니다. 성공하면 onSuccess를 호출하여 상태를 CLOSED로 되돌리고 실패 카운터를 0으로 리셋합니다.
"다시 살아났네요!" 실패하면 onFailure를 호출합니다. 실패 카운터를 1 증가시키고, threshold(3회)를 넘었는지 확인합니다.
넘었다면 상태를 OPEN으로 바꿉니다. "연속 3번 실패했으니 회로를 차단합니다." 실무에서는 어떻게 쓸까요? 넷플릭스의 Hystrix 라이브러리가 유명합니다.
넷플릭스는 수백 개의 마이크로서비스로 구성되어 있는데, 하나의 서비스가 느려지면 연쇄적으로 전체 시스템이 느려질 수 있습니다. 서킷 브레이커를 적용하면, 느린 서비스를 빠르게 차단하고 폴백으로 전환합니다.
예를 들어 추천 서비스가 죽으면 "추천을 불러올 수 없습니다"라는 메시지를 보여주거나, 캐시된 추천 목록을 보여줍니다. 핵심 기능인 비디오 재생은 계속 작동합니다.
쿠팡도 비슷한 전략을 씁니다. 결제 서비스는 절대 차단하면 안 되지만, 리뷰 조회 서비스는 차단해도 괜찮습니다.
서비스마다 다른 임계값과 타임아웃을 설정합니다. 김개발 씨의 구현 김개발 씨는 각 모델마다 서킷 브레이커를 만들었습니다.
const sonnetBreaker = new CircuitBreaker(3, 60000); const haikuBreaker = new CircuitBreaker(3, 60000); 그리고 폴백 로직을 수정했습니다. try { return await sonnetBreaker.execute(() => invokeSonnet(payload)); } catch { return await haikuBreaker.execute(() => invokeHaiku(payload)); } 효과는 즉각적이었습니다.
Sonnet이 3번 실패하면 서킷이 열립니다. 이후 요청은 0.001초 만에 즉시 Haiku로 넘어갑니다.
평균 응답 시간이 5초에서 1초로 줄었습니다. 60초 후 서킷이 HALF_OPEN으로 바뀌어 자동으로 Sonnet을 다시 시도합니다.
Sonnet이 복구됐다면 자동으로 정상화됩니다. 사람이 개입할 필요가 없습니다.
주의사항 임계값을 너무 낮게 설정하면 작은 문제에도 바로 차단됩니다. 네트워크가 순간적으로 느려진 것뿐인데 서킷이 열리면 곤란합니다.
보통 3~5회가 적당합니다. 타임아웃을 너무 짧게 설정하면 복구할 시간이 없습니다.
AWS가 서버를 재시작하는 데 1분 정도 걸리는데, 타임아웃이 10초라면 계속 실패만 반복합니다. 60초 정도가 적당합니다.
서킷 브레이커는 분산 시스템의 필수 패턴입니다. 한 서비스의 장애가 전체 시스템으로 번지는 것을 막아줍니다.
빠른 실패(Fail Fast)를 통해 사용자 경험도 개선됩니다.
실전 팁
💡 - 서킷 브레이커 상태 변화를 로그와 메트릭으로 추적하세요
- 중요한 서비스는 임계값을 높게, 부가 서비스는 낮게 설정하세요
- HALF_OPEN 상태에서는 한 번만 시도하고 결과에 따라 즉시 판단하세요
6. 사용자 친화적 에러 메시지
모든 에러 처리 로직을 완벽하게 구현한 김개발 씨는 뿌듯했습니다. 그런데 QA팀에서 리포트가 왔습니다.
"에러 메시지가 너무 기술적이에요. 일반 사용자는 뭔 소린지 모르겠대요." 박시니어 씨가 웃으며 말했습니다.
"마지막 퍼즐이 남았네요."
사용자 친화적 에러 메시지는 기술적인 에러를 사용자가 이해할 수 있는 언어로 번역하는 것입니다. "ThrottlingException"이 아니라 "지금 사용자가 많아 잠시 대기 중입니다"라고 표현합니다.
개발자용 상세 로그는 따로 기록하고, 사용자에게는 간결하고 실행 가능한 안내를 제공합니다.
다음 코드를 살펴봅시다.
// 에러를 사용자 친화적 메시지로 변환
interface UserFriendlyError {
userMessage: string; // 사용자에게 보여줄 메시지
retryable: boolean; // 재시도 가능 여부
errorCode: string; // 개발자용 에러 코드
technicalDetail?: string; // 기술 상세 정보 (로그용)
}
function translateError(error: any): UserFriendlyError {
// ThrottlingException 처리
if (error.name === 'ThrottlingException') {
return {
userMessage: '지금 사용자가 많습니다. 잠시 후 다시 시도해주세요.',
retryable: true,
errorCode: 'RATE_LIMIT',
technicalDetail: error.message
};
}
// ValidationException 처리
if (error.name === 'ValidationException') {
return {
userMessage: '입력 내용을 다시 확인해주세요.',
retryable: false,
errorCode: 'INVALID_INPUT',
technicalDetail: error.message
};
}
// ModelNotReadyException 처리
if (error.name === 'ModelNotReadyException') {
return {
userMessage: 'AI 서비스가 일시적으로 사용할 수 없습니다. 잠시 후 다시 시도해주세요.',
retryable: true,
errorCode: 'SERVICE_UNAVAILABLE',
technicalDetail: error.message
};
}
// 기타 에러
return {
userMessage: '일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
retryable: true,
errorCode: 'UNKNOWN_ERROR',
technicalDetail: error.message || String(error)
};
}
김개발 씨의 서비스는 이제 안정적으로 작동했습니다. 에러가 발생해도 재시도하고, 폴백하고, 서킷 브레이커로 보호됐습니다.
하지만 QA 담당자인 이테스터 씨가 불만을 제기했습니다. "사용자가 에러 화면을 캡처해서 보냈는데요, 뭔 소린지 하나도 모르겠어요.
'ThrottlingException: Rate exceeded for model anthropic.claude-3-5-sonnet'이라는데, 이게 뭔가요?" 김개발 씨는 당황했습니다. "그건...
AWS에서 요청이 너무 많아서..." 이테스터 씨가 고개를 저었습니다. "사용자는 AWS가 뭔지도 몰라요.
그냥 '안 된다'는 것만 알아요." 사용자 친화적 메시지란 무엇일까요? 마치 의사가 환자에게 설명하는 것과 같습니다. 전문 용어 대신 쉬운 말로 바꿔줍니다.
"상부 소화관 점막 염증"이 아니라 "위염"이라고 말합니다. "H.
pylori 박테리아 감염"이 아니라 "세균 때문에 속이 안 좋으신 겁니다"라고 설명합니다. 개발자에게는 "ThrottlingException"이 명확한 정보입니다.
하지만 일반 사용자에게는 외계어나 다름없습니다. "지금 사용자가 많아서 잠시 기다려야 합니다"라고 번역해줘야 합니다.
좋은 에러 메시지의 조건 첫째, 이해 가능해야 합니다. 전문 용어를 피하고 일상 언어를 사용합니다.
"API 호출 제한 초과"가 아니라 "사용자가 많습니다"라고 표현합니다. 둘째, 실행 가능해야 합니다.
사용자가 무엇을 해야 하는지 명확히 알려줍니다. "잠시 후 다시 시도해주세요", "입력 내용을 확인해주세요"처럼 구체적인 조치를 안내합니다.
셋째, 공감적이어야 합니다. "에러 발생"이 아니라 "일시적인 문제가 발생했습니다"라고 표현하면 사용자가 덜 불안해합니다.
"죄송합니다"라는 한마디도 큰 차이를 만듭니다. 코드를 살펴봅시다 translateError 함수는 AWS 에러 객체를 받아서 UserFriendlyError 객체로 변환합니다.
이 객체에는 네 가지 정보가 담깁니다. userMessage는 사용자에게 직접 보여줄 메시지입니다.
한글로, 쉬운 말로 작성합니다. "지금 사용자가 많습니다"처럼 상황을 설명하고, "잠시 후 다시 시도해주세요"처럼 조치를 안내합니다.
retryable은 재시도 가능 여부입니다. true면 "다시 시도" 버튼을 보여줄 수 있고, false면 "입력을 수정해주세요"같은 다른 안내를 제공합니다.
errorCode는 개발자용 코드입니다. 로그를 분석할 때 유용합니다.
"RATE_LIMIT"을 검색하면 ThrottlingException 관련 에러를 한 번에 찾을 수 있습니다. technicalDetail은 원본 에러 메시지입니다.
사용자에게는 보여주지 않지만, 개발자 로그에는 기록합니다. 디버깅할 때 필요합니다.
각 에러 유형별 처리 ThrottlingException은 "사용자가 많다"는 메시지로 번역합니다. 재시도 가능하므로 retryable은 true입니다.
사용자는 "아, 지금 바쁜가 보다. 조금 있다가 다시 해봐야지"라고 이해합니다.
ValidationException은 "입력 내용 확인"을 요청합니다. 재시도해도 소용없으므로 retryable은 false입니다.
사용자가 뭔가 잘못 입력했다는 것을 알려줍니다. ModelNotReadyException은 "서비스 일시 중단"으로 표현합니다.
AWS 내부 문제이므로 사용자가 할 수 있는 일은 없습니다. 기다리라고 안내합니다.
기타 에러는 포괄적인 메시지로 처리합니다. "일시적인 오류"라고 표현하여 사용자를 안심시킵니다.
대부분의 에러는 재시도로 해결되므로 retryable을 true로 설정합니다. UI에서는 어떻게 표시할까요? 프론트엔드에서는 translateError의 결과를 받아서 적절한 UI를 그립니다.
const friendlyError = translateError(error); if (friendlyError.retryable) { showMessage(friendlyError.userMessage, { action: '다시 시도', onClick: retry }); } else { showMessage(friendlyError.userMessage, { action: '확인', onClick: close }); } // 개발자 로그 console.error(`[${friendlyError.errorCode}] ${friendlyError.technicalDetail}`); 사용자에게는 친절한 메시지와 "다시 시도" 버튼을 보여줍니다. 개발자 콘솔에는 상세한 기술 정보를 로깅합니다.
두 가지 목적을 모두 달성하는 것입니다. 실무 사례 구글은 "404 Not Found" 페이지를 재미있게 꾸밉니다.
공룡 게임이 나와서 사용자의 불만을 달래줍니다. 슬랙은 "뭔가 잘못됐습니다"라는 메시지와 함께 귀여운 일러스트를 보여줍니다.
토스는 에러 메시지에 이모지를 활용합니다. "잠시만요" 같은 친근한 표현을 쓰고, 문제가 해결되면 "성공했어요!"라며 축하해줍니다.
에러조차 사용자 경험의 일부로 만든 것입니다. 깃헙은 기술적인 정보도 제공합니다.
일반 사용자에게는 간단한 메시지를 보여주지만, "상세 정보 보기"를 클릭하면 개발자용 로그를 볼 수 있습니다. 양쪽 사용자를 모두 만족시키는 전략입니다.
주의할 점 너무 모호한 메시지는 오히려 혼란을 줍니다. "오류가 발생했습니다"만 반복하면 사용자는 뭐가 문제인지 알 수 없습니다.
최소한 "네트워크 문제", "서버 문제", "입력 오류" 정도는 구분해서 알려줘야 합니다. 거짓말도 금물입니다.
"잠시 후 다시 시도하세요"라고 했는데, 사실은 서비스가 완전히 죽었다면? 사용자는 몇 시간을 기다리다가 포기합니다.
심각한 장애라면 솔직히 알려주는 게 낫습니다. 다국어 지원도 고려해야 합니다.
한국어, 영어, 일본어... 각 언어마다 적절한 표현이 다릅니다.
번역 테이블을 관리하고, 언어별로 다른 메시지를 제공해야 합니다. 김개발 씨의 최종 완성 김개발 씨는 모든 에러 처리 코드에 translateError를 적용했습니다.
프론트엔드에서는 친절한 메시지를 보여주고, 백엔드 로그에는 기술 상세를 기록했습니다. QA팀의 피드백이 왔습니다.
"이제 에러 메시지가 자연스러워요. 사용자들도 이해할 수 있을 것 같아요." 김개발 씨는 뿌듯했습니다.
서비스를 론칭한 후 일주일 동안 모니터링했습니다. 에러 발생률은 1% 정도였지만, 사용자들의 재시도율이 높았습니다.
"다시 시도"를 눌러서 대부분 성공했습니다. 친절한 메시지 덕분에 사용자들이 포기하지 않은 것입니다.
고객 지원팀도 고마워했습니다. "예전에는 '에러 났어요'라는 문의만 잔뜩 왔는데, 이제는 사용자들이 스스로 해결하거나 구체적인 문제를 알려줘요." 에러 메시지는 서비스의 얼굴입니다.
모든 게 완벽할 수는 없지만, 문제가 생겼을 때 어떻게 대응하느냐가 서비스의 품격을 결정합니다. 사용자를 존중하고 배려하는 에러 메시지를 만드세요.
실전 팁
💡 - 에러 메시지도 A/B 테스트하여 어떤 표현이 효과적인지 검증하세요
- 자주 발생하는 에러는 FAQ나 도움말 링크를 함께 제공하세요
- 에러 메시지에 추적 ID를 포함시켜 고객 지원 시 빠르게 찾을 수 있게 하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
UX와 협업 패턴 완벽 가이드
AI 에이전트와 사용자 간의 효과적인 협업을 위한 UX 패턴을 다룹니다. 프롬프트 핸드오프부터 인터럽트 처리까지, 현대적인 에이전트 시스템 설계의 핵심을 배웁니다.
자가 치유 및 재시도 패턴 완벽 가이드
AI 에이전트와 분산 시스템에서 필수적인 자가 치유 패턴을 다룹니다. 에러 감지부터 서킷 브레이커까지, 시스템을 스스로 복구하는 탄력적인 코드 작성법을 배워봅니다.
Feedback Loops 컴파일러와 CI/CD 완벽 가이드
컴파일러 피드백 루프부터 CI/CD 파이프라인, 테스트 자동화, 자가 치유 빌드까지 현대 개발 워크플로우의 핵심을 다룹니다. 초급 개발자도 쉽게 이해할 수 있도록 실무 예제와 함께 설명합니다.
실전 MCP 통합 프로젝트 완벽 가이드
Model Context Protocol을 활용한 실전 통합 프로젝트를 처음부터 끝까지 구축하는 방법을 다룹니다. 아키텍처 설계부터 멀티 서버 통합, 모니터링, 배포까지 운영 레벨의 MCP 시스템을 구축하는 노하우를 담았습니다.
MCP 동적 도구 업데이트 완벽 가이드
AI 에이전트의 도구를 런타임에 동적으로 로딩하고 관리하는 방법을 알아봅니다. 플러그인 시스템 설계부터 핫 리로딩, 보안까지 실무에서 바로 적용할 수 있는 내용을 다룹니다.