Python 2.7 서버의 CI Test 개선 - 13분에서 3분으로

Image not Found

들어가며

안녕하세요 Supply 그룹 Product Backend 팀의 Elric 입니다. 버즈빌에서 운영중인 허니스크린은 2013년에 출시된 서비스로 현재는 운영 및 유지보수에 집중하며 신규 피쳐를 테스트 할 수 있는 일종의 테스트 베드(test bed) 로 사용되고 있습니다.

Product Backend 팀에선 다양한 신규 프로모션을 출시하기 전 허니스크린에 먼저 서비스를 라이브하며 실 사용 테스트를 거쳐 정식 피쳐를 매체사에 제공합니다. 테스트 베드이지만 허니스크린 역시 실제 유저에게 제공되는 서비스이므로 빠른 배포와 대응이 필요한데요, 최근 긴급 배포 상황에서 CI 파이프라인의 긴 실행 시간(약 13분)이 병목이 됐고 이에 대해 근본적인 해결이 필요했습니다.

기존 CI 과정의 문제점

허니스크린은 현재 python2.7 버전으로 운영되지만 해당 버전은 2020년 1월부터 공식 지원이 중단되어 보안 패치나 버그 수정이 더 이상 제공되지 않습니다. 허니스크린의 CI 는 3가지 작업으로 이 작업은 모두 병렬로 실행되지만, 그럼에도 긴 시간이 소요되는 것은 특정 작업에서 병목이 있는 것으로 유추할 수 있었습니다.

현재 CI Pipeline 구성

  • CI-Build : 프로젝트 빌드 - 2m 48s 소요
  • CI-Security : 보안 검사 - 5m 16s 소요
  • CI-Test : 테스트 실행 (메인 병목 지점) - 14m 57s 소요 (가장 최근 빌드 소요시간)

중복되는 ci-test

ci-test 는 평균 13분을 소요할 뿐만 아니라 Pull Request 와 master merge 후에도 실행되기 때문에 동일한 (그리고 긴) 작업이 중복 실행되어 배포 완료까지 오랜 시간이 걸렸습니다. 아래 이미지에서 마지막 두 항목은 동일한 PR 에서 발생하는 ci-test 과정으로, PR 단계와 마스터 브랜치 merge 이후 단계에서 중복 실행되는 것을 확인할 수 있습니다.

image1.png


불필요한 패키지 설치

ci-build 와 달리 ci-test 는 불필요한 패키지 설치와 소스 빌드 과정을 포함했기 때문에 실제 테스트를 실행 시간은 2분 내외였지만 그 외의 리소스 설치 과정에서 병목이 있었습니다. 이미 공식 버전 지원을 종료한 python2.7 소스 빌드PowerShell 다운/설치, 불필요한 시스템 패키지 업그레이드 등 작업 속도를 늦추는 많은 단계가 포함돼 있었습니다.


현재 CI-Test 시간 분석

  • ci-test: 평균 11분 (메인 병목)
  • master merge: merge 후 ci-test 중복 실행
  • 전체 CI 시간: +13분 (PR + Master 머지)

1차 최적화: 불필요한 작업 제거 (13분 → 5분)

1. 실행 트리거 최적화

마스터 브랜치 merge 후에는 중복 ci-test 가 중복 실행되지 않도록 트리거를 제한했습니다. 이로써 마스터 merge 전에만 ci-test 를 실행해 배포 프로세스 속도를 높일 수 있었습니다.

  • 성능 개선 : 전체 배포 시간 50% 단축

carbon.png

2. PowerShell 설치 제거

기존 ci-test.yaml 에는 powershell 설치 커맨드가 있었습니다. ci-test 를 수행하려면 python2.7 설치가 선행되어야 하지만 powershell 은 테스트 실행에 전혀 필요하지 않았고, 무엇보다 설치 후 어떠한 작업도 수행하지 않았기에 이를 제거 했습니다. 예상컨대 과거에는 powershell 로 python2.7 을 실행했지만 시스템이 진화하면서 powershell 없이도 python 을 실행할 수 있게 됐으나 미처 지우지 못한 코드가 남아있는 것으로 추정했습니다.

결과적으로 powershell 설치 과정을 제거함으로 75MB 크기의 패키지 다운로드를 생략하며 테스트 시간을 대폭 줄일 수 있었습니다.

image2.png

  • 성능 개선 : 56s ➡️ 0s

powershell 이란
  • Microsoft가 개발한 태스크 자동화 및 구성 관리 프레임워크로 CLI 와 스크립팅 언어로 자동화를 지원합니다.

3. Python 소스 빌드 제거

공식 actions/setup-python이 Python 2.7 지원을 중단하여 MatteoH2O1999/setup-python@v2 액션을 사용하여 ci-test 과정에서 매번 소스코드를 다운 받아 컴파일 했습니다. 해당 액션은 아래의 과정을 거칩니다:

  1. Python 2.7 바이너리 찾기 시도 → 실패 (EOL로 인한 바이너리 부재)
  2. 소스 코드 다운로드 → GitHub 에서 Python 2.7.18 소스코드 다운로드
  3. 의존성 설치 → build-essential, libssl-dev 등 컴파일 도구 설치
  4. 소스 컴파일 → ./configure, make, make install (가장 시간 소모적)
  5. 툴 캐시 복사 → 다음 실행을 위한 캐시 저장

위 과정에서 약 1분 30초가 소요되어 이를 python2.7 이 설치된 Docker Image 를 사용하도록 개선하여 6초 내외의 시간으로 단축할 수 있었습니다. python2.7 빌드를 위한 docker image 는 최소한의 용량으로 구성된 경량 이미지로 python2.7-slim 을 선택했습니다. slim 버전은 불필요한 빌드 도구, 문서 파일, 테스트 라이브러리 등이 제거되어 이미지 전체 크기를 크게 줄일 수 있습니다. alpine 이미지의 크기가 더 작다는 장점이 있지만 현재 프로젝트에서 자주 사용되는 C 확장 패키지 설치 시 복잡한 종속성 문제와 빌드 실패 가능성이 존재하기 때문에 slim 이미지를 선택했습니다. 실제로 H 프로젝트는 native C 확장 패키지인 psycopg2, cryptography 등을 여럿 사용중이기 때문에 Debian 기반의 패키지 호환성이 좋은 slim 이미지를 선택했습니다.

  • 성능 개선 : Python 빌드 시간 93% 단축

carbon-2.png


4. apt upgrade 제거

우선 Docker 컨테이너는 기본적으로 root 권한으로 실행되기 때문에 sudo 오버헤드가 불필요하여 명령어에서 모든 sudo 를 제거했습니다. 두번째로는 apt 대신 apt-get 으로 명령어를 수정했습니다. apt 명령어는 사용자 친화적 인터페이스를 제공해 진행률 표시 등 기능이 있지만 ci 과정에선 불필요하기 때문에 스크립트 용에 맞게 apt-get 으로 변경했습니다. 마지막으로 필요한 패키지만 설치하도록 필수 패키지를 명시했습니다. sudo apt update && sudo apt upgrade -y 명령어는 모든 패키지 업그레이드를 시도하는데, 이는 ci 라는 일회성 환경에서 보안 패치까지 적용하는 불필요한 작업이며 모든 패키지를 다운받는데 12MB 의 추가 다운로드에 1분 이상 소요되는 점도 문제였습니다. gnupg2 패키지는 PowerShell 저장소 추가용이지만 PowerShell 자체가 불필요하므로 제거했고, apt-transport-https 역시 apt 에서 이미 HTTPS 지원하여 불필요했습니다.

carbon-3.png

  • 빌드용: gcc, python-dev (pycrypto 컴파일용)
  • 네트워크: curl, wget, aria2
  • DB Client: default-mysql-client
  • 필수 라이브러리: libssl-dev, zlib1g-dev

1차 최적화 결과

여기까지 과정으로 ci-test 시간을 평균 13분에서 5분으로 60% 이상 단축할 수 있었습니다. 이 정도 최적화도 충분하다고 생각했고 이를 반영하여 1차적으로 상용 배포를 나갔습니다. 그리고 한달 정도 지나 새롭게 빌드를 할 때 ci-test 시간이 5~7분 정도로 불규칙하게 느려진다는 걸 알게됐습니다. 기존 최적화 과정에선 불필요한 라이브러리(PowerShell) 제거, Python2.7 slim image 사용, 필요한 패키지만 설치하도록 필수 패키지를 명시했습니다. 두번째 최적화 단계에선 이와 더불어 필수로 설치해야 할 라이브러리를 캐싱하여 최적화 하기로 했습니다.


2차 최적화: 캐싱 전략 개선 (5분 → 3분)

1차 최적화로 불필요한 작업을 제거했지만 여전히 매번 패키지를 설치하는 비효율적인 작업이 존재했고 평균 5분이라는 시간도 들쑥날쑥하여 이보다 오래 걸릴 때도 있었습니다.

지난 최적화가 불필요한 작업 제거에 초점을 뒀다면 이번 개선에서는 필요한 것을 재사용하는 캐싱의 관점으로 ci-test 시간을 추가 단축한 과정을 공유합니다. 캐싱과 더불어 불필요한 패키지 설치 과정 제거 및 Disk I/O 대신 RAM 을 사용하여 추가 개선을 도모했습니다.

1. pip 패키지 캐싱 전략의 근본적 개선

기존 방식의 문제점

초기 CI 과정에서는 컨테이너 환경에서 GitHub Actions의 actions/cache 가 제대로 동작하지 않았습니다. 이는 컨테이너 내부 경로가 호스트의 캐시와 분리되어 있기 때문입니다.

GitHub Actions와 컨테이너 환경의 관계

일반적으로 GitHub Actions 워크플로우는 runs-on 키워드로 지정된 러너(Runner)에서 실행됩니다. 이 러너는 일종의 호스트 머신으로 그 내부에 Docker를 설치하여 컨테이너를 실행합니다. 즉, CI-Test 과정은 다음과 같이 동작합니다.

  1. 호스트 머신 (Kubernetes 노드): GitHub Actions 러너가 동작하는 기반 환경입니다. 이 머신에 Docker 데몬이 설치되어 있습니다.
  2. 컨테이너 : 호스트 머신 위에서 실행되는 격리된 환경으로 python:2.7-slim 이미지를 사용해 모든 테스트 스크립트를 실행합니다.

이런 구조로 인해 actions/cache 가 생성하는 캐시는 호스트 머신의 특정 디렉터리에 저장됩니다. 반면 APT와 pip로 설치된 패키지들은 컨테이너 내부의 /var/cache/apt나 ~/.cache/pip 같은 경로에 저장됩니다. 두 공간이 다르기 때문에 캐시가 연결되지 않습니다.

마치 호스트 머신에 저장한 물건을 컨테이너 안에서 찾으려 하는 것과 같았습니다.

해결책: $GITHUB_WORKSPACE 경로 활용

이 문제를 해결하기 위해 $GITHUB_WORKSPACE 경로를 사용했습니다. 이 경로는 GitHub Actions가 워크플로우를 체크아웃하는 디렉터리로 호스트와 컨테이너 간에 공유(bind mount)되는 유일한 공간입니다. 즉 호스트와 컨테이너가 공통으로 접근할 수 있는 “공유 폴더” 역할을 하는 셈이죠.

컨테이너와 호스트가 유일하게 공유하는 $GITHUB_WORKSPACE 경로를 활용해 APT와 pip의 캐시 디렉토리를 설정했습니다.

APT 캐시

APT 캐시 디렉토리를 워크스페이스 하위(.apt/cache/archives, .apt/state/lists)로 변경하고 apt-get 명령에 –cache-dir 옵션을 명시적으로 추가했습니다.

carbon-4.png

APT 패키지를 먼저 다운로드만 하고(download-only), 그 후에 캐시에서 설치하는 2단계 방식을 적용하여 캐시 효과를 극대화했습니다.

pip 캐시: 다운로드 캐시에서 설치 패키지 캐싱으로

H 서버는 150여 개의 파이썬 라이브러리를 사용중입니다. 1차 개선 후에는 pip의 다운로드 캐시만 저장하고 있었습니다. 이 방식은 패키지 파일(.whl, .tar.gz)은 캐싱하지만, 설치된 패키지 자체는 캐싱하지 않습니다. 그래서 requirements.txt가 변경되지 않아도 매번 pip install을 실행해야 했죠.

carbon-6.png

AS-IS: 다운로드 캐시만 있는 경우

pip install 실행 과정:
1. ✅ PyPI에서 패키지 다운로드 (.whl, .tar.gz) → 캐시 가능
2. 다운로드한 파일 압축 해제 → 매번 실행
3. site-packages에 설치 → 매번 실행
4. 의존성 검사 및 바이너리 컴파일 → 매번 실행

즉 원격 서버에서 패키지를 받아오는 시간은 절약되지만 압축 해제, 설치, 컴파일 작업은 여전히 매번 실행되고 있었습니다.

TO-BE: 설치된 패키지 자체를 캐싱

Docker Layer 캐싱 원리에 따라 “변경이 적은 레이어는 위쪽에"라는 원칙을 적용했습니다. pip 패키지는 requirements.txt가 바뀌지 않는 한 동일하기 때문에 설치된 site-packages 디렉토리 자체를 캐싱하기로 했습니다. 요리에 빗대어 보면 기존 방식은 재료(패키지 파일)만 캐싱, 개선된 방식은 완성된 요리(설치된 패키지)를 통째로 캐싱합니다.

carbon-7.pngsite-packages 를 캐싱할까요? /usr/local/lib/python2.7/site-packages 는 pip 가 모든 패키지를 설치하는 최종 목적지 입니다. 이 디렉토리에는 압축 해제된 python 모듈과 컴파일 된 바이너리, 패키지 메타데이터 등의 정보가 모두 준비된 상태로 저장됩니다. 예를 들어 python 이 import django 를 실행할 수 있는 완성된 상태가 위 디렉토리에 준비되어 있는 것이죠. 즉 실행 가능 상태의 재료가 site-packages 에 저장되므로 이를 캐싱하여 2분 넘게 걸리던 불필요한 실행 작업을 건너 뛸 수 있습니다 (2min -> 0s)

carbon-8.png

2. APT 패키지 설치 최적화

조건부 실행으로 불필요한 작업 제거

기존에는 설치 여부를 확인하지 않은 채 매번 시스템 패키지를 설치했다면, 시스템 패키지가 설치 여부를 체크 후 다음 스텝으로 넘어가도록 구조를 개선했습니다. command -v는 명령어가 존재하는지 확인하는 쉘 내장 명령어입니다. 모든 필수 패키지가 이미 설치되어 있다면 apt-get을 실행하지 않고 바로 종료합니다. self-hosted runner 환경에서는 이전 빌드의 패키지가 남아있는 경우가 많아 이 체크만으로도 2-3분을 절약할 수 있었습니다.

carbon-9.png

3. MySQL 테스트 최적화

테스트용 MySQL은 디스크 I/O가 병목이 될 수 있어서 tmpfs(메모리)를 사용하도록 설정했습니다.

img4.png

tmpfs는 temporary file system 의 약자로 디스크가 아닌 RAM 에 파일 시스템을 올리는 개념입니다. 일반적인 파일 시스템과 동일하게 사용할 수 있지만 모든 데이터가 메모리에 저장되어 Disk I/O 보다 빠른 속도를 낼 수 있습니다. CI 테스트 환경에서는 데이터를 영구적으로 보관할 필요가 없을 뿐더러 ci 속도 향상을 위해서 빠른 read/write 가 가능한 tmpfs 를 사용하는 것이 적절하다고 판단했습니다.

carbon-10.png

MySQL의 데이터 디렉토리(/var/lib/mysql)를 tmpfs로 마운트하면:

  • 디스크 I/O 대신 메모리 I/O 사용
  • 테이블 생성, INSERT, SELECT 등 모든 DB 작업이 빨라짐
  • 컨테이너 종료 시 자동 clean up
  • 메모리가 부족할 경우 swap 영역(디스크)으로 스왑될 수 있습니다. CI 환경에서는 일반적으로 문제가 되지 않지만 완전한 메모리 전용 동작을 원한다면 충분한 메모리를 확보해야 합니다.


결과 및 향후 계획

전체 개선 여정: 13분 → 5분 → 3분

1차 개선 작업에서는 불필요한 작업을 제거하여 CI 시간을 13분에서 5분으로 단축했고, 2차 개선에서는 캐싱 전략을 근본적으로 바꿔 5분에서 3분 대로 추가 단축할 수 있었습니다.

이번 개선을 통해 무엇을 캐싱할지에 따라 결과가 달라지는 것을 볼 수 있었습니다. Docker Layer 캐싱 원칙에 따라 변경이 적은 부분을 상위 layer 에서 처리하며 requirements.txt가 변하지 않는 대부분의 PR 에서 패키지 설치를 완전히 생략할 수 있었습니다. 최초 PR 이 open 할 땐 packages 설치 과정이 수행되겠지만, 그 이후 부터는 별도의 PR 에서도 캐싱의 이점을 활용할 수 있어 개발 생산성 향상에 기여할 수 있었습니다.

  • ❌ 잘못된 캐싱: 중간 과정(다운로드 파일)만 캐싱
  • ✅ 올바른 캐싱: 최종 결과물(설치된 패키지) 캐싱

전체 ci-test 결과 차이

img3.png


추가 최적화 방안: Pre-built Docker 이미지

현재 CI 과정은 사이즈가 큰(e.g python2-slim, mysql:5.7, amazon/dynamodb-local) 컨테이너 이미지를 매번 pull 해야 합니다. 이미지가 self hosted runner 에 없다면 Docker hub 에서 이미지 다운 후 압축 해제 및 준비 과정까지 거쳐야 합니다.

위의 캐싱 전략으로 상당한 시간을 단축했지만, 궁극적인 해결책은 모든 의존성을 미리 설치해둔 Pre-built Docker 이미지를 사용하는 것입니다.

  • honeyscreen/Dockerfile.ci : 필요한 모든 패키지(apt-get, pip)를 미리 설치하는 CI 전용 Dockerfile을 정의합니다.
  • .github/workflows/build-ci-image.yaml : 이 Dockerfile을 이용해 CI 이미지를 빌드하고 ECR(Elastic Container Registry)에 푸시하는 워크플로우를 구성합니다.
  • .github/workflows/ci-test-optimized.yaml : 실제 CI-Test 워크플로우에서 이 사전 빌드된 이미지를 사용합니다.
# Runner 서버에서 1회만 실행 (초기 설정 또는 정기 업데이트 시)
docker pull python:2.7-slim
docker pull mysql:5.7.34
docker pull amazon/dynamodb-local:latest

이 방식을 적용하면 패키지 설치 단계 자체를 완전히 제거하여 더욱 빠른 CI 시간을 달성할 수 있습니다. 이는 Python 2.7 환경의 근본적인 한계를 극복하는 가장 효과적인 방법입니다.

마치며

두 번의 개선 작업을 거쳐 최종적으로 CI-Test 시간을 13분에서 3분으로 단축할 수 있었습니다. 단순 시간 절약을 넘어 빠르게 테스트하고 배포할 수 있는 환경을 구축하여 생산성 향상에도 기여할 수 있었습니다.

이번 CI 최적화는 Python 3 마이그레이션을 위한 첫 번째 단계였습니다. 다음 글에서는 Python 2에서 Python 3로 마이그레이션 과정과 마주한 도전과제들을 다룰 예정입니다.

버즈빌 개발자 지원하기 (클릭)

버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)

You May Also Like

버즈빌, 아마도 당신이 원하던 회사!

지원하기