지금으로부터 약 한 달 간 자동화 프로그램 안정화 작업으로 다소 바쁜 일정을 보냈지만,
다행히 해당 작업이 마무리되어 이렇게 글로 정리해볼 여유가 생겼습니다.

구글 빅쿼리

이전 회고에서 사내 빅쿼리 도입을 고려하고 있다고 남긴 바 있었는데,
이제는 빅쿼리에 익숙해지며 SQL도 어느정도 원하는대로 다룰 수 있는 수준에 이르렀습니다.

원래는 구글 시트 API를 통한 데이터 적재 자동화에 익숙해진 후 빅쿼리로 차차 넘어갈 예정이었지만,
생각보다 구글 API의 사용법이 간단하여 얼마안가 빅쿼리 사용을 시도해볼 수 있었습니다.

MySQL 같이 잘 알려진 DB 조차 직접 다뤄본 경험이 없었기에
스키마 구성 등의 행위가 낯설었고, 몇 번이나 테이블의 구조를 갈아 엎었습니다.

빅쿼리는 적은 비용 대비 관리가 편하다는 장점도 있지만,
초급 개발자 입장에서 무엇보다 좋았던 것은 쿼리 시 예상되는 비용을 산정해주어
더욱 효율적인 쿼리문을 작성할 수 있게 도와준다는 점입니다.

이에 대한 것을 몰랐을 도입기 때는 어떠한 쿼리 최적화 기법도 적용하지 않아
당시 90MB의 테이블의 모든 데이터를 매 시간마다 대시보드에서 호출하게 했습니다.

해당 테이블은 매 시간마다 데이터가 쌓이는 구조였기 때문에 용량의 증가가 적지 않았는데,
장기적으로 빅쿼리에서 제공하는 월 1TB의 무료 용량을 초과할 수도 있겠다는 걱정이 있었습니다.

이러한 문제를 해결하기 위해 DB 최적화에 대해 고려하게 되었고,
파티셔닝과 정규화 기법을 적용하게 되었습니다.

파티셔닝

빅쿼리는 테이블 생성 시 하나의 수치형 열을 기준으로 파티션을 적용할 수 있습니다.
저는 보통 데이터 수집일시에 해당하는 날짜 열을 파티션의 기준으로 삼는데,
파티셔닝 시의 장점은 WHERE 문으로 파티션을 특정할 경우 해당 파티션의 데이터만을 호출하는 것입니다.

이 덕분에 당일의 데이터만이 필요한 대시보드에서 빅쿼리로 데이터를 요청할 경우
기존의 90MB에서 1MB 미만으로 쿼리 비용을 감소시킬 수 있었습니다.

지금은 일자별로 데이터가 누적되는 모든 테이블에 파티셔닝을 적용해
데이터가 축적되면서 증가할 수 있었던 쿼리 비용에 대한 걱정을 덜게 되었습니다.

DB 정규화

데이터가 누적되면서 발생할 수 있는 비용은 쿼리에서뿐 아니라 데이터 자체의 저장 비용에서도 발생합니다.

제가 업무에서 활용하는 상품 순위 데이터를 쌓기 시작할 당시에는
상품코드와 순위 뿐 아니라 해당 상품의 상품명, 판매처명 등의 문자열을 같이 기록했습니다.

상품명의 경우 문자열의 길이가 작지 않았기에 전체적인 데이터 용량의 증가를 가속시켰고
DB 최적화를 고려할 때쯤에 해당 상품명과 판매처명 등의 문자열 컬럼이 전체 테이블에서
절반 이상에 해당하는 용량을 차지하고 있었다는 것을 알게 되었습니다.

이러한 문제를 해결하기 위한 기법을 찾던 중 발견한 것이 DB 정규화 기법이었습니다.

DB 정규화 기법은 반복되는 데이터를 별도의 테이블로 분리하는 기법인데,
분리된 데이터는 필요할 때만 JOIN을 통해 불러오면 되기 때문에
더욱 효율적으로 데이터 공간을 구성할 수 있을 것이라 생각했습니다.

저는 별도의 인덱스 DB를 생성하고 공통된 코드로 묶을 수 있는 문자열 컬럼들을
여러 테이블로부터 하나의 인덱스 DB 아래 테이블로 분리시켰습니다.

그 결과 300MB에 달하는 테이블로부터 분리된 210MB의 데이터를 3MB로 압축시키면서
테이블의 용량을 2/3 가량 감소시켰습니다.

DB 정규화를 거치지 않았다면 지금쯤 500MB 정도의 데이터가 쌓였겠지만,
최적화를 통해 아직까지 140MB 정도의 용량이 기록되어 있습니다.

pandas-gbq

DB 설계도 중요하지만, 제 업무는 기본적으로 데이터 수집 및 분석이었기 때문에
수집된 데이터를 빅쿼리로 옮기는 과정을 구현해야 했습니다.

구글 시트의 경우 매번 JSON 인증 파일을 갖고 다니며 인증 요청을 수행해야 했기에
업로드를 위해 별도의 함수를 작성하고 호출해야하는 불편함이 있었습니다.

하지만, 빅쿼리는 평소에 사용하는 pandas 모듈에서 업로드용 모듈을 지원한 덕분에
엑셀을 저장하듯이 내장된 to_gbq 함수로 간단하게 업로드를 수행할 수 있었습니다.

로컬에서 빅쿼리 업로드를 위한 인증도 초기에 한번만 구글 로그인을 수행하면 되었기에
지금도 굉장히 간편하게 활용하고 있습니다.

구글 클라우드 펑션

윈도우 스케줄러와 파이썬을 통한 빅쿼리 업로드를 통해 어느정도 자동화를 이루었다고 생각했을 때쯤
예상치 못한 사건이 발생했습니다.

평소에 데이터를 가져오고 있던 네이버의 경우 하나의 IP 주소로 반복 요청 시
크롤링이 감지되어 차단당하는 문제 때문에 별도의 공인 IP를 제공하는 아이피팝 프로그램의 환경 아래서
자동화 프로그램이 돌아가고 있었는데, 해당 아이피팝이 공격을 당해 먹통이 되는 경우가 발생했습니다.

이 문제가 일주일 이상 지속되면서 결국 로컬에서 프로그램을 돌리는 것이 안전하지 않다는 것을 인식했고,
굳이 이것이 아니더라도 자동화 프로그램 실행 중 발생하는 문제를
해당 PC에 접근하지 않고는 해결할 수 없었던데서 불만을 갖고 있기도 했습니다.

대안을 찾기 위해 우선적으로 생각해본 것은 기존에 생각하고 있었던 Airflow 였지만,
단기간에 파이썬 코드를 Airflow에 맞게 수정하는 것은 어려움이 있다고 판단했습니다.

이때, 어디선가 들었던 AWS 람다가 떠올랐고 관련된 서버리스 서비스에 대해 찾아보다가
마침 현재 사용하고 있는 GCP 안에서 구글 펑션이라는 서버리스 기능을 제공한다는 것을 알게되었습니다.

구글 펑션을 활용하면 HTTP 요청 트리거로서 현재의 파이썬 함수를 실행할 수 있었습니다.

더욱이 매일 일정 시간에만 잠깐 수행되는 자동화 프로그램은
실시간으로 돌아가는 Compute Engine에서 돌리는 것보다 구글 펑션이 비용적으로 더욱 효율적임이 계산되었고,
무엇보다 HTTP 트리거가 매일보던 크롤링 작업에서 다뤘던 것과 크게 다르지 않았다는데서 적응하기 쉬웠습니다.

다만, 항상 클라이언트의 입장에서 요청만 하다가 이러한 요청을 받아서 처리하는 로직을 구현하게 되면서,
항상 보내던 헤더와 데이터가 서버에서 어떻게 보여지는지를 알게되는 등의 새로운 관점의 전환을 느꼈습니다.

구글 펑션의 사용법은 간단하여, 기존에 엑셀 기반의 설정을 읽어서 파이썬 함수를 호출하던 동작을
JSON 형식의 설정으로 대체하여 같은 함수에 동일한 파라미터를 전달해 호출하게 하면 되었습니다.

리다이렉트

구글 펑션을 도입하게 되면서 크롤링 기능을 비약적으로 개선할 수 있었습니다.

저는 이 기법을 리다이렉트라고 부르는데, 구글 펑션의 스케일 아웃 기능을 통해
여러 개의 쿼리를 나눠서 구글 펑션에 동시에 요청하면 요청 차단의 우려 없이
비동기적으로 크롤링을 수행할 수 있습니다.

기존 로컬에서도 비동기적으로 크롤링을 수행하긴 했지만,
네이버 등에서 요청이 차단당하는 문제 때문에 최대 동시 요청 횟수를 3회로 제한하고
요청 간 딜레이를 주었습니다.

물론, 이러한 방식이 상대방 서버 입장에서 안전하고
동시다발적으로 요청을 보내는 기법은 서버에게 공격으로 간주될 수 있다는 것을 인지하기 때문에,
리다이렉트의 동시 제한 횟수를 정해놓았습니다.

이 기법을 도입하면서 기존에 2시간마다 40분씩 걸리면서 돌렸던 자동화 프로그램을
1분 조금 넘는 실행 시간으로 단축시킬 수 있었습니다.

구글 클라우드 스케줄러

구글 펑션도 결국 누군가가 트리거를 걸어줘야 했기 때문에 아직까지 로컬에서 실행되는 문제를 벗어나지 못했습니다.

이러한 문제를 해결하기 위해 마찬가지로 GCP 안에서 활용할 수 있는 서비스를 탐색했고,
클라우드 스케줄러라는 것을 발견했습니다.

클라우드 스케줄러는 크론탭으로 일정 주기마다 HTTP 요청을 보낼 수 있게 설정할 수 있게 지원해주는데,
마침 HTTP 요청에 같이 담겨야되는 인증 정보를 자동으로 첨부할 수 있어서
로컬에서 요청을 보내는 것보다 더욱 간편하게 전송할 수 있는 장점이 있습니다.

사용법도 주기, 대상 주소, JSON 본문만 지정하면 매일 특정 시간마다 구글 펑션을 수행하게 설정할 수 있습니다.

이를 통해 로컬 환경을 탈피해 GCP 안에서 모든 자동화를 수행할 수 있게 되었습니다.

마무리

사실 GCP 내에서의 작업은 많은 시간이 걸리지 않았고
실질적으로 GCP에 맞게 기존 파이썬 코드를 리팩토링하는데서 더욱 깔끔하게 고치고 싶은 욕심이 들어
한 달 동안 전체 코드를 수정하게 되었습니다.

별도의 프레임워크 없이 제작한 제 크롤러는 scrapy 모듈에서 착안해 Spider, Parser, Pipeline의 구조로 이루어져 있는데
기존에 비동기와 동기식 Spider의 사용여부로 구분된 별도의 프로젝트를 하나로 합친다거나
crawl > gather/redirect > fetch의 단계로 추상 메소드를 정의하는 등의 전반적인 리팩터링 작업이 있었습니다.

마침 명절을 맞아 모든 작업이 안정화되었고 여유를 가지며 작업을 되돌아 볼 수 있었습니다.

지금까지 데이터 엔지니어링 관점에서 비약적으로 성장했다고 느끼면서도,
혼자만의 노력으로 이 이상 성장할 수 있을지에 대해서는 다소 불확실한 부분이 있습니다.

아마 당분간은 그동안 방치했었던 데이터 분석 쪽에 눈길을 돌려
그동안 쌓아둔 데이터들을 보면서 새로운 발견을 하게 될지도 모르겠습니다.