LINE 광고 서버 개발 팀의 DevOps 문화

안녕하세요. LINE Ads에서 DSP(Demand Side Platform)를 개발하는 Demand Side Dev 3팀의 김남우입니다. Demand Side Dev 3팀은 일본과 태국, 대만 등 전 세계 LINE 사용자 및 LINE Ads 네트워크의 수많은 퍼블리셔들에게 광고를 송출하는 랭킹 서버를 개발하는 업무를 주로 맡고 있습니다. 광고 랭킹 서버에서는 광고주가 선택한 타깃에 맞는 사용자를 대상으로 사용자의 선호도, 주로 CTR(Click Through Rate, 클릭할 확률)을 예측하고 입찰한 가격을 기반으로 순위를 매겨 전달하는 것을 눈 깜빡하는 시간보다 빠른 100ms 이내에 전달하는 것을 목표로 하고 있습니다. 전 세계 사용자로부터 수신하는 대용량 데이터를 실시간으로 처리해 예측력을 높이면서, 타깃에 맞는 정확한 광고를 골라내고, 사용자 경험을 개선하기 위한 여러 로직을 짧은 시간 내에 수행하기 위해서는 정확한 구현과 지속적인 검증, 그리고 꾸준한 모니터링이 필요합니다. 

이번 글에서는 대용량 트래픽을 다루는 고성능 서버를 실수 없이 완벽하게 개발하기 위해 노력하고 있는 우리 팀의 DevOps 문화에 대해서 공유하려는데요. DevOps의 여러 프로세스 중에서 작업을 이슈로 관리(plan)하면서 코드를 작성(code)해서 빌드(build)하고 테스트(test)를 수행한 뒤 새 버전을 출시(release)해서 배포(deploy)하는 과정에 대한 사례를 공유하겠습니다.

 

DevOps란?

DevOps는 개발(development)과 운영(operations)의 합성어로, 소프트웨어의 개발부터 운영까지 전반에 걸친 과정을 통합하고 자동화하는 것을 목표로 하는 문화와 방식 및 도구를 말합니다.

DevOps 각 단계의 효율을 높이고 자동화함으로써 제품 개발과 출시에 걸리는 시간을 단축할 수 있고 고객 만족도를 높여 높은 성과를 달성할 수 있습니다. 또한 새롭게 출시하는 제품의 오류를 사전에 방지해 안정성을 높이고, 자동화된 파이프라인을 이용해 발생한 오류를 더욱 빠르게 복구할 수 있습니다. DevOps의 각 단계에 대해 딱 이것이라고 할 수 있는 정답이 있는 것은 아닙니다. 여러분들이 프로세스를 수립하는 데 도움이 되실 수 있도록 아래와 같이 제 생각을 정리해 보았습니다.

  • 기획(plan): 기획 단계에서는 개발하려는 제품에 대한 아이디어를 도출하고 이를 정의해 기술합니다. 작업은 작은 규모의 단위로 쪼개서 관리하며 진행 상황을 추적합니다.
  • 코드(code): 기획한 설계를 바탕으로 코드를 작성하는 단계입니다. 버전 관리 시스템(Version Control System)을 이용해 여러 사용자가 작성한 코드 작업을 조율하고 변경 사항을 추적합니다. 작성한 코드는 개발자들이 함께 모여 코드 리뷰를 진행하면서 일관성 있게 아키텍처를 구현하고 오류를 사전에 방지하며 제품에 대한 구성원들의 이해도를 높입니다.
  • 빌드(build): 완성한 코드를 묶어서 실행 가능한 상태로 만드는 단계입니다. 이 과정에서 구문(syntax) 오류를 점검할 수 있고 정적 분석을 통해 실행 오류나 불필요한 코드, 표준 위배 사항 등을 발견해 코드의 완성도를 높일 수 있습니다.
  • 테스트(test): 완성한 코드에 대해 단위(unit) 테스트나 정밀(sanity) 테스트, 통합(integration) 테스트 등을 수행해 발생 가능성이 있는 논리적 오류를 미연에 방지합니다.
  • 릴리스(release): 완성한 코드를 배포 가능한 상태로 만드는 단계입니다. 완성본에 버전을 붙이면 변경 사항의 규모를 쉽게 확인할 수 있고 완성본을 만든 시점도 기록할 수 있습니다.
  • 배포(deploy): 완성한 결과물을 실제 제품에 반영하는 단계입니다. 지속적으로 배포할 수 있는 도구로 빠르게 배포하면 변경 사항 배포에 대한 위험을 줄이면서 비용과 시간도 절약할 수 있습니다. 문제가 발생할 경우에 대비해 신속히 확인해서 롤백(rollback)할 수 있는 준비도 해야 합니다.
  • 운영(operate): 완성한 제품을 유지하고 보수하는 단계입니다. 타 시스템과 연동해 서비스의 확장성을 높일 수도 있고 장비를 증설해 보다 안정적으로 서비스를 운영할 수 있습니다.
  • 모니터링(monitor): 완성한 제품에 이상이 없는지 주기적으로 확인하는 단계입니다. 물리적 장비의 CPU와 메모리, 저장 공간 등의 사용률을 확인하거나 제품의 응답 시간과 응답 결과물의 정확도 등의 서비스 지표를 지속해서 관찰하며 발생할 수 있는 여러 위험에 미리 대비합니다.

 

DevOps 프로세스

기획

개발 작업을 진행하기에 앞서 전반적인 요구 사항을 담은 기획안을 Atlassian 사의 Confluence라는 위키를 활용해 작성하고 있습니다. 위키를 활용하면 파일 형태로 문서를 공유하는 것보다 쉽게 이력을 관리할 수 있고 실시간으로 협업할 수 있으며 누구든 자유롭게 편집하고 의견을 개진할 수 있어서 협업에 큰 도움이 됩니다. 요구 사항이 명확해지면 개발 상황을 추적하고 관리할 수 있도록 작은 이슈 단위로 쪼갭니다. 이슈를 관리하기 위해서 Atlassian 사의 Jira라는 이슈 트래커를 사용하고 있습니다. 단위 기능을 개발할 때 사용하는 각 이슈는 아래와 같이 6개의 상태로 나눕니다. 각 상태에 대해 설명하겠습니다.

 

작업 프로세스

  • Open: 이슈 트래커에 이슈를 처음 등록한 상태입니다. 요구 사항을 정리하고 담당자를 지정하는 등의 작업을 진행합니다.
  • In Progress: 개발자가 개발에 착수하면 변경하는 상태입니다.
  • [Beta] Deployed: 개발과 코드 리뷰를 완료하고 개발 브랜치에 반영되어 베타 서버에 적용된 경우 변경하는 상태입니다.
  • [Beta] Confirmed: 베타 서버에 반영한 작업 결과물에 대해서 QA와 같은 기능 검증이 완료된 경우 변경하는 상태입니다. 이 상태로 변경한 작업물은 언제든지 실제 환경에 배포할 수 있는 상태로 존재해야 합니다.
  • [Real] Deployed: 작업을 완료한 결과물이 실제 환경에 반영되면 변경하는 상태입니다.
  • Closed: 실제 환경에 반영한 결과물에 대한 최종 검증이 끝나면, 각 이슈를 담당하는 PM 분들이 이슈를 이 상태로 변경하면서 모든 과정이 완료됩니다.
작업 프로세스에 따라 표시한 칸반 보드

 

코드

브랜치 전략

개발자가 개발에 착수하면 이슈 트래커의 상태를 ‘In Progress’로 변경하고 코드 작업을 진행합니다. 현재 버전 관리 시스템으로 Git을 사용하고 있기 때문에 git-flow 기반으로 아래와 같은 브랜치 전략을 만들어서 활용하고 있습니다.

  • 개발(develop): 모든 개발 작업물은 이 브랜치를 기준으로 체크아웃(checkout)하고, 머지(merge)합니다.
  • 피처(feature): 단위 기능을 개발하는 브랜치입니다. 브랜치를 생성할 때 이슈 트래커의 번호를 이용해서(예: feature/LADS-2797) 결과물을 쉽게 추적할 수 있고 머지를 완료한 브랜치를 주기적으로 정리할 수 있습니다. 작업을 완료한 결과물은 코드 리뷰를 통과해야만 개발 브랜치로 머지할 수 있습니다.
  • 마스터(master): 모든 개발 작업물이 배포를 위해 마지막으로 모이는 브랜치입니다. 개발 브랜치의 결과물을 마스터 브랜치로 머지한 직후 배포를 위한 새 태그를 생성합니다. 태그는 v(메이저 버전).(YYMMDD).(당일 배포 횟수)와 같은 형식으로 생성하고 있습니다(예: v1.210416.0).
  • 핫픽스(hotfix): 배포까지 완료한 개발 작업물에서 버그가 발생해 긴급 대응이 필요한 경우 이 브랜치를 생성해서 바로 마스터 브랜치로 머지합니다.

위 브랜치 전략과 git-flow 기반 브랜치 전략의 차이 점은 위 전략에선 별도의 릴리스 브랜치가 존재하지 않는다는 점입니다. 그렇기 때문에 아직 개발 중인 일부 기능이 실제 환경에 배포될 수 있습니다. 이 문제를 해결하기 위해 피처 플래그(feature flags)를 활용해 개발 단계별로 사용 가능한 기능의 온 오프(On/Off) 여부를 관리하면서 필요한 시점에 플래그를 조작하는 것으로 기능의 반영 여부를 결정합니다. 또한 개발과 마스터 브랜치에 새로운 결과물을 머지할 때마다 자동화된 여러 테스트를 이용해 새로 추가된 기능의 안정성을 지속해서 검증하고 관리합니다. 자동화된 여러 테스트에 대해서는 이후 단계에서 상세히 설명할 예정입니다.

 

코드 리뷰

단위 기능을 개발하는 브랜치에서 코드 작업을 시작하면 개발자는 Git 저장소에 커밋하고 푸시합니다. 현재 글로벌 플랫폼을 개발하고 있기 때문에 여러 나라의 개발자들과 협업할 기회가 많은데요. 이때 의사소통을 원활하게 진행하기 위해 커밋 메시지와 PR(Pull Request)에 대한 설명은 전부 영어로 작성하고 있습니다.

추가로 커밋 로그에는 이슈 트래커의 번호(예: LADS-2797)를 말머리에 포함하도록 하고 있는데요. 이를 통해 커밋 내용이 어떤 이슈와 관련 있는지 명확하게 파악할 수 있으며 배포할 때 포함된 이슈들을 전부 목록으로 만들어서 집중적으로 모니터링할 기능들을 빠르게 파악할 수 있습니다.

기능 개발을 완료하면 코드 리뷰를 진행하기 위해 GitHub의 비즈니스 버전인 GitHub Enterprise를 이용해 PR을 보냅니다. PR을 보내면 팀 내 모든 개발자가 리뷰어가 되며, 최소 1명 이상의 승인을 받아야 개발 브랜치에 코드를 머지할 수 있습니다.

 

빌드와 테스트

현재 세 종류의 자동화 테스트를 실시하고 있습니다. 먼저 기본 프로그래밍 언어로 사용하는 Go 언어의 테스트 패키지 testing을 이용한 단위 테스트를 실시하고 있고, 임의의 광고 요청을 서버에 발생시켜 예상한 광고가 정상적으로 응답하는지 확인하는 정밀 테스트를 실시하고 있으며, 서로 다른 두 개의 장비에 똑같은 광고 요청을 보내서 응답 결과가 동일한지 비교하는 API Comparator를 실시하고 있습니다. 이와 같은 다양한 테스트를 개발 단계에 따라 적절히 적용해 전반적인 코드의 품질을 향상시키고 버그를 미연에 방지합니다.

각 테스트는 아래와 같은 조건에서 빌드와 함께 자동으로 수행됩니다. 테스트를 원하는 시점에 자동으로 수행할 수 있도록 전사적으로 사용하는 Jenkins를 이용해 설정해두었습니다.

  • PR을 새로 업로드했거나 PR에 새 커밋을 푸시한 경우: 단위 테스트
  • 개발 또는 마스터 브랜치에 코드를 머지한 경우: 단위 테스트, 정밀 테스트, API Comparator
  • 특정 주기로 반복: 단위 테스트, 정밀 테스트, API Comparator

 

단위 테스트

새로 PR이 올라오면 Jenkins CI를 통해 자동으로 단위 테스트를 실행합니다. 개발한 코드가 다른 코드에 영향을 주는지 사이드 이펙트를 확인하고 새로 추가한 코드에 대한 코드 커버리지를 높여서 버그 발생 가능성을 최소화하는 가장 기본적인 테스트입니다. 단위 테스트에 실패하면 해당 PR은 머지할 수 없도록 규칙을 정해 놓았습니다.

단위 테스트

 

정밀 테스트

미리 작성하거나 실제 환경에서 발생했던 요청을 샘플링해서 개발 서버에 전달해 광고 응답이 정상적으로 오는지 검증합니다. 이 과정에서 특정 형태의 광고 응답을 확인해 각 기능을 정밀하게 확인할 수 있습니다.

정밀 테스트

API Comparator

프로덕션에서 발생하는 실제 광고 요청을 샘플링한 후 보관해서 이를 기반으로 서버에 광고 요청을 보내 응답이 정상적으로 오는지 검증합니다. 추가로 API Comparator를 이용해 개발 브랜치에 코드를 머지할 때마다 개발 브랜치와 마스터 브랜치 간의 광고 랭킹 결과가 동일한지 비교할 수 있습니다.

API Comparator

새로운 기능을 추가하거나 랭킹에 변경 사항이 있는 경우에도 API Comparator를 이용해 변경된 부분에서 광고 응답 값이 달라지는지를 명시적으로 확인할 수 있는데요. 기능을 추가하거나 기존 기능을 개선할 때 아주 유용하게 사용할 수 있습니다.

 

릴리스와 배포

배포를 준비하기 위해서 먼저 개발 브랜치의 코드를 마스터 브랜치로 머지하는 PR을 올립니다. 개발 브랜치가 앞서 소개한 다양한 테스트를 거쳐 검증이 완료됐는지 추가로 확인합니다.

그 후 PR에 포함된 이슈들이 [Beta] Confirmed 상태로 전부 변경됐는지 확인합니다. 별도의 배포용 헬퍼 스크립트를 이용해 PR 번호를 입력하면 자동으로 Jira에 접속해서 이슈의 상태를 점검하고, 문제가 없으면 배포 버전을 Jira에 등록한 뒤 각 이슈에 배포 버전을 기록해 주고 [Real] Deployed 상태로 변경합니다.

모든 과정이 끝나면 PR을 마스터 브랜치로 머지합니다. 머지가 완료되면 마스터 브랜치의 최종 커밋을 기준으로 태그를 생성합니다. 태그는 추후 배포 시 기준점으로 사용합니다.

배포를 진행하기 전에 변경 공지를 등록하고 이번 배포 일정에 어떤 변경 사항이 포함되는지 PM 분들께 먼저 공유합니다. 이렇게 공유하면 작업 진행 상황을 알릴 수 있다는 장점이 있습니다.

배포가 시작되면 전체 공지 채널에 배포 공지를 올립니다.

이와 같은 공지는 혹시 누군가의 배포 때문에 다른 팀에 사이드 이펙트가 발생하는 경우 채널에 속한 모두가 현재 어떤 컴포넌트가 배포되고 있는지 빠르게 파악할 수 있다는 장점이 있습니다. 배포가 종료되면 배포 공지 스레드에 댓글을 달아서 배포 종료를 알립니다.

배포 전략으로는 카나리(canary) 배포 전략을 사용하고 있습니다. 석탄 광산에서 유독 가스에 민감한 카나리아라는 작은 새를 이용해 유독 가스 누출을 확인하던 방법에 착안한 전략입니다. 새로운 버전을 모든 서버가 아닌 일부 서버에만 배포해서 긴 시간 충분히 모니터링하며 이상 여부를 판단한 후에 나머지 서버에 배포하는 방법으로, 문제 발생 여부를 빠르게 판단할 수 있다는 장점이 있습니다. 아래 그림과 같이 처음에는 서버 한 대에만 배포하고, 모니터링을 완료하면 서버의 3분의 1부터 전체에 이르기까지 여러 번에 나누어 배포를 진행합니다.

배포하다가 장애가 발생하면 먼저 배포 공지 스레드에 현재 상황을 공유하고 배포 도구에 남아 있는 이전 버전의 결과물로 재배포를 진행합니다. 롤백 과정을 완료하면 장애가 발생한 내용의 PR을 되돌리고(revert) 새로운 버전의 태그를 추가로 발급한 다음 장애가 발생한 버전의 태그를 Git에서 제거합니다. 이렇게 하면 나중에 잘못된 버전으로 롤백하는 것을 방지할 수 있습니다.

 

맺으며

지금까지 DevOps의 여러 단계 중 일부 과정에 대한 경험을 소개했습니다. 2020년 한 해 동안 119번의 배포를 진행하면서 잘못된 배포 때문에 발생했던 장애는 단 1건뿐이었는데요. 이렇게 자주 배포하면서도 장애가 거의 발생하지 않았던 이유는 개발 시작부터 최종 배포까지 모든 단계를 유기적으로 통합하고 지속적으로 테스트하는 DevOps 환경을 잘 구축한 덕분이라고 생각합니다. 

DevOps 프로세스를 정립하는 데에 단 하나의 정답만 있는 것은 아닙니다. 먼저 개발 프로세스에서 가장 병목 현상이 심하고 효율화가 시급한 부분을 발견하는 것이 중요하며, 발견한 병목 과정에 적용할 수 있는 가장 쉬운 방법부터 차근차근 팀에 맞게 적용하는 것이 핵심이라고 생각합니다. 이번 글에서 공유드린 사례가 여러분 팀에 DevOps 문화를 조성해 나가는 데에 많은 도움이 될 수 있다면 좋겠습니다.