2022년 하반기 작업 기록 겸 회고.
들어가며
작년 입사 후 얼마 지나지 않아 신규 서비스를 런칭하게 되었다. 서비스가 진행되면서 학생들이 문제를 풀고 활동하는 정보가 계속 데이터베이스에 쌓여갔다. 이 쌓여가는 데이터들을 통해 우리 서비스에 대한 인사이트를 얻기 위해선 어느정도 필요한 데이터를 정제해서 관리해야 한다. 이번 작업은 정답률 같은 통계 정보나 추천 모델을 만들기 위한 데이터 마트 구축에 앞서 산발적으로 흩어진 데이터를 모으는 데이터 웨어하우스 구축 작업이었다. 이번 작업을 한줄로 정의 한다면 "에어플로우를 사용해 스케줄 단위로 파이썬 ETL 스크립트를 실행시켜 빅쿼리에 적재" 라고 할 수 있겠다. 이런 류의(?) 작업을 처음하다보니 시행착오도 정말 많았고 그로 인해 시간이 정말 오래 걸리고 중간중간 스스로에게 답답함도 많이 느꼈었다. 한번 정리도 하고, 나중에 돌아보며 참고도 할겸 기록을 남긴다.
데이터의 특징
내가 다니는 곳은 에듀케이션 업계의 b2c 서비스를 하는 곳으로 이번에 모아야 했던 데이터는 학생의 각 학습활동 및 문제풀이에 관련된 데이터들이었다. 데이터는 크게 1. 학습 컨텐츠 데이터, 2. 학생의 활동 기록 데이터, 3. 학생의 학년이나 상품 같은 학생 정보 데이터로 나눠져있다. 여기서 1. 학습 컨텐츠 테이블에는 문제만이 아니라 단어암기나 강의 시청, 문장 순서 맞추기 등의 다양한 컨텐츠 활동이 존재한다. 그래서 처음엔 이 활동 타입별로 테이블을 나눠서 관리하는 방향으로 접근했는데, 우리가 하려는 것은 그 시간대의 학생 활동을 로그성으로 남기는 작업이기 때문에 테이블을 나누지 않고 모든 컬럼을 넣어서 하나의 거대한 테이블로 관리하기로 했다.
BigQuery
위에서 말한 것처럼 학생 활동을 로그성으로 남기는 작업이기 때문에 대규모 데이터를 관리하기 위해 빅쿼리를 선택했다. 빅쿼리는 유료로 조회를 할 때 용량에 따라 과금이 된다. 때문에 자주 비슷한 쿼리를 날리는 것 보다 가끔 많은양의 쿼리를 날리는 경우에 더 적합하다. 우리가 만드려는 것도 일단 활동을 모두 쌓고, 나중에 각 타입별 통계나 데이터 마트 구축시 사용할 목적이기에 빅쿼리 적재가 적합하다고 판단했다. 빅쿼리는 조회 비용 최적화가 중요하기에 파티션 분할과 클러스터링을 적용했다. 현재 학생활동 데이터는 매일 몇십만개가 쌓이기 때문에 파티션 분할을 day로 해서 관리하기로 했고, 클러스터링을 컨텐츠 타입에 걸어 쿼리 비용을 최적화하려고 했다.
빅쿼리의 특징 중 또 하나는 업데이트가 용이하지는 않다는 점이다. 빅쿼리는 분석, OLAP에 특화되어 있어 잦은 업데이트가 필요한 작업에는 적절하지 않다. 처음에는 학생활동이 변경되면 빅쿼리에도 업데이트 해주려고 했지만 이런 특성 때문에 업데이트는 포기하게 되었다. 대신 일주일 전 정보 수집(학생 활동이 더 이상 업데이트 되지 않을 시점이라는 가정)을 생각했으나, 변경 전 활동도 변경 후 활동도 전부 활동은 활동이기에 데이터가 update 된 시점을 조건으로 가져와서 insert 하는 것으로 정했다.
혼자서 프로젝트를 할때는 사실 데이터 베이스를 크게 사용할 일이 없었기 때문에 postgresql 이나 my sql 같은 데이터베이스밖에 사용해보질 않았다. 이번에 데이터 웨어하우스 구축에는 빅쿼리를 사용하기로 해서 테이블을 만들고 어떤 형식으로 들어가는지, 어떻게 쿼리문을 작성하는지 많이 연습했다. 기존 sql 문과 비슷하면서도 빅쿼리에는 record 라던지 unnest 같은 생소한 방식이 있어서 초반에 헤맸던 기억이 난다.
ETL 작업
여기서의 시행착오야 말로 무궁무진하게 많았기 때문에 키워드로 정리하겠다.
Extract
수많은 Join과 DB 이원화 - DB로 관리하지 않던 정보들이 최근에 DB화가 되어 기존 서비스와 다른 DB로 이원화가 되었다. 이 때문에 정보를 두군데에서 가져오고 정합성이 완전히 맞진 않는 key로 join하여 섞어 사용해야 했다. 정보를 다양하게 가져오다보니 테이블도 기본 10개가 넘는 join이 필요했다. 처음엔 늘상 조회하던대로 inner join만 사용해서 값을 가져왔는데 나중에 보니 누락되는 정보들이 존재해서 left join으로 전부 변경해야 했다.
쿼리 - 서비스 중인 DB에 쿼리를 너무 자주 날려 DB에 이상이 생기면 서비스에도 문제가 생긴다. 그래서 복제본인 replica DB를 생성해 쿼리를 날린다.
속도 이슈 - replica DB의 성능이 production보다 떨어지기 때문에 production을 사용 할 땐 느끼지 못했던 속도 저하 이슈가 발생했다. 이는 인덱스를 추가하는 것으로 해결했는데, extract 할때 필요한 컬럼에 index를 달았더니 11분 걸리던 쿼리가 1초아래로 떨어지는 것을 경험했다. 하지만 여기서 사용하는 컬럼이 실 서비스에서 업데이트가 자주 되는 컬럼이어서 인덱스 다는 작업시 업데이트가 자주 되는 상황에 락이 걸릴 수 도 있는 불안요소가 있었다. replica는 인덱스를 달 수 없기 때문에 일단은 stage DB에 인덱스를 달아서 수집하는 방향으로 선회해서 운영하고 있다.
Transformer
수많은 타입 - 문제, 강의, 암기 등 활동의 타입이 다양해서 각 타입별로 나눠서 따로 따로 처리를 해줘야 했다. 위에서 말한 것처럼 컨텐츠 데이터와 학생 활동 데이터 두가지가 있었기에 이 두군데에서 타입별로 어떤 정보들을 빼서 저장해야 할지 정하기 위해 데이터를 꼼꼼하게 보는 것도 시간이 오래 걸리는 작업중 하나 였다.
비정상 데이터 - 데이터를 하나씩 살펴보면서 한 타입씩 작업 하다보니 중간중간에 이상하게 들어있는 데이터도 많이 발견했다. 있어야 하는 값인데 없거나, 시작 시간과 끝 마치는 시간이 반대로 뒤바껴있거나, 풀이한 기록이 없는데 완료된 문제라고 되어있거나.. 다양한 케이스를 발견해서 제보했다. 이전에는 항상 되어있다고 가정하고 코딩을 했었는데, 이를 계기로 예외처리의 중요성을 더 깊게 깨닫게 됐다.
멀티문제! - 항상 되어있다고 가정한 것 중에 제일 크게 당한 부분인데.. 기본적으로 한 row에 하나의 활동만 담겨 있다고 가정하고 스크립트를 작성했다. 하지만 나중에 하나의 row에도 여러 문제, 즉 여러 활동 정보가 들어있을 수도 있다는 것을 알게 되었다. 이 때문에 하나의 row만 가져와서 처리하면 되는 부분을 그 안에서 다시 반복문을 돌면서 처리하도록 변경해야 했다. 이런식으로 나중에서야 알게되는 것이 몇개 있어서 계속 수정에 수정을 거치다보니 코드가 점차 더러워지는 것을 느꼈다.. 결국 나중에 또 시간을 들여서 리팩토링을 대대적으로 해야 했다. 초반 설계의 중요성과 설계시 고려해야 할 것을 놓치지 않기 위해 서비스와 데이터에 대한 이해도가 많이 필요하다는 것을 다시금 알게되었다. 특히 내 작업뿐 아니라 다른 팀원들이 하는 작업에 대해서도 어느정도 팔로우하고 있어야 데이터가 왜 이렇게 된건지 특이사항이 무엇인지 인지할 수 있겠구나 싶었다.
transformer에서 적재할 형식으로 변형해 최종 dictionary를 만들어 차곡차곡 list에 넣어 loader로 보내면 작업이 끝난다. dict를 사용한 이유는 key의 이름을 빅쿼리 컬럼 이름과 같이 만들면 순서를 지정하지 않아도 같은 이름의 컬럼으로 알아서 들어가기 때문이다.
Loader
빅쿼리 클라이언트 라이브러리에서는 insert_row_json이라는 사용해 bulk insert를 할 수 있다. 앞서 만든 list를 넣으면 알아서 한번에 모든 row가 insert 된다.! 값이 잘들어가는지 타입별로 몇백개씩 넣어보며 이상한 값이 없는지, 의도한대로 잘들어가는지 체크하는 작업을 했다. 이제 적재 테스트도 완료 되었으니 에어플로우 연동하면 바로 잘 돌겠지? 생각했으나..
Airflow
"Apache Airflow is an open-source platform for developing, scheduling, and monitoring batch-oriented workflows" 에어플로우는 배치 지향 워크플로우 개발, 스케줄링, 모니터링 하기 위한 오픈 소스 플랫폼이다. 작업을 예약한 시간에 정해놓은 순서대로 실행 할 수 있도록 해주는 오케스트레이터이다. 파이썬으로 워크플로우 설정을 할 수 있어서 편리하고, dag과 main 코드만 설정해주면 비교적 간단하게 실행 시킬 수 있다.
ETL 작업 완료 후 시작일을 정해서 에어플로우를 실행시켰더니 이게 웬걸, 에어플로우가 자꾸만 뻗어버리는 것이었다. 우리가 처음에 생각했던 것은 하루에 한번 학생 활동이 더딘 자정에 그날 데이터를 모두 처리해서 넣는 전략이었다. 하지만 하루에 생성되는 데이터가 몇십만개가 되다보니 한번에 처리하기에 메모리가 부족해서 실행이 되지 않았다. 이 부분에서 제시해주셨던 해결책은 한번에 처리하지 말고 분단위로 작업을 나눌 것, 분 단위 처리시 쿼리 또한 빠른 시간내에 실행 될 수 있도록 비동기 처리하기 였다. 그리하여 dag 설정만 해주면 금방 끝날 줄 알았던 작업은 또 다시 수정에 들어가게 되었다.
위에서 적었듯이 인덱스를 달아서 쿼리 속도를 해결했기 때문에 여러 테스트 끝에 비동기 처리는 하지 않았고, 대신 조건문을 수정했다. update 시간으로 데이터를 가져오면 다른 조건이 추가될 경우 각 쿼리의 소요 시간이 기하급수적으로 늘어난다. 그래서 쿼리 자체에서 조건문을 최대한 줄이고 내부 로직에서 데이터 전처리를 진행하는 방식을 택했다. update 시간을 조건으로 ID만 먼저 가져 온 후, IN(ids) 절을 활용해 결과를 빠르게 가져 올 수 있게 했다. 그 다음 loader에서도 한번에 insert를 너무 많이 하지 않고 값을 나눠서 들어갈 수 있게 만들었다. 이렇게 빠른 쿼리 실행으로 dag의 스케줄을 분으로 조절 할 수 있었고, 현재 업데이트가 많이 일어나는 서비스 시간에 1분 단위로 데이터를 수집하고 있다.
회고
처음에는 이 모든 작업이 끝나는데 한 스프린트(2주정도)로 잡았으나.. 데이터 파악도 부족하고, 빅쿼리도 처음 사용하고, 적은 내용 처럼 중간중간 시행착오가 정말 많았기 때문에 예상했던 기간보다 세배 정도 더 걸리게 되었다. 작업을 할 당시에는 마냥 내가 부족해서 기간이 늘어지나 보다 싶었는데 적어놓고 보니 물리적으로 시간이 많이 소요되는 작업을 한 것 같기도 하고.. 리팩토링만 몇번을 했는지 정말 절대 안 끝날 것 같았는데 어느새 다음 작업으로 넘어가서 이젠 적재되고 있는 데이터를 또 꺼내쓰고 있는게 신기하기도 뿌듯하기도 하다. 우여곡절이 많았던 첫 데이터 파이프라인 구축기겸 회고는 이만 마치도록 하겠다. 이번에 알게된 사실을 발판 삼아 다음 번엔 똑같은 실수를 안하고 해결책도 좀 더 빠르게 찾게된다면 좋겠다. 다음엔 지금 하고 있는 또 다른 파이프라인 구축기를 들고 올 예정이다.