SPRING

[250602 트러블 슈팅]Playwright 병렬 크롤링 중 TargetClosedError 해결기

도원좀비 2025. 6. 3. 20:52

1️⃣ 문제 상황

Spring Boot 기반 크롤러 프로젝트에서 Playwright(Java) 를 이용해 Danawa 상품 리뷰를 병렬로 크롤링하던 중, 다음과 같은 에러가 반복적으로 발생했습니다:

TargetClosedError: Target page, context or browser has been closed
PlaywrightException: Cannot find object to call __adopt__: browser-context@xxxxxxx
  • 병렬 처리: Virtual Thread 기반
  • 프록시: socks5://127.0.0.1:1080 (로컬 프록시 서버)
  • Catalog 마다 browser.newContext() 호출

2️⃣ 원인 분석

Playwright는 Java ↔ JS 런타임 간 통신을 통해 객체를 관리합니다.
특히 browser.newContext()는 내부적으로 context 객체를 등록(adopt) 하며 통신을 진행합니다.

하지만 병렬 환경에서 생성 직후 close()를 호출하거나
JS 통신이 완료되기 전에 GC 되면 아래와 같은 문제가 발생합니다:

  • TargetClosedError: 페이지나 컨텍스트가 조기에 닫힘
  • __adopt 에러: context 객체가 아직 등록되기 전에 닫힘

특히 프록시를 설정한 상태에서는 네트워크 레이턴시로 인해 이 타이밍 문제가 더욱 자주 발생했습니다.


3️⃣ 시도했던 해결 방법들

시도 결과
Catalog 마다 browser.newContext() 후 즉시 page.navigate() ❌ context가 adopt 되기 전에 GC 되는 경우 발생
context.close() 를 try-finally 에 감싸기 ❌ 일부는 해결되지만 여전히 오류 발생
Headless 모드 OFF 🔍 디버깅에 도움은 됐지만 본질적인 해결은 아님

 


4️⃣ 해결 방법 ✅

변경된 구조

Playwright playwright = Playwright.create();
Browser browser = playwright.chromium().launch(...);

for (Catalog catalog : catalogs) {
    BrowserContext context = browser.newContext(...);

    // 컨텍스트 adopt 시점까지 대기
    Thread.sleep(2000); // 또는 page.waitForTimeout(200);

    Page page = context.newPage();
    page.navigate(...);

    context.close();
}

🔑 핵심은 context가 완전히 등록될 시간을 보장해주는 것!
즉, 생성 직후 바로 폐기하지 않도록 지연을 두어야 race condition을 피할 수 있습니다.


5️⃣ 결과

  • 병렬 스레드 2개 × 총 2560개의 상품
  • 재시도 로직 없이도 100% 정상 크롤링 완료
  • 3시간 이상 장기 실행 테스트도 성공
  • 리뷰 저장 중복 제거 및 Proxy 정상 작동 확인

6️⃣ 마무리 

Playwright는 매우 강력하지만, Java 병렬 환경에서는 JS 런타임과의 타이밍 이슈를 주의해야 합니다.

이번 경험을 통해 다음 교훈을 얻었습니다:

  • 병렬 처리 시 리소스 생성/해제 시점은 명확히!
  • Playwright context는 adopt 되기 전에 close 하면 안 됨
  • 네트워크 환경(프록시 포함)에 따른 속도 차이 고려

비슷한 문제를 겪고 있다면 context 생성 후 약간의 대기시간을 주고, close는 확실하게 try-finally 또는 스코프 제어로 감싸보세요!