견고한 서비스를 만들기 위한 팁
새로운 개발팀에 합류하여 서비스 개발, 운영을 한지 벌써 1년이 넘었습니다. 이 번글에서는 그동안 빅데이터 플랫폼 엔지니어로 활동을 하다가 서비스 개발/운영 업무를 하면서 느꼈던 내용을 정리해보려고 합니다. 이 글은 초기 몇 개월 지났을 때 대충 윤곽을 잡았던 글인데 그 동안 미루다가 시간이 내어 다시 정리합니다. 그러다 보니 초기에 느낀 내용하고, 시간이 조금 지난 이 시점에서의 느낌은 조금 다르긴 하네요.
현재 필자가 운영하고 있는 서비스는 2개국에 서비스를 운영하고 있으며 2개국 모두 해당 분야에서 1위 트래픽을 유지하고 있는 서비스입니다. Ruby and Rails로 개발되었으며 모든 AWS를 사용하고 있습니다. 이런 서비스를 견고하게 운영하기 위해서 여러 원칙을 가지고 있지만 필자의 입장에서는 대략 다음과 같은 원칙들이 새롭거나 좋았다고 생각되었던 점이었습니다.
하나의 소스 트리로 여러 국가 서비스가 가능해야 함
이 제목은 이 항목을 작성한 시점에는 유효하지만 지금은 조금 수정되었습니다. 제가 처음 개발팀에 합류했을 때에는 하나의 소스로 여러 국가를 서비스하였고, 추가로 다른 국가 서비스 오픈도 준비하고 있었습니다. 물론 코드 내에 if country = 'ko' 와 같은 코드들이 있기는 하지만 국가별로 별도의 브랜치를 운영하지는 않았습니다. 물론 갑자기 큰 기능이 들어오거나 급하게 해당 국가에만 발생하는 문제가 있는 경우에는 별도 브랜치를 만들어서 작업하지만, 작업 완료 후 가능한 빠른 시간 내에 master로 merge 하는 전략을 취했습니다.
이렇게 한 가장 큰 이유는 작은 개발팀으로 여러 국가의 버전을 관리하는 것이 비 효율적 이었기 때문입니다. 최근에는 이런 전략에 조금 수정 했는데 결론적으로 보면 큰 틀에서 하나의 소스트리를 유지하는 것에는 변함이 없는 것 같습니다. 새로운 전략을 구성한 이유는 다른 국가에도 자체 개발팀이 구축되어 자체 기능을 개발할 수 있게 되었기 때문입니다.
그렇다고 아예 새로운 서비스를 만드는 것이 아니라 추가 기능에 대해서만 주로 적용하고 있습니다. 추가 기능을 개발할 때 국가별 우선 순위가 다르기 때문에 제한된 리소스로 최대 효과를 내기 위해서는 자신의 지역에 맞는 최상위 우선 순위 기능을 개발하게 됩니다. 이때 해당 국가에서는 자신의 브랜치에 필요 기능을 추가하고, 이것을 주기적(1 ~ 2개월)으로 병합하는 방식을 취하고 있습니다. 이렇게 하면 양 국가에서 개발된 기능이 하나로 합쳐지는 효과를 볼 수 있습니다.
예를 들어 A국가의 경우 국내 네이버 검색엔진의 키워드 광고를 하기 어려운 상황이기 때문에 구글 검색 엔진의 SEO에 많이 집중하고 있습니다. 이렇게 SEO에 최적화된 코드가 계속해서 통합된 코드에 반영되기 때문에 한국에 배포되는 코드에도 그런 기능이 자동적으로 추가됩니다.
이렇게 하나의 소스 트리를 유지하기 위해서는 국가별 다른 상황을 고려하기 위해 많은 환경 설정 값을 사용하기 때문에 프로그램은 조금 복잡해질 수도 있습니다. 반면 새로운 국가에 서비스를 오픈하기 위해서 준비 사항은 레이블 번역, 레이아웃 에러 수정, 데이터 준비만 되면 오픈 준비는 완료 되었다고 할 수 있습니다.
클라우드 서비스 적극 활용
스타트업 회사의 특성 상 큰 개발 조직을 운영하기 어렵습니다. 특히 서비스 운영을 위한 인프라 전문 조직을 갖추는 것은 개발 조직을 갖추는 것보다 상대적으로 더 어렵습니다. 안정적인 인프라 운영을 위해서는 서버, 네트워크, DB, 보안 이 네가지 파트는 반드시 있어야 하는데 한 명이 이 네가지 영역을 모두 알기는 어렵습니다. 따라서 작은 조직에서 견고하게 서비스를 운영하기 위해서 저희는 클라우드 서비스를 적극 활용하고 있습니다. AWS에서 제공해주는 서비스를 포함해서 대략 다음과 같은 업무에 클라우드 서비스를 활용하고 있습니다.
- 웹서버 및 기타 서버, 데이터베이스 등 서비스 운영을 위한 인프라
- 소스코드 형상관리
- 이슈관리
- 메일발송
- 국제화 리소스 번역 관리
- 시스템 모니터링
- UserVoice
- 문서 작성 도구 및 협업 도구 등
개발자는 운영 서버, 운영 DB에 접근하지 못함
저도 합류하기 전에 이런 운영 방침에 대한 이야기를 들었는데 설마 100% 그렇게 하고 있는가에 의문점이 있었습니다. 실제로 저와 다른 개발자는 운영 서버 및 운영 DB에 한번도 접근을 못했습니다. 심지어는 스테이징 서버 및 DB에도 접근할 수 없습니다.
이렇게 하는 것은 보안 강화의 목적도 있지만 시스템의 안정성을 보장하기 위해서 입니다. 배포 담당자만 운영 서버, 운영 DB에 접근할 수 있으며 운영 DB에 직접 SQL을 이용하여 테이블 생성, 변경, 데이터 입력, 수정, 삭제 등의 작업은 실행하지 않습니다. 데이터를 수정하기 위해서는 반드시 그 데이터를 관리하는 화면을 만들어서 처리하도록 하고 있습니다.
물론 가끔 코드 변경 등으로 인해 전체 데이터를 한번에 수정해야 하는 경우가 있는데 이 경우 SQL 문을 시스템 운영자에게 전달하고 실행하게 하는 방식입니다. 이런 상황은 1 ~ 2달에 한번 정도 발생하는 것 같습니다.
개발팀에 합류한지 얼마 되지 않아서 특정 데이터 한두건의 문제로 서비스 일부 기능에 문제가 발생 하였는데 이때 단순히 데이터만 수정하면 바로 해결할 수 있음에도 불구하고 관리하는 화면을 만들어서 운영에 배포한 뒤 서비스의 문제를 해결할 수 있었습니다.
이런 정책을 가짐으로써 데이터 수정을 통해 발생할 수 있는 더 큰 문제를 미리 방지할 수 있고 동일한 문제가 다른 국가에서 발생하더라도 한국에 있는 개발자나 DBA 등의 지원없이 각 국가별 서비스 운영자가 직접 관리할 수 있게 되는 장점이 있습니다. 그리고 Rails의 scaffold 기능과 db migraation 기능을 이용하면 쉽게 프로그램 기반으로 데이터를 관리할 수 있는 기능을 만들 수 있습니다.
좋은 모니터링 체계 구축
개발자가 운영 서버에 접속하지 못하는 경우 가장 큰 문제는 에러 발생 시 에러 로그를 확인하기 어렵다는 것입니다. 이런 문제를 극복하기 위해서는 모니터링 체계를 잘 갖추어서 해결하고 있습니다.
이 모니터링 시스템 내에서 특정 기능에 에러가 발생하면 이 에러를 관리 화면에 보여 주고, 개발자는 이 화면에 나타난 에러를 기반으로 문제의 원인을 찾아 해결합니다. 이 에러 화면에서 Add to Issue Tracker 기능을 이용하여 바로 이슈로도 등록이 가능하게 되어 있습니다. 저희는 이런 모니터링 체계를 Newrelic 클라우드 서비스를 이용하고 있습니다. 다음은 특정 화면에서 에러가 발생했을 때의 Error Trace 를 보여주는 화면인데 소스 코드의 라인번호와 이때의 Request Parameter와 Agent, Referrer 등의 추가 정보를 보여주기 때문에 에러를 분석하는데 많은 도움이 됩니다.
에러를 숨기지 말고 적극 노출, 노출된 에러는 즉시 수정
에러를 처리할 때 어떻게 할지에 대해서 프로젝트 초기에 많은 고민을 하게 됩니다. 자바의 경우 일반적으로 하위 모듈의 모든 에러는 Controller까지 throws로 전파시키고 Controller에서 Log4j 등을 이용하여 출력하게 하는 경우가 많습니다. 이 경우에도 서버의 log 파일에 접근해야만 로그를 볼 수 있는데 운영 서버에 접근하지 못하는 경우 다른 전략을 찾아야 합니다.
저희가 선택한 전략은 로그를 Controller에서 catch하지 말고 적극적으로 노출시키는 전략을 취했습니다. 물론 사용자 화면까지 이에러를 노출시키지는 않고 사용자에게는 500 Internal Server 에러가 전송되고 이를 중간에서 다른 화면으로 교체시키고 있습니다. 이렇게 에러를 Catch 하지 않아야 위의 Newrelic 에서 보는 것과 같이 에러가 모니터링 시스템에 노출되게 됩니다.
에러를 노출하게 되면 자연스럽게 운영 중인 서비스의 에러 현황을 파악할 수 있고 이렇게 파악된 에러 중 우선 순위와 영향도를 고려하여 바로 Fix 하여 배포하는 방식으로 운영하고 있습니다.
에러를 로그로 보내게 되면 로그 파일을 보지 않거나 별도로 이 로그를 이용하여 에러 정보를 모니터링하는 체계를 별도로 구성하지 않으면 영원히 그 에러는 Fix 되지 않기 때문에 적극적으로 노출시키는 전략도 나쁘지 않은 방법이라고 생각합니다.
또 다른 전략으로는 kafka 등과 같은 로그 수집 시스템을 잘 갖추고 이를 이용하여 시스템의 로그를 파악하는 방법이 있습니다. 최근에는 이런 구성을 하여 로그 정보를 볼 수 있도록 하고도 있지만 Newrelic 에서 제공해주는 에러 로그 만으로도 충분히 디버깅이 가능하기 때문에 이 로그는 디버깅 용도로는 잘 활용하고 있지 않습니다.
코드의 Merge 권한은 최소한의 인력만 접근
개발자들이 직접 master 브랜치에 commit 또는 merge 할 수 있게 되면 가끔 발생하는 실수로 인해 의도하지 않는 코드가 운영 환경에 배포될 수도 있습니다. 이를 방지하기 위해 저희는 최상위 매니저급 두명만 코드 Merge 권한을 가지고 있습니다. 원래는 한명이었는데 백업 차원에서 두명이 권한을 가지고 있습니다.
이렇게 하면 Merge 하기 전에 해당 Pull Request에 대해 코드 리뷰 여부도 확인할 수 있으며 중요한 기능에 대해서는 Merge 권한을 가지고 있는 담당자가 다시 한번 코드를 리뷰할 수 있는 장점이 있습니다.
배포는 근무 시간 중에만
이 항목을 가장 추천해드리고 싶습니다. 저희는 다음 시간에는 배포하지 않는 것을 원칙으로 하고 있습니다.
- 근무 시간 이외
- 퇴근 시간 1시간 남았을 때
- 금요일 또는 휴일 전날
이유는 근무시간에 배포를 해야 문제 발생 시 원인 파악, 의사 결정 등을 신속하게 할 수 있기 때문입니다. 처음에 저도 여기에 적응이 안되어서 일자가 바뀌면 애매한 상황이 있는 기능을 배포하기 위해 밤 12에 배포하는 것으로 일정을 잡았다가 컴플레인을 받은 적이 있습니다. 물론 그때는 어쩔수 없이 야간에 배포 했지만 그 이후에는 긴급 장애 처리를 위한 배포를 제외하면 모두 근무시간에만 배포를 하였습니다.
시간과 관계된 배포의 경우에도 프로그램 적으로 잘 처리해서 두번에 나누어 배포하는 등의 방식으로 우회 하면서 근무시간에만 배포할 수 있는 방법을 찾아 배포하고 있습니다. 어려울 것 같지만 실제로 해보면 그렇게 어렵지는 않습니다.
가장 애매한 부분이 데이터베이스의 컬럼 변경 등과 같이 DB Lock이 오래 잡히는 경우입니다. 이 경우에도 근무시간 중 배포를 하고 있는데 이것은 저희 서비스의 성격때문에 가능하다고 할 수 있지만, 전략을 잘 잡으면 가능하다고 생각합니다.
글을 마치며.
지금까지 플랫폼 엔지니어가 서비스 운영을 하면서 느꼈던 내용 중 과거(2000년대 중반)의 경험에 비해 지금의 서비스 운영에서 느낀점 중에 "이렇게 하면 과거보다 견고한 서비스 운영이 가능하겠구나" 라는 생각이 드는 항목을 정리해 보았습니다. 물론 서비스 특성에 따라 좋은 점이 될수도 나쁜 점이 될 수도 있지만 이렇게도 운영하고 있구나 정도로 생각해주시면 좋겠습니다.
(Featured image: https://intland.com/blog/alm/service-desk-incorporating-customer-feedback-in-development/)