Smilegate Devcamp내에서 프로젝트를 하면서 겪었던 트러블 슈팅과 이에 대해 고민했던 글입니다.
빠른 코드 통합 및 자동 검증을 위해, GitHub Actions 내에서 pnpm과 Turborepo를 활용하여 Lint, build, type-check 등의 프로세스를 그룹화하여 CI 파이프라인을 구축했습니다. 하지만 초기 파이프라인 설계에서는 몇 가지 성능 병목이 발생했고, 이를 해결하기 위해 여러 단계의 최적화를 거쳤습니다. 이번 글에서는 CI/CD 환경을 최적화하는 과정과 성능 개선 결과를 정리해보겠습니다.
changes
, type-check
, lint
, build
, required
)은 별도의 ubuntu-latest 러너에서 실행됩니다.ubuntu-latest
러너는 기본적으로 2-core CPU, 7GB RAM, 14GB SSD를 제공합니다.actions/checkout
, pnpm/action-setup
, 의존성 설치 등).type-check
와 lint
는 changes
완료 후 병렬로 실행될 수 있지만, 각각 별도의 러너에서 실행됩니다.build
는 changes
, lint
, type-check
모두 완료될 때까지 기다려야 합니다.required
는 모든 작업이 완료될 때까지 기다린 후 상태를 확인합니다.NODE_OPTIONS: --max-old-space-size=4096
설정은 메모리 제한을 4GB로 설정하지만, 이는 7GB RAM 환경에서 여전히 제한적입니다.pnpm install
을 실행하므로, 의존성 설치 시간이 중복됩니다.needs
구문을 통해 작업 간 의존성을 설정하지만, 이로 인해 병렬 처리의 이점이 감소합니다.required
job이 모든 작업 상태를 확인하기 위해 별도의 러너를 사용하는 것은 리소스 낭비입니다.actions/setup-node
의 캐시 기능 활용하였습니다. (cache: 'pnpm'
)cache-dependency-path
)--prefer-offline
)로 네트워크 요청 최소화하였습니다.--parallel
플래그로 가능한 작업을 동시에 실행하였습니다.--filter
옵션으로 필요한 패키지만 선택적으로 처리하였습니다.type-check
는 제외하고 중요 작업인 lint
와 build
에만 집중하였습니다.CI 시간내에서 51s가 그렇게 빠르다고 매력적으로 다가오지 못했었고 여전한 하나의 job runner가 3개의 task가 전부 돌아간다라고 파악을 했기 때문에 오히려 메모리 경합(memory contention)과 리소스 사용 비효율성이 발생한다고 생각했습니다.
메모리 누수(Memory Leak) vs 메모리 경합(Memory Contention)이란?
- 메모리 누수(Memory Leak): 프로그램이 사용한 메모리를 해제하지 않아 점점 늘어나는 현상
- 메모리 경합(Memory Contention): 여러 프로세스가 한정된 메모리를 공유하면서 경쟁하는 현상
➡️ 결과적으로 CI 속도가 드라마틱하게 개선이 되지 않았습니다.
이후 테스트 코드를 점진적으로 작성을 하게 되면서 CI 시간은 다시 1분 14초대로 늘어나게 되어버렸습니다. 그래서 추가적인 개선이 필요했고 두 번째 개선을 진행하였습니다.
actions/setup-node
의 캐시 기능 활용하였습니다. (cache: 'pnpm'
)cache-dependency-path
)--prefer-offline
)로 네트워크 요청 최소화하였습니다.--parallel
플래그로 가능한 작업을 동시에 실행하였습니다.--filter
옵션으로 필요한 패키지만 선택적으로 처리하였습니다.type-check
는 제외하고 중요 작업인 lint
와 build
에만 집중하였습니다.2차 최적화 이후 49s까지 최적화하였으나 결국 build랑 test같은 경우 각 job runner 하나씩 job하나를 처리하고 있으므로 idle 상태가 생긴다고 판단했었습니다. 그래서 태스크 작업의 각 특성을 파악하여 새롭게 추가적으로 최적화를 진행하였습니다.
.turb
캐시를 추가하였습니다.🚀 결과적으로 30s까지 최대 29s까지 개선할 수 있었습니다!!!
CPU Bound와 IO Bound는 컴퓨터 프로그램이나 작업의 성능 병목 현상이 어디에서 발생하는지 설명하는 용어입니다.
CPU Bound (CPU 바운드)
IO Bound (입출력 바운드)
🧐 CI/CD 파이프라인에서 판단한 기준은요?
lint
와test
는 주로 많은 파일을 읽고 간단한 검증을 수행하므로 IO Bound에 가깝다라고 판단했습니다.- Code
build
, Bundling, Optimization은 복잡한 변환을 수행하므로 CPU Bound에 가깝습니다.
IO Bound의 핵심 특성
리소스 활용의 상보성
Turborepo의 최적화
--parallel
플래그를 사용하면 가능한 한 많은 태스크를 동시에 실행하면서도 리소스 제약을 고려합니다.캐싱의 영향
🔍 모던 운영체제의 스케줄링
현대 운영체제는 IO 작업과 CPU 작업을 효율적으로 스케줄링하도록 설계되어 있습니다.
IO 대기 시간 동안 다른 프로세스에 CPU를 할당하는 것을 잘 처리합니다.
2개의 vCPU 환경에서도 여러 IO Bound 작업을 효율적으로 멀티태스킹할 수 있습니다.
🎊 결론
lint:web
, lint:@workspace/ui
, test:web
과 같은 IO Bound 작업들은 하나의 Job Runner에서 동시에 실행되어도 서로 크게 방해하지 않고, 오히려 시스템 리소스를 더 효율적으로 활용할 수 있습니다. build
는 별도의 Job Runner에서 실행하는 것이 더 효율적입니다.2차 개선 이후 3차 개선을 진행하면서 운영체제의 특성을 고려한 최적화를 적용했습니다. 이 과정에서 제가 설정한 한계를 깨고 더 나은 방향으로 개선함으로써 큰 성취감을 느꼈습니다. 동시에 이전에 제한된 지식으로 쉽게 만족하려 했던 점을 반성하게 되었습니다. 시스템 최적화에는 CS 기초 지식이 얼마나 중요한지 실감했고, 이를 계기로 CS 공부를 더욱 깊이 있게 진행할 예정입니다.