TIL - 20260828

juni·2026년 8월 28일

TIL

목록 보기
442/468

0828 백엔드 아키텍처 고도화 (5/N): Webhook, 외부 API 연동과 재시도 전략


✅ 1. 외부 API 연동이란 무엇인가?

  • 외부 API 연동은 우리 백엔드가 외부 서비스와 데이터를 주고받는 구조입니다.
  • 알림톡, SMS, 결제, 본인인증, 주소 검색, CRM, 통신사 내부 시스템, 광고 전환 API, Notion, Slack, S3 같은 서비스가 모두 외부 연동 대상이 될 수 있습니다.
  • 외부 API는 우리 코드 밖에 있기 때문에 언제든 느려지거나 실패할 수 있습니다.
우리 백엔드
  ↓
외부 API 호출
  ↓
외부 서비스 응답
  ↓
결과 저장

➕ 1-1. 외부 API 연동이 어려운 이유

외부 서비스 장애 가능
네트워크 timeout 가능
응답 형식 변경 가능
rate limit 가능
인증 토큰 만료 가능
중복 요청 가능
성공했는데 응답만 실패할 가능성
운영 로그와 추적이 필요함
  • 외부 API는 성공할 때보다 실패할 때 설계가 중요합니다.
  • “호출하면 되겠지” 수준으로 붙이면 운영에서 반드시 문제가 생깁니다.

✅ 2. Webhook이란 무엇인가?

  • Webhook은 외부 서비스가 우리 서버로 이벤트를 알려주는 방식입니다.
  • 우리가 계속 외부 API를 조회하는 것이 아니라, 외부 시스템이 특정 일이 생겼을 때 우리 API로 요청을 보냅니다.
외부 서비스에서 이벤트 발생
  ↓
우리 서버의 Webhook URL 호출
  ↓
우리 서버가 이벤트 저장/처리

➕ 2-1. Webhook 예시

결제 완료 알림
본인인증 완료 알림
배송 상태 변경 알림
알림톡 발송 결과 알림
통신사 개통 상태 변경 알림
광고 전환 이벤트 수신
외부 CRM 상담 상태 변경 수신

➕ 2-2. Webhook과 일반 API 호출 차이

구분일반 외부 API 호출Webhook
요청 주체우리 서버외부 서비스
방향우리 → 외부외부 → 우리
예시알림톡 발송 요청알림톡 발송 결과 수신
핵심요청/응답 처리수신/검증/중복 처리
  • Webhook은 “외부에서 들어오는 요청”입니다.
  • 따라서 인증, 검증, 중복 처리, 재시도 대응이 중요합니다.

✅ 3. 외부 API 연동의 기본 원칙

1. 외부 API 호출은 Adapter로 분리한다
2. transaction 안에서 외부 API를 직접 호출하지 않는다
3. timeout을 반드시 설정한다
4. 실패 결과를 DB에 기록한다
5. 재시도 가능/불가능 오류를 구분한다
6. 중복 요청과 중복 응답을 고려한다
7. Secret과 개인정보를 로그에 남기지 않는다
8. requestId/providerMessageId로 추적 가능하게 만든다

➕ 3-1. 특히 중요한 원칙

DB 정합성과 외부 API 호출을 분리해야 함
  • DB transaction은 외부 API 호출을 rollback할 수 없습니다.
  • 그래서 외부 API 호출은 Job/Worker 또는 Outbox 방식으로 분리하는 것이 안전합니다.

✅ 4. Adapter Pattern

  • Adapter는 외부 API 호출을 감싸는 계층입니다.
  • Use Case나 Worker가 외부 API의 URL, 인증 헤더, 응답 형식에 직접 의존하지 않도록 분리합니다.
Worker / Use Case
  ↓
AlimtalkAdapter
  ↓
외부 알림톡 API

➕ 4-1. 나쁜 예시

await axios.post('https://alimtalk.example.com/send', {
  phone,
  templateCode,
  variables,
}, {
  headers: {
    Authorization: `Bearer ${process.env.ALIMTALK_API_KEY}`,
  },
});

문제:

외부 API 주소가 업무 로직에 직접 노출
인증 방식이 여러 곳에 중복
timeout 설정 누락 가능
응답/에러 처리 중복
테스트 어려움

➕ 4-2. 좋은 예시

@Injectable()
export class AlimtalkAdapter {
  constructor(private readonly httpService: HttpService) {}

  async sendTemplate(params: {
    recipient: string;
    templateCode: string;
    variables: Record<string, string>;
    idempotencyKey?: string;
  }): Promise<AlimtalkSendResult> {
    const response = await firstValueFrom(
      this.httpService.post(
        '/messages/alimtalk',
        {
          phone: params.recipient,
          templateCode: params.templateCode,
          variables: params.variables,
        },
        {
          timeout: 5000,
          headers: {
            'Idempotency-Key': params.idempotencyKey,
          },
        },
      ),
    );

    return {
      providerMessageId: response.data.messageId,
      status: response.data.status,
    };
  }
}

➕ 4-3. Adapter의 역할

외부 URL 관리
요청 body 변환
인증 헤더 구성
timeout 설정
응답 body 변환
외부 에러를 내부 에러 형태로 변환
  • Adapter는 외부 서비스의 언어를 우리 시스템의 언어로 바꿔주는 번역기 역할입니다.
  • 외부 API가 바뀌어도 Adapter만 수정하면 되게 만드는 것이 목표입니다.

✅ 5. 외부 API 호출을 transaction 안에서 하면 안 되는 이유

  • transaction 안에서 외부 API를 호출하면 DB lock이 길어지고, rollback 불가능한 외부 효과가 생깁니다.
Transaction 시작
  ↓
consult 저장
  ↓
알림톡 API 호출
  ↓
외부 발송 성공
  ↓
audit log 저장 실패
  ↓
DB rollback
  ↓
하지만 알림톡은 이미 발송됨

➕ 5-1. 문제

외부 API는 rollback 불가
응답 지연 시 transaction 길어짐
DB lock 유지 시간이 늘어남
외부 장애가 DB 처리에 영향
성공/실패 상태가 꼬일 수 있음

➕ 5-2. 안전한 구조

Transaction 시작
  ↓
consult 저장
  ↓
notification_jobs 생성
  ↓
commit
  ↓
Worker가 알림톡 API 호출
  ↓
발송 결과 DB 저장
  • transaction 안에는 “발송해야 한다는 기록”만 남깁니다.
  • 실제 외부 API 호출은 Worker가 처리합니다.

✅ 6. Timeout 설정

  • 외부 API 호출에는 반드시 timeout이 있어야 합니다.
  • timeout이 없으면 외부 서비스가 응답하지 않을 때 Worker나 API 요청이 계속 붙잡힐 수 있습니다.

➕ 6-1. 위험한 구조

await axios.post(url, body);

문제:

응답이 없으면 오래 대기
Worker 처리 지연
Job이 PROCESSING에 오래 머무름
연쇄 지연 가능

➕ 6-2. timeout 적용

await axios.post(url, body, {
  timeout: 5000,
});

➕ 6-3. 기준

알림톡/SMS:
3~10초

결제/인증:
5~15초

파일 업로드:
용량에 따라 별도 설정

대량 API:
짧은 timeout + 재시도/분할 처리
  • timeout은 짧게 잡고 재시도하는 편이 안정적입니다.
  • 다만 작업 성격에 따라 기준을 다르게 가져가야 합니다.

✅ 7. 외부 API 에러 분류

  • 외부 API 실패는 모두 같은 실패가 아닙니다.
  • 재시도할 수 있는 오류와 재시도하면 안 되는 오류를 구분해야 합니다.

➕ 7-1. 재시도 가능한 오류

네트워크 timeout
외부 서버 500 오류
일시적인 502/503/504
rate limit
일시적인 연결 실패

➕ 7-2. 재시도하면 안 되는 오류

잘못된 전화번호
잘못된 템플릿 코드
필수 파라미터 누락
인증키 오류
권한 없음
수신 거부
존재하지 않는 대상

➕ 7-3. 내부 에러 타입으로 변환

type ExternalApiErrorKind =
  | 'RETRYABLE'
  | 'NON_RETRYABLE'
  | 'AUTH'
  | 'RATE_LIMIT'
  | 'UNKNOWN';

function classifyAlimtalkError(error: unknown): ExternalApiErrorKind {
  if (isTimeoutError(error)) {
    return 'RETRYABLE';
  }

  if (isRateLimitError(error)) {
    return 'RATE_LIMIT';
  }

  if (isInvalidTemplateError(error)) {
    return 'NON_RETRYABLE';
  }

  if (isAuthError(error)) {
    return 'AUTH';
  }

  return 'UNKNOWN';
}
  • 외부 서비스의 에러 코드를 그대로 서비스 전체에 퍼뜨리면 안 됩니다.
  • Adapter에서 내부 기준으로 변환해주는 것이 좋습니다.

✅ 8. Retry 전략

  • 외부 API는 일시적으로 실패할 수 있으므로 재시도 전략이 필요합니다.
  • 하지만 무한 재시도는 외부 서비스와 우리 서버 모두에 부담을 줍니다.
실패
  ↓
재시도 가능 여부 판단
  ↓
retryCount 증가
  ↓
nextRetryAt 설정
  ↓
일정 시간 후 재처리

➕ 8-1. 재시도 정책 예시

최대 재시도:
3회

간격:
1분 → 5분 → 15분

최종 실패:
FAILED 상태 저장
관리자 화면에 실패 사유 표시

➕ 8-2. Backoff

재시도 횟수가 늘수록 대기 시간을 늘리는 방식
function getNextRetryAt(retryCount: number) {
  const delays = [60, 300, 900];

  const delaySeconds = delays[retryCount] ?? 1800;

  return addSeconds(new Date(), delaySeconds);
}

➕ 8-3. Jitter

재시도 시간이 동시에 몰리지 않게 약간의 랜덤 시간을 추가
  • 재시도는 반드시 제한이 있어야 합니다.
  • 외부 API 장애 중에 모든 Job이 동시에 재시도되면 장애가 더 커질 수 있습니다.

✅ 9. Circuit Breaker 개념

  • Circuit Breaker는 외부 서비스 장애가 계속될 때 호출을 잠시 멈추는 패턴입니다.
  • 계속 실패하는 API를 무한히 호출하지 않고, 일정 시간 차단해 시스템을 보호합니다.
정상 상태:
외부 API 호출

실패 누적:
회로 열림

회로 열림 상태:
일정 시간 호출 차단

시간 경과:
일부 요청으로 회복 여부 확인

➕ 9-1. 필요한 상황

외부 알림톡 API 장애
결제 API 장애
인증 API 장애
외부 CRM API 장애
통신사 API 장애

➕ 9-2. 단순 구현 기준

최근 5분간 실패율 80% 이상
  ↓
10분간 호출 중단
  ↓
Job은 RETRYING 상태로 대기
  ↓
이후 다시 시도

➕ 9-3. 지금 단계 기준

초기:
retry/backoff + 실패 로그로 충분

장애가 반복됨:
circuit breaker 검토

외부 API 의존도가 높아짐:
도입 가치 증가
  • 지금 바로 복잡한 circuit breaker를 만들 필요는 없습니다.
  • 하지만 외부 API 장애가 우리 서비스 전체 장애로 번지지 않게 해야 한다는 개념은 중요합니다.

✅ 10. Idempotency

  • Idempotency는 같은 요청이 여러 번 처리돼도 결과가 한 번 처리된 것과 같게 만드는 성질입니다.
  • 외부 API 연동에서는 특히 중요합니다.
같은 알림 발송 요청이 2번 실행
  ↓
고객에게 알림이 2번 가면 안 됨

➕ 10-1. 필요한 상황

알림톡 발송
결제 승인/취소
Webhook 이벤트 처리
상담 신청 중복 클릭
외부 API timeout 후 재시도
Worker 재시작 후 Job 재처리

➕ 10-2. Idempotency Key

고유한 요청 key 생성
  ↓
이미 처리된 key인지 확인
  ↓
처리됨이면 기존 결과 반환
  ↓
처리 전이면 실행

➕ 10-3. 예시

알림톡:
notificationJobId를 idempotency key로 사용

Webhook:
providerEventId를 idempotency key로 사용

결제:
paymentId + eventType 사용
  • 외부 서비스가 idempotency key를 지원하면 적극적으로 사용하는 것이 좋습니다.
  • 지원하지 않더라도 우리 DB에서 중복 처리 방어를 해야 합니다.

✅ 11. Webhook 수신 구조

  • Webhook은 외부에서 우리 서버로 요청이 들어오는 구조입니다.
  • 중요한 것은 빠르게 수신하고, 검증하고, 중복을 막고, 실제 처리는 비동기로 넘기는 것입니다.
외부 Webhook 요청
  ↓
서명 검증
  ↓
eventId 중복 확인
  ↓
webhook_events 저장
  ↓
빠르게 200 응답
  ↓
Worker가 실제 처리

➕ 11-1. 왜 빠르게 응답해야 할까?

외부 서비스가 timeout으로 판단할 수 있음
같은 Webhook을 반복 재전송할 수 있음
긴 처리 중 실패하면 원인 추적 어려움

➕ 11-2. Webhook Controller 역할

raw body 수신
signature header 추출
서명 검증
eventId 추출
WebhookEvent 저장 Use Case 호출
200 응답
  • Webhook Controller에서 모든 비즈니스 처리를 끝내려 하지 않는 것이 좋습니다.
  • 수신과 실제 처리를 분리해야 안정적입니다.

✅ 12. WebhookEvent 테이블 설계

  • Webhook은 중복 수신될 수 있습니다.
  • 수신 이벤트를 저장하고 처리 상태를 관리하는 테이블이 필요합니다.
model WebhookEvent {
  id                Int              @id @default(autoincrement())
  provider          String
  providerEventId   String
  eventType         String
  status            WebhookEventStatus @default(RECEIVED)

  payload           Json
  headers           Json?
  signatureValid    Boolean          @default(false)

  processedAt       DateTime?
  errorCode         String?
  errorMessage      String?
  retryCount        Int              @default(0)
  nextRetryAt       DateTime?

  receivedAt        DateTime         @default(now())
  createdAt         DateTime         @default(now())
  updatedAt         DateTime         @updatedAt

  @@unique([provider, providerEventId])
  @@index([status, nextRetryAt])
  @@index([eventType, receivedAt])
}

enum WebhookEventStatus {
  RECEIVED
  PROCESSING
  PROCESSED
  FAILED
  IGNORED
  RETRYING
}

➕ 12-1. 컬럼 의미

컬럼의미
providerWebhook 제공자
providerEventId외부 이벤트 고유 ID
eventType이벤트 종류
status처리 상태
payload원본 또는 필요한 payload
signatureValid서명 검증 여부
retryCount재시도 횟수
nextRetryAt다음 재시도 시각

➕ 12-2. 설계 포인트

provider + providerEventId unique
중복 Webhook 방지
처리 상태 추적
실패 사유 저장
비동기 재처리 가능
  • Webhook은 중복 수신을 정상 상황으로 봐야 합니다.
  • unique constraint로 최종 방어선을 만들어야 합니다.

✅ 13. Webhook 서명 검증

  • Webhook은 외부에서 들어오는 요청이므로 진짜 외부 서비스가 보낸 것인지 확인해야 합니다.
  • 보통 signature header와 raw body를 이용해 검증합니다.
외부 서비스:
payload + secret으로 signature 생성

우리 서버:
같은 방식으로 signature 계산
  ↓
header signature와 비교

➕ 13-1. 검증이 필요한 이유

가짜 요청 방지
임의 상태 변경 방지
개통/결제/알림 결과 위조 방지
보안 사고 예방

➕ 13-2. 예시 코드

function verifyWebhookSignature(params: {
  rawBody: string;
  signature: string;
  secret: string;
}) {
  const expected = crypto
    .createHmac('sha256', params.secret)
    .update(params.rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(params.signature),
  );
}

➕ 13-3. 주의

raw body가 필요할 수 있음
JSON parse 후 body로 검증하면 실패할 수 있음
secret은 SSM/환경변수에서 관리
signature mismatch 로그에 secret 출력 금지
timingSafeEqual 사용 고려
  • Webhook 검증은 보안상 매우 중요합니다.
  • 특히 상태 변경, 결제, 개통 관련 Webhook은 검증 없이 처리하면 안 됩니다.

✅ 14. Webhook 중복 처리

  • 외부 서비스는 같은 Webhook을 여러 번 보낼 수 있습니다.
  • 우리 서버가 200 응답을 못 했거나, 외부 서비스가 안정성을 위해 재전송하는 경우가 있습니다.
Webhook eventId = abc123 수신
  ↓
처리 완료
  ↓
외부 서비스가 같은 eventId 다시 전송
  ↓
중복 처리되면 문제

➕ 14-1. 대응

try {
  await this.webhookEventRepository.create({
    provider: 'ALIMTALK',
    providerEventId: event.id,
    eventType: event.type,
    payload: event.payload,
    signatureValid: true,
  });
} catch (error) {
  if (isUniqueViolation(error)) {
    return {
      duplicated: true,
    };
  }

  throw error;
}

➕ 14-2. 기준

providerEventId로 unique 처리
이미 처리된 이벤트는 200 반환
중복 수신 로그는 남기되 비즈니스 처리 중복 금지
  • 중복 Webhook에 에러를 반환하면 외부 서비스가 계속 재전송할 수 있습니다.
  • 이미 처리한 이벤트라면 보통 200으로 응답하는 것이 좋습니다.

✅ 15. Webhook 실제 처리 Worker

  • Webhook Controller는 이벤트를 저장하고 빠르게 응답합니다.
  • 실제 비즈니스 반영은 Worker 또는 Handler에서 처리하는 것이 좋습니다.
webhook_events.RECEIVED
  ↓
Worker claim
  ↓
eventType별 Handler 실행
  ↓
상태 변경/이력/Audit Log 저장
  ↓
PROCESSED

➕ 15-1. Handler 구조

WebhookProcessor
  ├─ AlimtalkDeliveryResultHandler
  ├─ PaymentCompletedHandler
  ├─ CarrierStatusChangedHandler
  └─ CrmLeadUpdatedHandler

➕ 15-2. 예시

@Injectable()
export class WebhookProcessor {
  async process(event: WebhookEvent) {
    switch (event.eventType) {
      case 'ALIMTALK_DELIVERED':
        return this.alimtalkDeliveredHandler.handle(event);

      case 'ALIMTALK_FAILED':
        return this.alimtalkFailedHandler.handle(event);

      default:
        return this.webhookEventRepository.markIgnored({
          eventId: event.id,
          reason: '지원하지 않는 이벤트 타입입니다.',
        });
    }
  }
}

➕ 15-3. 장점

Webhook 수신 응답이 빨라짐
처리 실패 시 재시도 가능
이벤트별 처리 로직 분리
외부 재전송과 내부 처리 분리
  • Webhook은 수신과 처리를 분리하는 것이 실무적으로 안전합니다.
  • 특히 상태 변경이 포함된 Webhook은 transaction으로 내부 반영을 관리해야 합니다.

✅ 16. 외부 상태와 내부 상태 매핑

  • 외부 API나 Webhook은 외부 서비스의 상태 코드를 보냅니다.
  • 우리 시스템의 상태 코드와 그대로 같지 않을 수 있으므로 매핑이 필요합니다.

➕ 16-1. 예시

외부 알림톡 상태:
DELIVERED
FAILED
READ
EXPIRED

내부 NotificationJob 상태:
DONE
FAILED

➕ 16-2. 매핑 함수

function mapAlimtalkStatusToInternal(status: string): JobStatus {
  switch (status) {
    case 'DELIVERED':
      return 'DONE';

    case 'FAILED':
    case 'EXPIRED':
      return 'FAILED';

    default:
      return 'PROCESSING';
  }
}

➕ 16-3. 주의

외부 상태를 내부 enum에 그대로 섞지 않기
매핑되지 않은 상태는 IGNORED 또는 UNKNOWN 처리
외부 상태 원본은 필요 시 rawStatus로 저장
상태 변경 이력과 Audit Log 기준 정리
  • 외부 시스템의 상태를 내부 도메인 상태로 바로 사용하면 나중에 통제하기 어려워집니다.
  • Adapter 또는 Handler에서 명확히 매핑해야 합니다.

✅ 17. 외부 API 응답 저장 기준

  • 외부 API 응답 전체를 DB에 그대로 저장하는 것은 위험할 수 있습니다.
  • 개인정보, 토큰, 내부 디버그 정보가 포함될 수 있기 때문입니다.

➕ 17-1. 저장하면 좋은 값

providerMessageId
providerStatus
errorCode
safeErrorMessage
requestedAt
respondedAt
durationMs

➕ 17-2. 조심해야 할 값

전화번호 원본
고객 이름 원본
access token
refresh token
authorization header
provider secret
전체 response body
전체 request body

➕ 17-3. 기준

운영 추적에 필요한 최소값만 저장
원본 응답은 보관 필요성이 있을 때만 제한적으로 저장
민감정보 필터링 후 저장
로그와 DB 모두 같은 보안 기준 적용
  • 외부 API 응답은 장애 분석에 도움이 됩니다.
  • 하지만 전체를 저장하면 보안 리스크가 커집니다.

✅ 18. 외부 API 호출 로그

  • 외부 API 호출은 추적 가능해야 합니다.
  • 어떤 요청이 언제 나갔고, 얼마나 걸렸고, 어떤 결과를 받았는지 알아야 합니다.

➕ 18-1. 로그 필드

requestId
jobId
provider
operation
targetType
targetId
durationMs
status
errorCode
retryCount

➕ 18-2. 로그 예시

{
  "requestId": "req_123",
  "jobId": 10,
  "provider": "ALIMTALK",
  "operation": "SEND_TEMPLATE",
  "targetType": "CONSULT",
  "targetId": "532",
  "durationMs": 842,
  "status": "FAILED",
  "errorCode": "TIMEOUT",
  "retryCount": 1
}

➕ 18-3. 넣지 말아야 할 것

phone 원본
recipientEncrypted
Authorization header
API key
template 변수 중 개인정보
전체 request/response body
  • 로그는 원인 추적용입니다.
  • 고객 개인정보나 Secret 저장소가 되면 안 됩니다.

✅ 19. Rate Limit 대응

  • 외부 API는 일정 시간에 호출 가능한 횟수를 제한할 수 있습니다.
  • 알림톡/SMS, 광고 API, CRM API, 외부 인증 API에서 자주 발생합니다.
외부 API:
1분에 100건까지만 허용

우리 Worker:
1분에 500건 호출

결과:
429 Too Many Requests

➕ 19-1. 대응 방법

Worker concurrency 제한
초당/분당 처리량 제한
429 응답 시 재시도 간격 증가
provider별 rate limit 설정
대량 작업 분할 처리

➕ 19-2. Worker 처리량 제한 예시

const MAX_PER_MINUTE = 60;

// 간단한 구조에서는 interval 또는 batch size를 줄이는 방식으로 시작

➕ 19-3. 기준

외부 API 문서의 제한 확인
provider별 제한값 설정화
429는 재시도 가능 오류로 분류
backoff 적용
  • 외부 API는 우리 마음대로 호출할 수 없습니다.
  • Worker는 항상 외부 서비스의 제한을 존중하도록 설계해야 합니다.

✅ 20. Webhook 보안 체크리스트

➕ 20-1. 필수 보안

서명 검증
timestamp 검증
replay attack 방지
providerEventId unique
허용된 IP 대역 검토
secret 안전 보관
raw body 검증

➕ 20-2. Replay Attack

  • Replay Attack은 누군가 정상 Webhook 요청을 복사해 나중에 다시 보내는 공격입니다.
정상 Webhook 캡처
  ↓
나중에 같은 요청 재전송
  ↓
상태가 다시 변경될 위험

➕ 20-3. 대응

timestamp header 검증
너무 오래된 요청 거부
providerEventId unique로 중복 처리 방지
signature에 timestamp 포함 여부 확인
  • Webhook은 외부에서 들어오는 문입니다.
  • 검증 없이 열어두면 운영 데이터가 임의로 바뀔 수 있습니다.

✅ 21. 외부 API Secret 관리

  • 외부 API 키와 Webhook Secret은 코드에 직접 넣으면 안 됩니다.
  • 환경변수, AWS SSM, Secrets Manager 같은 도구로 관리해야 합니다.

➕ 21-1. 나쁜 예시

const apiKey = 'sk_live_abcdef...';

➕ 21-2. 좋은 기준

코드에 Secret 하드코딩 금지
Git 커밋 금지
SSM/Secrets Manager 사용
권한 최소화
운영/개발 Secret 분리
로그 출력 금지
주기적 rotation 고려

➕ 21-3. 현재 프로젝트 기준

로컬 .env 직접 보관 최소화
AWS SSM 경로로 관리
운영 Secret은 로컬/AI에 제공 금지
외부 API 키는 provider별로 분리
  • 외부 API 연동이 늘어날수록 Secret 관리가 중요해집니다.
  • 키 하나가 유출되면 발송 비용, 개인정보, 운영 데이터가 모두 위험해질 수 있습니다.

✅ 22. 외부 API 장애 대응 Runbook

# Runbook: 외부 API 장애 대응

## 상황
- 알림톡 발송 실패 증가
- 외부 API timeout 증가
- Webhook 수신 실패
- provider 인증 오류
- rate limit 발생

## 즉시 확인
- provider:
- operation:
- 장애 시작 시각:
- 실패 job 수:
- 주요 errorCode:
- retryCount 분포:
- 최근 배포:
- Secret 변경 여부:
- provider 공지 여부:

## 확인 절차
1. Worker 로그 확인
2. 실패 Job 목록 확인
3. errorCode별 분포 확인
4. 외부 API 상태 페이지 또는 공지 확인
5. 인증키/Secret 만료 여부 확인
6. rate limit 여부 확인
7. 재시도 가능한 오류인지 분류
8. 필요 시 Worker 일시 중지
9. circuit breaker 또는 retry 간격 증가
10. 관리자 화면/운영자에게 안내

## 복구 후
- FAILED Job 재처리 여부 결정
- 중복 발송 여부 확인
- 고객 영향 범위 확인
- 장애 기록 작성
- retry/backoff 기준 수정
  • 외부 API 장애는 우리 코드 문제가 아닐 수도 있습니다.
  • 하지만 고객과 운영자 입장에서는 우리 서비스 문제로 보이기 때문에 대응 기준이 필요합니다.

✅ 23. 현재 프로젝트 적용 우선순위

➕ 23-1. 1순위: AlimtalkAdapter 분리

이유:
상담 신청/상태 변경 알림과 연결
외부 API 장애 가능
개인정보 포함 가능
재시도/로그 기준 필요

적용 범위:

AlimtalkAdapter
NotificationWorker
NotificationJob
providerMessageId 저장
errorCode/errorMessage 저장
timeout 설정

➕ 23-2. 2순위: WebhookEvent 테이블

이유:
발송 결과/외부 상태 변경 수신 가능
중복 수신 방지 필요
처리 상태 추적 필요

적용 범위:

WebhookEvent table
Webhook Controller
signature 검증
providerEventId unique
WebhookProcessor

➕ 23-3. 3순위: 외부 API 실패/재시도 기준

이유:
외부 장애가 있을 때 운영 대응 필요
무한 재시도 방지
실패 사유 관리 필요

적용 범위:

retryable/non-retryable 분류
retryCount
nextRetryAt
backoff
FAILED 상태 관리

➕ 23-4. 4순위: Webhook 보안

이유:
외부 요청으로 내부 상태가 바뀔 수 있음
위조 요청 방지 필요

적용 범위:

signature 검증
timestamp 검증
raw body 처리
secret 관리
replay 방지
  • 알림톡 발송부터 안정적으로 분리하고, 이후 발송 결과 Webhook을 받는 구조로 확장하는 것이 현실적입니다.
  • Webhook은 보안 검증 없이 열면 안 됩니다.

✅ 24. AI/Codex에게 외부 API/Webhook 작업을 맡길 때 규칙

➕ 24-1. Codex 요청 예시: AlimtalkAdapter

알림톡 외부 API 호출을 AlimtalkAdapter로 분리해줘.

조건:
1. NotificationWorker에서 직접 axios/http 호출하지 말고 AlimtalkAdapter를 사용하게 해줘
2. Adapter에서 timeout을 설정해줘
3. 외부 API 요청/응답을 내부 타입으로 변환해줘
4. providerMessageId, providerStatus, errorCode를 반환하게 해줘
5. 외부 API 실패는 retryable/non-retryable/auth/rate_limit으로 분류해줘
6. 전화번호 원본, API Key, Authorization header는 로그에 남기지 마
7. API Key는 환경변수/SSM에서 주입받는 ConfigService로 처리해줘
8. Worker는 Adapter 결과에 따라 NotificationJob을 DONE/FAILED/RETRYING으로 업데이트하게 해줘
9. 테스트 가능한 구조로 만들어줘
10. 변경 후 위험 요소와 QA 체크리스트를 정리해줘

➕ 24-2. Codex 요청 예시: Webhook

알림톡 발송 결과 Webhook 수신 구조를 만들어줘.

조건:
1. Webhook Controller는 raw body와 signature header를 받아 검증하게 해줘
2. signature 검증 실패 시 401 또는 400으로 거절해줘
3. provider + providerEventId unique로 중복 수신을 막아줘
4. WebhookEvent 테이블에 payload, eventType, status, receivedAt을 저장해줘
5. 이미 수신한 eventId면 비즈니스 처리를 다시 하지 말고 200을 반환해줘
6. Controller에서 실제 비즈니스 처리까지 하지 말고 WebhookEvent 저장 후 빠르게 응답해줘
7. WebhookProcessor 또는 Worker가 RECEIVED 이벤트를 처리하게 해줘
8. eventType별 handler 구조로 분리해줘
9. payload와 로그에 개인정보/Secret이 남지 않게 해줘
10. 테스트 케이스와 보안 체크리스트를 정리해줘

➕ 24-3. 리뷰 기준

외부 API 호출이 Adapter로 분리되었는가?
timeout이 설정되어 있는가?
transaction 안에서 외부 API를 호출하지 않는가?
retryable/non-retryable 오류가 구분되는가?
Webhook 서명 검증이 있는가?
Webhook 중복 처리가 unique로 방어되는가?
Secret과 개인정보가 로그에 남지 않는가?
Webhook Controller가 빠르게 응답하는가?

✅ 25. 실무 체크리스트

➕ 25-1. 외부 API 연동 체크리스트

  • 외부 API 호출이 Adapter로 분리되어 있는가?
  • timeout이 설정되어 있는가?
  • 외부 API Secret이 코드에 하드코딩되어 있지 않은가?
  • 인증키가 SSM/환경변수로 관리되는가?
  • 외부 API 실패가 내부 에러 타입으로 분류되는가?
  • retryable/non-retryable 오류를 구분하는가?
  • providerMessageId 같은 추적 ID를 저장하는가?
  • 외부 API request/response 전체를 무분별하게 저장하지 않는가?

➕ 25-2. Webhook 체크리스트

  • Webhook URL이 인증/검증 없이 열려 있지 않은가?
  • signature 검증이 있는가?
  • timestamp 검증 또는 replay 방지 기준이 있는가?
  • providerEventId unique가 있는가?
  • 중복 Webhook 수신 시 비즈니스 처리를 다시 하지 않는가?
  • WebhookEvent 테이블에 수신 이력이 저장되는가?
  • Controller는 빠르게 응답하는가?
  • 실제 처리는 Worker/Processor로 분리되어 있는가?

➕ 25-3. Retry/Idempotency 체크리스트

  • maxRetry가 있는가?
  • retryCount가 저장되는가?
  • nextRetryAt이 있는가?
  • backoff가 적용되는가?
  • 재시도하면 안 되는 오류를 구분하는가?
  • idempotency key를 사용하는가?
  • 중복 발송/중복 처리 방어가 있는가?
  • 최종 실패 상태가 관리자 화면에 표시되는가?

➕ 25-4. 보안/로그 체크리스트

  • 전화번호 원본이 로그에 남지 않는가?
  • Authorization header가 로그에 남지 않는가?
  • API Key/Secret이 DB에 저장되지 않는가?
  • Webhook payload에 개인정보가 있을 경우 보존 기준이 있는가?
  • 외부 API 응답 전체를 저장하지 않는가?
  • requestId/jobId/providerMessageId로 추적 가능한가?
  • 장애 분석에 필요한 errorCode가 남는가?
  • 로그 보존 기간과 접근 권한이 정해져 있는가?

✅ 26. AI에게 Webhook/외부 API 연동을 물어볼 때 좋은 질문법

NestJS + Prisma + PostgreSQL 기반 온라인 휴대폰 판매몰에서 알림톡 외부 API와 Webhook 수신 구조를 설계하려고 해.

서비스 상황:
1. 고객 상담 신청 후 알림톡을 발송해야 함
2. 알림톡 발송은 NotificationJob + Worker에서 처리함
3. 외부 API timeout, rate limit, 인증 오류, 템플릿 오류를 구분하고 싶음
4. providerMessageId, errorCode, retryCount, nextRetryAt을 저장하고 싶음
5. 외부 API 호출은 Adapter로 분리하고 싶음
6. 외부 API Secret은 AWS SSM 또는 환경변수로 관리해야 함
7. 알림톡 발송 결과 Webhook을 받을 수 있음
8. Webhook은 signature 검증, providerEventId unique, 중복 처리 방지가 필요함
9. Controller에서는 WebhookEvent만 저장하고 빠르게 200 응답하고 싶음
10. 실제 이벤트 처리는 Worker/Processor가 담당하게 하고 싶음
11. 전화번호 원본, API Key, Authorization header, Secret은 로그에 남기면 안 됨

요청:
- AlimtalkAdapter 구조
- 외부 API timeout 설정 기준
- retryable/non-retryable 에러 분류
- retry/backoff 설계
- NotificationJob 상태 업데이트 기준
- WebhookEvent 테이블 설계
- Webhook signature 검증 구조
- 중복 Webhook 처리 방식
- Webhook Processor/Handler 구조
- 외부 상태와 내부 상태 매핑
- 로그/보안 체크리스트
- 장애 대응 Runbook
을 실무 기준으로 정리해줘.

➕ 26-1. AI 답변 검증 기준

외부 API 호출을 Adapter로 분리하는가?
timeout과 retry/backoff를 언급하는가?
재시도 가능/불가능 오류를 구분하는가?
transaction 안에서 외부 API를 호출하지 않게 하는가?
Webhook 서명 검증과 raw body 필요성을 설명하는가?
providerEventId unique로 중복 처리를 제안하는가?
Webhook Controller가 실제 처리를 길게 하지 않게 하는가?
Secret과 개인정보 로그 노출을 강하게 경고하는가?
현재 규모에서 과한 이벤트 시스템을 강요하지 않는가?

📌 요약

  • 외부 API 연동은 우리 시스템 밖의 서비스를 호출하는 구조이기 때문에 timeout, 실패 처리, 재시도, 로그, 보안 기준이 반드시 필요합니다.
  • 외부 API 호출은 Use Case나 Worker에 직접 작성하지 말고 AlimtalkAdapter, SmsAdapter, PaymentAdapter 같은 Adapter로 분리하는 것이 좋습니다.
  • DB transaction 안에서 외부 API를 직접 호출하면 rollback 불가능한 외부 효과가 생기고 transaction 시간이 길어지므로 피해야 합니다.
  • 상담 저장과 알림 발송은 분리하고, transaction 안에서는 NotificationJob row만 생성한 뒤 Worker가 실제 외부 API를 호출하는 구조가 안전합니다.
  • 외부 API 호출에는 반드시 timeout을 설정해야 하며, timeout, 5xx, 429 같은 재시도 가능한 오류와 잘못된 템플릿, 인증 오류, 필수값 누락 같은 재시도 불가능 오류를 구분해야 합니다.
  • Retry는 maxRetry, retryCount, nextRetryAt, backoff 기준을 가져야 하며 무한 재시도는 피해야 합니다.
  • Idempotency는 같은 요청이나 Webhook이 여러 번 처리되어도 결과가 한 번 처리된 것처럼 유지하는 기준이며, 알림톡 발송, 결제, Webhook 처리에서 중요합니다.
  • Webhook은 외부 서비스가 우리 서버로 이벤트를 보내는 구조이며, signature 검증, providerEventId unique, 중복 처리, 빠른 200 응답, 비동기 Processor 분리가 핵심입니다.
  • 외부 상태 코드는 내부 상태 코드와 분리하고 Adapter 또는 Handler에서 명확히 매핑해야 합니다.
  • 외부 API request/response, Webhook payload, Worker 로그에는 전화번호 원본, API Key, Authorization header, Secret, 전체 개인정보 body가 남지 않도록 해야 합니다.
  • 현재 프로젝트에서는 먼저 AlimtalkAdapter, NotificationJob, WebhookEvent, retry/backoff, 중복 발송 방지 기준부터 잡는 것이 현실적인 우선순위입니다.

0개의 댓글