본문 바로가기

프로젝트

[기록] lambda, 데이터 수집 중 나타난 429 응답 코드

개요

https://blaxsior-repository.tistory.com/245

 

[HTTP Status] 429 Too Many Requests

https://developer.mozilla.org/ko/docs/Web/HTTP/Status/429 429 Too Many Requests - HTTP | MDN HTTP 429 Too Many Requests 응답 상태 코드는 사용자가 주어진 시간 동안 너무 많은 요청을 보냈음을 나타냅니다("속도 제한"). deve

blaxsior-repository.tistory.com

HTTP 429 응답 코드는 사용자가 일정 시간 동안 너무 많은 요청을 보냈으므로, 요청 전송을 지연 또는 조절하라는 의미를 담고 있다. Retry-After 헤더를 통해 정확히 얼마나 지연해야 할지 알려주는 경우도 있지만, 필수는 아니다.

 현재 진행 중인 졸업 프로젝트는 특정 키워드에 대한 뉴스 기사 및 댓글 목록을 하루 단위로 수집한 후 BERT 모델을 이용하여 분석하는 과정을 거친다. 최근에 lambda, sqs, s3 및 eventbridge을 이용하여 자정마다 데이터를 수집하는 간단한 파이프라인을 구성했고, 테스트를 통해 서버와 연동되는 것까지 확인했다.

https://github.com/blaxsior/lambda-deploy-test

 

GitHub - blaxsior/lambda-deploy-test: AWS lambda에 Git Action 기반으로 배포하는 것을 연습하는 레포지토리

AWS lambda에 Git Action 기반으로 배포하는 것을 연습하는 레포지토리. Contribute to blaxsior/lambda-deploy-test development by creating an account on GitHub.

github.com

대략적인 동작 흐름을 구성한 것. 나중에 이해하기 쉽도록 구체화해야 한다...

 이후 데이터 수집 기능을 담당하는 news_crawling 함수에 대해 간단하게 값을 넣어 테스트하면서 정상적으로 동작하는 것을 확인했다. 가장 메인이 되는 모듈이 어느 정도 정상적으로 동작하는 것 같아 보여서 잠깐 행복을 느꼈다.

실행 후 데이터를 console.log로 출력해 본 모습. 정상적으로 잘 가져온다.


문제점

이후 전체 흐름을 테스트하기 위해, data_divider 람다 함수를 실행하여 전체 파이프라인을 활성화했는데, news_crawling 함수에서 간헐적으로 429 에러가 발생하고 있다는 사실을 알게되었다. 테스트 정보를 살펴보기 위해 cloud watch 로그 그룹을 탐색했더니, 다음과 같은 에러 메시지가 로그로 남아있었다.

cloud watch에 기록된 에러 정보


해결 과정

 나는 현재 news_crawling에서 발생하고 있는 에러를 고치기 위해 429 상태 코드에 대해 공부했다. 위에서 언급했다시피 429 상태 코드는 사용자에게 요청을 지연하라는 의미를 가진다. 따라서 현재 에러가 발생하는 위치에 대해 적당한 시간을 대기한 후 요청을 보내면 문제를 해결할 수 있다.

 로그를 통해 정확히 어떤 요청에서 에러가 발생하고 있는지 확인했다.

에러 메시지 내에 표현된 url

url 구조를 보아하니, 뉴스 본문을 가져올 때 에러가 발생했다. https://n.news.naver.com/~  형식의 주소는 뉴스 중에서도 네이버 내에서 볼 수 잇는 뉴스 목록을 의미하기 때문이다. 따라서 뉴스 본문 정보를 가져올 때 사용되는 getNewsBody 메서드에 retry 로직을 추가하기로 했다.

앞서 조사한 바에 따르면, 429 상태 코드 응답은 Retry-After 헤더를 포함하기도 한다. 만약 네이버 측에서 Retry-After 헤더를 통해 지연 시간을 정확히 명시하고 있다면, 해당 시간을 준수하는 편이 좋을 것이라 생각했다. 따라서 네이버가 정말 Retry-After 헤더를 포함하여 429 상태 응답을 보내고 있는지 로그를 통해 조사해봤다.

Retry-After 없음

내가 테스트한 결과에 따르면  Retry-After 태그는 따로 명시되어 있지 않았다. 따라서 개인적으로 대기 시간을 지정해서 사용하기로 했다. 이전에 뉴스 기사 리스트를 가져오는 코드에서 사용했던 retry 로직을 어느 정도 재활용하여 요청 재시도 역할을 담당하는 withRetry 메서드를 만들어 사용했다.

import { setTimeout } from "timers/promises";

type AsyncAction = () => Promise<void>;

/**
 * retry 로직에 대한 옵션
 */
export type RetryOptions = {
  /**
   * 최대 에러 대기 횟수
   */
  retry?: number;
  /**
   * 최초 대기 시간. 에러 횟수가 높아지면 중복될 수 있음.
   */
  waitTime?: number;
  /**
   * 문제 발생 시 출력할 에러 메시지
   */
  err_message?: string;
}
/**
 * 에러 발생 시 최대 retry 횟수만큼 반복하여 요청을 시도하는 함수.  
 * 에러가 1회 증가할 때마다 waitTime은 2배로 증가.
 */
export async function withRetry(action: AsyncAction, options: RetryOptions = {}) {
  const {
    retry = 3,
    err_message = '요청에 응답할 수 없음'
  } = options;
  let { waitTime = 1000 } = options;

  let count = 0;

  while (count < retry) {
    try {
      await action();
      break;
    } catch (e) {
      console.error(e); // 에러 출력
      await setTimeout(waitTime); // 일정 시간 대기
      count++;
      waitTime *= 2;
    }
  }
  if (count === retry) throw new Error(`ERROR[${action.name}]: ${err_message}`);
}

이후 뉴스 본문을 가져오는 axios get 부분을 withRetry로 감쌌다.

  let req: AxiosResponse;
  
  await withRetry(async () => {
    req = await axios.get(address, {
      headers: {
        'User-Agent':
          'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/98.0.4758.102',
      },
    });
  });

위와 같이 코드를 작성한 후 lambda 함수를 다시 실행했다. 결과는 다음과 같다.

대상 키워드 "이재명", "윤석열" 을 정상적으로 가져오는 모습

 중간에 429 에러가 발생했지만, 이에 대해 일정 시간을 대기하도록 코드를 구성하여 정상적으로 키워드에 대한 뉴스 데이터를 가져오는 모습을 볼 수 있었다. 


결론

 데이터 수집 관련 기능을 구현하고 동작 구조를 구축해보니, 생각보다 고려해야 할 부분이 많은 것을 알게 되었다. 당장 뉴스 기사 목록과 뉴스 본문을 가져올 때 데이터 수가 어느 정도 늘어남에 따라 각각 403, 429 에러가 발생했다. 이에 대한 예외처리 로직을 고민하고 구현하는 과정이 쉽지만은 않았지만, 그래도 점점 그럴듯한 구조가 되는 것 같아 기분이 좋다.