[Spring Boot] 스크래핑 비동기 처리

2024. 10. 9. 16:23·Back-End/Spring Boot
728x90

 

들어가며

 

2024년 7월 말부터 10월 초까지 약 3개월 간 진행했던 프로젝트에 대해 정리할 만한 내용을 블로그에 기재하기로 했다. 프로젝트명은 InConcert로, 공연 소식을 확인하고 동행을 구하는 서비스이다. 크게 정리할 내용은 다음과 같다.

  • 쿼리 최적화
  • 공연 정보 스크래핑 (비동기 처리) ➡️ 이번 게시글 주제!
  • 인기 공연 순위 알고리즘

이 글에서는 스크래핑 비동기 처리 과정에 대해 설명하려고 한다.

 

 

페이지 구성

메인 페이지

메인 페이지를 보면 '오늘의 인기 공연'란을 확인할 수 있다. Play DB에서 Selenium을 통해 공연 정보를 스크래핑한 것이다.

 

문제 상황

스크래핑 과정에서 발생했던 문제는 동기 처리로 스크래핑을 진행하다 보니 시간이 오래 걸린다는 점이었다. 동기 처리로 실행한 코드를 살펴보자.

 

스크래핑 코드 (동기 처리)
// CrawlingService
@Transactional
    public void crawlIfNecessary() {
        long startTime = System.currentTimeMillis();	// 시작 시간 측정
        Performance lastCrawl = performanceRepository.findTopByOrderByIdDesc();
        LocalDateTime now = LocalDateTime.now();

        // 크롤링이 되어있지 않은 상태
        if (lastCrawl == null) {
            performCrawling();
        }
        // 마지막으로 크롤링한 지 24시간이 지났을 때 다시 크롤링 (현재는 테스트 중이므로 1시간으로 설정함)
        else if(ChronoUnit.HOURS.between(lastCrawl.getUpdatedAt(), now) >= 1){
            performanceRepository.delete(lastCrawl);    // 이전 크롤링 지우기
            infoRepository.afterCrawling();
            performCrawling();
        }
        
        long endTime = System.currentTimeMillis();                      // 종료 시간 기록
        double executionTimeInSeconds = (endTime - startTime) / 1000.0; // ms를 초 단위로 변환

        System.out.println("스크래핑 실행 시간: " + executionTimeInSeconds + "초");
    }

    @Transactional
    protected void performCrawling() {
        for (int type = 1; type <= 4; type++) {
            infoService.crawlAndSavePosts(String.valueOf(type));
        }
    }

 

카테고리 별(뮤지컬, 콘서트, 연극, 기타)로 하나씩 스크래핑을 진행한다는 것을 알 수 있다. 스크래핑 실행에 대한 판단은 조건문을 통해 진행되도록 작성하였다.

 

// PerformanceService
public void crawlPerformances(String type) {
        String url = null;

        if (Integer.parseInt(type) >= 3) {
            url = "http://m.playdb.co.kr/Play/List?maincategory=00000" + type + "&playtype=3";
        } else {
            url = "http://m.playdb.co.kr/Play/List?maincategory=00000" + type;
        }

        WebDriver driver = getChromeDriver();
        driver.get(url);

        // 페이지 끝까지 스크롤
        JavascriptExecutor jsExecutor = (JavascriptExecutor) driver;
        long lastHeight = (long) jsExecutor.executeScript("return document.body.scrollHeight");

        while (true) {
            jsExecutor.executeScript("window.scrollTo(0, document.body.scrollHeight);");
            try {
                Thread.sleep(2000); // 잠시 대기하여 새로운 콘텐츠가 로드되도록 함
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            long newHeight = (long) jsExecutor.executeScript("return document.body.scrollHeight");
            if (newHeight == lastHeight) {
                break;
            }
            lastHeight = newHeight;
        }

        // HTML 요소 확인
        List<WebElement> elements = driver.findElements(By.cssSelector("#list li"));

        for (WebElement element : elements) {
            try {
                String poster = "";
                String title = "";
                String date = "";
                String place = "";

                try {
                    poster = element.findElement(By.cssSelector("a span:nth-child(1) img")).getAttribute("src");
                } catch (Exception e) {
                    log.warn("Failed to find poster for an element");
                }

                try {
                    title = element.findElement(By.cssSelector("a span:nth-child(2)")).getText();
                } catch (Exception e) {
                    log.warn("Failed to find title for an element");
                }

                try {
                    date = element.findElement(By.cssSelector("a span:nth-child(3)")).getText();
                } catch (Exception e) {
                    log.warn("Failed to find date for an element");
                }

                try {
                    place = element.findElement(By.cssSelector("a span:nth-child(4)")).getText();
                } catch (Exception e) {
                    log.warn("Failed to find place for an element");
                }

                // 스크래핑 한 정보 저장
                if (!title.isEmpty() && !date.isEmpty() && !place.isEmpty()) {
                    log.info("공연 정보: {} {} {} {} {}", title, poster, date, place, type);
                    Performance performance = Performance.builder()
                            .title(title)
                            .imageUrl(poster)
                            .date(date)
                            .place(place)
                            .type(type)
                            .build();
                    performanceRepository.save(performance);
                    System.out.println("Saved performance: " + performance.getTitle());

                    saveAsPost(performance, Long.parseLong(type));
                } else {
                    log.warn("Skipping element due to missing required information");
                }
            } catch (Exception e) {
                log.error("Failed to parse element: " + e.getMessage());
            }
        }

        driver.quit();
    }

 

PerformanceService의 메소드는 타깃 사이트의 HTML 요소를 분석하여 공연 정보를 얻어오는 코드이다. 동기 처리 방식을 사용한 코드의 실제 소요 시간을 측정한 결과, 게시물 589개 기준 평균 155초의 시간이 걸리고 있었다.

 

 

이 문제를 어떻게 해결할 수 있을까?

 

스크래핑 시간이 오래 걸리다 보니, 사용자 입장에서도 사이트를 이용하기 불편하다고 생각했다. 따라서 스크래핑 과정을 비동기로 처리하고자 했다. 동기와 비동기의 차이는 이 게시물을 통해 확인할 수 있다.

 

해결 과정

스크래핑 과정 (비동기 처리)
// PerformanceService
@Async
public void startCrawlingAsync() {
    long startTime = System.currentTimeMillis(); // 시작 시간 기록

    // type 별로 별도의 스레드에서 스크래핑
    synchronized (crawlingLock) {
        if (isCrawling) {
            log.info("Crawling is already in progress. Skipping this request.");
            return;
        }
        isCrawling = true;
    }

    CompletableFuture.runAsync(() -> {
        try {
            List<CompletableFuture<Void>> futures = new ArrayList<>();
            for (int type = 1; type <= 4; type++) {
                int finalType = type;
                futures.add(CompletableFuture.runAsync(() -> crawlPerformances(String.valueOf(finalType))));
            }
            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

        } finally {
            synchronized (crawlingLock) {
                isCrawling = false;
            }
            long endTime = System.currentTimeMillis();                      // 종료 시간 기록
            double executionTimeInSeconds = (endTime - startTime) / 1000.0; // ms를 초 단위로 변환

            System.out.println("스크래핑 실행 시간: " + executionTimeInSeconds + "초");
        }
    });
}

// 4시에 한번씩 새로 스크래핑
@Scheduled(cron = "0 0 4 * * ?")
@Transactional
public void scheduleCrawling() {
    performanceRepository.deleteAll();
    startCrawlingAsync();
}

 

@Async 어노테이션을 이용해 비동기 처리 로직을 작성하였다. CompletableFuture 클래스를 통해 카테고리마다 스레드를 각각 만들어 스크래핑이 별도로 진행되게 하였다. 또한 기존의 조건문을 제거하고, cron을 통해 매일 4시에 한 번씩 스크래핑이 진행되게 하였다. 비동기로 스크래핑을 처리한 결과, 게시물 589개 기준 평균 66초가 걸리는 걸 확인할 수 있었다.

 

결과

동기 ➡️ 비동기 처리로 수정한 결과 스크래핑 시간이 155초에서 66초로 줄어들었다. 성능이 약 57%가 향상되었다. 카테고리 하나씩 기다릴 필요 없이 스레드를 통해 카테고리마다 별도로 받아오도록 하니 성능이 향상되었다는 것을 알 수 있었다.

 

 

728x90
저작자표시 비영리 변경금지 (새창열림)

'Back-End > Spring Boot' 카테고리의 다른 글

[Spring Boot] 인기 공연 순위 알고리즘 구현  (5) 2024.10.19
[Spring Boot] Spring AOP  (0) 2024.10.10
[Spring Boot] N+1 문제  (1) 2024.10.05
[Spring Boot] JPA 쿼리 최적화  (1) 2024.10.04
[Spring Boot] Setter vs Builder  (0) 2024.03.28
'Back-End/Spring Boot' 카테고리의 다른 글
  • [Spring Boot] 인기 공연 순위 알고리즘 구현
  • [Spring Boot] Spring AOP
  • [Spring Boot] N+1 문제
  • [Spring Boot] JPA 쿼리 최적화
hxxzz
hxxzz
개발새발 안 되게 개발 노력 중
  • hxxzz
    개발새발
    hxxzz
  • 전체
    오늘
    어제
    • 분류 전체보기 (104)
      • Java (3)
      • Back-End (9)
        • Spring Boot (7)
        • DevOps (1)
        • Redis (1)
      • Computer Scrience (4)
        • Data Structrue (4)
        • Algorithm (0)
      • SQLD (3)
      • 코딩테스트 연습 (85)
        • Programmers (30)
        • 백준 (15)
        • etc. (0)
        • 99클럽 (40)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    프로그래머스
    코딩테스트 준비
    LeetCode
    java
    N+1 문제
    dfs
    스택
    redission
    BFS
    SQLD
    99클럽
    개발자 취업
    SQL
    자료구조
    백준
    til
    SpringBoot
    jpa
    Stack
    Spring Boot
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
hxxzz
[Spring Boot] 스크래핑 비동기 처리
상단으로

티스토리툴바