DV360 크롤러 OTP 인증 충돌 문제 해결

yeahcold·2025년 6월 10일
0

Data Engineering

목록 보기
20/20

문제 개요: OTP 인증 충돌과 세션 동기화 실패

최근 Google DV360 플랫폼에서 데이터를 크롤링하기 위해 Cloud Run Job을 병렬로 실행하는 구조를 도입했다. 하지만 병렬 실행 시 예상하지 못한 문제들이 발생하기 시작했다. 대표적인 이슈들은 다음과 같다.

  • 여러 Job이 동시에 실행될 경우 Google OTP 인증이 충돌
  • 같은 세션 파일을 여러 Job이 동시에 덮어쓰거나 읽으며 상태가 꼬임
  • 세션 파일이 존재함에도 불구하고 만료되었거나 비정상적인 경우 재인증되지 않음

이 글은 해당 문제를 분석하고 해결 방안을 적용해나가는 과정, 그리고 궁극적으로 어떤 구조로 안정화를 이뤘는지에 대한 기록이다.


⚠️ 상황 상세 및 증상

1. OTP 인증 충돌

Google의 2단계 인증은 TOTP(Time-based One-Time Password) 기반이다. 즉, 같은 키(secret)를 사용하는 경우 30초 간 동일한 OTP 값이 생성된다. 따라서 동시 실행된 여러 개의 크롤링 Job은 모두 동일한 OTP를 사용하게 된다.

문제는 이 OTP가 "1회성"이라는 점이다. 한 번 제출되면 같은 값으로 다시 인증할 수 없다.

결과적으로:

  • 최초 OTP 제출 Job 하나는 인증 성공
  • 나머지는 인증 실패 → 로그인 중단 또는 보안 페이지 전환

2. 세션 동기화 실패

하나의 Job이 성공적으로 로그인하고 세션 파일(dv360_session.json)을 GCS에 저장했더라도,

  • GCS 업로드가 완료되기 전 다른 Job이 실행될 수 있음
  • 다른 Job은 세션 파일이 없는 상태에서 restore_session() 시도 → 실패 → OTP 진입 시도
  • 결국 다시 OTP 충돌이 발생

3. 로그인 판단 기준 불명확

restore_session() 시 로그인 성공 여부 판단을 위해 "identifierId" HTML 요소 존재 여부만 판단했는데, 이는 불완전하다. 보안 페이지나 에러 페이지에서도 존재할 수 있어 로그인 성공 여부 판단이 정확하지 않다.


원인 분석 요약

지금까지의 상황을 요약하자면 다음과 같다.

OTP는 30초 간격 TOTP -> 동일한 OTP가 동시 제출되면 하나만 성공
세션 덮어쓰기 문제 -> 여러 Job이 동시에 세션 저장/읽기 시도 시 꼬임 발생
불완전한 로그인 판별 -> HTML 요소만으로는 로그인 성공 여부 판단 불가


✅ 해결 방안 설계 및 구조 개선

세션 복원 후 실패 시 로그인으로 전환하는 구조

기존에는 OTP 인증 Job과 크롤링 Job을 분리하려고 했으나, 모든 Job이 세션을 우선 시도하고 실패하면 로그인으로 전환하는 방식이 더 유연하다고 판단했다.

즉, Job이 다음 순서를 따르게 된다:

  1. 세션 복원 시도
  2. 세션이 정상 복원되면 로그인 생략
  3. 세션이 만료되었거나 복원 실패 시 OTP 인증 포함 로그인 수행
  4. 인증 성공 시 세션을 GCS에 다시 갱신 저장

이 구조는 단일 OTP Job을 따로 운영하지 않아도 되며, 실제 실패 상황에만 인증을 수행하여 충돌을 방지할 수 있다.

GCS 세션 저장 시 덮어쓰기 제한

  • 세션 저장은 정상 로그인 후에만 수행
  • GCS 파일 저장 전에 로그인 성공 여부를 확실히 판단
  • 이미 GCS에 세션이 존재해도 새로운 세션으로 갱신 저장은 1회만 수행

로그인 판단 기준 보강

try:
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.XPATH, "//div[text()='캠페인']"))
    )
    return True
except TimeoutException:
    return False

실제 적용 코드

진입부 로직

web = DV360Crawler()
web.open_chrome_programmatically()
sleep(3)

if not web.restore_session(plan_name):
    logging.info("세션 복원 실패 → OTP 로그인")
    web.login(plan_name)
else:
    logging.info("세션 복원 성공 → 로그인 생략")

OTP 인증 후 세션 저장

def login(self, plan_name):
    if self.b.html_includes("identifierId"):
        self.b.find_one("#identifierId").send_keys(DV360_LOGIN_ID)
        self.b.find_one("#identifierNext").click()
        sleep(2)
        self.b.find_one("input[name='password']").send_keys(DV360_LOGIN_PW)
        self.b.find_one("#passwordNext").click()
        sleep(5)

        otp_success = False
        try:
            otp_success = self.get_code(plan_name)
        except Exception as e:
            logging.error(f"OTP 인증 실패: {e}")

        if otp_success:
            cookies = self.b.driver.get_cookies()
            with open("/tmp/dv360_session.json", "w") as f:
                json.dump(cookies, f)
            self.upload_session_file("/tmp/dv360_session.json", "session/dv360_session.json")

결론

해당 구조로 변환하면서, 다음과 같은 기준을 충족할 수 있었다.

  • OTP 충돌 방지: 한 번만 인증 수행
  • 세션 복원 실패 시 재인증 로직 포함
  • 세션 갱신은 1회만 수행하여 덮어쓰기 오버헤드 최소화
  • 크롤링 Job 수십~수백 개 실행에도 안정성 확보

이 구조는 OTP 기반 인증 시스템의 병렬 처리에서 안정적이며 확장성을 가지고 있기 때문에 유사한 크롤링 환경에서도 충분히 재사용가능할 것 같다.

profile
Software Engineer

0개의 댓글