풀-스택 서비스 리팩토링하고 운영하는 후기

2019년부터 붙잡고 늘어지던 카페테리아 리팩토링 프로젝트가 드디어 끝을 향해 가고 있다.

왜 이 서비스를 선택했나

2학년 무렵에 카페테리아 앱이 처음 출시됐다. 핵심 기능은 ‘번호 알림’이었는데, 음식을 주문하고 받은 대기표의 주문번호를 등록하면 음식이 나왔을 때에 알림을 보내주는 기능이었다. 학식당에서 모니터만 목 빠지게 바라보던 학생들에게 구원같은 존재였다.

그런데 이게 런칭한지 몇 달도 안돼서 지원 중단됐다. 번호 알림은 학식당의 주문관리 시스템에 웹훅을 등록해서 주문 완료 이벤트가 발생할 때마다 카페테리아 서버로 HTTP 요청을 보내 주는 방식이었는데, 카페테리아 서버가 죽으면 응답을 기다리던 주문 시스템도 죽는 현상이 잦게 발생했다.

핵심 기능이 사라지고 앱과 서버도 방치된 채로 시간만 흘러가는게 너무 안타까웠다. 그래서 앱센터 들어오자마자 첫 번째 프로젝트로 선택했다.

어디서부터 뜯어고쳐볼까

앱센터에 지원할 때에 안드로이드 파트로 지원했다. 자연스럽게 안드로이드 앱을 맡게 됐다. 깃헙에서 소스를 클론해 주욱 살펴보니 잘 돌아가고 디버깅 가능한 나름 합리적인 설계가 구현되어 있었다. 그렇지만 당시 나는 막 클린 아키텍쳐와 ‘좋은’ 코드, 각종 기교에 심취해 있던 터라 눈이 잔뜩 높아진 상태였고, 뜯어고치기로 마음 먹었다.

새 프로젝트를 파고 app, domain, data, common, 이렇게 네 개의 모듈을 만들었다. 도메인 엔티티부터 짜고, repository 인터페이스를 만들었다. 그리고 네트워크 라이브러리를 붙이고 UI를 짜기 시작했다.

핵심이던 번호 알림 기능이 사라진 다음 남은 기능들을 잘 표현하기 위한 적절한 UI가 필요했다. 당시 디자인 쪽에서 도움을 받을 수 없어 그냥 어디선가 본 ‘잘빠진 예제 앱’ 스타일로 UI를 구성했다. 그리고는 서버가 주는 대로 잘 표시하는 앱을 만들어 3.0.0 버전을 붙여 스토어에 출시했다.

여기까지 했으면 마무리인가 싶겠지만, data 레이어를 짜다 보니 서버의 API를 새로 짜고 싶어졌다.

서버 프로그래밍 입문

졸지에 서버까지 짜게 됐다. 기술 스택은 Node.js에 commonJS를 사용하고 있었다. 새 서버는 ECMA Script 6를 사용하기로 결정했다. 그 이유는 다음과 같다:

  • es6는 import/export를 지원한다.

가장 큰 이유였다. 당시에는 타입스크립트에 관심이 없었기 때문에 적당한 현대성과 적당한 유연함을 제공하는 es6를 채택했다.

이후 서버를 짜면서 이어진 삽질은 이전 글에서 다루었다. 어마어마한 시행착오를 겪었고 많이 고통받았지만, 덕분에 머릿속에 방대한 ‘에러 대응 메뉴얼’을 구축했다.

인프라와 통신

서버가 생각보다 닿아있는 곳이 많았다. 단순히 모바일 클라이언트에게 데이터를 전달하는 정도가 아니었다.

  • 식단 API를 제공하며, 식단은 학교 앱 API로부터 가져온다.
  • 로그인 및 바코드 활성화 API를 제공하며, 학생 계정 검증은 교내 DB에 접근 가능한 API를 통해 수행한다.

여기까지가 사용자를 위한 서비스였다면, 학식당의 키오스크를 위한 API를 제공하기도 한다.

  • 학생이 바코드를 태그했을 때, 할인 승인 여부를 알려주는 API를 제공한다.

딱히 문서도 글도 없어서 소스를 땅 파듯이 파며 알아냈다. 다 정리해놓고 생각해보니 점점 문제가 커지고 있음을 느꼈다. 일단 식단 파싱부터 해결해야 했다.

식단 파싱

식단 정보는 처음에는 ‘스마트캠퍼스’ 앱이 사용하는 것과 같은 API를 사용했다. 그러나 해당 API는 포맷이 아주 지저분하고 알아볼 수도 없고 무엇보다 부정확했다.

식당이 있고 그 내부에 코너 이름이 있다. 그리고 코너별로 식단이 올라오는데, 위의 식단 API는 식단이 코너 이름에 정확히 대응되지 않았다. 그래서 식단 소스를 변경해기로 결정했다.

식단 정보 원본을 제공하는 곳은 교내 소비자생활협동조합(줄여서 ‘생협’)이었다. 식단은 생협 홈페이지를 통해 제공되고 있었기 때문에 적절한 HTML 파싱이 필요했다.

EC2 인스턴스를 통해 접속하면 IP를 차단하고는 사람임을 인증하라는 팝업을 띄웠다. 어쩔 수 없이 우회했다. Cafe24가 방화벽 포맷을 바꾸지 않는 이상 잘 작동할 것이다.

식단의 양식은 식단마다/날짜마다/메뉴마다 제각각이었다. 그 중에서 메뉴 이름과 가격, 열량을 뽑아내야 했다. 이를 구현하기 위해 선택한 최선의 방법은 모든 경우에 해당하는 정규표현식을 구비하는 것이었다.

가령 ‘(?[0-9,]+)원/(?[0-9,]+)[Kk]cal'같은 식이다. 이렇게 걸러낸 가격과 열량 부분을 제외한 나머지 부분이 메뉴 정보가 된다.

가져온 식단은 캐시에 보관하며, 1시간마다 캐시를 invalidate하도록 구현하였다.

할인 승인 API

학생이 키오스크에 바코드를 찍으면 여러 검증을 거쳐 할인을 승인 또는 거절한다. 이때 다음과 같은 검증 절차를 거쳐야 한다.

  • 해당 식당이 할인을 지원하는 시간대에 요청해야 한다.
  • 해당 식당이 할인을 지원해야 한다.
  • 할인받고자 하는 사용자의 로그인 이력이 있어야 한다.
  • 사용자의 바코드가 활성화되어 있어야 한다.
  • 갈은 날짜에 할인받은 기록이 없어야 한다.
  • 바코드를 15초 이내에 태그한 적이 없어야 한다.
  • 키오스크가 보내는 토큰이 유효해야 한다.

이렇게 7가지이다.

이 API를 구현하면서 가장 까다로웠던 것은, 기존의 API와 완전히 같은 동작(응답까지)을 재현하는 것이었다. 그러기 위해선 실제 환경에서 조건을 모두 맞춰 기존 서버의 응답을 관찰하고 분석하는 행위를 아주 많이 반복해야 했다.

조건에 따라 경우의 수가 상당히 많기 때문에(2^7) 테스트가 길고 복잡해졌다. DB와 네트워크 연결까지 필요했기 때문에 유닛 테스트만으로는 커버할 수가 없어 swagger documentation의 API 테스트 페이지를 주로 이용했다.

도메인과 SSL(또는 TLS)

기존 서버는 HTTP 연결만을 지원했다. 따라서 앱에서 로그인하면 비밀번호가 clear text로 전송되었다. 이 부분을 개선하고자 HTTPS 도입을 결정했다.

일단 인증서가 필요했다. 루트 인증 기관이 서명하였고 해당 사이트의 도메인에 대해 유효해야 했다. 도메인 이름이 사이트 ip의 단순 alias가 아닌 사이트의 ‘본질’에 가까운 것이었다. 그래서 GoDaddy로 가서 도메인을 발급받았다.

후에 HTTPS를 적용하기 위해 EC2 콘솔에서 로드밸런서를 만들고, ACM에서 생성한 위 도메인에 대한 인증서를 붙였다.

배포

다 만든 애플리케이션을 매번 터미널을 열고 git pull하여 내려받고 nohup npm start 할 수는 없는 노릇이었다. 앱 구동에 필요한 환경변수도 산더미 같았고, 재부팅 시에 자동으로 켜져야 했으며, 무엇보다 배포 과정이 실수가 끼어들 여지가 없을 만큼 간편하고 직관적이어야 했다.

초반에는 CodeDeploy를 검토했다. 허나 문서를 몇 번을 보아도 사용법이 이해도 안 되고 납득도 안 되어 직접 배포 시스템을 만들기로 결정하였다.

배포는 실 서버의 로컬에서 실행되는 스크립트에 의해 일어난다. 특정 트리거에 의해 배포 스크립트가 실행되면 스크립트는 저장소로부터 최신 버전의 앱을 내려받아 의존성을 설치하고 앱을 구동한다.

이때 무중단 배포를 실현하기 위해 blue-green 배포를 구현하였다. 전면에 nginx를 배치하고 서버 인스턴스를 두 개 띄워 둘 중 하나로 트래픽을 보내도록 하였다. 배포가 시작되면 nginx 리버스 프록시에 연결되지 않은 인스턴스를 종료한 뒤 업데이트 & 실행하고 프록시에 연결하도록 하였다.

배포 과정에서 문제가 생겼을 때를 대비해 각 동작(다운로드, 인스턴스 실행/종료, 프록시 연결 등)은 독립된 스크립트로 만들어 섬세한 작업이 가능하도록 만들었다. 그리고 인스턴스 상황을 보여주는 스크립트도 만들어 돌발상황에 당황하지 않고(명령어를 잊어버린다든가!) 대처할 수 있도록 하였다.

다시 안드로이드

새 서버가 배포를 준비중이던 때, 안드로이드 앱은 아직 이전 서버의 API를 사용하고 있었다. API만 업데이트하기에는 앱이 3.0.0 업데이트로부터 1년이나 지나 너무 낡았고 UI가 별로라는 평이 많았다. 그래서 또 새로 개발하기로 했다.

테마 색상과 Look-and-feel은 iOS 개발자님께서 제공해 주셨다. 그 위에 내가 UI를 새로 디자인했다. 인스타그램, 페이스북, 트위터, 앱스토어, 플레이스토어 등등 메이저 앱들을 레퍼런스 삼아 전형적인 탭 뷰 기반 스크롤 가능 UI를 가지는 앱을 만들었다. 그리고 4.0.0 버전을 달아 출시했다.

운영과 관리

앱과 서버를 배포해놓고 보니, 운영 관련하여 민원이나 요구사항이 들어왔을 때, 또는 공지를 전달하고 싶을 때, 서비스 동작을 바꾸고 싶을 때와 같은 상황에 대처할 수 있는 방법이 없었다. 보통 제대로 돌아가는 서비스라면 운영 포탈 또는 관리자 페이지 같은 것이 있기 마련이다. 그래서 하나 만들기로 했다.

앱/서버와 같이 프론트/백엔드가 분리된 웹앱/서버 스택으로 결정하였다. 서버는 타입스크립트에 도전해 보기로 했고, 처음 해보는 웹 프론트는 Vue를 쓰기로 했다. 타입스크립트 적응은 어렵지 않았다. 웹 프론트엔드가 어려웠다.

웹 개발 입문

SPA(단일 페이지 앱)을 개발하면서, 웹의 패러다임이 정말 많이 변했음을 느꼈다. 웹 프론트가 로컬에 다운로드되면 하나의 모바일 앱처럼 작동하며, 서버는 데이터만 전달해주는 방식이었다.

웹 프론트엔드는 아주 빠르게 성장중인 분야이고, 딱히 오래된 절대적 공룡 프레임워크가 없어 고민을 좀 했다. 그리고 Vue로 결정하였다. 그 이유는 다음과 같다.

  • React보다 입문이 쉽다고 하는 말을 어디선가 주워들었다.
  • React는 이미 너무 많이 쓰이고 있었다.
  • 대중적인 듯 하면서 마이너한 느낌이 왠지 정이 갔다.

처음 vue-cli로 앱을 만들자 정말 아무것도 없었다. 아 Vue가 모든 일을 해주지는 않는구나.. 하고는 UI 프레임워크를 찾아다니다가 Material design을 구현한 Vuetify를 선택했다. 그리고 공식 문서 페이지를 수백번씩 방문하며 꾸역꾸역 기능을 구현했다.

백엔드와의 통신에서는 GraphQL을 사용하였다. 딱딱한 REST에 살짝 질리던 참에 아주 신선한 기술이었다. 어차피 정적인 형식으로만 사용할 예정이었기에 오버헤드가 조금은 발생하였지만 신선함과 편리함이 더 컸기에 무시했다.

고객센터

진짜 서비스라면 공지 채널과 문의 채널이 있어야 한다. 특히 카페테리아는 사용자의 목소리에 응답한 적이 거의 없었기 때문에 더더욱 필요했다. 따라서 안드로이드 앱 4.1을 기획하며 다음 기능들을 만들기 시작했다:

  • 공지사항
  • 서비스 이용안내
  • 자주 묻는 질문
  • 앱 버전 체크(업데이트 확인)
  • 앱센터 카카오톡으로 문의
  • 생협 전화로 문의
  • 1:1 문의
  • 문의 내역

이중 서비스 이용안내와 자주 묻는 질문은 웹페이지로 만들어 앱 내 웹뷰에 띄우는 방식으로 구현했다. 웹 프론트 개발을 겪은 후였기 때문에 웹앱의 압도적인 생산성을 적극 도입하기로 결정했다.

나머지 기능들은 먼저 객체를 정의하는 것으로 시작했다. 엔티티를 설계하고, 필드를 확정하고, 컬럼 이름을 정하고, repository를 짜고, API를 짜고, UI까지 작성했다.

생각한 것이 증발하기 전에 구현하고 싶어 상당히 서둘렀다. 스스로를 쉬게 할 수 없었다는 표현이 더 정확할지도 모른다. 하루에 15시간씩 작업하며 4일만에 완성하였다.

협업

이렇게 만든 새 서버와 관리 페이지가 준비되어 있지만 아직 프로덕션에 투입되지는 않았다. 학식당 주문 시스템 개발사가 아직 이 서버를 사용하지 않고 있기 때문이다.

이유는 ‘이전에 학생들이 만들어놓은 것 때문에 시스템이 자주 뻗었기 때문’이었다. 그래서 업데이트에 회의적이었던 것. 이러한 인식을 되돌리고 설득하기 위해 애를 많이 먹었다. 지금은 드디어 승낙을 얻어 일이 진행되고 있다.

마치며

자력으로 서비스 완전체(앱, 서버, 배포시스템, 운영시스템)를 만들었다. 기획은 이미 있던 것이지만 코드는 모두 새로 작성하였다.

개발 기간은 길었지만 실제로는 몇 달 쉬다가 하루에 10커밋씩 몇 주 하고 또 몇 달 쉬는 식이었다. 시간이 충분히 주어져서 다행이다. 덕분에 코드가 충분히 안정된 상태까지 도달할 수 있었다.

프로젝트 코드 전체가 클린 아키텍쳐와 테스트-드리븐 개발 방법론의 영향을 크게 받았다. 다만 잘못 이해하거나 과하게 실천한(오버 엔지니어링) 부분이 다소 있을 것 같다.

프로젝트를 거칠 수록 다음엔 더 잘 할 수 있겠다는 생각이 든다. 충분한 시간과 여유가 있는 상태에서 모든 것을 직접 결정할 수 있는, 아주 좋은 기회였다.

댓글