키즈노트 CI/CD 파이프라인 진화기(6)
키즈노트 CI/CD 파이프라인 진화기: 없던 것을 만들고, 만든 것을 통합하기까지
Part 6 — 회고: 없던 것을 만들고 통합하기까지
이 글에 등장하는 수치와 인용은 실제 기록을 그대로 옮긴 것이 아니라, 당시 기억을 바탕으로 재구성한 예시입니다. 정확한 워딩이나 통계보다는 그 시기에 실제로 겪고 배운 것의 흐름을 전달하는 데 목적이 있습니다.
Frontend부터 시작해서 Mobile, Backend, 그리고 Shared Library까지. 플랫폼별로 따로 놀던 배포가 하나의 구조 안으로 들어오기까지 대략 1년이 넘는 시간이 걸렸다. 그 사이 파이프라인만 만든 게 아니라, 수없이 많은 배포 대응과 트러블슈팅을 반복했고, 그만큼 많은 개발자들과 부딪히고 조율했다.
이 글은 그 과정 전체를 마무리하며 남기는 회고다.
1. Outcome
정량적으로 달라진 것
| 지표 | Before | After |
|---|---|---|
| 배포 자동화 플랫폼 | Frontend만 부분 자동화, 나머지는 수동/개인 의존 | Frontend·iOS·Android·Backend 전 플랫폼 자동화 |
| 배포 스크립트 관리 | 플랫폼마다 중복, 잡 설정 화면에 개별 저장 | base 하나로 공통 로직 통합, Git으로 일원화 |
| 신규 서비스 파이프라인 셋업 | 매번 처음부터 스크립트 작성 | base의 공통 함수를 불러 쓰는 진입 스크립트 하나만 추가 |
| 배포 실패 원인 파악 | 콘솔 로그 전체를 처음부터 훑어야 함 | 단계별로 구조화된 로그로 실패 지점을 바로 특정 |
수치로 보면 단순한 개선처럼 보이지만, 그 이면에는 “배포 자동화 0개 플랫폼”에서 시작해 “전 플랫폼 자동화 + 공통화”까지 간 거리가 있었다.
배포 실패 원인 파악과 관련해서는, Shared Library 단계에서 각 스테이지의 결과(단계 이름, 성공/실패 여부, 소요 시간, 에러 메시지)를 구조화된 형태로 남기는 방식을 도입한 게 컸다. 이전에는 문제가 생기면 콘솔 로그를 처음부터 끝까지 읽어야 원인을 찾을 수 있었지만, 이후에는 실패한 스테이지와 에러 내용만 정리된 결과를 먼저 확인하고 필요할 때만 콘솔 로그로 들어가는 식으로 바뀌었다. 사소해 보이지만, 새벽에 장애 대응을 하는 입장에서는 원인 파악까지 걸리는 시간 자체가 곧 장애 대응 시간이었다.
정성적으로 달라진 것
숫자보다 더 크게 체감한 변화는 따로 있었다.
- “배포는 무서운 것”에서 “배포는 PR 머지처럼”으로. 예전에는 배포 = 담당자가 서버에 접속해서 명령어를 하나씩 입력하는, 실수하면 바로 장애로 이어지는 일이었다. 지금은 파라미터를 고르고 버튼을 누르는 일이 됐다.
- 담당자가 없어도 배포할 수 있는 구조가 됐다. 예전에는 배포할 수 있는 사람이 정해져 있었고, 그 사람이 자리를 비우면 배포 자체가 미뤄지는 일도 있었다. 지금은 누구든 파이프라인을 실행할 수 있었다.
- 장애가 나도 파이프라인 로그를 먼저 본다. 예전에는 장애가 나면 담당자에게 먼저 연락했지만, 이제는 파이프라인 로그를 먼저 열어보는 게 순서가 됐다.
이 변화들이 실제로 조직에 자리잡았다는 걸 체감한 순간들이 있었다. 배포 관련 문의가 나에게 오는 대신, 개발자들이 스스로 파이프라인 로그를 보고 원인을 찾아 알려주는 경우가 늘어난 것이다.
2. 기술적 회고
Scripted Pipeline 선택은 맞았는가
지금 다시 판단해도, 그 시점에는 Scripted Pipeline이 맞는 선택이었다고 생각한다. Declarative Pipeline은 문법이 정형화되어 있어 읽기는 편하지만, 서비스마다 완전히 다른 배포 로직(fabfile 기반, 명령어 기반, 정상적인 빌드가 있는 경우와 없는 경우)을 하나의 틀 안에 욱여넣기엔 제약이 많았다. Scripted Pipeline의 자유도가 없었다면 Backend의 7개 서비스를 각자의 사정에 맞게 옮기는 작업 자체가 훨씬 더 오래 걸렸을 것이다.
다만 그 자유도가 결국 스크립트 복잡도로 이어졌다는 것도 부정할 수 없다. 지금 다시 한다면, 처음부터 Shared Library의 vars/ 구조를 좀 더 계획적으로 나누고, 플랫폼별 진입 스크립트는 최대한 얇게 유지하는 규칙을 더 일찍 세웠을 것 같다.
Shared Library 추상화 수준 — 적절했는가
base 하나에 함수를 몰아넣는 방식은 시작 단계에서는 옳은 선택이었다. 무엇을 얼마나 나눌지 판단할 수 있을 만큼 데이터(중복 함수, 사용 빈도)가 쌓이기 전에 성급하게 구조부터 잡았다면, 오히려 잘못된 추상화에 발목 잡혔을 가능성이 크다. “일단 모으고, 필요해지면 나눈다”는 순서가 실제로는 더 안전했다.
다만 이 판단은 어디까지나 당시 규모 안에서의 이야기다. 서비스 수나 플랫폼 수가 지금보다 훨씬 많았다면, base 하나로 버티는 구조는 더 일찍 한계에 부딪혔을 것이다.
Git 관리를 처음부터 시작한 게 얼마나 다행이었는지
Part 1에서부터 파이프라인 스크립트를 Git 레포로 관리하기 시작했던 결정은, 돌아보면 이 시리즈 전체에서 가장 잘한 선택 중 하나였다. 형상관리만 하던 시절에도 최소한 “예전에 어떤 모습이었는지”를 되짚어볼 수 있었고, 그 이력이 있었기 때문에 Shared Library로 옮기는 작업도 무(無)에서 시작하지 않을 수 있었다. 만약 스크립트가 Jenkins 잡 설정 화면에만 존재했다면, Shared Library 전환은 코드를 모으는 작업이 아니라 코드를 처음부터 다시 발굴하는 작업이 됐을 것이다.
Jenkins의 한계를 몸으로 느끼다
전 플랫폼 표준화 작업을 어느 정도 마무리하고 나서, 오히려 Jenkins 자체의 한계가 더 선명하게 보이기 시작했다.
- 스크립트 가독성 저하. 로직이 복잡해질수록 스크립트를 짠 사람만 이해할 수 있는 수준이 됐다. Groovy와 셸 스크립트가 뒤섞인 코드는, 아무리 함수로 나눠도 결국 처음 짠 사람의 머릿속 구조를 따라가야 읽히는 경우가 많았다.
- 롤백의 어려움. 롤백 로직을 구현하는 것 자체는 가능했지만, 그럴수록 스크립트는 더 복잡해졌고, 결국 타겟 서버 리소스에 영향을 주는 방향으로 로직을 짤 수밖에 없었다. “배포”보다 “롤백”이 더 조심스러운 작업이 되어버린 셈이었다.
- 플러그인 의존성 리스크. Jenkins는 결국 플러그인 생태계 위에서 동작한다. 특정 플러그인이 더 이상 유지보수되지 않는 상황에 부딪히면, 스크립트 작업 자체가 막히는 경우도 있었다.
- 무거운 리소스 사용량. 빌드와 배포 자체가 Jenkins라는 무거운 애플리케이션 위에서 돌아가다 보니, 리소스 소모가 상당했다. 이 부분은 언젠가 반드시 손을 대야 하는 문제로 남아 있었다.
아이러니한 지점은 이거였다. 코드로 관리하고, 함수로 추상화하고, 공통 라이브러리로 묶어낸 것 — 애초에 목표했던 방향은 정확히 달성했다. 그런데 그 성취가 쌓일수록, 오히려 Jenkins라는 플랫폼 자체의 한계가 더 뚜렷하게 드러났다. 잘 만든 구조가 도구의 한계를 가려주진 않았다.
3. 조직적 회고
기술 표준화보다 어려웠던 “합의”
돌아보면 파이프라인을 만드는 일보다 어려웠던 건, 그 파이프라인을 둘러싼 사람들의 합의를 끌어내는 일이었다.
당시 같이 작업했던 선임이 했던 말이 아직도 기억에 남아있다.
“만약에 개발자들이 파이프라인 스크립트 중 실행되어야 하는 shell script를 작성했다고 해요. 그런데 그 shell script에서 오류가 났다면, 매번 그때마다 그 개발자한테 물어봐야 하는 거예요? 저희가 하는 쪽으로 가져올 수는 없었을까요? 결국 저희가 운영하고 관리해야 하잖아요.”
이 질문이 계기가 됐다. 배포와 관련된 로직은 개발자 개인의 기억이나 개인 스크립트에 맡겨두는 게 아니라, DevOps가 책임지고 흡수해서 관리할 수 있는 형태로 가져와야 한다는 방향이 여기서부터 분명해졌다. Shared Library로 공통 함수를 모으고, Git 기반으로 전환하고, 신뢰 기반 apply 사건을 겪으며 검증 절차를 만든 것도, 결국 이 질문에 대한 답을 계속 찾아가는 과정이었다.
각 파트(Frontend, iOS, Android, Backend)의 자율성과 표준화 사이에서 균형을 잡는 것도 쉽지 않았다. 어떤 파트는 자기 방식대로 하고 싶어 했고, 어떤 파트는 표준을 따르되 자기 요구사항이 반영되길 원했다. 완벽하게 만족스러운 균형점은 끝내 찾지 못했던 것 같다. 다만 표준화를 “강제”가 아니라 “가장 편한 기본값”으로 만들면, 자율성을 침해하지 않으면서도 자연스럽게 표준을 따르게 할 수 있다는 걸 배웠다.
“신뢰 기반 apply 사건”이 남긴 것
Part 5에서 다뤘던 체크아웃 사건은 단순한 버그 이야기가 아니었다. 그 사건이 진짜로 남긴 건 “공통으로 쓰는 코드는, 검증되지 않은 변화 하나가 전체에 영향을 줄 수 있다”는 감각이었다. 그리고 그 감각은 이후 모든 공통 코드 변경에 대한 태도를 바꿔놓았다. 편리함과 리스크는 항상 같이 온다는 걸, 장애를 통해서 배웠다.
안정성과 운영 비용
수도 없이 배포 대응과 트러블슈팅을 반복하면서, 기존 스크립트 로직을 개선하고 미처 파악하지 못했던 부분을 하나씩 찾아 조치했다. 그 과정에서 배운 건 단순했다. 플랫폼을 안정성 있게 만들지 못하면, 그 비용은 결국 운영과 유지보수로 돌아온다는 것. 그리고 그 비용은 DevOps 선에서 끝나지 않고, 개발팀과 기획팀까지 영향을 미치며 조직 전체의 운영 비용을 끌어올린다는 것.
이걸 체감하고 나서부터 스스로에게 묻는 질문이 바뀌었다. “어떻게 자동화할 것인가”에서 “어떻게 완성도 높은 플랫폼을 만들고 운영할 것인가”로. 이 전환이 이 시리즈 전체를 통틀어 가장 큰 방향 전환이었다고 생각한다.
DevOps는 도구가 아니라 문화
기술적으로는 Jenkins를 깊게 팠던 시간이었지만, 지나고 보니 가장 많이 배운 건 사람이었다. 1년이 넘는 시간 동안 서로 다른 개발자들과 소통하면서, 그들이 어떤 생각과 성향을 가지고 있는지, 기존 방식을 고수하는 개발자에게는 어떻게 다가가야 하는지, DevOps로서 개발자와 어떤 방식으로 협력해야 하는지를 수많은 시행착오를 통해 배웠다. iOS 팀처럼 전적으로 의존하는 경우, Frontend처럼 자신들에게 익숙한 도구를 쓰고 싶어 하는 경우, Backend처럼 서서히 표준을 받아들이는 경우 — 같은 표준화 작업이라도 파트마다 접근 방식을 다르게 가져가야 한다는 것도 이때 배웠다.
DevOps는 결국 도구를 잘 다루는 직무가 아니라, 도구를 통해 조직의 일하는 방식을 바꾸는 직무에 가까웠다.
4. 다음 단계
CI/CD 분리 — GitHub Actions와 Jenkins
Shared Library 작업이 마무리될 즈음, 개발자들 사이에서 CI 단계는 GitHub Actions로 하고 싶다는 흐름이 생기기 시작했다. Jenkins가 개발자들에게 익숙하지 않다는 게 가장 큰 이유였다. 그래서 CI는 GitHub Actions, CD는 Jenkins로 역할을 나누는 방향으로 설계를 바꿨다.
이 시점에 Jenkins 운영의 한계도 명확해진 상태였다. 스크립트 복잡도, 롤백의 어려움, 플러그인 의존성, 무거운 리소스 사용량 — 이 네 가지는 아무리 Shared Library로 코드를 잘 정리해도 해결되지 않는, 도구 자체의 한계였다. 결국 장기적으로는 GitHub Actions 기반으로 GitOps를 도입해 시스템을 고도화하는 방향이 더 합리적으로 보였고, 실제로 GitHub Actions 워크플로를 만들어 CI 단계부터 바꿔보는 시도를 시작했다.
완전한 GitOps로 가기 위한 남은 과제
Shared Library와 Git 기반 전환으로 “파이프라인 변경 = Git 커밋”이라는 초석은 놓았지만, 완전한 GitOps라고 부르기엔 아직 부족한 부분이 많았다. 배포 대상의 상태를 선언적으로 정의하고, 그 선언과 실제 운영 환경의 차이를 자동으로 감지·동기화하는 구조까지는 가지 못했다. Jenkins의 명령형(imperative) 스크립트로는 애초에 한계가 있는 목표이기도 했다. 이 지점이, 다음 여정에서 GitHub Actions와 선언적 배포 도구들을 검토하게 된 이유였다.
천재교육으로 이어진 것들
키즈노트에서의 시간은 단순히 Jenkins로 파이프라인을 만든 경험에 그치지 않았다. 카카오 공동체 소속으로 수많은 공동체 동료들과 소통하며 다양한 PoC를 진행했고, 그 과정에서 쿠버네티스를 비롯한 여러 기술을 실제로 테스트해볼 기회가 있었다. 그렇게 쌓인 인사이트는 이후 천재교육에서 플랫폼을 설계하고 운영하는 과정에서 그대로 힘을 발휘했다. 특히 쿠버네티스 기반 설계와 운영에는, 키즈노트에서의 PoC 경험이 직접적인 밑거름이 됐다.
기술적인 인사이트뿐만이 아니었다. 플랫폼을 어떻게 완성도 있게 만들 것인가에 대한 고민, 그리고 개발자를 대하는 태도 — 이 두 가지 역시 천재교육에서 플랫폼을 설계하고 운영하는 내내 그대로 이어졌다.
5. 글을 마치며
이 시리즈는 하나의 파이프라인을 만든 이야기가 아니라, 없던 표준을 만들고, 그걸 조직 전체가 쓸 수 있는 형태로 통합해나간 이야기였다.
Frontend에서는 파이프라인을 만드는 법을 배웠고, Mobile에서는 조직과 협업하는 법을 배웠고, Backend에서는 운영 환경을 해석하는 법을 배웠고, Shared Library에서는 공통 코드를 안전하게 다루는 법을 배웠다. 그리고 이 모든 과정을 통틀어, 결국 가장 크게 배운 건 기술이 아니라 사람이었다.
없던 것을 만들고, 만든 것을 통합하기까지. 그 여정은 여기서 일단락되지만, 그 경험이 남긴 방향성은 이후에도 계속 이어졌다.
본 시리즈는 총 6부로 구성됩니다.
| 파트 | 제목 |
|---|---|
| Part 1 | 시작 전야: 플랫폼마다 달랐던 배포 풍경 |
| Part 2 | 첫 번째 삽: Frontend Pipeline 만들기 |
| Part 3 | Mobile로의 확장: iOS와 Android Pipeline 구축 |
| Part 4 | 마지막 퍼즐: Backend Pipeline 구축 |
| Part 5 | 중복을 코드로: Shared Library 도입과 Git 기반 전환 |
| Part 6 | 회고: 없던 것을 만들고 통합하기까지 ← 현재 글 (완결) |