# MLOps 01: Data testing, why, what and how

원문: https://medium.com/@ongxuanhong/mlops-01-data-testing-why-what-and-how-59cd8005637c

테스트 작성을 꼭 해야 하는지, 데이터에서 무엇을 테스트해야 하는지, 효과적인 테스트 방법론에 대해 설명하는 글을 읽고 내용을 정리하였습니다.

# 왜 테스트를 작성해야 할까?

# 새로운 팀원 온보딩

프로젝트에 새로운 팀원이 합류하였을 때 적절한 테스트가 없다면, 코드를 한 줄씩 디버깅 하며 변수의 상태와 기능, 값이 어떻게 할당되는지 파악해야 합니다. 만약 단위 테스트가 존재한다면 아래의 구조적인 단계로 이해할 수 있었을 것입니다.

  • PREPARE: 데이터 이해
  • ACTION: 코드 실행 과정 파악
  • ASSERT: 실행 후 기대 결과 확인 적절한 단위테스트가 있다면 새로운 팀원은 프로젝트 문서를 2~3주간 파고들 필요 없이 단위 테스트를 살펴보는 것 만으로도 몇 일 안에 프로젝트에 익숙해질 수 있을 것입니다.

# 코드 리팩터링

코드 리팩터링 후에는 QA 전문가들이 시스템의 기능을 검증하는 것이 일반적인 관행. 자동화된 테스트 스크립트를 통해 잡을 수 있었던 버그를 매번 QA 팀이 반복적으로 점검해야 할 필요가 없을 것입니다. 개발 초기에 테스트를 적극적으로 작성하는 방식은 자신의 시간뿐만 아니라 팀과 전체 프로젝트에도 큰 도움이 됩니다.

# 신뢰할 수 있는 고품질 코드

시간이 지남에 따라 기술 부채가 계속 쌓이면, 새로운 기능을 추가하기가 어려워지고, 새로운 기능이 추가될 때마다 기존 시스템에 문제가 발생할 가능성이 커집니다. Pull Request 리뷰 시 단위 테스트를 무시하면 기술 부채의 악순환이 생겨나며, 팀이 혁신을 추구하고 건강한 코드베이스를 유지할 수 있는 능력이 저하될 수 있습니다.

# 신뢰할 수 있는 데이터 파이프라인

머신러닝 모델을 포함한 데이터 제품 시스템을 개발할 때 우리는 필연적으로 MLOps, 즉 머신러닝 운영의 영역과 맞닥뜨리게 됩니다. MLOps는 본질적으로 DataOps, ModelOps, DevOps가 융합된 개념으로, DevOps는 CI/CD(지속적 통합/지속적 배포) 프로세스의 원활하고 지속적인 실행을 지원하는 중요한 역할을 합니다. 시스템이 확장됨에 따라 새롭게 추가된 기능에 대해 CI가 정기적으로 테스트를 수행해주기 때문에 소프트웨어를 발전시키고 개선할 수 있다는 자신감을 얻게 됩니다.

# 어떻게 테스트를 작성할까?

소프트웨어 개발 과정에서 자주 적용되는 테스트 작성 방법은 FIRST 원칙이며, 이는 빠르고(Fast), 독립적이며(Independent), 반복 가능하고(Repeatable), 자체 검증(Self-Validating)되며 시의적절한(Timely) 테스트를 의미합니다.

  • 빠르게(Fast): 테스트가 빠를수록 더 많은 테스트를 실행할 수 있는 시간이 생깁니다. 테스트가 느리게 실행되면 팀은 자주 실행하지 않게 되고, 이로 인해 문제를 조기에 발견해 쉽게 수정할 기회를 놓칠 수 있습니다.
  • 독립적(Independent): 테스트는 서로 의존하지 않아야 합니다. 각 테스트 케이스는 다른 테스트 케이스에 의존하지 않고 독립적으로 실행될 수 있어야 합니다. 테스트 케이스가 서로 의존하면, 처음 실패한 테스트 케이스가 다른 테스트 케이스에도 오류를 유발해 문제 진단이 어려워집니다.
  • 반복 가능(Repeatable): 테스트는 모든 환경에서 실행될 수 있어야 합니다. 즉, 로컬 환경에서 성공적으로 실행된 테스트는 QA나 프로덕션 환경에서도 동일하게 실행될 수 있어야 합니다.
  • 자체 검증(Self-Validating): 테스트는 일관된 성공 또는 실패 결과를 출력해야 합니다. 오늘 실행하면 성공하고 내일은 실패하는 경우는 없어야 합니다.
  • 시의적절(Timely): 단위 테스트는 새로운 기능이 구현되는 시점에 작성되어야 합니다.

# TDD(Test Driven Development, 테스트 주도 개발)

테스트를 먼저 작성하고 이후에 기능을 구현하는 방식입니다. 새로운 기능에 대한 테스트를 작성한 후, 테스트가 실패하는지 확인합니다. 이후 테스트가 성공할 수 있도록 코드를 리팩터링합니다. 테스트가 여전히 성공하는지 확인하면서 코드를 최적화하고, 다른 기능도 이러한 과정을 반복합니다.

# 코드 커버리지(code coverage)

단순히 코드 커버리지를 99-100%로 올리기 위해 불필요한 테스트를 추가하는 것보다는, 필요한 만큼만 충분히 테스트를 작성하는 것이 좋다. 코드 커버리지는 프로젝트가 얼마나 테스트되고 있는지 추적하는 데 유용하며, 이를 통해 팀은 프로젝트 내에서 아직 단위 테스트가 작성되지 않은 영역을 평가하고 식별할 수 있습니다.

그러나 단순히 커버리지 지수를 올리기 위해 검증(assertion)을 신경 쓰지 않고 테스트를 작성하는 함정에 빠지기 쉽습니다. 왜냐하면 목(mock)을 사용하거나 단순히 함수를 실행하는 것만으로도 지수를 올릴 수 있기 때문입니다. 그러나 이런 방식은 결과를 평가하거나 검증하지 않으므로 코드 실행에만 시간을 소비하게 될 뿐, 실제 테스트는 전혀 이루어지지 않게 됩니다.

# 무엇을 테스트해야 할까?

# 기능 테스트

이 테스트 단계는 소프트웨어 개발 과정과 유사하며 데이터 테스트에서도 사용됩니다. 위에서 언급한 것처럼, 테스트를 수행하기 위해 다음 네 단계를 진행합니다:

  1. Arrange (준비): 코드와 샘플 데이터를 설정
  2. Act (실행): 테스트할 함수를 실행
  3. Assert (검증): 반환된 결과에 대해 논리적 검증 수행
  4. Cleanup (정리): 테스트에 사용된 리소스를 해제

Python에서는 다음과 같은 도구를 사용하여 테스트할 수 있습니다:

  • pytest: 프로젝트의 모든 테스트를 실행하는 데 사용됩니다. (https://docs.pytest.org/)
  • unittest: 테스트의 반환 결과를 확인하는 assertion 함수를 제공합니다. (https://docs.python.org/3/library/unittest.html)
  • coverage: 프로젝트의 코드 커버리지 지수를 측정하는 데 사용됩니다. (https://coverage.readthedocs.io/)

비록 시스템의 기능은 아니지만, 코드 포맷 및 구조를 점검하는 Lint 테스트도 있습니다. black을 사용해 코드를 자동으로 포맷할 수 있고 (https://github.com/psf/black), flake8을 통해 사용되지 않는 라이브러리 import, 선언되지 않은 변수명 사용, 함수 길이가 허용된 열 수를 초과하는 경우와 같은 코드 문제를 경고할 수 있습니다. (https://flake8.pycqa.org/en/latest/)

자주 사용되는 테스트 프로그래밍 기법

  • fixture: Arrange 단계에서 사용되며, 보통 설정, 애플리케이션 컨텍스트 시작, Spark을 사용한 계산 테스트 시 SparkSession 등을 설정합니다. (https://docs.pytest.org/en/6.2.x/fixture.html)
  • conftest.py: 여러 fixture를 모아 관리하기 쉽게 하며, 다른 테스트 모듈에서 접근하여 재사용할 수 있습니다.
  • parametrize: fixture와 유사하게 함수의 인자를 구성하는 설정을 준비하는 데 도움을 줍니다. (https://docs.pytest.org/en/6.2.x/parametrize.html)
  • mock: 함수의 동작이나 변수 값을 덮어쓰며, 통합 테스트에서 원하는 반환 동작을 모킹할 때 사용합니다. (https://docs.python.org/3/library/unittest.mock.html)

# 데이터 테스트

코드를 테스트하는 것은 어렵지만, 데이터 테스트는 그 예측 불가능한 특성 때문에 더 큰 도전이 될 수 있습니다. 데이터는 변경이 잦아 다양한 스키마 변경(새로운 컬럼 추가, 기존 컬럼 이름 변경, 컬럼 삭제 등), 데이터 타입 수정, 특정 날짜의 거래 누락, 데이터 손상 및 잡음 등을 포함합니다. 데이터를 다룰 때는 여러 단계에서 테스트를 수행해 데이터의 무결성과 신뢰성을 보장합니다.

  1. 원시 데이터 테스트
  2. 중간 및 핵심 데이터 모델 테스트
  3. 테이블 레벨에서의 테스트:
    • 행과 열의 수, 반환되는 컬럼 순서가 예상과 일치하는지 확인합니다.
    • 스키마가 기대와 일치하는지 확인합니다.
    • 데이터의 신선도(Freshness).
  4. 컬럼 레벨에서의 테스트:
    • 기본 키의 고유성: 중복된 행이나 키 값이 있는지 확인합니다.
    • NULL 값의 존재 여부.
    • 허용된 값: 값이 예측된 범위 내에 있는지, 참조 키가 상위 테이블의 기본 키에 존재하는지 확인합니다.
    • 제약 조건: 각 처리 함수에 특화된 논리를 바인딩하여 필터링된 값 목록이 반환 결과에 포함되지 않도록 하거나, 가격 필드는 사전 정의된 하한/상한 범위 내에 있어야 합니다.

데이터의 ACID 특성을 보장하고 몇 가지 제약 조건을 설정하는 것만으로 충분한데, 왜 이런 일이 이전에 이루어지지 않은 이유는 빅데이터이기 때문. 빅데이터 시스템의 목표는 빠른 저장과 빠른 연산. Apache Hudi, Apache Iceberg, Delta Lake와 같은 빅데이터용 RDBMS 유사 시스템들이 있어 빅데이터의 저장 및 연산 성능을 보장하면서도 데이터의 ACID 속성을 관리 가능. 또한 운영 중에는 데이터 계약(Data Contract), 스키마 운영(Schema Ops)과 같은 용어를 사용해 예기치 않은 변화를 관리하여 하위 데이터에 미치는 영향을 최소화.

대부분의 DataOps 라이브러리는 데이터 테스트 도구를 내장

  • Dagster: 데이터 파이프라인에 민첩한 테스트 작성 기능을 제공하여 Airflow를 능가합니다 (https://docs.dagster.io/concepts/testing).
  • DBT 테스트: 데이터 분석을 더욱 정확하게 만듭니다 (https://docs.getdbt.com/docs/build/tests).
  • great_expectations: 데이터 프로파일링과 데이터 검증 기능을 통해 입력 데이터 품질을 보장합니다 (https://docs.greatexpectations.io/docs/).

# 머신러닝 모델 테스트

모델은 데이터와 알고리즘이라는 두 가지 주요 구성 요소로 이루어집니다. 모델 훈련 과정에서는 데이터를 훈련 세트, 검증 세트, 테스트 세트로 나누어, 모델의 일반화 능력을 향상시켜 새로운 데이터를 사용하는 실제 환경에서도 정확도를 유지하도록 합니다. 그러나 시간이 지나면서 실제 데이터는 변화할 수 있으며, 이에 따라 두 가지 문제가 발생할 수 있습니다: 데이터 분포 $(P(X))$가 변하는 데이터 드리프트와 조건부 분포 $(P(Y|X))$가 변하는 컨셉 드리프트입니다.

모델 재훈련의 필요성을 판단하기 위해서는 지속적으로 다음 두 가지 지표를 모니터링해야 합니다. 우리는 통계적 테스트 지표를 사용하여 이러한 분포의 추세를 지속적으로 관찰하고 비교합니다. 추적 지표가 특정 임계값 이하로 떨어지면 MLOps 팀이 자동으로 데이터 수집, 모델 재훈련, 성능 평가, 모델 등록 및 프로덕션 배포와 같은 작업을 시작하게 됩니다. 다음은 이 목적에 자주 사용되는 통계적 테스트 지표입니다:

이 모니터링 시스템을 처음부터 수동으로 설치할 수 있으며, 또는 Evidently AI (opens new window)와 같은 오픈 소스 머신러닝 모니터링 도구를 참고할 수도 있습니다. 이 도구는 테스트 지표를 자동으로 계산하고, 리포트를 시각화하며, Airflow, MLFlow, Metaflow, Grafana와 같은 ML 파이프라인 프로그램과 통합할 수 있습니다.

# 결론

기술 부채는 어디에서 비롯될까요?

기술 부채는 주로 임박한 기한 압박에서 발생합니다. 이로 인해 팀은 코드 디자인의 기본 원칙을 무시하고 필수적인 테스트를 생략하게 됩니다. 이러한 급한 접근 방식은 결국 코드 가독성을 방해하고 구성 요소 간의 상호 의존성을 악화시켜 향후 수정 및 업그레이드를 어렵게 만듭니다. 또한, 비즈니스 요구 사항의 빈번한 변화는 프로젝트를 완전히 재작업해야 할 필요성을 초래할 수 있으며, 이때 우리는 출시 일정 준수를 우선시할지, 아니면 테스트를 작성하고 코드 품질을 검토하는 데 필요한 시간을 투자할지 고민하게 됩니다. 이로 인해 기술 부채는 계속 쌓여갑니다.

이런 상황에서 단위 테스트(unit testing)는 소프트웨어 개발에서 오랫동안 사용되어 온 검증된 기법이며, 이 글에서 설명한 장점 덕분에 데이터 제품 개발 영역으로도 확산되었습니다. 개념 증명(PoC) 모델을 프로덕션에 배포하는 것은 단순히 학교 프로젝트에서 작업한 임시 코드나 연구 논문을 완료했을 때와는 달리, 매우 어려운 일입니다. 모델을 성공적으로 배포하는 것이 시작일 뿐, 그 이후도 똑같이 중요합니다. 바로 모델 성능을 모니터링하는 일입니다. 모델 재훈련 여부는 데이터 드리프트(Data Drift)와 컨셉 드리프트(Concept Drift) 관련 지표에 따라 결정됩니다.

다행히도 오늘날 데이터 커뮤니티에서 테스트 작성의 중요성에 대한 관심이 커지고 있습니다. 그 결과, 데이터 전문가들을 위한 소프트웨어 개발을 전문으로 하는 여러 팀들이 등장하여 Dagster, DBT, great_expectations와 같은 유용한 도구들을 제공하고 있습니다. 이 도구들은 프로젝트에 단위 테스트를 쉽게 구현할 수 있도록 도와줍니다.