0701 백엔드 실무 심화 (8/N): 외부 API 연동과 실패 처리 설계
✅ 1. 외부 API 연동이란 무엇인가?
- 외부 API 연동은 우리 서비스가 다른 회사나 외부 시스템의 API를 호출해서 기능을 처리하는 구조입니다.
- 백엔드 실무에서는 문자 발송, 카카오 알림톡, 본인인증, 결제, 주소 검색, 광고 전환 API, 통신사 정책 API, CRM 연동처럼 외부 API를 사용하는 경우가 많습니다.
우리 백엔드 서버
↓
외부 API 호출
↓
외부 서비스 처리
↓
응답 수신
↓
우리 DB에 결과 저장
➕ 1-1. 외부 API 예시
| 구분 | 예시 |
|---|
| 문자/알림 | SMS, LMS, 카카오 알림톡 |
| 인증 | 본인인증, 휴대폰 인증, 이메일 인증 |
| 결제 | 카드 결제, 가상계좌, 환불 |
| 광고 | 네이버/구글/메타 전환 API |
| 파일/검색 | 주소 검색, 지도 API, OCR |
| 업무 연동 | CRM, 상담 시스템, ERP, 통신사 API |
- 외부 API는 우리 서비스 바깥의 시스템이기 때문에 언제든 느려지거나 실패할 수 있습니다.
- 그래서 외부 API 연동은 “성공했을 때”보다 실패했을 때 어떻게 처리할지가 더 중요합니다.
✅ 2. 외부 API 연동이 어려운 이유
➕ 2-1. 우리 마음대로 제어할 수 없다
- 외부 API 서버가 느려질 수 있습니다.
- 외부 API가 점검 중일 수 있습니다.
- 인증키가 만료될 수 있습니다.
- 요청 제한에 걸릴 수 있습니다.
- 문서와 실제 응답이 다를 수 있습니다.
우리 서버는 정상
↓
외부 알림톡 API 장애
↓
상담 신청 API까지 느려지거나 실패할 수 있음
➕ 2-2. 실패 유형이 다양하다
| 실패 유형 | 설명 |
|---|
| Timeout | 응답 시간이 너무 오래 걸림 |
| 400 오류 | 요청 형식이 잘못됨 |
| 401/403 오류 | 인증키 오류, 권한 없음 |
| 429 오류 | 요청 횟수 제한 |
| 500 오류 | 외부 서버 내부 오류 |
| Network Error | 네트워크 연결 실패 |
| 응답 포맷 오류 | 예상과 다른 응답 구조 |
| 부분 성공 | 일부 대상만 성공, 일부 실패 |
- 외부 API는 단순히 성공/실패만 보는 것이 아니라 실패 원인별로 대응이 달라야 합니다.
✅ 3. 외부 API 연동 기본 흐름
➕ 3-1. 동기 호출 흐름
사용자 요청
↓
우리 서버 DB 저장
↓
외부 API 호출
↓
외부 API 응답 대기
↓
결과 DB 저장
↓
사용자 응답
- 결제 승인, 본인인증 확인처럼 즉시 결과가 필요한 작업에 사용합니다.
- 단점은 외부 API가 느리면 사용자 응답도 느려진다는 점입니다.
➕ 3-2. 비동기 호출 흐름
사용자 요청
↓
우리 서버 DB 저장
↓
Queue에 외부 API 작업 등록
↓
사용자에게 빠르게 응답
Worker
↓
Queue에서 작업 처리
↓
외부 API 호출
↓
결과 DB 저장
- 알림톡, SMS, 이메일, 광고 전환 API처럼 즉시 결과가 필요하지 않은 작업에 적합합니다.
- 실패 시 재시도와 이력 관리가 가능합니다.
✅ 4. 동기 처리와 비동기 처리 기준
- 외부 API를 무조건 Queue로 빼는 것이 정답은 아닙니다.
- 사용자가 즉시 결과를 알아야 하는 작업은 동기 처리가 필요합니다.
- 사용자의 핵심 흐름을 막지 않아도 되는 작업은 비동기 처리가 좋습니다.
➕ 4-1. 동기 처리가 적합한 작업
| 작업 | 이유 |
|---|
| 로그인 | 즉시 성공/실패 필요 |
| 본인인증 확인 | 인증 결과가 있어야 다음 단계 진행 |
| 결제 승인 | 결제 성공 여부가 주문 상태에 직접 영향 |
| 쿠폰 사용 검증 | 주문 금액 계산에 즉시 필요 |
| 중복 신청 검증 | 저장 전에 결과 필요 |
➕ 4-2. 비동기 처리가 적합한 작업
| 작업 | 이유 |
|---|
| 신청 완료 알림톡 | 실패해도 신청 저장은 유지 가능 |
| 관리자 알림 | 사용자 응답과 분리 가능 |
| 광고 전환 API | 후속 분석용 |
| 이메일 발송 | 재시도 가능 |
| 대량 문자 발송 | 시간이 오래 걸림 |
| CRM 동기화 | 나중에 재처리 가능 |
핵심 데이터 저장:
동기 처리
부가 알림/전송/연동:
비동기 Queue 처리
✅ 5. Timeout 설정
- 외부 API를 호출할 때 timeout을 반드시 설정해야 합니다.
- timeout이 없으면 외부 API가 응답하지 않을 때 우리 서버 요청도 오래 묶일 수 있습니다.
➕ 5-1. Timeout이 없는 문제
외부 API 응답 없음
↓
우리 서버가 계속 기다림
↓
API 응답 지연
↓
서버 자원 낭비
↓
다른 요청까지 느려짐
➕ 5-2. Timeout 기준 예시
| API 종류 | Timeout 예시 |
|---|
| 인증 확인 | 3초 ~ 5초 |
| 결제 승인 | 5초 ~ 10초 |
| 알림톡/SMS | 3초 ~ 5초 |
| 광고 전환 API | 2초 ~ 3초 |
| 대량 파일 연동 | 별도 비동기 처리 |
- timeout은 외부 API 문서와 실제 응답 속도를 보고 조정해야 합니다.
- 너무 짧으면 정상 요청도 실패할 수 있고, 너무 길면 서버 자원을 오래 잡아먹습니다.
✅ 6. Axios 기반 외부 API Client 설계
- 외부 API 호출 코드를 Service 곳곳에 흩뿌리면 관리가 어려워집니다.
- 외부 API별로 Client 클래스를 만들어서 요청, 응답, 에러 처리를 한곳에 모으는 것이 좋습니다.
➕ 6-1. 좋지 않은 방식
async createConsult(dto: CreateConsultDto) {
const consult = await this.prisma.consult.create({ data: dto });
await axios.post('https://sms-provider.com/send', {
phone: dto.phone,
message: '신청 완료',
});
return consult;
}
- 외부 API 주소, 인증키, 에러 처리, timeout이 비즈니스 로직 안에 섞입니다.
- 나중에 API 제공업체가 바뀌면 여러 코드를 수정해야 합니다.
➕ 6-2. 좋은 방식
@Injectable()
export class SmsClient {
private readonly client: AxiosInstance;
constructor(private readonly configService: ConfigService) {
this.client = axios.create({
baseURL: this.configService.getOrThrow('SMS_API_BASE_URL'),
timeout: 5000,
headers: {
Authorization: `Bearer ${this.configService.getOrThrow('SMS_API_KEY')}`,
},
});
}
async sendSms(params: {
phone: string;
message: string;
}) {
const response = await this.client.post('/messages', {
to: params.phone,
message: params.message,
});
return response.data;
}
}
- 외부 API 설정을 Client에 모아두면 유지보수가 쉬워집니다.
- Service는
smsClient.sendSms()처럼 의도를 중심으로 사용할 수 있습니다.
✅ 7. 외부 API 응답 표준화
- 외부 API마다 응답 형식이 다릅니다.
- 우리 서비스 내부에서는 외부 응답을 그대로 쓰기보다 내부 표준 형태로 변환하는 것이 좋습니다.
➕ 7-1. 외부 응답 예시
{
"result_code": "0000",
"result_message": "success",
"message_id": "abc123"
}
➕ 7-2. 내부 표준 응답 예시
export interface SendMessageResult {
success: boolean;
providerMessageId?: string;
errorCode?: string;
errorMessage?: string;
}
➕ 7-3. 변환 예시
async sendSms(params: SendSmsParams): Promise<SendMessageResult> {
try {
const response = await this.client.post('/messages', {
to: params.phone,
message: params.message,
});
return {
success: response.data.result_code === '0000',
providerMessageId: response.data.message_id,
errorCode: response.data.result_code,
errorMessage: response.data.result_message,
};
} catch (error) {
return {
success: false,
errorCode: 'SMS_REQUEST_FAILED',
errorMessage: '문자 발송 요청에 실패했습니다.',
};
}
}
- 외부 API 응답 구조가 바뀌어도 내부 Service 영향 범위를 줄일 수 있습니다.
- 여러 문자 업체를 바꿔도 내부 응답 구조는 유지할 수 있습니다.
✅ 8. 에러 처리 기준
- 외부 API 에러는 사용자 메시지와 개발자 로그를 분리해야 합니다.
- 사용자에게 외부 API의 내부 에러 메시지를 그대로 보여주면 안 됩니다.
➕ 8-1. 사용자 메시지
좋은 메시지:
알림 발송에 실패했습니다. 잠시 후 다시 시도해 주세요.
나쁜 메시지:
ProviderError 500: Invalid template mapping exception from kakao gateway
➕ 8-2. 서버 로그
[ERROR] provider=KAKAO_ALIMTALK action=send template=CONSULT_DONE errorCode=TIMEOUT consultId=123
- 사용자에게는 이해 가능한 메시지를 보여줍니다.
- 서버 로그에는 원인 분석에 필요한 정보를 남깁니다.
- 단, 인증키, 토큰, 주민등록번호, 전체 전화번호 같은 민감정보는 로그에 남기면 안 됩니다.
✅ 9. 외부 API 실패 이력 테이블
- 외부 API 호출 결과는 DB에 이력으로 남겨두는 것이 좋습니다.
- 특히 알림톡, SMS, 결제, 광고 전환 API는 나중에 운영자가 확인할 수 있어야 합니다.
➕ 9-1. ExternalApiLog 모델 예시
model ExternalApiLog {
id Int @id @default(autoincrement())
provider String
action String
status String
requestId String?
relatedType String?
relatedId Int?
requestPayload Json?
responseBody Json?
errorCode String?
errorMessage String?
attempts Int @default(0)
createdAt DateTime @default(now())
}
➕ 9-2. 주요 필드 설명
| 필드 | 설명 |
|---|
provider | 외부 API 업체 |
action | 수행한 작업 |
status | SUCCESS, FAILED, PENDING |
relatedType | CONSULT, ORDER, PAYMENT 등 |
relatedId | 연관 데이터 ID |
requestPayload | 요청 내용 |
responseBody | 응답 내용 |
errorCode | 실패 코드 |
errorMessage | 실패 메시지 |
attempts | 시도 횟수 |
requestPayload에는 민감정보를 그대로 저장하지 않도록 주의해야 합니다.
- 전화번호는 마스킹하거나 필요한 최소 정보만 저장하는 것이 좋습니다.
✅ 10. 재시도 전략
- 외부 API는 일시적인 장애가 있을 수 있으므로 재시도 전략이 필요합니다.
- 하지만 모든 실패를 무조건 재시도하면 안 됩니다.
➕ 10-1. 재시도할 수 있는 실패
| 실패 | 재시도 여부 |
|---|
| Timeout | 가능 |
| 500 서버 오류 | 가능 |
| 502/503/504 | 가능 |
| 네트워크 오류 | 가능 |
| 429 Rate Limit | 지연 후 가능 |
➕ 10-2. 재시도하면 안 되는 실패
| 실패 | 이유 |
|---|
| 잘못된 전화번호 | 다시 해도 실패 |
| 잘못된 템플릿 코드 | 코드 수정 필요 |
| 인증키 오류 | 설정 수정 필요 |
| 권한 없음 | 계정/권한 수정 필요 |
| 요청 포맷 오류 | 코드 수정 필요 |
일시적 장애:
재시도 가능
요청 자체가 잘못됨:
재시도보다 원인 수정 필요
✅ 11. Exponential Backoff
- Exponential Backoff는 실패할수록 재시도 간격을 점점 늘리는 방식입니다.
- 외부 API 서버에 부담을 주지 않으면서 재시도할 수 있습니다.
➕ 11-1. 예시
1차 실패:
3초 후 재시도
2차 실패:
10초 후 재시도
3차 실패:
30초 후 재시도
최종 실패:
관리자 확인 대상으로 등록
➕ 11-2. BullMQ 예시
await this.notificationQueue.add(
'send-alimtalk',
{
consultId: consult.id,
},
{
attempts: 3,
backoff: {
type: 'exponential',
delay: 3000,
},
},
);
- 재시도 횟수는 반드시 제한해야 합니다.
- 무한 재시도는 외부 API 비용 증가와 서버 부하를 만들 수 있습니다.
✅ 12. 멱등성
- 멱등성(Idempotency)은 같은 요청이 여러 번 실행되어도 결과가 중복되지 않게 만드는 성질입니다.
- 외부 API 연동에서 매우 중요합니다.
➕ 12-1. 왜 필요한가?
결제 승인 API 호출 성공
↓
우리 서버가 응답 저장 전에 장애
↓
재시도
↓
결제가 중복 승인될 수 있음
알림톡 발송 성공
↓
성공 로그 저장 전 Worker 장애
↓
Job 재시도
↓
알림톡이 두 번 발송될 수 있음
- 결제, 포인트, 쿠폰, 알림 발송처럼 중복 실행이 문제 되는 기능은 멱등성을 반드시 고려해야 합니다.
➕ 12-2. 멱등성 키
- Idempotency Key는 같은 요청인지 판단하기 위한 고유한 키입니다.
consult-alimtalk:123:CONSULT_COMPLETE
payment:order-20260701-0001
coupon:user-1:event-202607
- 같은 멱등성 키로 이미 성공한 기록이 있으면 다시 실행하지 않도록 처리할 수 있습니다.
➕ 12-3. 예시
const existing = await this.prisma.externalApiLog.findFirst({
where: {
provider: 'KAKAO',
action: 'SEND_CONSULT_COMPLETE',
relatedType: 'CONSULT',
relatedId: consultId,
status: 'SUCCESS',
},
});
if (existing) {
return {
success: true,
skipped: true,
};
}
- 이미 성공한 작업은 건너뜁니다.
- 중복 발송과 중복 결제를 방지할 수 있습니다.
✅ 13. Circuit Breaker
- Circuit Breaker는 외부 API가 계속 실패할 때 일정 시간 동안 호출을 차단하는 패턴입니다.
- 계속 실패하는 외부 API를 무한히 호출하면 우리 서버도 함께 느려질 수 있습니다.
➕ 13-1. 필요한 상황
외부 API 장애 발생
↓
우리 서버가 계속 호출
↓
timeout 누적
↓
서버 자원 고갈
↓
우리 서비스까지 장애
➕ 13-2. 기본 개념
CLOSED:
정상 호출
OPEN:
실패가 많아져 호출 차단
HALF_OPEN:
일부 요청만 테스트 호출
- 작은 서비스에서는 처음부터 복잡한 Circuit Breaker 라이브러리를 도입하지 않아도 됩니다.
- 대신 실패율이 높은 외부 API 작업을 Queue로 분리하고, timeout과 재시도 제한을 거는 것부터 시작할 수 있습니다.
✅ 14. Rate Limit 대응
- 외부 API는 일정 시간 안에 호출 가능한 횟수 제한이 있을 수 있습니다.
- 429 Too Many Requests가 발생하면 무작정 즉시 재시도하면 안 됩니다.
➕ 14-1. 대응 방법
- 외부 API 문서에서 제한 기준 확인
- Worker 동시 처리 수 제한
- Queue 처리 속도 조절
- 429 발생 시 일정 시간 후 재시도
- 대량 발송은 배치 단위로 분리
- 실패 이력 저장
알림톡 API:
초당 10건 제한
대량 발송:
Worker concurrency를 낮추고 Queue로 순차 처리
✅ 15. 외부 API 설정 관리
- 외부 API 연동에는 base URL, API Key, Secret, 템플릿 코드, Callback URL 같은 설정값이 필요합니다.
- 이런 값은 환경변수나 Secret Manager로 관리해야 합니다.
➕ 15-1. .env 예시
KAKAO_API_BASE_URL=
KAKAO_API_KEY=
KAKAO_SENDER_KEY=
KAKAO_CONSULT_TEMPLATE_CODE=
SMS_API_BASE_URL=
SMS_API_KEY=
PAYMENT_API_BASE_URL=
PAYMENT_SECRET_KEY=
➕ 15-2. 주의할 점
- API Key를 GitHub에 올리면 안 됩니다.
- 프론트엔드 코드에 Secret을 넣으면 안 됩니다.
- 개발/운영 키를 분리해야 합니다.
- 템플릿 코드 변경 시 배포가 필요한지 확인해야 합니다.
- 외부 업체가 제공한 테스트 환경과 운영 환경을 구분해야 합니다.
✅ 16. Callback / Webhook 처리
- 외부 API는 처리 결과를 우리 서버로 다시 알려주는 Callback 또는 Webhook을 제공할 수 있습니다.
- 결제 결과, 본인인증 결과, 문자 발송 결과, 배송 상태 변경에서 자주 사용됩니다.
➕ 16-1. Webhook 흐름
우리 서버가 외부 API 요청
↓
외부 API가 비동기 처리
↓
외부 API가 우리 Callback URL 호출
↓
우리 서버가 결과 검증
↓
DB 상태 업데이트
➕ 16-2. Webhook에서 검증할 것
- 실제 외부 API 업체에서 온 요청인가?
- 서명값 또는 Secret 검증이 필요한가?
- 이미 처리한 이벤트는 아닌가?
- 연관 주문/상담/결제 데이터가 존재하는가?
- 상태 변경이 가능한 흐름인가?
- 처리 결과를 이력으로 저장하는가?
주의:
Webhook 요청을 검증하지 않으면
외부에서 임의로 결제 완료/인증 완료 요청을 보낼 수 있음
✅ 17. 외부 API와 트랜잭션
- 외부 API 호출은 DB 트랜잭션처럼 rollback되지 않습니다.
- 따라서 DB 트랜잭션 안에서 외부 API를 호출하는 것은 신중해야 합니다.
➕ 17-1. 좋지 않은 예시
await this.prisma.$transaction(async (tx) => {
const order = await tx.order.create({ data });
await this.paymentClient.approvePayment(paymentKey);
await tx.paymentLog.create({
data: {
orderId: order.id,
status: 'APPROVED',
},
});
});
- 결제 승인 API는 이미 성공했는데, 이후 DB 저장이 실패하면 결제만 되고 주문 데이터가 꼬일 수 있습니다.
- 결제는 취소 API 같은 보상 작업을 고려해야 합니다.
➕ 17-2. 더 안전한 사고방식
1. 주문을 PENDING 상태로 생성
2. 결제 승인 API 호출
3. 결제 성공 시 DB 상태 PAID로 변경
4. DB 저장 실패 시 결제 취소 보상 작업 실행
5. 모든 결과를 이력으로 저장
- 외부 API와 DB 작업은 “완벽한 하나의 트랜잭션”으로 묶을 수 없다는 점을 이해해야 합니다.
- 실패 보상, 이력, 재처리 가능성을 설계해야 합니다.
✅ 18. 실무 체크리스트
➕ 18-1. 외부 API 연동 체크리스트
- 외부 API 호출에 timeout이 설정되어 있는가?
- 외부 API Client가 별도 클래스로 분리되어 있는가?
- API Key와 Secret이 환경변수로 관리되는가?
- 개발/운영 API 주소와 키가 분리되어 있는가?
- 외부 응답을 내부 표준 응답으로 변환하는가?
- 실패 원인별로 재시도 여부를 구분하는가?
- 외부 API 호출 이력을 DB에 남기는가?
- 민감정보가 로그에 남지 않는가?
➕ 18-2. 재시도/Queue 체크리스트
- 즉시 결과가 필요 없는 작업은 Queue로 분리했는가?
- 재시도 횟수에 제한이 있는가?
- Exponential Backoff를 적용했는가?
- 400/401/403 같은 영구 실패를 무한 재시도하지 않는가?
- 429 Rate Limit 대응이 있는가?
- 최종 실패한 작업을 관리자 화면에서 확인할 수 있는가?
- Worker가 죽었을 때 알 수 있는가?
➕ 18-3. 멱등성/보상 체크리스트
- 같은 작업이 두 번 실행되어도 안전한가?
- 이미 성공한 알림/결제/쿠폰 지급을 다시 실행하지 않는가?
- Idempotency Key를 설계했는가?
- 결제 성공 후 DB 저장 실패 시 보상 작업이 있는가?
- S3 업로드 후 DB 저장 실패 시 파일 삭제를 고려했는가?
- Webhook 중복 수신을 처리할 수 있는가?
✅ 19. AI를 활용해 외부 API 연동을 설계할 때 질문법
- 외부 API 연동은 성공 코드만 붙이는 것이 아니라 실패, 재시도, 이력, 보안, 멱등성을 함께 설계해야 합니다.
- AI에게 요청할 때는 API의 성격과 실패 시 영향도를 같이 알려주는 것이 좋습니다.
➕ 19-1. 좋은 질문 예시
NestJS + Prisma 서비스에서 카카오 알림톡 발송 API를 연동하려고 해.
상황:
1. 상담 신청이 저장되면 신청 완료 알림톡을 보내야 함
2. 상담 신청 저장 자체는 알림톡 실패와 무관하게 성공 처리되어야 함
3. 알림톡 API는 timeout이나 500 오류가 가끔 발생할 수 있음
4. 실패 시 최대 3번 재시도하고 싶음
5. 이미 성공한 알림톡은 중복 발송되면 안 됨
6. 실패한 발송은 관리자 페이지에서 확인하고 재발송할 수 있게 하고 싶음
7. 전화번호와 API Key는 로그에 노출되면 안 됨
8. Redis + BullMQ Queue 사용을 고려 중
요청:
- 동기/비동기 처리 기준
- 외부 API Client 구조
- Queue Job 설계
- 실패 이력 테이블
- 재시도 전략
- 멱등성 처리
- 관리자 재발송 흐름
- 로그 보안 기준
을 실무 기준으로 설명해줘.
➕ 19-2. AI 답변 검증 기준
- 상담 신청 저장과 알림톡 발송을 분리하는가?
- 외부 API timeout을 설정하라고 하는가?
- 실패 원인별 재시도 가능 여부를 구분하는가?
- 이미 성공한 알림을 중복 발송하지 않도록 멱등성을 설명하는가?
- API Key와 전화번호를 로그에 남기지 말라고 하는가?
- 실패 이력과 관리자 재발송 흐름을 제안하는가?
- 400/401 같은 영구 실패를 무한 재시도하지 않게 하는가?
- 외부 API 호출을 DB 트랜잭션처럼 rollback할 수 없다고 설명하는가?
📌 요약
- 외부 API 연동은 문자, 알림톡, 결제, 본인인증, 광고 전환 API처럼 우리 서비스가 외부 시스템을 호출하는 구조입니다.
- 외부 API는 우리 통제 밖에 있기 때문에 timeout, 실패 처리, 재시도, 이력 관리가 필수입니다.
- 즉시 결과가 필요한 결제/본인인증은 동기 처리가 필요하고, 알림톡/SMS/이메일/광고 전환 API는 Queue를 이용한 비동기 처리가 적합합니다.
- 외부 API 호출 코드는 별도 Client 클래스로 분리하고, 외부 응답은 내부 표준 응답으로 변환하는 것이 유지보수에 좋습니다.
- 실패 원인에 따라 재시도 여부를 구분해야 하며, timeout/500/네트워크 오류는 재시도 가능하지만 요청 형식 오류나 인증키 오류는 먼저 원인 수정이 필요합니다.
- Queue 재시도에는 횟수 제한과 Exponential Backoff를 적용해야 하며, 무한 재시도는 피해야 합니다.
- 결제, 알림, 쿠폰 지급처럼 중복 실행이 위험한 작업은 멱등성 키와 성공 이력 확인이 필요합니다.
- Webhook/Callback은 서명 검증, 중복 처리, 상태 전이 검증을 반드시 고려해야 합니다.
- 외부 API 호출은 DB 트랜잭션처럼 자동 rollback되지 않으므로 실패 보상 작업과 운영 이력 관리가 중요합니다.