Prologue

Production의 개발은 어렵습니다. 많은 고객들이 언제든 사용할 수 있어야 하기 때문에 높은 신뢰도와 성능이 요구됩니다. 때로는 Time to market을 위하여 많은 것을 포기해야 할 때도 있습니다. 가장 어려운 점은 이미 출발한 기차 위에서 기차를 수리해야 한다는 점입니다. Backward compatibility를 보장하고 무중단 배포를 하면서 변화를 만드는 일은 간단한 문제도 복잡하게 만듭니다. 제 때 해결하지 않은 작은 문제들이 계속 쌓여 감당하기 어려운 상황이 되기도 합니다. 피플펀드에서 프론트엔드 기술 부채를 일부 상환한 여정의 이야기입니다.

rocking_bird

Minified JavaScript

피플펀드의 프론트엔드는 JavaScript와 PHP로 구성되어 있습니다. 이 중에서도 JavaScript는 Vanilla JavaScript, jQuery, 두 가지 버전의 React가 혼용되고 있는 독특한 환경입니다. 이러한 복잡성에서 오는 불편함보다도 가장 생산성을 저해하는 요소는 바로 Minified JavaScript를 비롯한 Transpiled 파일들이 Git index에 포함되어 있다는 것이었습니다. Minified JavaScript 파일은 원본 JavaScript 파일에서 Indentation을 최소화하고 Line break를 없앤 것입니다. 프론트엔드에서는 제공하는 파일 크기를 줄여 로딩을 빠르게 하기 위하여 브라우저에 Minified JavaScript 파일을 제공하는 것이 일반적입니다.

Minifying in Deployment / Development

Minifying을 배포 단계에서 수행하는 경우 Deployment pipeline이 복잡해지고 배포 시간이 늘어나지만, Git index에는 원본 JavaScript만 포함되어 있기 때문에 Git을 자연스럽게 활용할 수 있는 개발 환경이 됩니다. 반면 Minifying을 개발 단계에서 수행하는 경우 Deployment pipeline이 간단해지고 배포를 git pull 만으로 순식간에 할 수 있지만, Git index에 Minified JavaScript 파일을 포함해야 합니다. 엔지니어가 적거나 각자 관여하는 파일이 명확하게 구분되는 경우, 이는 큰 문제가 아닐 수 있습니다. 그러나 엔지니어 수가 많아지고 여러 사람이 한 파일을 수정하는 일이 빈번해질 때 엄청난 생산성 저하로 이어지게 됩니다.

min_js

The Problem

Git은 Line-by-line difference를 바탕으로 파일의 수정 사항을 판단합니다. Minified JavaScript 파일은 한 줄로 이루어져 있기 때문에, 어떠한 수정도 전체 수정으로 인식됩니다. 따라서 같은 파일의 서로 다른 부분을 수정하고 각각 Minifying을 한 경우, Git에서는 100% 확률로 Conflict가 발생합니다. 심지어, 각 개발자의 Local 환경에서 Minifying을 진행하기 때문에 IDE와 Library의 버전 차이로 인하여 원본 JavaScript 파일이 동일하더라도 서로 다른 Minified Javascript 파일이 생성되어 Conflict가 발생하기도 합니다. 이 때문에 Checkout을 하는 것만으로 Unresolved conflict 가 생겨, 다시 Checkout을 하지 못하는 말도 안되는 상황이 자주 벌어집니다. 같은 파일이 여러 Branch에서 수정되는 경우, Checkout을 할 때마다 Conflict가 발생하고 Back merge를 할 때마다 Conflict가 발생하여 사실상 Multi-branch를 활용할 수 없는 상황에 이르게 됩니다. 사람이 불편함을 피하게 되는 것은 너무나 자연스러운 일입니다. 같은 파일을 수정하는 것이 불편함을 초래하는 상황에서, 엔지니어들은 서로에게 영향을 줄 가능성이 큰 공통 부분을 점점 덜 수정하게 되고, 소스 코드는 점점 더 일관성을 잃고 개발은 더욱 비효율적으로 이루어집니다. 새로 온 엔지니어는 일관성을 찾기 어려운 코드 속에서 어떻게 개발을 해야할지 혼란스러워 하게 되고, 구조의 수정은 더욱 어려워집니다. 악순환이 계속됩니다.

The Solution

문제를 해결하기 위하여 Minifying in Deployment를 적용하기로 합니다. 더불어, React compiling과 SASS compiling, CSS Minifying 을 모두 배포 단계에서 진행하여, Git index에서 Transpiled 파일들을 완전히 분리하기로 합니다. End output은 명확하고, 구현할 것도 많지 않습니다. 다만, 다양한 것이 많이 혼재되어 있는 기존의 Git index에서 서로 다른 역할의 파일들을 골라내고 각각 경우에 맞추어 처리하는 것이 어려운 일입니다.

현황 파악

이상적인 상황이라면 모든 JavaScript 파일이 React로부터 생성되고, 모든 Minified JavaScript 파일이 원본 JavaScript로부터 생성되고, 모든 CSS 파일이 SASS 파일로부터 생성될 것입니다. 그렇다면 .js 파일과 .css를 .gitignore에 추가하고 Git index에서 삭제한 후, Deployment pipeline에서 Transpiled 파일들을 생성하는 것이 전부입니다. 하지만 늘 그렇듯이 현실은 녹록치 않습니다. 간단한 문제였다면 이미 누군가 해결하고도 남았을 것입니다. 어떤 JavaScript 파일은 Vanilla JavaScript로 작성되어 있고, 어떤 JavaScript 파일은 jQuery와 React를 함께 사용하고, 어떤 JavaScript는 React로부터 생성되고, Minified JavaScript 파일이 React로부터 직접 생성되기도 합니다. 어떤 CSS 파일은 Vanilla CSS로 작성되어 있고, 어떤 CSS 파일은 SASS로부터 생성됩니다. 정말 압도적입니다.

moving_castle

대청소

.gitignore 파일만 수정했다가는 특정 페이지가 뜨지 않거나 스타일이 깨지는 등 대참사가 벌어질 것이 분명합니다. 앞선 경우들을 무엇을 기준으로 구분할 수 있을지 프론트엔드 엔지니어로 오래 일하신 동료분께 자문을 구합니다. 다행히도 각각의 경우에 해당하는 파일들은 특정 폴더에 모여 있거나, 파일 이름에 특정한 규칙을 갖고 있습니다. 그러나 강제할 수 없는 규칙이 항상 지켜졌을리 없습니다. 규칙과 다르게 작성되어 있는 파일을 찾아내 다른 폴더로 옮기고, 이름을 바꾸는 등 청소를 합니다. .gitignore가 점점 복잡해지고, git rm으로 이에 맞추어 Git index를 업데이트합니다. 개발 서버에 적용해보고 페이지를 돌아다니며 이상이 있는 페이지를 찾아 청소를 하거나 잘못 수정한 파일을 복원합니다. 청소하고, 테스트하고, 청소하고, 테스트합니다. 반복 작업이 지루할 때면 틈틈히 배포 스크립트를 작성합니다.

Extra Mile

이왕 청소를 시작했으니, 가능한 많은 부분을 청소하고 싶다는 생각이 듭니다. 더 이상 사용하지 않는 페이지, 미처 Fade out 시키지 못한 코드를 삭제해둔다면 나중에 이 코드를 만나는 누군가의 시간을 크게 아낄 수 있을 것입니다. 기술 스택이 충분히 갖추어지기 전에 소스 코드에 직접 포함해둔 리소스 파일, 일회성으로 만들었다가 까먹고 지우지 않은 파일, Entry point가 없어 실행될 수 없는 외딴 코드를 삭제합니다. 상상하기 어려운 방법으로 사용되고 있는 코드가 있을 수 있으므로, 오래 일하신 프론트엔드 엔지니어 동료분께도 다시 자문을 구합니다. 마침 비슷한 생각을 갖고 계셨던 다른 엔지니어 분께서 Access log parsing으로 최근 수 개월 동안 사용되지 않은 Endpoint를 정리해주신 덕에, 예상보다 수월하게 마무리할 수 있었습니다.

Deployment Pipeline

Minified JavaScript가 Git index에 포함되어 있던 기존의 배포 구조는 매우 단순합니다. 모든 서버에서 git pull을 하는 것이 곧 배포입니다. 그러나 이제는 모든 Transpiled 파일들을 배포 시에 생성해야 합니다. Node.js runtime이 필요하고, 길어진 배포 시간에서 비롯되는 각 서버의 파일 버전 차이가 만들어낼 수 있는 문제를 방지해야 합니다. AWS CodeDeploy를 공부하고 실험해보며, 목적을 달성할 수 있는 가장 단순한 방법으로 Pipeline을 구성합니다. Pipeline 역시 배포하고, 테스트하고, 배포하고, 테스트합니다.

Production Ready

Production 적용을 위하여 QA 환경에서 모든 페이지와 배포 스크립트를 테스트합니다. 문제는 예상하지 못한 곳에서 발생하기 마련이기에, 여러 안전 장치를 추가합니다. Transpiling이 실패했을 때 배포가 멈추도록 하고, 이후 단계에서 오류가 발생하면 배포가 멈추도록 하고, 문제가 생겼을 때 배포하는 사람이 즉시 알 수 있도록 스크립트를 확장합니다. 배포 과정에서 일어날 수 있는 여러 문제 상황과 실수, 각 경우의 모니터링 방법을 테스트해봅니다. 응급 상황에 대응할 수 있도록 여러 팀의 프론트엔드 엔지니어 분들과 일정을 맞춥니다.

대참사

큰 사고는 작은 실수에서 비롯되는 경우가 많습니다. Production의 Node.js 버전을 맞춰두는 것을 까먹었습니다. Node.js 버전이 달라 Transpilng이 실패하고, 배포 과정에서의 오류를 판단하는 기준인 Exit code도 달라 모든 안전 장치가 작동하지 않았습니다. 잘못된 배포가 전서버에 그대로 진행됩니다. 프론트엔드 전 서버가 약 40분간 다운되는 참사가 일어납니다. 서비스 도메인은 물론, 같은 페이지를 Web view로 포함하고 있던 모든 곳에서 화면이 날아가거나, 간헐적 오류가 발생합니다. 함께 대기해주시던 프론트엔드 엔지니어 분들과 CTO 분의 도움으로 여러 응급처치들을 해서 20분 후에는 서비스를 최소한으로 복구하고, 40분 후에 전서버를 복구합니다. 엔지니어 뿐만이 아니라, 모든 부서가 갑작스런 사고에 맞추어 대응합니다. 마케팅을 끄고, 파트너사에 상황을 공유하고, 각자의 역할에서 최선을 다합니다. 사고의 원인 및 해결 방안을 공유하는 것과 함께, 죄송하고 감사하다는 말씀을 전달합니다. 확인된 문제 원인은 물론, 다른 예외 상황을 떠올려보고 계획을 보완합니다.

disaster

재도전

QA 환경이 그때 그때의 상황에 따라 독자적으로 관리되고 있기 때문에, Production과 유사한 테스트 환경을 별도로 구축합니다. Node.js runtime 버전을 맞추고 모든 테스트를 다시 진행합니다. 페이지의 일부가 뜨지 않거나, 정상적으로 뜨지만 특정 기능이 동작하는 않는 페이지를 발견하여 수정합니다. 배포 시나리오별 테스트 또한 다시 진행합니다. 여전히 놓치고 있는 부분이 있을 수 있으나, 핵심 페이지와 동작에 대하여 충분히 테스트했다는 확신이 있기에 재도전을 준비합니다. 어떠한 상황에서도 문제의 크기를 줄일 수 있는 확실한 방법은 고객이 적은 시간대에 시도하는 것입니다. Production 적용 일정을 새벽 시간대로 잡고, 다시 한 번 동료분들께 도움을 청합니다. 혹여 놓친 페이지가 있을까 테스트 환경에서 구석구석 확인하고, 배포 스크립트를 수차례 확인하며 새벽을 기다립니다. 배포를 시작합니다. 모니터링 수치들을 확인합니다. 페이지들을 직접 확인합니다. 퇴근합니다.

Follow Up

추가적인 사고는 없었으나, 소스 코드 전체의 구조와 Deployment Pipeline이 바뀌는 큰 변화가 있었던 만큼 아직 안심할 수 없습니다. 이번에 모니터링을 강화한 덕에, 기존에도 발생했지만 모르고 있던 문제들을 발견하여 수정합니다. 며칠간 프론트엔드 배포 결과를 보면서 특이사항이 없는지 확인합니다. 배포 시간차가 길어질 때 간헐적으로 트래픽이 실패하는 현상이 나타납니다. 배포가 완료된 서버에서 Static 파일을 배포가 진행 중인 서버로 잘못 요청하는 오류가 있었습니다. AWS CodeDeploy 설정을 변경하고 테스트 환경에서 문제가 해결되는지 확인한 후, 다시 새벽 배포 일정을 잡습니다. 문제 해결을 확인합니다.

yogi_berra_quote

Epilogue

기술 부채를 해결하는 일은 늘 허무합니다. 얻게 되는 결과는 “너무나 당연하고, 당연했어야 하는” 것들입니다. 이제 피플펀드의 프론트엔드 엔지니어는 Multi-branch를 활용할 수 있게 되었습니다. 갈 길이 멉니다. 아직 남아있는 압도적 기술 부채를 생각하면 숨이 막힙니다. 하지만 이번에 함께 경험한 문제 정의와 해결, 사고 대응을 바탕으로 더 많은 기술 부채를 갚아나갈 수 있으리라 기대합니다. 그리고 점점 더 높아지는 생산성을 바탕으로 피플펀드가 더 좋은 성과를 내는 바탕을 마련할 수 있으리라 믿습니다.