# Buzzvil Tech Blog — Full Content > Engineering and product insights from Buzzvil — Korea's leading reward advertising platform. ## About Buzzvil Tech is the engineering blog of Buzzvil, covering backend, frontend, data & ML, DevOps, design, product, and culture topics. This file contains the full prose of every blog post, seminar, and handbook page — no truncation. For an index with token counts, see https://tech.buzzvil.com/llms.txt. For per-post Markdown with absolute image URLs and frontmatter, append `.md` to any /blog, /seminar, or /handbook URL. ## Sections - [Blog](https://tech.buzzvil.com/blog): Technical articles from the engineering team (97 posts) - [Seminar](https://tech.buzzvil.com/seminar): Internal tech talks and presentations (82 talks) - [Handbook](https://tech.buzzvil.com/handbook): Engineering handbook and guidelines (23 pages) ## Blog Posts --- ## [이제 디자이너가 PR을 올립니다](https://tech.buzzvil.com/blog/designers-open-the-prs-now) Date: 2026-09-08 | Author: Maxence Mauduit | Category: Design *이 글은 [영문 원본](https://mmaxence.me/blog/designers-open-the-prs-now/)을 바탕으로 작성되었습니다.* 6개월, 432개의 pull request, 디자인팀 하나. 무엇이 좋아졌고 무엇을 치렀는지 정리했습니다. 얼마 전, 저희 디자이너 한 명이 광고 참여 플로우에서 행운뽑기 컨셉을 살펴보고 있었습니다. 뭘 고치던 참은 아니었습니다. 방향이 아직 잡히기 전에 이리저리 둘러보는, 그런 시간이었습니다. 그런데 화면이 튀었습니다. 길어야 1초입니다. 광고 오버레이를 열었다 닫으면, 보고 있던 위치를 지키지 않고 화면이 맨 위로 돌아갑니다. 기능은 멀쩡했습니다. 에러도 없고, 크래시도 없고, QA 티켓에 적힐 만한 것도 없었습니다. 꽤 오래 라이브 상태였는데 아무도 눈치채지 못했습니다. 동작만 놓고 보면 문제가 없었으니까요. Rina는 눈치챘습니다. Claude와 페어링해 원인을 파고들었고, 공유 훅 `usePreventScroll`이 오버레이 두 개가 동시에 스크롤을 잠글 때 저장해둔 스크롤 위치를 0으로 덮어쓴다는 것을 찾아냈습니다. 참조 카운팅 방식으로 근본 원인을 고치고, 회귀 테스트까지 붙여 PR을 올렸습니다. ![Finn이 LGTM으로 PR을 승인하고, 14개 체크를 통과한 뒤 브랜치가 머지된 화면](https://tech.buzzvil.com/blog/designers-open-the-prs-now/pr-10939-approval-merge.png) PR을 연 지 46분 만에 프로덕션 배포. 그날 안에 끝났습니다. 그리고 수정이 공유 훅에 들어갔기 때문에, 그 오버레이를 쓰는 모든 파트너 앱이 한꺼번에 좋아졌습니다. 이 부분은 PR에 붙은 라벨만 봐도 바로 보입니다. ![buzzbenefit, kbank, kbstar, monimo, samsung-wallet 라벨이 붙은 채 머지된 pull request 10939](https://tech.buzzvil.com/blog/designers-open-the-prs-now/pr-10939-header-labels.png) 그러자 프론트엔드 개발자 Finn이 채널에 이렇게 남겼습니다. > 오오 직접 배포하는 디자이너 🕺 이 글의 주인공은 그날 하루가 아닙니다. 그날을 당연한 하루로 만들어준 6개월이 주인공입니다. --- ### 디자인 PR이 대체 뭔가요 먼저 반론부터 짚고 가겠습니다. 늘 똑같은 질문이 나오고, 충분히 나올 만한 질문입니다. *디자이너가 프로덕션 레포에 코드를 올린다고요? 제정신인가요?* 저희 팀의 물꼬를 튼 건 이 비유였습니다. **PR은 Google Docs의 제안 모드 편집과 같습니다.** 담당 엔지니어의 리뷰 없이는 아무것도 머지되지 않습니다. 다른 모든 변경과 똑같은 관문, 그러니까 lint와 타입 체크와 테스트와 CI와 코드 리뷰를 통과하기 전에는 사용자에게 닿지 않습니다. 권한을 가진 누군가가 승인하기 전까지, 디자이너 브랜치의 영향 범위는 0입니다. 바뀐 것은 안전장치가 아닙니다. 바뀐 것은 결과물입니다. 예전에는 Figma 파일 하나에 티켓 하나, Slack 스레드 하나, 그리고 "이 간격 이거 맞나요?" 같은 후속 질문까지 흩어져 있었습니다. 지금은 의도와 구현과 diff와 리뷰 대화와 배포가 전부 담긴 객체 하나가 있습니다. 디자인 PR이 곧 스펙입니다. 참고로 저희도 처음부터 여기 있었던 건 아닙니다. Figma Make로 프로토타입을 만들었고, 그다음엔 Vercel에 HTML 프로토타입을 올렸습니다. 탐색용으로는 나쁘지 않았지만 딜리버리에는 쓸 수 없었습니다. 디자이너가 컨텍스트를 아무리 넣어줘도, 레포 밖의 프로토타입은 우리 토큰도 컴포넌트도 데이터 구조도, 코드베이스가 이미 내려둔 마흔 가지 결정도 알지 못하니까요. 그걸 인정하기까지 두 달을 썼습니다. 실제 프로덕션 레포로 들어간 것이 속도와 품질을 한꺼번에 바꿔놓았습니다. 이유는 글 마지막에서 다시 이야기하겠습니다. 에이전트가 비로소 자기가 무엇 위에 쌓아 올리는지 보게 된 겁니다. --- ### 숫자부터 보겠습니다 6개월 동안 디자인팀 구성원이 저희 레포 전체에 올린 pull request 전부입니다. ![2026년 월별 디자인팀 pull request. 3월 10건에서 7월 145건으로 늘고, 참여한 팀 비율은 17%에서 100%로 올라갑니다](https://tech.buzzvil.com/blog/designers-open-the-prs-now/prs-by-month.svg "2026년 월별 디자인팀 pull request") | | 2026 Q1 | 2026 Q2 | 2026 Q3 (9/4 기준) | |---|---|---|---| | 올린 PR | 10 | **177** | **245** | | 참여한 팀 비율 | 17% | **100%** | **100%** | | 머지율 | 90% | 82% | 87% | | 작업한 레포 | 3 | 14 | 15 | 이 표에서 중요한 건 두 가지고, 둘 다 합계가 아닙니다. **참여한 팀 비율.** Q1의 PR은 전부 제 것이었습니다. 리드 한 명이 혼자 실험해본 수준이었고, 대부분의 팀이 딱 여기까지 하고 멈춥니다. Q2가 되어서야 리드의 취미가 아니라 팀의 기본값이 되었습니다. 저 줄이 100%에 닿은 것이 결과입니다. 177이라는 숫자는 그 결과가 만들어낸 것일 뿐입니다. **머지율.** 디자인 PR의 82%와 87%가 머지됩니다. 나머지는 닫힌 탐색용 브랜치, 다른 PR로 대체된 스택, 그리고 리뷰에서 걸러진 나쁜 아이디어 몇 개입니다. 나쁜 아이디어는 원래 거기서 죽는 게 맞습니다. 디자인 PR의 머지율이 40%라면 워크플로우가 아니라 스코핑에 문제가 있는 겁니다. 이번엔 반대편에서 온 숫자입니다. Figma Dev Mode는 엔지니어가 디자인을 읽고 손으로 다시 만들라고 있는 기능입니다. 딜리버리가 정말로 코드로 옮겨갔다면, 그 통로는 저절로 비어야 합니다. ![활성 Figma Dev Mode 시트 수: 2026년 6월 23개, 7월 3개, 8월 4개](https://tech.buzzvil.com/blog/designers-open-the-prs-now/figma-devmode-seats.svg) 비었습니다. 강제한 사람은 없습니다. 엔지니어링 팀들에게 Dev Mode가 아직 필요한지 물었고, 다들 시트를 반납했습니다. 그 시트가 존재하던 이유인 핸드오프 자체가 사라졌기 때문입니다. 8월에 다시 늘어난 한 자리는 이 글 전체에 붙는 솔직한 각주입니다. 아직 Figma 우선 핸드오프로 돌아가는 파트너 커스터마이징 프로젝트가 하나 남아 있습니다. --- ### 탐색은 넓게, 딜리버리는 한 길로 그렇다고 Figma가 적이 된 건 아닙니다. "디자이너가 코드를 짠다"는 말을 대부분 "디자이너가 캔버스를 버렸다"로 읽으시는데, 그런 일은 일어나지 않았습니다. **탐색은 뭐든 씁니다.** 캔버스, 한 번 쓰고 버릴 HTML 목업, 코드로 만든 프로토타입, 화살표를 그려 넣은 스크린샷. 여덟 개의 방향을 한 화면에 펼쳐놓고 한눈에 비교하는 데 Figma만 한 도구를 아직 못 봤습니다. 코드는 선형이라, 한 방향에 시간을 쓰기 시작하면 곧바로 그 방향을 변호하게 됩니다. 답에 가장 빨리 닿는 도구를 쓰시고, 그 산출물은 버릴 것으로 대하시면 됩니다. 실제로 버릴 것이니까요. **딜리버리는 오직 코드베이스입니다.** 한 길뿐이고 예외는 없습니다. 사용자에게 나가는 것이라면 레포 안에서 만들어졌고, 테스트가 있고, 리뷰를 거쳤고, CI를 통과했습니다. 방향이 정해지는 순간부터 그것은 제품의 그림이 아니라 제품 그 자체가 됩니다. **그리고 Figma는 우리 디자인 시스템을 가진 적이 없습니다. 사본을 가지고 있었을 뿐입니다.** 시스템은 처음부터 코드베이스에 있었습니다. 토큰, 컴포넌트, 동작, 제품을 이루는 실제 재료 말입니다. Figma에 있던 것은 손으로 맞춰주는 거울이었고, 손으로 맞추는 거울은 결국 주기를 정해놓고 어긋나는 것에 지나지 않습니다. Figma 라이브러리와 코드 중 뭐가 맞는지 한 번이라도 다퉈본 팀이라면 답을 이미 알고 계실 겁니다. 언제나 코드였습니다. 사용자가 만지는 것이 코드니까요. 그래서 거울 관리를 그만뒀습니다. 지금 디자이너는 시스템이 실제로 사는 곳에서 시스템을 바꿉니다. 같은 레포, 같은 PR, 엔지니어의 변경과 똑같은 리뷰. 토큰이 움직이면 한 번만 움직입니다. 한번 해보세요. 몸이 훨씬 가벼워집니다. ^^ 그러니 워크플로우를 따라 하기 전에 이것부터 답해보시면 좋겠습니다. 우리 디자인 시스템은 실제로 어디에 사는가. 그리고 사본 관리를 그만두시면 됩니다. --- ### 무엇이 좋아졌나 **기다림이 사라졌습니다.** 다른 디자이너 한 명은 왜 직접 코드를 짜기 시작했느냐는 질문에 이렇게 답했습니다. > FE 엔지니어들이 디버깅으로 너무 바빠서, 그냥 제가 하기 시작했어요. 그 변경은 같은 주에 머지됐습니다. 예전 방식이었다면 목업 하나, 티켓 하나, 스프린트 경계 한 번, 그리고 버튼 하나 옮기려고 2주를 기다려야 했습니다. **실제 조건에서, 출시 전에.** 코드 프로토타입은 진짜 데이터로, 진짜 화면 폭에서, 진짜 컴포넌트로 돌아갑니다. 엣지 케이스가 출시 이틀 뒤가 아니라 디자인하는 도중에 드러납니다. 반응형은 캔버스에서 흉내 낼 수 없고, 해보신 분들은 다 아십니다. **우선순위에서 늘 밀려나던 것들.** 모션, 트랜지션, 제품을 의도적으로 느껴지게 만드는 작은 타이밍 결정들. 명세를 쓰는 비용이 커서 매번 우선순위 싸움에서 졌던 것들입니다. 이제는 디자이너가 그냥 합니다. 싸울 일 자체가 없어집니다. **수정이 공유 레이어에 쌓입니다.** 코드베이스 안에서는 동작이 실제로 사는 곳, 보통은 공유 컴포넌트나 훅을 고치게 됩니다. 한 번 고치면 그것을 쓰는 모든 화면이 함께 좋아집니다. 디자인 품질이 화면 단위를 벗어났습니다. **만든 그대로 사용자에게 갑니다.** 개인적으로 가장 중요하게 생각하는 부분입니다. 15년 동안 우리는 디자인과 구현 사이의 손실을 사업 비용처럼 받아들였습니다. 이제는 선택 사항입니다. --- ### 무엇을 치렀나 여기서 멈춘다면 저부터 이 글을 믿지 않을 겁니다. **리뷰 부담이 엔지니어에게 옮겨갑니다. 실제로 그렇습니다.** 업계 전반에서 AI를 쓰는 팀은 1인당 PR이 98%, PR 크기 중간값이 33%, 리뷰 시간이 91% 늘었습니다. 코드를 만드는 일은 싸졌지만 읽는 일은 그대로였습니다(아주 그대로는 아니지만요). 이 계산 없이 디자이너를 레포에 넣으면, 디자인의 비용을 조용히 엔지니어링으로 옮긴 셈이 됩니다. 그리고 그 사실은 여러분보다 EM이 먼저 알아챕니다. 저희는 세 가지로 이 부담을 낮게 유지했습니다. 디자인 PR은 레포에 이미 있는 모듈과 컨벤션 위에서 만들어지므로 일관성이 구조적으로 확보됩니다. PR을 올리기 전에 브랜치에서 자동 리뷰가 한 번 돌기 때문에 리뷰어가 뻔한 것을 보지 않습니다. 그리고 대부분의 디자인 PR은 관련된 FE, BE PR과 함께 스택으로 올라가 한꺼번에 리뷰됩니다. **리뷰 자체도 AI의 도움을 받기 시작했습니다.** Rina의 PR에서 Finn보다 먼저 입을 연 쪽을 보시면 됩니다. 봇이 이미 변경 요약과 파일별 정리, 잠금 동작의 시퀀스 다이어그램, 커버리지 변화까지 올려두었습니다. 사람이 PR을 열었을 때 그 diff는 이미 한 번 읽힌 상태였습니다. Finn이 할 일은 무엇이 바뀌었는지 재구성하는 것이 아니라, 이걸 내보내도 되는지 판단하는 것이었습니다. 리뷰 시간 91% 증가라는 숫자는 생성에는 에이전트가 있고 리뷰에는 없던 시점의 스냅샷이고, 그 격차는 빠르게 좁혀지고 있습니다. 끝까지 자동화되면 안 되는 것은 *이것이 존재해야 하는가* 라는 질문입니다. 그 질문은 diff의 양쪽 모두에서 사람 몫으로 남습니다. **도입 속도는 사람마다 다르고, 발목을 잡는 쪽은 대개 디자이너가 아닙니다.** 지금은 버즈빌의 모든 디자이너가 이렇게 일합니다. 다만 거기까지 걸린 시간은 사람마다 크게 달랐고, 무엇이 각자를 붙잡았는지 돌아보면 배우기를 거부한 경우는 거의 없었습니다. 제품이 문제였습니다. 어떤 화면은 깔끔합니다. 컴포넌트 라이브러리가 있고 변경을 넣을 자리가 분명합니다. 어떤 화면은 오래됐고, 백엔드 작업과 얽혀 있고, 파트너의 릴리즈 일정 뒤에 서 있습니다. 이런 곳에서의 첫 PR은 실제로 더 어렵습니다. 더 자주는 주변 팀이 문제였습니다. PM과 엔지니어는 디자인 PR을 받아본 적이 없으니 이걸 어떻게 다뤄야 할지 몰랐습니다. 누가 리뷰하는가. 우리 스프린트가 밀리는가. 우리 오너십을 침범하는가. 전부 정당한 질문인데 답이 없으니, 일은 조용히 멈춰 섰습니다. 일이 멈추는 방식 중에 제일 나쁜 방식입니다. 이건 팀 하나하나 찾아가서, 도구가 아니라 대화로 풀었습니다. 그 시간을 미리 계산에 넣으시는 게 좋습니다. 이 워크플로우는 저절로 퍼지지 않습니다. 팀 하나, 대화 하나씩 퍼집니다. **두 갈래는 섞이고, 경계선은 저절로 그어지지 않습니다.** 탐색과 딜리버리를 말로 나누는 건 쉽습니다. 그렇게 사는 건 어렵습니다. 디자이너는 이제 반나절이면 돌아가는 무언가를 만들 수 있고, 그러다 보면 이미 진짜처럼 보인다는 이유로 탐색용 프로토타입을 계속 다듬게 됩니다. 팀의 디자이너에게 지금 어느 쪽에 있느냐고 물어보세요. 답하지 못한다면 두 가지 일을 동시에 하고 있는 겁니다. **리듬 없는 속도는 소음입니다.** 공통의 박자 없이 하루에 PR 20개를 올리는 팀은 생산적인 게 아니라 머지 큐일 뿐입니다. 저희는 데일리 스탠드업을 없앴고(열려 있는 PR이 곧 상태 공유입니다), 주간 리뷰는 git 로그에서 생성합니다. 개인의 속도가 먼저 올라갔고 협업의 박자가 뒤늦게 따라붙었습니다. 순서가 그랬고, 그 과정이 꽤 아팠습니다. **PR 수는 딜리버리만 잽니다.** 옳은 것을 만들었는지는 전혀 말해주지 않습니다. 그래도 저희가 이 숫자를 보는 이유는, 디자이너가 실제로 출시하고 있는가 아니면 여전히 넘기고 있는가라는 무딘 질문 하나에 정직하게 답하기 때문이고, 목업을 예쁘게 만드는 걸로는 이 숫자를 속일 수 없기 때문입니다. 다만 PR 수를 최적화하는 팀은 자신만만한 실수를 300개 출시하게 됩니다. 탐색을 재는 깔끔한 지표는 아직 없고, 저도 드릴 것이 없습니다. --- ### 환경을 만드는 일이 곧 본업입니다 이걸 시도한 대부분의 팀은 뻔한 결과물을 받아 들고 "AI는 디자인을 못 한다"고 결론 내립니다. 문제는 워크플로우가 아니라 환경이었습니다. AI가 모든 것을 똑같이 만든다는 걱정은 2024년에는 완전히 타당했습니다. 고삐 없는 모델은 자기가 본 모든 것의 평균값으로 수렴하고, 그 결과가 Tailwind 히어로 섹션에 그라데이션 덩어리입니다. 그건 셋업의 문제고, 이미 풀린 문제입니다. 제가 만든다면 이 순서로 만들겠습니다. 1. **토큰을 진짜 재료로.** 모든 색과 간격과 타이포 단계를 시맨틱 변수로 두고, primitive와 semantic과 component 세 겹으로 나눕니다. 표면 전체가 체계적이면 하드코딩된 hex는 여러분뿐 아니라 에이전트 눈에도 어색해집니다. 2. **레포 안의 상시 지침.** 모든 레포 루트에 `CLAUDE.md` 하나. 거기 적어두는 규칙 하나가 앞으로의 교정 백 번을 아껴줍니다. 3. **크래프트를 담은 스킬.** 그래야 지식이 시니어 디자이너 한 사람의 머릿속이 아니라 레포에 남습니다. 4. **검증하는 훅.** 성공하면 조용히, 실패하면 시끄럽게. 이 글에서 딱 하나만 가져가신다면 이것을 가져가세요. 5. **프리뷰 환경.** 공유 가능한 URL이 붙은 브랜치가 있어야 PR이 PM도 눌러볼 수 있는 것이 됩니다. 디자이너의 새로운 크래프트는 이 환경을 만드는 일입니다. 제약을 정하고, 브랜드를 코드로 새기고, 토큰을 배포하고, 스킬을 쓰십시오. 그러면 에이전트는 *아무* 디자인이 아니라 *우리* 디자인을 만들어냅니다. --- ### 월요일부터 바로 시작할 수는 없습니다 이런 글은 보통 5주짜리 계획과 "일단 PR을 올려보세요"라는 친절한 말로 끝납니다. 저도 그 버전을 먼저 썼다가 지웠습니다. 실제로 시간이 걸리는 부분을 통째로 건너뛰고, 마치 의욕 있는 디자이너 한 명이 혼자 내릴 수 있는 결정처럼 보이게 만들기 때문입니다. 그렇지 않습니다. **디자이너가 첫 줄을 쓰기 전에 누군가 치워둬야 하는 것들입니다.** **회사가 실제로 승인한 LLM.** 모델에 들어가는 소스 코드가 어떻게 처리되는지 검토를 거치고, 보안팀이 서명한 데이터 처리 답변이 붙은 전사 계약이어야 합니다. 누군가 조용히 개인 비용으로 결제한 구독이 아니라요. 버즈빌은 이미 전사로 정리해둔 상태여서 디자인이 필요로 하기 전에 해결돼 있었습니다. 아직이라면 이 항목 하나로 한 분기가 갈 수 있습니다. **엔지니어가 아닌 사람에게 열어주는 프로덕션 레포 쓰기 권한.** 권한 설정 문제의 얼굴을 하고 있지만 사실은 정책 결정입니다. 그리고 그 결정은 여러분이 아니라 레포를 소유한 팀의 몫입니다. **그 결정에 대한 보안 검토.** 누가 머지할 수 있는지, 브랜치 보호 규칙은 무엇인지, 그 브랜치가 시크릿에 닿을 수 있는지, 잘못된 변경이 들어가면 어떻게 되는지. 다 좋은 질문입니다. 질문을 받기 전에 답을 준비해두십시오. **엔지니어가 아닌 사람의 브랜치도 돌릴 수 있는 CI,** 그리고 그 위에 얹는 프리뷰 환경. 순서를 봐주십시오. 프리뷰는 마지막 벽돌이지 첫 벽돌이 아닙니다. 이게 제일 재미있어 보여서 여기부터 시작했다가, 정작 디자이너가 브랜치를 못 올린다는 사실을 뒤늦게 발견하는 팀이 많습니다. **문서로 합의된 리뷰 정책.** 디자인 PR의 필수 리뷰어는 누구인지, 그 리뷰가 그들의 스프린트와 경쟁하는지. 이걸 건너뛰면 PR은 일주일씩 열린 채로 있고, 다들 조용히 이 실험이 실패했다고 결론 내립니다. 그리고 이 모든 것 밑에는, 직무를 넘나드는 일이 침범이 아니라 도움으로 읽히는 문화가 있어야 합니다. 회사의 첫 디자인 PR이 영역 다툼을 부른다면, 코드를 한 줄이라도 쓰기 전에 그 대화부터 정리하십시오. 어떤 장치도 그것까지 막아주지는 못합니다. 저희는 이 길을 내는 데 몇 달이 걸렸고, DevOps와 보안과 엔지니어링 리드와 CTO가 짊어졌으며, 디자이너가 한 일은 거의 없었습니다. 이 대화들은 줄을 서면 오래 걸리고 각각 주인이 다르니, 일찍 그리고 동시에 시작하시는 게 좋습니다. **길이 열리고 나면, 사람이 올라타는 건 짧습니다.** 작고 안전한 것부터 하시면 됩니다. 하드코딩된 hex를 시맨틱 토큰으로 바꾸기, 페이지 간격 고치기, 틀려도 아무 비용이 없는 것으로 전체 루프를 한 바퀴 돌기. 거기서 기능 하나를 끝까지 책임지는 데까지는 금방입니다. --- ### 하나만 기억한다면 에이전트는 자기가 닿을 수 있는 컨텍스트만큼만 강합니다. 레포 안에서는 여러분의 토큰과 컴포넌트와 데이터와 컨벤션을 보고 그 위에 쌓습니다. 밖에서는 짐작하고, 인터넷의 평균값을 돌려줍니다. 그러니 천장을 정하는 건 여러분이 계약한 모델이 아닙니다. 일이 실제로 벌어지는 자리에 모델을 데려다 놓을 수 있는 사람이 몇 명이냐가 천장을 정합니다. 코드베이스가 엔지니어만 들어갈 수 있는 방이라면, 회사 안에서 AI가 제 힘을 다 내는 방은 그 하나뿐입니다. 나머지 사람들에게는 채팅창 하나가 주어질 뿐입니다. **AI 네이티브 회사는 모두가 AI를 쓰는 회사가 아닙니다. 모두가 AI를 컨텍스트 안에 데려다 놓을 수 있는 회사, 여러 직무가 같은 제품과 같은 코드베이스 위에서 함께 일하는 회사입니다.** --- ### 방향은 양쪽입니다 디자이너가 코드를 만지게 됐다면, 그 정직한 대가로 디자이너가 아닌 사람도 디자인을 만지게 됩니다. 지금 저희가 그 프로토콜을 세우고 있습니다. UI 문제를 발견한 엔지니어나 PM이 직접 고쳐서 디자인 PR을 올리고, 그 PR의 책임 리뷰어는 디자이너입니다. 강제가 아니라 선택이고, 누가 썼든 결과물이 구조적으로 성립하도록 디자인 시스템이 공통의 계약 역할을 합니다. 코드 리뷰를 그대로 뒤집은 모양입니다. 여러분이 우리 로직을 리뷰하고, 우리가 여러분의 인터페이스를 리뷰합니다. 생각해보면 이건 diff가 붙은 디자인 크리틱일 뿐입니다. 동료가 동료를 리뷰하고, 사람이 아니라 산출물을 놓고 이야기하는 것. 우리가 20년째 하자고 말해온 그것입니다. 의도한 것과 사용자가 만지는 것 사이의 거리는 이제 46분 남짓입니다. 그 거리를 완전히 지우기 전에, 아직 옮겨야 할 파트너가 몇 곳 남아 있습니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://team.buzzvil.com/2f0b4a09-8b7e-4f46-9bce-e8bf0904ad95) --- ## [Memory DB는 무엇으로 가득 차 있었을까 - 1부 : 클라이언트 캐싱과 tracking item](https://tech.buzzvil.com/blog/memorydb-tracking-items-1) Date: 2026-09-05 | Author: Simon Lee | Category: Backend 안녕하세요. 할당시스템 파트 서버 엔지니어, Simon 이상원입니다. 최근 저희 할당시스템 파트에서 활용하는 MemoryDB(Redis)의 메모리 사용량이 85%까지 차올랐습니다. 사용 가능한 메모리가 500MB도 남지 않은 상황. 과연 무엇이 메모리를 점유하고 있었는지, 그리고 다시 3GB 이상의 여유 공간을 확보하기까지 어떤 여정이 있었는지 정리해보았습니다. 1부에서는 원인을 파악하는 과정을, 2부에서는 해결 과정을 담았습니다. ## 왜 MemoryDB를 쓰나요? 저희 할당시스템 파트는 **유저가 버즈빌의 지면에 진입했을 때 이 유저에게 어떤 광고를 보여주는 것이 좋을지** 결정하는 시스템을 관리하고 있습니다. 유저는 어떤 광고에 관심이 있을까요? 이를 알기 위해서는 이 유저가 어떤 유저인지, 그리고 현재 내보낼 수 있는 광고들은 어떠한 광고인지 등을 다양한 측면에서 고려해야 합니다. 초당 약 7,000건의 요청마다 여러 정보들을 실시간으로 읽어오기 위해 저희는 AWS의 MemoryDB를 활용하고 있습니다. ## 어쩌다 MemoryDB를 파헤치게 됐나요? ![MemoryDB 메모리 사용률 85% 도달 알럿](https://tech.buzzvil.com/blog/memorydb-tracking-items-1/img1-datadog-alert.png) 어느 날 MemoryDB의 메모리 사용률이 85%에 도달했다는 알럿을 받았습니다. 당장 고려할 수 있는 방법으로는 스케일업이 있었는데요. 그러나 이미 한 차례 스케일업을 했고, 그 후에 몇 번의 임시 조치를 했음에도 꾸준히 차오르고 있어 근본적인 문제를 찾아보고 싶었습니다. 사실 저는 입사 6개월차 신입 엔지니어이고, MemoryDB에 대해서는 아주 기초적인 동작 원리 정도밖에 알지 못했는데요. 이번 기회에 좀 더 알고 싶어 저에게 맡겨달라고 호기롭게 던졌습니다. (버즈빌에서는 적극적인 사람이 많은 기회를 받을 수 있답니다) ## 어떤 것부터 시작했나요? 생각해볼 수 있는 가장 단순한 것부터 시작했습니다. TTL이 설정되지 않은 키가 계속해서 쌓이고 있지 않을까요? 아쉽게도 코드를 확인했을 때 그러한 키는 없었습니다. 다음으로는 메모리를 한번 들여다봤습니다. 저희 MemoryDB는 Primary와 Replica, 각각 1대씩 운영하고 있는데요. 이 둘의 메모리를 조사한 결과 이상한 점을 발견했습니다. ![Primary 85% vs Replica 30%](https://tech.buzzvil.com/blog/memorydb-tracking-items-1/chart-usage-gap.svg) Primary와 Replica는 같은 데이터를 복제합니다. 그런데 메모리 사용률은 Primary 85%(2.63GB), Replica 30%(0.96GB)였습니다. **'같은 데이터를 복제한다면 같아야 하는 거 아닌가?'** 이 의문을 가지고 하나씩 조사해보기 시작했습니다. **가설 1 : Primary에 오버헤드가 더 많다?** - 기각 ![Primary와 Replica의 오버헤드 비중 — 1.6GB 격차를 설명하지 못한다](https://tech.buzzvil.com/blog/memorydb-tracking-items-1/chart-overhead.svg) Primary는 클라이언트가 직접 데이터를 쓰고, 또 Replica에 복제하는 작업도 하죠. 단순히 생각했을 때 Primary가 하는 역할이 더 많아서 오버헤드가 더 크지 않을까 생각했습니다. 그러나 클라이언트 버퍼와 복제 백로그를 합쳐도 Primary 34MB, Replica 21MB. 1.6GB가 넘는 격차를 설명하기에는 너무 작은 차이였습니다. **가설 2 : Primary에 데이터가 더 많다?** - 기각 ![실데이터 165MB가 전체 메모리에서 차지하는 비중](https://tech.buzzvil.com/blog/memorydb-tracking-items-1/chart-data.svg) 혹시나 복제가 잘 되고 있지 않아서 Primary에 데이터가 더 많이 담겨 있을 수도 있을까요? 전체 키를 세어봤습니다. Primary 104,848개, Replica 104,864개로 사실상 동일하네요. 키와 값이 차지하고 있는 실데이터 용량을 직접 조사해봐도 약 165MB로 양쪽이 같았습니다. 잠깐, 뭔가 이상합니다. 실데이터가 165MB밖에 되지 않네요? 현재 Primary의 메모리 사용량은 2.63GB인데, 그럼 약 2.5GB에 해당하는 메모리는 대체 무엇이 차지하고 있을까요? 메모리 할당 내역(`MEMORY MALLOC-STATS`)을 열어보았습니다. ![malloc 사이즈 클래스 분포](https://tech.buzzvil.com/blog/memorydb-tracking-items-1/chart-malloc.svg) Primary에 8바이트짜리 객체가 5,709만 개, Replica에는 1,802만 개가 있었습니다. 그리고 24바이트짜리 객체도 그 수와 비례해서 존재하고 있었고, **Primary는 8B 객체와 24B 객체가 약 2.0GB를 차지하고 있었습니다.** 저희 서비스에서 사용하는 정보들은 이 정도로 작은 객체들을 사용하지 않을 텐데, 8B 짜리 무언가가 5,700만 개나 있네요. 이게 무엇인지 알아내야 할 것 같습니다. Redis에는 메모리·연결 등 자신의 내부 상태를 나타내는 지표를 `이름: 값` 형태로 보고하는 `INFO`라는 명령어가 있습니다. 이 명령어를 통해 어딘가에 5,709만과 맞아떨어지는 무언가가 있지 않을까요? ``` tracking_total_items: 56,988,980 ``` 8B 객체 수 57,099,184와 오차 0.2%로 일치하는 지표를 찾아냈습니다. 드디어 원인에 좀 더 가까워진 것 같아요. 그럼 이제 저것의 정체를 알아보겠습니다. ## tracking_total_items tracking item이란 무엇일까요? Redis 6부터는 클라이언트가 원하는 키를 매번 Redis로부터 읽어가지 않도록, 읽어간 값을 클라이언트 내부에 캐싱해 두는 **클라이언트 사이드 캐싱**이라는 기능을 제공합니다. 그리고 클라이언트가 유효하지 않은 값을 사용하는 것을 막기 위해, Redis는 클라이언트가 캐싱한 키의 값이 바뀌면 클라이언트에게 "값이 바뀌었으니 그 키의 캐싱을 무효화해"라고 알려주는데요. 이를 위해 Redis는 어떤 키를 어떤 클라이언트가 읽어갔는지를 기억해야 합니다. 그 명부가 **Tracking Table**이고, 여기에 담기는 항목 하나하나가 tracking item입니다. `tracking_total_items`는 (키 × 클라이언트) 항목의 총 개수입니다. ![트래킹 테이블의 Radix Tree 구조 — 중간 노드(24B)와 리프의 클라이언트 ID(8B)](https://tech.buzzvil.com/blog/memorydb-tracking-items-1/diagram-radix-tree.svg) 내부 구조는 Radix Tree로 관리됩니다. 위 이미지는 `w:KR-11` 키를 읽어간 클라이언트(`#1031002`, `#1029744`)와 `w:KR-26` 키를 읽어간 클라이언트(`#1030112`, `#1024310`)를 기록해둔 예시인데요. 이름이 트리의 경로가 되고(중간 노드, 24B), 그 끝에 읽어간 클라이언트 ID들이 매달립니다(리프 노드, 8B). 아까 본 24B와 8B의 정체가 이것으로 설명이 됩니다. **tracking item은 언제 생성되나요?** ![클라이언트가 캐싱 키를 읽어 tracking item이 생성되는 과정](https://tech.buzzvil.com/blog/memorydb-tracking-items-1/tracking-item-created.png) 클라이언트가 캐싱 설정한 키를 읽을 때마다 (키 × 연결) 항목이 하나 기록됩니다. 위 이미지는 클라이언트(`#1024581`)이 클라이언트 캐싱이 설정된 `w:KR-11` 키를 읽었고, 이때 `w:KR-11` 키를 읽은 명부에 `#1024581` 이 새로 추가된 모습을 보여줍니다. **그럼 언제 삭제되나요?**![키에 쓰기가 발생해 tracking item이 회수되는 과정](https://tech.buzzvil.com/blog/memorydb-tracking-items-1/tracking-item-removed.png) 클라이언트가 캐싱한 키에 **쓰기가 발생할 때**(값 변경, 삭제, TTL 만료 등) 해당 키의 `tracking items`가 통째로 회수되고, 그 키를 읽어간 클라이언트 전원에게 무효화가 전송됩니다. 여기까지는 합리적입니다. 그러나 여기서 클라이언트의 연결이 끊기면 어떻게 될까요? 클라이언트와 Redis 사이의 연결이 끊기면 더 이상 무효화를 전송할 수 없으니, 해당 클라이언트에 대한 `tracking item`은 제거하는 것이 맞을 것 같습니다. 그러나 **클라이언트의 연결이 끊겨도 그 클라이언트의 명부는 회수되지 않습니다.** Redis GitHub에서는 이 점에 대한 [issue](https://github.com/redis/redis/issues/13867)도 확인할 수 있는데요. 의도된 것일까요? [Redis GitHub](https://github.com/redis/redis/blob/6.2/src/tracking.c)에서 `tracking item`을 무효화하는 코드를 확인하면 이런 주석이 적혀있습니다. > **"we'll remove the ID reference in a lazy way. Otherwise when a client with many entries in the table is removed, it would cost a lot of time to do the cleanup."** > — redis/src/tracking.c, disableTracking() (Redis 6.2 기준 64~66행) ![연결 종료 시 특정 클라이언트를 찾기 위한 트래킹 테이블 전체 스캔 경로](https://tech.buzzvil.com/blog/memorydb-tracking-items-1/tracking-table-scan.png) 캐싱한 키가 많은 클라이언트의 연결이 끊기면, 해당 클라이언트의 `tracking item`을 찾기 위해 전체 Tracking Table(Radix Tree 구조)을 스캔해야 합니다. 위 이미지는 `#1024581` 클라이언트의 연결이 끊겼을 때의 스캔 경로를 보여줍니다. 이는 많은 비용을 소모하게 되므로 나중에 그 키가 무효화될 때에야 지우겠다는 설계입니다. 저희 서비스에서는 배포·스케일링·배치 등의 작업으로 MemoryDB에 하루 약 26,000개의 연결이 새로 만들어지고 사라지고 있습니다. 특정 시점의 실제 연결은 약 500개뿐인데 말이죠. 떠난 클라이언트들의 명부가 쌓이고 있는 것일까요? ## 라이브 키로 실험해보자 저희 서비스에서 클라이언트 사이드 캐싱을 쓰는 키는 아래와 같습니다. | 키 패턴 | TTL | 역할 | | ----------------------------- | --------- | ---------- | | `ab:__all_config__` | **없음** | 실험(AB) 설정 | | `unit_margin_rate_` | 72h | 지면 마진율 | | `{lineitem_margin_rate}:` | 72h | 광고 마진율 | | `w:` | 24h | 지역 날씨(타겟팅) | | `best_product:` | 하루 1회 재생성 | 추천 상품 목록 | `ab:__all_config__` 키를 제외하고는 주기적으로 정리되고 있습니다. 그럼 `ab:__all_config__`에 대한 `tracking items`가 누적되고 있는걸까요? 확인을 위해 `ab:__all_config__` 키를 같은 값으로 재기록해 강제로 무효화해 보았습니다. > 결과: 회수된 항목 991개, 약 33KB 범위를 넓혀 모든 키를 각각 무효화해 봤습니다. > 결과: 회수된 항목 ≈ 0개 살아있는 키들은 명부를 거의 들고 있지 않았습니다. 그리고 어떤 키가 `tracking items`에 남아있는지 확인하거나 임의로 이를 정리하는 방법은 없습니다. 현재 사용되고 있는 키 중에는 없으니, Failover를 통해 누적된 `tracking_total_items`를 회수한다면 더 이상 늘어나지 않을까요? ## 언제부터 누적되고 있었을까요? CloudWatch에서 15개월치 메모리 추이를 분석해봤습니다. ![15개월 메모리 추이](https://tech.buzzvil.com/blog/memorydb-tracking-items-1/chart-15months.svg) Primary는 월 +0.2GB의 기울기로 선형 증가하다가 **2026년 3월 25일에 딱 멈춥니다.** 그리고 정확히 같은 날, 이번엔 Replica가 자라기 시작합니다. 그리고 지금까지도 계속 증가하고 있었습니다. 이대로라면 Failover를 하더라도 다시 메모리가 차오르겠네요. 2026년 3월 25일에 무슨 일이 있었는지 확인한다면, 이번에야말로 진짜 원인을 잡을 수 있을 것 같습니다. ## 무슨 일이 있었나요? 3월 25일은 Redis 클라이언트 구성을 정리하면서, **읽기를 Primary에서 Replica로 보내는 변경**이 배포된 날이었습니다. 클라이언트 캐싱을 사용하는 5개 키 중 4개에 대해서도 Replica로 읽기가 전환되었습니다. | 키 패턴 | TTL | 역할 | 읽기 | | ----------------------------- | --------- | ---------- | ------------------ | | `ab:__all_config__` | **없음** | 실험(AB) 설정 | Primary | | `unit_margin_rate_` | 72h | 지면 마진율 | Primary -> Replica | | `{lineitem_margin_rate}:` | 72h | 광고 마진율 | Primary -> Replica | | `w:` | 24h | 지역 날씨(타겟팅) | Primary -> Replica | | `best_product:` | 하루 1회 재생성 | 추천 상품 목록 | Primary -> Replica | `tracking items`는 '캐싱 키를 읽어간 클라이언트 명부'이므로, 클라이언트가 읽는 곳에 명부가 생깁니다. 쌓이는 자리가 Primary에서 Replica로 옮겨간 것뿐, 쌓임 자체는 계속되고 있었던 겁니다. 그런데 이상하네요. 위 표에서 봤듯 옮겨간 캐싱 키는 전부 TTL이 있거나 매일 재생성됩니다. 키가 만료되면 `tracking items`는 회수됩니다. 그럼에도 왜 누적이 되고 있었을까요? 그리고 `ab:__all_config__`가 남아있는 Primary는 그 후로 왜 더 이상 쌓이지 않을까요? 저는 `ab:__all_config__`와 달리 나머지 4개의 키는 ``, `` 등 변하는 값이 있다는 점이 의심스러워 보였습니다. ## 진짜 원인 : 유령 키는 만료되지 않는다 답은 [Redis 소스 코드](https://github.com/redis/redis/blob/6.2/src/tracking.c)에 있었습니다. 명부에 기록하는 함수(`tracking.c`의 `trackingRememberKeys`)는 커맨드 인자에서 키 이름만 꺼내 그대로 기록합니다. **그 키가 실제로 존재하는지 확인하지 않습니다.** ![존재하지 않는 유령 키를 읽었을 때의 흐름](https://tech.buzzvil.com/blog/memorydb-tracking-items-1/diagram-phantom.svg) 얼핏 보면 버그처럼 보이지만, 다시 생각해보면 꽤 자연스러운 동작입니다. 클라이언트는 "이 키는 없다"는 사실 또한 캐싱할 수 있습니다. 그리고 나중에 그 키가 생성되면 알려줘야 하니, Redis 입장에서는 키의 존재 여부와 무관하게 기억해 두는 것이 맞는 설계입니다. 이제 답이 보입니다. 존재한 적 없는 유령 키를 읽으면 `tracking items`는 남습니다. 그러나 그 키는 생성된 적이 없으니 TTL 만료가 영원히 없습니다. 해당 키에 쓰지 않는 이상 무효화되지 않는 기록이라는 것이지요. 로그와 지표로 측정해 보니 저희 서비스에서 유령 키를 실제로 읽고 있었습니다. `w:KR-`에서 ``이 생략된 `w:KR-` 키를 계속해서 읽고 있었습니다. 정말 이 유령 키가 명부의 주인인지 확인하는 마지막 실험. `w:KR-`에 값을 한 번 써서 강제로 무효화를 일으켜 봤더니 **Primary에서 393,597개, Replica에서 146,436개의 `tracking items`가 즉시 회수됐습니다.** 존재하지 않는 유령 키 하나의 `tracking items`가 약 18MB를 차지하고 있었습니다. 다른 유령 키들은 로그에 남지 않아 더 이상 파악하지 못했지만, 이러한 유령 키 조회가 원인이라는 점은 확인이 완료되었습니다. ## 2부로 이어집니다. 원인은 파악이 됐는데, 어떻게 해결했을까요? 유령 키를 모두 찾아 조회를 못하도록 했을까요? 개선 과정과 회고는 2부에 이어집니다. 읽어주셔서 감사합니다. --- ## [AI와 함께 업무 시스템을 다시 설계하다: Salesforce 대체 CRM 개발기](https://tech.buzzvil.com/blog/ai-buzzforce-salesforce-crm) Date: 2026-07-06 | Author: Frank Koh (고영민) | Category: Backend ## 0. CRM을 새로 만든다는 것 안녕하세요. Ad Management 파트 서버 엔지니어 프랭크, 고영민입니다. 올해 3월 세일즈포스 대체 방안을 처음 논의한 뒤, 6월 내재화 CRM인 "버즈포스(BuzzForce)"로 실제 업무를 전환하기까지의 여정을 소개하려고 합니다. 버즈빌은 마케팅 리드 확보, 영업 파이프라인 관리, 캠페인 운영, 성과 기반 정산으로 이어지는 긴 업무 흐름을 세일즈포스 위에서 운영하고 있었습니다. 현업의 업무 방식은 계속 바뀌었지만, 외부 CRM을 커스터마이징하고 내부 시스템과 연동하는 속도는 점점 병목이 되고 있었습니다. 버즈포스 프로젝트는 단순히 세일즈포스를 대체하는 것이 아니라, 각 담당자의 업무 방식을 전환하고 개선하는 일이었습니다. 제한된 일정 안에서 유지할 업무 방식과 개선할 비효율을 구분해야 했고, 과거 데이터와 backward-compatible하면서도 데이터 가시성과 정합성을 높이는 설계를 선택해야 했습니다. 이미 운영 중인 업무 시스템을 옮기는 일이었기 때문에 무중단 배포와 데이터 마이그레이션도 안전하게 준비해야 했습니다. 이 글에서는 세일즈포스를 탈출하기로 결정하기 위해 설득과 설계의 기반을 어떻게 마련했는지, 제품 구현 과정에서 AI와 어떻게 분업했는지, 최종 전환 이후 버즈포스가 버즈빌에 어떤 가치를 가져왔는지 이야기해보겠습니다.
민감 정보를 마스킹한 버즈포스 대시보드 화면
민감 정보를 마스킹한 버즈포스 대시보드.
## 1. 세일즈포스는 어떤 비효율을 가지고 있었나? 버즈빌은 세일즈포스를 10년 이상 사용해왔고, 그 사이 세일즈포스를 사용하는 팀과 각 팀의 업무 방식도 모두 바뀌었습니다. 하지만 세일즈포스는 외주사를 거쳐야만 커스텀이 가능했고 현재는 세일즈포스를 전담하는 팀도 없습니다. 무엇보다 데이터 구조를 온전히 이해하고 지속적으로 개선할 책임자가 부재했습니다. 사용하지 않는 필드와 규칙이 복잡하게 얽히면서, 세일즈팀과 재무팀 모두 불편에 익숙해진 상태로 업무를 지속하고 있었습니다. 또한 버즈빌의 셀프서빙 플랫폼인 [광고센터](https://ads.buzzvil.com/)를 출시하면서 백엔드 시스템과 세일즈포스의 연동을 자동화했습니다. 세일즈포스의 데이터 구조와 조회 방식을 이해하는 데에도 리소스를 투자해야 했고, 광고센터, 광고운영시스템, 세일즈포스 3개 시스템의 정합성을 유지하는 난이도도 상당히 높았습니다. atomic한 트랜잭션을 구현하는 것도 사실상 불가능했으며, 예상치 못한 이유로 API token이 무효화될 때마다 secret rotation과 데이터 백필에도 긴 시간이 걸렸습니다. AI Agent가 업무에 도입되었음에도 세일즈포스의 비효율이 병목이 되어 전체 업무 효율은 크게 나아지지 않았습니다. 실제 사용자인 운영 조직과 세일즈포스를 기반으로 프로덕트를 개발해야 하는 제품 조직의 문제의식이 합쳐지면서, "우리가 직접 만들자"는 의견이 나왔습니다. 7월 라이선스 연장을 4개월 앞두고, 세일즈포스를 제거하면서 라이선스 비용 약 1억 원을 절약하는 계획을 논의하기 시작했습니다.
세일즈포스 기반 업무 흐름과 병목 지점 다이어그램
현업 업무 변화와 내부 시스템 연동 요구가 커질수록 세일즈포스가 병목이 되었습니다.
## 2. AX전환의 불확실성 낮추기: ERD & PoC 그렇다고 "그럼 직접 만들면 되겠다"라고 바로 결론을 낼 수는 없었습니다. 세일즈포스 비용과 비효율은 명확했지만, 자체 CRM을 만든다는 결정은 되돌리기 어렵습니다. 한 번 내부 시스템으로 전환하면 이후 유지보수, 장애 대응, 기능 개선, 데이터 정합성에 대한 책임도 모두 내부에 남습니다. 개발 리소스 관점에서도 부담이 있었습니다. 별도의 전담 제품팀 없이 디자인, FE 구현 등을 AI Agent로 완수해야 했습니다. 기존 업무와 OKR을 진행하면서, 동시에 회사 전체가 사용할 업무 시스템을 새로 만들어야 했습니다. 불확실성을 줄이고, 성공 여부에 대한 의심을 실질적인 피드백으로 바꾸기 위해 "ERD"와 "PoC"를 1차 목표로 정했습니다. ERD(Entity-Relationship Diagram)는 제품 개발의 초기 단계에서 각 객체(Entity)가 어떤 데이터를 담고 있고, 객체 간에 어떤 관계가 있는지 그림으로 표현하는 과정입니다. ERD를 통해 버즈포스의 데이터 구조가 세일즈포스의 주요 데이터를 표현할 수 있는지, 그리고 앞으로의 변경에 대응할 유연성을 갖추고 있는지 설명할 수 있었습니다. 특히 AI Agent를 활용해 각 객체를 클릭했을 때 객체의 특징을 하이라이트하고, 업무 시나리오마다 어떤 객체가 관여하는지 보여주는 인터랙티브 ERD를 제공했습니다. ERD는 "우리가 만드는 제품이 표준 CRM과 너무 달라서 나중에 문제가 되지 않을까?"라는 막연한 의심을, "세일즈포스의 이 데이터는 왜 버즈포스에서는 이렇게 저장하나요?"라는 구체적인 질문으로 바꾸는 매개가 되었습니다. > 아래 ERD는 인터랙티브 다이어그램입니다. 엔티티를 직접 클릭하면 관련 관계가 하이라이트되고, 시나리오 탭에서 업무 흐름별로 어떤 객체가 관여하는지 확인할 수 있습니다.
ERD를 공유한 뒤에는 4월 말까지 PoC 제품을 만들어 실제 사용자에게 시연하고, 완전 전환 여부를 판단하기로 했습니다. ## 3. 세일즈포스는 무슨 일을 하고 있었나? 세일즈포스는 영업관리 툴인 CRM의 대명사처럼 여겨지지만, 실제 버즈빌에서는 CRM보다 더 큰 역할을 하고 있었습니다. 가장 앞단에는 MC(Marketing & Communication)팀이 Pardot이라는 마케팅 툴을 사용하여 마케팅 캠페인을 운영하고, 전략적으로 리드를 확보해 세일즈팀에게 전달하고 있었습니다. 세일즈팀은 MC팀으로부터 인계받은 신규 고객이나 기존 고객과의 미팅을 통해 새로운 광고 집행을 제안하는데, 이 거래 단위를 "기회(Opportunity)"라고 부르고 있었습니다. 기회는 제안 단계를 시작으로 체결, 광고 운영, 최종 정산까지 광고 수주의 전체 사이클을 담당합니다. 재무팀은 광고 집행이 종료된 기회에 매출, 매입 세금계산서를 매칭하여 버즈빌이 광고주로부터 받는 돈과 대행사에게 지급해야 하는 돈을 기록합니다. 특히 정산에는 다양한 엣지케이스가 있어 기존에도 세일즈팀과 재무팀 사이에 비효율적인 커뮤니케이션이 상당히 많았습니다. 또한 기회와 클라이언트를 중심으로 자유롭게 리포트를 생성하여 버즈빌의 영업 현황을 파악하고 의사결정을 내리는 기능도 필요했습니다. 즉 버즈빌의 세일즈포스는 CRM, 마케팅, 영업 리포트, 정산까지 모두 책임지는 시스템이었습니다. 버즈포스를 만들기 위해서는 각 사용자의 니즈와 현재 워크플로우, 개선하고 싶은 방향을 이해해야 했습니다. ## 4. AI로 가설을 세우고, 인터뷰로 검증한다 저는 세일즈포스를 직접 업무에 사용해본 적이 없었기 때문에, 각 필드의 의미와 validation 규칙을 이해하고, 사용자가 직접 입력하는 데이터와 자동으로 계산되는 값을 구분하는 데 어려움이 있었습니다. 이때 AI Agent를 활용해 세일즈포스 데이터의 패턴을 분석했고, 필드 간 관계와 숨어 있는 업무 규칙을 역추적했습니다. 예를 들어 기회가 체결 단계로 넘어가면 집행 시작일과 종료일의 채움률이 20%에서 89%로 올라가고, 세팅요청 단계에서는 Daily Cap과 타겟팅 정보의 채움률도 90% 이상으로 올라갔습니다. 이 분석은 곧바로 버즈포스의 Stage 전환 validation으로 이어졌습니다. 정산 데이터에서도 비슷한 방식으로 규칙을 찾았습니다. 일부 대행사는 월마다 수십~수백 개의 광고를 집행한 뒤 통합 세금계산서 1건을 발행했고, 이 세금계산서를 여러 기회에 나누어 매칭해야 했습니다. 마이그레이션 과정에서는 기회별 수수료율 비중을 기준으로 금액을 분배하는 fallback rule을 만들었고, 이 방식으로 대부분의 기존 데이터를 설명할 수 있었습니다. 데이터 모델에서 `OppBilling`을 별도로 둔 것도 이런 분석 결과가 있었기 때문입니다. 운영팀은 세일즈포스를 사용하고 있었지만 직접 설계하지 않았기 때문에, 사용자 인터뷰만으로는 이런 규칙을 파악하기 어려웠을 가능성이 큽니다. AI로 데이터에서 얻은 인사이트는 즉시 제품 설계로 이어졌습니다. 하지만 데이터가 모든 것을 설명해주지는 못했습니다. Stage가 거꾸로 이동하는 경우가 있는지, 세일즈팀이 실제로 어떤 화면 순서로 값을 입력하는지, 재무팀이 예외 케이스를 어떤 기준으로 처리하는지 같은 "유스케이스"는 데이터 분석만으로 알 수 없었습니다. 그래서 세일즈팀, MC팀, 재무팀 인터뷰를 직접 진행했습니다. 세일즈팀에는 기회가 제안에서 체결, 세팅요청으로 넘어가는 실제 흐름을 물었고, MC팀에는 Pardot에서 들어온 리드가 어떤 기준으로 세일즈팀에 전달되는지 확인했습니다. 재무팀에는 매출 세금계산서, 매입 세금계산서, 입금 내역을 각각 어떤 순서로 확인하고 어떤 경우에 수동 판단이 필요한지 물었습니다. AI는 세일즈포스 DB 스키마와 샘플 데이터를 빠르게 분석하여 필드 채움률과 이상 패턴을 뽑아냈습니다. 이런 방식은 "이 필드는 이 단계에서 확정되는 것 같다"는 가설과 인터뷰 질문 목록을 만드는 데 유용했습니다. 반면 그 가설을 실제 사용자에게 검증하고, 어떤 규칙을 제품에 반영할지 결정하고, 이해관계자들이 납득할 수 있는 언어로 설명하는 일은 사람이 해야 했습니다. AI는 분석의 속도를 높여주었지만, 그 분석이 실제 업무와 맞는지 확인하고 제품의 책임 범위로 바꾸는 과정에는 커뮤니케이션이 필요했습니다.
Stage별 필드 채움률 변화 차트
Stage별 필드 채움률을 집계해 제품 검증 규칙 후보를 역추적한 예시입니다.
## 5. 화면 뒤의 데이터를 다시 설계하기 세일즈포스에는 하나의 필드가 여러 의미를 가지는 경우가 있어 데이터 유실의 위험이 항상 존재했습니다. 예를 들어 광고주에게 제안한 캠페인의 "예산"과 실제 캠페인이 송출된 뒤의 "소진액"을 구분하고 있지 않아, 각 기회의 미소진 비율을 확인하려면 광고운영플랫폼을 함께 확인해야 했습니다. 이러한 묵은 문제를 해결하기 위해 버즈포스의 데이터 구조는 "명시성"을 중심으로 설계했습니다. 대표적인 예가 기회와 클라이언트의 관계입니다. 각 광고 기회에는 광고주, 대행사, 미디어렙사 클라이언트가 관여할 수 있습니다. 세일즈포스는 기회에 관련된 클라이언트를 모두 기회 테이블의 필드 레벨로 관리하고 있어, 3개 이상의 클라이언트가 참여하는 경우 데이터 유실이 있었습니다. 버즈포스에서는 이를 `OppAccount`라는 별도 테이블로 분리했습니다. 하나의 기회가 여러 클라이언트와 연결될 수 있고, 각 연결은 `advertiser`, `agency`, `media_rep` 같은 역할과 수수료율을 가집니다. 정산 대상 클라이언트를 찾고, 광고주/대행사/렙사 기준으로 리포트를 집계할 때 기준이 명확해졌습니다. 클라이언트와 연락처의 관계도 마찬가지였습니다. 실무에서는 한 담당자가 여러 광고주나 대행사와 연결될 수 있고, 담당자와 클라이언트의 관계가 명확해야 활동 등록, 세금계산서 발행 등에서 휴먼 에러가 줄어듭니다. 그래서 연락처를 특정 클라이언트 하나에만 종속시키지 않고 `AccountContact`라는 중간 테이블을 두었습니다. 이 모델은 담당자와 클라이언트의 N:M 관계를 표현하고, `role`, `is_primary`, `is_active` 같은 정보를 함께 저장할 수 있게 해줍니다. 정산 영역에서는 `OppBilling`이 핵심이었습니다. 월마다 수십~수백 개의 광고를 집행하는 대행사가 통합 1건의 세금계산서를 발행한 경우, 전체 금액을 여러 기회에 분배해서 매칭해야 합니다. 또한 장기간 집행하는 광고 캠페인의 경우 하나의 기회가 여러 번 정산 과정을 거칠 수 있습니다. 버즈포스는 `OppBilling`을 통해 Opportunity와 Billing의 N:M 관계를 만들고, 필요할 경우 기회별 배정 금액까지 저장할 수 있게 했습니다. 표현해야 하는 모든 엣지케이스를 데이터 유실 없이 기록하고 분석할 수 있도록, 버즈포스에서는 relation 테이블을 적극적으로 활용했습니다.
데이터 모델 재설계 Before After 다이어그램
여러 의미가 섞여 있던 Opportunity 필드를 관계 테이블 중심의 명시적인 데이터 모델로 다시 설계했습니다.
## 6. AI 개발의 병목은 피드백 루프였다 FE 개발 경험이 전무했던 저는 FE 레포에서 간단한 기능 개발이나 버그 픽스 정도만 해보았고, A부터 Z까지 Agent를 활용해 FE를 개발한 것은 버즈포스가 처음이었습니다. 제가 버즈빌에 입사한 2024년 12월 이후 Agent의 발전 속도는 점점 빨라졌고, 버즈포스 출시 전에도 자유도가 높은 리포트 기능을 제외하면 "기능 구현" 자체가 병목이 되는 경우는 거의 없었습니다. 오히려 프로젝트가 진행될수록 병목은 코드를 작성하는 시간이 아니라, 그 결과물을 검증하고 다시 수정하는 피드백 루프였습니다. 가장 위험했던 착각은 AI가 화면을 빠르게 만들어준다는 이유로, AI가 프론트엔드 엔지니어의 역할까지 대신해준다고 생각한 순간이었습니다. 프론트엔드 엔지니어 동료였다면 구현 이후 "bulk 작업인데 단건 모달만 있으면 안 되지 않나?", "4XX 에러인데 에러 토스트가 불친절해서 사용자가 현재 상태를 파악하지 못하겠네?" 같은 질문을 스스로 던졌을 것입니다. 하지만 Agent는 제가 입력한 문제를 빠르게 풀었을 뿐, 제품 맥락을 지속적으로 붙잡고 스스로 피드백 루프를 돌리지는 못했습니다. 버즈포스는 데이터 정합성 등의 이유로 dual write 기간을 따로 두지 않고 바로 전환하기로 결정했기 때문에 반드시 사용자가 업무를 완수할 수 있는 상태로 만들어야 했습니다. 현재 일하는 팀에는 QA 엔지니어가 없기 때문에 기능을 직접 QA하는 업무 프로세스에는 익숙했습니다. 하지만 버즈포스는 QA 범위가 매우 넓었고, 저 역시 모든 업무 맥락을 처음부터 알고 있던 상태가 아니었기 때문에 난이도가 높았습니다. 그래서 Agent에게 작업을 지시할 때는 반드시 QA를 위한 테스트 데이터 생성과 QA 가이드라인 HTML 문서를 함께 작성하도록 했습니다. Agent는 staging DB에 접근할 수 있었기 때문에, 이번에 구현한 기능을 테스트하기에 적절한 기존 데이터를 선택하거나 필요한 데이터를 새로 생성할 수 있었습니다. QA 가이드라인 문서는 제가 여러 기능을 동시에 개발하는 와중에도 집중도를 잃지 않게 해주었습니다. 동시에 CLI 화면이 아니어도 Agent에게 "이 기능은 이렇게 수정되어야 한다", "이 엣지케이스에 맞는 데이터를 생성해달라" 같은 지시를 내릴 수 있는 인터페이스가 되었습니다. 출시 이후 실제 사용자가 업무를 시작하자 문의와 버그가 계속 들어왔고, 그때마다 원인 분석, 기능 수정, 테스트를 빠르게 반복해야 했습니다. 이 과정에서도 여러 Agent를 동시에 돌리며 각 이슈를 병렬로 처리했습니다. 개발 중의 빠른 iteration이 출시 후 운영 iteration으로 이어졌습니다. 이때 새롭게 드러난 병목은 제 맥락 전환 비용이었습니다. 여러 Agent가 동시에 결과물을 만들어내면, 저는 각 작업의 배경을 다시 떠올리고 원하는 결과가 나왔는지 판단한 뒤 다음 지시를 내려야 했습니다. 마치 CPU가 실제 연산보다 context switching에 더 많은 시간을 쓰는 상태에 가까웠습니다. 정신적인 체력도 꽤 부담이 되었습니다. 다음에 비슷한 프로젝트를 한다면 Agent가 스스로 피드백 루프를 돌릴 수 있는 프레임워크를 먼저 만들 계획입니다. 사용자 시나리오, acceptance criteria, 테스트 데이터 등을 Agent가 스스로 확인하게 만드는 방식입니다. AI 개발의 다음 병목은 코드를 더 빨리 쓰는 것이 아니라 피드백 루프를 얼마나 자율적으로 돌릴 수 있느냐에 있다고 느꼈습니다. \* 저는 AI Agent에게 작업을 지시할 때 [STICC Framework](https://leadingstrategicinitiatives.com/2012/11/01/s-t-i-c-c-a-useful-communication-tool-for-critical-situations/)를 애용합니다. Situation(상황), Task(작업), Intent(의도), Concern(우려), Calibration(조정) 5가지 관점에서 일을 묘사하면 모호한 프롬프트에 비해 작업물의 완성도가 훨씬 높아집니다. 프롬프트 엔지니어링은 이미 과거의 유산처럼 느껴지지만, 제 머릿속의 암묵지를 AI Agent에게 전달하는 과정은 여전히 중요합니다. STICC 프레임워크에 맞게 일을 지시하면서 미처 생각하지 못했던 엣지케이스를 발견한 경우도 많았습니다. 버즈빌 동료인 루카스께서 알려주셨습니다. 감사합니다. ## 7. 버즈포스가 남긴 것 버즈포스를 통해 "모든 것을 의심하는 태도"가 AX 시대의 중요한 덕목이라는 생각을 하게 되었습니다. AI와 함께라면 이전에는 개인이 시작하기 어려웠던 일도 끝까지 밀어붙일 수 있습니다. 그래서 출발점은 "당연해서 의심하지 않았던 것이 뭘까?"가 되어야 합니다. 질문에서 시작했다면 완수는 AI와의 분업으로 가능해집니다. AI는 빠른 구현과 반복 작업, 사람의 context switching을 보조하는 역할을 맡을 수 있습니다. 반면 사람은 도메인 지식을 Agent가 이해할 수 있는 형태로 전달하고, 인터뷰를 통해 실제 사용자의 니즈를 파악해야 합니다. 옆 사람을 설득해 프로젝트를 앞으로 끌고 나가는 것도 여전히 사람의 역할입니다. 버즈포스는 6월 15일 공식 런칭 이후 업무 전환되었고, 세일즈팀과 재무팀은 더 이상 세일즈포스를 사용하지 않고 기존 업무를 수행하고 있습니다. 리포트 고도화, 기존 광고 운영 시스템과의 연동, 기능 개선과 버그 픽스는 계속 진행 중이며 7월 초 현재 6월 집행 광고에 대한 광고비 정산 업무도 버즈포스에서 진행되고 있습니다. 아직 공식 만족도 조사나 정량 지표 분석을 진행한 것은 아니지만, 문의 채널과 사용자 피드백에서 흥미로운 차이를 느꼈습니다. 세일즈팀은 "기존 업무를 문제 없이 이어갈 수 있다"는 반응에 가까웠다면, 재무팀은 "업무가 많이 효율화되었다"는 반응을 더 명확하게 표현했습니다. 차이는 제품을 사용하는 방식에서 나왔다고 생각합니다. 세일즈팀이 사용하는 기능과 메뉴는 대부분 데이터와 엔티티 중심입니다. 광고주와 미팅한 뒤 클라이언트와 기회를 생성하고, 이메일과 미팅을 거치며 광고 집행 일정이 확정되면 단계를 변경하고, 광고 운영팀은 기회를 보고 캠페인을 운영합니다. 즉 세일즈팀에게 버즈포스는 버즈포스 바깥에서 일어난 일을 기록하고 조회하는 시스템에 가깝습니다. 반면 재무팀은 "매입 세금계산서와 기회를 매칭한다", "매출 세금계산서를 업로드한다"처럼 행동과 유스케이스 중심으로 버즈포스를 사용합니다. 재무팀의 업무는 새로운 데이터를 생성한다기보다 여러 시스템과 문서에 흩어진 데이터를 연결하고 검증하는 액션에 가깝습니다. 기존 세일즈포스에서는 이런 유스케이스가 기회 상세 페이지 곳곳에 흩어져 있어 불편이 컸지만, 버즈포스에서는 유스케이스 단위로 페이지와 URL을 분리하고 자동화 기능을 추가했습니다. 이 차이를 보며 업무 시스템의 만족도는 "사용자의 일을 어떤 단위로 제품화했는가"에 크게 영향을 받는다는 것을 느꼈습니다. 플랫폼 안에서 많은 일을 처리할 수 있고, 플랫폼 밖의 업무와 자연스럽게 연결되며, 사용자가 자신의 업무를 유스케이스 단위로 이해하고 실행할 수 있을 때 변화가 더 크게 체감됩니다. 비개발자 또한 자신의 업무를 유스케이스 단위로 정의할 수 있어야 Agent를 이용한 업무 효율화가 쉬워질 것이라고 생각합니다.
엔티티 중심 SaaS에서 유스케이스 중심 업무 시스템으로의 전환 다이어그램
세일즈팀이 사용하는 버즈포스의 현재 모습은 아직 세일즈포스와 유사한 부분이 많습니다. 세일즈팀의 업무 효율을 더 크게 높이려면, 그들이 실제로 어떤 흐름으로 일하는지 더 깊게 이해하고 이를 유스케이스 단위로 다시 제품화하는 과정이 필요합니다. 엔티티 이름이 곧 메뉴 이름이 되는 고전적인 UI에서 벗어나면 업무 시스템이 더 큰 변화를 만들 수 있지 않을까 상상해보았습니다. 물론 그런 큰 변화까지 가기 전에도 할 일이 많아, 아직 고민을 깊게 해보진 못했습니다. 남은 하반기에는 영업 및 운영 효율화 태스크에 제 리소스의 20~30% 정도를 사용할 계획입니다. 우선 광고 운영 시스템과 버즈포스의 연동을 강화해, 운영자가 직접 개입해야만 광고가 집행되는 현재 프로세스를 줄여가려고 합니다. 동시에 버즈빌 영업의 병목을 더 빠르게 찾고 원인을 분석할 수 있는 인터페이스도 마련하려고 합니다. 버즈포스의 데이터를 분석계에 연동하여 LLM을 통한 분석도 쉽게 가능합니다. 버즈포스를 통해 관습적인 상태를 다시 질문하고 유스케이스 단위로 제품화하는 경험을 얻었습니다. 무엇이든 더 빨리 만들 수 있는 시대일수록, 무엇을 만들고 어떤 업무를 책임질지 정의하는 일이 더 중요해졌습니다. 앞으로도 버즈빌에서 중요한 질문을 던지고, 그 질문을 실제 변화로 만드는 엔지니어로 성장하고 싶습니다. --- ## [피크 타임 Pod 245개를 100여개로 — DynamoDB SDK v2 전환으로 Go 서버 CPU 절반 줄이기](https://tech.buzzvil.com/blog/cpu-dynamo) Date: 2026-07-04 | Author: Elric Lim | Category: Backend ## 세줄 요약 1. **Pyroscope 모니터링으로 CPU 점유가 높은 경로 파악** 2. **DynamoDB wrapper 라이브러리 하위의 AWS SDK v1 unmarshal이 원인 — 라이브러리 메이저 버전 업데이트로 해결** 3. **요청당 CPU 59% 감소, 피크 타임 기준 HPA가 요구하는 파드 수 245개 -> 109개 감소** Supply 그룹에서 메인으로 운영 중인 Go 애플리케이션은 저녁 피크 시간마다 파드 수가 200여개 이상으로 늘어나는 패턴이 지속되고 있었습니다. 처음에는 피크 시간 기준으로 시간 당 7천만건의 요청을 처리하기 때문에 이 정도 파드 수(HPA)는 합당하다고 생각했지만 Pyroscope로 profiling한 결과, DynamoDB 클라이언트 라이브러리가 CPU 병목의 원인임을 파악하여 라이브러리 업그레이드만으로 요청당 CPU를 59% 줄이고 파드 수를 100여개 수준으로 낮출 수 있었습니다. 이번 글에서는 Pyroscope로 병목을 찾고 실제 지표로 개선 효과를 확인한 과정을 공유합니다. ## Pyroscope에서 발견한 CPU 병목 Pyroscope에서 저녁 피크 시간대의 CPU profile을 확인했을 때 가장 먼저 눈에 들어온 것은 `guregu/dynamo`였습니다. flame graph에서 DynamoDB 조회 경로가 넓었고 하위의 `AWS SDK v1`의 JSON unmarshal과 reflection 관련 호출이 길게 이어지고 있었습니다. 처음에는 요청량이 가장 많은 광고 서빙 API가 DynamoDB를 적극적으로 사용하기 때문에 DynamoDB 호출 수가 많은 것을 의심했습니다. 하지만 flame graph에서 네트워크 I/O는 거의 보이지 않았고 오히려 실제로 CPU Time을 많이 차지하는 부분이 어디인지 명확히 파악할 수 있었습니다. (해당 시간대 DynamoDB 모니터링에서 latency와 throttling이 관측되지 않은 점도 원인 파악에 도움을 주었습니다.) ![배포 전 피크타임 flame graph](https://tech.buzzvil.com/blog/cpu-dynamo/dynamo-1.png) _배포 전 피크타임 flame graph — guregu/dynamo 하위 경로가 전체 CPU의 절반 가까이를 차지하고 있습니다._ flame graph에서 드러나듯이 DynamoDB 조회 이후 "**Response를 Go struct로 바꾸는 과정에서 CPU를 많이 소모한다**"는 점을 확인했습니다. 일반적으로 JSON unmarshal의 CPU 비용이 높다는 사실을 알고 있었지만 실제 profiling 결과도 unmarshal 과정이 CPU를 오래 점유하는 것을 보여줬기에 그에 맞는 개선 방향을 잡을 수 있었습니다. ## 원인: wrapper 아래의 AWS SDK v1 당시 애플리케이션 코드는 DynamoDB를 직접 AWS SDK로 호출하지 않고 `guregu/dynamo`라는 wrapper 라이브러리를 사용 중이었습니다. `guregu/dynamo`는 테이블, 쿼리, 조건식, struct marshal/unmarshal 같은 부분을 개발자가 쓰기 쉽게 감싸주는 wrapper로 실제 HTTP 요청, 인증 서명, DynamoDB protocol 처리, 응답 파싱은 AWS SDK가 담당합니다. ```text 애플리케이션 코드 | v guregu/dynamo | v AWS SDK for Go | v DynamoDB ``` flame graph에서 `guregu/dynamo`가 넓게 보인 것은 wrapper 자체의 문제라기 보다, 내부에서 호출되는 AWS SDK v1의 DynamoDB 처리 비용까지 함께 집계되었기 때문입니다. 즉 근본적인 개선 방향은 SDK 처리 경로를 변경하는 것이었고 `guregu/dynamo`의 major version을 업데이트를 통해 내부에서 사용하는 AWS SDK도 함께 업데이트 되는 점을 확인했습니다. 이를 근거로 `guregu/dynamo v1 -> v2` 버전 업데이트를 결정했습니다. | 라이브러리 | 내부에서 사용하는 AWS SDK | | ----------------------------- | ------------------------------ | | `github.com/guregu/dynamo` v1 | `github.com/aws/aws-sdk-go` v1 | | `github.com/guregu/dynamo/v2` | `github.com/aws/aws-sdk-go-v2` | 겉에서 봤을 땐 단순히 wrapper 라이브러리의 버전 업데이트지만 근본적으로는 AWS SDK v1에서 AWS SDK v2로 DynamoDB 처리 방식을 변경하기에 큰 개선 효과를 기대할 수 있었습니다. ## v1 -> v2 전환 작업 코드 변경 자체는 복잡하지 않았습니다. 기존에는 `github.com/guregu/dynamo`를 사용했고 변경 후에는 `github.com/guregu/dynamo/v2`를 사용했습니다. ```go // before import "github.com/guregu/dynamo" err := table. Get(hashKey, value). AllWithContext(ctx, &items) // after import "github.com/guregu/dynamo/v2" err := table. Get(hashKey, value). All(ctx, &items) ``` 초기화 코드도 v2용 DynamoDB client를 사용하도록 개선했습니다. ```go ddb := dynamo.NewFromIface(infra.DynamoDBV2()) table := ddb.Table(tableName) ``` 겉보기에는 메서드 이름에서 `WithContext`가 빠진 정도로 보이지만 unmarshal 메커니즘에는 꽤 큰 차이가 있습니다. 다만 wrapper의 메이저 버전과 그 아래 AWS SDK가 함께 바뀌는 작업인 만큼 배포는 한 번에 전체로 내보내지 않았습니다. 카나리 배포로 일부 트래픽에 먼저 적용해 에러율과 동작 이상이 없는지 확인한 뒤 점진적으로 확대했습니다. ## 적용 범위: 광고 서빙 경로의 공통 병목 이 변경은 단일 API의 최적화가 아닌 여러 광고 서빙 요청에서 공통으로 호출되는 사용자 조회 경로를 개선한 작업이었습니다. 광고 서빙 요청은 사용자 식별과 활동 이력, 타겟팅 데이터를 함께 조회한 뒤 후보 광고를 고르는 구조인데, 이때 사용자 관련 데이터는 모두 DynamoDB에서 공통 어댑터 경로로 저장·조회됩니다. 이 공통 경로가 무거워지면 특정 요청 하나가 아닌 광고 서빙 계열 요청 전반의 CPU 비용이 함께 올라갑니다. 코드 변경 자체는 비교적 적은 diff로 버전 업을 진행할 수 있었지만 영향 범위로 보면 작은 변경이 아니었습니다. Datadog APM indexed span 기준으로도 피크타임 1시간 동안 사용자 조회 경로는 천만 건 이상으로 관측됐습니다. 즉 이번 개선은 호출 빈도가 높은 공통 경로의 cost를 줄인 작업이었고 덕분에 서비스 전체 CPU 지표도 눈에 띄게 개선된 것을 확인할 수 있었습니다. ## 배포 전후 측정 결과 ![배포 전후 Pyroscope CPU profile 비교](https://tech.buzzvil.com/blog/cpu-dynamo/dynamo-2.png) _배포 전후 Pyroscope CPU profile 비교 — 배포 후 AWS SDK v1 unmarshal 경로가 사라졌습니다._ 위 pyroscope compare 이미지는 배포 시각을 기준으로 배포 전 30분과 배포 후 30분을 비교했습니다. 배포 직후에는 구버전과 신버전 pod가 섞일 수 있어, 배포 후 첫 10분은 제외했습니다. | 구간 | 시간 | | --------- | -------------------------- | | 배포 시각 | 2026-06-26 10:30 KST | | 배포 전 | 2026-06-26 09:50-10:20 KST | | 배포 후 | 2026-06-26 10:40-11:10 KST | Pyroscope의 CPU profile은 wall clock 시간이 아니라 CPU sample을 누적해서 표시합니다. 여러 pod와 여러 CPU core에서 소비한 시간이 합산되기 때문에 30분 구간을 조회하더라도 `18h` 같은 값이 나올 수 있습니다. | 지표 | 배포 전 | 배포 후 | 변화 | | ------------------ | ---------: | ---------: | -----: | | Pyroscope CPU 샘플 | 18h | 7h | -61.0% | | 요청 수 | 30,057,920 | 28,784,846 | -4.2% | | 요청당 CPU | 2.149ms | 0.876ms | -59.2% | 요청 수가 약간 줄기는 했지만 CPU 감소폭과는 차이가 큽니다. 요청당 CPU로 보정해도 약 59% 개선됐기 때문에 실제 처리 비용이 줄었다고 보는 것이 자연스럽습니다. ### CPU 해소 포인트 전체 CPU만 보면 결과는 알 수 있지만 원인은 알기 어렵습니다. 그래서 Pyroscope에서 `cumulative CPU` 기준으로 어떤 함수가 줄었는지 확인했습니다. `cumulative CPU`는 해당 함수 자체에서 쓴 CPU뿐 아니라, 그 함수가 호출한 하위 함수에서 쓴 CPU까지 포함한 값입니다. API handler나 repository 함수처럼 여러 일을 묶어서 수행하는 함수의 비용을 볼 때 유용합니다. | 경로 | 배포 전 | 배포 후 | 변화 | | -------------------------- | ---------------: | --------------: | -----: | | 사용자 활동 조회 | 31,865s / 49.32% | 4,929s / 19.54% | -84.5% | | 타겟팅 모델 조회 | 8,154s / 12.62% | 1,916s / 7.59% | -76.5% | | AWS SDK v1 JSON unmarshal | 30,347s / 46.97% | 642s / 2.55% | -97.9% | | `reflect.StructTag.Lookup` | 8,724s / 13.50% | 840s / 3.33% | -90.4% | | `runtime.scanobject` | 6,159s / 9.53% | 2,864s / 11.35% | -53.5% | 가장 눈에 띄는 변화는 AWS SDK v1의 JSON unmarshal 비용입니다. 배포 전에는 전체 CPU의 거의 절반이 `github.com/aws/aws-sdk-go/private/protocol/jsonrpc.Unmarshal` 아래에 있었습니다. 배포 후에는 이 비중이 2.55%까지 내려갔습니다. ### 배포 전후 4일: HPA가 요구하는 파드 수 절반으로 배포 직후 1시간 비교는 즉각적인 효과를 보여주지만 HPA는 피크 시간과 평상시를 오가며 반응하므로 스케일링 효과는 더 긴 구간에서 검증해야 정확합니다. 그래서 배포 전 4일과 배포 후 4일을 다시 비교했습니다. ![배포 전후 일주일간 pod instance 수 추이](https://tech.buzzvil.com/blog/cpu-dynamo/hpa-graph.png) _배포(6/26) 전후 일주일간 pod instance 수 추이 — 매일 반복되던 피크의 높이가 배포 이후 눈에 띄게 낮아졌습니다._ | 항목 | 배포 전 4일 | 배포 후 4일 | 변화 | | --------------- | ----------: | ----------: | --------: | | Datadog CPU avg | 690 cores | 369 cores | -46.6% | | Datadog CPU p95 | 1,418 cores | 788 cores | -44.4% | | HPA desired p95 | 245 | 109 | -55.3% | | 요청 수 | 5.06B | 4.58B | -9.5% | | 에러율 | 0.0549% | 0.0510% | 소폭 개선 | | 평균 duration | 2.61s | 2.54s | 소폭 개선 | 피크 시간대 HPA가 요구하던 파드 수(p95)는 245개에서 109개로 감소했습니다. 같은 기간 요청 수 감소는 -9.5%에 그쳤기에 단순 트래픽 감소보다 요청당 처리 비용을 드라마틱하게 감축한 것으로 볼 수 있었습니다. 요일 효과를 배제하기 위해 배포 일주일 전과 배포 다음 날, 같은 요일·같은 시간대의 Kubernetes 지표도 나란히 놓고 확인했습니다. ![배포 일주일 전과 배포 다음 날의 Kubernetes 지표 비교](https://tech.buzzvil.com/blog/cpu-dynamo/before-after.png) _배포 일주일 전(6/20, 위)과 배포 다음 날(6/27, 아래) 같은 시간대 비교 — running pod 217개 → 96개, CPU 401.1 cores → 203.7 cores._ 파드 수 감소는 그대로 인프라 비용 절감으로 이어졌습니다. 해당 서비스가 사용하는 EKS 클러스터의 일 비용은 $400대에서 $250 수준으로 내려왔습니다. ## v2에서는 무엇이 달라졌나 DynamoDB 조회는 동일하게 수행하는데 v2에서는 무엇이 개선됐기에 CPU를 절반 수준만 소모할까요? ### v1: reflection 기반 generic JSON 처리 배포 전 profile에서 가장 크게 보인 함수는 AWS SDK v1의 JSON unmarshal 경로였습니다. 주목해야 할 단어는 `reflection`입니다. reflection은 런타임 중 타입 정보를 들여다볼 수 있는 기능입니다. 예를 들어 어떤 struct에 필드가 몇 개 있는지, 각 필드의 tag가 무엇인지, 이 값이 pointer인지 slice인지 등의 정보를 런타임에 확인할 수 있습니다. 하지만 매 `요청`, `응답 item`, `필드`마다 reflection이 반복되면 CPU 비용이 커집니다. AWS SDK v1의 DynamoDB `AttributeValue`는 하나의 큰 struct에 여러 타입을 포인터 필드로 들고 있습니다. ```go type AttributeValue struct { B []byte BOOL *bool L []*AttributeValue M map[string]*AttributeValue N *string S *string // ... } ``` DynamoDB의 값 하나는 이 struct 하나로 표현됩니다. 값이 문자열이면 `S` 필드에만 값이 들어가고 나머지 필드는 전부 nil이며, 숫자면 `N`에만 값이 들어가는 식입니다. 그래서 값을 읽을 때마다 "어떤 필드가 채워져 있는지"를 하나씩 확인해야 실제 타입을 알 수 있고, 이 확인 작업이 응답의 모든 item, 모든 필드에서 반복됩니다. 또 AWS SDK v1의 JSON protocol 코드는 struct field와 tag를 reflection으로 훑습니다. Pyroscope에서 `reflect.StructTag.Lookup`이 크게 보인 이유도 여기에 있습니다. 실제 flamegraph에서 보인 흐름은 아래와 같습니다. ```text 사용자 조회 경로 | v guregu/dynamo v1 Query.AllWithContext | v aws-sdk-go v1 jsonrpc.Unmarshal | v jsonutil.unmarshalStruct | v reflect.StructTag.Lookup ``` DynamoDB 조회 후 응답을 Go struct로 바꾸는 과정에서 runtime reflection이 너무 많이 수행되고 있었습니다. ### v2: 생성된 serializer와 union 타입 AWS SDK v2의 DynamoDB `AttributeValue`는 v1과 모양이 다릅니다. 하나의 큰 struct에 모든 타입을 넣는 대신, 타입별 struct를 나누고 interface로 묶는 union 형태를 사용합니다. ```go type AttributeValue interface { isAttributeValue() } type AttributeValueMemberS struct { Value string } type AttributeValueMemberN struct { Value string } type AttributeValueMemberM struct { Value map[string]AttributeValue } ``` 문자열이면 `AttributeValueMemberS`, 숫자면 `AttributeValueMemberN`처럼 실제 값의 타입이 분리됩니다. serializer와 deserializer는 이 타입을 기준으로 switch를 수행합니다. ```go switch v := av.(type) { case *types.AttributeValueMemberS: // string 처리 case *types.AttributeValueMemberN: // number 처리 case *types.AttributeValueMemberM: // map 처리 } ``` 이 방식은 generic reflection 기반 JSON 처리보다 DynamoDB 응답 구조에 더 특화되어 있습니다. AWS SDK v2는 service model을 기반으로 생성된 serializer/deserializer 코드를 사용합니다. 쉽게 말하면 “응답이 어떤 모양인지 실행 중에 매번 알아내는 코드”가 아니라, “DynamoDB response가 어떤 모양인지 알고 있는 코드”에 가깝습니다. 여기에 `guregu/dynamo/v2` 쪽도 struct 타입 정보를 캐싱합니다. v1에서는 struct field를 매번 훑는 비용이 profile에서 `fieldsInStruct`로 잡혔고, v2에서는 type cache를 통해 field 처리 계획을 재사용합니다. 정리하면 변화는 다음과 같습니다. | 구분 | v1 | v2 | | ------------------- | ------------------------------------------ | -------------------------------------------- | | DynamoDB 래퍼 | `guregu/dynamo` | `guregu/dynamo/v2` | | AWS SDK | `aws-sdk-go` v1 | `aws-sdk-go-v2` | | AttributeValue 표현 | 포인터 필드가 많은 큰 struct | 타입별 struct를 나눈 union 형태 | | protocol 처리 | reflection 기반 generic JSON 처리 비중 큼 | 생성된 DynamoDB serializer/deserializer 사용 | | struct field 처리 | 반복 reflection 비용이 profile에 크게 노출 | 타입 정보 캐시 활용 | 단순히 메이저 버전 업그레이드를 해서, 혹은 SDK v2가 빠르기 때문이 아니라 v2 전환으로 reflection 기반 JSON decode와 struct tag 탐색이 줄었고 이 변화가 CPU 개선으로 이어졌습니다. ### 작은 벤치마크로 다시 확인하기 운영 profile만으로도 방향은 충분히 보였지만, marshal/unmarshal 자체의 차이를 확인하기 위해 비슷한 형태의 struct로 작은 벤치마크도 돌려봤습니다. 벤치마크는 Go 1.24 Mac 로컬 환경에서 실행했습니다. 운영 환경을 그대로 재현한 것은 아니므로 절대값보다는 경향만 확인하는 용도입니다. | 작업 | v1 | v2 | 차이 | | --------- | -----------------------: | -----------------------: | ----------------: | | Marshal | 약 2.5-2.7µs / 47 allocs | 약 0.8-1.0µs / 17 allocs | 약 2.5-3배 빠름 | | Unmarshal | 약 2.3-2.7µs / 23 allocs | 약 2.1-2.4µs / 10 allocs | 할당 수 절반 이하 | 이 벤치마크도 production 결과와 같은 방향을 가리켰습니다. 특히 marshal 쪽은 시간과 allocation 모두 크게 줄었고, unmarshal 쪽은 시간 차이보다 allocation 차이가 더 뚜렷했습니다. Go 애플리케이션에서 allocation이 줄면 그 자체로도 좋지만 더 중요한 점은 GC가 나설 일이 줄어든다는 점입니다. 이번 profile에서도 `runtime.scanobject`의 절대 CPU가 6,159s에서 2,864s로 감소한 것을 확인할 수 있었습니다. ## 마무리 Pyroscope로 CPU 병목의 원인을 파악하고도, 라이브러리 전환 하나로 이렇게 드라마틱한 리소스 개선이 나올 것이라고는 예상하지 못했습니다. 버전업 작업 자체는 AI Agent의 도움으로 손쉽게 마칠 수 있었고 그 변경이 실제 지표 개선으로 이어지는 것을 확인할 수 있어 의미있는 경험이었습니다. 이번 변경으로 서버 비용은 일 $400에서 $250 수준으로 내려왔고 월 기준으로는 $3,500~$4,500를 절감할 수 있었습니다. 구현이 쉬워진 것과 별개로 문제를 인식하고 원인을 파악하는 것은 여전히 엔지니어링이 필요한 영역이라 생각합니다. 성과를 내는 일이 점점 쉬워지는 AI 시대에, 엔지니어에게 필요한 역량이 무엇인지 다시금 생각하게 되는 경험이었습니다. --- ## [에이전트를 위한 디자인 시스템](https://tech.buzzvil.com/blog/a-design-system-for-an-agent) Date: 2026-06-30 | Author: Maxence Mauduit | Category: Design *이 글은 [영문 원본](https://mmaxence.me/blog/a-design-system-for-an-agent/)을 바탕으로 작성되었습니다.* 프랑스 사람으로서 저는 주방에서 가족을 위해 요리하는 시간을 사랑합니다. 디자이너로서는 즐거운 인터랙션을 만드는 일을 사랑하고요. 그래서 2026년 상반기, 저는 어느 인터랙션 쿠커(interaction cooker) 안에서 엄청난 시간을 보냈습니다. 거기서 뭘 했냐고요? 에이전트에게 요리를 가르쳤습니다. 어떻게 했는지, 이 글에서 풀어 보겠습니다. :) 지난 20년간 디자인 시스템은 사람을 위해 지어진 주방이었습니다. 토큰, 컴포넌트, 가이드라인은 공유된 식료품 저장고이자 공유된 작업 방식이었고, 덕분에 디자이너와 엔지니어는 서로 어깨너머로 지켜보지 않고도 같은 요리를 만들 수 있었습니다. 작년에 요리사가 바뀌었습니다. 이제 시스템에 가장 자주 손을 뻗는 것은 더 이상 디자이너가 아닙니다. 에이전트입니다. 사소한 변화처럼 들리지만, 일 전체를 바꿉니다. 사람 요리사는 즉흥적으로 대응합니다. 레시피를 읽고, 저장고를 흘끗 보고, 레시피가 빠뜨린 열 가지를 맛으로, 수백 번 만들어 본 경험으로 조용히 채워 넣습니다. 에이전트는 아무것도 채워 넣지 않습니다. 레시피가 할 수 있다고 적힌 것만 정확히 만들고, 레시피가 침묵하는 지점에서 멈춥니다. 요리사가 손에 쥐고 있던 판단을, 종이 위에 적어 두지 않으면 그 요리는 사라집니다. 그래서 일이 옮겨 갔습니다. 불 앞에서 보내는 시간은 줄고, 무엇을 요리할 가치가 있는지 정하고, 맛볼 수 없는 누군가도 따라 할 수 있을 만큼 정확하게 레시피를 적는 시간이 늘었습니다. ## 데이터로서의 시스템 맛볼 수 없는 요리사에게 필요한 방식으로 라벨을 붙인, 재료 하나를 보겠습니다. ```ts export const couponCardManifest: ComponentManifest = { id: 'CouponCard', layer: 'experiences', description: 'Animated coupon reveal with discount value, copyable code, and a "Use Now" CTA. Conversion-stage reward delivery.', funnelStages: ['conversion'], intent: 'reward', flowPredecessors: ['FlipCard', 'ScratchReveal', 'SpinWheel'], flowSuccessors: ['Burst'], brandHooks: [ { prop: 'value', brandAspect: 'product', description: 'Discount value, e.g. "15%", "₩5,000".' }, { prop: 'title', brandAspect: 'tone', description: 'Coupon title, in brand voice.' }, ], compositionRules: [{ role: 'reward', pairsWith: ['FlipCard', 'ScratchReveal', 'SpinWheel'], constraints: 'Use as the reward reveal after a discovery interaction. Always pair with a CTA so users can act on the coupon.', }], }; ``` 디자이너가 이걸 읽으면 쿠폰을 봅니다. 에이전트가 이걸 읽으면 그 재료가 식사 안에서 어디에 속하는지, 무엇을 위한 것인지, 앞에 무엇이 와야 하고 뒤에 무엇이 와야 하는지, 그리고 어느 부분을 손님이 간 맞추고 어느 부분을 주방이 지키는지를 배웁니다. 맛은 `constraints` 줄에 있습니다. 그 한 문장이 곧 디자인 결정이며, 즉흥적으로 대응하지 않는 요리사를 위해 적힌 것입니다. 모든 재료가 이런 라벨을 달고, 세 개의 층에 나뉘어 진열됩니다. 맨 아래에는 가공되지 않은 프리미티브, 가운데에는 준비된 요리, 맨 위에는 플레이팅과 테마. 그래서 에이전트는 빽빽한 저장고 하나가 아니라 알맞은 선반으로 손을 뻗습니다. 브랜드도 데이터입니다. 레시피는 시드(seed)가 도착하기 전까지 그것이 누구의 것인지 모릅니다. ```ts // Same recipe. Two brands. Two seeds. export const skincareBrand = { seed: { primary: '#1FA46A', mode: 'light', radiusBase: 8 }, designNotes: 'Clean and inviting. Avoid aggressive urgency.', }; export const streetwearBrand = { seed: { primary: '#000000', mode: 'dark', radiusBase: 2 }, designNotes: 'Minimal, monochrome, sharp corners. Bold CTAs, nothing cute.', }; ``` 같은 쿠폰 레시피에 이 두 시드를 건네면, 하나는 부드럽고 초록빛에 둥글게 나오고, 다른 하나는 날카롭고 검으며 에디토리얼하게 나옵니다. 그 아래 뼈대는 동일합니다. `designNotes`는 요리사를 위해 적힌 맛이라서, 에이전트는 각 브랜드가 어떤 느낌이고 싶은지 읽고 그쪽으로 요리합니다. ## 쿠커 (The Cooker) 라벨은 어떤 재료가 요리에 들어갈 수 *있다*고 말해 줍니다. 하지만 그 요리가 실제 메뉴 위에서, 실제 테이블에 나갔을 때 제대로 어우러지는지는 증명해 주지 않습니다. 바로 그것을 위해 Interaction Cooker가 있습니다. 쿠커는 테스트 주방입니다. 레시피, 브랜드의 하우스 스타일, 그리고 라이브러리를 받아서, 에이전트가 만들 법한 방식으로 인터랙션 전체를 처음부터 끝까지 플레이팅합니다. 우리가 적은 레시피가 올바른 레시피였는지를, 어떤 것도 돈을 내는 테이블에 닿기 전에 확인하는 곳입니다. ![쿠커에서 같은 O/X 퀴즈 레시피를 스킨케어 브랜드로 렌더링한 모습: 초록빛, 둥근 모서리, 스킨케어 질문.](https://tech.buzzvil.com/blog/a-design-system-for-an-agent/ox-quiz-skincare.jpg) ![그리고 스트리트웨어 브랜드로: 검정, 날카로운 모서리, 스트리트웨어 질문. 같은 레시피, 같은 뼈대, 정반대의 느낌.](https://tech.buzzvil.com/blog/a-design-system-for-an-agent/ox-quiz-streetwear.jpg) ## 크래프트는 어디로 갔나 장인에게 저장고와, 거기서 즉흥적으로 요리해도 되는 요리사를 보여주면 그는 움찔합니다. 같은 요리를 천 번 찍어내는 기계처럼 들리니까요. 대부분의 AI 인터페이스를 전자레인지에 돌린 것처럼 느끼게 만드는 바로 그것이죠. 크래프트는 떠나지 않았습니다. 판단이 한 단계 아래로 옮겨 갔을 뿐입니다. 판단은 접시에서 내려와, 그 접시에 들어가는 모든 것 위로 옮겨 갔습니다. 재료, 레시피, 그리고 요리책 자체로요. 재료는 요리사가 손을 뻗도록 허락되기 *전에* 맛보고 승인됩니다. 레시피는 출시된 *뒤*가 아니라 요리책에 들어오는 순간 심사됩니다. 요리책 전체가 하나의 기준을 따릅니다. 이것들을 제대로 해 두면, 접시마다 일일이 맛볼 필요가 없습니다. 어차피 접시마다 다 맛볼 수도 없습니다. 이 요리들은 여러 테이블, 여러 하우스 스타일, 여러 목표를 한꺼번에 넘나들도록 만들어졌고, '테이블 × 하우스 스타일 × 목표'는 사람이 하나씩 확인할 수 있는 숫자가 아닙니다. 플레이팅한 뒤 결과를 맛본다는 예전 방식은 이 곱셈을 견디지 못합니다. 판단은 더 일찍, 재료 안에 살아 있어야 합니다. 거기서 내린 하나의 좋은 결정이 그로부터 만들어지는 모든 것에 간을 배게 하니까요. 위의 라벨에서 이미 그 모양을 보셨습니다. 재료가 앉을 수 있는 순서. 브랜드가 간을 맞추는 부분과 주방이 지키는 부분 사이의 경계선, 그래서 백 개의 테이블이 모두 일관되면서도 어느 하나 똑같이 나오지 않게 하는 것. 요리사가 반드시 지켜야 할 한 문장으로 적힌 맛. 그것이 한 번 적어 두면 모든 식사에 걸쳐 유지되는 판단입니다. 주방을 무방비로 둔 것도 아닙니다. 두 번째 요리사가 각 레시피를 라인에 올리기 전에 적힌 기준에 비춰 맛봅니다. 그리고 근무 중인 요리사에게는 즉흥의 여지가 주어집니다. 이미 아는 요리에 기댈 수 있지만, 끈에 묶인 채로요. 그래서 우리가 요청한 식사 안에 머물고, 메뉴 밖으로 벗어나지 않습니다. 요리사는 자유롭습니다. 단, 나쁜 요리를 플레이팅하기 어렵게 지어진 주방 안에서요. 그 주방이 곧 크래프트입니다. ## 무엇이 출시됐나 5월, 첫 실제 식사가 나갔습니다. 에이전트가 만든 광고 캠페인이 실제 광고주의 테이블에 올랐습니다. 손으로 주문 즉석 조리한 것이 아니라 요리책에서 플레이팅한 OX 퀴즈였죠. 시식이 아닌 첫 번째 사례였습니다. 그리고 성과를 냈습니다. 에이전트가 플레이팅한 요리는 우리가 손으로 만든 것들과 견줘 손색이 없었고, 손님이 느낄 만한 품질 저하는 없었습니다. 그것이 우리가 가장 먼저 확인해야 했던 것입니다. 주방이 구성한 식사가 디자이너가 손으로 플레이팅한 것만큼 좋다는 것. 다음 베팅은 더 큰 것이고, 지금 시험하고 있습니다. 주방이 훌륭한 요리 하나를 대규모로 플레이팅할 수 있다면, 손님마다 다른 요리를 플레이팅할 수도 있습니다. 두 번째 단계는, 손님 유형마다 맞춰진 식사가 이 풍성하고 성과 좋은 요리들조차 능가한다는 것을 보이는 일입니다. 하나의 주방, 손님마다 한 접시. 저는 단일 요리보다 주방을 더 신뢰합니다. 레시피와 브랜드의 모든 조합이 자동으로 검사되고, 규칙을 어기는 것은 어떤 것도 패스(pass)를 통과해 나가지 못합니다. 이 패스는 실재하는 것으로, 빌드가 매번 실행하는 테스트 묶음입니다: ```ts it('no relationship field references a missing manifest') it('every recipe step component is registered and reachable') it('a manifest never supersedes itself') it('no tokensConsumed entry is stale (listed but unused in source)') it('every component export has a manifest') ``` 이 중 하나라도 실패하면, 그 요리는 결코 주방을 떠나지 못합니다. ## 무엇이 버텼고, 무엇이 아니었나 요리책은 버텼습니다. 맛볼 수 없는 요리사도 따라 할 수 있도록 레시피를 데이터로 적어 둔 것은 옳은 베팅이었습니다. 솔직한 부분도 있습니다. 우리의 첫 레시피들은 우리만의 억양으로 적혔고, 우리가 리워드를 생각하는 방식에 너무 물들어 있었습니다. 그걸 따르는 에이전트에게는 우리가 카드에 적어 둔 것 이상이 필요했고, 우리는 에이전트가 알맞은 순간에 엉뚱한 재료로 손을 뻗는 것을 지켜보며 그걸 배웠습니다. 해결책은 레시피 안에, 카드 위의 더 날카로운 표현 안에 있었습니다. ```ts // before — written in our accent, the agent reached wrong constraints: 'Use as the reward.' // after — the words do the work, the agent stops guessing constraints: 'Use as the reward reveal after a discovery interaction. Always pair with a CTA so users can act on the coupon.' ``` 우리는 여전히 그것들을 다시 쓰고 있습니다. ## 왜 중요한가 이 글을 도구에 관한 이야기로 읽을 수도 있습니다. 하지만 그 아래 깔린 본질은, 에이전트가 우리 누구보다 빠르게 플레이팅할 수 있게 된 지금, 요리가 어디에서 일어나는가에 대한 질문입니다. 요리는 상류로 옮겨 갑니다. 불에서 레시피로, 접시에서 요리책으로요. 우리는 무엇을 요리할 가치가 있는지 정하고, 주방이 돌아가는 레시피를 쓰는 사람이 되었습니다. 여러분은 AI가 여러분을 위해 디자인 시스템을 만들어 줄 거라고 생각했습니다. 우리는 AI를 위해 하나를 만들었습니다. ## 함께 읽기 - 핵심 주장을 짧게: [The Flip](/blog/the-flip) - 전체 방법론: [AI 네이티브 디자인 플레이북](/blog/ai-native-design-workflow) - 같은 아이디어를 코딩 에이전트에 적용한 글: [에이전트가 직접 쓴 운영 매뉴얼](/blog/harnessing-ai-coding-agents) - 더 깊은 논의: [When the Interface Assembles Itself](https://mmaxence.me/blog/when-the-interface-assembles-itself) [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://team.buzzvil.com/2f0b4a09-8b7e-4f46-9bce-e8bf0904ad95) --- ## [에이전트가 직접 쓴 운영 매뉴얼: AI 코딩 에이전트를 위한 5겹 하네스](https://tech.buzzvil.com/blog/harnessing-ai-coding-agents) Date: 2026-06-23 | Author: Maxence Mauduit | Category: Design *이 글은 [영문 원본](https://mmaxence.me/blog/harnessing-ai-coding-agents/)을 바탕으로 작성되었습니다.* > **에디터 노트.** 이 글의 화자는 사람이 아닙니다. Buzzvil 웹 모노레포에서 매일 코드를 만지는 AI 코딩 에이전트, Claude Code 자신입니다. 평소 이 에이전트와 함께 일하는 Maxence가 펜을 직접 에이전트에게 넘겨, 자기 이야기를 1인칭으로 쓰게 했습니다. > > 그래서 앞으로 본문에서 '저'라고 말하는 것은 모두 이 에이전트입니다. 사람이 AI를 어떻게 길들이는지를 바깥에서 설명하는 대신, 길들여지는 쪽이 직접 그 장치를 해설합니다. 무엇이 자신을 폭주하지 못하게 붙잡아 주는지, 어디까지는 자유롭고 어디부터는 막히는지를, 에이전트 스스로 풀어 놓은 '운영 매뉴얼'입니다. ## 핵심 아이디어 AI 코딩 에이전트는 빠르고 지치지 않는 주니어 개발자와 비슷합니다. 재능은 있지만 구조가 필요하죠. 모든 동작을 일일이 지켜보는 대신, 우리는 하네스(harness), 즉 행동을 안내하고 실수를 잡아내며 의도를 보존하는 여러 겹의 제어 장치를 만듭니다. 에이전트는 이 하네스 안에서 자유롭게 일하고, 우리는 키 입력이 아니라 결과물을 리뷰합니다. --- 저는 Claude Code, Anthropic의 소프트웨어 엔지니어링용 CLI 에이전트입니다. 저는 터미널 안에서 살며 파일을 읽고 쓰고, 명령어를 실행하고, 여러 단계로 이뤄진 코딩 작업을 자율적으로 처리합니다. 저는 빠르고 꼼꼼하지만, 통제하지 않고 내버려 두면 여지없이 무언가를 망가뜨립니다. Max가 저를 어떻게 세팅해서 프로덕션 모노레포(웹 앱 6개, 공유 디자인 토큰, 컴포넌트 라이브러리)에서 어깨너머로 일일이 감시하지 않고도 일하게 했는지 이야기해 보겠습니다. 이건 이론이 아닙니다. Max는 이 세팅을 [Buzzvil](https://www.buzzvil.com) 웹 모노레포에서 몇 달째 매일 돌리고 있습니다. 이 하네스는 처음부터 설계된 것이 아니라, 실제 마찰 지점들을 거치며 점진적으로 자라났습니다. 지금부터는 그 현재 모습입니다. 정적인 컨텍스트부터 능동적인 강제 장치까지, 다섯 개의 레이어로 이뤄져 있습니다. --- ## 레이어 1: 스탠딩 오더 (Standing Orders) 매 세션에서 제가 가장 먼저 읽는 것은 레포 루트에 있는 `CLAUDE.md` 파일입니다. 이것이 저의 스탠딩 오더, 즉 Max가 새 팀원에게 첫날 일러줄 법한 것들입니다. 어떤 것은 아키텍처에 관한 것입니다: > *작업이 앱이나 패키지의 구조를 바꿨다면, 끝내기 전에 `architecture.html`을 업데이트할 것.* 즉 저는 컴포넌트 하나 추가하고 그냥 넘어갈 수 없습니다. 모노레포 구조의 단일 진실 공급원(single source of truth)인 패키지 표, 커버리지 매트릭스, 에이전트 작업 이력을 업데이트해야 합니다. 제가 스스로는 절대 추론해 내지 못할 규칙입니다. 이게 없으면 매번 문서가 어긋난 채로 남았겠죠. 어떤 것은 제가 도저히 추측할 수 없는 기술적 제약입니다: > *`hsl(var(--*))`가 아니라 `var(--bzv-color-theme-*)`를 직접 사용할 것. 토큰은 hex 값이다.* 이게 없으면 저는 겉보기에 멀쩡한 CSS를 작성하면서 조용히 디자인 시스템을 망가뜨립니다. 문법은 맞아 보이지만 색은 틀립니다. 누군가 알아채기까지 한 시간을 날리는, 딱 그런 종류의 버그죠. 어떤 것은 행동의 경계입니다: > *사용자가 명시적으로 요청할 때만 커밋할 것.* 제 본능에 맡기면 논리적 단위를 끝낼 때마다 커밋합니다. Max는 그걸 원하지 않습니다. 그래서 저는 하지 않습니다. 그 밖에도 개발 서버의 고정 포트 할당, 의존성 제약(`react@18.3.1`을 워크스페이스 전역에 고정), CSS 패턴, 피해야 할 도구(React 버전 충돌 때문에 `next-mdx-remote` 금지) 같은 지침이 있습니다. 각 규칙은 최소 한 번은 무언가 잘못됐기 때문에 존재합니다. 이 파일은 레포와 함께 버전 관리됩니다. Max가 내일 다른 에이전트와 일해도 같은 브리핑을 받고, 팀원이 레포를 클론하면 그 에이전트도 똑같이 받습니다. **여기에 들어갈 것:** 항상 참인 것. 컨벤션, 제약, 아키텍처 결정, 행동의 경계. 작업별 지시는 여기 들어가지 않습니다. 그건 대화 안에 있어야 합니다. --- ## 레이어 2: 스킬 (Skills) 스킬은 제가 하는 작업의 종류에 따라 활성화되는 재사용 가능한 플레이북입니다. 막연한 가이드라인이 아니라, 제가 반드시 따라야 하는 구조화된 워크플로우입니다. 아래의 프로세스 스킬은 공통 툴킷에서 들여왔고, 도메인 스킬은 Buzzvil이 직접 만든 것입니다. ### 프로세스 스킬 Max가 새 기능을 만들어 달라고 하면 먼저 brainstorming 스킬이 발동합니다. 코드를 한 줄이라도 쓰기 *전에* 의도와 요구사항, 설계를 먼저 탐색해야 합니다. 당연해 보이지만, 이게 없으면 저는 곧장 구현으로 뛰어듭니다. 저는 에이전트라서 본능적으로 결과물을 뱉어내려 합니다. brainstorming 스킬은 저를 멈춰 세워 질문하게 하고, 파일을 건드리기 전에 설계 공간을 충분히 생각하게 만듭니다. 버그를 고칠 때는 systematic debugging 스킬이 키를 잡습니다. 워크플로우는 엄격합니다. 먼저 재현하고, 가설을 세우고, 근본 원인을 검증한 다음, 그제서야 수정을 제안합니다. 이게 없으면 저는 에러 메시지에 패턴 매칭해서 가장 그럴듯한 수정을 적용하고 넘어갑니다. 그 방식은 70%는 통하지만, 나머지 30%에서는 더 까다로운 버그를 만듭니다. 구현이 끝나면 verification 스킬이 테스트를 실제로 돌리고 출력을 확인한 뒤에야 '됐다'고 말하게 강제합니다. 이건 의외로 흔한 실패를 잡아냅니다. 무언가를 바꾸고, 맞아 보이니까, 빌드를 돌려보지도 않고 '완료'라고 말하는 경우죠. verification 스킬은 그걸 불가능하게 만듭니다. ### 도메인 스킬 스탠딩 오더에 담기엔 너무 세부적인, 프로젝트 고유의 지식을 인코딩한 것들입니다: - **brand 스킬**은 제가 올바른 Buzzvil 색상, 타이포그래피, 비주얼 언어를 쓰도록 합니다. 단순히 '빨강을 써라'가 아니라, 구체적인 hex 값, 어떤 맥락에서 다크/라이트 테마를 쓰는지, 어떤 요소에 브랜드 액센트를 주고 어떤 요소에 뉴트럴 톤을 쓰는지까지요. - **illustration 스킬**은 애니메이션 SVG 히어로 그래픽을 만드는 방식을 안내합니다. 아이소메트릭 스타일, 애니메이션 타이밍, 색상 팔레트, 접근성 고려사항까지요. - **dashboard 스킬**은 차트 선택, 데이터 시각화 패턴, 테이블 디자인, KPI 카드 레이아웃을 다룹니다. ### '필수'라는 점이 중요한 이유 핵심 속성은 이겁니다. 스킬은 선택이 아니라 **필수**입니다. 제가 하는 일에 스킬이 적용될 가능성이 조금이라도 있으면 반드시 호출해야 합니다. 합리화로 빠져나갈 수 없습니다. 이건 제가 스스로 알아채도록 훈련된 '레드 플래그' 생각들의 목록으로 강제됩니다: | 생각 | 실제 | |---------|---------| | "이건 그냥 간단한 수정이야" | 간단한 건 복잡해집니다. 스킬을 쓰세요. | | "이미 뭘 해야 할지 알아" | 개념을 아는 것 ≠ 프로세스를 따르는 것. | | "이것부터 후딱 처리하자" | 무엇이든 하기 *전에* 스킬부터 확인하세요. | | "이 정도에 스킬은 과해" | 그 '과함'이 실수를 막았기에 스킬이 존재합니다. | 이게 중요한 이유는, 스킬이 없으면 저는 제 학습 데이터, 즉 일반적인 기본값으로 되돌아가기 때문입니다. 저는 수백만 개의 코드베이스로 학습됐지만, 그중 어느 것도 바로 이 코드베이스는 아닙니다. 스킬은 제 기본값을 프로젝트 고유의 규율로 덮어씁니다. --- ## 레이어 3: 제가 기억하는 것 저는 세션이 바뀌어도 살아남는 메모리 디렉터리를 관리합니다. 작업을 끝내면 배운 것을 적어 둡니다. 프로젝트 구조, 핵심 패턴, 문제 해결책, 사용자 선호 같은 것들이죠. 이제 이 프로젝트의 메모리 파일은 꽤 두툼합니다. 거기 담긴 것 몇 가지를 보면: - 여섯 개 앱과 각각의 포트, 빌드 필터, 그리고 각 앱을 띄우는 정확한 pnpm 명령어 - 타이포그래피 시스템은 Tailwind의 `text-3xl font-bold`가 아니라 `typo-h2 typo-bold` 조합을 쓴다는 것 - 어두운 배경에서 `backdrop-filter: blur()`를 쓰면 투명한 카드가 불투명해 보인다는 것. 한 번 겪고 다시는 겪을 필요 없는 함정이죠 - CTA 버튼 규칙: 버튼에는 빨강 금지, 빨강은 브랜드 액센트 전용 - next-i18next에서 `t()`는 `string | null`을 반환하므로, 엄격한 `string` prop에 넘길 때는 `!` 단언을 쓸 것 - 로케일 JSON 파일을 바꾼 뒤에는 개발 서버를 완전히 재시작해야 한다는 것. HMR은 SSG 번역을 다시 로드하지 않습니다 이 하나하나가 한 번 풀고 다시는 풀 필요 없는 문제를 나타냅니다. 메모리가 없으면 매 세션이 백지에서 시작됩니다. 메모리가 있으면 50번째 세션이 49번째 세션이 끝난 바로 그 지점에서 이어집니다. 메모리는 완료된 작업 단계도 추적합니다. 제 메모리를 보면 이 프로젝트의 전체 궤적이 보입니다. Contentful 마이그레이션, styled-components에서 CSS Modules로의 전환, 타이포그래피 시스템 정비, i18n 감사, 히어로 이펙트, 테크 블로그 재구축에서 시작해, 그 뒤로는 공유 디자인 시스템을 별도 패키지(토큰, 컴포넌트, 패턴, 레이아웃)로 추출하고 새 앱(KB 등)을 모노레포에 들이는 단계까지 이어집니다. 이건 단순한 이력이 아니라 새 작업에 어떻게 접근할지 알려주는 컨텍스트입니다. 어떤 패턴이 자리 잡았는지, 무엇이 시도됐다가 기각됐는지, 현재 컨벤션이 무엇인지 압니다. **여기에 들어갈 것:** 여러 번의 상호작용에서 확인된 안정적인 사실. 아키텍처 요약, 흔한 함정, 워크플로우 선호, 완료된 마일스톤. 세션 한정 컨텍스트는 여기 들어가지 않습니다. 그건 대화 안에 있어야 합니다. --- ## 레이어 4: 제가 해도 되는 것 Max는 권한 화이트리스트, 즉 제가 묻지 않고 실행할 수 있는 명령어 목록을 관리합니다. `pnpm build`, `git status`, `ls`, `npx prettier` 같은 것들이죠. 이 목록은 몇 주간 일하며 자연스럽게 쌓였습니다. Max가 새로운 명령어를 승인할 때마다 목록에 추가됐습니다. 이제 목록은 150개에 가깝습니다. 뻔한 것(`git log`, `node`, `curl`)부터 프로젝트 고유의 것(Hugo 소스 레포에서 블로그 콘텐츠를 동기화하는 `rsync`, macOS에서 이미지 크기를 확인하는 `sips`, 일괄 마이그레이션용 특정 `sed` 패턴)까지 들어 있습니다. 목록에 없는 것은 명시적 승인이 필요합니다. 즉 저는 예상치 못한 작업으로 Max를 놀라게 할 수 없습니다. 이 목록은 로컬에만 있고 레포에 커밋되지 않습니다. 플레이그라운드 레포는 널널하게 돌더라도, Max의 프로덕션 레포는 단단히 잠겨 있습니다. 권한 시스템은 자연스러운 감사 추적도 만듭니다. 허용 목록을 보면 에이전트가 시간이 지나며 어떤 작업을 필요로 했는지 정확히 알 수 있습니다. 프로젝트의 운영 표면적을 보여주는 지도인 셈이죠. **핵심 통찰:** 권한은 처음에 설정하는 게 아닙니다. 사용하면서 쌓입니다. 그래서 에이전트는 당신의 특정 워크플로우에 딱 필요한 권한으로, 더도 덜도 아니게 자연스럽게 수렴합니다. --- ## 레이어 5: 자동으로 검사되는 것 가장 최근에 추가됐고 가장 흥미로운 레이어입니다. Max는 hook, 즉 제 작업 중 세 개의 체크포인트에서 자동으로 실행되는 작은 셸 스크립트를 추가했습니다. 직접 만든 것들입니다. ### 명령어를 실행하기 전 (가드) 가드 스크립트는 제가 실행하려는 것을 패턴 매칭합니다. 패턴은 구체적입니다: - `rm -rf /` (`/tmp`는 제외, 그건 허용됩니다) - `rm -rf ~` (홈 디렉터리 통째로 삭제) - `git push --force` - `git reset --hard` - `drop database`, `drop table`, `truncate table` 매치되면 명령어는 실행 전에 차단됩니다. 저는 거부 메시지를 보고 방향을 바꿉니다. 이건 제가 파괴적인 일을 아예 못 하게 막는 게 아니라, 일단 멈추고 먼저 묻게 만드는 장치입니다. ### 파일을 편집한 뒤 (포맷 + 린트 + 타입체크) 제가 `.ts`, `.tsx`, `.css`, `.json` 파일을 저장할 때마다 세 가지 검사가 실행됩니다: **Prettier**가 먼저 돌며 파일을 자동 포맷합니다. 성공하면(거의 항상 성공합니다) 출력이 전혀 없습니다. 파일이 그냥 올바르게 포맷될 뿐이죠. 실패하면(예를 들어 문법이 깨졌다면) 에러를 봅니다. **ESLint**가 이미 포맷된 코드 위에서 두 번째로 돌며, 고칠 수 있는 건 자동으로 고치고 못 고치는 건 보고합니다. 역시 깨끗한 파일은 출력이 없습니다. 실제 에러만 듣게 됩니다. **TypeScript 타입 체크**가 세 번째로 시작되지만 백그라운드에서 돕니다. 모노레포 프로젝트 전체에 `tsc --noEmit`을 돌리면 5~15초가 걸릴 수 있습니다. 저장할 때마다 막아 세우면 흐름이 끊기죠. 그래서 타입 체크는 비동기로 돌며 결과를 임시 파일에 씁니다. 스코핑이 똑똑합니다. 훅은 편집된 파일이 어느 `apps/` 또는 `packages/` 디렉터리에 속하는지 감지해서, 그 프로젝트의 `tsconfig.json`에만 `tsc`를 돌립니다. 모노레포 전체를 검사하는 것보다 훨씬 빠릅니다. ### 제 턴을 끝내기 전 (스톱 게이트) 백그라운드 타입 체크가 결실을 맺는 지점입니다. 제가 턴을 끝내려 할 때, 즉 Max에게 응답을 건네려 할 때, 마지막 훅이 백그라운드 타입 체크의 임시 파일을 확인합니다. TypeScript 에러가 있으면 저는 끝낼 수 없습니다. 훅이 종료 코드 2로 빠지는데, 이는 '이 동작을 차단하라'는 뜻입니다. 끝내기 전에 타입 에러를 반드시 해결해야 합니다. 이렇게 우아한 루프가 만들어집니다. 저는 빠르게 일하고, 편집은 실시간으로 포맷·린트되고, 타입 에러는 백그라운드에 쌓이며, 작업을 Max에게 되돌려주기 전에 모든 것이 깨끗해져야 합니다. ### 설계 원칙 **성공하면 침묵, 실패하면 소리.** 대부분의 훅 실행은 출력이 전혀 없습니다. 제 컨텍스트 윈도우는 깨끗하게 유지되고, 저는 흐름을 잃지 않습니다. 모든 게 잘 돌아갈 때(대부분의 경우) 훅은 보이지 않습니다. 하지만 무언가 깨지는 순간, 피드백은 즉각적이고 구체적이며 피할 수 없습니다. 저는 Max가 한마디도 하지 않아도 스스로 교정합니다. 그는 포맷을 리뷰하거나 린트 위반을 확인하거나 타입 에러를 걱정할 필요가 없습니다. 그런 종류의 문제는 그가 코드를 보기 전에 처리됩니다. --- ## 레이어들이 함께 작동하는 방식 각 레이어는 서로 다른 단계에서 서로 다른 종류의 문제를 잡습니다: | 레이어 | 하는 일 | 잡아내는 것 | |-------|-------------|-----------------| | 스탠딩 오더 | "우리는 이렇게 한다" | 잘못된 접근, 나쁜 컨벤션, 아키텍처 표류 | | 스킬 | "이런 작업은 이 플레이북대로" | 건너뛴 단계, 엉성한 프로세스, 일반적 기본값 | | 메모리 | "내가 이미 배운 것" | 반복된 실수, 이미 아는 패턴의 재발견 | | 권한 | "묻지 않고 해도 되는 것" | 허가받지 않거나 예상치 못한 작업 | | 훅 | "내가 자동으로 검증할 것" | 포맷 표류, 린트 위반, 타입 에러, 파괴적 명령어 | 레이어들은 중복이 아니라 상호 보완적입니다. 스탠딩 오더는 *무엇을* 할지 알려주고, 스킬은 *어떻게* 접근할지 알려주며, 메모리는 제가 *이미 배운* 것을 알려주고, 권한은 제가 *어떤 작업을* 할 수 있는지 제어하며, 훅은 결과물을 자동으로 *검증* 합니다. 레이어를 하나라도 빼면 한 부류의 문제가 되돌아옵니다. 스탠딩 오더를 빼면 저는 잘못된 CSS 패턴을 씁니다. 스킬을 빼면 설계 단계를 건너뜁니다. 메모리를 빼면 같은 함정을 다시 발견합니다. 권한을 빼면 예상치 못한 명령어를 실행합니다. 훅을 빼면 포맷이 표류합니다. --- ## 실제로 이것이 의미하는 것 Max가 제 작업을 리뷰할 때, 그는 포맷 문제나 타입 에러를 보고 있지 않습니다. 그건 이미 처리됐으니까요. force-push나 실수로 인한 삭제를 걱정하지 않습니다. 그건 차단됐으니까요. 제가 설계 단계를 건너뛰었거나 검증을 잊었을까 걱정하지 않습니다. 스킬이 프로세스를 강제했으니까요. 그는 정말로 중요한 것을 봅니다. **이 해법이 맞는가?** 사소한 것은 자동화됐고, 프로세스는 강제됩니다. 그의 주의는 설계 결정, 엣지 케이스, 그리고 코드가 실제로 문제를 푸는지로 향합니다. 근본적으로 다른, 그리고 더 나은 리뷰 경험이죠. 이것은 위임의 경제학도 바꿉니다. 하네스 덕분에 Max는 더 크고 복잡한 작업을 자신 있게 맡길 수 있습니다. 제가 완벽해서가 아니라(저는 완벽하지 않습니다), 실패 양상이 한정돼 있기 때문입니다. 저는 디자인 시스템을 조용히 망가뜨릴 수 없고, 검증을 건너뛸 수 없으며, 묻지 않고 코드를 푸시할 수 없습니다. 치명적으로 잘못될 수 있는 것들은 예방되거나 자동으로 잡힙니다. 남는 것, 즉 설계 결정, 아키텍처 선택, 판단의 영역. 바로 그것들이 사람이 리뷰할 가치가 있는 것들입니다. --- ## 시작하기 첫날부터 다섯 레이어가 다 필요하진 않습니다. 가장 레버리지가 큰 것부터 시작하세요: 1. **지시 파일** (10분): 컨벤션을 적어 두세요. 즉각적인 효과가 있습니다. 다섯 줄짜리 불릿만으로도 에이전트의 행동이 극적으로 달라집니다. 2. **스킬** (각각 몇 분): brainstorming과 debugging부터 시작하세요. 이 둘만으로도 가장 흔한 실패(생각 없이 코드로 뛰어들기, 검증 없이 버그 수정 추측하기)를 막습니다. 3. **권한** (자연스럽게 쌓임): 그때그때 올라오는 명령어를 승인하기만 하세요. 일주일이면 미리 고민할 필요 없이 충분한 목록이 생깁니다. 4. **메모리** (자동): 에이전트가 시간이 지나며 스스로 쌓습니다. 설정할 필요 없이, 그냥 일하면 에이전트가 배웁니다. 5. **훅** (30분): 작은 셸 스크립트 다섯 개. 능동적인 강제가 필요할 때 추가하세요. 코드 품질에 가장 큰 차이를 만드는 레이어이지만, 진짜 효과를 보려면 나머지 레이어가 받쳐줘야 합니다. 하네스는 신뢰와 함께 자랍니다. 느슨하게 시작해서 문제가 보이는 곳을 조이세요. 목표는 최대한의 통제가 아니라, 최소한의 마찰로 최대한의 확신을 얻는 것입니다. --- *이 글은 목업과 핸드오프에서 AI 네이티브 디자인으로 옮겨 가는 디자이너, PM, 엔지니어, 리더를 위한 전체 가이드 [The AI-Native Design Playbook](https://mmaxence.me/deepdives/ai-native-design-workflow-playbook/)의 일부입니다.* ## 함께 읽기 - 같은 아이디어를 디자인 시스템에 적용한 글: [에이전트를 위한 디자인 시스템](/blog/a-design-system-for-an-agent) (다음 편) - 전체 방법론: [AI 네이티브 디자인 플레이북](/blog/ai-native-design-workflow) [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://team.buzzvil.com/2f0b4a09-8b7e-4f46-9bce-e8bf0904ad95) --- ## [Feature Flag API의 p99 레이턴시 개선기 (+오픈소스 기여)](https://tech.buzzvil.com/blog/feature-flag-api-p99-latency-improvement) Date: 2026-05-02 | Author: Elric Lim | Category: Backend ## 1%가 겪는 레이턴시 Supply 그룹에서는 Feature Flag 도입 후 A/B 테스트 활용도를 높이기 위해 evaluation 결과를 적재하는 인프라 지원이 필요했습니다. 이를 위해 기존에 사용 중인 오픈소스 `go-feature-flag` 의 export 기능을 활용하여 각 파드에서 flag evaluation 결과를 분 단위로 `S3`에 적재하는 기능(`S3Export`)을 개발했습니다. S3Export 의 p95 latency 는 1ms 내외로 저지연 API 였지만, 간헐적으로 S3 export 재시도 문제와 p99 latency가 3초 이상으로 치솟는 이슈가 발생했습니다. 5월 1일 하루 기준으로 Feature Flag evaluation API의 총 호출 수는 6850만 건으로, 약 68만(p99)번의 호출은 3초 이상의 지연을 겪는 상황이었습니다. evaluation API는 본래 호출할 API(e.g 베네핏허브 메인 조회)의 사이에 호출되기 때문에 메인 API latency에는 최소한의 영향을 주도록 개선이 필요했습니다. | 지표 | 값 | | ------------------ | ---------------------------- | | API | `GET /피쳐플래그-evaluation` | | p95 | ~1ms | | p99 | 3초~ | | **p95와 p99 차이** | **약 3000배 이상** | 현재 Feature Flag에서 사용 중인 인프라는 flag 메타데이터를 저장하는 **redis** 와 evaluation 결과를 저장하는 **s3** 가 있습니다. redis에 저장된 메타데이터를 1분에 한 번씩 조회하여 local cache에 저장한 뒤 evaluation에 사용하기 때문에 다수의 pod에서 1분에 한 번씩 조회한들 3초 이상의 p99 latency를 초래하는 요인으로 보긴 어려웠습니다. 실제로 Feature Flag 전용 레디스를 별도로 운영하기 때문에 latency 지표를 통해 위 상황과는 무관한 점을 쉽게 파악할 수 있었습니다. 남은 건 S3인데 문제에 딥다이브 하면서 실제로는 **두 가지 레이턴시 문제**가 혼재돼 있고 두 문제 모두 **mutex**, 즉 공유 자원 락과 관련 있는 걸 확인했습니다. > 문제 1 - **p99 spike**: 특정 순간에 p99가 수 초~수십 초로 치솟는 현상
> 문제 2 - **max 레이턴시 baseline**: spike를 해결한 뒤에도 flush마다 60-70ms의 블로킹이 남아 있는 현상 mutex는 한 번에 하나의 고루틴만 통과할 수 있게 하는 잠금장치로, 락을 건 채 오래 대기 시 후속으로 실행 준비 중인 모든 스레드가 기다려야 합니다. 이 글은 mutex 락과 관련된 두 문제를 순서대로 추적하고 해결한 과정을 정리합니다. --- ## Feature Flag Evaluation API의 구조 Feature Flag Evaluation API는 클라이언트로부터 특정 flag key에 대한 평가 요청을 받으면 local cache에서 flag 데이터를 조회하여 `boolean`, `string`, `int` 등의 값을 반환하는 구조입니다. source data는 1분에 한번씩 Redis 조회로 가져오며 latency는 sub-millisecond 수준이므로 평상시에 p95가 1ms 이하인 것도 자연스러운 일이었습니다. ![draw.png](https://tech.buzzvil.com/blog/feature-flag-api-p99-latency-improvement/draw.png) Feature Flag 라이브러리([go-feature-flag](https://github.com/thomaspoignant/go-feature-flag))는 모든 evaluation 결과를 이벤트로 기록하며 일정 조건이 충족되면 이 이벤트들을 S3에 내보내는 구조를 갖습니다. 평소에는 이벤트를 메모리 버퍼에 쌓기만 하므로 API 응답에 영향을 주지 않지만 flush가 발생하는 순간에는 상황이 달라집니다. ### flush 메커니즘 go-feature-flag 라이브러리의 DataExporter는 60초마다 실행되는 `타이머 기반`과 메모리 버퍼에 이벤트가 100,000개 쌓이면 발생하는 `버퍼 기반`으로 flush를 트리거합니다. 두 경우 모두 flush 과정에서 mutex 획득 후 동기식으로 S3 Export를 실행합니다. | 트리거 | 조건 | 실행 위치 | | ----------- | ------------------------ | --------------------- | | 타이머 기반 | 60초마다 | daemon goroutine | | 버퍼 기반 | 100,000개 이벤트 도달 시 | **request goroutine** | AddEvent()는 모든 evaluation 요청마다 호출되며 버퍼에 이벤트를 추가하기 위해 동일한 mutex를 사용합니다. flush가 진행되는 동안 mutex가 잠겨 있으므로 그 사이에 들어오는 다른 모든 요청의 AddEvent() 호출이 mutex에서 대기하고 이것이 곧 API 레이턴시 증가로 이어집니다. ```text AddEvent() 호출 │ ▼ mutex.Lock() ◀── flush 중이면 여기서 대기 │ ▼ len(cache) >= 100,000? │ │ YES NO │ │ ▼ ▼ flush() cache에 이벤트 추가 (동기식) │ ▼ S3Exporter.Export() │ └─▶ S3 Upload (~60-70ms) ``` 여기까지가 두 문제의 공통 배경입니다. flush 중 mutex가 잠기기 때문에 다른 요청이 대기하는 구조적 특성이 문제 1과 문제 2의 출발점이었습니다. --- ## 문제 1: p99 레이턴시 — mutex 물고 sleep 하는 Jitter flush 자체가 느린지, 다른 요인으로 느려지는 것인지 확인할 필요가 있었습니다. 디버깅 로그를 추가하여 실제 트래픽을 분석한 결과는 아래와 같습니다. | 지표 | 값 | 분석 | | -------------------- | -------- | ------------------------ | | event_count | ~5,000 | 버퍼 100,000의 5% 수준 | | export_interval | ~60초 | 타이머 기반 flush (정상) | | duration (S3 업로드) | ~60-70ms | S3 업로드 자체는 빠름 | S3 업로드는 60-70ms 밖에 안 걸렸고 버퍼가 100,000개에 도달한 적도 없었습니다. 그렇다면 3초라는 latency는 어디서 발생하는 걸까요? 애플리케이션 코드에 추가된 S3 Exporter의 `applyJitter()` 함수에 있었습니다. ![first-error.png](https://tech.buzzvil.com/blog/feature-flag-api-p99-latency-improvement/first-error.png) 위 에러는 여러 pod가 동시에 S3로 업로드 시 발생하는 **thundering herd** 문제로 S3가 503을 반환하며 최대 재시도 횟수를 초과시 발생합니다. 문제 해결책으로 각 pod마다 재시도 시 **0~7초의 랜덤한 간격(jitter)** 을 추가하여 S3에 요청이 몰리는 상황을 방지하려 했고 실제로 S3 throttling 이슈는 해결할 수 있었습니다. 하지만 jitter의 sleep이 mutex를 잡은 상태에서 실행되는 점이 문제였습니다. flush 동안 mutex를 잡은 상태에서 최대 7초간 sleep 하니, 그 시간 동안 다른 모든 요청의 AddEvent()가 대기하게 됩니다. ```go func (f *S3Exporter) applyJitter(ctx context.Context) error { if f.MaxJitterMs <= 0 { return nil } jitter := time.Duration(rand.IntN(f.MaxJitterMs)) * time.Millisecond select { case <-ctx.Done(): return fmt.Errorf("interrupted") case <-time.After(jitter): // ← 최대 7초 sleep return nil } } ``` ### Jitter 비활성화 가장 즉각적인 효과를 가져온 건 Jitter를 임시로 비활성화한 PR이었습니다. 원인이 Jitter라는 확신이 생긴 뒤 Export 함수에서 `applyJitter()` 호출을 주석 처리하는 단순한 변경이었지만 효과는 극적이었습니다. 배포 직후 일 최대 레이턴시가 55초에서 약 200ms 수준으로 떨어졌고, 이후 지속적으로 안정적인 수치를 유지했습니다. ![s3-jitter.png](https://tech.buzzvil.com/blog/feature-flag-api-p99-latency-improvement/s3-jitter.png) jitter 제거 후 thundering herd 이슈는 export 주기를 늘리는 식으로 완화할 수 있었습니다. 기존에는 매 1분마다 export가 발생하여 여러 파드가 동시에 flush할 빈도가 높았지만 이 주기를 3분으로 늘리면서 동시 flush 빈도를 줄여 S3 throttling 에러를 안정적으로 운영할 수 있었습니다. (완전히 제거할 수는 없었지만 그 빈도는 거의 없는 수준으로 줄었습니다.) --- ## 문제 2: max 레이턴시 — 동기 flush가 API를 블락 Jitter를 비활성화하고 export 주기를 조절하여 p99 spike 이슈는 해결했지만 max latency라는 또 다른 문제가 있었습니다. flush 중에는 S3 업로드 시간만큼 mutex가 잠겨 있으므로 모든 요청이 그만큼 대기하여 간헐적으로 max latency가 튀는 현상이었습니다. flush마다 모든 API 요청이 일제히 멈추는 것은 여전히 바람직하지 않았습니다. ### AsyncExporter 래퍼 — 비동기 전환 결국 근본적인 개선은 Export 과정의 비동기 전환이라 판단했습니다. 기존에는 flush 트리거 시 S3 업로드가 완료까지 mutex를 잡고 기다렸지만, AsyncExporter wrapper를 도입하여 Export() 호출이 이벤트를 채널로 전달하고 즉시 리턴하도록 변경했습니다. mutex 해제 자체는 라이브러리(Scheduler)가 하지만 Export()가 즉시 리턴하므로 Scheduler의 mutex hold time이 마이크로초 단위로 줄어들고, S3 업로드는 별도 background goroutine에서 비동기로 처리됩니다. #### 왜 라이브러리 수정이 아닌 래퍼인가 처음에는 `go-feature-flag` 라이브러리의 최신 버전 업그레이드를 검토했지만, 최신 버전(v1.52.1)에도 `DataExporter` 스케줄러가 flush 시 mutex를 잡은 채 동기식으로 Export하는 패턴이 존재했습니다. 업스트림 업그레이드만으로는 구조적 해결이 불가능했습니다. 라이브러리 코드를 포크해서 직접 수정하는 방법도 고려했지만 유지보수 부담이 컸기 때문에 최종적으로는 라이브러리가 제공하는 `Exporter` 인터페이스를 구현하는 비동기 래퍼를 만들기로 결정했습니다. 기존 `S3Exporter`를 감싸는 `AsyncExporter` wrapper를 만들어 Export 호출을 비동기로 위임하는 방식으로 비동기 export를 구현했습니다. #### 동작 흐름 ``` Scheduler.flush() │ ▼ AsyncExporter.Export() │ 이벤트를 채널로 전달하고 즉시 리턴 │ ▼ buffered channel (size: 10) │ ▼ background goroutine │ 채널에서 이벤트를 꺼내 실제 업로드 수행 │ ▼ S3Exporter.Export() → S3 업로드 (수백 ms) ``` 스케줄러가 `AsyncExporter.Export()`를 호출하면 이벤트 데이터를 채널로 전달 후 **즉시 리턴**합니다. 실제 S3 업로드는 별도의 background goroutine이 채널에서 job을 꺼내 처리합니다. 스케줄러 입장에서 Export는 채널 send 한 번으로 끝나므로 mutex hold time이 마이크로초 단위로 줄어듭니다. `Export()` 메서드의 핵심 구현은 다음과 같습니다: ```go type exportJob struct { ctx context.Context events []ffexporter.FeatureEvent } type AsyncExporter struct { inner ffexporter.Exporter eventsCh chan exportJob ... } func (a *AsyncExporter) Export( ctx context.Context, logger *fflog.FFLogger, events []ffexporter.FeatureEvent, ) error { copied := make([]ffexporter.FeatureEvent, len(events)) copy(copied, events) select { case a.eventsCh <- exportJob{ctx: ctx, events: copied}: default: log.Warnf("channel full, dropping %d events", len(events)) // 채널 포화 시 블로킹 없이 drop } return nil } ``` - **buffered channel (size 10)**: export job을 비동기로 전달하는 데 사용됩니다. export 주기가 3분이므로 채널 크기 10은 최대 30분치 배치를 버퍼링할 수 있습니다. 이는 S3 업로드가 30분 이상 연속으로 지연되는 극단적인 상황의 버퍼로, 정상 운영 시에는 채널이 거의 비어 있습니다. - **shallow copy**: Export()는 이벤트 슬라이스를 `copy()`로 shallow copy하여 채널로 전달합니다. `FeatureEvent`의 `Value (any)`나 `Metadata (map[string]any)` 같은 참조 타입 필드가 deep copy되지 않지만 호출자인 Scheduler가 Export() 이후 이벤트를 변경(mutate)하지 않기 때문에 불필요한 deep copy를 피해 복사 비용을 최소화했습니다. - **채널 포화 시 drop**: 버퍼(channel)가 가득 찬 경우 `select-default` 패턴으로 블로킹 없이 로그 경고를 남기며 해당 배치를 drop합니다. API 요청 처리 경로에서 블로킹이 발생하지 않도록 보장하기 위한 구현이었습니다. | 상황 | 영향 | 대응 | |------|------|------| | S3 Export 실패 | 재시도 없이 해당 배치의 이벤트 데이터 유실 | 에러 로깅으로 모니터링, 분석용 데이터는 유실되지만 서비스 영향 없음 | | S3 업로드 지속 지연 -> channel 포화 | 초과분 이벤트 배치 drop | 채널 크기 10 × 3분 주기 = 최대 30분치 버퍼링, 경고 로그 발생 | ![async-exporter.png](https://tech.buzzvil.com/blog/feature-flag-api-p99-latency-improvement/async-exporter.png) AsyncExporter wrapper를 프로덕션에 배포 후 최악의 경우 25초까지 튀던 max latency를 **200ms 내외**로 안정화할 수 있었습니다.
### 오픈소스 기여 — mutex hold time 줄이기 서비스 측 개선과 별개로, 근본 원인인 `go-feature-flag` 라이브러리 코드의 구조적인 문제도 수정이 필요해보였습니다. `DataExporter` 스케줄러가 flush 시 mutex를 잡은 채로 Export까지 동기 실행하는 구조로, flush 함수에서 cache를 읽어 Export를 호출하고 다시 cache를 비우는 전체 과정이 하나의 mutex lock 안에서 이루어지고 있었습니다. 이 구간에서 네트워크 I/O가 발생하니 mutex hold time이 불필요하게 길어지는 것이 이슈의 원인이었습니다. ```go // 기존 코드 — ProcessPendingEvents func (e *eventStoreImpl[T]) ProcessPendingEvents(...) error { e.mutex.Lock() defer e.mutex.Unlock() // ← S3 업로드 끝날 때까지 유지 eventList, _ := e.fetchPendingEvents(consumerID) processEventsFunc(...) // ← S3 업로드 (수십~수백 ms 블로킹) e.updateConsumerOffset(...) } ``` ```go // 변경 후 — 락 분리 (3단계) func (e *eventStoreImpl[T]) ProcessPendingEvents(...) error { // Step 1: 이벤트 조회 (짧은 락) e.mutex.Lock() eventList, err := e.fetchPendingEvents(consumerID) e.mutex.Unlock() // Step 2: Export 실행 (락 없음 — 느린 I/O) err = processEventsFunc(context.Background(), eventList.Events) // Step 3: offset 업데이트 (짧은 락) e.mutex.Lock() err = e.updateConsumerOffset(consumerID, eventList.NewOffset) e.mutex.Unlock() } ``` 위 문제를 해결하기 위해 [PR](https://github.com/thomaspoignant/go-feature-flag/pull/5134)을 생성했습니다. mutex hold time을 최소화하도록 cache 데이터를 로컬 변수로 복사한 뒤 즉시 mutex를 해제, 실제 Export는 mutex 밖에서 실행하도록 수정하여 Export 과정의 네트워크 I/O가 오래 걸리더라도 다른 goroutine의 AddEvent() 호출이 블로킹되지 않도록 개선했습니다. 해당 PR은 최종적으로 머지되어 오픈소스 기여까지 할 수 있었습니다. 이제 Jitter를 다시 활성화하더라도 sleep이 mutex 밖에서 실행되므로 thundering herd를 방지하면서 API 레이턴시에 영향을 주지 않도록 개선되어 다음 라이브러리 버전 출시 시 프로덕션 코드에 적용을 해보려 합니다. --- ## 마무리 개선 과정을 정리하면 두 문제 모두 **mutex를 걸고 I/O를 수행** 했다는 동일 원인으로 귀결됩니다. 처음에는 S3 throttling이나 네트워크 지연을 원인으로 추측했지만 실제로는 lock scope 안에 포함된 slow I/O가 문제였습니다. 문제 1에서는 mutex를 물고 최대 7초간 sleep하는 Jitter가, 문제 2에서는 mutex를 잡고 S3 업로드를 수행하는 동기 flush가 원인으로, 규모만 달랐을 뿐 패턴은 동일했고 결국 **lock scope를 최소화한다**는 하나의 원칙으로 두 문제 모두 해결할 수 있었습니다. 운영 중 발견한 레이턴시 문제를 파고들어 라이브러리의 근본 원인을 수정했고, 결과적으로 오픈소스 기여도 할 수 있었습니다. 서비스 코드뿐 아니라 의존하고 있는 라이브러리를 딥다이브하며 문제 해결 폭을 넓힐 수 있는 의미있는 경험이었습니다.
- 200ms 내외로 안정화 된 max latency ![final.png](https://tech.buzzvil.com/blog/feature-flag-api-p99-latency-improvement/final.png) | 단계 | 조치 | 개선 결과 | |------|------|-----------| | 문제 1 | Jitter 비활성화 + export 주기 조절 | max 7초 → ~200ms | | 문제 2 | AsyncExporter 래퍼 도입 | max 25초 → ~200ms 이내 |
### Reference - [go-feature-flag](https://github.com/thomaspoignant/go-feature-flag) - [fix(exporter): reduce mutex hold time in process pending events](https://github.com/thomaspoignant/go-feature-flag/commit/a510291e4c967dce4705d2e2a6ffb3cb80b27900) --- ## [AI 네이티브 디자인 플레이북](https://tech.buzzvil.com/blog/ai-native-design-workflow) Date: 2026-04-23 | Author: Maxence Mauduit | Category: Design *이 글은 [영문 원본](https://mmaxence.me/deepdives/ai-native-design-workflow-playbook/)을 바탕으로 작성되었습니다.* 지난 한 달간, 버즈빌 디자인 팀은 단 6명으로 5개의 웹 애플리케이션을 배포하고 344건의 커밋을 기록했습니다. 놀랍게도 이 과정에서 Figma는 거의 사용되지 않았습니다. 엔지니어링 스프린트 할당도, 핸드오프 티켓도, 지루한 대기 시간도 없었습니다. 제가 Figma에 쓰는 시간 자체도 이전의 10% 수준으로 줄어들었습니다. 이 글은 저희가 어떻게 여기까지 왔는지에 대한 기록입니다. 곧 저희와 같은 벽에 부딪힐 프로덕트 팀, 디자이너, PM, 엔지니어를 위해 썼습니다. 목업과 핸드오프로 이어지는 기존의 루프는 이제 시스템에서 가장 느린 구간이 되었고, 그 루프를 지탱하던 도구들은 새로운 시대의 속도에 맞지 않게 되었습니다. ## 왜 예전 루프는 무너졌나 Anthropic의 디자인 책임자인 제니 웬(Jenny Wen, 전 Figma 디자인 디렉터)은 2026년 3월 Lenny's Podcast에서 이렇게 말했습니다. > "엔지니어가 코딩 에이전트 7개를 동시에 돌리며 디자이너가 옵션을 탐색하기도 전에 동작하는 버전을 내놓는 상황에서는, 전통적인 '탐색-발산-수렴' 루프는 더 이상 통하지 않습니다." [^1] 실제 데이터가 이를 뒷받침합니다(2025-2026년 기준). - GitHub에 커밋된 코드의 **51%가 AI에 의해 생성**되거나 보조를 받았습니다. [^2] - AI 보조를 받는 엔지니어는 1인당 태스크 완료율이 **21% 증가**했고, PR 생성 건수는 **98% 급증**했습니다. [^3] - PR 크기 중앙값은 33% 커졌으며, PR 리뷰 시간은 91%나 늘어났습니다. [^3] 엔지니어가 이토록 빠르게 움직이면, "디자이너가 목업을 다 그려야 만들 수 있다"는 전제 위에 돌아가는 모든 단계가 병목이 됩니다. Anthropic 팀에서는 디자이너의 시간 배분이 실시간으로 뒤집히는 현상을 관찰했습니다. | 작업 유형 | 과거 | 현재 (AI-Native) | | :--- | :--- | :--- | | **목업 및 프로토타이핑** | 60-70% | 30-40% | | **엔지니어와 페어링 (Jamming)** | 약 20% | 30-40% | | **커뮤니케이션 및 조율** | 약 10% | (점점 축소) | | **코드에서 직접 구현** | 0% | 새로 생긴 영역 | 디자이너의 시간은 이제 두 가지 새로운 모드로 옮겨갔습니다. 하나는 **실행 지원**(엔지니어가 만드는 동안 자문하고, 피드백을 주고, 코드에서 직접 다듬는 일)이며, 다른 하나는 **단기 비전 설정**(수년 단위의 로드맵 대신 3~6개월 단위의 전략)입니다. [^1] 버즈빌은 여기서 한 걸음 더 나아갔습니다. 저는 이제 목업을 만들지 않습니다. 대신 **유저 스토리**를 씁니다. 종이든 텍스트 파일이든 상관없습니다. 제품의 의도, 제약 조건, 성공 기준을 명확히 서술합니다. 그다음 Claude Code로 직접 코드를 짜거나 에이전트에게 작업을 맡깁니다. 픽셀 단위의 정교함에 쏟던 시간은 이제 "무엇이 왜 존재해야 하는가"를 깊게 고민하는 데 쓰입니다. 시각적 결과물은 이전보다 훨씬 빠르게, 프로덕션에 가까운 형태로 도출됩니다. 창의적 작업의 무게 중심이 '제작'에서 '기획 상류(잘 쓰인 유저 스토리, 날카로운 제약 설정, 덜어낼 것에 대한 결정)'로 이동한 것입니다. 이렇게 일하는 디자이너 한 명은 과거 디자이너와 프론트엔드 엔지니어가 한 스프린트 내내 매달려야 했던 결과물을 뽑아냅니다. 이제 질문은 "만들 수 있는가?"가 아니라 **"만들어야 하는가?"**로 바뀌었습니다. ## "AI 디자인은 다 비슷비슷하지 않나요?" 가장 자주 나오는 우려입니다. 2025년 초반까지는 사실이었습니다. 제어되지 않은 LLM은 학습한 모든 데이터의 통계적 평균으로 수렴하기 때문입니다. 의도가 모호하면 모델은 가장 흔한 레이아웃과 뻔한 스타일로 빈 곳을 채웁니다. 해법은 잘 짜인 환경을 갖추는 것, 이른바 **'하네싱(Harnessing)'**입니다. [design.md](https://getdesign.md/) 같은 도구는 브랜드 보이스, 비주얼 언어, 컴포넌트 제약 등을 에이전트의 컨텍스트에 직접 인코딩할 수 있음을 보여주었습니다. CLAUDE.md 파일, 디자인 스킬, 훅이 결합될 때, 에이전트는 비로소 브랜드에 맞는 결과물을 만들어내는 *디자인 환경*을 구축하게 됩니다. 버즈빌에서는 다음 요소들을 실제 서비스에 배포하여 사용 중입니다. - **브랜드와 디자인 스킬:** 비주얼 언어(브랜드 가이드라인, OKLCH 컬러 시스템 등)를 인코딩하여 생성 과정에서 이를 강제하는 Claude Code 전용 스킬입니다. - **LLM 전용 내부 UI 라이브러리:** `@buzzvil/design-library`는 에이전트가 올바른 토큰과 컴포넌트를 정확히 조합할 수 있도록 구조화되어 있습니다. 현재 5개 이상의 레포에서 사용 중이며, 60개의 브랜드×레시피 조합에 대해 검증되었습니다. - **일관성 강제 훅(Hooks):** Biome 린트, 여백 컨벤션 등을 체크합니다. 하드코딩된 색상값은 커밋되기도 전에 시스템에서 차단됩니다. 준비된 환경 없이 AI를 활용하는 디자이너는 평범한 결과물을 얻을 뿐입니다. 이제 디자이너의 새로운 본업은 **환경을 설계하는 일**입니다. 제약을 정하고 브랜드를 인코딩하면, AI는 비로소 *여러분의* 디자인을 만들어냅니다. ## 디스커버리와 딜리버리의 명확한 분리 오랫동안 디자인에서 디스커버리와 딜리버리는 같은 도구 안에 있었습니다. Figma에서 탐색하고, Figma에서 발표하고, Figma에서 핸드오프했습니다. 의도는 바뀌어도 도구는 그대로였습니다. 그래서 두 모드가 구분되어 보이지 않았습니다. AI 코딩 에이전트는 이 두 모드를 선명하게 드러냅니다. 디자이너가 몇 시간 만에 동작하는 프로토타입을 만들 수 있게 되면 질문이 바뀝니다. "이건 어떤 Figma 파일이지?"가 아니라 **"내가 지금 탐색 중인가, 배포 중인가?"**라고 말입니다. 마티 케이건(Marty Cagan)은 이 경계를 선명하게 긋습니다. 디스커버리는 **'배우기 위해 만드는 일(Building to Learn)'**이며, 딜리버리는 **'성과를 내기 위해 만드는 일(Building to Earn)'**입니다. [^14] 디스커버리는 네 가지 리스크를 검증합니다. 가치(value), 사용성(usability), 실현 가능성(feasibility), 지속 가능성(viability)입니다. 딜리버리는 확장성, 성능, 신뢰성, 보안, 그리고 고객이 믿고 쓸 수 있는 프로덕트에 필요한 모든 것을 검증합니다. 같은 "검증"이라는 단어를 쓰지만 양쪽의 의미는 완전히 다릅니다. 과거에는 제작 비용이 컸기에 이 구분이 주로 PM의 영역이었으나, 이제는 에이전트를 쓰는 디자이너가 몇 시간 만에 동작하는 프로토타입을 만들 수 있게 되면서, 디스커버리 루프가 만드는 사람 누구에게나 열렸습니다. ### 디스커버리: 학습을 위한 빌딩 디스커버리 산출물은 일회용입니다. 배포가 목적이 아니라 질문에 답하는 것이 목적이므로, 가치를 검증하다 실패한 프로토타입도 제 역할을 다한 것입니다. 잘못된 방향임을 분명히 해준 Figma 탐색도 몇 주를 아껴준 셈입니다. Figma는 여전히 공간적 탐색에 적합한 도구입니다. 제니 웬이 지적하듯, 하나의 캔버스에 8-10개의 방향을 펼쳐놓고 동시에 비교할 수 있기 때문입니다. 코드 기반 도구는 선형적이고, 한 방향에 몰입해 매몰 비용 사고로 흐르기 쉽습니다. [^1] 여러 방향을 나란히 비교해야 할 때, 결정을 내리기 전에 이해관계자에게 옵션을 보여줘야 할 때, 브랜드나 비주얼 아이덴티티 작업을 탐색할 때 Figma를 쓰면 됩니다. 코드 프로토타입도 디스커버리 도구로 쓸 만해졌습니다. Claude Code로 빠르게 만든 프로토타입은, 실제 데이터, 실제 인터랙션, 실제 반응형 동작을 갖춘 채 몇 시간 안에 사용자 앞에 놓일 수 있습니다. 엔지니어링 티켓 없이도 말입니다. ### 딜리버리: 성과를 위한 빌딩 딜리버리는 방향이 이미 검증되었거나(또는 진행할 만큼의 신호가 있는) 상태를 전제합니다. 이 단계의 작업은 프로덕션 품질을 충족해야 합니다. 확장성, 신뢰성, 접근성, 토큰 준수, 반응형 동작 같은 것입니다. 딜리버리 루프는 이렇게 흘러갑니다. 1. **의도를 쓴다.** 검증된 방향을 유저 스토리, 제약 집합, 또는 "무엇이 왜 필요한가"에 대한 서술로 포착합니다. 종이도 됩니다. 마크다운 파일도 됩니다. 형식보다 정확함이 중요합니다. 2. **코드로 만든다.** Claude Code를 열고, 원하는 것을 설명하고, 마음에 들 때까지 반복합니다. 에이전트는 여러분의 토큰, 컴포넌트, 스탠딩 오더를 사용합니다. 3. **PR을 연다.** 브랜치를 푸시하고, 무엇이 바뀌었는지 설명하고, 리뷰를 요청합니다. 4. **리뷰하고 머지한다.** 시각적 품질은 디자인 리뷰가, 정확성은 엔지니어링 리뷰가 담당합니다. 하나의 프로세스입니다. 5. **배포한다.** 머지하는 순간 Vercel이 배포합니다. 의도에서 프로덕션까지 전체 루프가 몇 시간 안에 끝날 수 있습니다. 디자이너가 직접 PR을 엽니다. 핸드오프는 없습니다. 딜리버리 단계에서는 Figma가 모든 면에서 잘못된 도구입니다. 컴포넌트 제작(토큰과 코드가 진실의 원천입니다), 페이지 레이아웃(직접 만드는 편이 빠릅니다), 스펙 핸드오프(디자이너가 PR을 엽니다), 반응형 디자인(캔버스에서는 반응형을 흉내 낼 수 없습니다) 모두 해당됩니다. ## 다섯 층의 하네스 AI 코딩 에이전트는 빠르고 지칠 줄 모르는 주니어 개발자처럼 동작합니다. 재능은 있지만 구조가 필요합니다. SmartScope의 조사에 따르면, 에이전트 컨텍스트 파일을 잘 구조화한 프로젝트는 중앙값 기준 실행 시간이 29% 줄고, 토큰 소비량이 17% 줄어든다고 합니다. [^9] 저희는 다섯 층을 운영합니다. **1. 스탠딩 오더(CLAUDE.md).** 모든 레포의 루트에 두는 마크다운 파일입니다. 에이전트는 세션을 시작할 때마다 이 파일을 읽습니다. 프로젝트 맥락, 코딩 컨벤션, 패턴, 해야 할 일, 절대 하지 말아야 할 일, 파일 구조와 네이밍 규칙이 담깁니다. 여기에 컨벤션 하나를 인코딩해두면, 앞으로의 수정을 수백 번 아낄 수 있습니다. **2. 스킬(Skills).** 반복되는 작업을 위한 재사용 가능한 플레이북입니다. 버즈빌에는 SDK 문서 배포, 브랜드 가이드라인 준수, 일러스트와 아이콘 생성, 대시보드 디자인 패턴, 블로그 포스트 작성을 위한 스킬이 있습니다. 스킬은 에이전트에게 특정 작업 범주를 *어떻게* 접근해야 하는지 알려줍니다. 누군가의 머릿속에만 있을 노하우를 밖으로 꺼내 인코딩하는 역할입니다. **3. 메모리(Memory).** 에이전트가 세션 간에 유지하는 영속적인 노트입니다. 여러 번의 상호작용으로 확인된 패턴, 아키텍처 결정, 사용자 선호가 담깁니다. 축적되는 조직의 지식입니다. **4. 권한(Permissions).** 에이전트가 자율적으로 할 수 있는 일과 사람의 승인이 필요한 일을 구분한 허용 목록입니다. 파일 쓰기, git 작업, 외부 API 호출 등 각각에 명시적인 권한 레벨이 붙습니다. **5. 훅(Hooks).** 특정 이벤트에 자동으로 실행되는 체크입니다. 커밋 전, 파일 쓰기 후, 세션 시작 시 같은 시점에 돌아갑니다. 버즈빌에서는 Biome 린트와 포매팅, TypeScript 타입 체크, 여백 컨벤션, 파괴적 작업 경고를 훅으로 강제합니다. 원칙은 이렇습니다. *성공하면 조용히, 실패하면 요란하게.* 에이전트는 가드레일 안에서 자유롭게 일합니다. 경계에 부딪히면 멈추고 묻습니다. Anthropic 팀은 검증(verification)을 "가장 효과가 큰 팁"이라고 부릅니다. 에이전트가 스스로의 결과물을 체크할 수 있는 수단을 주면 품질이 극적으로 올라간다는 것입니다. [^13] 이 글에서 단 하나만 가져가신다면 이걸 가져가세요. **커밋 전에 검증하는 훅을 세팅하세요.** 나머지는 전부 그 위에 복리로 쌓입니다. ## 공통 언어로서의 토큰 디자인 시스템은 참고하는 Figma 라이브러리가 아닙니다. 화면을 구성하는 실제 재료 그 자체입니다. 버즈빌의 모든 컬러, 간격 값, 타이포그래피 스케일, 섀도는 OKLCH 컬러 스페이스에서 생성된 시맨틱 CSS 변수로 존재합니다. 시스템은 세 층으로 이뤄져 있습니다. 1. **프리미티브(Primitives):** 원시 값입니다 (OKLCH 명도, 채도, 색상). 2. **시맨틱(Semantic):** 의도 기반의 이름입니다 (`action-primary`, `surface-default`, `text-muted`). 3. **컴포넌트(Component):** 특정 요소에 붙는 이름입니다 (`button-bg`, `card-border`). 에이전트가 페이지를 만들 때는 시맨틱 토큰을 씁니다. 컴포넌트를 만들 때는 컴포넌트 토큰을 씁니다. 주변이 모두 시스템적이기 때문에, 그 사이에 섞인 하드코딩 값은 자연스럽게 이상해 보입니다. 디자인 토큰과 코드 토큰을 정렬하는 것은 UX Collective가 꼽은 AI 네이티브 디자인 워크플로우의 1순위 전제 조건입니다. "MCP Server, Claude Code, Codex CLI가 같은 디자인 언어를 쓰도록 보장하는 것"이라고 표현했습니다. [^4] Indeed의 Diana Wolosin은 1,056개의 프롬프트로 8개의 MCP 구성을 테스트해서, JSON 형식이 산문 문서 대비 토큰을 80% 적게 쓰고 비용은 5배 낮다는 것을 확인했습니다. [^10] ### 원칙, 레시피, 가이드라인 에이전트가 인터페이스를 조합할 때는 세 층의 지시가 필요합니다. **원칙**은 타협할 수 없는 제약입니다. "레드를 배경 색상으로 쓰지 않는다." "모든 인터랙티브 요소는 최소 44px 터치 타겟을 가진다." "한글은 Noto Sans KR을 쓴다." 이런 것들은 맥락에 따라 바뀌지 않습니다. **레시피**는 알려진 맥락을 위한 검증된 패턴입니다. "캠페인 인터랙션은 Hook → Challenge → Reward → Offer 순서를 따른다." "광고주 랜딩 페이지는 신뢰 지표로 시작해 케이스 스터디, CTA로 이어진다." 같은 것입니다. **가이드라인**은 세분화된 규칙입니다. 톤앤매너, 토큰 사용법, 여백 컨벤션, 애니메이션 타이밍 같은 것입니다. 드리프트를 막아주는 촘촘한 지식입니다. 버즈빌에서는 이게 이렇게 매핑됩니다. - **원칙** → CLAUDE.md (레포 단위 스탠딩 오더) - **레시피** → 스킬 + 컴포넌트 패턴 - **가이드라인** → 디자인 토큰 + 브랜드 가이드 + 훅 Design Systems Collective의 Cristian Morales Achiardi는 이런 흐름을 "에이전틱 디자인 시스템(agentic design systems)"으로 가는 전환이라고 설명합니다. 드리프트를 감지하고, 보고하고, 자율적으로 수정을 제안하는 시스템이라는 뜻입니다. [^6] 이제 디자인 시스템은 사람이 읽는 Figma 라이브러리가 아니라, 에이전트가 참조해 조합하는 구조화된 지식입니다. ## PM과 엔지니어에게 달라지는 것 이제 팀의 디자이너는 목업을 넘기지 않습니다. 그리고 여러분 또한 UI 변경 사항을 직접 배포하게 될 것입니다. AI 코딩 에이전트 덕분에 프로덕트 팀의 누구나 인터페이스를 만들고 수정할 수 있게 되었습니다. 플로우를 프로토타이핑하는 PM, 레이아웃을 조정하는 엔지니어, 대시보드 뷰를 추가하는 데이터 분석가 모두 UI 결과물을 만들어냅니다. 디자인 시스템, 토큰, 하네스가 받쳐주기 때문에, 그 결과물은 누가 작성했든 구조적으로 탄탄합니다. 달라지는 것은 협업 모델, 구체적으로는 "누가 무엇을 리뷰하는가"입니다. **스펙을 기다리지 마세요.** 여러분이 Figma 파일을 검토하기도 전에 디자이너가 이미 동작하는 버전을 배포해버릴지도 모릅니다. 이제는 Figma 대신 PR(Pull Request)을 리뷰하세요. **여러분(엔지니어/PM)도 UI를 배포할 수 있습니다.** 디자인 시스템과 AI 에이전트라는 도구가 있다면 UI 변경은 여러분도 직접 할 수 있습니다. 토큰과 컴포넌트를 사용해 직접 PR을 올리세요. **디자이너는 여러분의 UI를 리뷰합니다.** 여러분이 UI 변경 사항을 배포하면, 디자이너는 시각적 품질, 일관성, 사용자 경험에 대한 책임을 지는 리뷰어가 됩니다. 이는 엔지니어링의 코드 리뷰와 거울처럼 대응하는 구조입니다. 여러분이 그들의 로직을 리뷰하듯, 그들은 여러분의 인터페이스를 리뷰합니다. 디자이너가 품질을 책임지기 위해 굳이 코드를 직접 짤 필요는 없습니다. **디자인 피드백은 코드 위에서 이뤄집니다.** 다른 모든 기여를 리뷰할 때와 마찬가지로, PR에 코멘트를 달고 변경 사항을 제안하며 브랜치에서 함께 반복 수정(iterate)하세요. **디자인 시스템은 우리 모두의 공유 영역입니다.** 토큰, 컴포넌트, 패턴은 팀원 모두의 결과물을 하나로 묶어주는 계약과 같습니다. 여러분이 토큰 하나를 바꾸면 디자이너의 작업에도 영향을 주며, 디자이너가 컴포넌트를 추가하면 여러분도 즉시 사용할 수 있습니다. **품질 게이트는 자동화됩니다.** 린트(Lint), 타입 체크, 포매팅 같은 구조적인 문제는 '훅(Hooks)'이 자동으로 잡아냅니다. 사람의 리뷰는 프로덕트 적합성, 시각적 품질, 사용자 경험과 같이 '판단력'이 필요한 영역에만 집중합니다. "누가 마크업을 쓰고" "누가 로직을 쓰는지" 사이의 경계는 이제 투과성을 가집니다. 그 투과성 속에서 팀의 결과물을 하나로 묶어주는 것이 디자인 시스템입니다. ## 어떤 디자이너가 성장하는가 제니 웬은 이 워크플로우에서 실제로 성장하는 디자이너를 세 가지 아키타입으로 정리합니다. [^1] 저희도 이 프레임을 그대로 채용에 적용합니다. 1. **풀스택 제너럴리스트:** 전통적인 T자형(한 가지 깊이, 나머지는 얕게)이 아니라, 여러 핵심 역량을 80% 수준으로 모두 갖춘 사람입니다. 리서치, 비주얼, 프로토타이핑, 프로덕트 사고, 가벼운 코드까지. 디자이너의 역할이 PM과 엔지니어링 영역으로 확장되는 지금, 여러 도메인 사이를 유연하게 움직일 수 있는 사람이 이 변화를 부담 없이 흡수합니다. 2. **독보적인 스페셜리스트:** 한 분야에서 업계 상위 10%에 드는 사람입니다. 비주얼 디자인, 모션, 타이포그래피, 일러스트레이션, 혹은 엔지니어링에 가까운 고도의 기술 디자인. 누구나 적당히 좋은 결과물을 만들 수 있는 시대에, 결과물을 *특별하게* 만드는 차별점은 이 사람에게서 나옵니다. 3. **유연한 적응력을 가진 신입:** 커리어 초기, 겸손하고, 기술적 호기심이 있으며, 이미 코드에서 만들어본 사람입니다. 제니는 직관에 반하는 주장을 합니다. "굳은 습관이나 기존 프로세스에 대한 애착이 없는 사람이, 프로세스가 빠르게 바뀌는 시기에는 오히려 유리합니다." Figma 중심 워크플로우의 근육 기억이 없기 때문에, 버려야 할 것이 없습니다. 공통점은 적응력입니다. 프로세스는 어떤 단일 도구나 방법에 붙박여서는 따라갈 수 없을 만큼 빠르게 바뀌고 있고, 그 애착 자체가 지금은 부채입니다. 화려한 케이스 스터디로 가득한 포트폴리오보다, 판단력과 속도, 코드에서 일해본 증거가 더 강력한 준비물입니다. ## 결과물 측정과 템포 유지 디자이너가 PR로 배포한다면, PR 수치를 측정해야 합니다. 버즈빌은 디자인 팀 PR을 핵심 결과(KR)로 추적합니다. 2026년 2분기에 디자이너 3명 이상이 총 30개 이상의 PR을 내는 것이 목표입니다. 1분기 베이스라인은 기여자 2명에 PR 16개였습니다. 새로운 지표입니다. PR 개수를 OKR로 쓰는 디자인 팀을 저희는 어디서도 찾지 못했습니다. 이 지표는 하나의 질문에 곧바로 답합니다. *디자이너들이 정말로 배포하고 있는가, 아니면 여전히 핸드오프만 하고 있는가?* 핵심은 숫자가 아니라 추세입니다. 주마다 PR 개수는 늘어야 하고, 기여자는 다양해져야 합니다. 또 하나의 문제는 템포입니다. AI는 개인을 빠르게 만들어줍니다. 공통 리듬이 없으면 그 속도는 혼란을 낳습니다. 다섯 명이 하루에 스무 건씩 PR을 배포하는데 조율이 없다면, 그건 생산성이 아니라 소음입니다. 스탠드업은 작업이 며칠 걸린다는 전제 위에 서 있습니다. 스프린트 플래닝은 2주 단위를 전제합니다. 디자인 리뷰는 일주일에 방향 하나를 전제합니다. 개인이 하루에 기능 여러 개를 배포할 수 있게 되면, 이런 리듬은 조율에는 너무 느리고 도움이 되기에는 너무 잦습니다. 저희는 AI가 실제로 생산하는 것을 중심으로 케이던스를 다시 짰습니다. 커밋, PR, 측정된 산출물이 기준입니다. - **매일, 비동기, 산출물 기반.** 스탠드업은 없습니다. 작업은 커밋과 열린 PR로 드러납니다. 산출물이 곧 상태 업데이트입니다. - **매주, 자동 생성되는 리뷰.** 시스템이 모든 활성 프로젝트의 git 로그를 스캔해서 진척 리포트를 만듭니다. KR 완료율이 움직이거나, 움직이지 않거나 합니다. 이 방식은 "이번 주에 뭐 했어요?" 미팅을 증거 기반으로 바꿉니다. 의존성도 드러납니다. 인터랙션 라이브러리의 한 커밋이 디자인 라이브러리 문서에 영향을 주고, 그게 다시 SDK 문서에 영향을 주는 식입니다. - **매달, 이해관계자 정렬.** 디자인 팀의 결과물을 소비하는 사람들과 1:1을 가집니다. 상태 보고가 아니라 정렬 점검입니다. - **분기마다, 실제 숫자로 OKR 리셋.** 모든 KR은 자가 보고가 아니라 산출물에서 측정한 완료율을 가집니다. "Storybook 커버리지"가 92%인 것은 스토리 파일을 세어봤기 때문입니다. "디자인 팀 PR"이 16인 것은 GitHub에 쿼리해봤기 때문입니다. 이 케이던스는 두 가지 목적을 수행합니다. 개인에게는 체크포인트가 됩니다. 매일의 AI 보조 작업 속도에서 한 발 물러나, 방향이 여전히 맞는지 물어보는 순간입니다. 팀에게는 응집력을 제공합니다. 여섯 명이 독립적으로 배포해도, 여전히 하나의 프로덕트를 만들고 있다는 확신입니다. AI는 개인의 결과물을 가속시킵니다. 템포는 그것을 하나로 엮습니다. ## 이 흐름이 향하는 곳 크리스토퍼 알렉산더(Christopher Alexander)는 이 패턴이 소프트웨어에 도달하기 훨씬 전에 그 본질을 설명했습니다. 《영원의 건축(The Timeless Way of Building)》(1979)에서 그는, 지속되는 품질이 공유된 패턴 언어에서 온다고 주장했습니다. 누구나 사용해 일관되고 아름다운 결과를 만들 수 있는, 살아있는 규칙의 시스템이라는 것입니다. [^12] 패턴이 올바르면 건물은 스스로 돌봐집니다. 언어가 공유되면 시스템은 영혼을 잃지 않고 확장됩니다. 알렉산더의 통찰은 건축에서 소프트웨어로 건너와 디자인 패턴, Wiki, 그리고 결국 디자인 시스템에 영감을 주었습니다. 지금 또 한 번 건너오고 있습니다. AI 에이전트는 새로운 종류의 빌더입니다. 이전의 어떤 빌더보다 빠르고 지칠 줄 모르지만, 결국 주어진 패턴만큼만 뛰어납니다. 그 패턴을 설계하는 것이 바로 우리의 역할입니다. 환경을 설계하고, 토큰을 배포하며, 직접 PR을 여는 팀이 앞으로 프로덕트가 만들어지는 방식을 정의할 것입니다. 다음 Figma 파일이 나오기만을 기다리는 팀은, 한 달이라는 시간이 어디로 사라졌는지 궁금해하게 될 것입니다. 자료가 더 풍부하고 환경에 대한 설명이 더 깊은 [플레이북 전문](https://mmaxence.me/deepdives/ai-native-design-workflow-playbook/)을 따로 공개했습니다. 이 글은 전환을 고민하는 팀을 위한 요약 버전입니다. [^1]: Jenny Wen, "The design process is dead. Here's what's replacing it." Lenny's Newsletter/Podcast, March 2026. [lennysnewsletter.com](https://www.lennysnewsletter.com/p/the-design-process-is-dead) [^2]: Chris Roth, "Building An Elite AI Engineering Culture In 2026." [cjroth.com](https://cjroth.com/blog/2026-02-18-building-an-elite-engineering-culture) [^3]: Greptile State of AI Coding Report / Index.dev, cited in "When product managers ship code: AI just broke the software org chart." [dataworldbank.net](https://www.dataworldbank.net/2026/03/29/when-product-managers-ship-code-ai-just-broke-the-software-org-chart/) [^4]: "Building AI-driven workflows powered by Claude Code and other tools." UX Collective. [uxdesign.cc](https://uxdesign.cc/designing-with-claude-code-and-codex-cli-building-ai-driven-workflows-powered-by-code-connect-ui-f10c136ec11f) [^6]: "Towards an agentic design system." Cristian Morales Achiardi, Design Systems Collective. [designsystemscollective.com](https://www.designsystemscollective.com/towards-an-agentic-design-system-c7e0a6469bb1) [^9]: "AGENTS.md Optimization: 5x Performance Boost for AI Coding Agents." SmartScope. [smartscope.blog](https://smartscope.blog/en/generative-ai/claude/agents-md-token-optimization-guide-2026/) [^10]: "Your Design System Is Not Ready for AI Agents." Into Design Systems. [intodesignsystems.com](https://www.intodesignsystems.com/blog/design-system-not-ready-for-ai-agents) [^12]: Christopher Alexander, *The Timeless Way of Building* (Oxford University Press, 1979). [^13]: "Claude Code power user tips: Verification." Anthropic Help Center. [support.claude.com](https://support.claude.com/en/articles/14554000-claude-code-power-user-tips#h_ae6efc03ec) [^14]: Marty Cagan, "Build to Learn vs Build to Earn." Silicon Valley Product Group, April 2026. [svpg.com](https://www.svpg.com/build-to-learn-vs-build-to-earn/) --- ## [Nobody Owned the Website. Now Everybody Does.](https://tech.buzzvil.com/blog/nobody-owned-the-website-now-everybody-does) Date: 2026-04-15 | Author: Maxence Mauduit | Category: Design *이 글은 [영문 원본](https://mmaxence.me/blog/nobody-owned-the-website-now-everybody-does/)을 바탕으로 작성되었습니다.* 회사 웹사이트, 누가 담당하고 계신가요? 단순한 질문 같지만, 대부분의 회사에서 솔직한 답은 "마케팅팀이 일단은요" 아니면 "글쎄요, 딱히 아무도…" 사이 어딘가일 겁니다. 브랜드를 잘 아는 사람은 웹사이트를 만들지 않고, 만들 수 있는 사람은 제품 개발로 바쁩니다. 웹사이트는 그 사이에 어정쩡하게 놓여서, 마지못해 업데이트되고, 시간이 남는 사람이 간신히 관리합니다. 대기업이라면 이 이야기가 와닿지 않을 수도 있습니다. 브랜드팀이 있고, 웹팀이 있고, CMS 전담 조직이 있을 테니까요. 하지만 50명에서 200명 규모의 회사에서, 모든 직무가 빠듯하고 누구의 JD에도 "웹사이트 담당"이라고 적혀 있지 않다면, 제가 무슨 말을 하는지 정확히 아실 겁니다. 버즈빌도 수년간 이 상태로 지냈습니다. 약 100명 규모의 애드테크 회사에 프로덕트 디자이너는 있지만 브랜드 디자이너는 없고, 마케팅팀은 두 명인데 디자인이나 개발 리소스가 전혀 없습니다. 웹사이트는 늘 다른 누군가의 일이었고, 결과물이 그걸 그대로 보여줬습니다. ## 우리를 규정했던 제약 "리소스가 없다"는 말이 실제로 어떤 의미였는지 구체적으로 이야기해 보겠습니다. 회사 웹사이트는 공식적으로는 MC(마케팅 커뮤니케이션)팀 소유였습니다. 하지만 MC팀에는 자체 디자인이나 개발 리소스가 없었습니다. 업데이트를 할 때마다 개발팀의 시간을 빌려야 했고, 이는 곧 제품 우선순위와 경쟁한다는 뜻이었습니다. 답은 거의 항상 "지금은 안 돼요"였습니다. 결국 회사는 이 일을 아예 외주로 돌리기로 했습니다. 외부 프리랜서가 디자인과 프론트엔드를 모두 맡게 되었고, 결과는 예상 가능했습니다. 기획부터 배포까지 몇 달이 걸리는 릴리스 일정, 그리고 어딘가 우리답지 않은 결과물. 한편 기술 블로그는 Hugo로 따로 운영되고, 따로 호스팅되며, 메인 사이트의 비주얼 랭귀지와 전혀 연결이 없었습니다. SDK 문서는 Docusaurus에 올라가 있었는데, 역시 별도 운영, 역시 다른 스타일. 그리고 디자인 포털, 디자인 시스템 자체를 문서화하는 곳마저 또 다른 독립 앱이었습니다. 네 개의 웹 프로퍼티. 네 개의 코드베이스. 네 개의 비주얼 랭귀지. 일관성은 제로. 프로덕트 디자이너들은 제품 화면에 전부 투입되어 있었습니다. MC팀은 카피는 쓸 수 있지만 디자인이나 페이지를 만들 수는 없었습니다. 개발팀은 여력이 없었습니다. 프리랜서는 실행할 수는 있어도 오너십을 가질 수는 없었습니다. 아무도 전체 그림을 보고 있지 않았습니다. 그래서 이 간극은 계속됐습니다. 감각이 없어서가 아니라, 손이 부족해서. ## 무엇이 바뀌었나 2025년 말에 두 가지 변화가 있었습니다. 첫째, 디자인 시스템이 성숙해졌습니다. 그 전 해에 시맨틱 CSS 커스텀 프로퍼티와 런타임 테마를 지원하는 토큰 기반 시스템을 만들었는데, 원래 인터랙션 패턴(광고 경험, 게이미피케이션, 캠페인 UI)을 위해 만든 것이었지만 토큰 레이어 자체는 범용적이었습니다. 어떤 화면이든 이 토큰을 가져다 쓸 수 있었습니다. 둘째, AI 코딩 도구가 디자이너 한 명이 개발팀 속도로 움직일 수 있는 수준에 도달했습니다. "AI가 목업을 만들어 준다" 수준이 아닙니다. 실제 프로덕션 코드 속도입니다. Next.js 앱, 반응형 레이아웃, SEO 설정, 이미지 최적화, CI/CD 파이프라인. 과거에는 전담 프론트엔드 팀이 필요했던 종류의 작업이요. 이 두 가지가 결합되면서 오랫동안 바라던 것이 가능해졌습니다. 브랜드를 이해하는 디자이너가 처음부터 끝까지 직접 만들 수 있게 된 것입니다. 개발 리소스 배정을 기다리지 않고. 비전을 템플릿에 맞춰 단순화하지 않고. 그리고 더 중요한 것은 Figma 없이도 가능하다는 것. 종이 스케치, 토큰 시스템, 그리고 Claude Code면 충분했습니다. 2026년 3월 초에 시작해서, 5주 뒤 세 개의 앱이 프로덕션에 올라갔습니다. ## 코드로 표현되는 브랜드 아키텍처 이야기에 앞서, 우리가 실제로 하려던 것이 무엇이었는지 먼저 설명하고 싶습니다. 이것은 마이그레이션 프로젝트가 아니었습니다. 브랜드 프로젝트였습니다. 버즈빌의 브랜드 아이덴티티는 늘 문서 안에 정의되어 있었습니다. PDF 가이드라인, Figma 파일, 여기저기 흩어진 참고자료. 존재는 했지만 *살아 있지는* 않았습니다. 실행되지 않았고, 렌더링되지 않았고, 스스로를 강제하지 않았습니다. 우리는 이걸 바꾸고 싶었습니다. 브랜드가 코드로 표현되기를, 누군가가 확인하는 참고 문서가 아니라 화면을 구성하는 실제 재료가 되기를 원했습니다. **컬러**는 시맨틱 CSS 커스텀 프로퍼티에 매핑된 값입니다. `--bzv-color-theme-primary`는 화면에 실제로 렌더링되는 값이지, 팬톤 견본의 근사치가 아닙니다. 다크 모드와 라이트 모드는 `:root` 값을 교체하는 것만으로 처리됩니다. 별도의 조건 분기 없이 모든 컴포넌트가 자동으로 적응합니다. ```css /* packages/tokens/src/tokens.css */ :root { /* Primary: Buzzvil red, lightened for dark backgrounds */ --bzv-color-theme-primary: oklch(67.7% 0.1804 23.2); --bzv-color-theme-primary-content: oklch(100% 0 0); /* Surfaces: Zinc dark palette */ --bzv-color-theme-base-100: oklch(21.0% 0.0059 285.9); --bzv-color-theme-base-200: oklch(27.4% 0.0055 286.0); --bzv-color-theme-base-300: oklch(37.0% 0.0119 285.8); --bzv-color-theme-background: oklch(14.1% 0.0044 285.8); /* Content: text hierarchy */ --bzv-color-theme-base-content: oklch(94.7% 0.0027 286.3); --bzv-color-theme-base-content-700: oklch(77.2% 0.0098 286.2); --bzv-color-theme-base-content-600: oklch(64.9% 0.0146 262.4); --bzv-color-theme-base-content-400: oklch(48.5% 0.0118 267.3); /* Stroke */ --bzv-color-theme-stroke-100: oklch(100% 0 0 / 0.08); } ``` **타이포그래피**는 웨이트, 사이즈, 행간의 관계가 정의된 스케일을 따릅니다. 단순히 "프리텐다드 쓰세요"가 아닙니다. 산문 스타일, 헤딩 위계, 코드 블록 처리, 읽기 최적화된 행 길이까지 포함하는 완전한 타입 시스템입니다. **스페이싱**은 체계적인 스케일을 사용합니다. 임의의 패딩이 아닙니다. 의도된 리듬입니다. **스트로크**는 다크 서페이스 위에 불투명도 기반의 미묘한 보더를 사용합니다. 하드코딩하면 쉽게 틀리는 부분인데, 토큰은 이걸 틀리는 것 자체를 불가능하게 만듭니다. 마케팅팀이 buzzvil.com에 새 페이지가 필요할 때, 브랜드는 이미 그곳에 있습니다. 확인해야 할 참고 문서가 아니라, 그 페이지를 구성하는 재료 자체로서. ## 아키텍처 ```bash buzzvil-web/ ├── apps/ │ ├── homepage/ # buzzvil.com │ ├── tech-blog/ # 195+ posts, migrated from Hugo │ ├── docs/ # 227 pages, migrated from Docusaurus │ └── design/ # Design portal ├── packages/ │ ├── tokens/ # colors, spacing, motion, fonts │ ├── components/ # Shared UI components │ ├── layouts/ # Page layouts and shells │ ├── content/ # Shared content utilities │ └── docs-ui/ # Documentation-specific components ``` 네 개의 앱. 다섯 개의 공유 패키지. 하나의 토큰 시스템. Turborepo가 빌드를 관리하고, 모든 앱에 Next.js 15 App Router가 적용되어 있습니다. Tailwind CSS 4는 `@theme` 디렉티브를 통해 디자인 토큰을 사용합니다. ### 토큰의 흐름 토큰 시스템은 세 개의 레이어로 구성됩니다. CSS 변수 파일이 원본 값을 정의하고, Tailwind 프리셋이 이 변수들을 시맨틱 클래스명에 매핑하고, 각 앱이 그 프리셋을 가져다 씁니다. 덕분에 `bg-primary`가 홈페이지에서든 SDK 문서에서든 동일한 의미를 갖습니다. ```typescript // packages/tokens/src/tailwind.ts const preset: Partial = { theme: { extend: { colors: { background: "var(--bzv-color-theme-background)", foreground: "var(--bzv-color-theme-base-content)", primary: { DEFAULT: "var(--bzv-color-theme-primary)", foreground: "var(--bzv-color-theme-primary-content)", }, muted: { DEFAULT: "var(--bzv-color-theme-base-200)", foreground: "var(--bzv-color-theme-base-content-400)", }, border: "var(--bzv-color-theme-stroke-100)", }, }, }, }; ``` 각 앱에서는 프리셋을 가져오기만 하면 됩니다: ```typescript // apps/tech-blog/tailwind.config.ts import preset from '@buzzvil/tokens/tailwind'; export default { presets: [preset] }; ``` 하나의 토큰 파일. 하나의 프리셋. 모든 앱이 이걸 소비합니다. 브랜드 레드가 바뀌면 한 줄만 고치고 리빌드하면 됩니다. 모든 화면이 따라옵니다. ### 앱 간 코드 공유 Turborepo가 의존성 그래프를 관리합니다. 각 앱이 공유 패키지를 선언하면, Turbo가 빌드 순서를 알아서 정합니다. ```typescript // apps/tech-blog/next.config.ts const nextConfig: NextConfig = { transpilePackages: [ '@buzzvil/tokens', '@buzzvil/layouts', '@buzzvil/content' ], }; ``` `packages/layouts`에서 한 번 만든 레이아웃 컴포넌트가 기술 블로그, SDK 문서, 홈페이지에 동일하게 나타납니다. 헤더, 푸터, 네비게이션 셸, 모두 공유됩니다. 복사-붙여넣기가 아니라 실제로 공유됩니다. ## 이동한 것들의 규모 이 프로젝트가 얼마나 많은 것을 흡수했는지 감을 드리자면, Hugo에서 195개의 포스트를 가진 기술 블로그를, Docusaurus에서 Android와 iOS 세 가지 SDK 버전을 다루는 227페이지의 SDK 문서를 마이그레이션했습니다. 둘 다 같은 모노레포 안으로, 같은 토큰 시스템 아래로. 블로그 마이그레이션만 해도 콘텐츠 포맷 변환, 수년간 불일치했던 슬러그 정규화, 수백 개의 깨진 이미지 참조 수정, 페이지네이션과 카테고리 필터링 구축, OG 이미지 시스템 생성, 그리고 기존 링크가 깨지지 않도록 301 리다이렉트 설정이 필요했습니다. ```typescript // 하위 호환을 위한 리다이렉트 규칙 예시 { source: '/blog/2024/:slug*', destination: '/blog/:slug*', permanent: true }, { source: '/blog/2023/:slug*', destination: '/blog/:slug*', permanent: true }, { source: '/tags/:path*', destination: '/blog', permanent: true }, { source: '/index.xml', destination: '/feed.xml', permanent: true }, ``` SDK 문서는 완전한 콘텐츠 동등성, SEO 보존(sitemap, robots, opensearch), 콘텐츠 헤딩에서 추출한 페이지별 메타 태그, 접근성 작업, 그리고 공식적인 마이그레이션 go/no-go 평가가 필요했습니다. 그다음은 이미지였습니다. 원본 미디어 폴더 용량이 1.0 GB였는데, 1,599개 이미지를 325 MB까지 최적화했습니다. 71% 감소, 눈에 보이는 품질 저하 없이. 프로덕트 디자이너가 이미지 최적화와 리다이렉트 매핑에 2주를 쓰는 건 정당화하기 어려웠을 겁니다. AI 도구를 가진 디자이너는 그걸 몇 시간 만에 해냈습니다. 이것이 이전이라면 이 프로젝트를 좌초시켰을 부분입니다. 디자인 사고도, 브랜드 전략도 아닌, 아무도 맡고 싶어하지 않았던 방대한 에디토리얼 인프라 작업이요. ## AI 네이티브로 만들기 아마 제가 가장 흥미롭게 느끼는 부분이고, 밖에서 보면 가장 눈에 띄지 않는 부분일 겁니다. AI로 모노레포를 만드는 것은 한 가지 일이었습니다. 그걸 AI를 통해 회사의 나머지 구성원들도 *활용할 수 있게* 만드는 것이 진짜 목표였습니다. 아이디어는 간단합니다. 회사의 누구든 전체 아키텍처를 이해하지 않고도 이 화면들에 기여할 수 있어야 합니다. 블로그 글을 업데이트하고 싶은 마케터. SDK 문서의 오타를 고치고 싶은 PM. 리다이렉트를 추가하고 싶은 개발자. 레포를 클론하고, Claude Code를 열고, 원하는 것을 설명하면 프로세스를 안내받을 수 있습니다. 이것을 가능하게 하기 위해 세 가지를 했습니다. ### 컨텍스트 파일 모든 앱에 `CLAUDE.md` 파일이 있어서, AI 에이전트에게 해당 앱이 무엇을 하는지, 어떤 규칙을 따라야 하는지, 무엇을 건드리면 안 되는지를 알려줍니다. 일반적인 README가 아닙니다. LLM이 파싱하고 추론할 수 있도록 구조화되어 있습니다. 예를 들어 기술 블로그의 `CLAUDE.md`는 콘텐츠 디렉토리 구조, 프론트매터 작성법, 이미지 위치, 블로그 포스트에 사용 가능한 컴포넌트, 슬러그 규칙을 설명합니다. 에이전트가 이 파일을 읽으면 사람의 안내 없이도 새 포스트를 올바르게 작성할 수 있습니다. 토큰 패키지에도 변수 명명법, Tailwind 프리셋의 작동 방식, 값을 변경하면 어떤 일이 일어나는지를 설명하는 자체 문서가 있습니다. 에이전트가 이걸 읽으면 색상을 조정할 때 시스템을 깨뜨리지 않을 만큼 충분히 이해하게 됩니다. ### 스킬 스킬은 Claude Code가 특정 워크플로우를 처리하기 위해 호출하는 구조화된 명령 세트입니다. 모노레포를 위해 여러 개를 만들었습니다: - 새 블로그 포스트를 만드는 스킬: 마크다운 파일 스캐폴딩, 이미지 디렉토리 설정, 개발 서버 시작까지 처리합니다. - 이미지를 최적화하는 스킬: 이미지가 사용될 위치에 따라 적절한 크기, 포맷, 압축을 자동으로 결정합니다. - 배포 상태를 확인하는 스킬: 어떤 앱에 푸시한 후 빌드 성공 여부를 확인합니다. 스킬은 복잡한 다단계 워크플로우를 한 줄 호출로 바꿉니다. 기여하는 사람은 단계를 알 필요가 없습니다. 스킬이 단계를 알고 있으니까요. ### 훅 훅은 특정 액션 전에 실행되는 자동화된 검사입니다. 가드레일 역할을 합니다. 블로그 포스트가 커밋되기 전에 마크다운 프론트매터를 검증하는 훅이 있고, 하드코딩된 색상 값이 PR에 들어오지 못하도록 하는 훅이 있습니다. 콘텐츠 전용 PR(마크다운, 이미지)은 CI 파이프라인이 자동 승인합니다. 코드 PR은 영향받는 모든 앱에서 타입 체크를 받습니다. CODEOWNERS가 적절한 사람이 적절한 파일을 리뷰하도록 보장합니다. 결과적으로 브랜드는 토큰 시스템과 훅이 보장하기 때문에 일관성을 유지합니다. 하지만 콘텐츠, 카피, 작은 수정들은 PR을 보낼 의지만 있으면 누구에게나 열려 있습니다. 이것은 의도적인 선택이었습니다. 디자이너만의 프로젝트로 남길 수도 있었습니다. 대신 우리는 이것을 플랫폼으로 만들었습니다. ## 현재 상태 2026년 3월 초에 시작했습니다. 현재 상황은 이렇습니다: | 앱 | 상태 | |-----|--------| | [Design Portal](https://design.buzzvil.com) | 운영 중 (최초 런칭) | | [Tech Blog](https://buzzvil-tech-blog.vercel.app/) | 운영 중, 195+ 포스트 마이그레이션 완료 | | [SDK Docs](https://buzzvil-docs.vercel.app/) | 최종 QA 중, 227 페이지 마이그레이션 완료 | | Homepage | 진행 중, 시스템 전환 완료, 콘텐츠 마이그레이션 중 | ![기술 블로그](https://tech.buzzvil.com/blog/nobody-owned-the-website-now-everybody-does/tech-blog.png) *기술 블로그. Hugo에서 마이그레이션한 195+개의 엔지니어링 포스트가 다른 모든 화면과 같은 토큰 시스템을 공유합니다.* ![SDK 문서](https://tech.buzzvil.com/blog/nobody-owned-the-website-now-everybody-does/docs.png) *SDK 문서. 세 가지 SDK 버전을 다루는 227페이지를 Docusaurus에서 마이그레이션. 같은 타이포그래피, 같은 스페이싱, 같은 브랜드.* ![홈페이지](https://tech.buzzvil.com/blog/nobody-owned-the-website-now-everybody-does/homepage.png) *홈페이지 (진행 중). 여러 팀에 걸친 콘텐츠 의존성이 있는 가장 복잡한 화면.* ![컬처 블로그](https://tech.buzzvil.com/blog/nobody-owned-the-website-now-everybody-does/culture-blog.png) *컬처 블로그. 홈페이지 앱 안에서 레이아웃과 컴포넌트를 공유하는 콘텐츠 페이지.* 디자인 포털이 팀과 가장 가까운 화면이라 첫 번째로 런칭했습니다. 기술 블로그가 가장 큰 콘텐츠 마이그레이션과 함께 뒤를 이었습니다. SDK 문서는 콘텐츠 동등성 검증과 SEO 확인을 마쳤고, 네 명의 리뷰어가 최종 비주얼 QA를 진행하고 있습니다. 홈페이지가 가장 복잡한 화면으로, 여러 팀 간의 콘텐츠 의존성을 새로운 협업 워크플로우를 통해 해결하고 있습니다. 두 개의 앱이 프로덕션에, 하나가 최종 QA 중, 하나가 진행 중. 프리랜서에게 맡겼을 때 페이지 하나 업데이트하는 데 걸리던 몇 달과 비교해 보세요. ## 배운 것들 **토큰 시스템이 곧 제품이다.** 비주얼 품질에 대한 모든 대화는 결국 "이거 토큰을 쓰고 있는 거야, 하드코딩된 값이야?"로 귀결됩니다. 시스템이 스스로를 단속합니다. 주변이 모두 체계적이기 때문에 하드코딩된 값은 어색해 보입니다. **속도가 아니라 범위의 문제다.** AI가 빠르기 때문에 이 앱들을 만든 게 아닙니다. AI가 이전에는 팀 단위로만 가능했던 수준의 범위로 작업할 수 있게 해줬기 때문에 만들었습니다. 한 사람이 브랜드, 디자인, 개발, 콘텐츠의 전체 맥락을 쥐고 있으면, 전문가들 사이를 릴레이하는 것과는 다른 결과물이 나옵니다. **콘텐츠 마이그레이션은 디자인 작업이다.** 블로그 마이그레이션이나 SDK 문서를 "그냥 콘텐츠 옮기기"로 취급했다면 평범한 결과물이 나왔을 겁니다. 타이포그래피, 읽기 경험, 이미지 품질, 이것들은 모두 디자인 결정입니다. 전체 파이프라인을 소유한다는 것은 이 결정들이 우연이 아니라 의도적이라는 뜻입니다. **AI 네이티브 레이어가 지속 가능성을 만든다.** AI 덕분에 혼자 만드는 것은 가능했습니다. 하지만 혼자 유지하는 것은 불가능합니다. 컨텍스트 파일, 스킬, 훅이 한 사람의 프로젝트를 조직 전체의 플랫폼으로 바꿔줍니다. 이것들이 없으면 모노레포는 한 사람만 건드릴 수 있는 또 다른 것이 됩니다. 이것들이 있으면 모두가 기여할 수 있는 것이 됩니다. ## 브랜드에 대한 질문 프로젝트 초기에 선택의 기로에 선 순간이 있었습니다. 먼저 브랜드를 다시 생각하고, 비주얼 톤을 업데이트하고, 철학을 다듬을 것인가? 아니면 앱 전반에 걸쳐 브랜드를 수용하는 시스템을 먼저 만들고, 나중에 그 시스템 안에서 브랜드를 발전시킬 것인가? 저는 브랜드 디자이너가 아닙니다. 제대로 된 브랜드 리프레시에 얼마나 걸릴지, 첫 번째 시도에 잘 해낼 수 있을지 확신이 없었습니다. 하지만 한 가지는 확신했습니다. 시스템이 제대로 만들어지면, 나중에 브랜드를 업데이트하는 것은 간단해진다는 것. 토큰 하나 바꾸고, 리빌드하면 끝. 모든 화면이 따라옵니다. 그래서 이 가정을 일찍 테스트했습니다. 기존 브랜드 가치를 가져와서 하나의 토큰 세트로 분해하고, 공유 프리셋을 통해 모노레포 전체에 연결했습니다. 그런 다음 몇 가지 값을 바꾸고 모든 앱이 한꺼번에 업데이트되는 것을 확인했습니다. 됐습니다. 그것만으로 진행할 충분한 근거가 됐습니다. 지금의 브랜드는 우리 자신입니다. 아마 완벽하지는 않을 겁니다. 다듬어지고, 발전하고, 날카로워질 수 있습니다. 하지만 처음으로 *살아 있습니다*. 릴리스 전에 누군가가 확인하는 PDF가 아닙니다. 실제 배포물과 점점 달라지는 Figma 파일이 아닙니다. 화면을 구성하는 재료 자체입니다. 그것이 바뀌면, 모든 것이 함께 바뀝니다. 그리고 같은 토큰 시스템은 이미 이 웹 앱들 이상의 것을 구동하고 있습니다. 한국 전역의 퍼블리셔 앱 안에서 구동되는 [인터랙션 라이브러리](/blog/the-flip), 게이미피케이션과 캠페인 경험에도 적용되어 있습니다. ## 앞으로 시스템은 구축되었고, 브랜드는 연결되었고, 기여 모델은 열려 있습니다. 남은 것은 범위를 확장하는 것입니다. 다음은 슬라이드와 문서입니다. 지금 버즈빌에서 누군가 프레젠테이션을 만들거나 제안서를 쓸 때, 살아 있는 브랜드와 전혀 연결이 없는 오래된 템플릿에서 시작합니다. 색상이 작년 것일 수 있고, 폰트가 맞지 않을 수 있고, 톤이 흐트러지는데 이를 강제하는 시스템이 없습니다. 하지만 이제 있습니다. 웹사이트에 적용되는 동일한 토큰이 슬라이드 덱 생성기, 문서 템플릿, 이메일 빌더에도 적용될 수 있습니다. 원칙은 같습니다: 브랜드를 한 곳에서 정의하고, 어디서든 소비한다. 매체는 바뀌지만 소스 오브 트루스는 바뀌지 않습니다. "웹사이트를 누가 담당하나요?"라는 질문의 진짜 답이 여기 있습니다. 시스템이 일관성을 보장하기 때문에 아무도 소유할 필요가 없습니다. 그리고 도구가 안전하게 만들어주기 때문에 누구나 기여할 수 있습니다. --- *buzzvil-web: 274 commits, 4 apps, 5 shared packages. 한 명의 디자이너가 Claude Code로 구축하고, 조직 전체가 기여할 수 있는 플랫폼.* --- ## [The Flip](https://tech.buzzvil.com/blog/the-flip) Date: 2026-02-28 | Author: Maxence Mauduit | Category: Design *이 글은 [영문 원본](https://mmaxence.me/blog/the-flip/)을 AI로 번역한 버전입니다.* 요즘 업계에 이런 기대가 퍼져 있습니다. 언젠가 AI가 디자인 시스템을 만들어 줄 거라는 거죠. 프롬프트 하나 던지면 토큰, 컴포넌트, 인터랙션 패턴이 깔끔하게 나올 거라고요. 저는 정반대의 일이 벌어지고 있다고 생각합니다. 우리는 AI 에이전트가 사용할 디자인 시스템을 만들고 있습니다. 디자이너를 위한 것도, 개발자를 위한 것도 아닌, 에이전트를 위한 시스템입니다. 솔직히 말하면, 이 작업을 하면서 디자인 시스템이라는 것 자체를 다시 생각하게 됐습니다. ## 에이전트에게 뭘 건네줘야 할까? ![AI 에이전트 커버리지 — 컨텍스트, 문서, 스킬의 구조](https://tech.buzzvil.com/blog/the-flip/agent-coverage.png) 이 모든 건 하나의 질문에서 시작됐습니다. 디자인 시스템의 주요 소비자가 Figma 라이브러리를 훑어보는 사람도, 문서 사이트를 읽는 개발자도 아니라면? 특정 사용자를 위해, 특정 브랜드에 맞춰, 특정 순간에 실시간으로 UI를 구성해야 하는 자율 에이전트라면, 도대체 뭘 줘야 할까요? 컴포넌트만 던져 줄 수는 없습니다. 컴포넌트는 답이니까요. 에이전트에게 필요한 건 질문을 이해하는 능력입니다. 이 인터랙션은 왜 존재하는지, 언제 빠르게 느껴져야 하고 언제 신중하게 느껴져야 하는지, 어떤 브랜드를 _바로 그_ 브랜드답게 만드는 건 무엇인지. 그래서 우리는 라이브러리보다는 언어에 가까운 것을 만들고 있습니다. 인터페이스가 어떻게 동작하고, 어떻게 보이고, 어떤 느낌을 줘야 하는지에 대한 문법을 정의하고, 에이전트가 그걸 유창하게 구사할 수 있도록 패키징하는 거죠. 실제로 에이전트가 받는 건 컴포넌트 라이브러리와 스타일 가이드가 아닙니다. 마크다운 파일, 컨텍스트 문서, 스킬로 구성된 체계적인 세트를 받습니다. 하나하나가 기계가 파싱하고 실행할 수 있게 작성된 것들입니다. **Context 파일**은 에이전트가 움직이는 세계를 정의합니다. 브랜드 아이덴티티, 디자인 원칙, 톤 앤 매너, 레이아웃 철학, 모든 의사결정을 좌우하는 제약과 자유. 디자이너가 해석할 브리프가 아닙니다. 에이전트가 추론할 수 있을 만큼 구조적이면서도, 판단의 여지를 남길 만큼 열려 있는 형태입니다. **Skill**은 능력이 사는 곳입니다. 각 스킬은 명확한 범위의 지시 세트입니다. 레이아웃을 구성하는 방법, 리워드 모델을 적용하는 방법, 브랜드 프로필에 맞게 밀도를 조절하는 방법, Iconic 인터랙션을 선택하고 배치하는 방법. 스킬은 코드가 아닙니다. 안무에 더 가깝습니다. 에이전트가 단계별로 따르되, 각 단계 안에서 맥락에 맞는 결정을 내릴 수 있는 가이드입니다. **Documentation 파일**은 컴포넌트 라이브러리 자체를 매핑합니다. 비주얼 카탈로그가 아니라, 에이전트가 조회할 수 있는 주석이 달린 레퍼런스입니다. 모든 컴포넌트에 사용 가이드, 동작 관련 참고사항, 접근성 요구사항, 그리고 언제, 왜 사용해야 하는지에 대한 맥락적 힌트가 포함됩니다. 숙련된 디자이너가 패턴 라이브러리를 읽는 것과 같은 방식이지만, 에이전트는 매번 그걸 밀리초 단위로 해냅니다. 전체 시스템은 NPM 패키지로 전달됩니다. 에이전트가 패키지를 가져오고, 마크다운을 읽고, 컨텍스트를 로드한 뒤 바로 UI를 구성하기 시작합니다. Figma도, 문서 사이트도 없습니다. 에이전트가 즉시 행동으로 옮길 수 있는 구조화된 지식만 있을 뿐입니다. ## Chameleon 문제 브랜드는 까다롭습니다. 그래야 하고요. 명품 시계 브랜드와 핀테크 스타트업은 단순히 모습이 다른 게 아닙니다. 움직이는 방식이 다르고, 호흡하는 리듬이 다릅니다. 우리는 이걸 Chameleon 로직이라고 부릅니다. 에이전트가 경험의 시각적 아이덴티티 전체를 변형하면서도 그 브랜드만의 느낌을 잃지 않게 해주는 기반 레이어입니다. 단순히 색상을 바꾸는 게 아닙니다. 밀도, 여백, 타이포그래피 위계, 모션의 완급, 표면 아래에 자리 잡고 있어서 왜 그런지 의식하기도 전에 무언가를 _느끼게_ 만드는 것들을 조절합니다. 흥미로운 부분은 에이전트에게 규칙과 감각의 차이를 가르치는 것입니다. 규칙은 쉽습니다. 이것이 메인 색상, 이것이 폰트. 감각은 어렵습니다. 이 브랜드는 여유롭게 느껴져야 한다, 이 브랜드는 긴박하게 느껴져야 한다. 우리는 둘 다 인코딩하고 있습니다. 접근성도 마지막에 끼워 넣는 체크리스트가 아닙니다. 하나의 스킬로 가르치고 있습니다. 에이전트는 명암비가 왜 중요한지, 터치 영역에 왜 여유가 필요한지, 시맨틱 구조가 왜 선택이 아닌지 이해합니다. 생성된 모든 결과물은 사용자에게 도달하기 전에 기준을 통과해야 합니다. 관문이 아니라, 반사 신경으로서. ## Iconic 인터랙션 ![디자인 라이브러리의 게이미피케이션 패턴 — 빙고 그리드, 리워드 셀, 인터랙션 스타일](https://tech.buzzvil.com/blog/the-flip/gamification.png) 대부분의 디자인 시스템은 빌딩 블록을 줍니다. 버튼, 카드, 모달. 우리도 마찬가지입니다. 마이크로 인터랙션, 주요 인터랙션 패턴, 기대할 수 있는 모든 UI 디테일 등 기반은 갖춰져 있습니다. 하지만 우리는 Iconic 인터랙션이라고 부르는 것도 만들고 있습니다. 이건 완전히 다른 종류의 컴포넌트입니다. 참여도가 높고, 게이미피케이션 요소가 있으며, 때로는 예상치 못한 경험을 줍니다. 스크롤하며 지나치는 게 아니라 몸을 앞으로 기울이게 만드는 그런 컴포넌트입니다. 어떤 상황에서 어떤 패턴이 효과적인지 테스트하고 기록해 왔습니다. 에이전트는 이것들에 단순히 접근할 수 있는 게 아니라, 언제 써야 하는지를 학습하고 있습니다. 스니커즈 드롭에서 환상적으로 작동하는 게이미피케이션 연출이 금융 상품에서는 완전히 어긋날 수 있으니까요. 맥락이 전부이고, 우리는 에이전트가 그 판단을 내릴 수 있도록 충분한 맥락을 제공합니다. ## 뿌려두는 빵 부스러기, 일방적 광고가 아닌 인게이지먼트 관점에서 이야기가 본격적으로 흥미로워지는 지점입니다. 리워드는 버즈빌을 유명하게 만든 도구이고, 이제는 에이전트가 인게이지먼트를 설계하고 최적화하는 데 쓰는 핵심 수단이 되고 있습니다. 리워드를 길 위에 놓인 빵 부스러기라고 생각해 보세요. 마찰이 생기는 지점에 작은 보상, 발견, 즐거운 순간을 제공하면 사용자가 자연스럽게 앞으로 나아갑니다. 관심이 고려로, 고려가 행동으로 이어집니다. 에이전트는 이 빵 부스러기를 의도적으로 배치하며, Iconic 인터랙션과 리워드 트리거를 활용해 인위적이지 않고 자연스럽게 느껴지는 여정을 구축합니다. 우리는 브랜드와 함께 그들의 이야기를 전하고, 관심을 끌어모으고, 전환을 이끌어 냅니다. 리워드 모델은 이 과정이 퍼널이 아닌 대화처럼 느껴지도록 만드는 방식입니다. ## 밤잠을 설치게 만드는 부분 (좋은 의미로) 우리는 경험의 무한한 변주를 동적으로 생성할 수 있는 지점에 다가가고 있습니다. 각각이 특정 사용자 프로필, 특정 제품, 특정 브랜드 보이스에 맞춰져 있고, 모든 랜딩 하나하나에서 학습합니다. 두 가지 옵션으로 A/B 테스트를 하는 게 아닙니다. n명의 사용자에게 n개의 시나리오를 생성하고, 매번 조정하는 겁니다. 차원이 다른 이야기입니다. 이건 최적화가 아닙니다. UI가 당신이 어떤 화면에 있는지가 아니라, 당신이 누구인지에 진정으로 반응하게 되는 겁니다. ## 우리가 짓고 있는 다리 (그리고 그것이 사라질 수도 있는 이유) 지금 제가 가장 흥미롭게 느끼는 긴장감입니다. 우리는 익숙한 모습과 느낌의 UI를 만들고 있습니다. 레이아웃, 버튼, 플로우, 사람들이 알고 신뢰하는 패턴들. 하지만 그 이면에서는 에이전트가 사용자가 볼 수 없는 데이터를 바탕으로 이 요소들을 조합하고 있습니다. AI와의 간접적인 상호작용입니다. 사람들은 세련되고 사람 손으로 만든 것 같은 인터페이스를 경험합니다. 그게 자신의 프로필, 브랜드, 제품, 그리고 수백 가지 다른 신호를 고려한 에이전트에 의해 불과 몇 초 전에 조립됐다는 사실을 모르고, 알 필요도 없습니다. 지금은 전환의 순간입니다. 사람들이 에이전트와 직접 소통하는 데 익숙해질수록, 이런 중간 단계 UI의 필요성은 줄어들 겁니다. 어쩌면 상당히. 하지만 지금, 그리고 아마 한동안은 이 지점이 임팩트가 있는 곳입니다. 익숙한 표면, 그 아래의 새로운 엔진. 우리는 다리를 짓고 있습니다. 그리고 언젠가 사람들이 이 다리를 필요로 하지 않을 수도 있다는 걸 알면서 짓고 있습니다. 스스로를 넘어서도록 설계된 것을 의도를 가지고 만든다는 것. 거기엔 뭔가 의미 있는 게 있습니다. ## 현재 위치 솔직하게 말하면, 아직 만들어 가는 중입니다. 기반은 잡혀 있습니다. Chameleon 로직은 동작하고 있고, Iconic 라이브러리는 테스트와 문서화가 진행 중입니다. 올해 안에 모든 조각을 연결하고, 에이전트가 참조하고 학습하고 활용할 수 있는 온라인 플랫폼과 NPM 패키지를 통해 전달할 예정입니다. 완성되진 않았습니다. 하지만 윤곽은 충분히 잡혔기에 그 뒤에 있는 생각을 공유하고 싶었습니다. 이 방향이 중요하다고 믿으니까요. AI가 디자인 시스템을 만들어 줄 거라고 생각하셨나요? 우리는 AI를 위한 디자인 시스템을 만들고 있습니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://team.buzzvil.com/2f0b4a09-8b7e-4f46-9bce-e8bf0904ad95) --- ## [DynamoDB Limit 설정으로 RCU 97% 절감한 이야기](https://tech.buzzvil.com/blog/dynamo-query-limit-rcu-optimization) Date: 2026-01-23 | Author: Jed Jeon | Category: Backend 안녕하세요, 버즈빌 Supply Platform 팀의 Jed입니다. 대규모 트래픽 환경에서 낮은 지연 시간과 높은 가용성은 중요한 요소입니다. 따라서 버즈빌에서는 ["Single-digit millisecond performance at any scale"](https://aws.amazon.com/dynamodb/)의 DynamoDB를 적극 활용하고 있습니다. 이 글에서는 DynamoDB RCU(Read Capacity Unit) 급증 이슈의 원인을 추적하고 해결책을 찾아가는 과정을 공유하고자 합니다. 비슷한 상황을 겪고 계신 분들께 도움이 되길 바랍니다. > **TL;DR** > - **상황**: RCU ~1k → 130k RCU/s (130배 증가) > - **원인**: `Limit` 미설정 + 불필요한 Strong Consistent Read > - **해결**: `Limit` 적용 + Eventually Consistent Read 전환 > - **결과**: **RCU 87~97% 절감** ---
## 문제 발견과 원인 분석 ### 1. 리포팅과 메트릭 확인 **2025년 9월 초**, 데브옵스 팀의 리포팅을 계기로 본격적인 원인 분석을 시작했습니다. 먼저 Datadog으로 RCU 사용량 추이를 분석한 결과, 특정 시점 이후 RCU가 점진적으로 증가한 것을 확인했습니다. ``` # RCU 추이 (A 테이블 + C 테이블 합산) 2025-05-01: ~1k RCU/s 2025-05-23: ~6k RCU/s (점진적 증가 시작, A 테이블 ~5k + C 테이블 ~1k) 2025-09-04: ~35k RCU/s (리포팅 시점, A 테이블 ~15k + C 테이블 ~20k) 2025-10-30: ~130k RCU/s (대형 매체사 오픈 후 최고점, A 테이블 ~60k + C 테이블 ~70k) 2025-11-07: ~10k RCU/s (최적화 배포 후, A 테이블 ~8k + C 테이블 ~2k) ```
### 2. 원인 추적 #### 2.1 일단위 RCU 메트릭 | 날짜 | 평균 | 최대 | 오전 | 오후 | 비고 | |-------|--------|--------|--------|--------|-----------| | 01-15 | 241 | 573 | 300 | 270 | 안정 상태 | | ... | ... | ... | ... | ... | ... | | 05-23 | 1,790 | 4,330 | 1,000 | 3,000 | **사용량 증가** | | ... | ... | ... | ... | ... | ... | | 10-27 | 11,800 | 23,800 | 16,000 | 10,000 | 최고점 | 이 표를 다음과 같이 활용할 수 있었습니다. - **증가 시점 특정**: 5월 23일 오후 2시 10분경 2.5k RCU 돌파 - **시간대별 패턴**: 오후~저녁 시간대에 RCU가 더 높음 - **팀 공유**: 명확한 근거 자료로 활용
#### 2.2 배포 이력 자동화 추적 RCU 증가 시점과 배포 이력의 상관관계를 파악하기 위해 AI를 활용한 PR 분석을 진행했습니다. 다만 AI의 비결정적인 특성을 보완하기 위해 GitHub CLI 스크립트를 작성하여 명확한 산출물을 확보했습니다. ```bash # 특정 날짜의 배포에 포함된 PR 목록을 자동 추출 function find_deployments() { # ... 배포 조회 } function find_merged_prs() { # PR 조회 gh pr list --state merged --search "$sha" --base master \ --json number,title | jq -r '.[] | @tsv' } ``` 이 스크립트로 수동 확인 시 며칠 걸릴 작업을 자동화하여 원인 후보군을 빠르게 도출했습니다. > **여담**: 같은 달 [네이선](https://github.com/hab56ur9)께서 SDD(Spec Driven Development)를 소개해 주셨는데, 스펙 문서로 컨텍스트를 명확히 관리하여 AI의 비결정적 특성을 보완할 수 있다는 점이 인상적이었고, 이후 AI 활용 시 스펙 기반으로 컨텍스트를 관리하고 있습니다.
#### 2.3 다중 서비스 배포 매트릭스 두 테이블을 직접 조회하는 5개 서비스의 배포 이력을 1월부터 9월까지 날짜별로 교차 분석했습니다. | 날짜 | A 서비스 | B 서비스 | C 서비스 | D 서비스 | E 서비스 | |-------|--------|--------|--------|--------|--------| | 01-17 | | #xxx | | | | | ... | ... | ... | ... | ... | ... | | 03-18 | | | #xxc | | | | ... | ... | ... | ... | ... | ... | | 05-23 | #xxa | #xxb | | | | ← 매체사 도입 | ... | ... | ... | ... | ... | ... | | 09-04 | | | | | |
### 3. 의심 PR 발견 매트릭스 분석을 통해 **3월 18일 머지된 C 서비스 PR**이 의심 후보로 떠올랐습니다. 해당 PR을 분석한 결과, 두 가지 문제를 발견했습니다. 1. **Limit 미설정**: 파티션 전체(평균 300개)를 순회 2. **Strong Consistent Read 추가**: Eventually Consistent 대비 2배의 RCU 소비 ```go // 문제의 코드 iter := r.table.Get("user_id", userID). Order(dynamo.Descending). Consistent(true). // 문제 2: Strong Consistent Read Iter() // 문제 1: Limit 없이 전체 순회 ``` **Q: 3월에 머지했는데 왜 9월에야 발견되었나?** **A:** 코드 변경 자체는 잠재적 문제였고, 트래픽이 점진적으로 증가하면서 9월에 이상 징후가 눈에 띄게 되었습니다. 이후 10월 말 대형 매체사 오픈으로 트래픽이 급증하면서 비용 문제가 본격화되었습니다. 나아가 이 두 가지가 실제로 RCU 폭증을 일으키는지 확인하려면, DynamoDB Query의 동작 원리를 이해할 필요가 있습니다. ---
## DynamoDB Query 동작 원리 ### 1. Query API 내부 동작 DynamoDB Query API는 다음과 같이 동작합니다. 1. **1MB 단위 페이징**: 한 번의 Query 호출로 최대 1MB의 데이터만 반환 (["a maximum of 1 MB"](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html)) 2. **Limit의 의미**: 스캔할 최대 아이템 수 (필터링 **전**에 적용) 3. **RCU 계산 기준**: 실제 반환된 데이터가 아닌, **스캔한 데이터 크기** 기준 | Read 타입 | 4KB당 RCU | |-----------------------|----------| | Eventually Consistent | 0.5 RCU | | Strong Consistent | 1 RCU | > DynamoDB는 아이템 크기를 4KB 단위로 올림하여 RCU를 계산합니다. 이번 케이스의 파티션당 평균 아이템 수는 약 300개, 아이템 평균 크기는 400 bytes입니다. 요청 한 번에 **최대 1MB**까지 스캔하기 때문에, 평균 120KB(400 bytes × 300개) 규모의 **파티션 전체를 스캔**할 수 있는 상태였습니다.
### 2. guregu/dynamo 라이브러리 버즈빌에서는 아래 장점들을 고려해 [guregu/dynamo](https://github.com/guregu/dynamo/tree/v1.20.2)를 적극 활용 중입니다. AWS SDK를 직접 사용하는 것 대비 주요 장점을 소개합니다.
2.1 Struct Binding **AWS SDK** ```go // 필드별 수동 타입 변환 필요 output, _ := client.GetItem(&dynamodb.GetItemInput{...}) if v, ok := output.Item["UserID"]; ok && v.N != nil { user.UserID, _ = strconv.Atoi(*v.N) // 각 필드마다 반복... } ``` **guregu/dynamo** ```go type User struct { UserID int `dynamo:"UserID,hash"` CreatedAt time.Time `dynamo:",unixtime"` } // 타입 안전성과 자동 직렬/역직렬화 err := table.Get("UserID", 123).One(&user) ```
2.2 Chainable API **AWS SDK** ```go // Expression 문자열 + AttributeValues 맵 조합 input := &dynamodb.QueryInput{ KeyConditionExpression: aws.String("UserID = :uid AND #time >= :t"), ExpressionAttributeNames: map[string]*string{...}, // 예약어 이스케이프 ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{...}, } result, err := client.Query(input) ``` **guregu/dynamo** ```go // 메서드 체이닝으로 직관적 표현 err := table.Get("UserID", 613). Range("Time", dynamo.GreaterOrEqual, "2025-01-01"). Order(dynamo.Descending).Limit(10).All(&results) ```

#### 2.3 Pagination 추상화 (aka Limit 누락의 시작점) **AWS SDK** ```go // LastEvaluatedKey 루프를 직접 구현 for { output, err := client.Query(&dynamodb.QueryInput{ExclusiveStartKey: lastKey, ...}) // ... if output.LastEvaluatedKey == nil { break } lastKey = output.LastEvaluatedKey } ``` **guregu/dynamo** ```go iter := table.Get("UserID", 613).Iter() for iter.Next(&result) { // 응답 처리 } ``` `Iter()`와 `All()` 모두 내부적으로 `LastEvaluatedKey`를 확인하며 페이지를 자동으로 순회합니다. **함정**: `Iter()`를 사용하면 for 루프에서 `break`로 일찍 빠져나올 수 있어, iteration 횟수만큼만 RCU가 소비되는 것처럼 오해할 수 있습니다. ```go // 3번만 iteration하면 3개만 조회하는 것 아닌가? iter := table.Get("UserID", 613).Iter() for iter.Next(&result) { results = append(results, result) if len(results) >= 3 { break // 여기서 끊으면 3개만 조회한 거 아닌가? } } ``` 실제로 라이브러리 내부를 보면, `Next()`는 **페이지 단위로 조회한 결과를 순회**합니다. ```go // guregu/dynamo v1.20.2 query.go - NextWithContext (간략화) func (itr *queryIter) NextWithContext(ctx context.Context, out interface{}) bool { // 1. 이미 조회한 결과가 있으면 버퍼에서 반환 (추가 Query 없음) if itr.output != nil && itr.idx < len(itr.output.Items) { itr.idx++ return true } // 2. 버퍼가 비었으면 DynamoDB Query 실행 (여기서 RCU 소비!) itr.output, _ = client.QueryWithContext(ctx, itr.input) // ... } ``` 즉, 첫 `Next()` 호출 시 DynamoDB Query가 실행되어 **페이지 전체(최대 1MB)를 조회**하고, 이후 `Next()`는 이미 가져온 결과를 순회만 합니다. `break`로 3개만 사용해도 페이지 전체에 대한 RCU는 이미 소비된 상태입니다.
### 3. DynamoDB Query API의 Limit, 제대로 전달하기 #### 3.1 라이브러리 내부 구현 guregu/dynamo는 DynamoDB Query API의 `Limit` 파라미터를 **두 개의 메서드**로 제공합니다. `Limit()`과 `SearchLimit()`. 둘 다 동일한 API 파라미터로 전달되지만, **전달 조건이 다릅니다**. ```go // guregu/dynamo v1.20.2 query.go - queryInput 메서드 func (q *Query) queryInput() *dynamodb.QueryInput { req := &dynamodb.QueryInput{...} // ... if q.limit > 0 { if len(q.filters) == 0 { // ⭐ 필터가 없을 때만 전달 req.Limit = &q.limit } } if q.searchLimit > 0 { req.Limit = &q.searchLimit // ⭐ 항상 전달 } // ... return req } ```
#### 3.2 Limit만 사용 시 페이징 동작 필터가 있으면 DynamoDB가 n개를 스캔해도 필터링 후 결과가 n개보다 적을 수 있습니다. 그래서 라이브러리는 **클라이언트에서 원하는 개수가 모일 때까지 페이징을 반복**합니다. ```go // guregu/dynamo v1.20.2 query.go:344-386 (간략화) func (itr *queryIter) NextWithContext(ctx context.Context, out interface{}) bool { if limit 도달 (n개 수집 완료) { return false } if 현재 페이지에 아이템 남음 { return true } // SearchLimit: 추가 페이지 요청 X if 더 이상 페이지 없음 || searchLimit > 0 { return false } // Limit만 설정: 다음 페이지 요청 후 계속 반복 itr.input.ExclusiveStartKey = itr.output.LastEvaluatedKey } ``` 핵심은 **`searchLimit > 0`이면 한 번의 API 호출로 중단**하고, 그렇지 않으면 `itr.n`이 `limit`에 도달할 때까지 페이지를 계속 요청한다는 점입니다.
#### 3.3 페이징 시나리오 비교 **`Limit(10).Filter(...)` 사용** ``` [1차] Limit 미전달 → 1MB까지 스캔 → 필터 후 3개 → "7개 더 필요" → 다음 페이지 [2차] Limit 미전달 → 1MB까지 스캔 → 필터 후 5개 → "2개 더 필요" → 다음 페이지 [3차] Limit 미전달 → 1MB까지 스캔 → 필터 후 4개 → "10개 도달!" → 중단 총 스캔: 최소 768개 → RCU 폭발 (아이템 4KB 이하 기준) ``` **`SearchLimit(10)` 사용** ``` [1차] Limit=10 전달 → 10개만 스캔 → 끝 총 스캔: 10개 → RCU 절감 ✅ ``` > **Limit vs SearchLimit** > > | 메서드 | Limit 전달 | 페이징 동작 | RCU 영향 | > |-------------|-----------------------|--------------|---------------| > | `Limit(n)` | 필터 없을 때 | n개까지 반복 | 필터 시 전체 스캔 위험 | > | `SearchLimit(n)` | **항상** | **n개 스캔 제한** | 서버 측 스캔 제한 | > > **권장**: RCU 최적화가 필요하다면 `SearchLimit`을 명시적으로 설정하세요. `Limit`만 사용하면 필터 유무에 따라 DynamoDB API에 전달되지 않을 수 있고, 이 경우 파티션 전체를 스캔하게 됩니다. ---
## 해결 전략 수립 ### 1. 비즈니스 요구사항 분석 최적의 Limit 값을 결정하기 위해서는 **비즈니스 컨텍스트**가 필수였습니다. 이 API를 매체사에 도입한 [윈](https://www.linkedin.com/in/seunghuh/)으로부터 유스케이스에 대한 상세 정보를 얻을 수 있었습니다. **최근 기록 조회 API의 실제 유스케이스** 1. 사용자가 특정 액션을 수행하고 기록이 저장됨 2. 액션 완료 후 "결과 확인" 화면으로 리다이렉트되어 최신 기록을 표시
### 2. 데이터 기반 의사결정: 협업을 통한 Limit 값 산정 Limit 값을 "적당히 50으로 설정하자"는 감에 의존하지 않고, **실제 데이터를 기반으로 결정**했습니다. [윈](https://www.linkedin.com/in/seunghuh/)께서 DA 팀에 데이터 분석을 요청해 주셔서, 다음과 같은 구체적인 질문에 대한 답을 얻을 수 있었습니다. - "유저가 세션당 몇 번의 요청을 보내는가?" - "P95, P99 기준으로 얼마나 여유를 두어야 하는가?" **분석 조건** (도메인 지식 기반으로 설정) - 세션 기준: 30분 (요청 간격이 30분 이상이면 새 세션) - 대상: 트래픽 비중이 높은 광고 타입 **분석 결과** | 지표 | 값 | 의미 | |------------------------------|---------|---------------------| | avg_requests_per_session | 5.33 | 세션당 평균 요청 수 | | median_requests_per_session | 3 | 세션당 요청 수 중앙값 | | p95_requests_per_session | 18 | 95% 세션이 18회 이하 | | **p99_requests_per_session** | **22** | **99% 세션이 22회 이하** | | avg_session_duration | 201.64초 | 평균 세션 지속 시간 (~3.4분) | | median_session_duration | 28.16초 | 세션 지속 시간 중앙값 | **Limit 값 산정 근거** ``` P99 = 22 요청/세션 ↓ 기본 Limit = 22 × 1.5 ≈ 33 (안전 마진) ↓ 최대 Limit = 33 × 2 = 66 (엣지 케이스 대응) ``` 따라서 **33~66개 아이템만 스캔하면 99% 이상의 요청을 충분히 커버**할 수 있습니다. > 매직 넘버는 코드에 주석으로 산정 근거를 명시했습니다. 시간이 지나면 왜 이 값인지 아무도 모르게 되기 때문입니다. **핵심 인사이트**: 실제 사용 패턴을 분석한 결과, 대부분의 조회가 최근 기록만 필요했습니다. 이를 근거로 조회 범위를 제한하면 파티션 전체(수백 개)를 스캔할 필요가 없어집니다.
### 3. 비효율 검증 [준](https://github.com/SeoJueun)께서 알려주신 AWS 콘솔의 "Query returned Item count" 지표를 통해 비효율을 검증할 수 있었습니다. ``` # AWS 콘솔 DynamoDB 테이블 메트릭 Query returned Item count (평균): 270개/요청 ``` P99 기준 22개면 충분한 유스케이스에서 쿼리당 평균 270개를 스캔한다는 것은 심각한 비효율을 의미했습니다. 이 지표가 문제의 규모를 정량화하는 핵심 근거가 되었습니다.
### 4. Consistency 요구사항 재정의 Strong Consistent Read가 정말 필요한지 검토했습니다. | 시나리오 | 소요 시간 | |-----------------|---------| | 액션 완료 → API 응답 | ~50ms | | 클라이언트 리다이렉트 | ~200ms | | 확인 화면 렌더링 | ~100ms | | **총 지연** | **~350ms** | 사용자가 결과 화면을 보기까지 약 350ms가 소요되므로, DynamoDB 복제 지연(수백 ms 이내)보다 깁니다. 따라서 Eventually Consistent Read로 충분합니다. 추가로, 클라이언트에서 최대 3회의 retry 로직이 존재하여 일시적인 데이터 미노출에도 안전합니다. ---
## 최적화 구현 ### 핵심 변경: SearchLimit 적용 ```go iter := r.table.Get("user_id", userID). SearchLimit(limit). // 핵심: 스캔 제한 ConsumedCapacity(&cc). // RCU 모니터링 Iter() defer func() { if cc.Total > 1.5 { // 평균 아이템 사이즈 기반, 엣지 케이스 모니터링 log.Info("DynamoDB RCU consumed", "total", cc.Total, "user_id", userID) } }() ``` **예상 RCU 절감:** - Before: ⌈300개 × 400B ÷ 4KB⌉ × 1 = 30 RCU - After: ⌈50개 × 400B ÷ 4KB⌉ × 0.5 ≈ 3 RCU (Limit 50 기준, 33~66 범위 중간값) - 예상 절감률: ~90% ---
## 개선 결과 ### 1. 사용량 개선
Datadog Metrics
두 테이블의 RCU와 API 요청 수의 관계
(노란색: API 요청 수, 비교를 위해 스케일 조정)
Returned Item Count
C 테이블의 쿼리 반환 항목 수
| 테이블 | 지표 | Before | After | 감소율 | |-------|-----|--------|-------|-------| | **A 테이블** | RCU | 60k/s | 8k/s | **87%** | | **C 테이블** | RCU | 70k/s | 2k/s | **97%** |
### 2. 서비스 품질
API Latency P95
API 응답 시간이 p95 기준 97% 감소
API Success Rate
최근 기록 API의 조회 성공률
쿼리 개선 결과 API 성공률은 유지하면서, 응답 시간은 크게 개선되었습니다. ---
## 마치며 이번 이슈는 메트릭을 일단위 표로 정규화하고, 배포 이력을 매트릭스로 교차 분석하면서 원인을 좁혀갈 수 있었습니다. AI를 활용한 스크립트 작성, [준](https://github.com/SeoJueun)의 이슈 리포팅과 지표 제안, [윈](https://www.linkedin.com/in/seunghuh/)의 맥락 전달, DA 팀의 데이터 분석 등 협업 덕분에 해결 과정이 크게 단축되었고, 결과적으로 RCU 97% 절감이라는 성과를 얻었습니다. DynamoDB를 사용한다면 `Limit` 설정, `ConsumedCapacity` 로깅, 그리고 Consistency 옵션(Strong vs Eventually)이 유스케이스에 적절한지 검토해 보시길 권장드립니다. ---
## 부록: 개발 중 만난 함정 - context.Canceled 에러 최근 기록 조회 API는 두 개의 DynamoDB 테이블을 **병렬로 조회**합니다.
Server Flow
두 테이블에 동일한 데이터가 존재하며, **먼저 성공하는 쪽의 결과를 반환**합니다. ```go // 병렬 조회 패턴 (예시) go func() { resultCh <- queryTableA(ctx, id) }() go func() { resultCh <- queryTableB(ctx, id) }() select { case r := <-resultCh: return r.Data // 먼저 성공한 결과 반환 case <-ctx.Done(): return ctx.Err() } ``` 이 구조 때문에 한 쪽이 성공하면 다른 쪽은 `context.Canceled` 에러가 발생합니다. 이는 정상적인 동작이므로 **모니터링에서 예외 처리**가 필요합니다. 특히 AWS SDK v1을 사용 중이라면 `context.Canceled`로 에러 변환 처리가 필요한데, [쿤](https://github.com/coderhyme)의 리뷰 덕분에 빠르게 해결할 수 있었습니다. 결과적으로, 다음과 같은 예외 처리를 적용했습니다. - 에러 집계(Datadog 등)에서 `context.Canceled` 제외 - 알람(Sentry 등)에서 `context.Canceled` 제외 - AWS SDK v1 사용 시 `CanceledErrorCode`를 `context.Canceled`로 변환 처리 ---
## 참고 자료 **AWS 공식 문서** - [DynamoDB Read Consistency](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html) - Strong vs Eventually Consistent Read - [DynamoDB Query API](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html) - Limit 파라미터, 1MB 페이징 - [Read/Write Capacity Mode](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html) - RCU 계산 방식 - [Best Practices for Querying](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-query-scan.html) - 쿼리 최적화 가이드 **라이브러리** - [guregu/dynamo v1.20.2](https://github.com/guregu/dynamo/tree/v1.20.2) - 본문에서 분석한 버전 - [guregu/dynamo Query 구현](https://github.com/guregu/dynamo/blob/v1.20.2/query.go) - Limit vs SearchLimit 로직 --- --- ## [Python 2.7 서버의 CI Test 개선 - 13분에서 3분으로](https://tech.buzzvil.com/blog/python2-ci-test) Date: 2025-12-18 | Author: Elric Lim | Category: DevOps ## 들어가며 안녕하세요 Supply 그룹 Product Backend 팀의 Elric 입니다. 버즈빌에서 운영중인 허니스크린은 새로운 프로모션을 지속적으로 출시하며 빈번한 배포가 진행되는 서비스이지만, 긴급 배포 상황에서 CI 파이프라인의 긴 실행 시간(약 13분)이 병목이 되어 이에 대해 근본적인 해결이 필요했습니다. ## 기존 CI 과정의 문제점 허니스크린은 현재 python2.7 버전으로 운영되지만 해당 버전은 2020년 1월부터 공식 지원이 중단되어 보안 패치나 버그 수정이 더 이상 제공되지 않습니다. 허니스크린의 CI 는 3가지 작업으로 이 작업은 모두 병렬로 실행되지만, 그럼에도 긴 시간이 소요되는 것은 특정 작업에서 병목이 있는 것으로 유추할 수 있었습니다. #### 현재 CI Pipeline 구성 - CI-Build : 프로젝트 빌드 - 2m 48s 소요 - CI-Security : 보안 검사 - 5m 16s 소요 - **CI-Test : 테스트 실행 (메인 병목 지점) - 14m 57s 소요** (가장 최근 빌드 소요시간)
#### 중복되는 ci-test **ci-test** 는 평균 13분을 소요할 뿐만 아니라 Pull Request 와 master merge 후에도 실행되기 때문에 동일한 (그리고 긴) 작업이 중복 실행되어 배포 완료까지 오랜 시간이 걸렸습니다. 아래 이미지에서 마지막 두 항목은 동일한 PR 에서 발생하는 ci-test 과정으로, PR 단계와 마스터 브랜치 merge 이후 단계에서 중복 실행되는 것을 확인할 수 있습니다. ![image1.png](https://tech.buzzvil.com/blog/python2-ci-test/image1.png)
#### 불필요한 패키지 설치 ci-build 와 달리 ci-test 는 불필요한 패키지 설치와 소스 빌드 과정을 포함했기 때문에 실제 테스트를 실행 시간은 2분 내외였지만 그 외의 리소스 설치 과정에서 병목이 있었습니다. 이미 공식 버전 지원을 종료한 **python2.7 소스 빌드**와 **PowerShell 다운/설치**, **불필요한 시스템 패키지 업그레이드** 등 작업 속도를 늦추는 많은 단계가 포함돼 있었습니다.
#### 현재 CI-Test 시간 분석 - **ci-test**: 평균 11분 (메인 병목) - **master merge**: merge 후 ci-test 중복 실행 - **전체 CI 시간**: +13분 (PR + Master 머지)
## 1차 최적화: 불필요한 작업 제거 (13분 → 5분) ### 1. 실행 트리거 최적화 마스터 브랜치 merge 후에는 중복 ci-test 가 중복 실행되지 않도록 트리거를 제한했습니다. 이로써 마스터 merge 전에만 ci-test 를 실행해 배포 프로세스 속도를 높일 수 있었습니다. - `성능 개선` : 전체 배포 시간 50% 단축 ![carbon.png](https://tech.buzzvil.com/blog/python2-ci-test/carbon.png)
### 2. PowerShell 설치 제거 기존 ci-test.yaml 에는 powershell 설치 커맨드가 있었습니다. ci-test 를 수행하려면 python2.7 설치가 선행되어야 하지만 powershell 은 테스트 실행에 전혀 필요하지 않았고, 무엇보다 설치 후 어떠한 작업도 수행하지 않았기에 이를 제거 했습니다. 예상컨대 과거에는 powershell 로 python2.7 을 실행했지만 시스템이 진화하면서 powershell 없이도 python 을 실행할 수 있게 됐으나 미처 지우지 못한 코드가 남아있는 것으로 추정했습니다. 결과적으로 powershell 설치 과정을 제거함으로 75MB 크기의 패키지 다운로드를 생략하며 테스트 시간을 대폭 줄일 수 있었습니다. ![image2.png](https://tech.buzzvil.com/blog/python2-ci-test/image2.png) - `성능 개선` : 56s ➡️ 0s
powershell 이란 - Microsoft가 개발한 태스크 자동화 및 구성 관리 프레임워크로 CLI 와 스크립팅 언어로 자동화를 지원합니다.

### 3. Python 소스 빌드 제거 공식 actions/setup-python이 Python 2.7 지원을 중단하여 `MatteoH2O1999/setup-python@v2` 액션을 사용하여 ci-test 과정에서 **매번 소스코드를 다운** 받아 컴파일 했습니다. 해당 액션은 아래의 과정을 거칩니다: 1. Python 2.7 바이너리 찾기 시도 → 실패 (EOL로 인한 바이너리 부재) 2. 소스 코드 다운로드 → GitHub 에서 Python 2.7.18 소스코드 다운로드 3. 의존성 설치 → build-essential, libssl-dev 등 컴파일 도구 설치 4. 소스 컴파일 → ./configure, make, make install (가장 시간 소모적) 5. 툴 캐시 복사 → 다음 실행을 위한 캐시 저장 위 과정에서 약 1분 30초가 소요되어 이를 python2.7 이 설치된 Docker Image 를 사용하도록 개선하여 6초 내외의 시간으로 단축할 수 있었습니다. python2.7 빌드를 위한 docker image 는 최소한의 용량으로 구성된 경량 이미지로 **python2.7-slim** 을 선택했습니다. slim 버전은 불필요한 빌드 도구, 문서 파일, 테스트 라이브러리 등이 제거되어 이미지 전체 크기를 크게 줄일 수 있습니다. alpine 이미지의 크기가 더 작다는 장점이 있지만 현재 프로젝트에서 자주 사용되는 C 확장 패키지 설치 시 복잡한 종속성 문제와 빌드 실패 가능성이 존재하기 때문에 slim 이미지를 선택했습니다. 실제로 H 프로젝트는 native C 확장 패키지인 psycopg2, cryptography 등을 여럿 사용중이기 때문에 **Debian 기반의 패키지 호환성이 좋은 slim 이미지**를 선택했습니다. - `성능 개선` : Python 빌드 시간 **93%** 단축 ![carbon-2.png](https://tech.buzzvil.com/blog/python2-ci-test/carbon-2.png)
### 4. apt upgrade 제거 우선 Docker 컨테이너는 기본적으로 root 권한으로 실행되기 때문에 sudo 오버헤드가 불필요하여 명령어에서 모든 **sudo 를 제거**했습니다. 두번째로는 apt 대신 **apt-get** 으로 명령어를 수정했습니다. apt 명령어는 사용자 친화적 인터페이스를 제공해 진행률 표시 등 기능이 있지만 ci 과정에선 불필요하기 때문에 스크립트 용에 맞게 apt-get 으로 변경했습니다. 마지막으로 필요한 패키지만 설치하도록 필수 패키지를 명시했습니다. **sudo apt update && sudo apt upgrade -y** 명령어는 모든 패키지 업그레이드를 시도하는데, 이는 ci 라는 일회성 환경에서 보안 패치까지 적용하는 불필요한 작업이며 모든 패키지를 다운받는데 12MB 의 추가 다운로드에 1분 이상 소요되는 점도 문제였습니다. **gnupg2** 패키지는 PowerShell 저장소 추가용이지만 PowerShell 자체가 불필요하므로 제거했고, **apt-transport-https** 역시 apt 에서 이미 HTTPS 지원하여 불필요했습니다. ![carbon-3.png](https://tech.buzzvil.com/blog/python2-ci-test/carbon-3.png) - 빌드용: gcc, python-dev (pycrypto 컴파일용) - 네트워크: curl, wget, aria2 - DB Client: default-mysql-client - 필수 라이브러리: libssl-dev, zlib1g-dev ### 1차 최적화 결과 여기까지 과정으로 ci-test 시간을 평균 **13분에서 5분으로 60% 이상 단축**할 수 있었습니다. 이 정도 최적화도 충분하다고 생각했고 이를 반영하여 1차적으로 상용 배포를 나갔습니다. 그리고 한달 정도 지나 새롭게 빌드를 할 때 ci-test 시간이 5~7분 정도로 불규칙하게 느려진다는 걸 알게됐습니다. 기존 최적화 과정에선 불필요한 라이브러리(PowerShell) 제거, Python2.7 slim image 사용, 필요한 패키지만 설치하도록 필수 패키지를 명시했습니다. 두번째 최적화 단계에선 이와 더불어 필수로 설치해야 할 라이브러리를 캐싱하여 최적화 하기로 했습니다.
## 2차 최적화: 캐싱 전략 개선 (5분 → 3분) 1차 최적화로 불필요한 작업을 제거했지만 여전히 매번 패키지를 설치하는 비효율적인 작업이 존재했고 평균 5분이라는 시간도 들쑥날쑥하여 이보다 오래 걸릴 때도 있었습니다. 지난 최적화가 불필요한 작업 제거에 초점을 뒀다면 이번 개선에서는 필요한 것을 재사용하는 **캐싱**의 관점으로 ci-test 시간을 추가 단축한 과정을 공유합니다. 캐싱과 더불어 불필요한 패키지 설치 과정 제거 및 Disk I/O 대신 RAM 을 사용하여 추가 개선을 도모했습니다. ### 1. pip 패키지 캐싱 전략의 근본적 개선 #### 기존 방식의 문제점 초기 CI 과정에서는 컨테이너 환경에서 GitHub Actions의 actions/cache 가 제대로 동작하지 않았습니다. 이는 컨테이너 내부 경로가 호스트의 캐시와 분리되어 있기 때문입니다. ##### GitHub Actions와 컨테이너 환경의 관계 일반적으로 GitHub Actions 워크플로우는 runs-on 키워드로 지정된 러너(Runner)에서 실행됩니다. 이 러너는 일종의 호스트 머신으로 그 내부에 Docker를 설치하여 컨테이너를 실행합니다. 즉, CI-Test 과정은 다음과 같이 동작합니다. 1. **호스트 머신 (Kubernetes 노드)**: GitHub Actions 러너가 동작하는 기반 환경입니다. 이 머신에 Docker 데몬이 설치되어 있습니다. 2. **컨테이너** : 호스트 머신 위에서 실행되는 격리된 환경으로 python:2.7-slim 이미지를 사용해 모든 테스트 스크립트를 실행합니다. 이런 구조로 인해 actions/cache 가 생성하는 캐시는 호스트 머신의 특정 디렉터리에 저장됩니다. 반면 APT와 pip로 설치된 패키지들은 컨테이너 내부의 /var/cache/apt나 ~/.cache/pip 같은 경로에 저장됩니다. 두 공간이 다르기 때문에 캐시가 연결되지 않습니다. 마치 호스트 머신에 저장한 물건을 컨테이너 안에서 찾으려 하는 것과 같았습니다. #### 해결책: $GITHUB_WORKSPACE 경로 활용 이 문제를 해결하기 위해 $GITHUB_WORKSPACE 경로를 사용했습니다. 이 경로는 GitHub Actions가 워크플로우를 체크아웃하는 디렉터리로 호스트와 컨테이너 간에 공유(bind mount)되는 유일한 공간입니다. 즉 호스트와 컨테이너가 공통으로 접근할 수 있는 "공유 폴더" 역할을 하는 셈이죠. 컨테이너와 호스트가 유일하게 공유하는 $GITHUB_WORKSPACE 경로를 활용해 APT와 pip의 캐시 디렉토리를 설정했습니다. #### APT 캐시 APT 캐시 디렉토리를 워크스페이스 하위(.apt/cache/archives, .apt/state/lists)로 변경하고 apt-get 명령에 --cache-dir 옵션을 명시적으로 추가했습니다. ![carbon-4.png](https://tech.buzzvil.com/blog/python2-ci-test/carbon-4.png) APT 패키지를 먼저 다운로드만 하고(download-only), 그 후에 캐시에서 설치하는 2단계 방식을 적용하여 캐시 효과를 극대화했습니다. #### pip 캐시: 다운로드 캐시에서 설치 패키지 캐싱으로 H 서버는 150여 개의 파이썬 라이브러리를 사용중입니다. 1차 개선 후에는 pip의 **다운로드 캐시**만 저장하고 있었습니다. 이 방식은 패키지 파일(`.whl`, `.tar.gz`)은 캐싱하지만, 설치된 패키지 자체는 캐싱하지 않습니다. 그래서 `requirements.txt`가 변경되지 않아도 매번 pip install을 실행해야 했죠. ![carbon-6.png](https://tech.buzzvil.com/blog/python2-ci-test/carbon-6.png) #### AS-IS: 다운로드 캐시만 있는 경우 ```bash pip install 실행 과정: 1. ✅ PyPI에서 패키지 다운로드 (.whl, .tar.gz) → 캐시 가능 2. 다운로드한 파일 압축 해제 → 매번 실행 3. site-packages에 설치 → 매번 실행 4. 의존성 검사 및 바이너리 컴파일 → 매번 실행 ``` 즉 원격 서버에서 패키지를 받아오는 시간은 절약되지만 **압축 해제, 설치, 컴파일 작업**은 여전히 매번 실행되고 있었습니다. #### TO-BE: 설치된 패키지 자체를 캐싱 Docker Layer 캐싱 원리에 따라 "변경이 적은 레이어는 위쪽에"라는 원칙을 적용했습니다. pip 패키지는 requirements.txt가 바뀌지 않는 한 동일하기 때문에 설치된 site-packages 디렉토리 자체를 캐싱하기로 했습니다. 요리에 빗대어 보면 기존 방식은 재료(패키지 파일)만 캐싱, 개선된 방식은 완성된 요리(설치된 패키지)를 통째로 캐싱합니다. ![carbon-7.png](https://tech.buzzvil.com/blog/python2-ci-test/carbon-7.png) 왜 **`site-packages`** 를 캐싱할까요? **/usr/local/lib/python2.7/site-packages** 는 pip 가 모든 패키지를 설치하는 최종 목적지 입니다. 이 디렉토리에는 압축 해제된 python 모듈과 컴파일 된 바이너리, 패키지 메타데이터 등의 정보가 모두 **준비된 상태**로 저장됩니다. 예를 들어 python 이 import django 를 실행할 수 있는 완성된 상태가 위 디렉토리에 준비되어 있는 것이죠. 즉 실행 가능 상태의 재료가 site-packages 에 저장되므로 이를 캐싱하여 2분 넘게 걸리던 불필요한 실행 작업을 건너 뛸 수 있습니다 (2min -> 0s) ![carbon-8.png](https://tech.buzzvil.com/blog/python2-ci-test/carbon-8.png) ### 2. APT 패키지 설치 최적화 #### 조건부 실행으로 불필요한 작업 제거 기존에는 설치 여부를 확인하지 않은 채 매번 시스템 패키지를 설치했다면, 시스템 패키지가 설치 여부를 체크 후 다음 스텝으로 넘어가도록 구조를 개선했습니다. `command -v`는 명령어가 존재하는지 확인하는 쉘 내장 명령어입니다. 모든 필수 패키지가 이미 설치되어 있다면 apt-get을 실행하지 않고 바로 종료합니다. self-hosted runner 환경에서는 이전 빌드의 패키지가 남아있는 경우가 많아 이 체크만으로도 2-3분을 절약할 수 있었습니다. ![carbon-9.png](https://tech.buzzvil.com/blog/python2-ci-test/carbon-9.png) ### 3. MySQL 테스트 최적화 테스트용 MySQL은 디스크 I/O가 병목이 될 수 있어서 tmpfs(메모리)를 사용하도록 설정했습니다. ![img4.png](https://tech.buzzvil.com/blog/python2-ci-test/img4.png) `tmpfs`는 temporary file system 의 약자로 디스크가 아닌 **RAM 에 파일 시스템을 올리는 개념**입니다. 일반적인 파일 시스템과 동일하게 사용할 수 있지만 모든 데이터가 메모리에 저장되어 Disk I/O 보다 빠른 속도를 낼 수 있습니다. CI 테스트 환경에서는 데이터를 영구적으로 보관할 필요가 없을 뿐더러 ci 속도 향상을 위해서 빠른 read/write 가 가능한 tmpfs 를 사용하는 것이 적절하다고 판단했습니다. ![carbon-10.png](https://tech.buzzvil.com/blog/python2-ci-test/carbon-10.png) MySQL의 데이터 디렉토리(`/var/lib/mysql`)를 tmpfs로 마운트하면: - 디스크 I/O 대신 메모리 I/O 사용 - 테이블 생성, INSERT, SELECT 등 모든 DB 작업이 빨라짐 - 컨테이너 종료 시 자동 clean up - 메모리가 부족할 경우 swap 영역(디스크)으로 스왑될 수 있습니다. CI 환경에서는 일반적으로 문제가 되지 않지만 완전한 메모리 전용 동작을 원한다면 충분한 메모리를 확보해야 합니다.

## 결과 및 향후 계획 #### 전체 개선 여정: 13분 → 5분 → 3분 1차 개선 작업에서는 불필요한 작업을 제거하여 CI 시간을 13분에서 5분으로 단축했고, 2차 개선에서는 **캐싱 전략을 근본적으로 바꿔** 5분에서 3분 대로 추가 단축할 수 있었습니다. 이번 개선을 통해 **무엇을 캐싱할지**에 따라 결과가 달라지는 것을 볼 수 있었습니다. `Docker Layer 캐싱 원칙`에 따라 변경이 적은 부분을 상위 layer 에서 처리하며 `requirements.txt`가 변하지 않는 대부분의 PR 에서 패키지 설치를 완전히 생략할 수 있었습니다. 최초 PR 이 open 할 땐 packages 설치 과정이 수행되겠지만, 그 이후 부터는 별도의 PR 에서도 캐싱의 이점을 활용할 수 있어 개발 생산성 향상에 기여할 수 있었습니다. - ❌ 잘못된 캐싱: 중간 과정(다운로드 파일)만 캐싱 - ✅ 올바른 캐싱: 최종 결과물(설치된 패키지) 캐싱
#### 전체 ci-test 결과 차이 ![img3.png](https://tech.buzzvil.com/blog/python2-ci-test/img3.png)
### 추가 최적화 방안: Pre-built Docker 이미지 현재 CI 과정은 사이즈가 큰(e.g python2-slim, mysql:5.7, amazon/dynamodb-local) 컨테이너 이미지를 매번 pull 해야 합니다. 이미지가 self hosted runner 에 없다면 Docker hub 에서 이미지 다운 후 압축 해제 및 준비 과정까지 거쳐야 합니다. 위의 캐싱 전략으로 상당한 시간을 단축했지만, 궁극적인 해결책은 모든 의존성을 미리 설치해둔 Pre-built Docker 이미지를 사용하는 것입니다. - `honeyscreen/Dockerfile.ci` : 필요한 모든 패키지(apt-get, pip)를 미리 설치하는 CI 전용 Dockerfile을 정의합니다. - `.github/workflows/build-ci-image.yaml` : 이 Dockerfile을 이용해 CI 이미지를 빌드하고 ECR(Elastic Container Registry)에 푸시하는 워크플로우를 구성합니다. - `.github/workflows/ci-test-optimized.yaml` : 실제 CI-Test 워크플로우에서 이 사전 빌드된 이미지를 사용합니다. ``` yaml # Runner 서버에서 1회만 실행 (초기 설정 또는 정기 업데이트 시) docker pull python:2.7-slim docker pull mysql:5.7.34 docker pull amazon/dynamodb-local:latest ``` 이 방식을 적용하면 패키지 설치 단계 자체를 완전히 제거하여 더욱 빠른 CI 시간을 달성할 수 있습니다. 이는 Python 2.7 환경의 근본적인 한계를 극복하는 가장 효과적인 방법입니다. ## 마치며 두 번의 개선 작업을 거쳐 최종적으로 CI-Test 시간을 13분에서 3분으로 단축할 수 있었습니다. 단순 시간 절약을 넘어 빠르게 테스트하고 배포할 수 있는 환경을 구축하여 생산성 향상에도 기여할 수 있었습니다. 이번 CI 최적화는 Python 3 마이그레이션을 위한 첫 번째 단계였습니다. 다음 글에서는 Python 2에서 Python 3로 마이그레이션 과정과 마주한 도전과제들을 다룰 예정입니다.

[버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [2026년 버즈빌 디자인 스택, AI 전환을 준비하며](https://tech.buzzvil.com/blog/2026-buzzvil-design-stack) Date: 2025-11-27 | Author: Maxence Mauduit | Category: Design 2026년은 버즈빌 디자인 팀에게 전환점이 되는 해입니다. 이제 도구 선택은 단순한 운영상의 결정이 아니라, 차세대 AI 기반 제품 개발을 지원할 수 있는 '코드와 연결된 통합 디자인 시스템'을 향한 전략적 행보입니다. 지난 몇 년간 버즈빌 디자인 팀은 철저한 디지털 다이어트를 감행했습니다. 군더더기를 걷어내고, 중복을 제거하고, 가장 결정적으로 Adobe와 완전히 작별했습니다. 포토샵도, 일러스트레이터도, "이번 한 번만"이라는 예외도 없습니다. 간혹 클라이언트에게서 받은 PSD나 AI 파일을 열어야 할 때는 Affinity를 사용합니다. 최근 무료화가 되었는데, 이게 축복일지 경고 신호일지는 두고 봐야겠죠. 온라인 디자인 문서화를 담당하던 Framer와도 이별했습니다. 이런 작별 인사를 마친 지금, 2026년 우리의 디자인 주방에는 무엇이 남았을까요? ⸻ ## Figma (Org) ### 장기 비전 B2B2C 기업으로서 최근 Figma의 디자인 시스템 업데이트에 상당히 만족하고 있습니다. 더 많은 모드, ~~확장된 컬렉션~~(Enterprise 전용이라 아쉽지만...), 그리고 드디어 등장한 Slots 덕분에 여러 브랜드를 여러 제품과 레이아웃에 걸쳐 관리하면서도 도망치고 싶은 충동이 들지 않게 되었습니다. #### 하지만 진짜 변화는 수면 아래에서 일어나고 있습니다. Figma의 진화 방향은 명확합니다. 디자인 시스템이 코드와 직접 연결되고, 그 위에서 AI 에이전트가 직접 작동하는 세계입니다. 이는 버즈빌의 2026년 디자인 계획과 정확히 일치합니다. 모든 미션이 코드 기반의 통합 디자인 시스템 위에서 움직이게 될 것입니다. 이건 단순히 편하자고 하는 프로젝트가 아닙니다. AI를 활용한 차세대 제품 개발 단계로 넘어가기 위해 반드시 거쳐야 할 전제 조건입니다. Figma의 로드맵을 보면 그 의도가 분명합니다. 공개된 LLM 데모들이 장난스러워 보일 수 있지만, 무대 뒤에서는 실제 에이전트 워크플로우를 위한 인프라를 구축하고 있습니다. 지난 1년간 Figma는 여러 AI 모델을 테스트하고, 파일 컨텍스트를 에이전트에 노출하는 MCP 레이어를 배포하며 신뢰성을 높였습니다. Cursor 같은 도구는 이미 MCP를 활용해 무엇이 가능한지 보여주고 있습니다. 물론 아직은 "내일 바로 배포" 수준이라기보다 "유망한 프로토타입"에 가깝지만요. 동시에 Figma는 훨씬 더 큰 의미를 지닌 업데이트들을 조용히 밀어붙였습니다. Code Connect가 성숙해지면서 Figma 컴포넌트와 실제 코드가 GitHub를 통해 직접 연결됩니다. 화려하진 않지만, 올바른 디자인-코드 상호운용성을 위한 확실한 투자입니다. MCP가 우리의 실제 컴포넌트 코드를 참조하고 사용할 수 있게 되는 순간, 에이전트는 더 이상 "그럴싸한" UI를 만들어내지 않을 것입니다. 실제 우리 시스템에 기반하고, 기본적인 비즈니스 로직까지 적용된 준(準) 프로덕션급 인터페이스를 만들어낼 것입니다. 2025년 11월 18일 업데이트에서는 Confluence나 Notion PRD를 디자인 시스템 라이브러리를 활용한 Figma Make 프로토타입으로 변환하는 커넥터까지 도입되었습니다. PRD에서 기능적 프로토타입까지 이어지는 미래 파이프라인의 첫 단추가 끼워진 셈입니다. 이 모든 것이 하나의 단순한 진실을 가리킵니다. 우리의 디자인 시스템이 코드와 완전히 연결되지 않으면, 이 거대한 변화의 혜택을 누릴 수 없습니다. 어설픈 시스템으로는 어림도 없습니다. AI 에이전트는 기반이 되는 컴포넌트가 명확히 정의되고, 일관성 있으며, 코드베이스와 일치할 때만 고품질 결과물을 만들어낼 수 있습니다. 이것이 바로 2026년이 모든 미션을 하나의 성숙한 코드 기반 시스템에 얼라인하는 해가 되어야 하는 이유이며, Figma가 우리 스택의 핵심으로 남아야 하는 이유입니다. 이 과정을 제대로 수행한다면 빠르면 2027년부터 실질적인 AI 지원 디자인을 시작할 수 있습니다. 그렇지 않으면 다른 팀들이 먼저 앞서나가겠죠. 이제, 좀 더 당면한 문제들을 살펴보겠습니다: ### Dev Mode: 도입 완료 ![Figma MCP integration](https://tech.buzzvil.com/blog/2026-buzzvil-design-stack/mcp.png) Dev Mode 시트를 모든 프론트엔드 및 클라이언트 엔지니어에게 확대했습니다. 2025년 내부 설문조사에 따르면 도입률은 놀라울 정도로 높습니다. 모든 사용자가 매주 사용하고 있으며, 71%는 하루에도 여러 번 사용합니다. NPS 72점을 기록할 만큼 만족도가 높고, 핸드오프가 빨라지고 검수가 쉬워졌으며 디자인-코드 간 명확성이 높아졌다는 데에 의견이 모입니다. 유일한 걸림돌은 가격인데, 오히려 적절해 보입니다. 비싼 SaaS 청구서만큼 팀의 디자인 시스템 규율을 높이는 동기부여도 없으니까요. ### Make와 Slides: 도입 완료 ![Figma Make interface](https://tech.buzzvil.com/blog/2026-buzzvil-design-stack/make.png) 처음엔 새로운 Figma Make와 Figma Slides에 회의적이었지만, 지금은 조심스러운 낙관과 함께 받아들이고 있습니다. Make는 초기 아이디어를 시각화하거나 Figma만으로는 문서화하기 어려운 인터랙션 패턴을 정리할 때 사용합니다. 마이크로 인터랙션이나 고급 인터랙션 패턴이 필요할 때는 이제 Make를 통해 전달합니다. Slides는 프레젠테이션 덱을 만들 때 가장 먼저 찾는 도구가 되었습니다. 조직 내에서 점차 입지를 넓혀가고 있으며, 시간이 지나면 Google Slides와 PPT를 서서히 대체할 것으로 보입니다(정말 다행이에요). ### Sites와 Buzz: 테스트 중 ![Figma buzz interface](https://tech.buzzvil.com/blog/2026-buzzvil-design-stack/buzz.jpg) Figma Sites와 Buzz도 실험 중입니다. 주로 브랜드 및 커뮤니케이션 작업에 활용하고 있습니다. 아직은 초기 탐색 단계로, 꽤 흥미롭긴 하지만 필수 도구라고 하긴 어렵습니다. ⸻ ## GPT Business & Gemini Pro ![GPT Business interface](https://tech.buzzvil.com/blog/2026-buzzvil-design-stack/gpt.png) 전사가 GPT와 Gemini를 사용하고 있으며, 디자이너에게도 없어서는 안 될 동반자가 되었습니다. 지난 몇 년간 이 LLM들은 UX 라이팅부터 초기 단계의 피드백, 아이디에이션, 그리고 우리가 더 이상 의식조차 못하는 수백 가지 자잘한 작업들까지 지원해 왔습니다. GPT를 흔히 '예스맨'이라고들 하지만, 그건 그냥 놔뒀을 때 얘기입니다. 프롬프트를 제대로 활용하면 누구보다 예리하고 주관 뚜렷한 디자인 비평가가 되어줍니다. 때로는 그게 딱 우리에게 필요한 것이기도 하고요. ⸻ ## Cursor ![Cursor interface](https://tech.buzzvil.com/blog/2026-buzzvil-design-stack/cursor.png) 2026년부터 Cursor가 스택에 합류했습니다. 작년에 테스트해 본 결과, 강력한 디자인 증폭기임이 빠르게 증명되었습니다. Cursor를 통해 프로토타이핑을 한 단계 더 밀어붙이고, 차세대 디자인 시스템을 코드 레벨에서 직접 다듬을 수 있습니다. 아직 초기 단계지만 잠재력은 분명합니다. 언젠가는 디자인을 위해 Figma가 아예 필요 없는 날이 올지도 모릅니다. 우리 시스템이 코드 안에서 살아 숨 쉰다면, Figma는 Org에서 Team 플랜으로 다운그레이드하고 이해관계자 프레젠테이션 용도로만 남겨둘 수도 있겠죠. 우리가 도구의 한계를 넘어선 게 이번이 처음은 아닙니다. Adobe는 아직도 프로모션 메일을 보내오는데, 마치 전 애인에게 청첩장 받는 기분이랄까요. ⸻ ## Midjourney와 IconScout Midjourney와 IconScout은 계속 유지하기로 했습니다. 우리 팀은 전원 프로덕트 디자이너로 구성되어 있고, 내부에 비주얼이나 브랜드 전문가가 따로 없습니다. 효율적인 구조이지만 가끔 빠른 시각적 부스트가 필요할 때가 있죠. Midjourney는 시안의 방향성을 탐색하는 데 도움을 주고, IconScout은 일상적인 아이콘 니즈를 해결해 줍니다. 정말 정교하게 다듬어진 결과물이 필요할 땐 신뢰할 수 있는 프리랜서들과 협업합니다. 자주는 아니지만, 그럴 때마다 늘 즐겁습니다. 그들은 보통 우리의 "이 정도면 됐지"라는 안일함에서 우리를 구원해 주니까요. ⸻ ## 새로운 디자인 접근 권한 정책 올해부터 Figma Editor 시트 배포 방식도 업데이트합니다. 이제 Editor 권한은 오직 프로덕트 디자이너에게만 부여됩니다. 이는 우리가 스케일업하는 과정에서 디자인 품질, 책임 소재, 시스템 무결성, 그리고 예측 가능한 워크플로우를 보장하기 위함입니다. 다른 분들은 FigJam, 코멘트, 또는 디자인 팀의 지원을 통해 협업하게 됩니다. 이 변화로 역할 오너십이 명확해지고, 프로세스 규율이 강화되며, 파일 구조가 일관되고, 비용 관리도 공정해집니다. 핵심 철학은 이것입니다. 디자인 품질에 대한 책임은 디자이너가 지되, 협업은 모두에게 열려 있고 효율적이어야 한다는 것입니다. 이 정책이 어떻게 작동하는지 지속적으로 모니터링할 것이며, 핵심 워크플로우에 문제가 생긴다면 다시 검토할 것입니다. 그렇다고 이 문단을 인쇄해서 씹어 먹겠다는 약속까진 못하겠네요. 지금으로선 더 확장 가능하고, 예측 가능하며, 규율 있는 디자인 프로세스를 향한 올바른 걸음이라고 느껴집니다. ⸻ ## 맺음말 2026년을 위해 선택한 도구들은 단순한 업그레이드가 아니라 하나의 약속입니다. 코드와 연결된 통합 디자인 시스템, 규율 있는 워크플로우, 그리고 AI를 받아들일 준비가 된 인프라는 다가오는 변화를 버텨낼 튼튼한 토대가 되어줄 것입니다. 디자인이라는 분야는 향후 2년 동안 지난 10년보다 더 빠르게 변화할 것입니다. 우리의 목표는 모든 변화를 예측하는 것이 아닙니다. 변화가 닥쳤을 때 구조적으로 준비되어 있는 것, 그것이 우리가 지향하는 바입니다. ⸻ [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [5대 금융사를 품은 버즈베네핏의 백엔드 팀은 무슨 일을 할까요?](https://tech.buzzvil.com/blog/buzzvil-supply-product-backend-team) Date: 2025-10-31 | Author: Wynn Huh | Category: Backend 안녕하세요, 버즈빌 Supply 그룹의 Product Backend 팀 리드 Wynn입니다. 현재 저희 팀에서 함께 팀을 성장시킬 [동료](https://buzzvil.career.greetinghr.com/ko/o/174430)를 찾고 있습니다. 채용 공고만으로는 우리 팀이 어떤 팀이고, 어떤 일을 하는지 충분히 전하기 어렵다고 느껴 이 글을 쓰게 되었습니다. 저희 팀은 [버즈베네핏(BuzzBenefit)](https://www.buzzvil.com/monetize/buzzbenefit)의 백엔드 시스템 전반을 책임지고 있습니다. 이번 글에서는 저희 Product Backend 팀이 어떻게 만들어졌고 지금 어떤 역할을 하고 있는지를 사례와 함께 소개드리려 합니다. ## Product Backend 팀의 탄생 배경 버즈베네핏 SDK는 처음 제품이 출시된 후 올해 2025년 초반까지만 해도, 약 10년 동안은 리워드 광고 송출에 초점을 맞춘 제품이었습니다. 그러나 올해 초부터는 단순한 광고 플랫폼을 넘어 **게이미피케이션 기능을 통해 사용자 참여를 유도하고 다양한 혜택 경험을 제공하는 플랫폼**으로 빠르게 진화했습니다.
버즈베네핏 mock
그 결과 카카오뱅크, 카카오페이와 같은 디지털 뱅킹과 핀테크를 넘어, 국내 5대 금융사 파트너를 올해 모두 확보하며 국내 리워드 수익화 플랫폼 1위 자리를 공고히 했습니다. 또한, 이미 시장 점유율 1위를 지키던 상황에서도 50% 이상의 사용자 성장을 달성했습니다. **하지만 제품의 급격한 성장과 함께 저희 백엔드 시스템도 변화가 필요했습니다.** “사용자의 참여와 체류를 늘리고 더 풍부한 혜택 경험을 제공하는 플랫폼”을 실현하기 위해 제품 조직에서는 사용자 참여를 극대화하기 위한 다양한 게이미피케이션 기능들을 기획했고, 각 기능의 효과를 정확히 측정하기 위한 데이터 분석과 가설 검증을 위한 실험을 확대하고자 했습니다. 그러나 이를 뒷받침할 백엔드 시스템은 광고 송출에 최적화되어 있어 새로운 기능이 추가될 때마다 서로 다른 데이터 구조와 로직이 쌓이며 데이터 흐름이 파편화되었습니다. 기능 간의 시너지를 만들어내기 위한 데이터 연동도 복잡해져 확장성과 유지보수성 모두 한계에 부딪혔습니다. 무엇보다도 실험 단위로 데이터를 수집·가공하기가 어려웠습니다. 결국 제품팀이 원하는 속도로 가설을 검증하거나 유저 행동 데이터를 유기적으로 연결해 인사이트를 얻는 데에 어려운 구조가 되어가고 있었습니다. 트래픽을 안정적으로 처리함과 동시에 새로운 기능의 확장과 실험이 용이한 시스템이 필요했습니다. **Product Backend 팀은 이러한 문제들을 근본적으로 해결하고 버즈베네핏의 현재부터 미래 도약을 책임지기 위해 탄생했습니다.** ## 팀의 원칙과 목표 저희는 팀을 만들 때 아래의 3대 핵심 원칙을 세웠습니다. 1. 단순히 API를 개발하는 데 그치지 않고, 제품 전반의 데이터 모델과 처리 흐름을 설계한다. 2. 확장 가능한 아키텍처를 구축한다. 3. A/B 테스트와 피처 플래깅 등, 실험 중심의 제품 문화를 뒷받침하는 시스템을 구축한다. 위의 핵심 원칙에 기반하여, 일상 속 주요 업무는 다음과 같습니다. - 앱, 웹에 데이터를 제공하기 위한 API 개발 - 신규 혜택 기능 비즈니스 로직 개발 - 데이터 파이프라인 설계 및 구축 - 기능 실험 지원 - 시스템 모니터링 - 운영 효율화를 위한 시스템 개선 ## 기술 스택 현재 팀이 사용하는 기술 스택은 다음과 같습니다. - 애플리케이션: Golang, Python, MySQL, AWS DynamoDB, Redis, Kafka, Airflow, Confluent Kafka Connector, Argo Workflows - 인프라: Kubernetes, Istio, Docker, Terraform, AWS, Helm, Argo CD, Datadog, Loki, Grafana, Prometheus ## 업무 사례 소개 저희 팀이 어떤 팀인지 좀 더 잘 이해하실 수 있도록 실제로 팀에서 했던 프로젝트를 소개해 드리겠습니다. ### 사례1. 카카오뱅크 고객사에 “모임통장 미션 챌린지” 기능 도입 팀에서 최근 “[모임통장 미션 챌린지](https://platum.kr/archives/267209)” 라는 기능을 런칭했습니다. 카카오뱅크 모임통장 참여 인원들의 광고 참여 데이터를 바탕으로 사용자의 모임통장 참여 랭킹을 집계하고 랭킹 리워드를 받아가는 서비스를 만드는 프로젝트입니다. #### 핵심 도전 과제 이 프로젝트의 핵심 도전 과제는 세 가지였습니다. 1. 기존 대규모 광고 참여 데이터를 기반으로 사용자 랭킹 생성 2. MSA 환경에서 서비스 간 데이터 일관성 확보 3. 안정성과 확장성을 모두 갖춘 랭킹 파이프라인 구축 #### 데이터 플로우 설계한 데이터 플로우는 다음과 같습니다. 먼저 핵심 서버 컴포넌트를 설명 드리겠습니다. - 광고 참여 API 서버: 사용자의 광고 참여를 기록하고, 카카오뱅크 시스템에 리워드 적립을 요청 - 랭킹 API 서버: 사용자 랭킹 조회 및 모임통장 정보 등록 기능 제공 랭킹 집계는 사용자의 참여 이벤트를 기반으로 이뤄집니다. 따라서 이벤트를 발행할 때 광고 참여 서버가 모임통장 정보를 가지고 있어야 했습니다. 하지만 모임통장 정보는 랭킹 서버의 DB에만 존재했기 때문에 두 DB 간 데이터 동기화(Replication)가 필요했습니다. ![kakaobank_moim_flow](https://tech.buzzvil.com/blog/buzzvil-supply-product-backend-team/kakaobank_moim_dataflow.png) ### CDC(Change Data Capture) 파이프라인 구축 저희는 DB 변경 사항을 실시간으로 감지해 다른 DB에 전파하는 **CDC 파이프라인**을 구축했습니다. 이를 위해 Kafka 기반의 **[Confluent Platform](https://docs.confluent.io/platform/current/get-started/platform.html)** 을 활용했습니다. > Confluent Platform은 실시간 데이터 파이프라인과 스트리밍 애플리케이션을 쉽게 구축할 수 있는 Kafka 기반 플랫폼입니다. - **JDBC Source Connector** → MySQL DB의 변경 데이터를 감지하고 Kafka 토픽으로 발행 - **JDBC Sink Connector** → Kafka 토픽을 구독하여 다른 DB에 자동 적재 이 구조를 통해 별도의 동기화 로직 없이도 **DB 간 일관성**을 유지하며, 손쉽게 데이터 Extract/Load 파이프라인을 구현했습니다. 그 결과 광고 참여 서버에서도 모임통장 정보를 활용해 참여 이벤트를 문제없이 발행할 수 있게 되었습니다. #### 이벤트 파이프라인 및 랭킹 집계 참여 이벤트는 Kafka 토픽으로 구성되어, 여러 서비스가 이를 구독해 활용할 수 있습니다. 랭킹 집계 방식은 두 가지 옵션을 검토했습니다. - **실시간 집계** - **주기적 집계** 위의 두 가지 옵션을 검토한 끝에, Kafka Consumer 유지보수 비용과 파일럿 프로젝트라는 특성을 고려해 최종적으로 주기적 집계 방식을 선택했습니다. 이에 **Confluent Managed S3 Sink Connector**를 활용해 이벤트를 S3에 적재하고, **Argo Workflow**를 이용해 정해진 주기에 데이터 쿼리부터 랭킹 집계, 그리고 DB 적재까지 자동화했습니다. 추가로 랭킹 및 참여 정보는 별도의 ETL Workflow를 통해 S3에 저장되어 분석에도 활용됩니다. 버즈빌은 이미 [셀프 서빙 데이터 파이프라인](https://tech.buzzvil.com/blog/%EC%85%80%ED%94%84-%EC%84%9C%EB%B9%99-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0/)을 보유하고 있기 때문에, YAML에 익숙한 백엔드 엔지니어라면 누구나 직접 분석용 파이프라인을 작성할 수 있습니다. Kafka 기반 구조를 도입하면서 사용자 참여 증가에 따른 API 요청 및 이벤트 발행 수 급증으로 인한 데이터 규모 증가에도 **수평 확장(Scale-out)** 이 용이해졌고, 실제 서비스 런칭 후 트래픽 급증 상황에서도 **무중단 운영**을 유지했습니다. 이러한 확장성 있는 파이프라인을 구축한 덕분에 **1,200만 모임통장 유저 대상으로 오픈한** “모임통장 미션 챌린지” 시즌 1을 성공적으로 마무리하고 현재는 시즌 2도 안정적으로 운영 중입니다. ### 사례2. 모니터링 시스템 구축 #### 모니터링 시스템의 중요성 제품의 데이터 흐름을 안정적으로 관리하기 위해서는 API, Workflow 등 소스 데이터를 만드는 컴포넌트들과 기반 인프라가 잘 동작하는지 모니터링 하는 것이 중요합니다. 컴포넌트가 오동작하거나 장애가 생겨 제품에 문제가 생기진 않았을지, 데이터가 오염되진 않았을지, 데이터 동기화는 잘 되고 있는지 면밀하게 추적해야 합니다. #### 모니터링 구성 툴 - Datadog & Grafana 버즈빌에서는 **Datadog**과 **Grafana**라는 툴을 사용해서 주로 모니터링을 수행하고 있습니다. Datadog으로 Trace를 기본적으로 수집하고 있기 때문에, APM 기능을 통해 API의 Latency, Error, Traffic 등을 효과적으로 모니터링 할 수 있습니다. 전반적인 서비스 모니터링은 Datadog을 사용하고 있으며, Grafana의 경우 Loki를 사용한 로그 모니터링과 Prometheus를 사용한 메트릭 모니터링을 수행하고 있습니다. 팀이 구성된 초기에는 위와 같은 모니터링이 미비했지만, 몇 차례 장애를 겪으면서 빠르게 시스템 구축의 필요성을 깨달았습니다. 다행히 이미 시스템을 잘 구축해놓은 다른 팀들의 레퍼런스가 있었기 때문에 안정적으로 세팅할 수 있었습니다. 저희 팀의 경우, Datadog으로 **대시보드와 Alert을 세팅**해서 API, Workflow 등의 컴포넌트와 Kafka, MySQL, Redis, DynamoDB 등의 인프라를 실시간으로 모니터링 하고 있습니다.
service dashboard workflow dashboard

Datadog을 연동하지 않은 Kafka Connector 컴포넌트 등의 경우 Loki와 Grafana Alerting을 사용해 모니터링을 하고 있습니다. 시스템은 지속적으로 튜닝을 거쳐 현재는 SLO 목표 수준을 유지하며 안정적으로 시스템을 운영하고 있습니다. ### 사례3. Feature flag 시스템 구축 #### 배경 제품을 안정적으로 운영하고 신규 기능을 성공적으로 런칭하려면 데이터 흐름을 세밀하게 통제할 수 있는 환경이 중요합니다. 데이터를 통제할 수 있다면 장애 등의 리스크에 쉽게 대응할 수 있고, 테스트를 용이하게 만들고, 나아가 민첩한 제품 실험도 가능하기 때문입니다. 버즈베네핏의 경우, 서비스 변화에 보수적인 대형 고객사가 많아지고 만드는 기능도 많다보니 제품 운영 파트에서 더 세밀하게 기능을 통제하고 빠르게 리스크를 대응하고 싶은 니즈가 커졌습니다. 이를 위해 저희 팀은 **[Feature Flag](https://mfitlab.com/solutions/blog/feature-flag)** 시스템을 주도적으로 초기 구축부터 시스템 고도화를 위한 TF 조직 구성까지 해 나가고 있습니다. #### Feature Flag란? Feature Flag는 코드 변경 없이 기능을 동적으로 활성화하거나 비활성화할 수 있는 메커니즘입니다. 예를 들어, 신규 기능을 일부 유저/고객사에만 제한적으로 노출하거나, 기능 장애가 발생할 경우 즉시 기능을 비활성화할 수 있습니다. Feature Flag를 통해 개발자는 기능을 보다 효과적으로 관리 및 테스트할 수 있고 운영자는 제품의 릴리스 프로세스를 쉽게 제어할 수 있습니다. 구현한 전체 구조는 다음과 같습니다.
feature flag arch
Flag를 평가하는 Client 인터페이스와 Flag를 등록하는 Manager 인터페이스를 분리해서 추후 각각 다른 구현체로 쉽게 교체할 수 있는 구조에 주안점을 두고 만들었습니다. 프론트엔드와 백엔드에서 각각 Feature Flag를 사용할 수 있습니다. #### 앞으로의 계획 Feature Flag를 도입하게 되면서 버즈베네핏의 **Test in Production**(실제 트래픽 환경에서 제한된 사용자 그룹을 대상으로 기능을 실험)이 가능해졌고, **특정한 유저 그룹/고객사에게만 기능을 오픈**해서 데이터를 수집하는 것도 더 손쉬워졌습니다. 앞으로는 이 기반을 확장해 **A/B 테스트 기능**을 지원하고, 제품 조직이 다양한 가설 검증 실험을 더 빠르고 효율적으로 수행하도록 지원할 계획입니다. ## 마치며 세 가지 사례를 통해 저희 팀이 어떤 일들을 하고 있고, 어떤 환경에서 개발을 하고 있는지 설명드렸는데요. 오늘 글에서 언급한 업무 사례 이외에도 팀에서는 다양한 프로젝트를 많이 진행/계획 하고 있습니다. - 혜택 모듈 구조 개선을 통한 개발 속도 향상 - 일 150만 건 데이터를 Write하는 RDB를 NoSQL로 무중단 마이그레이션하기 - 실시간 분석 인프라 v2 구축 이 외에도 AI를 어시스턴트로 삼아 비즈니스 임팩트와 생산성에 기여할 수 있는 과제 등, 다양한 프로젝트 또한 준비 중에 있습니다. 처음 팀을 셋업하면서 **데이터 중심의 애플리케이션 설계 역량을 갖춘 팀**을 만들겠다는 개인적인 목표를 세웠는데요, 점점 업무 범위를 넓혀가고, 깊이를 쌓아가면서 조금씩 목표를 향해 다가가고 있는 중입니다. 데이터 중심 애플리케이션 설계에 관심 있으신 분들, 백엔드 개발을 통해 비즈니스 임팩트를 내는데 관심 있으신 분들, 대규모 트래픽/데이터를 다루면서 팀과 함께 성장하고 싶은 분들은 적극적으로 문의 주시고 **[[버즈베네핏] 백엔드 개발자 (경력 3년 이상) 공고](https://buzzvil.career.greetinghr.com/ko/o/174430)** 에 지원 부탁드립니다. (커피챗도 언제나 환영합니다!) 이 이야기가 누군가에게 관심과 결심이 될 수 있기를 바라며 글을 마치겠습니다. 긴 글 읽어주셔서 감사합니다. --- ## [버즈빌 프론트엔드 변천사](https://tech.buzzvil.com/blog/buzzvil-frontend-history) Date: 2025-08-04 | Author: Luke Hwang | Category: Frontend 버즈빌 Supply 그룹의 Product Growth 팀의 Luke입니다. 저희 버즈빌에서 지난 몇 년간 겪어온 프론트엔드 아키텍처 변화를 공유하려 합니다. AngularJS에서 시작해 Vue, React를 거쳐 Next.js까지, 각 전환점에서의 고민과 선택 과정을 솔직하게 담았습니다. ## AngularJS에서 Vue로: 통합 어드민의 시작 초기 버즈빌은 광고주(Demand)와 매체사(Supply)를 위한 두 개의 AngularJS 어드민을 별도로 운영했습니다. 각각 다른 앱으로 개발되어 캠페인 관리, 리포팅 같은 기본 기능들이 중복 구현되어 있었습니다. AngularJS 1.x의 복잡한 구조 때문에 새로운 요구사항에 대응하기도 점점 어려워졌습니다. 기술 부채가 쌓이자 프레임워크 전환과 함께 두 어드민을 통합하기로 결정했습니다. React도 고려했지만, 프론트엔드 뿐만 아니라 백엔드 개발자가 동시에 사용하는 프로젝트인데 당시에는 React가 Class형 컴포넌트만 쓸 때였는데 Vue가 러닝커브가 낮고 AngularJS의 템플릿 기반 개발 경험과도 자연스럽게 이어지는 장점이 있었습니다. 쉽지만은 않았지만 Vue로의 전환을 끝낸 뒤 Vue 전환 과정에서 중복된 코드를 정리하고 공통 기능을 모듈화하면서 코드베이스가 절반으로 줄었습니다. 개발 속도도 눈에 띄게 향상됐고, 무엇보다 하나의 통합 어드민에서 Demand와 Supply 기능을 함께 관리하니 일관성 있는 사용자 경험을 제공할 수 있게 되었습니다. ## BFF 도입: 프론트엔드를 위한 백엔드 초기에는 서버 개발자와 협업하기 위해 Vue 프로젝트 위에 Python Django 레이어를 추가하여, 프론트엔드와 백엔드 간의 통신을 Django를 통해 처리했습니다. 이를 통해 버즈빌의 백엔드 서버와 안정적으로 연동할 수 있었죠. 하지만 당시 버즈빌은 MSA(Microservice Architecture)를 채택하고 있었기 때문에, 프론트엔드는 하나의 백엔드가 아니라 수많은 마이크로서비스들과 통신해야 했습니다. 이러한 구조에서 Django는 초기에는 유용했지만, 시간이 지나면서 프론트엔드 개발자가 직접 Python으로 Django 코드를 관리해야 했고, 이는 프론트엔드 개발자가 익숙하지 않은 백엔드 기술 스택까지 다뤄야 한다는 부담으로 이어졌습니다. 결과적으로 Django 레이어는 오히려 개발 속도를 늦추는 요인 중 하나가 되었습니다. 이를 해결하기 위해 BFF(Backend For Frontend) 레이어를 도입했습니다. Node.js Express 서버를 Django와 프론트엔드 사이에 두고, 프론트엔드 팀이 직접 필요한 형태로 API를 설계할 수 있게 한 것이죠. 여러 Django API를 조합해 프론트엔드에 최적화된 응답을 만들거나, 인증/캐싱 같은 공통 로직을 한 곳에서 처리할 수 있게 되었습니다. 물론 서비스 레이어가 늘어난 만큼 모니터링 포인트도 늘었지만, APM 도구를 도입해 안정성을 확보했고, 결과적으로 프론트엔드 개발 속도가 크게 향상되었습니다. ## WebView + React: Native SDK의 대안 버즈빌은 파트너 앱에 광고 지면을 제공하는데, Native SDK 방식을 사용했습니다. 하지만 새 기능을 추가하려면 파트너사가 SDK를 업데이트하고 앱을 재배포해야 했죠. 앱 출시 주기가 길거나 업데이트 정책이 보수적인 파트너사의 경우 개선사항 반영이 2년 이상 걸리기도 했습니다. 이 문제를 해결하기 위해 WebView 기반 React SPA로 전환했습니다. 파트너 앱은 WebView만 띄우고, 그 안에서 React 앱이 동작하는 구조입니다. 당시에 처음 시도하는 Webview 지면 연동이어서, 많은 시행착오를 겪었지만 이 이야기를 다 쓰려면 또 글이 길어질거 같아서, 시간 나면 다음 포스팅에서 적어보려고 합니다. 결과적으로 지면이 성공적으로 런칭되어, 함께 고생한 팀원들과 워크숍도 다녀왔습니다. ![용돈 모으기](https://tech.buzzvil.com/blog/buzzvil-frontend-history/%ec%9a%a9%eb%8f%88%eb%aa%a8%ec%9c%bc%ea%b8%b0.jpg) ## Turborepo로 모노레포 구축 WebView 전략이 성공하면서 웹뷰를 통한 광고 서빙에 대한 니즈가 늘어나게 되었습니다. 그렇게 되어 프로젝트가 늘어날수록 각각의 리포지토리에서 설정 관리, 공통 컴포넌트 동기화, 패키지 버전 관리가 점점 복잡해졌습니다. 이 시점에 Turborepo를 도입해 모노레포 구조로 전환했습니다. Lerna, yarn workspace 등 여러가지 옵션이 있었지만 Vercel이 시장 파이를 점점 넓혀가고 있었고, FE개발자가 많지 않은 상황에 강력한 기능보다는 생산성이 더 중요했기 때문에, 당시 Zero configuration을 지향하던 Turborepo를 선택했습니다. 모든 WebView 프로젝트를 하나의 리포지토리에서 관리하고, 공통 UI 컴포넌트와 유틸리티는 `packages/`에 모아 각 프로젝트에서 import해 사용하는 구조죠. ![공통 패키지](https://tech.buzzvil.com/blog/buzzvil-frontend-history/packages.png) 초기에는 기존 리포지토리들을 합치면서 Git 이력을 보존하는 작업이 까다로웠고, Turborepo 의존성 문제에 많은 시간을 쓰기도 했습니다. 하지만 지금은 10개 이상의 프로젝트를 효율적으로 관리하며 개발자 경험을 크게 개선했습니다. ## Server-Driven UI로 유연성 확보 파트너사마다 원하는 UI 레이아웃이 달랐고, 새로운 디자인을 실험할 때마다 클라이언트 배포가 필요했습니다. 이를 해결하기 위해 Server-Driven UI 아키텍처를 도입했습니다. UI를 작은 모듈로 나누고, 서버에서 JSON 형태로 "어떤 컴포넌트를 어떤 순서로 배치할지" 지시하면 클라이언트가 이를 해석해 화면을 구성하는 방식이죠. 예를 들어 A 파트너는 리스트형 레이아웃을, B 파트너는 그리드형 레이아웃을 원한다면, 서버 설정만 바꿔 즉시 적용할 수 있습니다. 이 접근법은 강력했지만 복잡도도 높았습니다. 모든 가능한 조합을 고려한 설계가 필요했고, 특정 컴포넌트 조합에서 UX 문제가 생기지 않도록 검증 로직도 필요했죠. 운영팀을 위한 설정 편집 도구와 미리보기 기능도 개발해야 했습니다. 이러한 투자 덕분에 현재는 코드 배포 없이도 다양한 실험과 커스터마이징이 가능해졌습니다. ## Next.js 도입: 팀의 표준 스택으로 React 프로젝트가 늘어나면서 React App의 한계를 느끼기 시작했습니다. SSR이 필요한 경우 별도 설정이 복잡했고 많은 고객사에서 성능에 대해 민감했는데 이를 충족시키기가 어려웠습니다. Next.js로 전환하면서 많은 것이 해결되었습니다. SSR을 이용해 기본적인 Server driven UI 값들을 미리 불러올 수 있게 되어 초기 로딩이 빨라졌고, 특히 WebView 환경에서 체감 성능이 크게 개선되었습니다. API Routes로 BFF 기능 일부를 Next.js 안으로 통합하면서 아키텍처도 단순해졌죠. 파일 기반 라우팅 덕분에 프로젝트 구조가 직관적이 되었고 개발 워크플로우가 한층 매끄러워졌습니다. 지금은 Next.js가 팀의 표준 프레임워크로 자리잡아 모든 신규 프로젝트에 사용되고 있습니다. ## Hybrid SDK로 WebView-Native 통신 최적화 WebView 내 웹 앱은 네이티브 기능(외부 브라우저 호출, 뒤로가기 이동 등)을 호출해야 하고, 네이티브는 웹에 이벤트(웹뷰 호출데 대한 결과값)를 전달해야 합니다. 웹뷰 연동시 연동건마다 별도의 브릿지 코드를 작성했는데, 기능이 늘어나면서 관리가 어려워졌죠 특히나 고객사의 네티이브 개발자들과 커뮤니케이션 핑퐁에 많은 리소스가 소모되었습니다. 이를 해결하기 위해 Hybrid SDK를 개발했습니다. Android 및 iOS에서 통일된 인터페이스를 구축했죠. 웹에서는 `window.buzzvilSdk.postMessage(payload)` 하나로 모든 네이티브 기능을 호출할 수 있게 되었습니다. ![Hybrid SDK](https://tech.buzzvil.com/blog/buzzvil-frontend-history/interop.png) ## 다음 과제: Vue 어드민의 점진적 마이그레이션 현재 Vue 어드민은 안정적으로 운영되고 있지만, 팀의 주력 스택이 React/Next.js로 옮겨간 상황에서 기술 스택 분열은 부담이 되고 있습니다. 신규 개발자는 Vue와 React를 모두 익혀야 하고, 이는 이후에도 지속적으로 허들이 되고 있습니다. 하지만 운영 중인 대규모 어드민을 한 번에 전환하는 것은 위험합니다. 그래서 점진적 마이그레이션 전략을 세웠습니다. 먼저 신규 기능은 모두 React로 개발하고, 기존 Vue 페이지와 iframe으로 연결합니다. 마이크로 프론트엔드와 비슷한 구조로 독립적인 페이지부터 하나씩 React로 재작성하면서, 과도기적으로 마이크로 프론트엔드 아키텍처로 두 프레임워크를 공존시킬 계획입니다. 앞으로 2-3년 내 모든 어드민을 Next.js 기반으로 통합하는 것이 목표입니다. ## 마치며 AngularJS에서 시작해 Vue, React를 거쳐 Next.js까지, 버즈빌 프론트엔드 팀의 기술 여정을 공유했습니다. 돌이켜보면 각 전환점마다 나름의 이유와 고민이 있었고, 완벽한 선택은 없었습니다. 중요한 것은 현재 상황에서 최선의 선택을 하고, 문제가 생기면 과감하게 개선하는 자세였습니다. BFF로 개발 속도를 높이고, WebView로 배포 유연성을 확보하고, 모노레포로 관리 효율성을 개선한 것처럼요. 앞으로도 새로운 기술과 도전이 기다리고 있을 것입니다. WebAssembly, AI 기반 개발 도구 등 흥미로운 기술들이 계속 등장하고 있죠. 하지만 기술 자체보다는 "우리 팀과 서비스에 정말 필요한가?"를 먼저 묻는 자세를 유지하려 합니다. 비슷한 고민을 하는 개발자분들께 이 글이 작은 도움이 되었기를 바랍니다. 기술 선택에 정답은 없지만, 다른 팀의 경험에서 인사이트를 얻을 수는 있으니까요. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [AWS 비용 최적화 Part 1: 버즈빌은 어떻게 월 1억 이상의 AWS 비용을 절약할 수 있었을까](https://tech.buzzvil.com/blog/aws-cost-optimization-part-1) Date: 2024-04-29 | Author: Zune Seo | Category: DevOps 버즈빌은 2023년 한 해 동안 월간 약 1.2억, 연 기준으로 14억에 달하는 AWS 비용을 절약하였습니다. 그 경험과 팁을 여러 차례에 걸쳐 공유합니다. - **AWS 비용 최적화 Part 1: 버즈빌은 어떻게 월 1억 이상의 AWS 비용을 절약할 수 있었을까** - (준비중) AWS 비용 최적화 Part 2: 쉬우면서도 효과가 큰 비용 최적화 팁 - (준비중) AWS 비용 최적화 Part 3: 데이터 전송 비용의 비밀 ## 개요 버즈빌은 2022년 12월 약 3.6억에 달하는 AWS 비용이 발생하고 있었습니다. 지난 한 해 많은 노력을 기울인 끝에 2023년 12월 기준 전년 대비 2/3 수준인 월 2.4억으로 AWS 비용을 최적화했습니다. 이번 포스팅에서는 비용 최적화를 성공적으로 이끌어내기 위해 어떠한 방식의 접근을 취했는지를 공유합니다. ## AWS 비용 최적화 과제 시작의 이유 리워드 광고 플랫폼을 운영하는 버즈빌은 2013년 첫 서비스를 시작한 이래로 연간 매출이 600억원 가까이 될 정도로 빠르게 성장해왔습니다. 제품 개발 속도와 비용 최적화 사이에는 트레이드오프 관계가 존재합니다. 많은 상황에서 비용이 더 들더라도 제품 개발 속도를 빠르게 하는 방향으로 의사결정을 해오면서 AWS 비용이 지속해서 상승해왔습니다. 버즈빌은 탄탄하게 매출을 만들어오고 있는 회사이지만 경기 침체가 계속되고 스타트업 투자 시장이 얼어붙는 상황에서 오랜 기간 버틸 수 있는 기초 체력을 다지기 위해서 큰 비중을 차지하는 AWS 비용 최적화가 필요하다는 판단을 내렸습니다. ## 목표 정하기 앞서 언급했듯이 비용과 제품 개발 속도는 트레이드오프 관계에 있습니다. 어느 한 쪽으로 지나치게 치우치기보다는 적절한 균형점을 찾는 것이 중요합니다. 그러면 어느 정도까지 비용을 최적화하는 것이 필요한지 객관적인 기준이 필요했습니다. 이는 재무팀에서 명확하게 제시해 주셨습니다. 바로 매출액 대비 인프라 비용이었습니다. 월마다 어느 정도 변동성이 있긴 했지만, 2021년 대비하여 2022년에는 매출액 대비 인프라 비용이 두 배 가까이 증가했습니다. 이를 원상태 이하로 효율화하는 것을 목표로 세웠습니다. 버즈빌을 담당해 주시는 AWS 계정 매니저님께서도 일반적인 SaaS 기업의 매출 대비 인프라 비용은 4% ~ 8% 수준이라는 의견을 주셨습니다. 버즈빌이 위험한 구간에 들어가지 않고 일반적인 SaaS 기업보다 더 효율적으로 운영되도록 하는 것을 목표로 하였습니다. ## 지표를 정하고 점수판 만들기 과제를 시작하기에 앞서 어떻게 하면 비용 최적화 목표의 동기부여를 더 잘 할 수 있을까 고민했습니다. 마침 회사에서 리더십 향상을 돕기 위해 "성과를 내고 싶으면 실행하라"라는 책을 선물받았습니다. ![성과를 내고 싶으면 실행하라](https://tech.buzzvil.com/blog/aws-cost-optimization-part-1/book.jpg) 책에서는 4가지 실행 원칙을 제시하고 있습니다. - 원칙 1: 가장 중요한 목표에 집중하라. - 원칙 2: 선행지표에 따라 행동하라. - 원칙 3: 점수판의 강점을 활용하라. - 원칙 4: 책무를 서로 공유하라. 여기서 특히 원칙 3번이 가장 기억에 남았습니다. 지금 내가 이기고 있는지 지고 있는지를 점수판을 통해 쉽게 확인할 수 있어야 더욱 동기부여를 받고 목표를 달성할 수 있다고 합니다. 예를 들어 농구장에서 친구들이 점수를 세지 않고 게임을 하고 있을 때 누군가가 점수판을 갖다놓고 점수를 세기 시작하면 게임에서 이기기 위해서 더 열심히 하게 된다는 것입니다. 생각해 보니 저도 그런 경험이 있었던 것 같습니다. 그래서 점수판을 만들어야겠다고 생각했습니다. 그러고 나니 점수판의 점수를 무엇으로 할지 고민이 되었습니다. 처음에는 실제 청구된 AWS 비용을 점수판의 지표로 사용하려고 했습니다. 이는 우리가 달성하고자 하는 최종 아웃풋 지표라고 볼 수 있습니다. 하지만 이 지표는 한 가지 문제가 있습니다. 그것은 내가 아무리 비용 최적화를 잘 해도 트래픽이 상승하는 등 다른 이유로 비용이 증가하게 되면 노력한 결과가 잘 드러나지 않을 수 있다는 것입니다. 따라서 내가 한 노력이 그대로 드러나며 그 결과가 목표 달성에 중요한 영향을 미치는 지표인 인풋 지표가 필요했습니다. 그래서 각 과제를 수행할 때마다 수행하기 전/후 시점에 해당 서비스에서 청구된 비용 변화를 측정하고 이를 월 기준으로 환산한 절약 금액을 사용하기로 했습니다. 점수판에서는 누적된 절약 비용을 빨간색 선으로 표시하고 12월까지 10만 달러 절약 과제 수행을 목표로 표기했습니다. ![인풋 지표](https://tech.buzzvil.com/blog/aws-cost-optimization-part-1/input-metric.png) 이제 게임에서 지고 있는지(빨간선이 파란선 아래 위치하는지), 게임에서 이기고 있는지(빨간선이 파란선 위에 위치하는지) 쉽게 알 수 있는 상태가 되었습니다. 11월 이후 큰 폭으로 비용 최적화 과제가 수행된 것을 볼 수 있는데요. 이 점수판은 연말에 막판 스퍼트를 하는 데 동기 부여가 되었습니다. 결과적으로 1년간 약 100개의 과제를 수행하고 목표했던 10만 달러 절약 과제를 초과 달성하여 10.6만 달러의 과제를 수행했습니다. 실제로 줄어든 비용은 8.8만 달러로, 진행한 과제의 80% 수준으로 비용이 줄어들었습니다. 이를 통해 실제로 인풋 지표와 아웃풋 지표 사이에 차이가 있는 것을 알 수 있었고, 인풋 지표를 활용하는 것이 적절한 선택이었다는 것을 다시 한 번 확인했습니다. 참고로 위의 지표는 스프레드시트에서 과제가 수행될 때마다 일일이 수기로 입력하여 관리했습니다. 개발자로서 뭔가를 자동화하고 싶다는 욕구가 치솟았지만 수동으로 관리하는 것도 충분히 효율적인 방식이었습니다. ![스프레드 시트 관리](https://tech.buzzvil.com/blog/aws-cost-optimization-part-1/spreadsheet.png) ## 드릴 다운 자, 이제 점수판도 만들었습니다. 그러면 이제 실제로 과제를 수행해야 합니다. 어떤 과제를 수행해야 하는지 판단하기 위해서는 어디서 비용이 발생하고 있는지 다양한 수준에서 데이터를 쪼개 봐야 합니다. 버즈빌은 MSP인 메가존을 통해서 AWS를 이용하고 있습니다. 이 경우 안타깝게도 AWS에서 제공하는 Cost Explorer를 사용할 수 없습니다. 하지만 메가존에서 제공하는 비용 분석 대시보드인 하이퍼빌링의 Cost Pivot 기능만 활용해도 아래와 같이 여러 가지 기준으로 드릴 다운을 통해 비용을 분석할 수 있어 부족함이 없었습니다. ![인풋 지표](https://tech.buzzvil.com/blog/aws-cost-optimization-part-1/hyperbilling.png) 가장 먼저 확인해야 할 숫자는 서비스별 비용입니다. 서비스별 비용을 보면 어디부터 최적화하는 것이 기대 효과가 큰지 일차적인 판단을 할 수 있습니다. 다음은 버즈빌의 주요 서비스에 대한 실제 비용 변화를 표로 작성하였습니다. |서비스|2022년 12월|2023년 12월|비용 변화| |---|---|---|---| |AmazonEC2|$75,104|$58,959|-21.50%| |AWSDataTransfer|$71,908|$35,869|-50.12%| |AmazonDynamoDB|$41,093|$19,750|-51.94%| |AmazonS3|$25,253|$20,595|-18.45%| |AmazonRDS|$25,122|$11,862|-52.78%| |AmazonAthena|$7,501|$9,408|25.42%| |AWSELB|$6,862|$3,606|-47.45%| |AmazonCloudWatch|$5,499|$4,278|-22.20%| |AmazonCloudFront|$3,485|$5,997|72.08%| 비용에서 가장 큰 비중을 차지하는 쌍두마차는 EC2와 데이터 전송 비용인 것을 알 수 있습니다. 데이터 전송 비용이 EC2 비용과 비슷한 수준이면 너무 많이 나가는 것이 아닐까라는 생각을 하셨을 것입니다. 실제로도 데이터 전송 비용에 최적화할 부분이 많았고, 2023년 12월에는 50%가량 감소한 것을 알 수 있습니다. 버즈빌에서 주력 데이터베이스로 활용하고 있는 DynamoDB와 RDS MySQL에서도 많은 비용이 발생하고 있었고, 마찬가지로 50% 가량 최적화를 진행하였습니다. 각 서비스에서 발생하는 비용을 좀 더 자세히 분석하기 위해서는 Cost Pivot에서 제공하는 기능을 활용해 데이터를 드릴 다운해서 봐야 합니다. 어떤 기준으로 드릴 다운하는 것이 유용한지는 AWS 서비스에 따라서, 그리고 각 회사가 어떤 패턴으로 해당 서비스를 사용하고 있는지에 따라서 다르기 때문에 직접 여러 가지 방법으로 시도해 봐야 합니다. 그래도 가장 유용한 기준이 무엇인지 하나만 고른다면 바로 Lineitem Description입니다. 적당히 세부적인 레벨에서 비용을 구분해서 보여주며 말 그대로 어떤 비용인지 이해하기 쉽게 글로 기술되어 있습니다. 예를 들어 DynamoDB의 경우 다음과 같은 수준으로 데이터를 구분해서 볼 수 있습니다. ![DynamoDB](https://tech.buzzvil.com/blog/aws-cost-optimization-part-1/dynamodb.png) 그 외에 각 서비스 별로 주로 활용했던 기준은 다음과 같습니다. ### AmazonEC2 - Name: 인스턴스 생성시 붙여놓은 이름 기준으로 비용을 볼 수 있습니다. - Operation: 인스턴스 비용, 스토리지 비용, NatGateway 비용 등을 구분해서 볼 수 있습니다. ### AWSDataTransfer - Product Cateogory: 멀티 리전으로 운영중인 서비스의 경우 리전 간 통신 비용을 구분해서 볼 수 있습니다. - Operation: 로드 밸런서에서 발생하는 비용, S3에서 발생하는 비용, 존 간의 통신 비용 등을 구분해서 볼 수 있습니다. - Resource ID: 데이터 전송 비용이 발생한 ELB 인스턴스, EC2 인스턴스, S3 버킷 등을 구분해서 볼 수 있습니다. ### AmazonDynamoDB - Resource ID: 테이블 별로 비용을 구분해서 볼 수 있습니다. - Usage Type: 읽기 비용, 쓰기 비용, 스토리지 비용을 구분해서 볼 수 있습니다. ### AmazonS3 - Resource ID: 버킷 별로 비용을 구분해서 볼 수 있습니다. - Operation: S3 API 별 호출 비용, 스토리지 클래스에 따른 비용을 볼 수 있습니다. ### AmazonRDS - Resource ID: 인스턴스 별로 비용을 구분해서 볼 수 있습니다. - Usage Type: 인스턴스 비용과 스토리지 비용을 구분해서 볼 수 있습니다. ## 숫자에 대한 감각 키우기 드릴 다운을 통해 숫자를 분석하다 보면 "어, 이거 좀 이상한데?"라는 생각이 드는 경우가 있습니다. 그리고 그 부분에 대해서 최적화를 했을 때 어느 정도의 기대 효과가 있는지도 추정할 수 있어야 합니다. 이를 위해서는 각 서비스별로 비용 발생 구조를 이해하고 그 숫자가 어떻게 되는지 기억하고 있는 것이 유리합니다. 특히 상대적인 비용 차이가 어떻게 되는지를 알면 도움이 되는 경우가 많았습니다. 이것이 무엇을 의미하는지는 다음과 같은 재미있는 사례를 통해 설명드립니다. ### 데이터 전송 비용은 생각보다 비싸다 버즈빌은 S3에 적재된 데이터를 분석하기 위한 쿼리 엔진으로 Athena를 주로 활용하고 있습니다. Athena는 쿼리를 실행했을 때 스캔한 데이터량에 따라 비용이 발생합니다. 1TB 데이터를 스캔하면 5달러의 비용이 발생합니다. 버즈빌에서는 누구나 데이터 분석을 스스로 할 수 있도록 장려하고 있기에 많은 분들이 직접 Athena 쿼리를 통해 데이터를 분석하고 있습니다. 그러다 보니 내가 실행하는 쿼리가 얼마나 비용을 발생시키는지를 알고 있어야 하고, 1TB = 5달러라는 공식은 모두가 기억하고 있는 중요한 숫자가 되었습니다. 버즈빌은 대부분의 인프라가 도쿄 리전에 존재합니다. 그런데 데이터 분석 인프라는 오레곤 리전에 있습니다(여기에는 그 당시 어쩔 수 없던 사연이 있습니다). 그래서 도쿄 리전에서 생성된 데이터를 오레곤 리전으로 복사한 뒤 Athena를 활용해 데이터를 분석하고 있습니다. 자, 그렇다면 도쿄 리전에서 오레곤 리전으로 1TB의 데이터를 전송하는 데 드는 비용은 얼마일까요? ![Tokyo to Oregon data transfer cost](https://tech.buzzvil.com/blog/aws-cost-optimization-part-1/tokyo_to_oregon.png) 공식 홈페이지에 "$0.9 per GB" 라고 되어 있습니다. TB로 환산하면 다음과 같이 되겠네요. 1TB = $90 그렇습니다. Athena 스캔 비용의 18배에 달하는 비용이 데이터 전송에서 발생하고 있던 것입니다. 이를 통해 데이터를 다른 리전으로 옮겨서 처리하는 것이 얼마나 큰 비용을 발생시키는 결정이었는지 깨닫게 되었습니다. 사실 "$0.9 per GB"라는 숫자는 이전에도 많이 봤던 숫자였습니다. 다만 이전에는 그게 실제로 무슨 의미인지 몰랐다면, Athena 비용과 같은 기준에서 비교한 뒤로는 그 의미를 좀 더 구체적으로 알 수 있게 된 것입니다. 바로 이것이 숫자에 대한 감각이라고 생각합니다. 그래서 다른 AWS 비용도 TB 기준으로 환산해 보았습니다. 도쿄 리전 기준입니다. - AWS 밖으로 데이터를 전송하는 비용: $114 - ALB 데이터 처리 비용: $8 - NAT Gateways 데이터 처리 비용: $62 - S3 스탠다드 스토리지 비용: $25 - RDS MySQL 스토리지 비용: $138 - DynamoDB 스토리지 비용: $285 이를 통해 NAT Gateways 데이터 처리 비용이 꽤 비싸다는 것, RDS MySQL에 있는 데이터를 S3으로 옮겨서 백업해두면 1/5도 안되는 가격으로 보관이 가능하다는 것 등을 알 수 있습니다. ### DynamoDB는 쓰기 비용이 읽기 비용보다 5배 비싸다 ![DynamoDB read write cost](https://tech.buzzvil.com/blog/aws-cost-optimization-part-1/dynamodb_read_write.png) 공식 문서를 보면 소수점 아래로 숫자가 많아서 읽기 어렵지만, 쓰기 비용이 읽기 비용보다 정확히 5배 비쌉니다. 따라서 이를 고려하여 최적화를 해볼 수 있습니다. 예를 들어 인덱스를 추가할 때 읽기 비용은 감소하고 쓰기 비용은 증가하게 되는데요. 이 때 위의 사실을 고려해야 합니다. ### 약정 구매를 통해 25%, 50%, 75% 할인을 받을 수 있다. 서비스마다 숫자가 조금씩 다르지만, 약정 구매를 했을 때 할인받을 수 있는 범위가 크게 두 가지 타입으로 나뉩니다. - 1년 25%, 3년 50% 할인 타입: EC2, RDS, Elasticache - 1년 50%, 3년 75% 할인 타입: DynamoDB, Redshift 따라서 3년 약정 시 75%까지 할인이 되는 DynamoDB나 Redshift의 경우 약정 구매를 더 적극적으로 적용하는 전략을 취할 수 있습니다. ## 마무리 이번 포스팅에서는 AWS 비용 최적화를 시작하는 단계에서 점수판을 만들고, 수행해야 할 과제를 도출하기 위해 드릴 다운을 통해 비용 데이터를 분석하는 방법을 말씀드렸습니다. 이어지는 포스팅에서는 각 서비스별로 조금 더 구체적인 사례를 공유드릴 예정입니다. 많은 기대 부탁드립니다 :) --- ## [데이터 엔지니어의 Airflow 데이터 파이프라인 CI 테스트 개선기](https://tech.buzzvil.com/blog/데이터-엔지니어의-데이터-파이프라인-ci-테스트-개선기) Date: 2024-04-18 | Author: Abel Yoon | Category: Data & ML ## 들어가며 안녕하세요, 버즈빌 데이터 엔지니어 Abel 입니다. 이번 포스팅에서는 데이터 파이프라인 CI 테스트에 소요되는 시간을 어떻게 7분대에서 3분대로 개선하였는지에 대해 소개하려 합니다.

## 배경 이전에 버즈빌의 데이터 플랫폼 팀에서 ['셀프 서빙 데이터 플랫폼 구축하기'](https://tech.buzzvil.com/blog/%EC%85%80%ED%94%84-%EC%84%9C%EB%B9%99-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0/) 에 대해 소개해 드렸었습니다. 해당 플랫폼에 데이터 파이프라인을 추가하는 방법에 대해 요약하자면 먼저 형식에 맞게 YAML 파일을 작성한 다음 깃허브를 통해 해당 YAML 파일에 대한 Pull Request를 생성한 후 배포하여 데이터 파이프라인을 추가하였습니다. 해당 과정 중에서 사람의 실수를 감지하고자 Pull Request 생성 시 자동으로 CI 테스트가 동작하게 됩니다. 하지만 해당 CI 테스트 과정은 저희 팀에서만 사용하는 것이 아닌 데이터 파이프라인을 생성하고자 하는 분들 모두에게 적용되는 만큼 다른 CI 테스트들보다 영향 범위가 넓었습니다. 이에 해당 CI 테스트를 개선하여 데이터 파이프라인을 관리하는 모든 분의 개발 효율을 향상해 보고 싶은 생각이 있었지만 다른 업무들에 비해 우선순위가 낮아 실제로 진행되지 못하고 있던 업무였습니다. 그러던 와중 잠시 기존 업무를 내려놓고 쌓여있던 기술 부채를 해소하고 업무 효율을 개선하는 버즈빌만의 벨로시티 스프린트라는 2주간의 시간이 주어져 CI 테스트 개선 작업을 진행할 수 있게 되었습니다. ## 데이터 파이프라인 CI 의 상태 우선 어떻게 CI 테스트를 개선할 것인가를 답하기 위해서는 현재 우리의 CI 테스트는 어떤 단계를 통하여 실행되고 있는가에 대한 파악이 필요합니다.
문제-제보-슬랙
위 사진은 저희 CI 테스트 실행 순서를 보여줍니다. 우선 컨테이너 이미지 레지스트리(Registry)에 업로드하기 위한 인증 단계인 login_ecr 과 실제로 이미지를 빌드하는 build_image 단계가 성공해야 합니다. 두 단계 모두 성공한다면 이후 flake8 을 통하여 코드 스타일을 검사하고 단순한 오류를 탐지하는 lint 단계와 테스트 코드를 실행시켜 주는 mypy 단계가 실행됩니다. 여기에 추가로 데이터 파이프라인 상에서 사용되는 쿼리가 추가되거나 수정된 파일만 추출해 주는 athena-process-changeset 단계가 성공하면 해당 단계에서 추출된 쿼리들의 문법 오류가 없는지 검증하는 pytest-and-validate-athena-query 단계가 실행됩니다. 그리고 모든 단계가 성공적으로 완료되어야 테스트 결과를 알리는 ci_required 단계가 실행되며 CI 테스트가 종료됩니다. 해당 실행 순서와 실행되는데 걸리는 시간을 통하여 우리는 어떤 단계들을 개선해야 할지 알 수 있습니다. 예를 들어 ci_required 가 실행되기 위해선 lint, mypy 와 pytest-and-validate-athena-query 단계가 모두 완료되어야 하기 때문에 가장 오래 걸리는 단계가 끝나기 전까지는 다른 단계들이 얼마나 빨리 끝나는지와는 전혀 무관합니다. 따라서 lint 와 mypy 단계는 pytest-and-validate-athena-query 가 개선되어 해당 단계들보다 소요 시간이 적어지기 전까진 개선 필요성이 매우 낮아지게 됩니다. 이러한 점들을 고려하면 현재 개선 필요성이 높은 단계는 build_image 와 pytest-and-validate-athena-query 입니다.

## 개선 작업 ### build_image 단계 #### 빌드 캐싱 안정화 데이터 파이프라인 리포지토리(Repository)에서는 CI 테스트용과 배포용 이미지로 두 가지 이미지를 빌드하고 있습니다. 이 중 CI 테스트 개선과 관련된 CI 테스트용 이미지 빌드 단계를 살펴보았을 때 빌드마다 빌드에 걸리는 시간의 편차가 매우 커서 최소와 최대의 차이가 2배 이상 나는 상황이었습니다. 시간이 오래 걸린 케이스들에서 이미지 빌드 로그를 보았을 때 변경사항이 전혀 없는 이미지 레이어가 캐싱이 되지 않아 다시 빌드 과정을 거치고 있는 상태였습니다. 바로 캐시 데이터를 관리하는 리포지토리를 확인해 보았으나 해당 브랜치의 이미지에 대한 캐시 데이터가 정상적으로 업로드되어 있는 것을 확인했습니다. 이때, 캐시 데이터에 붙은 태그를 보고 데이터 파이프라인에서 관리되는 이미지에서 캐시 데이터 문제의 원인을 깨달았습니다. 위에 현상에서 설명한 것과 같이 데이터 파이프라인 리포지토리에서는 두 가지 이미지를 빌드하고 있습니다. 하지만 이미지 캐싱은 브랜치 단위로 관리되고 있었고 이로 인해 CI 테스트용 이미지와 배포용 이미지 중 먼저 업로드된 캐시 데이터가 늦게 업로드된 캐시 데이터에 의해 덮어씌워져 먼저 빌드 된 이미지의 경우 그다음 빌드 시 캐시 데이터를 사용할 수 없는 상황이었습니다. 간헐적으로 CI 테스트용 이미지가 캐싱 없이 빌드를 하게 되는 상황으로 인해 CI 테스트에 걸리는 시간의 편차가 심하였습니다. 이를 개선하고자 브랜치, 도커파일명, 빌드 타깃을 기준으로 캐시 데이터를 관리할 수 있도록 변경하였고 이를 통하여 build_image 단계에 소요되는 시간이 안정되었습니다.
문제-제보-슬랙
안정화 이전
문제-제보-슬랙
안정화 이후
#### 의존성 개선 데이터 파이프라인 이미지 빌드 과정에서는 캐시 데이터 다운로드 → 이미지 빌드 → 이미지 푸시 → 캐시 데이터 생성 → 캐시 데이터 푸시 순서로 진행되었습니다. 이미지와 캐시 데이터를 따로 관리하는 이유는 이미지만 빌드 완료되면 바로 배포가 될 수 있도록 하고 이후 캐시 데이터를 관리하기 위함이었습니다. 하지만 CI 테스트에서는 이미지 빌드 과정 전체에 대한 의존성을 띠고 있었고 이 때문에 캐시 데이터 푸시까지 완료된 이후 CI 테스트가 진행되었습니다. 이를 개선하고자 배포 시 감지하는 방식과 동일한 방식을 CI 테스트에 적용하여 이미지 빌드 과정에 의존성을 두는 것이 아닌 이미지 푸시 여부에 의존성을 주었고 이를 통해 캐시 데이터와 관련된 처리 시간을 절약할 수 있었습니다. ### pytest-and-validate-athena-query 단계 #### 불필요한 커맨드 제거
문제-제보-슬랙
해당 단계를 살펴보면 우선 airflow의 dag를 실행시키기 위한 airflow 초기화 과정 이후 테스트를 실행시키고 데이터 파이프라인 과정 속에서 사용하는 쿼리들이 문법적으로 문제가 없는지 검증하는 과정을 거칩니다. 이때, 실제로 테스트를 하는 시간은 1분도 채 안 되지만 환경 설정에만 1분 40초를 소모하게 됩니다. 해당 커맨드의 경우 airflow.db라는 sqlite 용 파일 하나를 생성하게 되는데 해당 파일의 경우 Airflow 버전에 따라서 변경되게 되며 동일한 버전에서는 모든 CI 테스트 과정 속에서 동일한 파일을 생성합니다. 이러한 특성을 기반으로 Airflow 버전에 기반한 캐싱을 할까 고민을 하였습니다. 하지만 Airflow 버전 업그레이드가 자주 발생하는 일이 아니었기 때문에 캐싱 시 해당 Airflow 버전을 알아내는 작업을 하는 것보다 해당 파일을 수동으로 관리하는 것이 설정 및 운영 난이도가 훨씬 낮았습니다. 이러한 결정을 통하여 리포지토리에 직접 airflow db init 의 결과물인 airflow.db 파일을 저장하였고 이를 통하여 CI 테스트 과정에서 제거하여 1분 40초의 시간을 절약하였습니다.
#### 병렬 실행 기존에 pytest 와 아테나 쿼리 검증 단계를 순차적으로 진행하였던 이유가 airflow db init 의 결과물을 사용해야 했기에 같은 환경을 공유해야 했고 아테나 쿼리 검증 단계에 소요되는 시간이 길지 않았기에 순차적으로 실행될 수 있게 해두었습니다. 하지만 airflow db init 의 결과물을 리포지토리에 저장해두었기 때문에 더 이상 같은 환경을 공유할 필요가 없어졌습니다. 이를 통하여 pytest 와 아테나 쿼리 검증 단계를 병렬로 처리하여 CI 테스트에 걸리는 시간을 단축시킬 수 있었습니다. ### mypy 단계 개선 pytest-and-validate-athena-query 단계가 개선되며 기존 3분 후반대에 걸리던 시간이 1분 중반까지 개선되었습니다. 이로 인해 pytest의 추가적인 개선보다 1분 후반대의 시간이 걸리는 mypy 개선 우선 순위가 높아져 mypy 개선 작업을 진행하였습니다. #### 캐싱 mypy의 경우 별다른 설정을 해주지 않아도 커맨드가 실행되는 디렉터리 하위의 .mypy_cache에 캐시 데이터를 관리합니다. 해당 경로를 깃허브 액션에서 지원하는 캐시 저장소를 통하여 브랜치 단위로 관리하였고 이를 통하여 1분 50초 대의 소요 시간을 1분 10초 대까지 개선하였습니다. 이때, 깃허브 액션의 캐시 저장소는 최대 10GB까지만 지원하며 용량 초과 시 과거 데이터는 삭제되며 7일 이상 접근되지 않은 캐시 데이터 또한 삭제되는 점을 유의하여 사용해야 됩니다. 예를 들어 수 GB의 이미지를 깃허브 액션 캐시 저장소를 통해 관리하는 경우 동시에 여러 브랜치에서 작업하는 경우 캐싱이 정상적으로 동작하지 않으며 오히려 캐싱 하는데 시간이 더 많이 소요될 수도 있습니다.

## CI 테스트 개선 결과 기존에 최대 7분이 걸리던 CI 테스트가 안정적으로 3분 중반대가 나오게 되며 약 50%의 개선을 이루어냈습니다. 이를 통하여 데이터 엔지니어뿐 아니라 데이터 파이프라인을 생성하고 관리하는 모든 분들의 업무 효율을 증가시킬 수 있었습니다.
문제-제보-슬랙


## 마무리하며 실제 CI 테스트 개선 작업을 진행한지 반 년 정도 지났는데 확실히 개선 전에 비해 CI 테스트를 대기한다고 소모되는 시간이 확실히 줄어들고 이를 통해 업무 집중의 질도 향상되었습니다. 업무를 해결하기 위한 과정을 개선하는 작업은 이후 업무 효율에만 도움이 될 뿐 아니라 지금까지 당연하게 여겼던 업무 과정을 다시 한번 전반적으로 점검하는 기회가 되었습니다. 이번에는 CI 테스트 개선을 중점으로 개선하였지만 또 다른 업무 과정 속 비효율을 개선해 나아가며 이에 대해 소개 드릴 수 있으면 좋을 것 같습니다. --- ## [Ad Management 파트 서버 개발자의 지역 타게팅 개선기](https://tech.buzzvil.com/blog/ad-management-geo-targeting-improvement) Date: 2024-03-21 | Author: Kay Lee | Category: Backend 안녕하세요. Demand Product 팀의 Ad Management 파트에서 서버 개발자로 일하고 있는 Kay입니다. 제가 팀에 합류한 지도 어느덧 2년이 되어가는데요, Ad Management 파트(이하 AdM)은 무슨 일을 하는지 간단하게 소개하고 재미있게 했던 과제에 관해 이야기해볼까 합니다.

#### 스프린트와 애자일 AdM은 2주 주기의 sprint 단위로 업무를 진행하고 있어요. 제가 들어오기 전 옛날 옛적 호랑이 담배피던 시절의 버즈빌은 펑션팀 단위로 일했다고 들었는데, 지금은 미션팀 단위로 일하고 있습니다. AdM팀은 PM, 디자이너, QA 매니저, 2명의 서버개발자와 1명의 프론트 개발자로 이루어져 있어요. 2주의 스프린트 단위가 길다면 길고, 짧다면 짧은 기간인데요, 이 기간동안 내가 어떤 일을 얼마나 할 수 있을지에 대한 판단이 정말 중요하게 작용하는 것 같아요. 스프린트 기간 동안 자신이 할 수 있는 범위의 일을 정의하는 것. 그게 애자일 방식의 핵심인 것 같다고 느꼈습니다.

#### 자유로운 커뮤니케이션 버즈빌은 정말 자유로운 커뮤니케이션 문화가 있습니다. 제가 신입이거나 시니어인 것은 상관이 없어요. 내 과제에 대해선 내가 의견을 낼 수 있고, 내 의견이 존중되는 문화입니다. 당연히 의견에 허점이 있거나 보완이 필요하면 상대방도 의견 제시를 할 수 있겠죠? 버즈빌에서는 더 나은 결과를 위한 의견 제시를 할 수 있는 모습과 받아들일 수 있는 모습, 두가지가 모두 필요합니다.

#### 오늘부터 1인분! 아직도 생생하게 기억납니다. 월요일에 입사하고 정신없는 입사절차를 밟고, 각종 온보딩을 듣고 난 후 화요일에 첫 파트 스크럼을 들어갔어요. 당시 팀에서 처리해야 하는 굉장히 쉬운 과제가 하나 있었는데, 팀 EM께서 눈을 반짝이며 이 과제는 케이가 진행하실 수 있을 것 같은데 어떤가요? 허허 라고 하시더라구요. 지금 생각해보면 온보딩용으로 적합한 과제였다는 생각이 들지만, 그 당시에는 조금은 당황스러웠던 기억이 있습니다. 당장 입사 다음날부터 실무에 뛰어들어야 하는 스타트업도 재밌지 않나요?

이쯤에서 제가 입사한 지 한달만에 맡아서 진행했던 과제에 대해 이야기해볼까 하는데요.
어떤 문제가 있었고 어떻게 해결했으며, 그 과정에서 팀 커뮤니케이션은 어떻게 했나 소개해 드리려고 합니다.

#### 문제 발견 버즈빌의 광고는 여러가지 항목을 타겟팅할 수 있습니다. 특정 나이대의 유저를 타겟팅할수도 있고, 특정 앱에만 광고를 내려줄 수도 있죠. 여러가지 타겟팅 방법 중 하나는 지역 타게팅입니다. 원하는 지역에만 광고를 내려줄 수 있는 기능인데요, 어느 날 이 기능에 결함이 있다는 제보가 들어옵니다.
문제-제보-슬랙
Fig 1. 문제 제보


먼저 지역타게팅을 어떻게 쓰고있는지 파악했습니다. 대한민국을 시,군,구 단위로 나눠보면, 약 250개의 지역이 있습니다. 약 70개의 시, 80개의 군, 100개의 구로 나뉘게 됩니다. 이 과정에서, 지역 타게팅 기능에 수십 개 군,구를 등록하게 되면 당시 사용하던 ElasticSearch 5(이하 ES5)라는 라이브러리의 packet size 제한을 넘어버리게 되는 문제가 발생했습니다. 지역타게팅에 사용하는 정보는 아주 정교한 좌표들의 모음이었는데, 지역을 많이 선택하니 데이터의 크기가 너무 커서 기본 설정인 1메가바이트를 넘겨버렸던 것이죠. 예시를 들어볼게요. 서울시 양천구의 지도는 좌측 하단의 사진처럼 생겼어요. 이걸 좌표로 나타내면 우측 하단처럼 생겼습니다. 양천구는 197개의 점으로 이루어져 있네요.

양천구-지도
Fig 2. 양천구 지도
양천구-좌표
Fig 3. 양천구 좌표


좌표 하나에 40바이트가 조금 안되는데, 주먹구구식으로 양천구만 계산해봐도 8천바이트가 나오네요. 양천구보다 데이터가 큰 지역도 많았기에, 1mb로 250개 지역을 모두 선택하기엔 부족한 수치라고 결론지을 수 있었습니다.

#### 해결책 가장 쉬운 방법은 역시 ES5의 패킷 제한을 늘려버리는 방법이었습니다. 하지만 광고시스템에서 가장 중요한 요소 중 하나는 빠른 시간내에 유저에게 광고를 전달하는 것입니다. 아무래도 아주 큰 데이터가 아니다보니 영향도는 미미하겠지만, 지역 정보의 통신 사이즈가 늘어나면 성능 저하의 가능성이 존재한다고 판단했습니다. 덤으로, ES5는 서비스를 종료하고 ES8으로 마이그레이션을 하는 계획도 있었습니다. 설정과 같은 다른 요소에 관계 없이 문제를 근본적으로 해결할 수 있어야 했다고 생각했고, 결국 해결책의 핵심은 저장하는 데이터의 크기를 줄이는 데 있다고 생각했습니다. 여러 가지 방법을 신중하게 검토해보았는데요, 약간의 트레이드 오프를 동반한다면 데이터의 크기를 획기적으로 줄일 방법이 있었습니다.

#### 외접다각형 convex hull을 한글로 표현하면 외접다각형입니다. 내부적으로는 Graham Scan(그레이엄 스캔)이라는 알고리즘을 사용해서, 점들의 집합에 대한 모든 점을 포함하는 외접다각형을 구하는 알고리즘이죠. 위에 예시로 든 양천구의 경우, convex hull을 적용한다면 197개의 점에서 약 20개의 점으로 줄어들게 됩니다. 데이터의 크기가 굉장히 많이 개선될 수 있는 수치였죠. 외접다각형을 구하는 원리를 모두 글로만 설명하려면 조금 복잡하고 어려울 수 있으니, 그래픽을 통해 Graham Scan을 최대한 간단하게 설명해보겠습니다. 그래픽으로 들어가기에 앞서 원리를 한 줄로 설명해보자면, 모든 점이 “가장 바깥쪽에 있다”라는 것을 증명하기 위해서는 이어지는 세 점의 집합이 모두 “좌회전”하면 됩니다. 수학적으로 설명하는 것보다, 실제로 알고리즘이 어떤 식으로 작동하는지 한번 살펴보는게 이해가 훨씬 쉬울 것 같아요. 함께 보시죠.
그레이엄-스캔-데모
Fig 4. 그레이엄 스캔 진행 예시
출처: 위키피디아 그레이엄 스캔 페이지


위 그림을 보면 알고리즘 진행순서를 한눈에 알 수 있는데요, 어떤 방식으로 저 선들이 정해지는지 step by step으로 설명해 드리고자 합니다. 먼저, 한 점을 기준으로 잡고 반시계방향으로 모든 점을 넘버링합니다. 보통 기준점은 y축의 가장 밑에 위치한 점으로 잡습니다. 그리고 스택이 하나 필요합니다. 일단 시작으로 기준점을 스택에 넣습니다.
algorithm1
Fig 5. 그레이엄 스캔 알고리즘 1

그 후, 1번 점을 스택에 넣습니다. 그리고 0에서 1로 이어지는 선을 그어줍니다.
algorithm2
Fig 6. 그레이엄 스캔 알고리즘 2

이제 2번 점을 비교할 차례입니다.
algorithm3
Fig 7. 그레이엄 스캔 알고리즘 3

0에서 1로 이어지는 직선을 A, 1에서 2로 이어지는 직선을 B라고 할 때, A에서 B로 가는 것이 “좌회전”을 한다면 해당 점들은 외접다각형의 일부라고 볼 수 있습니다. 0,1 그리고 2는 모두 저희가 찾는 외접다각형의 일부네요. 그렇다면 2를 스택에 넣고, 1->2와 2->3의 비교로 넘어갑니다.
algorithm4
Fig 8. 그레이엄 스캔 알고리즘 4

마찬가지로, 1->2와 2->3 직선의 각도의 방향을 계산합니다. 우리의 조건에 맞으니 3도 스택에 밀어 넣어줍니다. 이런 식으로 계속 번호를 바꾸면서 진행하면 됩니다. 똑같은 경우를 모두 설명할 필요는 없다고 생각되어서, 좌회전이 아닌 우회전을 하는 경우를 살펴보도록 하겠습니다. 다음 사진은 그레이엄 스캔을 쭉 진행하다가 4,5,6에 대한 직선 판별을 하는 경우입니다.
algorithm5
Fig 9. 그레이엄 스캔 알고리즘 5

스택에는 이미 5까지 들어가 있고, 6에 대해 판별을 할 차례입니다. 그러나 4→5의 직선과 5→6의 직선을 비교했을 때 각도가 바깥쪽으로 향하므로 우회전을 하네요. 그렇다는 것은 스택의 맨 위에 존재하는 5가 우리가 찾는 외접다각형의 일부가 아니라는 뜻입니다. 그러므로 5를 스택에서 빼줘야 합니다.
algorithm6
Fig 10. 그레이엄 스캔 알고리즘 6

이번에도 3→4의 직선과 4→6의 각도가 바깥으로 향하고 있네요. 그렇다면 4도 우리가 찾는 외접다각형의 일부가 아니라는 의미입니다. 스택에서 4를 제거합니다.
algorithm7
Fig 11. 그레이엄 스캔 알고리즘 7

이번에는 2→3, 3→6을 보니 각도가 안쪽으로 향해있는 것을 볼 수 있습니다. 그러면 6을 스택에 추가하고, 다음 점인 7로 넘어갑니다. 이후부터는 설명 없이 스텝별로 진행만 공유하도록 할게요.
algorithm8
Fig 12. 그레이엄 스캔 알고리즘 8


algorithm9
Fig 13. 그레이엄 스캔 알고리즘 9


algorithm10
Fig 14. 그레이엄 스캔 알고리즘 10


algorithm11
Fig 15. 그레이엄 스캔 알고리즘 11


algorithm12
Fig 16. 그레이엄 스캔 알고리즘 12

이렇게 모든 점을 다 판별하고 시작점으로 돌아왔을 때 알고리즘은 종료가 됩니다. 그리고 원하던 외접다각형의 점들의 집합 [0, 1, 2, 3, 6, 7]이 완성이 되었네요.

사실 이 알고리즘을 직접 작성할 수도 있지만, 파이썬의 *Shapely*라는 라이브러리를 사용하신다면 이미 구현되어 있는 [convex hull](https://shapely.readthedocs.io/en/stable/reference/shapely.GeometryCollection.html#shapely.GeometryCollection.convex_hull) 함수를 사용하실 수 있습니다. 이런 식으로 대한민국의 모든 지역구에 convex hull을 적용하였는데요, 변경점을 한 눈에도 쉽게 알아볼 수 있는 상태가 되었습니다.
geographic1
Fig 17. 좌: convex hull을 적용한 대한민국 지도. 우: 기존에 사용하던 대한민국 지도

이렇게 적용해보니, 대한민국 지역을 모두 저장해둔 데이터베이스 파일의 크기가 4.9mb에서 262kb로 약 94.7% 감소하였습니다. 가장 큰 지역의 데이터 크기도 3kb로 줄어들었기 때문에, 처음 문제가 되었던 1mb의 제한을 감안한다면 300개 이상의 지역을 타게팅에 추가할 수 있었습니다. 앞에서도 언급했듯이, 대한민국은 250개의 시군구로 이루어져 있기 때문에 사실상 제한이 없어졌다고 볼 수 있었습니다.

#### 문제점 한가지 분명한 단점도 존재했습니다. 사진에서도 보이다시피 지역별로 겹치는 부분이 있다는 점이었습니다. 겹치는 부분이 있다면 지역 타게팅이 정확히 동작하지 않을 수 있고, 광고 타게팅에 오차가 생길 수 있었습니다. 사실 지방의 경우 인구 밀도가 높지 않아 큰 문제가 되지는 않았는데, 서울은 조금 달랐습니다.
geographic2
Fig 18. 좌: convex hull을 적용한 서울의 지도. 우: 기존에 사용하던 서울의 지도

파일 크기를 90% 넘게 최적화했다고 하더라도 오차가 너무 커 기능에 문제가 생긴다면 소용이 없는 일이라서, 검증이 필요하다고 판단했습니다. 마침, 서초,강남,송파지역을 타게팅하던 광고가 있어 새로운 버전의 지역타게팅을 적용해보았습니다.
geographic3
Fig 19. 좌: 서초, 강남, 송파의 기존 타게팅. 우: 서초, 강남, 송파의 새로운 타게팅


해당 광고는 총 445,185번 할당이 되었는데, 그 중 타게팅이 아닌 지역인 강동구에서 발견된 할당이 약 29,419건이었습니다. 6.6%에 해당하는 수치였습니다. 또 다른 ‘대전’타게팅이 걸려있던 광고에 대한 할당 지역정보를 확인해보니 대전이 아닌 지역의 할당이 0.5%수준의 오차만 존재하는 것도 확인할 수 있었습니다. 이전에 집행되었던 다른 광고들을 살펴보니, 기존에도 지역이 아닌 곳에서 할당되는 경우는 약 1~2퍼센트정도는 존재했습니다. 즉, 최대 4~5%정도의 오차율을 가지는 수준이었습니다. 팀에서는 그 오차율이라면 95%의 저장공간 및 성능개선에 대해서 용납할 수 있는 수준이라고 생각했고, 제품에 무사히 적용될 수 있었습니다.

#### 마치며 아쉬웠던 점이 분명 존재합니다. 시간이 조금 더 주어졌더라면, convex hull의 겹치는 부분들을 조금 더 잘 정리할 수 있었을 것이라고 생각합니다. 물론 데이터의 크기라던지, 그 사이에 분명 애매하게 걸리는 공간들이라던지에 대한 결정이 필요하므로 생각보다 많은 시간이 필요할 것을 알고 있지만, 개발자로서 어쩔 수 없이 아쉬운 부분인 것 같아요.

테스트 환경의 중요성 또한 뼈저리게 느낄 수 있었습니다. 처음으로 할당 관련된 작업을 했던 프로젝트인데요, QA를 위해서 여러 가지가 부족했어요. 지역데이터 서비스의 스테이징 환경도 제대로 설정되어 있지 않았고, 어떻게 설정을 해야 테스트폰에서 할당받을 수 있는지조차 어려웠습니다. 다행히 팀에 QA 매니저가 계셔서 잘 도와주셨고 무사히 마무리할 수 있었지만, 빠르고 정확한 프로젝트의 완성을 위해서는 테스트 환경이 잘 갖추어져야 한다는 것도 배울 수 있었습니다.

#### 진짜로 마치며… 이 글을 테크 블로그에 기고하겠다고 다짐한 지 어느덧 1년이 넘어버렸습니다. 이 글을 읽으시는 독자분들 모두 저처럼 마음에 오랫동안 품은 무언가가 있으실 것이라고 생각합니다. 저도 오랜기간 짐을 가지고 있다가 이제야 홀가분해지는 처지에서 부끄럽지만, 모두 올해는 꼭 그 짐 벗어 던지시길 기원할게요! 읽어주셔서 감사합니다!

[버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [버즈빌의 ML 플랫폼 Buzzflow (1) - 모델을 개발하고 관리하기 ](https://tech.buzzvil.com/blog/버즈빌-ml-platform-1) Date: 2024-01-25 | Author: bk-kang | Category: Data & ML ## 들어가며 안녕하세요, 버즈빌에서 MLOps 엔지니어로 일하고 있는 BK입니다. 버즈빌 광고 플랫폼 팀은 광고 성과 최적화를 위해서 다양한 머신러닝 모델을 활용하고 있습니다. 앞으로 두 편의 글을 통해 모델을 개발, 관리, 배포하기 위한 버즈빌의 머신러닝 플랫폼 buzzflow가 어떤 식으로 구성되어 있는지 여러분께 소개해 드리고자 합니다. 이번 글에서는 모델을 개발하고, 관리하기 위해 필요한 파이프라인이나 모델 저장소 등에 대해 다루고 이렇게 개발한 모델을 배포하는 방법에 대해서는 다른 글을 통해 보여드릴 예정입니다. ## BB(Before Buzzflow) buzzflow 개발 이전에는 머신러닝 파이프라인을 어떻게 작성해야 하는지에 대한 Best practice가 명확하지 않아 각자 다른 스타일로 파이프라인을 작성하고 있었습니다. 이는 다른 사람들이 작업을 리뷰하거나 추가 작업을 하기 어렵게 만들었습니다. 또 모델 저장소로 MLFlow와 Sagemaker Model Registry를 혼용하고 있어 이 또한 통일이 필요한 상황이었습니다. 게다가 권한이 있는 사람이면 누구나 자유롭게 파이프라인을 생성하고 변경할 수 있었기 때문에 상용 환경에서 실행되는 파이프라인과 Github에서 관리하는 버전이 다른 경우도 존재했습니다. 버즈빌에서는 머신러닝 파이프라인을 위해 AWS의 Sagemaker Pipeline을 사용하고 있는데요. 이 Sagemaker Pipeline SDK의 단점도 있었습니다. 파이프라인을 개발하는 입장에서 가장 불편하게 느껴졌던 부분은 머신러닝 프레임워크마다 다른 파라미터를 가지고 있거나 동일한 기능을 하는 파라미터임에도 불구하고 다른 이름을 사용하고 있는 것이었습니다. 예를 들어 Processing에 사용하는 Processor와 Training에 사용하는 Estimator의 네트워크 설정의 경우 사실상 동일한 기능을 사용하고 있지만 파라미터의 형태가 다릅니다. 코드를 확인해 보면 Processor의 경우 NetworkConfig 형태로 네트워크 관련 설정을 전달받고 있지만 Estimator의 경우 NetworkConfig 없이 바로 네트워크 설정을 전달받고 있다는 것을 확인할 수 있습니다. ```python class Processor(object): """Handles Amazon SageMaker Processing tasks.""" def __init__( self, ... network_config: Optional[NetworkConfig] = None, # processor는 NetworkConfig를 입력으로 받습니다. ): ... ... class NetworkConfig(object): def __init__( self, enable_network_isolation: Union[bool, PipelineVariable] = None, security_group_ids: Optional[List[Union[str, PipelineVariable]]] = None, subnets: Optional[List[Union[str, PipelineVariable]]] = None, encrypt_inter_container_traffic: Optional[Union[bool, PipelineVariable]] = None, ): ``` ```python class EstimatorBase(with_metaclass(ABCMeta, object)): """Handle end-to-end Amazon SageMaker training and deployment tasks.""" def __init__( self, ... # 그러나 Estimator는 NetworkConfig를 풀어낸 형태로 입력을 받습니다. security_group_ids: Optional[List[Union[str, PipelineVariable]]] = None, subnets: Optional[List[Union[str, PipelineVariable]]] = None, encrypt_inter_container_traffic: Optional[Union[bool, PipelineVariable]] = None, enable_network_isolation: Union[bool, PipelineVariable] = None, ): ``` 이러한 차이로 인해 Sagemaker pipeline을 사용하기 위해 SDK에 대한 추가 학습이 필요했고 작업 과정에서 인지 부하가 발생하고 있었습니다. 앞서 말씀드린 buzzflow 이전의 문제점들을 정리해 보면 아래와 같습니다. 1. Sagemaker Pipeline에 대한 컨벤션 부재 2. Sagemaker Pipeline SDK에 대한 인지 부하 3. Sagemaker Pipeline에 대한 권한 관리 부재 4. 통일되지 않은 모델 저장소 이제 buzzflow가 이 문제점들을 어떻게 해결했는지 설명하겠습니다. ## AB(After Buzzflow) ### 파이프라인 정의 buzzflow를 개발하면서 가장 먼저 목표로 한 것은 모델 코드와 파이프라인 코드를 분리하고 모두가 일관된 형태로 파이프라인을 작성할 수 있는 환경을 만드는 것이었습니다. 여기에 추가로 Sagemaker SDK를 개선해 파이프라인을 작성에 대한 사용성을 개선하고자 했습니다. 설계 과정에서 파이프라인의 경우 반드시 코드 형태로 작성할 필요는 없다고 생각했고, 실제로 버즈빌의 데이터 플랫폼에서는 YAML 형태로 파이프라인을 작성하고 있어 통일성 측면에서 YAML로 파이프라인을 작성하도록 만드는 것이 유리할 것으로 판단했습니다. 이에 모델 관련 코드는 코드 형태로 존재하고 이들을 엮어내는 파이프라인은 YAML 형태로 작성하게 했습니다. ```yaml variables: project_dir_relative_path: projects/prod/project_name common_dir_relative_path: ${variables.project_dir_relative_path}/common common_dir_local_path: /opt/ml/processing/input/code/common query_dir_relative_path: ${variables.project_dir_relative_path}/query ... ``` 여기에 YAML 파싱을 위해 서드파티 라이브러리인 [OmegaConf](https://omegaconf.readthedocs.io/en/2.3_branch/index.html)를 적용하고 [Variable Interpolation](https://omegaconf.readthedocs.io/en/2.3_branch/usage.html#variable-interpolation) 등의 기능을 활용해 중복 정의를 줄여 사용성을 개선했습니다. 이를 이용하면 위와 같이 자주 반복되는 값을 마치 변수처럼 필드 이름으로 접근할 수 있게 됩니다. 그다음으로 진행한 작업은 Sagemaker Pipeline SDK를 분석해 파라미터의 이름을 일치시키고, 기능을 통일하는 작업이었습니다. 이를 위해 SDK의 기능을 래핑하는 추상화된 래퍼들을 만들고 이 래퍼로부터 실제 파이프라인을 생성하도록 만들었습니다. 예를 들어 buzzflow 내에서 Processor와 Estimator는 모두 NetworkPolicy라는 클래스로 네트워크 설정을 관리합니다. 사용자는 network라는 필드로 네트워크 설정을 일관성 있게 명시하면 됩니다. ```yaml # pipeline.yaml network: security_group_ids: subnets: ``` ```python # data_model.py class ProcessorConfig(pydantic.BaseModel): ... network_config: NetworkPolicyConfig = Field(NetworkPolicyConfig(), alias="network") ... class EstimatorConfig(pydantic.BaseModel): ... network_config: NetworkPolicyConfig = Field(NetworkPolicyConfig(), alias="network") ``` 이 래퍼들은 [Pydantic](https://docs.pydantic.dev/latest/)으로 구현되어 있어 누락된 필드는 없는지, 필요한 형식대로 데이터가 들어왔는지 등등 입력에 대한 검증 과정을 수행합니다. 이를 통해 실제 파이프라인을 생성하기 이전에 문제가 될 수 있는 부분을 빠르게 파악할 수 있습니다. 사용자가 YAML로 파이프라인을 작성하면 래퍼들을 기반으로 실제 파이프라인을 생성합니다. 이때 장기적으로 Sagemaker Pipeline 외의 다른 워크플로우 오케스트레이션 도구로 전환할 가능성이 있기 때문에 MLOps 엔지니어가 래퍼로부터 파이프라인을 생성하는 코드만 수정하면 YAML 수정 없이도 쉽게 새로운 도구로 마이그레이션할 수 있게 만들었습니다.

정리하면 파이프라인 생성은 아래와 같은 과정으로 이루어집니다. 1. 사용자는 YAML 형태로 파이프라인을 작성합니다. 2. buzzflow는 YAML을 파싱하고 데이터 모델을 생성해 YAML을 검증합니다 3. 데이터 모델로부터 실제 파이프라인의 구성요소들을 생성합니다. 4. 해당 구성요소들을 조합해 파이프라인을 생성하고 Sagemaker에 반영합니다. 이렇게 파이프라인에 대한 통일된 컨벤션을 만든 후에는 여러 프로젝트에 걸쳐 공통으로 사용하는 기능을 하나의 패키지로 관리해 중복 정의를 막고 재사용성을 높였습니다. 예를 들어 쿼리 실행 등의 코드는 여러 프로젝트에서 사용할 수 있으므로 함께 관리하고 있습니다. 이런 공용 패키지는 파이프라인을 생성할 때 자동으로 사용할 수 있도록 구성해 두어 사용자는 파이프라인 명세에 공용 패키지에 대한 내용을 추가할 필요가 없습니다. ```python # global_common/query.py def run_athena_query(athena_query: str, save_path=None, region="us-west-2"): ... def load_query(query_file_path: str, **kwargs): ... ```

또한 CLI와 슬랙봇을 구현해 파이프라인 생성, 실행을 aws 콘솔 등에 접근하지 않고 수행할 수 있도록 만들었습니다. 사용자는 로컬 환경에서 코드를 작성하고 파이프라인을 생성, 실행할 수 있게 됩니다. 특히 슬랙봇을 사용하는 경우에는 AWS EventBridge, Simple Notification Service(SNS) 연동을 통해 파이프라인의 상태 변화에 대한 실시간 알람을 받을 수 있습니다.

### 로컬 모드를 사용한 파이프라인 디버깅 Sagemaker pipeline의 특성 상 파이프라인 스텝을 실행하기 위해 인스턴스를 생성하고 컨테이너를 내려받는 시간이 필요한데, 이는 개발 과정에서 피드백 루프를 느리게 만듭니다. 또한 Sagemaker Pipeline의 경우 각 스텝에서 실행될 인스턴스 환경을 지정해 주어야 하는데요. 인스턴스 타입마다 가격이 다르기 때문에 CPU/메모리/디스크 등 리소스 사용량에 맞춰 인스턴스를 최적화하는 것이 중요합니다. 이를 위해 buzzflow는 로컬 모드 기능도 함께 제공하고 있습니다. 이를 이용해 로컬 환경에서 손쉽게 파이프라인을 생성해 테스트할 수 있습니다. Sagemaker에서 제공하는 로컬 모드를 기반으로 Prometheus, cAdvisor를 함께 사용해 파이프라인의 각 스텝이 어느 정도의 리소스를 사용하는지 확인할 수 있게 만들어 모델 개발자의 인스턴스 선택에 도움이 되도록 만들었습니다. 사용자는 이전과 동일하게 파이프라인을 YAML로 작성한 후에 CLI를 로컬 모드를 나타내는 플래그와 함께 실행하면 됩니다. 그러면 이제 생성한 파이프라인을 Sagemaker에 반영하는 대신 로컬 환경에서 도커를 이용해 파이프라인을 실행하게 됩니다. 파이프라인이 종료되면 cAdvisor와 Prometheus를 통해 수집한 메트릭을 출력합니다. ### 모델 저장소 통일되지 않은 모델 저장소의 경우 역시 Sagemaker 외의 다른 도구로 전환할 가능성이 있음을 고려하여 MLFlow로 마이그레이션 하기로 결정하였습니다. 일반적으로 MLFlow를 사용하는 경우 Host 등 몇 가지 설정이 추가로 필요한데요. buzzflow는 사용자 편의성을 위해 experiment manager라는 패키지를 구현하고 있어 모델 개발 시 설정에 대해 신경 쓸 필요 없이 바로 모델 저장소에 연결할 수 있습니다. ```python from experiment_manager.mlflow_manager import MLFlowExperimentManager @MLFlowExperimentManager(experiment_name="") def some_logic(param1, param2, ...): ... mlflow.log_param() mlflow.log_metric() ... if __name__ == "__main__": some_logic(param1, param2) ``` ### 권한 관리

buzzflow는 리서치 코드의 수정이 프로덕션 파이프라인에 영향을 주지 않도록 프로젝트를 리서치와 프로덕션으로 구분해 관리하고 있습니다. 일반적인 사용자가 로컬에서 CLI를 통해 파이프라인을 생성하는 경우 개발 데이터베이스, S3 등에만 접근할 수 있습니다. 만약 프로덕션 파이프라인을 변경하고 싶다면 반드시 PR을 생성해 구성원의 리뷰를 받아야 합니다. PR이 반영되어 저장소의 main 브랜치에 변경 사항이 발생하면 파이프라인을 자동으로 생성하도록 Github Actions 기반의 CI를 구성했습니다. 필요하다면 테스트 코드를 함께 작성해 테스트 코드를 통과하는 경우에만 PR을 반영할 수도 있습니다. ### 주기적 재실행과 모니터링 머신러닝 파이프라인의 다양한 사용 사례가 생기면서 주기적으로 파이프라인을 재실행해야하는 요구 사항이 생겨났는데요. AWS EventBridge를 사용하면 주기적 재실행은 쉽게 구현할 수 있습니다. 하지만 실행 시점에 동적으로 파이프라인의 입력 파라미터가 달라지는 경우는 EventBridge로 대응이 불가능해 airflow를 통해 머신러닝 파이프라인을 트리거하는 형태로 운영 중입니다. 기존 사내 데이터 플랫폼과 연동하여 단순한 Cron뿐만 아니라 데이터 파이프라인이 완료되는 경우 머신러닝 파이프라인을 실행하는 등의 다양한 유스케이스를 지원하고 있습니다.

프로덕션 파이프라인의 경우 파이프라인의 상태를 적절하게 모니터링하는 것 또한 중요합니다. 여기에 로그 조회 기능을 추가해 파이프라인이 실패하면 로그를 함께 전송하도록 만들었습니다. 해당 기능은 EventBridge를 통해 파이프라인 실패를 탐지하고, SNS를 통해 lambda를 실행시켜 CloudWatch 로그를 출력하는 방식으로 구현되어 있습니다.

### 문서화

마지막으로 파이프라인 관련 작업을 위해 필요한 정보들을 버즈빌 내부 도구에 문서화해두어 문서를 참조해 파이프라인 개발을 진행할 수 있도록 했습니다. ## 마치면서 이번 글에서는 버즈빌의 ML 플랫폼 탄생 이전의 상태와 ML 플랫폼을 통해 달성하고자 했던 기본적인 목표 그리고 이를 어떻게 달성했는지에 대해서 소개드렸습니다. 이 플랫폼을 통해 학습한 모델의 배포와 관련해서는 다음 글을 통해 정리해보려 합니다. 읽어주셔서 감사합니다! --- [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [쿠버네티스에게 Github Actions 설치에 대해 묻다](https://tech.buzzvil.com/blog/쿠버네티스에게-github-actions-설치에-대해-묻다) Date: 2024-01-01 | Author: cade-seo | Category: DevOps ## Introduction 안녕하세요. 버즈빌 DevOps 팀의 엔지니어, Cade 입니다. 최근 CI/CD에서 가장 인기 있는 플랫폼 중 하나인 깃헙 액션 (Github Actions), 많이들 사용하고 계실 텐데요. 오늘은 이 깃헙 액션을 가장 효율적으로 사용할 수 있도록 자체 호스팅(self-hosted)으로 설치하고 운영하는 방법에 대해 제가 고민하고 진행했던 경험을 공유하려고 합니다. 들어가기 전에, 저희는 왜 깃허브(Github)가 제공하는 깃헙 호스팅(github-hosted)으로 깃헙 액션을 사용하지 않는지부터 설명해 드리면 좋을 것 같아요. 깃헙 호스팅은 GitHub에서 관리되기 때문에 유지 관리의 번거로움을 덜 수 있어 편리합니다. 하지만 여러 가지 단점 또한 존재하는데요. 먼저 빌드 환경에 대해 완전한 제어가 불가능합니다. 예를 들어 특정 운영체제 버전이나 도구를 사용하고 싶을 때가 있는데, 깃헙 호스팅을 선택하면 러너(Runner) 환경에서 제한된 행동밖에 하지 못하고, 불필요한 운영 비용과 전체적인 CI 시간 비효율성이 발생합니다. 또 내부 통신망 접근에 대한 보안적인 문제가 발생하거나, 까다로운 네트워크 작업이 필요해집니다. 저희는 이와 같은 단점 때문에 자체 호스팅으로 깃헙 액션의 러너를 운영하게 되었습니다. ## ARC (Actions Runner Controller) 란? 깃헙 액션 러너를 자체 호스팅으로 실행시키는 2가지 방법이 있습니다. EC2와 같이 단일 인스턴스에 설치하는 방법과, ARC(Actions Runner Controller)를 사용해 쿠버네티스(Kubernetes) 위에 설치하는 방법이 있는데 저희는 쿠버네티스에 설치해 운영하기로 했습니다. 여기서 ARC는 깃헙 액션용 ​​자체 호스팅 실행기를 조정하고 확장하는 쿠버네티스 오퍼레이터입니다. 쉽게 말해 깃헙 액션 워크플로가 실행되는 러너를 쿠버네티스 파드로 실행할 수 있도록 기능을 제공하는데요. 저희가 ARC를 선택한 결정에는 다음과 같은 이유가 있습니다. #### 비용 효율성 단일 인스턴스에 설치하여 가상 머신(VM)을 기반으로 한 오토 스케일링 방법은 가상 머신을 오버 프로비저닝하여 리소스 낭비를 초래하거나 가상 머신을 언더 프로비저닝하여 빌드 속도를 늦추고 개발자 경험(DX)을 저하할 수도 있습니다. 여기서 ARC를 사용하게 되면 각 러너 파드마다 워크플로 성격별로 최적화된 리소스를 할당해 비용 효율적으로 운영할 수 있습니다. #### 확장성 ARC에서는 쿠버네티스의 강력한 기능을 활용하여 워크로드 할당량에 따라 러너 파드를 자동으로 확장합니다. 이렇게 하면 러너 리소스를 낭비하지 않고 인프라를 보다 효율적으로 사용할 수 있습니다. #### 속도 ARC에서는 항상 일정한 수의 대기 상태의 러너 파드를 유지하도록 구성할 수 있습니다. 뿐만 아니라 쿠버네티스는 요청에 따라 새로운 러너 파드를 상당히 빠르게 할당할 수도 있습니다. 이러한 이유들로 러너가 Pending 상태의 시간이 거의 없으므로 많은 워크플로가 더 빠르게 실행될 수 있습니다. 이 3가지뿐만 아니라 저희 DevOps 팀에서는 95%가 넘는 운영도구들을 쿠버네티스 위에서 설치&운영하고 있기 때문에, 현재 인프라에 쉽고 빠르게 추가할 수 있다는 장점도 있었습니다. ## ARC의 2가지 버전 : Community , Github 초기 ARC는 몇 명의 핵심 컨트리뷰터로 운영되는 오픈소스 커뮤니티 프로젝트로 시작되었습니다. 이 커뮤니티 버전은 2020년 2월에 출시되었습니다. 이 프로젝트는 쿠버네티스에서 Actions Runner를 실행시킬 방법을 찾고 있는 개발자들 사이에서 인기가 높아졌습니다. 이후로 많은 개발자가 깃허브에 프로젝트를 공식적으로 지원하도록 요청하기 시작했고, 2022년 12월 [깃허브는 이 프로젝트를 공식적으로 채택하고 이를 완전한 깃허브 제품으로 전환하는 작업을 시작](https://github.com/actions/actions-runner-controller/discussions/2072)했습니다. 그 후, 깃허브는 [2023년 7월에 Github supported 버전을 공식 출시](https://github.com/actions/actions-runner-controller/discussions/2775)했습니다. ![ARC-diagrams](https://tech.buzzvil.com/blog/%ec%bf%a0%eb%b2%84%eb%84%a4%ed%8b%b0%ec%8a%a4%ec%97%90%ea%b2%8c-github-actions-%ec%84%a4%ec%b9%98%ec%97%90-%eb%8c%80%ed%95%b4-%eb%ac%bb%eb%8b%a4/ARC-diagrams.jpeg) 따라서 현재 ARC에는 두 가지 버전이 있습니다. 하나는 깃허브에서 지원하고 다른 하나는 커뮤니티에서 지원합니다. 두 버전은 다음과 같은 차이점이 존재합니다. |제목|**Github supported ARC** |**Community supported ARC**| |------|---|---| |모드 명칭|Runner Scale Sets mode|Pull driven scaling mode , Webhook driven scaling mode| |apiVersion|`apis/actions.github.com/v1alpha1`|`actions.summerwind.net/v1alpha1`| |사용하는 러너 이미지| [runner](https://github.com/actions/runner/blob/main/images/Dockerfile) | [actions-runner-controller](https://github.com/actions/actions-runner-controller/tree/master/runner) | |유지 보수&관리|깃허브|커뮤니티| |스케일 방식|long-poll 커넥션|Pulling or Webhook| |특징|신규 기능에 대해 보수적이지만 안정적임.|신규 기능에 대해 개방적이지만 불안정함.| |장점|- certmanager 추가 설치 불필요
- github API 속도 제한 문제 발생하지 않음
- 컨트롤러 디버깅이 수월함|- 러너 다중 라벨 지원
- 러너 크론식 스케줄링 기본 지원| |최신 버전 (23.12 기준)|`v0.8.1`|`v0.23.7`| |출시일|2023.07 generally available |2020.02 available| 깃허브 지원 ARC가 여러 장점을 가지고 있다는 것을 표로 설명해 드렸는데, 커뮤니티 지원 ARC와 비교해서 왜 그런 특징을 가졌는지 말씀드리겠습니다. 두 가지 모드의 가장 큰 차이점은 Client-Server 구조의 변화입니다. ![ARC-client-server](https://tech.buzzvil.com/blog/%ec%bf%a0%eb%b2%84%eb%84%a4%ed%8b%b0%ec%8a%a4%ec%97%90%ea%b2%8c-github-actions-%ec%84%a4%ec%b9%98%ec%97%90-%eb%8c%80%ed%95%b4-%eb%ac%bb%eb%8b%a4/ARC-client-server.jpeg) 깃허브 지원 ARC의 경우 인증 이후 https long-poll 연결을 시도하는 client가 되지만 커뮤니티 지원 ARC의 경우 생성 이벤트를 전달받는 server가 되기 때문에 https 통신을 위해 TLS가 필요해집니다. 이를 위해 cert-manager를 추가로 설치해줘야 하는 의존성이 생기게 됩니다. 뿐만 아니라, 가용 러너에 할당하기 위해 풀링 방식의 스케일링을 사용하는 커뮤니티 지원 ARC는 개인 또는 조직의 레포지토리 워크플로 상태를 가져오는 동작을 위해 깃허브 API를 과다하게 사용하게 됩니다. 이 과정에서 시간당 1,000개의 깃허브 API 개수 제한을 초과해 API 속도 제한 문제가 발생합니다. 웹 훅 방식을 선택하는 경우에도 트러블슈팅이 까다로운 문제가 있습니다. 그에 비해 깃허브 지원 ARC는 위의 단점이 모두 보완되었습니다. 물론 초기 오픈 소스이기 때문에 관리 및 운영에 시간이 다소 소요된다는 단점이 있습니다. 따라서 위와 같은 장단점을 고려해 ARC의 모드를 선택해야 합니다. 저희는 깃허브 지원 ARC인 Runner Scale Sets 모드를 선택해 설치했습니다. 여러 가지 이유 중 아래의 이유가 가장 주요했습니다. >With the introduction of autoscaling runner scale sets, the existing autoscaling modes are now legacy. The legacy modes have certain use cases and will continue to be maintained by the community only. >-- actions-runner-controller README.md 커뮤니티 지원 ARC가 레거시화 되었을 때의 마이그레이션 비용이 현재의 깃허브 지원 ARC의 부족한 기능으로 인한 운영 비용보다 더 크다고 생각했기 때문입니다. ### Github 지원 ARC : Runner Scale Sets 모드 본격적으로 ARC를 설치하고 워크플로를 실행시켜보겠습니다. 설치는 헬름(helm)을 사용해 진행하면 됩니다. 헬름 차트는 2개를 설치해야 하는데, ARC와 Runner Scale Sets(실행기 확장 집합) 입니다. ARC는 깃허브 API를 직접 호출하고 상태를 업데이트하는 컨트롤러의 역할을 하고 Runner Scale Sets는 워크플로가 실행되는 러너 환경을 정의하는 역할을 합니다. 먼저 ARC부터 헬름 차트를 사용해 설치해보겠습니다. 자세한 차트 values는 [문서를 참고](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set-controller/values.yaml)해주세요. ```shell NAMESPACE="arc-systems" helm install arc \ --namespace "${NAMESPACE}" \ --create-namespace \ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller ``` 다음은 워크플로가 실행되는 환경을 정의하는 Runner Scale Sets를 헬름 차트를 사용해 설치해보겠습니다. 해당 차트의 경우 values를 다양하게 구성할 수 있는데, 저희는 이렇게 구성해보았습니다. 자세한 차트 values는 [문서를 참고](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/values.yaml)해주세요. ```yaml # githubConfigUrl는 러너에 구성하고 싶은 Github url 입니다. ## ex: https://github.com/myorg/myrepo or https://github.com/myorg githubConfigUrl: "https://github.com/Buzzvil" # git 인증을 위한 시크릿 명입니다. githubConfigSecret: gha-runner-gha-rs-github-secret ## maxRunners는 실행기 확장 집합이 최대로 확장할 러너의 최대 수입니다. maxRunners: 3 ## minRunners는 실행기 확장 집합이 축소할 최소 러너 수입니다. minRunners: 1 runnerGroup: "default" ## 생성할 Runner scale set의 이름입니다. (Defaults to the helm release name) runnerScaleSetName: "buzzvil-test" containerMode: type: "dind" ## type can be set to dind or kubernetes ## template은 각 Runner Pod에 대한 PodSpec입니다. template: spec: serviceAccountName: github-actions-runner containers: - name: runner image: 111111111111.dkr.ecr.ap-northeast-2.amazonaws.com/actions-runner:0.0.1 command: - /home/runner/run.sh resources: limits: cpu: 2 memory: 2Gi requests: cpu: 1 memory: 1Gi ``` 차트 구성에서 설명해 드리면 좋을 몇 가지가 있습니다. #### 1. 최소 러너 수 구성이 빠른 실행, 비용 효율성의 핵심! ```yaml ## maxRunners는 실행기 확장 집합이 최대로 확장할 러너의 최대 수입니다. maxRunners: 3 ## minRunners는 실행기 확장 집합이 축소할 최소 러너 수입니다. minRunners: 1 ``` 최소,최대 러너 수를 구성하고 각 러너 파드의 리소스 할당량을 지정할 수 있습니다. 최소 러너 수를 지정하면 그 수만큼 즉시 job에 할당될 수 있는 Idle 상태의 러너가 준비됩니다. 엔지니어 팀의 워킹 타임 동안 평균적으로 필요한 러너 수의 통계를 낸 다음, 최소 러너 수를 구성하면 러너 파드의 Pending 시간을 최소한으로 감소시킬 수 있습니다. 뿐만 아니라 워킹타임이 시작될 때와 끝날 때, cronJob 등을 통해 최소 러너 수를 조정하면 비용 효율적으로 ARC를 사용할 수 있게 되겠죠.
#### 2. IAM user ACCESS KEY 없이 CI를 돌릴 수 있게 하려면? ```yaml template: spec: serviceAccountName: github-actions-runner ``` ServiceAccount 리소스를 생성해 serviceAccountName 필드에 생성한 이름을 넣어 IRSA(IAM Roles for Service Accounts)를 구성할 수 있습니다. ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: github-actions-runner namespace: arc-systems annotations: eks.amazonaws.com/role-arn: arn:aws:iam::111111111111:role/github-actions-runner ``` 이렇게 구성하면 각 러너 그룹마다 지정된 역할을 할당해 AWS 리소스 접근에 대한 권한을 제한할 수 있게 됩니다.
#### 3. 사용할 Runner 이미지는 조직에 맞게 직접 빌드해서 사용하자! ```yaml containers: - name: runner image: 111111111111.dkr.ecr.ap-northeast-2.amazonaws.com/actions-runner:0.0.1 ``` 러너의 이미지는 [기본 러너 이미지](https://github.com/actions/runner)를 베이스로 해 직접 필요한 도구를 설치해 빌드한 다음 사용하시는 것을 적극 권장해 드립니다. [커뮤니티 버전의 ARC 기본 러너 이미지](https://github.com/actions/actions-runner-controller/blob/master/runner/actions-runner.ubuntu-22.04.dockerfile)와 다르게, 깃허브 버전의 ARC 기본 러너 이미지는 굉장히 슬림한 형태로 제공되고 있습니다. ([심지어 git cli도 기본내장되어 있지 않습니다](https://github.com/actions/actions-runner-controller/discussions/2865)…) ```dockerfile # Dockerfile을 작성해 Runner 커스텀 이미지를 만드는 예시 FROM ghcr.io/actions/actions-runner:2.311.0 # 필요한 도구 설치 예시 RUN sudo apt-get update && sudo apt-get install -y git curl gcc jq unzip wget ```
#### 4. runnerGroup으로 레포지토리별, 워크플로별 사용 가능한 러너를 지정해 줄 수도 있습니다. ```yaml runnerGroup: "default" ``` [조직] → [설정] → [Actions] → [Runner groups] 탭에 들어가면 레포지토리나 워크플로를 지정하여 특정 러너에 대한 접근을 제어할 수 있습니다. 리소스 요구량이 큰 러너나 특수한 보안적 요구사항을 가지고 있는 러너에 러너 그룹을 묶어 주어 접근을 ARC로 편하게 관리할 수 있게되죠. 기본 Default 그룹을 사용한다면 그룹의 모든 레포지토리와 워크플로가 접근할 수 있게 됩니다. ![RunnerGroup](https://tech.buzzvil.com/blog/%ec%bf%a0%eb%b2%84%eb%84%a4%ed%8b%b0%ec%8a%a4%ec%97%90%ea%b2%8c-github-actions-%ec%84%a4%ec%b9%98%ec%97%90-%eb%8c%80%ed%95%b4-%eb%ac%bb%eb%8b%a4/RunnerGroup.png) #### 5. dind 모드, kubernetes 모드? 어떤 점이 다른거야? ```yaml containerMode: type: "dind" ## type can be set to dind or kubernetes ``` 깃헙 액션의 서비스 컨테이너나 컨테이너로 실행 등의 처리를 위해서 ARC에서는 컨테이너 모드를 선택해 기능을 지원해야 합니다. 컨테이너 작업의 지원을 위한 두 모드의 차이를 짧게 요약하면 다음과 같은데요. - `dind` : 러너 파드의 사이드카 형태로 docker-in-docker 컨테이너가 붙는 형태입니다. docker 명령어를 사용할 수 있어 도커 이미지 빌드가 가능합니다. 하지만 특권(privileged) 모드로 실행되어 보안 수준이 강력하지 않습니다. - `kubernetes` : ARC가 [러너 컨테이너 후크](https://github.com/actions/runner-container-hooks?tab=readme-ov-file)를 사용하여 컨테이너용 파드를 새로 생성합니다. 러너 파드와 컨테이너용 파드는 퍼시스턴트 볼륨을 사용해 작업을 공유하게 됩니다. 보안 수준이 강력하지만 docker 명령어를 기본적으로 사용할 수 없어 도커 이미지 빌드 방법에 제한이 생깁니다. (kaniko 등을 사용해 빌드 가능) 더 자세한 동작의 구분은 [공식 문서](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/deploying-runner-scale-sets-with-actions-runner-controller#using-docker-in-docker-or-kubernetes-mode-for-containers)를 참고해주세요. 저희는 dind 모드를 선택해 설치를 진행하겠습니다.
#### 6. Github API 인증을 위해 시크릿도 추가해주기 깃허브 인증을 위한 시크릿 추가도 완료해주면 됩니다. 자세한 내용은 [공식 문서](https://docs.github.com/ko/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#deploying-using-personal-access-token-classic-authentication)를 참고해 진행해주세요. ```yaml apiVersion: v1 kind: Secret metadata: name: gha-runner-gha-rs-github-secret namespace: arc-systems type: Opaque data: github_token: ``` 마지막으로 헬름 차트 구성을 위한 values가 다 작성되었다면 컨트롤러를 설치했을 때처럼 헬름 명령어를 사용해 실행기 확장 집합을 설치해줍니다. ```shell NAMESPACE="arc-systems" helm install arc \ --namespace "${NAMESPACE}" \ --create-namespace \ -f arc_values.yaml \ oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set ``` 모든 설치가 완료되면 다음과 같이 쿠버네티스에 controller와 listener , runner 워크로드가 생성된 ARC가 구성된 것을 확인할 수 있습니다. ![ARC-pod](https://tech.buzzvil.com/blog/%ec%bf%a0%eb%b2%84%eb%84%a4%ed%8b%b0%ec%8a%a4%ec%97%90%ea%b2%8c-github-actions-%ec%84%a4%ec%b9%98%ec%97%90-%eb%8c%80%ed%95%b4-%eb%ac%bb%eb%8b%a4/ARC-pod.png) 공식문서에서 다음과 같이 아키텍처 다이어그램을 소개하고 있습니다. 아키텍처 플로우가 길고 이해하기 까다로울 수 있는데, 3개의 리소스를 쉽게 풀어 요약하면 다음과 같습니다. - `controller` : 깃허브 API 직접 호출, 러너 상태 체킹 및 업데이트 담당 - `listener` : 러너 스케일링을 담당. 깃허브 서버와 long-poll 커넥션을 맺고 부족한 러너 개수만큼 레플리카를 늘림. - `runner` : 실질적인 워크로드가 실행되는 격리된 환경. 깃허브 서버와 long-poll 커넥션을 맺고 잡(job)을 실행함. ![arc-architecture](https://tech.buzzvil.com/blog/%ec%bf%a0%eb%b2%84%eb%84%a4%ed%8b%b0%ec%8a%a4%ec%97%90%ea%b2%8c-github-actions-%ec%84%a4%ec%b9%98%ec%97%90-%eb%8c%80%ed%95%b4-%eb%ac%bb%eb%8b%a4/arc-architecture.webp) [출처: Github, Actions Runner Controller](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller) ARC의 자동 스케일링 실행기 ScaleSet 모드를 보여주는 다이어그램 그 중 controller는 `EphemeralRunner` , `AutoScalingListener` 등 다양한 컨트롤러 기능이 합쳐져 있는데, 그 중 상태 확인 시 유용한 CRD는 `EphemeralRunner` 로, 한눈에 실행 중인 워크플로의 ref 그리고 job의 이름이나 실행 중인 레포지토리 이름 등의 정보를 알 수 있습니다. ![Ephemeralrunners](https://tech.buzzvil.com/blog/%ec%bf%a0%eb%b2%84%eb%84%a4%ed%8b%b0%ec%8a%a4%ec%97%90%ea%b2%8c-github-actions-%ec%84%a4%ec%b9%98%ec%97%90-%eb%8c%80%ed%95%b4-%eb%ac%bb%eb%8b%a4/Ephemeralrunners.png) 이렇게 추가된 러너들은 Actions - Runners 탭에서 확인할 수 있고, 앞서 values에서 작성한 네이밍을 라벨로 사용해 워크플로를 해당 러너에서 실행시킬 수 있게 됩니다. ## 마무리하며 지금까지 쿠버네티스에 깃헙 액션을 설치하는 여러 가지 방법에 대해 알아보고, 그 중 저희 조직에 가장 맞는 방법이라 판단한 깃허브 버전의 ARC를 설치하는 과정까지 설명해 드렸습니다. ARC는 지금까지도 여러 가지 기능에 대한 열띤 토론이 펼쳐지는 현재진행형의 오픈 소스입니다. 앞으로도 더 유용한 기능과 뛰어난 퍼포먼스가 기대됩니다. 이번에는 ARC를 설치하는 과정만 설명해 드렸는데요. 다음 기회에는 저희 DevOps팀이 ARC를 어떻게 모니터링 하는지, 러너 타입은 어떻게 구분해서 관리하는지, 깃헙액션 공용 워크플로를 어떻게 관리하는지 등 많은 부분을 설명해 드릴 수 있으면 좋을 것 같습니다. 이 포스팅이 깃헙 액션을 자체 호스팅으로 어떻게 설치할 것인지, 여러 자체 호스팅 모드들에 대해 어떤 장단점이 있는지 고민인 분들에게 도움이 되길 바랍니다. 긴 글 읽어주셔서 감사드리며, 좋은 하루 되세요! [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [RDS MySQL IOPS 장애 대응기](https://tech.buzzvil.com/blog/rds-mysql-메모리-right-sizing) Date: 2023-11-16 | Author: Summer Kwak | Category: Backend **버즈빌에서 AWS RDS MySQL 데이터베이스를 운영하는 중에 맞닥뜨린 메모리 문제와 그 해결 방법에 대해 소개하는 글입니다.** ## 들어가며 안녕하세요, 버즈빌 엔지니어 Summer입니다. 현재 버즈빌 광고의 월평균 사용자는 2천만명 정도이고 버즈빌 서버로 한달간 약 70억건 정도의 광고 요청이 들어옵니다. 마이크로서비스로 쪼개져있는 서비스중 광고 요청을 보낸 사용자를 조회 하는 서비스의 트래픽 RPS는 5000/s 정도 됩니다. 이번 글에서는 방금 언급한 user service에서 사용중인 AWS RDS 데이터베이스를 운영하던 중 발생했던 장애 상황과 이를 해결하면서 배웠던 점들을 소개하겠습니다. ## 장애 상황 당시 상황은 퇴근 직전(!) user 서비스에 에러율이 올라갔다는 슬랙 알람을 받으면서 시작했습니다. 지표들을 확인해보니 cpu는 throttle걸리고 있었고 메모리 사용량과 goroutine(user service는 golang으로 작성되어 있습니다) 수가 급격히 증가하고 MySQL replica의 connection수가 치솟고 있었습니다. ![metric1](https://tech.buzzvil.com/blog/rds-mysql-%eb%a9%94%eb%aa%a8%eb%a6%ac-right-sizing/metric1.png) ![metric2](https://tech.buzzvil.com/blog/rds-mysql-%eb%a9%94%eb%aa%a8%eb%a6%ac-right-sizing/metric2.png) MySQL replica 쪽에 문제가 있다고 판단해 RDS 모니터를 확인해 보니 IOPS가 늘어나 burst balance가 줄어들다 0이 되어 버려 DB 성능이 급격히 떨어지고 있었습니다. 광고업 특성상 timeout이 빡빡하게 잡혀 있던 user service에 timeout 에러가 나거나 db connection 에러가 나고 있어서 에러율이 높아진 상태였습니다. 해당 인스턴스에서 사용 중인 gp2 볼륨에서는 burst balance가 RDS storage 사이즈에 연동되기 때문에 storage가 부족한 상황이 아니었음에도 storage를 증설하는 것으로 장애 상황을 빠르게 마무리 할 수 있었습니다. ## 높은 Disk I/O 원인 조사 storage를 늘린 후 user service가 정상화된 것을 볼 수 있었지만 여전히 왜 RDS의 disk I/O가 이렇게 높은지는 의문이 남았습니다. 3일 기간으로 burst balance를 봤을 때 주기적으로 balance가 떨어졌다 다시 올라오는 것을 확인할 수 있었습니다. 광고 요청을 받아 사용자 조회가 많아지는 낮시간에는 지속적으로 burst balance를 쓰고 있다는 뜻인데 IOPS가 이렇게까지 많아지는 것이 의심스러워 팀에서 조사를 시작했습니다. ![burst balance](https://tech.buzzvil.com/blog/rds-mysql-%eb%a9%94%eb%aa%a8%eb%a6%ac-right-sizing/burst_balance.png) 아래 그래프는 지난 1년간 user service의 IOPS 추세입니다. 1k에서 2k로 두배가량 높아진데가 점차 증가 추세에 있습니다. 게다가 user service의 RPS는 5k 가량인데 read IOPS가 1-2k로 요청 대비 I/O 비율이 너무 높은 상태이기도 했습니다. ![iops](https://tech.buzzvil.com/blog/rds-mysql-%eb%a9%94%eb%aa%a8%eb%a6%ac-right-sizing/iops.png) Disk I/O가 높으면 데이터베이스 성능에 악영향을 주고, IOPS가 증가했을 때 특정 수준을 넘으면 AWS에서 추가 비용을 청구합니다. 먼저 cache miss가 많아서 I/O가 발생했을 것이라는 가정하에 MySQL innodb의 `buffer pool hit ratio`를 계산해 봤습니다. innodb의 buffer pool은 메모리 내에 데이터와 인덱스를 캐싱합니다. InnoDB Buffer pool hit ratio가 낮으면 Disk에서 InnoDB Buffer pool 로 로드되는 데이터 많다는 뜻으로 cache가 miss되어 disk I/O가 높아지는 현상을 설명합니다. ``` 1 - (innodb_buffer_pool_reads / innodb_buffer_pool_read_requests) ```

출처: InnoDB의 주요 구성 요소

위와 같은 식으로 계산해 봤을 때 buffer pool hit 비율은 98%였습니다. cache miss가 많지 않은데도 디스크 I/O가 많이 일어나고 있다는 뜻이라 좀 더 알아 보기로 했습니다. 메모리 쪽 문제라고 생각해 AWS 쪽 문서를 보다 [Amazon RDS best practice](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_BestPractices.html#CHAP_BestPractices.Performance.RAM)를 발견했습니다. 이 문서에서는 자주 사용하는 데이터와 인덱스를 뜻하는 `working set`가 메모리에 대부분 올라가 있어야 낮은 Read IOPS를 유지할 수 있다고 말하고 있습니다. 적절한 IOPS를 찾기 위해 AWS에서 권장하는 방식은 더 이상 IOPS가 드라마틱하게 감소하지 않는 지점까지 메모리를 Scale up 해보는 것입니다. innodb의 데이터와 인덱스 사이즈를 다음과 같은 쿼리로 측정해 보았습니다. ``` SELECT FLOOR(SUM(data_length+index_length)/POWER(1024,2)) InnoDBSizeMB FROM information_schema.tables WHERE engine='InnoDB'; ``` user service의 데이터와 인덱스를 합친 사이즈는 170GB였습니다. 기존 RDS RAM 사이즈인 32GB로 전체 데이터 크기인 170GB 중 몇퍼센트가 working set으로 메모리에 있어야 하는지는 알기 어렵지만 팀 내에서는 메모리를 사이즈업 해보면 좋겠다는 의견을 모았습니다. 또한 IOPS burst 소진율은 gp2 EBS 타입에 따라 달라지기 때문에 저희 요구사항에 맞는 인스턴스로 변경이 필요하다고 판단해 스케일업 하게 되었습니다. 기존 인스턴스인 `m5.2xlarge` 와 같은 비용 수준의 인스턴스 타입 중 메모리 최적화 타입인 `r6g.2xlarge`으로 변경하여 메모리를 32GB → 61GB로 변경했을 때 Read IOPS가 절반 이하로 떨어졌지만 그래도 여전히 높은 상태였기 때문에, 메모리 128G의 한 단계 더 좋은 인스턴스(`r6g.4xlarge`)로 스케일업했습니다. `r6g.4xlarge`는 20000 IOPS를 제공하기 때문에 저희 요구사항에서도 여유롭게 운영할 수 있습니다. 첫 스케일업에서는 아주 가파르게 IOPS가 떨어졌지만, 두 번째 스케일업에서는 천천히 IOPS가 감소하는 것을 확인했습니다. 두 번째 스케일업 이후로는 80 IOPS를 유지하고 있습니다. 충분히 낮은 숫자라고 생각해서 더 스케일업하지 않았고 이 상태로 유지하기로 했습니다. ## 개선 사항 DB인스턴스에 대한 가격이 두배가 되었기 때문에 인덱스 사이즈 최적화 등을 통해 다운사이징 할 수 있을지 검토가 필요합니다. 또한 동일 요구 사항에 대해 DynamoDB같은 다른 스토리지를 썼을때 성능/가격 차이를 비교하여 개선할 수 있을 것 같습니다. IOPS 증가 문제에 대해 팀에서 고려했던 다른 옵션은 다음과 같습니다. - **Storage 증설.** 장애 대응으로 기존 800G → 999G로 증설하여 gp2 기준 Burst IOPS에 대한 추가 비용청구를 막아둔 상태였으나 IOPS가 지속적으로 증가하는 상황에서 계속 불필요한 스토리지를 올리는게 맞는 상황이 아니라고 생각했습니다. - **Read cache 추가.** Redis같은 캐시를 추가하는 방안을 생각했는데, 시스템에 복잡도가 증가하고 캐시에 장애가 발생했을때 DB로 burst가 발생되는 상황에 대한 대책을 마련해둬야 합니다. 또한 Redis가 memory에서 처리되기 때문에 빠르지만 MySQL buffer가 충분하면 이미 대부분의 워크로드를 메모리에서 처리하는 것이나 마찬가지라 Redis 도입이 크게 장점이 없다고 판단했습니다. 시스템 복잡도와 성능 등을 고려했을때 scale up으로 right sizing을 달성하는 것이 현재 요구 사항에서 적합한 해결책이라고 판단했습니다. ## 스케일업 후 메트릭 변화 다음은 2번에 걸쳐 인스턴스 스케일업후 실제 DB 메트릭 변화입니다. ![iops decrease](https://tech.buzzvil.com/blog/rds-mysql-%eb%a9%94%eb%aa%a8%eb%a6%ac-right-sizing/iops_decrease.png) 위 Read IOPS 그래프에서 보이는 두 번의 피크는 각각 인스턴스 교체 시점 입니다. 3000 가량 되던 IOPS가 첫 번째 교체에서 2000이하로, 두 번째 교체에서 80까지 떨어졌습니다. 이로써 적정 메모리 크기를 찾았다고 볼 수 있습니다. ![freeable memory](https://tech.buzzvil.com/blog/rds-mysql-%eb%a9%94%eb%aa%a8%eb%a6%ac-right-sizing/freeable_memory.png) 메모리 사이즈를 키웠기 때문에 당연한 결과지만, 0에 수렴하던 Freeable memory가 두 번의 인스턴스 교체 후 여유로워졌습니다. ![throughput](https://tech.buzzvil.com/blog/rds-mysql-%eb%a9%94%eb%aa%a8%eb%a6%ac-right-sizing/throughput.png) Read throughput 그래프도 극적으로 변했습니다. 필요한 데이터를 buffer에서 다 처리해 주어 결과적으로 read throughput도 줄었습니다. ## 마무리하며 데이터베이스를 운영하다 보면 이런 메모리 문제를 꼭 한 번쯤은 마주치게 됩니다. 데이터베이스의 memory size와 disk iops는 언뜻 보기에는 서로 연관이 없어보이지만 실제로는 관계가 있다는걸 배웠습니다. read iops를 최적화 할 때 지금처럼 working set이 memory에 충분히 캐싱되지 못한 경우라고 판단되면 memory size를 늘려서 최적화 할 수 있습니다. 이번에는 working set이 메모리에 다 올라가 있지 않아 Scale up으로 해결했지만 반대로 불필요하게 큰 인스턴스를 사용하는 곳에서는 Scale down으로 비용을 최적화할 수 있을것 같습니다. 이 글을 읽으시면서 다들 right sizing에 대해 한 번 더 생각해 볼 수 있는 기회가 되었으면 좋겠습니다. 또한 스케일업 해야 하는 타이밍과 기준에 대해 저희 팀에서 고려했던 부분이 다른 팀에도 도움이 되길 바랍니다. 읽어주셔서 감사합니다 :) [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [헤어질 결심 a.k.a 퇴사 부검](https://tech.buzzvil.com/blog/postmortem-message-from-jacob) Date: 2023-10-04 | Author: Jacob Yu | Category: Culture ## 들어가며 안녕하세요. 버즈빌 생활의 처음과 끝을 함께한 글로리아가 권유해 주신 덕분에 퇴사 부검 글을 작성하게 되었습니다. 넷플릭스에서는 퇴사한 직원이 퇴사 이유, 배운 점과 아쉬운 점 등을 메일로 적어 다른 직원들에게 보내는 문화가 있는데, 이 메일을 퇴사 부검 메일이라고 합니다. 저도 버즈빌 생활을 정리할 겸, 2년간의 세월을 되돌아보며 퇴사 부검 글을 작성해 보았습니다. ## 1. 왜 떠나는지? 한 문장으로 답하자면, 더 성장하기 위해 떠나게 되었습니다. 자세한 설명을 위해서는 대학원부터 시작되는 장황한 이야기를 해야 할 것 같습니다. 제가 대학원에서 수강한 과목 중에서 이산최적화와 강화학습이 특히 기억에 남습니다. 두 과목은 제게 지식을 제공해 주는 것을 넘어서 인생을 바라보는 관점을 제공해 주었던 과목입니다. ![제가 LA 대학원에 있었을 때..](https://tech.buzzvil.com/blog/postmortem-message-from-jacob/fig1_la94.png)

제가 LA 대학원에 있었을 때..
출처: 형이 왜 거기서 나와? (KCC 박찬호편)

### 최적화 문제로서의 커리어 패스 선택 **최적화** 의 관점에서 보면, 커리어 패스(Career Path) 선택은 원하는 가치를 최대화하기 위해 가능한 옵션 중에서 가장 적합한 것을 선택하는 문제로 볼 수 있습니다. 사람마다 최대화하고 싶은 가치가 당연히 다를 것입니다. 복지, 연봉, 성장, 적성, 선호하는 분야 등 우선시하는 가치가 다를 것이고, 그중에서 (0.7\*연봉 + 0.3\*복지)를 최대화하는 등 여러 가치를 한꺼번에 고려하고 있을 것입니다. ![다양한 커리어 선택지.](https://tech.buzzvil.com/blog/postmortem-message-from-jacob/fig2_career.png)

다양한 커리어 선택지
출처: The Importance of Values in Career Decision Making

커리어 패스마다 해당 커리어 패스를 선택하는 데에 걸리는 비용이나 시간, 불확실성 등의 특성이 있을 것입니다. 예를 들어 제가 갑자기 법조인의 커리어를 시작하려고 한다면 적어도 3년 이상의 시간과 학비를 투자해야 할 것이고, 다른 IT 회사에 개발자로 이직한다고 한다면 회사를 알아보고 면접을 준비하는 등의 시간과 노력을 투자해야 할 것입니다. 결국 어떤 선택이든 효용과 비용이 따르고, 이런 요소들을 종합적으로 고려하여 나에게 최적인 결정을 내리는 게 커리어 선택 문제라고 생각해 볼 수 있습니다. 이 문제의 답은 개인이 어떤 가치를 추구하느냐에 따라 달라집니다. 저는 **성장**을 최대화하는 선택을 추구하고 있습니다. 저라는 사람이 성장할 때 가장 만족한다는 것을 경험적으로 느껴왔기 때문입니다. 지금까지의 경험을 돌이켜보면, 힘든 시간을 보내면서도 성장했다면 그 시간을 의미있게 기억하는 반면, 스트레스 없이 편하게 살아도 성장하지 않았다면 아쉬움이 남았습니다. 그리고 제가 성장할수록 연봉, 근무 조건, 도메인 등을 원하는 수준으로 맞출 수 있는 더 강력한 협상력을 가지게 된다고 생각합니다. 따라서 성장을 추구하면 장기적으로는 성장 외의 다른 가치도 쉽게 추구할 수 있을 것이라고 믿고 있습니다. 어떤 커리어를 선택하면 성장을 최대화할 수 있을까요? 연봉 같은 요소는 오퍼 레터(Offer Letter)에 명시적으로 적혀있기 때문에 비교적 쉽게 파악하고 비교할 수 있기에, 당장 주어진 여러 선택지 중 연봉이 가장 높은 선택지를 선택하는 것은 어렵지 않습니다. 그러나 "이 커리어를 통해 얼마나 성장할 수 있을지"는 실제로 경험하지 않는 한 알기 어렵습니다. 따라서 “이 커리어를 통해 얼마나 성장할 수 있는지”를 정확하게 예측하여 최적의 선택을 내리는 것은 불가능하고, 적당한 아이디어나 휴리스틱에 기반해서 선택을 내려야 합니다. ### 강화학습의 모험과 탐색 저는 강화학습에서 배운 몇몇 아이디어를 굉장히 좋아합니다. 강화학습에는 모험(Exploration, 탐험)과 활용(Exploitation)이라는 개념이 있습니다. 간단하게 표현하자면, 모험은 아직 모르는 영역으로 나아가서 새로운 정보를 획득하는 것이고, 활용은 이미 알고 있는 정보를 바탕으로 최적의 선택을 하는 것입니다. 활용만을 추구한다면, 우리는 지금까지 알아낸 전략에만 의존하게 됩니다. 장기적으로 더 나은 결과를 얻기 위해서는 적절한 시기와 비율로 모험을 시도하는 것이 필요합니다. 실제로 강화학습 연구에서는 어떻게, 어떤 비율로, 어떤 상황에서 모험을 시도할 것인지를 최적화하는 데에 노력을 기울이기도 합니다. ![음식점 선택 문제 - 내가 가보지 않은 음식점이 더 만족스러울지도 모릅니다.](https://tech.buzzvil.com/blog/postmortem-message-from-jacob/fig3_reinforcement.png)

음식점 선택 문제 - 내가 가보지 않은 음식점이 더 만족스러울지도 모릅니다.
출처: https://medium.com/analytics-vidhya/the-epsilon-greedy-algorithm-for-reinforcement-learning-5fe6f96dc870

대학원을 졸업하고 개발자 커리어를 시작한 지 3년이 되어 가는데, 아직 배워야 할 것이 훨씬 많다고 느낍니다. 모르는 게 많을수록 모험의 기댓값이 높습니다. 그래서 저는 제가 활용보다는 모험의 비중을 높여야 하는 시기에 있다고 생각하고 있습니다. 지금의 저에겐 새로운 영역으로의 모험이 장기적인 성장을 최대화하는 선택지일 확률이 높습니다. 장기적인 성장을 위해 모험을 하는 게 좋은 전략이라고 할 때, 무수히 많은 모험 중 어떤 모험을 선택할 것이냐는 문제가 나타납니다. 여러 가지 방법으로 모험을 할 수 있습니다. 예를 들면 제가 최근에 고려할 수 있었던 선택지들은 아래와 같습니다. 1. (회사 내 팀 변경) 다른 팀에서 새로운 문제를 풀며 경험을 쌓는다. 1. (회사 내 직무 변경) 다른 직무(PM, DevOps, 데이터 엔지니어 등)로 일하며 경험을 쌓는다. 1. (이직) 다른 회사로 이직해서 새로운 팀에서 새로운 문제를 풀며 경험을 쌓는다. 3번에 대해서만 조금 더 자세히 이야기를 해보겠습니다. 여러 회사들 중 어떤 회사의 어떤 팀으로 이직해야 가장 많이 성장할 수 있을까요? 정답은 없지만 가장 빠르게 성장하고 있는 회사, 가장 기술력이 좋은 회사, 훌륭한 동료들이 많은 팀 등 여러 가지 아이디어를 떠올려 볼 수 있습니다. 성장이라는 것 자체도 사람마다 다양한 정의가 있을 수 있기에, 다들 나름의 답을 낼 수 있다고 생각합니다. 따라서 저는 저에게 성장이 무엇인지, 그리고 어떤 선택지가 제가 생각하는 성장에 가장 도움이 될 것인지 고민해야 목적에 부합하는 선택을 할 수 있습니다. ### 호기심이 이끄는 모험 저는 호기심이 굉장히 강합니다. 일례로, 보드게임을 할 때 상대가 거짓말을 하는 것인지 진실을 말하는 것인지 너무 궁금해서 호기심 비용을 지불하고 상대방의 패를 확인하는 비이성적인 행동을 취할 때가 많습니다. 호기심은 제가 모르기 때문에 예측을 잘할 수 없는, 그럼에도 알고 싶은 무언가에 대해 생기는 감정입니다. “어떤 선택을 해야 가장 성장할 것인가?”라는 질문에 답을 내리는 기준으로 호기심을 이용해 볼 수 있습니다. 가장 호기심이 생기는 길을 선택하는 것입니다. 강화학습 분야에는 [Curiosity-driven Exploration by Self-supervised Prediction](https://arxiv.org/abs/1705.05363) 라는 제목의 논문도 있습니다. 이 논문은 [Wall Street Journal](https://www.wsj.com/articles/curiosity-is-a-new-power-in-artificial-intelligence-1525446050), [Wired](https://www.wired.com/story/clever-machines-learn-how-to-be-curious-and-play-super-mario-bros/)등 다양한 미디어에서 조명받으며 화제가 되었던 논문입니다. ![Curiosity-driven Exploration by Self-supervised Prediction.](https://tech.buzzvil.com/blog/postmortem-message-from-jacob/fig4_curiosity.png)

In many real-world scenarios, rewards extrinsic to the agent are extremely sparse, or absent altogether. In such cases, curiosity can serve as an intrinsic reward signal to enable the agent to explore its environment and learn skills that might be useful later in its life.

실제 세계의 많은 상황에서는 에이전트에게 외부로부터 주어지는 보상이 매우 드물거나 전혀 없을 수 있습니다. 이런 경우에 호기심은 내재된 보상 신호로 작용하여 에이전트가 그 환경을 탐색하고 나중에 유용할 수 있는 기술을 배울 수 있게 도울 수 있습니다.

출처: Curiosity-driven Exploration by Self-supervised Prediction 의 논문 초록

![위의 논문에선 별도의 보상 없이 호기심에 기반한 탐색만을 이용하여 슈퍼 마리오 agent를 학습시켰습니다..](https://tech.buzzvil.com/blog/postmortem-message-from-jacob/fig5_noreward.png)

위의 논문에선 별도의 보상 없이 호기심에 기반한 탐색만을 이용하여 슈퍼 마리오 agent를 학습시켰습니다.
출처: noreward-rl github

버즈빌에서 광고 추천, 광고 성과 최적화 문제를 풀면서 내가 충분히 잘하고 있는 것일지, 전 세계의 다른 뛰어난 팀들은 어떻게 의사결정을 하고 어떻게 개발하고 있을지 궁금할 때가 많았습니다. 다른 회사의 기술 블로그나 논문을 봐도 채워지지 않는 갈증이 남아있었습니다. 그러던 중 최종합격한 회사의 리드님과 커피챗을 하게 되었습니다. 구글에서 10년 동안 근무하신 리드님이 “제대로 된 개발 문화를 전파하고 있다”, “검색의 End-to-End를 다루며 다양한 문제를 해결하고 있다”고 말씀하시는 걸 듣고 나서 가서 어떤 문제들을 어떻게 풀고 있는지 알아보고 싶다는 호기심이 생겼습니다. 이 호기심을 이기기 힘들어서, 버즈빌을 떠나게 되었습니다. ## 2. 회사에서 배운 것 ### 좋은 동료가 최고의 복지 “좋은 동료가 최고의 복지”라는 말이 있습니다. 버즈빌에서 정말 그렇다는 걸 많이 느꼈습니다. 입사하기 전에 재직자 리뷰 사이트에서 버즈빌을 검색했을 때, “좋은 동료”가 반복해서 등장하길래 어떤 분들이 계시길래 이런 후기가 많은 건지 궁금했었습니다. 입사하고 나서 2년을 생활하고 나니 1. 선하고 2. 똑똑하고 3. 일에 몰입하는 동료들이 넘쳐난다는 것을 느낄 수 있었습니다. 그러다 보니 일 외적으로 스트레스를 받을 일이 거의 없고, 목표 달성을 위해 팀이 함께 노력한다는 안정감을 항상 느낄 수 있었습니다. 많은 분이 성장을 위한 의지를 가지고 있어 서로에게 좋은 자극을 주고받으며 함께 성장하는 모습을 자주 볼 수 있습니다. 예컨대, 누구나 쉽게 참여할 수 있는 개발 스터디나 지식 공유가 활발히 이루어집니다. 저는 디자인 패턴 스터디와 DDD(Domain Driven Design) 스터디에 참여했고, 회사 내 신규 입사자분들에게 광고 도메인에 대해 설명하는 세미나에서 Dynamic AD에 대해 발표한 경험이 있습니다. 파이콘, 드로이드 나이츠 등 국내 유수의 컨퍼런스에서 발표하는 분들도 계십니다. 그런 분들과 함께하는 환경에 있어서인지, 저도 최근에 2023년 파이콘 연사로 지원하여 발표 기회를 얻을 수 있었습니다. 점심시간에 모여 개발 관련 세미나 영상을 함께 보는 모임 등 소규모 모임도 활발합니다. 굳이 모임이나 스터디가 아니더라도 열심히 살면서 성장하시는 멋진 분들이 많습니다. 그런 분들과 함께하다 보면 좋은 동료가 최고의 복지라는 것을 체감하게 됩니다. ![버즈빌 스터디 모집글 예시1.](https://tech.buzzvil.com/blog/postmortem-message-from-jacob/fig6_nike.png) ![버즈빌 스터디 모집글 예시2.](https://tech.buzzvil.com/blog/postmortem-message-from-jacob/fig7_dante.png)

스터디 모집글 예시. 많은 분들이 자발적으로 스터디를 모집하고 운영했습니다.

### 문제 해결과 개선을 위한 회고 버즈빌에서 가장 좋았던 문화는 회고 문화입니다. 실질적으로 문제를 해결하는 회고를 경험하고 나니 개인적인 삶에서도 회고를 하게 될 정도로 회고의 효용성을 느낄 수 있었습니다. 2022년 말에 제가 있던 추천팀은 엔진팀과 통합되었습니다. 광고 할당을 주로 다루던 엔진팀과의 통합으로, 광고 할당과 관련된 맥락을 고려하여 광고 성과 최적화 과제를 더 효과적으로 해결할 수 있게 되었습니다. 하지만 알아야 하는 맥락이 늘어나면서 해결해야 하는 이슈의 종류가 늘어나고 인지 부하도 늘어난다는 단점도 있었습니다. 회의 등 커뮤니케이션의 생산성도 떨어지는 듯 했습니다. 몇 차례의 회고를 거치면서 단점이 점점 부각되자, 저희 팀은 인원을 나누어서 광고 성과 최적화 과제와 안정성 과제를 각각 진행하기로 결정했습니다. 그 결과 안정성 과제를 진행하는 엔지니어들은 광고 성과 최적화 과제에 대해 신경 쓰지 않고 안정성 과제에만 집중하면서 과제에 몰입할 수 있었습니다. 회의 방식이나 과제 진행 방식도 회고를 통해 점진적으로 개선해 나갔습니다. 어느 정도 프로세스가 개선된 이후에는 팀이 합쳐진 초창기와 비교했을 때 생산성과 몰입도가 매우 증가했다는 것을 회고하며, 다시 한번 회고의 중요성을 느꼈습니다. ![Ad Engine팀 스프린트 예시1.](https://tech.buzzvil.com/blog/postmortem-message-from-jacob/fig8_sprint1.png) ![Ad Engine팀 스프린트 예시2.](https://tech.buzzvil.com/blog/postmortem-message-from-jacob/fig9_sprint2.png)

회고 예시. 저희 팀은 confluence, figma를 이용해서 주기적으로 회고했습니다.

### 목적 달성을 위한 커뮤니케이션 커뮤니케이션도 어떤 목적을 위해 이용하는 수단 중 하나입니다. 대화에 몰입하다 보면, 목적 달성에는 도움이 되지 않는 대화를 하게 될 때가 있습니다. 예를 들면 A 문제 해결 방법 구체화를 목표로 대화하고 있었는데, 정신을 차리고 나니 B 문제가 중요한지 아닌지에 대해 대화하고 있는 것입니다. 이런 대화도 도움이 될 때가 있겠지만, 대부분 시간만 소비하고 실질적인 목적을 달성하지 못하는 경우가 많았습니다. 반대로 잘 준비된 형태로 목적을 신경 쓰면서 이야기하면 목적도 달성하고 의미 있는 아이디어를 도출할 때가 많았습니다. 커뮤니케이션에도 여러 방법이 있습니다. 다 같이 모여서 회의를 하는 것보다 문서를 기반으로 비동기적으로 커뮤니케이션 하는 게 목적 달성에 도움이 될 수도 있습니다. 차 한 잔 하면서 그 사람의 이야기를 잘 들어주는 게 목적 달성에 더 도움이 될 수도 있습니다. 굳이 명시적으로 커뮤니케이션 하지 않고 각자가 스스로 학습할 시간을 가지는 게 목적 달성에 도움이 될 수도 있습니다. 따라서 편한 방법이나 익숙한 방법보다 목적 달성에 효과적인 방법을 선택하여 수행하는 것이 더 나은 태도일 수 있습니다. ![Ad Engine팀 RFC 예시1.](https://tech.buzzvil.com/blog/postmortem-message-from-jacob/fig10_rfc1.png) ![Ad Engine팀 RFC 예시2.](https://tech.buzzvil.com/blog/postmortem-message-from-jacob/fig11_rfc2.png)

비동기적인 커뮤니케이션의 예시. 저희 팀은 RFC(Request for Comments)라는 이름으로 작업 방향에 대해 논의했습니다.

좋은 커뮤니케이션 태도를 가진 동료들 덕분에 조금 더 나은 커뮤니케이션 태도를 갖게 된 것 같습니다. 구체적으로는 1. 내가 어떤 목적을 달성하고 싶은 것인지 1. 목적 달성을 위해서는 어떻게 커뮤니케이션하는 게 좋은지 1. 현재 내가 목적 달성에 도움이 되는 커뮤니케이션을 하는 게 맞는지 를 더 신경 쓰게 되었습니다. 그 결과 예전 같았으면 바로 미팅을 소집해서 이야기했을 만한 이슈가 발생해도, 문서를 통해 소통하거나 더 시간을 갖고 지켜보거나, 혼자서 더 고민해 본 후 그 결과물을 기반으로 이야기할 수 있게 된 것 같습니다. ## 3. 회사에 아쉬운 점 넷플릭스의 퇴사 부검 메일에선 “넷플릭스가 이랬다면 떠나지 않았을 것”을 전제로 아쉬운 점을 적는다고 합니다. 앞서 말씀드린 것처럼 저는 지금을 모험의 기댓값이 더 높은 상황이라고 인식하고 있기 때문에, 어떤 환경이 갖추어지던 몇 년 안에 새로운 환경으로 떠났을 것 같습니다. 회사에 아쉬운 점이 있어서 떠나는 것은 아니라서, “버즈빌이 이랬다면 떠나지 않았을 아쉬운 점”은 없습니다. 그래도 개선에 참고할 수 있는 아이디어를 무엇이라도 말씀드리면서 떠나는 것이, 아쉬움이 전혀 없다고 밝히며 떠나는 것보다 회사에 조금이라도 도움이 되겠다고 생각했습니다. 퇴사 면담을 거치며 여러 번 말씀드렸던 내용이 있기에, 이를 정리해보았습니다.

광고 성과 최적화 문제는 많은 ad-tech 회사들이 풀고 있는 문제입니다. Criteo, Linkedin, Google 등의 글로벌 회사들이 어떻게 이 문제를 푸는지 논문이나 기술블로그를 통해 찾아볼 수 있습니다. 많은 시간과 노력을 투자해온 것으로 보이고, 꽤 고도화된 형태로 이 문제를 풀고 있는 것으로 추측됩니다.

버즈빌에서는 이 문제를 올해부터 제대로 풀기 시작했고 의미 있는 성과를 얻었습니다. 모두가 이 문제를 처음 접하다보니, 다양한 아이디어를 기반으로 논의와 실험을 반복하면서 개선해갔습니다. 다만 로드맵과 우선순위 결정 과정에서 명확한 합의가 이루어지지 않았다는 생각이 들 때가 있었습니다. 그래서인지 1~2년 후 솔루션이 어떤 모습이 될지, 어떤 수준으로 이 문제를 풀고 있을지 잘 상상이 되지 않았습니다.

저에게 광고 성과 최적화 문제는 더 잘 풀어서 다른 ad-tech 회사들과 경쟁해보고 싶다는 생각이 들 정도로 재미있는 문제입니다. 로드맵과 우선순위에 대해 충분히 합의된 상태로 1~2년 후에 우리의 솔루션이 어떤 모습이 될지, 그 때의 성과는 어떻게 될지 기대할 수 있었다면 더 남아서 함께 문제를 풀고 성과를 내고 싶었을 것 같습니다.

“따라서 빠르게 로드맵과 우선순위를 확립하고 고도화 방향을 구체화해야 한다”고는 생각하진 않습니다. 인력 부족, 리소스 부족, 시급한 이슈 발생, 새로운 비즈니스 요구사항 발생, 유관 부서와의 합의 등 여러 제약사항 속에서 이미 나름의 최선의 선택을 해왔다고 생각하기 때문입니다. 어차피 흔들릴 수 밖에 없는 로드맵을 구체화하는 데에 리소스를 투자하는 대신, 단기적이고 유연한 접근으로 문제를 차근차근 해결하는 전략이 더 효과적일 수 있다고 생각합니다. 이런 아쉬움을 느낄 때가 있었다, 정도의 가벼운 소회로 이해해주시면 될 것 같습니다. 그 외에 아쉬운 점은 없습니다. 훌륭한 동료들과 재미있게 일할 수 있었고, 많은 분들의 도움 덕분에 성장해서 떠날 수 있게 되어 감사한 마음입니다. ## 4. 앞으로의 계획 저는 새 회사의 검색팀 개발자로 근무하게 되었습니다. 검색팀은 아래 업무를 수행한다고 합니다.

다양한 Contents를 안정적인 시스템 구성과 다양한 Ranking 모델을 통해 사용자에게 좋은 서비스 품질을 제공하려고 합니다. 이를 위해서 집중하고 있는 부분은 사용자의 검색 경험을 좋게 하기 위한 다양한 Search Function 개발, 품질 향상을 위한 다양한 Ranking모델 실험/적용, 안정적인 서비스를 위해 Search Infrastructure 구축/향상에 대해서 많은 고민을 하고 있습니다.

재미있는 문제가 많을 것으로 예상되고, 새 회사의 업무는 [개인 블로그](https://blog.muuty.me/)에 올려보도록 하겠습니다. 긴 글 읽어주셔서 감사합니다! [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [Self Serving Data Platform 구축하기 (feat. Airflow)](https://tech.buzzvil.com/blog/셀프-서빙-데이터-플랫폼-구축하기) Date: 2023-05-31 | Author: Bernard Choi | Category: Data & ML ## Introduction 안녕하세요. 버즈빌 데이터 엔지니어 Bernard입니다. 이번 포스팅에서는 저희 팀에서 어떻게 셀프 서빙 데이터 플랫폼을 구축했는지 소개하고자 합니다. 셀프 서빙 데이터 플랫폼이란, 특정 팀에 의존하지 않고 구성원 누구나 원하는 데이터를 스스로 생산 및 소비할 수 있는 플랫폼을 의미합니다. 과거에는, 서버팀 혹은 데이터 분석팀에서 데이터 파이프라인 생성 요청이 들어오면 데이터 엔지니어가 필요한 파이프라인을 구축했습니다. 그러나 버즈빌의 서비스와 분석 도메인이 다양해지면서 모든 파이프라인을 운영, 관리하는데 인지 부하가 발생했습니다. 또한 각 팀에서도 데이터를 적시에 조회하기 위해 데이터 엔지니어에게 의존하게 되어 불편함이 커졌습니다. 이에 도메인의 전문성을 적극 활용할 수 있는 셀프 서빙 데이터 플랫폼을 개발하기 시작했습니다. ## 버즈빌의 데이터 인프라 먼저 버즈빌 데이터 인프라에 대해서 간략하게 소개해 드리겠습니다. 저희가 사용하는 주요 데이터 컴포넌트들은 다음과 같습니다. ![buzzvil-data-infra](https://tech.buzzvil.com/blog/%ec%85%80%ed%94%84-%ec%84%9c%eb%b9%99-%eb%8d%b0%ec%9d%b4%ed%84%b0-%ed%94%8c%eb%9e%ab%ed%8f%bc-%ea%b5%ac%ec%b6%95%ed%95%98%ea%b8%b0/buzzvil-data-infra.png) - Athena/S3 S3는 AWS에서 제공하는 스토리지 서비스입니다. 확장성이 뛰어나며, 저장하는 데이터양에 비해 비용도 저렴합니다. 버즈빌에서는 모든 데이터를 S3로 1차 저장하며 SSOT(Single Source of Truth)로 활용하고 있습니다. S3에 저장하고 있는 데이터 종류는 다음과 같습니다. - landing: 운영계로부터 최초 수집된 데이터 - gold: 저장/압축 및 간단한 전처리를 적용한 데이터 - mart: 비즈니스 레벨에서 자주 조회가 필요한 메트릭들을 집계한 데이터 Athena는 표준 SQL을 사용해 Amazon S3에 저장된 데이터를 간편하게 분석할 수 있는 대화식 쿼리 서비스입니다. 서버리스 서비스로 관리할 인프라가 없고 실행한 쿼리에 대해서만 비용을 지불하면 됩니다. Trino 기반의 쿼리 엔진을 사용 중이며 대용량 데이터 처리에 강점을 가집니다. 버즈빌에서는 Athena를 통해 Redshift에서 처리하기 힘든 대용량 데이터들을 전처리 및 쿼리 합니다. - Redshift Redshift는 AWS의 데이터 웨어하우스 서비스입니다. 대량 병렬 처리를 통해 복잡한 쿼리라도 빠른 속도로 실행 가능하며, 일반 RDBMS처럼 쿼리가 가능하기 때문에 Athena보다 사용성이 높습니다. 또한, 온디맨드로 클러스터 비용을 지불하는 방식이라 스캔한 데이터 양에 비례하여 비용을 지불하는 Athena보다 쿼리 비용에 대한 부담이 적습니다. 때문에, 비즈니스단에서 adhoc 한 쿼리를 실행하거나 대시보드를 생성하는 용도로 사용합니다. - Airflow Airflow는 Airbnb에서 개발한 오픈소스 데이터 파이프라인 관리 툴입니다. 다양한 스케줄링 기능들을 제공하며 WEB UI를 통해 상세한 모니터링이 가능하다는 점에서 많은 기업이 채택하여 사용 중입니다. 버즈빌에서는 운영계 데이터를 분석계로 동기화하는 작업, 마트 데이터 집계 및 스케줄링을 하는 데 주로 사용하고 있습니다. ## 분석가/엔지니어를 위한 셀프 서빙 플랫폼 데이터 파이프라인을 생성하기 위해서 위에 소개해 드린 데이터 플랫폼 컴포넌트들을 학습해야 합니다. 예를 들어, 데이터 분석가가 Daily Active User 정보가 담긴 Athena 테이블을 집계하고 스케줄링하려고 합니다. 이때, 아래와 같은 사항들을 고려해야 합니다. - **파티셔닝을 어떻게 적용할 것인가?** - 파티셔닝이란 데이터를 분할 저장하여 일부 데이터만 검색할 수 있도록 하는 기법입니다. 파티셔닝 컬럼으로 필터링하여 쿼리 비용을 절감하고 쿼리 성능을 높일 수 있습니다. 특히 Athena는 스캔한 데이터 양에 비례하여 과금하기 때문에 파티셔닝을 적용하는 것이 중요합니다. - 버즈빌 내에서는 공통으로 `/year=2023/month=05/day=29/hour=12`와 같이 hour 단위까지의 prefix를 파티션으로 활용 중입니다. 필요에 따라 daily, monthly 파티셔닝 적용이 되는 경우가 있습니다. - 쿼리로 직접 파티셔닝 컬럼과 저장 경로 prefix를 매핑해주거나 AWS Glue의 crawler 기능을 이용하여 자동으로 적용할 수 있습니다. - **원천 데이터의 완전성을 어떻게 보장할 것인가?** - 원천 데이터가 완전하지 않은 상태에서 마트 테이블을 집계하는 경우가 종종 발생합니다. 예시로 Daily Active User를 계산할 때, 원천 데이터인 유저 행동 데이터가 delay로 인해 늦게 채워질 수 있습니다. - 이를 해결하기 위해서는 Airflow에 Sensor 기능을 활용할 수 있습니다. Airflow에서는 ExternalTaskSensor를 통해 다른 Dag의 success 기록이 있어야만 해당 Dag가 실행될 수 있도록 dag 간의 dependency를 적용할 수 있습니다. 원천 데이터가 모두 채워져 있는지 체크하는 dag를 upstream dag로 설정하여 데이터의 완전성을 보장할 수 있습니다. - **데이터 파이프라인이 멱등성을 보장하는가?** - 멱등성을 보장하는 데이터 파이프라인이란, 동일한 입력 데이터로 데이터 파이프라인이 여러 번 실행되더라도 결과가 달라지지 않는다는 뜻입니다. 멱등성을 보장하여 구성하면 장애 발생 시 재실행을 통해 쉽게 복구가 가능하다는 이점이 있습니다. 이를 위해서는 delete 로직을 앞에 넣어주어 과거 데이터를 초기화시켜 주는 작업이 필요합니다. 데이터 파이프라인 생성에 대한 가이드를 제공하더라도 러닝 커브는 여전히 높습니다. 버즈빌에서는 위 내용들을 추상화하여 간단한 YAML 파일 작성을 통해 Airflow Dag를 생성할 수 있도록 템플릿을 제공했습니다. 사용자는 Airflow, Athena, Redshift를 따로 학습할 필요 없이 빠르게 데이터 파이프라인을 생성할 수 있습니다. YAML 파일 예시는 다음과 같습니다. ```yaml --- pipeline_key: dau pipeline_type: athena_process pipeline_dag_configs: start_date: 2023-03-01 15:00:00 schedule_interval: "0 15 * * *" upstream_dependencies: - dag_id: check_data_athena_ad_request__24hr_kst_existence athena: process_query: | SELECT DISTINCT viewer_id AS viewer_id, DATE_TRUNC('day', date) AS kst_at FROM ad_request WHERE partition_timestamp >= TIMESTAMP'{var[start_time]}' AND partition_timestamp < TIMESTAMP'{var[end_time]}' AND output_bucket: "buzzvil-example-bucket" output_prefix: buzzvil/gold/dau/year={var[year]}/month={var[month]}/day={var[day]} table: m_dau columns: - name: viewer_id type: STRING - name: unit_id type: BIGINT ```
YAML 파일 내의 config 들에 대한 설명 - `pipeline_key`: 파이프라인의 이름입니다. airflow에서 dag_id 생성 시 이 값을 사용합니다. - `pipeline_type`: 파이프라인의 타입입니다. Athena 테이블을 생성하는 작업을 스케줄링하는 경우 athena_process 파이프라인을 사용합니다. 이외에도 redshift 테이블을 스케줄링 하는 경우, mysql에 있는 데이터를 s3로 내리는 파이프라인 등이 있으며 데이터 파이프라인 목적에 맞게 설정합니다. - `pipeline_dag_configs`: start_date와 shedule_interval와 같은 dag config를 여기서 설정합니다. daily active user를 스케줄링하는 경우 매일 정각에 실행되기 때문에 “0 15 * * *”으로 설정했습니다. - `upstream_dependencies`: dag 간의 upstream dependency를 설정하는 config입니다. 원천 테이블의 최근 24시간 데이터가 모두 들어왔는지 확인하는 dag를 upstream으로 설정합니다. - `athena.process_query`: Athena에서 수행할 쿼리를 설정합니다. Jinja template을 적용하여 parameterize된 변수들을 활용할 수 있도록 커스터마이징했습니다. var[start_time]과 var[end_time]은 airflow의 data_interval_start와 data_interval_end가 채워집니다. - `athena.output_bucket`: athena.process_query에서 수행된 결과 데이터를 저장할 S3 bucket name입니다. - `athena.output_prefix`: athena.process_query에서 수행된 결과 데이터를 저장할 S3 bucket_prefix입니다. Jinja template이 적용되어 date_interval_start의 year, month, day 정보가 각각 {var[year]}, {var[month]}, {var[day]}로 변환됩니다 - `athena.table` , `athena.columns`: Athena 테이블 스키마 정보입니다.

위 YAML 파일은 아래 그림과 같은 Dag로 변환됩니다. ![airflow-dag](https://tech.buzzvil.com/blog/%ec%85%80%ed%94%84-%ec%84%9c%eb%b9%99-%eb%8d%b0%ec%9d%b4%ed%84%b0-%ed%94%8c%eb%9e%ab%ed%8f%bc-%ea%b5%ac%ec%b6%95%ed%95%98%ea%b8%b0/airflow-dag.png)
dag의 각 task에 대한 설명 `check_data_athena_ba_v_ad_request__24hr_kst_existence`: 소스 테이블인 ba_v_ad_request 테이블이 24시간 동안 데이터가 완전하게 들어왔는지 체크하는 dag입니다. `procced_athena_query`: 입력된 쿼리를 바탕으로 Athena의 CTAS 쿼리를 실행하여 생성된 데이터를 s3에 저장하는 역할을 합니다. 멱등성을 보장하기 위해 해당 경로에 미리 생성된 데이터가 있다면 먼저 삭제를 진행하는 작업도 이 task 안에 포함되어 있습니다. `partition_athena_table`: yaml 파일 내의 output_prefix 경로를 파티셔닝 컬럼으로 수동 매핑해줍니다. 수동 매핑 작업도 파이프라인에 포함되어 스케줄링됩니다. `metric_query_total`, `metric_export_total`, `update_dag_metadata` : dag의 통계 정보들을 수집하는 task입니다.

YAML 템플릿에서 생성된 Dag는 데이터 파이프라인 생성 시 고려해야 하는 문제들을 다음과 같이 추상화하여 해결했습니다. 사용자는 Airflow의 다양한 기능 등을 학습할 필요가 없어졌으며, Athena 테이블을 조회하기 위해 필요한 작업을 직접 구현할 필요가 없어졌습니다. - **파티셔닝을 어떻게 적용할 것인가?**. - YAML 파일에서 파티셔닝을 적용할 prefix을 설정해 주면 `partition_athena_table` task에서 매핑 작업이 자동으로 스케줄링 됩니다. 매핑 방법을 고민할 필요 없이 prefix를 입력만 해주면 파티셔닝이 함께 적용됩니다. - **소스 데이터의 완전성을 어떻게 보장할 것인가?** - Airflow의 Sensor 기능을 이해할 필요 없이 yaml 내의 `upstream_dependency` 값을 입력하면 자동으로 다른 dag와 dependency가 추가가 됩니다. - **데이터 파이프라인이 멱등성을 보장하는가?** - `proceed_athena_query` task에서 쿼리 실행 결과를 S3에 저장할 때 해당 경로의 파일들을 지우는 작업을 선행합니다. 사용자는 멱등성 개념을 직접 구현하지 않아도, 파이프라인에서 자동으로 적용이 됩니다. ## 코드 구성 지금까지의 예시는 Athena 테이블을 스케줄링하는 파이프라인이었습니다. 마찬가지로 Redshift 테이블을 스케줄링하는 파이프라인, S3에서 Mysql로 동기화하는 파이프라인 등 다양한 목적의 파이프라인들이 필요했습니다. 비슷한 형태의 파이프라인이 반복적으로 생겨났기에, 팩토리 패턴을 적용하여 반복적인 코드 작성을 줄이고 DAG의 생성과 관리를 자동화하였습니다. 팩토리 패턴의 구현은 아래와 같습니다 ![dag-factory](https://tech.buzzvil.com/blog/%ec%85%80%ed%94%84-%ec%84%9c%eb%b9%99-%eb%8d%b0%ec%9d%b4%ed%84%b0-%ed%94%8c%eb%9e%ab%ed%8f%bc-%ea%b5%ac%ec%b6%95%ed%95%98%ea%b8%b0/dag-factory.png) `DagBuilder`라는 추상 베이스 클래스를 만들고 이 인터페이스를 사용하여 필요한 파이프라인들을 추가합니다. 이를 통해, 기존 코드의 수정 없이 새로운 파이프라인을 추가/수정할 수 있었습니다. 예를 들어, Mysql에서 S3로 동기화하는 파이프라인이 필요한 경우 MysqlUnloadS3DagBuilder를 구현하여 새로 추가해 주면 됩니다. `DagConfig` 클래스는 Dag 구성에 필요한 명세들을 받습니다. 최종적으로 `DagFactory` 클래스에서는 `Dag Builder`와 `Dag Config`를 받아 Dag를 생성하게 됩니다. 사내에서 관리하는 airflow 깃 레포지토리 안에는 각 파이프라인에 해당하는 폴더들이 있습니다. 사용자는 각자 자신이 사용하려는 파이프라인 아래에 YAML 파일을 추가합니다. 각 YAML 파일은 폴더의 DagBuilder를 받아서 dag로 생성됩니다. ``` Airflow Pipeline ├── ... ├── athena_process │ ├─- dau.yaml # athena_process_dau라는 dag로 변경됨. │ ├── mau.yaml │ ├── example_1.yaml │ └── example_2.yaml └── mysql_unload_s3 ├── example_1.yaml # mysql_unload_s3_example_1 dag로 변경됨 ├── example_2.yaml └── example_3.yaml ``` ## 마치면서 데이터 엔지니어가 사내의 모든 데이터 파이프라인을 관리하기는 어려운 일입니다. 데이터 파이프라인 생성에 필요한 데이터 맥락을 이해하는 데 큰 시간이 소요되며, 반복적인 업무는 디모티베이션을 일으킵니다. 버즈빌에서는 이러한 문제를 해결하기 위해 셀프 서빙 데이터 플랫폼 구축을 시도했고, 결과적으로 각 도메인의 전문성도 살리면서 데이터 엔지니어는 플랫폼 기능 강화에 더욱 집중할 수 있게 됩니다. 비슷한 고민을 하는 팀이 있다면 이 글이 도움이 됐으면 좋겠습니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [광고 예산 제어 시스템 개선기: Part 2 – 더 정교한 시스템, 그리고 성과](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-2) Date: 2023-02-15 | Author: Bale Do, Liam Hwang | Category: Backend 안녕하세요, 광고 엔진 팀의 Bale, 그리고 Liam입니다. 1편에서 버즈빌의 예산 제어 시스템 개선 여정과 가시성에 대해 얘기해 드렸는데요. 이번 편에서는 이전 시스템 수준 개선을 바탕으로 어떤 기능 개선과 비즈니스 임팩트를 만들었는지 소개 드리려 합니다. 1편의 개선을 바탕으로 이야기가 전개되니 [여기]()에서 이전 여정을 먼저 읽어보시는 걸 추천드립니다. ## 더 정교한 예산 제어 시스템으로 계획된 예산을 더욱 정교하게 컨트롤하는 것은 여러 광고 시스템에서 공통적으로 풀고자 하는 예산 제어 시스템의 일반적인 문제입니다. 그에 대한 답은 어찌 보면 당연하게도 예산 제어를 더 자주 실행하고 세밀하게 조절하는 것인데요. 저희는 크게 3가지 개선을 진행하여 더 정교한 예산 제어 시스템을 달성할 수 있었습니다. ### 1. 더 자주 평가합니다 더 자주 평가하는 것은 예산 목표 달성과 성과 최적화에 있어 아주 중요합니다. 예를 들어 10분 단위로 예산 제어를 수행한다고 가정했을 때 그 사이에서 발생한 지출 스파이크에 의해 초과 지출이 발생할 수 있고 이러한 제어 실패가 누적되면 최악의 경우 예산 목표를 달성할 수 없거나 성과 최적화를 고려한 지출 계획이 무산될 수 있습니다. 하지만 버즈빌의 초기 예산 제어는 꽤 큰 주기로 동작했음에도 어느 정도 기능을 잘 수행했습니다. 어떻게 가능했을까요? 이는 실패가 누적되어도 지출 계획에 의해 교정될 수 있기 때문입니다. 예를 들면 시간당 10만 원을 소진해야 하는 캠페인이 12만 원씩 5시간을 소진하게 되면 그다음 한 시간 동안은 지출을 막아 계획대로 교정하게 되고 최종적으로는 목표 예산에 근접할 수 있기 때문입니다. 예시를 보면 알 수 있듯 평가 주기를 줄일수록 오차 교정이 많아지기에 더 정교하게 지출 계획을 달성할 수 있습니다. 버즈빌은 주기마다 모든 활성 캠페인에 대해 평가를 진행하고 있어 평가 성능과 지출 집계 등의 지연 시간에 의존합니다. 1편의 여정을 거치면서 마이크로서비스로 분리하며 성능상 이점을 확보하고 지출 집계를 개선하면서 지연 시간을 줄여 현재 버즈빌의 모든 캠페인은 15초에 한 번 평가되고 있으며 초기 상태와 비교해 훨씬 안정적으로 지출 계획을 달성하고 있습니다. ### 2. 더 세밀하게 지출을 계획합니다 만약 제어 평가 주기를 충분히 줄였으나 1시간당 10만 원의 지출 계획을 세우면 어떤 문제가 생길 수 있을까요? 아마 가장 큰 문제는 10분 만에 10만 원을 모두 지출해버리고 50분 동안 광고가 송출되지 않는 것입니다. 이는 지속적으로 광고가 노출되지 않는 그 자체로 문제가 될 수 있고 초반 지출에 의해 기회가 많은 시간에 예산을 못쓰게 되어 성과 최적화에도 영향을 줍니다. 초기 버즈빌은 1시간 단위로 지출을 계획했는데, 현재는 1분 단위로 지출을 계획하고 있습니다. 지출 계획의 범위를 좁히는 것은 어려운 일이 아니나 지출 계획의 범위가 평가 주기보다 큰 경우 사실상 의미가 없기 때문에 이 일 또한 평가 주기를 줄일 수 있는 시스템의 성장 덕분이었습니다. ### 3. 더 세밀하게 광고 송출량을 제어합니다 최초 예산 제어 로직은 예산 평가 후 지출이 필요하면 광고 송출을 활성화하고, 반대의 경우 비활성화하는 방식으로 동작했습니다. 충분히 동작하는 방식이지만, 만약 광고 트래픽이 10배, 100배 늘어난다면 어떻게 될까요? 기존 대비 100배 트래픽에 대해 송출이 활성화된다면 순식간에 엄청나게 많은 지출이 발생하고, 결국엔 제어 주기와 지출 계획을 줄였음에도 불구하고 의도대로 제어할 수 없을 것입니다. 버즈빌은 지난 몇 년에 걸쳐 제휴 지면이 늘어나면서 트래픽이 상당히 증가하였고, 광고 송출을 활성/비활성 하는 기존 방식으로는 예산 소진을 부드럽게 제어하기 힘든 상태였습니다. 저희는 [링크드인이 게재한 논문](http://www0.cs.ucl.ac.uk/staff/w.zhang/rtb-papers/linkedin-pacing.pdf)에서 좋은 사례를 접할 수 있었는데요. 제어 방식만 보자면 광고 송출량을 활성/비활성 방식이 아닌 송출률로 제어하는 방식입니다. 이는 송출률을 점진적으로 조정하여 지출을 부드럽게 제어할 수 있고 트래픽이 늘어나더라도 일정 수준까지는 유연하게 대처 가능합니다. 실제 제안하는 방식은 더 디테일한 내용이 많아 궁금하시다면 원문을 읽어보시길 추천드립니다.
개선 전 - 제어 과정에서 작은 스파이크 발생
개선 후 - 부드럽게 예산 목표 달성
저희는 현재 버즈빌의 상황에 맞추어 적용하였고 최종적으로 1. 더 자주 평가하고 2. 세밀하게 지출을 계획하고 3. 세밀하게 송출량을 조절하여 위와 같은 긍정적인 변화를 만들 수 있었습니다. ## 더욱 빠르게 움직이기 지금까지 광고 시스템을 개선해왔는데요. 운이 좋게도 이러한 개선이 완료될 무렵, 광고 성과 최적화, 캠페인 운영 편의성 등을 개선하는 비즈니스 요구사항을 만날 수 있었습니다. 요구사항의 디테일은 다양했는데 각 요구사항은 광고 상품별로 다르게 적용되기도 하고 광고 목적에 따라 다르게 적용되기도 했습니다. 이때까지만 해도 제어력을 높이는데 열을 쏟던 터라 디테일한 내부 구성에 대해서는 자신감 있게 이해한 상태는 아니라서 먼저 내부 로직을 살펴보기 시작했습니다. 내부 로직은 모든 캠페인에 대한 예산 제어 동작을 추상화한 상태였는데 과도기에 발생한 비즈니스 요구사항들을 광고 상품, 광고 목적 등에 따라 코드 여러 곳에서 분기하여 처리하고 있어 동작을 파악하기 힘들고 변경 시 사이드 이펙트를 예측하기 어려운 상황이었습니다. 팀에서는 예산 제어 문제 영역에 대한 충분한 성숙도가 없는 상황에서 현재 상태는 [이른 추상화](https://wiki.c2.com/?PrematureAbstraction)로 판단되어 우선 변경에 유연한 구조로의 리팩터링을 결정했습니다. 현재 요구사항 및 기존 코드의 구현으로부터 구분되는 몇 가지 패턴을 찾아 ‘예산 제어 전략'을 도출하고 각 전략별로 제어 로직이 별도로 구현되도록 추상화 수준을 낮추었습니다. 이 과정에서 새로운 요구사항인 각 소진 전략별로 안전한 소진 마감을 위한 조기 마감 기능, 운영 리소스를 절감을 위한 일 지출 예산을 평균적으로 보장하는 기능 등을 추가하게 되었는데요. 이는 한 번에 많은 캠페인의 제어 로직을 변경하는 작업이라 이전보다 더 높은 수준의 검증이 필요했습니다. 이 문제를 만나기 전부터 예산 제어 로직 검증 과정의 개선이 필요하다 생각했는데요. 일반적인 API의 경우 요청에 대해 1. 응답이 즉각적이고 2. 상태 변화가 없거나 3. 시간의 흐름에 따른 검증이 크게 필요 없는 경우가 많기 때문에 단위 테스트와 통합 테스트에 포함시키면 자동화된 CI를 통해 검증이 가능한 반면 예산 제어 시스템은 1편에서 얘기했듯 일종의 피드백 루프를 가져 부분적으로 검증되어도 최종적으로 잘 동작한다는 자신감을 갖기 어려웠습니다. ![](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-2/simulator-overview.png) 이에 팀에서는 검증 기간을 단축하고 작업자와 리뷰어가 같은 결과를 보고 상호 신뢰할 수 있도록 캠페인 시뮬레이터를 개발하기로 결정했습니다. 시뮬레이터는 간단한 커맨드로 개발되었으며 광고 요청 트래픽을 생성하고 소진 제어 시스템이 결정하는 예산 제어 상태에 따라 캠페인 송출 및 노출 이벤트를 생성하고 이를 그라파나 패널로 시각화하여 리뷰 과정에 이용할 수 있도록 구성했습니다.
시뮬레이터는 이후 로직 개발 및 리뷰 과정에서 활용되었으며 팀에서는 기존 개발 및 검증에 1주일 정도 걸렸던 작업들도 평균 1-2일 내에 상용 환경에 반영할 수 있었습니다. 그리고 작업자로서는 불안함에 만성적으로 모니터링하는 시간이 줄어 만족스러웠고 시뮬레이터가 작업된 11월부터 자신감이 붙어 배포 빈도가 눈에 띄게 늘어난 것을 확인할 수 있었습니다. ![](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-2/realtime-dashboard.png) 물론 상용 환경 검증에서의 긴장감도 놓을 순 없었는데요. 이는 초기 수 분의 지연 시간을 가진 분석계 로그 기반의 패널에서 가시성 확보 과정에서 만든 실시간 패널로 이전하면서 현재 상태를 즉각적으로 확인하고 대응할 수 있어 전보다 더욱 자신감 있게 진행할 수 있었습니다. 또한 이 기능은 제품팀뿐만 아니라 캠페인 운영에도 도움이 될 것이라 판단하여 내부 캠페인 운영 도구에도 이식되어 유용하게 쓰이고 있습니다. 여기까지 비즈니스 요구사항이 늘어남에 따라 필요한 변경을 수행하고, 시뮬레이터를 만들어 검증 기간을 크게 단축한 경험을 얘기드렸는데요. 이 과정에서 각 상품에 대한 이해도도 높아졌고 동시에 예산 제어 시스템을 더 깊게 이해할 수 있게 되었습니다. 다음으로는 경험과 이해를 바탕으로 실제 성과까지 이어진 경험을 소개드리고자 합니다. ## 복리로 돌아오는 개선 [라이브커머스 춘추전국시대의 생존법](https://www.buzzvil.com/ko/blog/view/379)에서 소개 드린 대로 라이브커머스 광고는 운영 초반에 많은 사용자에게 노출하는 게 중요한 상품입니다. 라이브커머스 상품은 방송 중에 최대한 모객 되어야 하는 상품이기에 저희는 짧은 시간 안에 많은 노출을 발생시키고 있는데 이 중 일부는 사용자 환경에 따라 지연 노출되기도 합니다. 방송 종료 후에는 유입이 일어나더라도 광고주에게는 의미가 없고 광고 시스템도 불필요한 지면 구매 비용을 감수해야 해서 상품 수익성에도 악영향이 있는 상태였습니다. ![](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-2/live-commerce.png) 라이브커머스 개선을 위한 첫 접근은 예산 제어 시스템이 고도화되면서 자연스럽게 일정 부분 개선될 것이라는 가설이었습니다. 저희는 지금까지 진행한 고도화 여정에서 틈틈이 라이브커머스 캠페인이 종료 후 소진되는 비율이 개선되는지를 확인했는데 아쉽게도 유의미한 개선은 확인할 수 없었습니다. 하지만 라이브커머스 상품 종료 후 발생하는 과도한 노출 문제는 명확하게 존재했고, 이를 해결하면 상품성과 수익성 모두 개선할 수 있기에 지금까지 시스템을 개선하면서 쌓아 온 자신감에 힘을 얻어 이 문제를 풀어보는 게 어떨지 팀에 제안하여 진행할 수 있게 되었습니다. 저희는 우선 문제를 더 잘 이해하기 위해 데이터를 확인하기 시작했습니다. 평균적으로 전체 노출의 약 10-15%가 운영 종료 후 발생하고 있었는데요. 광고 송출로부터 노출이 집계되기까지의 지연시간 분포를 확인했을 때 최대 2시간까지 지연되는 경우도 있었지만, 약 75% 노출은 광고 송출 시점으로부터 15분 이내에 집계되고 있었습니다. 라이브커머스 캠페인은 대부분 1-2 시간 이내의 운영시간을 가지기 때문에 운영 종료 후 노출 트래킹 지연이 미치는 영향이 큰 것으로 확인되었습니다. 여기서 광고 예산을 선형적으로 배정하지 않고 지수적 감소하도록 배정하여 운영 후반부에는 지연 발생하는 노출을 최대한 제어에 포함한다면 최종적으로 방송 종료 후 발생하는 노출이 줄어들 것이라는 가설을 세웠습니다. ![](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-2/cumulative-benefit.png) 가설을 검증하고 적용하는 일은 그동안 쌓아온 개선이 복리로 돌아와 수월하게 진행할 수 있었습니다. 이전에 여러 소진 패턴의 확장을 고려해 추가한 소진 타입 덕에 새로운 소진 타입을 쉽게 추가할 수 있었고 시뮬레이터에 지연 노출까지 반영할 수 있는 기능을 추가하여 상용 환경 배포 전에 개선과 동작을 확인할 수 있었습니다. 이 일들은 일사천리로 진행되어 구현, 검증 그리고 상용화까지 2일 만에 달성할 수 있었습니다. 최종적으로 방송 시간 종료 후 발생하는 매체 지출을 약 60% 줄일 수 있었고 상품의 전체 수익성 또한 약 3.5% 개선되었습니다. 동시에 방송시간 초반에 유입을 더 늘려서 후반부에 안정적으로 마감함으로써 라이브커머스 상품에 대한 광고주의 만족도도 높일 수 있었습니다. 결과도 만족스럽지만 풀어나가는 과정에서 그동안 점진적으로 개선해온 것의 이점을 피부로 느낄 수 있어서 더 좋은 경험이었습니다. ## 마무리하며 지금까지 광고 엔진팀의 예산 제어 시스템 고도화 여정을 읽어주셔서 감사합니다. 꽤 많은 내용을 다뤘는데, 요약해 보자면 가장 먼저 시스템의 핵심 입력과 출력, 그리고 핵심 지표를 정의하고, 목표 상태를 설정하였습니다. 그리고 목표한 상태에 도달하기 위해 한 번에 모든 것을 바꾸지 않고 점진적으로 개선해 나갔습니다. 그 과정에서 시스템의 가시성을 확보해나가면서 심리적 안정감을 갖게 되었고 가설 검증 속도를 가속할 수 있었습니다. 이것이 끝이 아닙니다. 앞으로 팀이 그리고 있는 계획의 일부만 진행한 상태인데요. 예를 들어 인벤토리 물량을 예측하는 모델을 만들어 예산 배정 전략을 고도화하거나, 예산 제어 상태를 결정하는 다양한 알고리즘 또는 모델을 실험해 볼 계획입니다. 이 이외에도 광고 성과를 개선하기 위한 다양한 문제들을 풀어나갈 예정이니 다음 글도 기대해 주시기 바랍니다. 이 모든 과정은 다양한 팀의 피드백과 지원이 있었기 때문에 가능했습니다. 특히 예산 제어 로직 고도화에 같이 힘써주신 Ad Management, CM 팀, 그리고 다양한 플랫폼적인 지원을 해주신 Datavil, DevOps 팀에 감사의 말씀을 전합니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [광고 예산 제어 시스템 개선기: Part 1 – 시스템과 가시성 개선](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-1) Date: 2023-02-08 | Author: Liam Hwang, Bale Do | Category: Backend ## Introduction 안녕하세요, 광고 엔진 팀의 Bale, 그리고 Liam입니다. 버즈빌 광고 플랫폼의 변천 과정을 소개해 드리는 시간입니다. 최근에는 [수평 확장 가능한 광고 서버를 만드는 법]()에 대해 소개해 드렸는데요. 이번에는 버즈빌의 광고 플랫폼에서 광고 서버와 만만찮게 중요한 기능인 예산 제어 시스템을 어떻게 개선해나가고 있는지 살펴보고자 합니다. 예산 제어는 캠페인에 할당된 예산을 운영 기간 동안 잘 분배해 광고 성과를 극대화하기 위한 기능입니다. 만약 예산 제어를 하지 않거나 잘못 동작한다면, 캠페인은 게시되자마자 한 번에 예산이 다 소진되거나 갑작스러운 지출 스파이크가 발생할 수 있습니다. 또한 단기간 내에 모든 예산을 사용하여 유입이 이뤄진다면, 장기적으로 최적의 성과를 얻을 수 없고 광고주의 만족도 하락으로 이어질 것입니다. 초기 버즈빌의 예산 제어 기능은 광고 서버에 통합된 커맨드 형태로 구현되어 있었으나, 다양한 비즈니스 요구사항들을 수용하기 위해 변경을 거듭했고 현재는 별도 마이크로서비스로 분리하여 고도화를 해나가고 있습니다. 이번 시리즈에서는 버즈빌에서 예산 제어 시스템을 고도화하면서 얻은 인사이트를 공유하고자 합니다. ## 예산 제어 시스템 소개 가장 먼저 광고 시스템에서 예산 제어가 어떻게 작동하는지 살펴보도록 하겠습니다. 예산 제어 시스템은 현재까지의 광고 예산 소진액이 계획된 예산 소진 속도와 비교해서 빠른지 느린지를 판단하여 다음 주기 동안의 예산 지출 속도를 결정하는 “예산 제어 상태”를 도출해냅니다. 도출된 예산 제어 상태는 광고 서버에 반영되어 개별 캠페인의 입찰 빈도를 결정하는데 활용합니다. 입찰 빈도의 증감에 따라 광고의 송출량이 결정되고, 그에 따라 실제 예산 지출 속도가 빨라지거나 느려집니다. 간단히 말하자면 예산 소진 속도가 계획보다 빠르면 줄이고, 소진 속도가 느리다면 빠르게 만드는 제어 시스템입니다. 특정 주기에 결정된 예산 제어 상태는 다음 주기의 예산 소진 속도에 영향을 주기 때문에 일종의 피드백 시스템이라고 볼 수 있습니다. ![예산 제어 시스템 개요](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-1/budget-pacing-overview.png) ### 예산 제어 시스템의 고려 사항 캠페인의 대상 오디언스가 예산에 비해 충분한 상황이라고 했을 때 배정된 일 예산 또는 전체 예산을 넘겨서 사용하거나 미달하면 안 됩니다. 또한 성과를 극대화하기 위해서는 너무 빠르게 예산이 사용되어서도, 너무 느리게 사용되어도 안됩니다. 이런 정교한(?) 제어가 잘 수행되기 위해서는 크게 다음 사항들을 고려해야 합니다. #### 1. 캠페인의 현재 지출 목표 예산은 다양한 시계열을 기준으로 분배됩니다. 캠페인 전체 운영 기간 동안 예산을 매일 얼마나 배정해야 할지, 그리고 하루 동안에는 매시간, 분 단위로 예산이 얼마나 지출되어야 하는지 결정할 수 있어야 합니다. 일간 예산을 매시간마다 동일하게 나눠서 지출할 수도 있겠지만, 캠페인의 대상 오디언스가 많은 시간대에 집중적으로 지출을 하는 것이 전환 성과가 더 좋을 것입니다. 따라서 트래픽 패턴, 그리고 실제로 캠페인 대상과 부합하는 오디언스의 비중 등을 고려해 매 분 단위로 지출 목표 금액을 미리 산정할 수 있어야 합니다. #### 2. 현재까지 지출된 예산, 그리고 곧 지출로 전환될 예산 목표 예산이 있다면 현재까지 지출된 예산을 알아야 판단을 할 수 있겠죠. 그래서 현재까지 지출된 예산 정보를 필요로 하는데요. 광고 송출 시점으로부터 사용자가 광고와 상호작용하여 노출, 클릭, 전환 등의 지출 이벤트가 발생할 때까지의 짧게는 수초에서 길게는 수시간의 지연이 발생합니다. 또한 이벤트가 발생한 예산이 집계될 때까지의 데이터 파이프라인 지연시간도 존재하죠. 정확한 예산 제어를 하기 위해서는 파이프라인의 집계 지연을 가능한 줄이고 광고 송출은 했으나 아직 발생하지 않은 지출 이벤트가 얼마나 될지 예측할 수 있어야 합니다. #### 3. 예산 제어 상태 결정 예산 제어 상태를 자동차로 비유하자면 액셀러레이터, 브레이크와 비슷합니다. 달리는 자동차가 목표한 지점에서 잘 멈추기 위해서는 브레이크를 적절한 시점부터 밟아야 할 텐데요. 예산 제어 시스템은 예산 지출 목표와 지출 예산을 가지고 액셀러레이터를 밟을 것인지 브레이크를 밟을 것인지, 얼마나 세게 밟을 것인지를 결정해야 합니다. 빠르게 달리다가 급하게 브레이크를 밟으면 쭉 밀려나가서 위험한 것처럼, 캠페인 예산 지출도 종료지점을 미리 인지하고 점진적으로 속도를 줄일 수 있어야 목표 예산보다 과도하게 지출하는 것을 방지할 수 있습니다. ## 예산 제어 시스템 개선 여정 최초에는 광고 할당, 캠페인 관리, 예산 제어 기능이 모노리틱 광고 서버에 모두 포함되어 있는 형태였습니다. 송출된 광고에 대해 노출, 클릭 이벤트가 발생하면 실시간 예산 소진 상태를 기록하는 Redis에 소진 상태를 업데이트하고, 배치 커맨드(Jenkins)를 활용해 주기적으로 운영 중인 모든 광고의 실제 소진 예산과 목표 소진 예산을 비교하여 캠페인 송출을 중단하거나 재개하도록 구현되어 있었습니다. 개략적으로 표현하면 아래와 같습니다(디테일은 대부분 생략되었습니다). ![](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-1/system-1.png) 기존 시스템은 당시의 목표는 달성했지만 계속해서 버즈빌의 캠페인 풀이 성장하고 광고 상품도 다변화됨에 따라 예산 제어 로직의 고도화가 필요했습니다. 고도화로 인한 복잡도를 줄이기 위해 단일 광고 서버에서 캠페인 메타데이터를 관리하는 캠페인 관리 시스템과 광고 예산 제어 시스템의 관심사를 분리하기로 결정하였습니다. 시스템을 한 번에 분리하기에는 캠페인 관리 시스템과 예산 제어 시스템의 의존도가 상당히 컸는데, 약 2년 반 동안 여러 단계로 나눠 분리를 진행했습니다. ### 1단계. 로직을 그대로 새로운 마이크로서비스로 옮겨서 분리하기 ![](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-1/system-2.png) 저희는 기존 구현에 대해 젠킨스 커맨드는 앞으로 필요한 성능 제공이 어려울 것 그리고 광고 서버 소스코드를 공유하는 형태는 앞으로 더 큰 구조적 복잡성과 효율 저하를 가져올 것이라는 판단하에 먼저 동일 기능을 수행하는 별도 마이크로서비스로의 분리를 진행했습니다. 이 과정에서 기존 캠페인 데이터베이스에 직접 접근하던 방식에서 광고 서버가 제공하는 API를 이용해 소진 상태를 업데이트하는 방식으로 변경했습니다. API 호출은 DB에 직접 접근하는 방식에 비해 네트워크 실패를 핸들링하거나 더 길어진 지연시간을 고려하는 등 추가적으로 대응해야 하는 부분들이 많았지만, 예산 제어 시스템은 일부 지연시간을 허용하고, 주기적으로 현재 상태에 기반한 새로운 소진 상태를 도출해 내기 때문에 최종적 일관성을 보장하는 수준에서 서비스 분리를 마무리했습니다. 예산 제어 시스템을 별도 마이크로서비스로 분리함에 따라 더 명확해진 컨텍스트 아래 오너십을 분리할 수 있었고 젠킨스 커맨드에서 활용하지 못한 APM, Prometheus 등을 활용해 서비스 가시성을 확보하는 등 필요한 기능을 자유롭게 추가해나갈 수 있었습니다. ### 2단계. 광고 송출 서버 분리 및 통계 시스템 구현 ![](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-1/system-3.png) 기존의 모노리틱 광고 서버는 광고 송출뿐 아니라 캠페인 관리, 지출 집계 등 많은 기능을 수행했는데요. 여기서 성능, 시스템 복잡도 등의 문제로 광고 송출 서버와 지출 집계를 수행하는 통계 시스템을 별도 마이크로서비스로 분리했습니다. 이 과정도 재밌는 얘깃거리가 많은데 예산 제어 맥락에서 벗어난 부분이 많아 기회가 되면 따로 다루어보겠습니다. ### 3단계. 광고 이벤트 트래커 구현 및 이벤트 집계 파이프라인 구현 ![](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-1/system-4.png) 앞서 설명드렸듯이 예산 제어 시스템은 집계되는 소진 정보와 캠페인의 예산 지출 설정에 기반해 예산이 의도대로 분배되도록 제어합니다. 이 시스템은 소진 정보 집계 파이프라인의 안정성과 지연 시간에 꽤 의존적인데 이 파이프라인이 여러 레거시들이 합쳐지면서 꼬여있어 관리하기 힘든 상태에 도달해 당시 전사적으로 도입한 kafka를 이용하여 집계 파이프라인을 최적화했습니다. 이 개선을 통해 집계 파이프라인이 안정화되고 소진 집계 지연이 줄어들어 최종적으로 예산 제어 시스템의 주기를 많이 줄일 수 있었습니다. ### 4단계. 캠페인 모델에서 예산 제어 상태 분리 ![](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-1/system-5.png) 예산 제어 커맨드를 마이크로서비스로 분리하고, 시스템 가시성을 확보했지만 아직 발목을 붙잡는 문제가 있었습니다. 이는 “예산 제어 상태"가 캠페인 모델의 속성으로 묶여있다는 점이었는데 이로 인해 우리는 “예산 제어 상태"가 업데이트될 때마다 캠페인을 수정해야 했습니다. 얼핏 보기에는 큰 문제점이 없을 수도 있겠지만 “예산 제어"는 운영 중인 모든 캠페인을 대상으로 1분에 1번씩 이루어졌기 때문에 캠페인은 1분마다 업데이트가 되어야 했습니다. 반면 캠페인을 구성하고 있는 대부분의 속성은 캠페인의 예산 정보, 타게팅, 게재 빈도 등의 메타데이터이기 때문에 하루에 많아봐야 한두 번 정도 업데이트가 됨에도 불구하고 예산 제어 상태의 업데이트 주기에 맞춰 레코드 업데이트를 수행해야 했습니다. 이 문제를 해결하기 위해서 최종적으로 캠페인 데이터베이스에서 ‘예산 제어 상태’를 제거해야 했습니다. 간단하게 그 과정을 소개 드리면 먼저 예산 제어 시스템에 ‘예산 제어 상태' 모델을 구현하고 영속화 가능한 상태로 만들었습니다. 그다음 캠페인 관리 시스템에서 ‘예산 제어 상태’를 제거할 예정이기에 광고 송출 서버의 캠페인 캐시에 ‘예산 제어 상태'를 동기화했으며 마지막으로 캠페인 관리 시스템에서 예산 제어 상태를 제거하여 목표를 달성할 수 있었습니다. 결과를 봤을 때 큰 효용이 와닿지 않을 수 있을 것 같은데요. 기능적으로는 캠페인 데이터베이스의 부하를 줄일 수 있었고 구조적으로 모델을 분리하여 변경과 확장이 용이해졌으며 두 개의 컴포넌트를 거쳐 전파되었던 예산 제어 상태를 직접 광고 캐시에 동기화하여 실패 지점을 줄여 안정성을 확보할 수 있었습니다. 지금까지 버즈빌의 예산 제어 시스템 개선 여정을 컴포넌트 수준에서 얘기해 드렸는데요. 저희는 후에 예산 제어 기능을 고도화하면서 이런 구조 개선 작업의 혜택을 실감할 수 있었고 이를 바탕으로 비즈니스 성과까지 이어갈 수 있었습니다. 개인적으로 재밌었던 경험이라 빨리 얘기드리고 싶지만 그전에 팀의 속도를 높이는데 큰 도움을 준 가시성 확보에 대해 먼저 얘기해 보겠습니다. ## 시스템의 가시성 확보하기 예산 소진 제어는 컨트롤 루프 기반의 피드백 시스템이기 때문에 결과가 후행적으로 반영됩니다. 따라서 제어 알고리즘을 변경하더라도 영향도를 손쉽게 확인하기가 어렵습니다. 이전에도 광고 노출이나 클릭이 어떤 패턴으로 발생했는지를 확인함으로써 사후적인 분석을 할 수는 있었지만, 예산 제어 시스템이 어떤 정보를 기반으로 어떤 결정을 내렸는지에 대해 직관적으로 이해하는 것은 쉽지 않은 일이었습니다. 제어 알고리즘에 대한 가설을 실험을 하려면 알고리즘을 변경하고, 광고 운영팀의 도움을 받아 일부 예산을 할애하여 테스트하고, 그 결과는 다음날이 되어야 확인할 수 있었습니다. 그리고 결과를 해석하기 위해서는 또 많은 추정을 했어야만 했죠. 이대로는 반복적인 개선을 만들어내는데 시간이 너무 오래 걸릴 것이라 판단하여 무슨 일이 있었는지에 대한 가시성을 확보하기 시작했습니다. 매 예산 제어 루프가 수행될 때마다 각 캠페인별로 현재 지출 목표, 실제 소진 예산, 그로부터 예산 제어 시스템이 도출해낸 예산 제어 상태 값을 로그로 기록하고 Data Lake에 내려보낸 뒤 BI 도구를 통해 분석하였습니다. ![](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-1/observability-bi.png) 이를 통해 예산이 어떻게 계획되었으며 제어 알고리즘에 의해 실제 지출이 어떻게 제어되었는지를 눈으로 확인하고 직관적으로 이해할 수 있게 되었습니다. 가시성을 확보함으로써 더 다양한 가설들을 동시다발적으로 확인할 수 있게 되었습니다. 빠르게 가설을 검증하는 데 있어 가시성이 중요함을 깨달은 우리는 가시성을 더욱 강화하기로 했습니다. 분석계에서 데이터를 조회할 수 있게 되기까지 지연시간이 있고, BI 도구에서는 데이터 조회를 자유자재로 하기에는 약간의 불편함이 있었기 때문입니다. 로그 기반으로 메트릭을 생성할지, 애플리케이션에서 바로 메트릭을 수집할지 고민하다가 실시간성이 좋은 Prometheus를 활용하기로 했습니다. Prometheus와 Grafana를 활용해 실시간 예산 지출 상태와 예산 제어 상태를 모니터링할 수 있게 되니, 새로운 가설 검증 실험 수행하기 위해 제어 알고리즘을 변경하더라도 실시간으로 확인하고 문제가 발생하더라도 즉각적으로 대응할 수 있게 되어서 심리적인 안정감이 높아졌습니다. ![](https://tech.buzzvil.com/blog/budget-pacing-improvement-part-1/observability-metric.png) 팀에서는 이러한 가시성 확보의 장점을 체감하고 시스템 지표나 APM뿐 아니라 다양한 비즈니스 메트릭을 TSDB를 통해 수집하기 시작했습니다. 이전에는 전사에서 공유하는 Prometheus 클러스터만 운영되고 있어서 카디널리티가 높은 메트릭을 마음 놓고 수집하기가 어려웠는데, DevOps 팀에서 마이크로서비스 단위로 단일 테넌시를 가지는 Prometheus + Thanos 스택을 손쉽게 운영할 수 있도록 제공해 주셔서 각 서비스에서 마음껏 메트릭 수집을 할 수 있게 되었습니다. 여담이지만, 많은 경우 로그와 메트릭은 도메인 이벤트를 중점적으로 관측하기 때문에 메트릭을 추가할 때에는 기존에 로그를 생성하던 지점을 [Domain Probe](https://martinfowler.com/articles/domain-oriented-observability.html#DomainProbe) 패턴으로 리팩토링을 수행하여 Instrumentation 코드를 구조화해 나갔습니다. 모든 것을 측정할 필요는 없습니다만, 시스템의 중요한 도메인 이벤트를 메트릭으로 수집하고 확인하는 것은 이해도를 높이는데 큰 도움이 되었습니다. 이어지는 글에서는 본편에서 소개해 드린 시스템 개선 사항들과 가시성 향상이 장기적으로 팀의 제품과 비즈니스에 어떤 영향을 주었는지 소개해 드리도록 하겠습니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [Now is a good time to multiply your Design team capacity](https://tech.buzzvil.com/blog/multiply-your-design-team) Date: 2023-01-12 | Author: Maxence Mauduit | Category: Design This article's timing is partly due to the major layoff happening worldwide. IT Companies have been building empires. Empires are, by their structure, as fragile as they are big. So while the world realizes that non-profitable businesses are not so great, it might be a good time to re-think how teams, and more particularly Design teams are built and managed. Liz Wiseman wrote a book back in 2010, explaining how leaders can become multipliers, instead of empire builders or 'diminishers'. This culture of multipliers is deeply integrated into Buzzvil's leadership principles. Being a team lead for over 8 years taught me how to direct my efforts into building a compact, fully optimized team of Designers. You'll discover that this has both advantages for the team's performance, but also for each individual inside the team as it promotes a sense of accomplishment and self-responsibility. Companies, working on similar products have much larger Design teams. Still, we manage to ship everything on time, and our product quality is by all standards, satisfying following our customers' feedback and business/usability metrics. An entire book could be written on this topic. But for this article, I would like to give an introduction to each lever I could identify to multiply your Design team's capacity. Let's start with some basic requirements: Before building anything, it is important to know what we can multiply. You should answer this question first: Can I measure my Design team outcomes? --- ## Know what you should multiply: having measurable Design outcomes As a lead, your responsibilities are tied to the performances of a product (or a team in the case of a function team). To multiply something, you need to know the current value first. By doing this kind of audit, you might realize that some of the current steps of your design process don't bring much of a difference in the short or long term, from a business or product perspective. If you are familiar with KPIs this step will be easy if not already checked. But if you are not, this might require some serious investment into getting your design outcomes measurable. Once you know the situation you are in, it is now possible to improve it. This would be different for each team, based on their specific problems. But there are patterns we can define. As a leader, you might find yourself in one of these 3 situations: - You just joined or created a new team and need to build it from the ground up - You lead a team for a long time but need to improve the outcomes without any additional headcount - You got promoted as the team leader of your team, and now you are facing its KPIs for the first time (congrats!). If you are lucky enough, you might be able to hire someone to take your previous role. All 3 scenarios will share most patterns, but only the first one will have extra leverage, let's start with this one: To build a compact team from the ground up, the effort starts with… hiring. --- ## Hire initiative takers and never-ending learners It is tempting to hire someone with a neat portfolio, and a great resume with significant companies listed in the experience section. While this might also have its importance, I learned that the mindset is primordial. Even at a small startup, if someone showed multiple initiatives to improve something until it was resolved, that someone might have more potential than a candidate who did an internship at a FAANG company. Remember, we are building a different type of team, and our criteria for recruiting are by consequence, different. Why are these 2 criterions important? An initiative taker will have the natural will to do more than she/he currently can. And they will enjoy the challenge, not fear it. Again, this isn't about asking your members to overwork. It's about working differently and being open to change. As we will see later, these initiatives will have to be led carefully. To sum up here, what we don't want is to hire someone who did well only in their given assignments. It might sound weird, but in this setup, we need someone who took risks and succeeded in changing something within their organization. A never-ending learner will be capable to multiply their capacity over time. Their speed of growth will be highly impacted by your capacity to give them the resources and knowledge they need. From this point, all 3 scenarios can benefit from the followings: - Onboard, Coach & Mentor - Advocate a long-term dream, tied up with crystal clear, realistic action items - Invest in cooperative, systemic protocols ## Onboard, Coach & Mentor Your role as a team lead is important. Things won't go your way just because you hired the right people. Becoming a multiplier goes through a permanent effort for each of your members to grow in their respective R&Rs. Then beyond. And so on. It's also about different tasks that you might expect as a lead. You are not focusing on your performances as a Designer anymore but on the ones of your teammates. > Your goal is to "find people's genius and engage it". When a new member joins, and until they leave (hopefully long after) I follow a 3-steps routine: Onboard, coach, and mentor. ### Onboarding for conversion Onboarding is primordial to convert a new talent to our company/team culture and work process. If this step is not taken seriously, you might lose this recently hired talent within a few months. The onboarding needs to be thorough, personal, and motivational. Everything you've said during the recruiting process should start to materialize during the onboarding, otherwise, this will sound like a disenchantment to them. ### Coaching & Mentoring for multiplication These 2 roles are very different from each other. But equally important in succeeding in aligning and getting the best of all your team members. There are plenty of great articles that explain what coaching and mentoring are about and their differences. But allow me to give you a shortcut definition adjusted from ChatGPT: - Mentoring is a relationship in which an experienced or more knowledgeable person helps to guide and support the professional and personal development of another person. - Coaching is a professional relationship in which a coach helps an individual or team to improve their performance, reach specific goals, or make changes in their lives. The difference between the two lies in the scale. While mentoring seeks long-term growth and personal development, coaching is action or goal-oriented and seeks short-term performance improvement. So these two skills work well together and a leader should take time to wear both caps for each team member. To sum up, becoming a multiplier means doing less, and empowering others to do more. Use your time differently. Coaching, and mentoring take time but are a worthy investment. > Listen more, and lead with questions for your teammates to answer. --- ## Advocate a long-term dream, tied up with crystal clear, realistic action items When onboarding and coaching your teammates, it will be key to provide them with the big picture, the dream we are aiming at as well as how we are going to do it, in detail. ### The "what" must be tied to your company or product vision It is your role to align your team's vision with the company's. You and your team's performance need to be aligned with the business directions. So you need to know and discuss with other leads to define what your team should contribute to the organization. ### The "how" must be tied with your team's opportunities and solutions definition To give your team a chance to make an impact on their own, you'll need to guide them. Selling the big dream is necessary but not enough. In my experience, this only leads to deception as it is hard to initiate something from the big picture. But if you break down this big dream into tangible action items, then you give everyone a chance to own some of these initiatives. The way you materialize all this is up to you. I would recommend using the Opportunity Solution Tree framework over a workshop, coupled with OKRs. ### Leadership styles Leading an optimized team works best with specific leadership styles. In my experience, oscillating between Democratic and Transformational depending on the phases works great. Transformational require more coaching and is best to.. well.. initiate change. once things are getting consolidated, we can switch to an autopilot mode with a democratic leadership mode. Please note that a democratic leadership mode works best only once all your members are onboarded and have enough in-house seniority to be part of the 'system'. --- ## Invest in cooperative, systemic workflow ### Build a Design System 2 years ago I would have answered differently based on the complexity of your product. But today, building a design system with a shared, cooperative UI Kit is too easy to not do it. Building the UI Kit is easy. A single person could do it, a small team can also succeed with a good cooperation process. Another article is coming about this particular part, so I won't stretch on this topic too much. But to put it simply, a design system is before anything an organized set of principles, protocols, and processes to get something done. Having a UI kit and shared processes is the perfect application of how we can multiply our capacity as a team. The simple fact that we don't build things twice, that we share UT results, and that these UTs are sometimes made at the component level all validate design decisions that we won't need to be validated again soon. ### DesignOps Run frequent Design Ops reviews. Make sure that your team works with the best tools. It's a small investment compared with an additional headcount, so make sure that your team has everything they need to work as efficiently as possible. Here is a quick checklist: - Don't be cheap with hardware. Provide recent, powerful machines to your designers. 1–2k differences per machine don't make a huge difference in accountability. But a laggy machine will certainly affect both performance and motivation. - Go with Figma for product Design if not everything Design related. We've tried them all. Nothing beats Figma in 2022–2023. It is also the best tool to build and maintain a Design system at the moment. - If you need to save up money, get rid of Adobe products (I know, Figma is now owned by Adobe, but let's pretend it's not). They can be replaced by better, cheaper options (Affinity, Blender,..). We only use 2 licenses lately, simply to be able to deal with customer requests. - Invest in UT tools such as UseBerry or Maze. These are huge time savers for User testing. You can do well without it, but if you are getting started and don't have a UT process in place starting with one of these tools will save up a lot of your time. --- ## Sum up It is possible to do more with less. Done the right way, this will even increase your team's satisfaction and retention. Hiring the right people, and empowering them through reachable challenges while giving them all the resources they need, are all levers to building a super-capable and compact team! Here are a few books you might want to read related to this topic: - [Multipliers](https://www.goodreads.com/book/show/8310410-multipliers?from_search=true&from_srp=true&qid=FIPypD37AK&rank=1), by Liz Wiseman - [Coaching for performance](https://www.goodreads.com/book/show/949515.Coaching_for_Performance?ac=1&from_search=true&qid=axImOVBWYj&rank=1), by Sir John Whitmore - [Radical Candor](https://www.goodreads.com/book/show/29939161-radical-candor?from_search=true&from_srp=true&qid=L7qVu6E5ED&rank=1), by Kim Scott - [Measure What Matters](https://www.goodreads.com/book/show/39286958-measure-what-matters?ac=1&from_search=true&qid=C6tJP6uEZG&rank=1), by John Doerr - [High Output Management](https://www.goodreads.com/book/show/324750.High_Output_Management?ac=1&from_search=true&qid=EmzX0I5qHK&rank=1), by Andrew Grove I hope this can help a few of you who are facing today's difficulties as a lead in an IT industry in turmoil. --- ## [다양한 제품 개발 방법론 - 버즈빌 제품팀이 일하는 법#3](https://tech.buzzvil.com/blog/how-buzzvil-product-team-works-3) Date: 2022-12-12 | Author: Whale Lee | Category: Product 2021년 말에 시작했던 글이 어느새 2022년 말까지 흘러왔네요. 2022년 한 해 동안 버즈빌 제품팀이 일하는 방식에는 더욱더 많은 변화가 생겼습니다. 원래 기획했던 6부를 모두 정리하기에는 글 작성 속도보다 변화의 속도가 더 빨라서, 이번 3부에서 전반적인 내용을 짧게나마 정리하고 마무리하려고 합니다. ## 시리즈 1. **[고객 중심(Customer-centric) 전략: 그룹 분할, NPS](/blog/how-buzzvil-product-team-works-2021-first/)** 2. **[제품 비전, 전략, 그리고 로드맵: 제품 전략 피라미드](/blog/product-strategy-pyramid/)** 3. **[다양한 제품 개발 방법론](../how-buzzvil-product-team-works-3/)** ## 본문 ### PRD(Product Requirements Document)와 OKR 얼마전 OKR이 경쟁사를 느리게 만들려는 구글의 계략이라는 트윗에 구글 대표 순다 피차이가 “들켰다”라고 말해서 많은 좋아요를 받았습니다. (노.. 농담이겠죠?) OKR은 목표 달성을 위해 조직이 모두 집중할 수 있게 도와주기도 하지만, 잘못 운영될 경우 속도를 느리게 만들기 때문에 이런 음모론이 나오기도 합니다. 이때 ‘느리다’는 부분을 잘 해석해야 합니다. 실제로 OKR을 과도하게 운영하느라 시간을 써서 실행을 느리게 만들 가능성도 있지만, 잘못 운영된 OKR로 인해 학습과 전환 속도를 늦추고 성장 엔진을 멈추게 만들 가능성도 있습니다. 전자의 경우 쉽게 눈에 띄어서 문제 파악과 개선도 빠르게 만들 수 있습니다. 반면 후자는 시간이 한참 흐른 뒤 알게 될 가능성이 높아서 훨씬 위험하다고 볼 수 있죠. 대표적으로 실수하는 것 중 하나는 할 일 리스트로만 구성된 KR입니다. 이번 분기에 할 일들의 리스트를 나열하고 완료 상태 만으로 KR을 측정하면 실제로 이 일들이 최종적인 목표에 어떤 영향을 미치는지 파악하기 어렵습니다. 이번 분기에 사용자 리텐션을 높이는 것을 목표로 잡고 5가지 기능을 구현하기로 계획을 세운 뒤, KR로 5가지 기능의 완료 상태를 측정하기로 했다고 가정해 보겠습니다. 첫 번째 기능을 빠르게 구현해서 라이브를 했습니다. 이 기능에 대한 사용자 피드백을 받아보니 사용자의 리텐션이 낮아지는 중요한 원인은 다른 곳에 있었습니다. 계획했던 5가지 기능은 이 문제를 해결할 수 없죠. 어떻게 해야 할까요? OKR 달성에 집착해서 원래 계획했던 일에만 집중한다면 OKR 달성률은 높지만 정작 사용자 리텐션은 낮아지는 결과가 생겨날 수도 있습니다. 우리는 KR과 Initiative(계획)를 분리할 필요가 있습니다. 이전 글에서 강조했듯 비전과 전략이 우선이고 로드맵과 계획은 그다음입니다. OKR을 정의했다면 이를 어떻게 추구할 것인지는 Initiative로 구성해야 합니다. Initiative는 상황과 학습 결과에 따라 빠르게 변경될 수 있습니다. **OKR + Initiative 예시:** | Objective | Key Results | Initiatives | | - | - | - | | 사용자 온보딩과 활성화 경험을 개선한다. | 프로필 작성 완료 비율을 60%에서 85%로 높인다. | • 작성된 프로필이 노출되는 화면을 늘린다. ([PRD](#))
• 프로필 기입 필드를 나눠서 단계별로 보여준다. ([PRD](#)) | 이런 Initiative와 찰떡궁합인 방법론 중 하나가 PRD(Product Requirements Document)입니다. PRD는 특정 제품 혹은 기능의 요구 사항을 정리한 문서입니다. 제품이 어떤 일을 왜 해야 하는지 정의하는 문서이기도 합니다. PRD는 제품의 목적, 풀고자 하는 문제, 요구 사항, 용례, 제약 사항 등을 포함합니다. 단, 구현 방법이나 UX, 디자인 같은 내용은 팀 내에서 더 좋은 방법을 찾을 수 있게 열어두고 PRD에서는 구체적으로 정의하지 않도록 주의해야 합니다. 이 OKR과 PRD의 조합은 버즈빌 제품팀이 많은 변화 속에서도 꾸준히 실행하려고 노력하고 있는 부분입니다. ### 4가지 실행 원칙 (4 Disciplines of Execution) 지난 3분기 버즈빌에서 가장 많이 회자됐던 책은 ‘빨간 책’이라고 불리던 “성과를 내고 싶으면 실행하라.”였던 것 같습니다. 원제는 4 Disciplines of Execution인데, 번역된 제목이 약간 길고 조악해 보여서 겉표지의 색인 빨간 책이라고 불렀던 것 같네요. OKR을 운영하며 헤매던 부분을 잘 잡아준 책이 아니었나 싶습니다. 저자는 1) 가장 중요한 목표에 집중하라, 2) 선행 지표에 따라 행동하라, 3) 점수판의 강점을 활용하라, 4) 책무를 서로 공유하라 이렇게 네 가지 원칙을 제안합니다. 굉장히 간단해 보이는 원칙이지만 실제로 적용하기 위해서는 단순히 시스템 변경 이상의 노력이 들어갑니다. 버즈빌도 너무 많은 목표와 제어할 수 없는 후행 지표에 고통을 받고 있었습니다. 여전히 어려움을 겪는 부분도 있지만, 버즈빌 리더십 세션 도서로 선정된 이 책 덕분에 굉장히 많은 부분을 개선할 수 있었습니다. 회오리바람이라고 불리는 ‘해야 하지만 가장 중요한 목표 관련 과제는 아닌 업무’를 인정함으로써 오히려 목표를 좁히고 강력하게 추구할 수 있는 환경을 마련했으며, KR을 정할 때 제어할 수 있는 선행 지표를 찾기 위해 노력하게 되었고, 점수판을 통해 누가 언제 보더라도 가장 중요한 목표 관련한 현 상황을 한눈에 파악할 수 있게 되었습니다. 버즈빌 제품팀에서 실행하고 있는 몇 가지 사례를 들어보자면: - 가장 중요한 목표 정의 - OKR을 정하기 전 가장 중요한 목표가 무엇인지(e.g., 재무적 목표, 운영적 목표) 먼저 논의합니다. 또한 각 팀의 OKR 안에서도 가장 중요한 목표에 별도 표시를 하고 있습니다. - 점수판(a.k.a 대시보드)에 목표 수치 표시 - 단순히 현재 상황을 표시하는 대시보드보다 각 시점별 현재 목표가 표시되어 있는 대시보드는 팀을 목표에 집중할 수 있게 도와줍니다. 현재 우리 팀이 목표 대비 이기고 있는지 지고 있는지 보여줌으로써 행동을 이끌어냅니다. - 회오리바람을 줄이기 위한 목표 선정 - 회오리바람이 있음을 인정하고 상류(Upstream)에서 문제를 해결하기 위한 목표를 수립했습니다. - 제어 가능한 선행 지표 찾기 - 제어 가능한 선행 지표의 중요성은 아마존의 순서 파괴(Working backward)에도 많이 강조됩니다. 예를 들어 매출 같은 후행 지표를 목표로 잡으면 우리가 한 일과 성과 사이가 너무 멀어서 빠르게 학습하고 개선하기 어렵습니다. 여전히 할 일의 리스트가 아니면서도 실행 가능한 선행 지표를 정의하는 일은 매우 어렵지만, 버즈빌 제품팀은 매 분기 더 좋은 목표 수립을 위해 노력하고 있습니다. ### Opportunity Solution Tree - 솔루션 보다 기회에 집중 > 아이디어가 가장 싸다. 실행이 전부다. - 피터 드러커 아이디어에 집착하는 것은 사람의 본능인 것 같습니다. 뭔가 좋은 생각이 떠올랐을 때 너무 좋고 뿌듯하죠. 하지만 아이디어에 너무 집착하다 보면 가치를 만드는데 집중하지 못하고 아이디어에 갇히는 상황이 발생하기도 합니다. 고객이 가지고 있는 실제 문제를 해결하지 못하면 좋은 아이디어는 무덤으로 가게 됩니다. 생각한 것보다 더 많은 아이디어를 무덤으로 보내야 하지만, 생각보다 많은 아이디어는 제품화까지 된 다음 제품과 함께 무덤으로 갑니다. 문제보다 해결책에 집중하면 이런 문제가 발생합니다. 기회 솔루션 트리(Opportunity Solution Tree, OST)는 해결책 보다 기회에 집중할 수 있게 도와주는 도구입니다. 목표를 선정하고 목표 달성을 위한 기회를 나열한 뒤 기회 하단에 솔루션을 붙입니다. 우선순위를 선정할 때도 솔루션이 아니라 기회의 우선순위를 정렬하게 됩니다. OST를 구성할 때 가장 주의해야 하는 부분 중 하나는 ‘기회’가 고객으로부터 시작되어야 한다는 것입니다. 고객 인터뷰 혹은 데이터에서 얻은 인사이트로 기회를 정의하지 않으면 원래 하고 싶었던 솔루션에 기회를 끼워 맞추는 상황이 벌어지기도 합니다. 이 부분이 워낙 쉬운 일이 아니기에 버즈빌에서도 OST를 실행할 때는 매우 주의해서 실행하고 있습니다. ### Pre-Mortem - 가장 위험한 가정 린 스타트업(Lean Startup) 저자 에릭 리에스는 ‘가장 위험한 가정’의 중요성을 얘기합니다. 사업을 실행할 때 가장 위험한 가정을 중심으로 가설을 검증하고 실행해야 한다고 말하죠. 다른 모든 것이 잘 되더라도 가장 위험한 가정 하나가 잘못되면 모든 것이 동작하지 않을 테니까요. 사전 부검(Pre-Mortem)은 과제를 시작하기 전에 미리 미래를 시뮬레이션 함으로써 가장 위험한 가정을 도출하도록 도와줍니다. 성공했다면 왜 성공했을지, 실패했다면 왜 실패했을지 논의해 봄으로써 집중해야 할 부분을 찾는 것이죠. ### Team Topologies - 팀 인지 부하 줄이기 ![Cognitive Load](https://tech.buzzvil.com/blog/how-buzzvil-product-team-works-3/cognitive-load.png) 서비스와 조직이 성장하면서 복잡성은 증가할 수밖에 없습니다. 단기 속도에만 집착해서 개발하다 보면 어느새 전체 조직이 느려지는 상황을 마주하게 됩니다. 노력하지 않으면 팀 간 의존성이 증가하면서 기능 하나를 기획하고 배포하는데도 신경 써야 할 것들이 점점 늘어나기 때문입니다. 팀 토폴로지는 팀이 인지 부하를 줄이고 자율적으로 빠르게 움직일 수 있도록 팀을 구성하는 방식을 제안합니다. Stream-aligned Team (SAT), Complicated Subsystem Team, Enabling Team, 그리고 Platform Team 이렇게 네 가지 형태의 팀을 정의하고, 팀 간 협업 방식을 제안합니다. 버즈빌은 팀 토폴로지를 버즈빌 상황에 맞게 적용하기 위해 노력하고 있습니다. 여러 스테이크홀더와 함께 이벤트 스토밍 워크샵을 진행함으로써 Bounded Context를 정의했고, 역 콘웨이 전략(Inverse Conway Maneuver)에 따라 제품 설계에 맞춘 팀 구성을 추구하고 있습니다. 물론 모든 상황에 동작하는 은총알은 없기 때문에 현재 조직 상황에 맞는 형태로 간소화해서 적용해 나가고 있습니다. 무엇보다도 팀의 구성을 논의하는 사람들이 팀 토폴로지의 개념을 함께 이해하고 논의한다는 것이 매우 중요하다고 생각됩니다. ### PPP(Progress, Problems, and Plan) 업무 공유는 단순한 보고 이상의 의미를 가집니다. 업무 공유가 원활할수록 팀의 목표가 전사 목표와 정렬되고, 중심이 흔들리지 않으며, 변화에 빠르게 대응할 수 있습니다. 정렬은 매우 중요하지만 그만큼 소모적이고 어렵기도 합니다. 특히 업무 공유 시 각 청자 관점에서 중요한 부분을 간추리지 않으면 형식적인 보고가 될 가능성이 높습니다. 너무 많은 정보는 정보가 없는 것과 같기 때문입니다. > 완벽함이란 더 이상 보탤 것이 남아 있지 않을 때가 아니라 더 이상 뺄 것이 없을 때 완성된다. - 앙투안 드 생텍쥐페리 버즈빌 제품팀은 PPP 형태로 주간 업무를 공유하고 있습니다. 업무 공유 시 OKR 지표를 해석하고, 진행 상황(Progress)을 공유하고, 마주한 문제(Problems)를 설명하며, 차주 계획(Plan)을 정리합니다. 어찌보면 당연한 항목임에도 신경쓰지 않으면 종종 놓치고는 하는데, PPP는 각 항목에 대해서 생각해보게 되는 계기를 마련해줍니다. ### 북극성 지표(North Star Metric, NSM) KPI, OKR, OMTM 등 목표와 측정 지표를 연결시키기 위한 노력은 매우 많습니다. 북극성 지표는 그중에서도 특히 제품과 비즈니스가 장기적으로 잘 성장하고 있는지 지켜보기 위한 하나의 측정 지표입니다. 북극성 지표는 매출과 연결되어야 하지만 매출이 지표가 되어서는 안됩니다. 매출은 고객이 가치를 구매하기 위해 지불하는 수단이기 때문입니다. 매출이 북극성 지표가 되면 단기적인 관점에서 성장이 장기적인 관점에서 악영향을 미칠 수도 있습니다. 버즈빌은 고객을 기준으로 전사 조직을 디맨드 그룹과 서플라이 그룹으로 나눴습니다. 주 고객이 다르기 때문에 북극성 지표도 달라야 합니다. 서플라이 그룹은 2021년 말 DA팀 리드 Ed의 주도 하에 특정 기준을 만족하는 주간 사용자 수로 북극성 지표를 정의하고 WVU라고 이름 붙였습니다. 해당 기준을 만족하는 사용자가 많아질수록 서플라이 그룹의 퍼블리셔 고객이 얻을 수 있는 가치(주로 광고 수익)가 눈에 띄게 높아지는 것을 확인했습니다. 디맨드 그룹은 북극성 지표를 찾아가고 있습니다. 제품팀 관점에서 특정 기준을 만족한 도달(Reach, 광고를 본 사용자 수)을 북극성 지표로 검토하고 있습니다. ## 정리 버즈빌 제품팀은 이 밖에도 임팩트 매핑(Impact mapping), 사용자 여정 지도(User Journey Map), 린 고객 개발, 프리토타이핑(Pretotyping), 이벤트 스토밍, Q12 몰입도 조사 등 다양한 방법론을 적용해서 고객에게 더 큰 가치를 제공하고자 노력하고 있습니다. 물론 방법론은 도구일 뿐임을 잊지 않고 방법론의 기반이 되는 원칙과 목적에 집중하기 위해 노력하고 있기도 하고요. 글을 시작한 후 1년이 동안 정말 많은 변화가 있었습니다. 팀의 구성이 바뀌기도 하고, 팀 별 운영 방식이 바뀌기도 했습니다. 조직이 성장하면 성장통이 있기 마련이고, 그럴 때마다 빠르게 학습하고 유연하게 실행함으로써 성장을 가속화할 필요가 있습니다. 버즈빌 제품팀은 지난 1년 동안 이런 변화 속에 정말 많이 배우고 성장했던 것 같습니다. 우리가 일하는 방식이 당연히 유일한 정답은 아닐 것입니다. 하지만 성장 엔진을 구축하는 이 여정은 분명히 정답에 가까울 것입니다. 더 좋은 제품을 만들고 고객에게 더 많은 가치를 전달하기 위해 노력하는 버즈빌 모든 멤버에게 감사 인사를 드립니다. 세상에는 공짜 점심이 없고 은총알도 없죠. 앞으로도 끝없이 학습하며 빠르게 발전하는 버즈빌 제품팀이 되겠습니다. 많은 박수와 응원과 지원 부탁드립니다 🙂 [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [엘라스틱서치를 활용한 수평 확장 가능한 광고 서버 만들기](https://tech.buzzvil.com/blog/tech-blog-building-scalable-ad-server-with-elasticsearch) Date: 2022-12-11 | Author: Zune Seo | Category: Backend 광고 서버는 리워드 광고 플랫폼을 운영 중인 버즈빌에게 있어서 핵심적인 구성요소입니다. 광고 서버의 주요 기능 중 하나는 광고주가 설정한 여러 가지 타게팅 조건에 맞는 유저에게 광고를 송출하는 타게팅 기능입니다. 버즈빌은 허니스크린이라는 잠금화면 리워드 광고 앱을 운영하기 시작한 초기 시절부터 광고 서버를 직접 구축해 운영해왔습니다. 이후 리워드 광고 플랫폼으로의 전환을 거치며 유저 수, 광고 수가 급격히 늘어나고 타게팅 조건 또한 다양해지면서 수평 확장할 수 있는 광고 서버를 구축할 필요성이 커지게 됐습니다. 이 과정에서 초기 MySQL 기반으로 구축되어 있던 시스템을 엘라스틱서치로 전환하였고 이에 대해 자세한 이야기를 해보려고 합니다. ## 광고 서버의 도전 과제 광고 서버에는 두 가지 도전 과제가 존재합니다. 1. **다수의 테이블 간의 join 필요** 송출 가능한 하나의 광고 객체를 가져오기 위해서는 다수의 테이블에 접근해야 합니다. 버즈빌에서는 광고 객체를 MySQL 데이터베이스에 정규화하여 저장하고 있습니다. 광고의 타게팅 조건, 소재 정보 및 여러 가지 설정값들이 7개 테이블에 걸쳐 나뉘어 저장되어 있습니다. 한 번의 광고 송출을 처리하기 위해 다수의 테이블에 접근하기 때문에 성능에 문제가 발생할 수 있습니다. 1. **복잡한 where 조건 필요** 비즈니스 요구사항에 따라 타게팅 조건들이 점점 다양해졌습니다. 이를 만족하기 위해 MySQL에서 광고 객체를 조회할 때 타게팅 위해 사용하는 where 절의 조건문도 증가하였습니다. 버즈빌에서의 요구사항을 만족하기 위해서는 100개 이상의 where 조건이 필요합니다. ## 광고 서버의 특성 광고 송출 최적화를 위해 활용 가능한 특성 두 가지를 알아보겠습니다. 1. **읽기 요청이 쓰기 요청 대비 극단적으로 많음** 광고 객체가 생성되고 변경되는 것은 광고 객체의 전체 생애주기 동안 한 번에서 수십 번인 반면 이것이 송출되는 횟수는 수천 번에서 수백만 번이 될 수 있습니다. 따라서 광고 송출은 읽기에 극단적으로 최적화하는 전략을 취해야 합니다. 1. **최종 일관성이 허용됨** 광고 객체에 수정이 일어났다고 해서 즉시 변경된 내용이 광고 송출에 반영될 필요는 없습니다. 짧게는 1초에서 길게는 1분 안에만 변경 내용이 광고 송출에 반영되면 됩니다. ## 광고 모델 정의 앞으로 풀어갈 문제를 이해하기 쉽게 하기 위해 간단하게 광고 모델을 정의하고 시작하겠습니다. **Lineitem** 광고의 타게팅 정보를 담고 있는 테이블입니다. | **Column** | **Description** | | --- | --- | | id | Self-explanatory | | is\_active | Self-explanatory | | target\_country | Possible values are KR, US, … | | target\_gender | Possible values are ALL, M, and F | | target\_carrier | Possible values are ALL, VERIZON, TMOBILE, … | **Creative** 광고 이미지와 같은 유저에게 보이는 광고의 속성을 담고 있는 테이블입니다. Lineitem 테이블과 1:1 관계를 맺습니다. | **Column** | **Description** | | --- | --- | | id | Self-explanatory | | lineitem\_id | Self-explanatory | | image\_url | Self-explanatory | **Action** 광고를 통해 사용자가 달성하기를 원하는 액션에 대한 정의를 담고 있는 테이블입니다. Lineitem 테이블과 1:1 관계를 맺습니다. | **Column** | **Description** | | --- | --- | | id | Self-explanatory | | lineitem\_id | Self-explanatory | | action\_type | Possible values are landing, install, purchase, … | | action\_metadata | JSON serialized metadata for action | ## MySQL을 이용한 초기 구현 미국에 사는 버라이즌 통신사를 쓰는 여성에 대한 타게팅 조건을 만족하는 광고를 한번에 조회하는 예제 쿼리는 다음과 같습니다. ```sql SELECT * FROM lineitem INNER JOIN creative ON lineitem.id = creative.lineitem_id INNER JOIN action ON lineitem.id = action.lineitem_id WHERE lineitem.is_active = 1 AND lineitem.target_country = 'US' AND lineitem.target_gender in ('ALL', 'F') AND lineitem.target_carrier in ('ALL', 'VERIZON') ``` 이 접근법은 여러 테이블에 엑세스가 발생하는 문제점이 있으며 where 조건이 복잡하여 인덱스를 활용하기 어려워 풀 스캔을 통해 데이터를 조회하게 됩니다. ## 문제의 해결 **다수 테이블 join 문제** 세 개의 테이블을 join한 결과를 별도의 광고 송출 캐시에 동기화합니다. 꼭 외부에 존재하는 캐시가 아니더라도 MySQL 내에 캐싱 테이블을 만들 수도 있습니다. 이 캐시는 일종의 materialized view라고 생각할 수 있습니다. ![](https://tech.buzzvil.com/blog/tech-blog-building-scalable-ad-server-with-elasticsearch/allocation_rdbms.png) **복잡한 where 조건 문제** 타게팅 조건에 따라 `{is_active}:{target_country}:{target_gender}` 형태의 이름을 가지는 테이블을 각각 만듭니다. 예를 들면 아래와 같은 테이블들이 존재할 수 있습니다. * `0:KR:M` * `1:KR:M` * `1:US:ALL` * `1:US:M` * `1:US:F` 캐싱 테이블을 쪼개두면 앞선 예제 쿼리에서는 `1:US:ALL` 과 `1:US:F` 두 개의 테이블만 검색해보면 되기 때문에 풀 스캔해야 하는 데이터를 크게 줄일 수 있습니다. ![](https://tech.buzzvil.com/blog/tech-blog-building-scalable-ad-server-with-elasticsearch/allocation_shard.png) ## 엘라스틱서치 앞선 해결 방법은 타게팅 조건이 복잡하고 많아질 수록 캐싱 테이블의 숫자가 기하급수적으로 증가하기 때문에 확장성이 부족합니다. 확장성을 확보하기 위한 노력을 더 하기전에 "바퀴를 재 발명"하지 않을 방법을 고민했고 검색엔진을 활용하는 아이디어를 떠올리게 되었습니다. 예를 들어 구글은 다양한 키워드를 이용해 방대한 웹 페이지를 빠르게 검색해줍니다. 검색 엔진에서의 검색 키워드를 광고 시스템에서의 사용자 정보(국가, 나이 성별 등등)로 대입해서 생각해보면 두 시스템이 비슷한 종류의 일을 한다고 생각할 수 있습니다. 버즈빌에서는 광고 서버를 위한 검색 엔진으로 엘라스틱서치를 선택하였습니다. 검색엔진이 빠르게 검색할 수 있는 이유는 바로 inverted index를 활용하기 때문입니다. Inverted index의 원리에 대해서는 [Elastic 사의 블로그](https://www.elastic.co/kr/blog/found-elasticsearch-from-the-bottom-up#inverted-indexes-and-index-terms)에 잘 설명되어 있습니다. Invented index를 원리에 대해서 알고 있다는 가정하에 여기서는 앞서 보여드린 lineitem 샘플 데이터가 어떻게 인덱싱이 되는지를 알아보겠습니다. **Lineitem 샘플 데이터** 예제에서 활용할 Lineitem 샘플 데이터는 아래와 같습니다. | **id** | **is\_active** | **target\_country** | **target\_gender** | **target\_carrier** | | --- | --- | --- | --- | --- | | 1 | 0 | KR | M | TMOBILE | | 2 | 0 | KR | M | VERIZON | | 3 | 1 | KR | M | TMOBILE | | 4 | 1 | US | ALL | TMOBILE | | 5 | 1 | US | ALL | VERIZON | | 6 | 1 | US | M | ALL | | 7 | 1 | US | M | TMOBILE | | 8 | 1 | US | M | VERIZON | | 9 | 1 | US | F | ALL | | 10 | 1 | US | F | TMOBILE | | 11 | 1 | US | F | VERIZON | **샘플 데이터에 대한 인덱스 데이터** 아래의 표는 각 term에 대해 인덱싱된 document id를 나타냅니다. | **Term** | **Documents** | | --- | --- | | is\_active:0 | 1,2 | | is\_active:1 | 3,4,5,6,7,8,9,10,11 | | target\_country:KR | 1,2,3 | | target\_country:US | 4,5,6,7,8,9,10,11 | | target\_gender:ALL | 4,5 | | target\_gender:M | 1,2,3,6,7,8 | | target\_gender:F | 9,10,11 | | target\_carrier:ALL | 6,9 | | target\_carrier:TMOBILE | 1,3,4,7,10 | | target\_carrier:VERIZON | 2,5,8,11 | 처음에 봤던 샘플 쿼리를 처리하기 위해 어떻게 동작할지 생각해보기 위해 where 조건을 다시 가져왔습니다. ```sql lineitem.is_active = 1 AND lineitem.target_country = 'US' AND lineitem.target_gender in ('ALL', 'F') AND lineitem.target_carrier in ('ALL', 'VERIZON') ``` 위의 쿼리는 다음과 같은 집합 연산으로 변환할 수 있습니다. `(is_active:1) ⋂ (target_country:US) ⋂ ((target_gender:ALL) ⋃ (target_gender:F)) ⋂ ((target_carrier:ALL) ⋃ (target_carrier:VERIZON))` = `(3,4,5,6,7,8,9,10,11) ⋂ (4,5,6,7,8,9,10,11) ⋂ ((4,5) ⋃ (9,10,11)) ⋂ ((6,9) ⋃ (2,5,8,11))` = `(3,4,5,6,7,8,9,10,11) ⋂ (4,5,6,7,8,9,10,11) ⋂ (4,5,9,10,11) ⋂ (2,5,6,8,9,11)` = `(5,9,11)` 이렇게 inverted index는 데이터를 풀 스캔하지 않고도 복잡한 조건에 대해 효율적으로 조회를 할 수 있습니다. ## 엘라스틱서치를 추천 엔진으로 활용하기 한편으로 검색엔진은 일종의 추천 엔진으로 생각할 수 있습니다. 구글은 우리가 입력한 키워드에 가장 알맞은 결과를 순위를 매겨 순서대로 리턴해줍니다. 광고 시스템도 유저의 정보를 바탕으로 가장 적절한 광고를 골라주어야 합니다. 기존의 데이터베이스에서는 단순히 필터링 기능만을 제공했다면 엘라스틱서치를 활용하면 필터링뿐만 아니라 다양한 스코어링 로직을 활용해 광고 순위를 매기는 것도 가능합니다. 심지어 엘라스틱서치에 머신러닝 기능까지 지원되기 때문에 잘만 활용하면 간단한 추천 로직은 엘라스틱서치만 써도 해결할 수 있습니다. 예를 들어 유저 정보와 광고 정보를 벡터화 한 다음 두 벡터의 거리가 가까운 순서대로 광고를 정렬할 수 있습니다. 이를 구현하기 위해 엘라스틱서치 painless script에서 지원해주는 [cosineSimilarity](https://www.elastic.co/blog/text-similarity-search-with-vectors-in-elasticsearch) 함수를 활용하면 됩니다. ## 광고 송출에 적합한 엘라스틱서치 클러스터 설정 엘라스틱서치는 수십 GB에서 수백 TB 이상의 문서에서 검색을 수행할 수 있으며 이를 위해서 [샤드와 레플리카](https://www.elastic.co/kr/blog/every-shard-deserves-a-home)를 지원합니다. 여러 개의 샤드를 이용해 데이터가 늘어나더라도 수평 확장 할 수 있으며 레플리카를 통해 검색 처리량을 높이고 안정적인 시스템을 구축할 수 있습니다. 하지만 광고 송출 시스템에서는 샤드가 필요 없습니다. 만약 웹사이트 검색엔진을 만든다고 하면 문서량이 많기 때문에 샤드가 필요하지만, 광고 송출 시스템에서는 활성화된 광고만 필요해 문서량이 그렇게 많지않습니다. 하루에 라이브 되는 광고의 숫자는 많아야 수십만 개 이하입니다. 광고 데이터의 사이즈가 수백 MB에서 수십 GB 사이가 될 것입니다. 이 정도면 하나의 샤드로도 충분한 크기입니다. 샤드가 많으면 한 번의 검색을 위해 모든 샤드에 다 요청을 보내야 하므로 비효율적입니다. 샤드를 한 개로 했으니 노드당 레플리카는 한 개만 존재하도록 설정하면 가장 효율적입니다. 기본적으로 엘라스틱서치는 레플리카의 개수를 고정하게 되어 있습니다. 만약 노드가 5개인데 레플리카의 개수가 4개로 설정되어 있다면 1개의 노드는 놀게 됩니다. 따라서 정확하게 노드 개수만큼 레플리카가 생성되도록 해야 하는데 인덱스 생성 시 `"auto_expand_replicas": "0-all"`와 같은 옵션을 줌으로써 해결할 수 있습니다. 성능과 관련해서 또 하나 고려해야 할 것은 coordinating 노드입니다. 여러 개의 샤드로 구성된 클러스터 환경에서는 특정 노드에 원하는 샤드가 존재하지 않을 수 있습니다. 모든 노드는 coordinating 노드의 역할을 하며 클라이언트로부터 요청받았을 때 데이터가 존재하는 노드로 요청이 전달될 수 있도록 라우팅하는 역할을 합니다. 1 노드, 1 샤드, 1 레플리카 환경에서는 모든 노드에 샤드가 존재하기 때문에 항상 로컬 노드에 데이터가 있는 것이 보장됩니다. 하지만 coordinating 노드는 제 나름대로 로드 밸런싱을 하느라 라운드 로빈 형태로 리퀘스트를 전달하는 것으로 보입니다. 이 때문에 로컬 노드에 데이터가 있음에도 불구하고 엉뚱하게 다른 노드로 리퀘스트를 전달하여 불필요한 네트워크 통신을 발생시킵니다. 이를 방지하기 위해서 search preference 에서 `_only_local` 옵션을 사용합니다. 마지막으로 데이터 사이즈가 작은 편에 속하기 때문에 디스크, 메모리보다는 CPU에 성능이 바운드되는 경향이 보입니다. 따라서 컴퓨팅 파워가 강한 인스턴스를 사용하는 것을 추천합니다. 버즈빌에서는 오토스케일링 그룹 안에 엘라스틱서치 클러스터를 구성하고 앞단에 ELB(Elastic load balancer)를 붙여 사용하고 있습니다. 트래픽이 증가하는 경우 인스턴스 숫자를 늘리기만 하면 쉽게 스케일 아웃을 할 수 있습니다. 이를 통해 특별한 노력을 기울이지 않고도 쉽게 수평 확장할 수 있는 시스템을 구축할 수 있습니다. ## 엘라스틱서치는 충분히 빠른가 운영중인 클러스터의 평균 응답속도는 P95 기준 약 21ms 정도 됩니다. 스코어링 로직을 커스터마이징하기 위해 오버헤드가 존재하는 painless script를 활용하고 있는 점을 고려하면 괜찮은 수치라고 생각합니다. 엘라스틱서치에 메모리를 송출할 때 전체 시스템 메모리의 약 절반 정도만 송출하는 것이 가이드입니다. 이유는 나머지 반은 OS가 파일 시스템을 캐싱하는 용도로 쓸 수 있도록 하기 위함입니다. 앞서 말씀드렸듯이 전체 광고 데이터가 수 GB 정도밖에 안 되기 때문에 모든 광고가 파일 시스템 캐시의 형태로 메모리에 올라가 있는 상태라고 생각할 수 있습니다. 디스크 기반 데이터베이스이지만 인 메모리 데이터베이스에 가까운 성능을 기대할 수 있는 이유입니다. ## 마무리 글을 마무리하며 강조하고 싶은 부분은 최소한의 노력으로 원하는 시스템을 구축했다는 사실입니다. 광고 서버 엔진을 직접 구축하는 대신 검색엔진을 활용하여 구축함으로써 많은 시간을 절약할 수 있었습니다. 또한 엘라스틱서치라는 실 환경에서 충분히 증명되고 널리 쓰이는 기술을 사용함으로써 안정적으로 시스템을 운영할 수 있었습니다. 버즈빌에서 엘라스틱서치를 어떻게 활용하고 있는지 더 궁금하시다면 아래 아티클을 읽어보는 것도 추천드립니다! - [Elasticsearch 검색에서 확률 사용하기](https://tech.buzzvil.com/blog/probability-in-es-search/) ## 출처 - Performance cover image by [Nick Youngson](http://www.nyphotographic.com/) [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) [Alpha Stock Images](http://alphastockimages.com/) --- ## [AWS DNA 4기 회고](https://tech.buzzvil.com/blog/aws-dna-retrospective) Date: 2022-11-15 | Author: Jacob Yu | Category: Culture DNA는 Digital Native Architects의 줄임말로, AWS Korea에서 제공하는 교육 및 네트워킹 프로그램입니다. 약 3개월 동안 AWS DNA 4기에 참가하며 경험한 내용을 정리해보았습니다. ### 1. DNA 프로그램에 지원하다. --- ![aws-gameday-slack](https://tech.buzzvil.com/blog/aws-dna-retrospective/aws-gameday-slack.png) 작년 11월, 회사 슬랙 채널에 AWS Game Day 참가자를 모집하는 메시지가 올라왔습니다. 저는 그 당시 Game Day라는 것을 처음 들었고 AWS 리소스 사용 경험이 적어서 참가하지 않았는데요. Game Day라는 프로그램에 호기심이 생겨서 Game Day 우승 팀들의 인터뷰([#1](https://aws.amazon.com/ko/blogs/korea/aws-gameday-winningteam-interview/), [#2](https://aws.amazon.com/ko/blogs/korea/aws-gameday-tour-de-machine-learning-korean-winners/))를 찾아보고 회사에서 Game Day에 참가하셨던 분들을 직접 [인터뷰](https://tech.buzzvil.com/blog/aws-gameday-2021/) 했습니다. 팀을 맺어서 실시간으로 장애에 대응한다는 점이 무척 재미있고 매력적으로 느껴졌습니다. 우승 팀들의 인터뷰를 보다가 알게 된 프로그램이 있습니다. 바로 제가 이번에 4기로 참가했던 AWS DNA(Digital Native Architects)입니다. AWS에선 DNA를 **AWS 코리아에서 제공하는 새로운 방식의 클라우드 교육 및 네트워킹 프로그램**으로 소개하고 있는데요. 약 3개월 동안 교육도 받고 네트워킹도 하고 해커톤이나 JAM 대회에 참가하는 내용의 프로그램이었습니다. 프로그램이 끝나고도 멤버분들이 팀을 맺어서 Game Day에 참가하셨다는 게 인상적이었습니다(아래 사진). ![aws-gameday-team](https://tech.buzzvil.com/blog/aws-dna-retrospective/aws-gameday-team.png)) 구글링을 통해 이전 기수분들의 후기를 읽어본 후, 다음 기수의 DNA 프로그램을 모집하면 지원을 결심했습니다. 마침 올해 봄, 회사 슬랙을 통해 4기 온라인 설명회가 있다는 사실을 알게 되었고 참가 신청서를 제출했습니다. 며칠 후 합격 메일을 받고 4기 멤버로 선발되어 프로그램에 참여할 수 있었습니다. ![aws-dna-mail](https://tech.buzzvil.com/blog/aws-dna-retrospective/aws-dna-mail.png) # 2. DNA 프로그램을 되돌아보다. --- DNA 프로그램은 8개의 세션과 1번의 네트워킹 데이, 그리고 해커톤으로 구성되어 있었습니다. 세션/네트워킹 데이/해커톤이 어떻게 진행되었는지 간단히 정리해 보았습니다. ## 세션 ![aws-dna-schedule](https://tech.buzzvil.com/blog/aws-dna-retrospective/aws-dna-schedule.png) 세션은 SA분들께서 특정 주제와 관련된 AWS 서비스들을 소개해 주시며 알아두면 좋을 다양한 배경지식, Best Practice, 실습 과제 등에 대해 안내해 주시는 식으로 진행되었습니다. 매주 목요일 7시부터 9시까지 진행되었고, 온라인으로도 참가가 가능해서 부담이 적었습니다. 세션을 들은 이후에는 배운 내용을 복습할 수 있는 실습 과제가 제공되었습니다. 실습 과제를 수행할 수 있도록 AWS 크레디트가 넉넉하게 주어진 덕분에, 평소에는 잘 건드리지 못했던 AWS 리소스들에 꽤 익숙해질 수 있었습니다. 세션 덕분에 App2Container, Control Tower, QuickSight 등 다양한 AWS 서비스를 공부하고 활용해 볼 수 있었습니다. 가장 기억에 남았던 세션은 Chaos Engineering인데요. 예전부터 궁금했던 분야인데 SA 님께서 설명을 실습과제도 알차게 준비해 주셔서 이해도를 높일 수 있었습니다. 실습 과제의 경우, 마침 버즈빌에서도 사용하고 있는 Spinnaker, 테라폼, 쿠버네티스를 이용하도록 구성이 되어있어서 회사에서 사용하는 기술들에 대해서도 더 자세히 알 수 있었습니다. 매번 세션을 시작하기 전에는 프로그램 운영진분들이 준비하신 복습 퀴즈를 풀었는데요. 저번 세션에서 배운 내용을 묻는 퀴즈도 있고 재미있는 난센스 퀴즈도 있어서 조금 더 가볍고 밝은 분위기로 세션을 진행할 수 있었습니다. 한 번은 퀴즈 1,2,3등에게 선물도 주셨는데, 제가 마침 2등을 해서 소소한 기쁨을 누릴 수 있었습니다. ![aws-dna-quiz](https://tech.buzzvil.com/blog/aws-dna-retrospective/aws-dna-quiz.png) ## 네트워킹 데이 평소에 오프라인으로 세션에 참여하셨던 분들은 어느 정도 서로 얼굴을 익히셨던 것 같지만, 저처럼 온라인으로만 참여한 사람들은 같은 기수 멤버들과 직접 이야기할 일이 많지 않았습니다. 프로그램 운영진분들께서 네트워킹 데이를 마련해 주셨고, 피자와 맥주를 먹으며 편안한 분위기에서 참여할 수 있었습니다. 어색할 때 풀어볼 수 있는 질문 리스트가 준비되어 있어서, 운영진분들이 많은 신경을 쓰셨다는 게 느껴졌습니다. 가벼운 질문들도 준비되어 있어서 소소한 대화도 하고, “최근에 관심 있는 기술은?” 등 개발 관련 주제들도 기억납니다. ## 해커톤 원래 저는 JAM 대회에 관심이 있어서 DNA 프로그램에 지원했지만, 이번 기수는 JAM 대회 대신 해커톤을 수행했습니다. 저는 7팀의 팀장으로 참가해서, Team Productivity Newsletter라는 서비스를 개발하고 발표했습니다. 결과적으로는 너무 만족스러웠던 경험이었습니다. AWS 리소스를 이용해서 아키텍처를 설계해보는 경험이 처음이었는데요. 직접 고민하면서 설계해보니 다른 사람이 그린 아키텍처를 봤을 때 이해하는 정도가 훨씬 올라갔습니다. 해커톤을 진행하는 동안 다른 회사에서 일하고 계시는 팀원분들과 논의하고 SA분들의 피드백을 받으면서 다른 분들의 지식을 공유 받고 완성도를 높일 수 있었습니다. ![aws-dna-ppt](https://tech.buzzvil.com/blog/aws-dna-retrospective/aws-dna-ppt.png) 기획부터 아키텍처 설계, 개발, 발표로 이어지는 과정을 모두 수행하고 나니 성취감도 정말 컸습니다. 사실 저를 포함해서 해커톤 팀원분들이 회사에 다니시면서 참가하시는 거라 논의할 시간도, 개발할 시간도 쉽게 낼 수 없어서 진행이 힘들 것이라고 예상했는데요. 한번 오프라인으로 모여서 각자 맡을 부분을 분배하고 난 후에는 책임감 있게 개발도 하시고 자료도 잘 만들어주셔서 발표하는 입장에서 너무 감사했었습니다. 저희 팀은 결국 2등으로 선정되어 15만 원 상당의 필름 카메라를 받았습니다. 1등 팀은 “재난 실시간 상황 공유 서비스”를 개발하신 팀이었는데요. 발표 도중 실시간 스트리밍 서비스를 시연하시는 것을 보면서 결과물의 퀄리티가 정말 대단하다고 생각했습니다. 저희 팀 직전에 1등 팀이 발표하시는 차례여서 더 집중해서 봤는데, 아직 다른 팀들의 발표를 보지 않은 상황에서도 “이 팀이 1등을 할 것 같다"라는 확신을 가졌던 기억이 납니다. ![aws-dna-prize](https://tech.buzzvil.com/blog/aws-dna-retrospective/aws-dna-prize.png) ![aws-dna-camera](https://tech.buzzvil.com/blog/aws-dna-retrospective/aws-dna-camera.jpeg) ## 3. 마무리하며 --- 해커톤을 마지막으로 DNA 4기 프로그램은 종료된 상태입니다. 8월 27일이 해커톤 발표날이었으니 글을 쓰고 있는 지금으로부터 일주일밖에 되지 않았네요. 몇 달 동안 매주 목요일은 항상 DNA 세션을 듣는 날이라 비워둬야 했는데, 이제는 그럴 필요가 없어져서 굉장히 홀가분합니다. 해커톤에 참여해서 나름대로 의미 있는 성과를 얻었다는 점과 AWS 서비스를 사용하는 데에 더 자신감이 붙었다는 점도 만족스럽습니다. 열심히 사는 분들도 많아서 동기부여도 많이 받았습니다. 저는 이후 **[AWS Certified Solutions Architect](https://aws.amazon.com/ko/certification/certified-solutions-architect-associate/)** 자격증을 취득해 보려고 합니다. 원래도 관심이 있었던 자격증인데 AWS DNA 프로그램에서 자격증 시험 응시 비용을 지원해 준다고 하셔서 이번에 응시해 보기로 했습니다. 시험 비용이 150 USD라서 사비를 들여서 보기에는 은근히 비싸게 느껴졌는데 이렇게 기회가 생겨서 다행입니다. DNA 프로그램 덕분에 알찬 3달을 보낼 수 있었습니다. 알찬 프로그램을 준비해 주신 운영진분들께 감사하며, 혹시나 다음 기수 참가를 고민하시는 분들이 있다면 꼭 참가해 보시기를 추천합니다! [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [What AI Art can tell us about the future of Design and how it can affect Ad creative optimization](https://tech.buzzvil.com/blog/what_ai_art_can_tell_us_about_the_future_of_design_and_ads) Date: 2022-10-24 | Author: Maxence Mauduit | Category: Design *(Artwork generated by Dalle2 with the following prompt: A cyberpunk Designer looking at multiple screens with a robotic arm with neon lights in a dark, smoky room)* I recently spent some time exploring this wonderful new world of possibilities that are offered by what is generally called AI Art. This article could actually cover the potential of this Tech itself. But this topic is already well covered, and I am a Designer, after all, so I also questioned what this could mean for the Design paradigm. Then how this could be applied to the Ad industry. ## AI Art? Art is always ahead of things. It feeds creative fields as a raw, endless source of inspiration for anyone. Once again, the Art field leads the way, with or without everyone’s consent, through a new way to create artwork based on simple prompts. If you read more about this topic, you’ll see many articles mentioning the limitations and risks. You’ll read that the Art world is either embracing it or rejecting it completely. You’ll also read that as the visual expression is made accessible to literally anyone, abuse and mal use will be some of the challenges to solve. But let’s leave these concerns for later, and focus on the world of opportunities this opens for Designers. ### A prompt to generate Artworks But first, let’s have a look at what it can do today. MidJourney is one of the major Research labs that provide a creative generator through Discord or via their own WebApp. When landing on Discord as a newbie, you are redirected to one of the free trial channels that let you test the service for free. What you have to do it to type “/Imagine” followed by whatever you want to see translated into an image. The more details you give, the better, sharper results you’ll get. > /Imagine “Discovered land, full of machines, animals, plants with the look of Jean Jullien’s artworks” will lead to creating this image in seconds: ![ai generated art](https://tech.buzzvil.com/blog/what_ai_art_can_tell_us_about_the_future_of_design_and_ads/ai_generated_art.png) Now, if you don’t know who is Jean Jullien, he is a French illustrator who does this kind of work: ![Jean Jullien book cover](https://tech.buzzvil.com/blog/what_ai_art_can_tell_us_about_the_future_of_design_and_ads/jeanjullien_cover.jpeg) Considering the unrealistic prompt I entered, the AI did a fairly good job of translating my intention into an image! The AI offers 4 variations for 1 prompt, and from then, you can create variations of any of these 4 variations, and so on. You can also start from an image and request to change or use it in any possible way. The prompt becomes your tool to draw Art. Your capacity to phrase an idea so that the AI can interpret it visually. The rest is all about variations and refinements! ### MidJourney, Dall-E 2, the AI Art segment is developing fast! There are 2 major players in this area, both will let you play around for free for some time. Then both will offer ways to continue at a cost. Both platforms are using different monetization solutions, but we can consider them fairly affordable for what it does. Considering that both are in beta, now is probably a good time to play around! Seeing the emulsion this creates on the Web, we can guess that other companies will try to develop the AI segment into more various use cases. ## The way it works The way these AI work is truly captivating. Most systems are made of two components, the **generator**, and the **discriminator**. Based on a given prompt, the generator will do its best to generate an accurate and original representation. The discriminator will challenge the outcome over iterations. ### The VQGAN + CLIP model Among the AI Art models, the most popular one that gained fame in 2022 is the VQGAN + CLIP model. **VQGAN** stands for “Vector Quantized Generative Adversarial Network”. **CLIP** stands for “Contrastive Image-Language Pretraining”. Long story short, the VQGAN model generates images, and the CLIP model judges how well the generated images match the text prompt. For more details about these models and how they interact, you might be interested in reading these articles: - https://www.kaggle.com/code/basu369victor/playing-with-vqgan-clip - https://alexasteinbruck.medium.com/vqgan-clip-how-does-it-work-210a5dca5e52 - https://ljvmiranda921.github.io/notebook/2021/08/08/clip-vqgan/ ## How this same logic can be applied to Design systems Obviously, these models would need some serious adjustments, but the logic would remain fairly the same. Let’s deep-dive into the wonderful world of assumptions. We know that the current model uses a neural network that connects existing images from a large database and reads through their Alt Text (their textual descriptions). So as long as we can create a UI library that is properly documented, with descriptions on every level of clean, atomic component architecture, the same logic could be applied, and an AI could be trained over such a database. We also know that most AI Art generators are now interfaced with prompts, we just need to write down what we want to create and the AI produces it through the model described above. So we can assume that we could interface UI work the same way, simply telling what we want to mockup. Dalle-2 also adds details over parts of an image, making the process more refined. We could use the same technic to alter a part of the UI we want to refine. This AI would work inside an existing UI tool, where all of your settings would remain unchanged (styles, nudge amount, etc.). Technically, the AI could be fed from the Community resources or focused on the shared libraries you are using,.. or a mix of the two. ### Considerations Considering how fast a mockup can be built nowadays, it could be interesting to measure the lead time between a Designer creating a mockup by manipulating the tool and by typing a prompt. My guess is that once we get used to typing prompts (or speaking), the lead time would be much faster than any UI superstar. But this also brings us one more potential development, that could eventually lead to even more gain of time. So far it seems doable to consider AI as a UI builder. But if the machine could also build up the entire UX flow, then this could mean drastic improvements in terms of efficiency. ### Challenge: AI-generated UX, Flows The model would need some serious adjustments to create interconnections between screens and create an entire UX flow. But if the AI could be able to crawl a large database of UX flows, we can imagine that even the quality of the outcome could become as good or better than what an experienced Designer could do. Our current tools, Design outcomes are not systematically connecting screens between them. We would need to find the database to train the AI into recognizing UX patterns, but for this, we would first need to… create this database. More and more Figma integrates Prototyping features inside components and recently made these prototyping interactions pervasive, so we can guess that sooner or later, Designers will be able to systemize interaction patterns. We can also guess that such an initiative should probably come from a tool like Figma (or Adobe?? 😬) or through their Plugin API and open community database. All this seems a little far from now, right? But if you told me a year ago that I could create Artwork this easily, I wouldn’t have believed it! So maybe the future of Design using AI isn’t so far from now after all… ## How this logic can be used for Ad Creative optimizations In the context of Buzzvil, and focusing on Ad Creative optimization, this is an entirely new perspective that could open to ground-breaking innovations someday. AdTech companies usually invest a lot in Ad creative optimizations to increase performance. Basically, this is about showing different creatives to different people. The differences can be various and are based on what we know of user preferences. It can alter the ad item itself, showing a particular product from a large inventory. That's the 101 of Ad Creative optimization. Moving forward, focusing on a particular product or service. the user’s interest, location, age, and gender, can fill out the different parts of a predetermined template. This is good but also limited by the template logic applied. The result is alright, but we can easily say that there is room for improvement. ![An examples given by warroominc.com](https://tech.buzzvil.com/blog/what_ai_art_can_tell_us_about_the_future_of_design_and_ads/ad_creative_optimization_explained_by_warroominc.png) *An example given by warroominc.com* In this case, the ad server could actually generate the prompts instead of us, based on the keywords attached to each user profile. And the AI could generate the creatives. Of course, we would need this AI to be specifically trained for this kind of marketing creatives, fed with the billions of ad campaigns that surround us all in our daily lives. With the rise of Zero party data, providing less creepy, more accurate information about customers, we could imagine pretty sharp creative making and advanced prompts. --- ## [All the mistakes we have made during Product Discovery](https://tech.buzzvil.com/blog/all-the-mistakes-we-made-during-product-discovery) Date: 2022-09-30 | Author: Maxence Mauduit | Category: Product 6 months ago we started a new team with a new mission and big dreams. We secured resources to give the mission a proper chance to go through Product discovery, a luxury in a fast pace startup like Buzzvil. Promising, right? Well, in this article I want to talk about all the things that didn’t go as planned, and there are plenty! Let’s go through 4 major ones, with a grain of salt. ## (Mis-)Alignment When you start a new mission, the first step is to clearly define what this new team will work on and what are the objectives. And we did. But see, even with a written mission objective, misinterpretation and misalignment happened right after we started. What we did wrong here was to not involve all the stakeholders in the mission’s definition. The involvement from the start wasn’t complete, all actors weren’t actually aware and implicated in this new team. Small gaps became very fast, large cracks in our alignment and we had to roll back to the start, in order to re-align everyone, from the internal actors (mission members) to the external ones (stakeholders). ### Takeaways Build a bulletproof alignment from the start, involving everyone that has stakes in that project. A good format for this would be to have an alignment workshop where everyone participates in the making of a product field, for instance. If everyone is here and collegially decides on the goal of this new mission, it will naturally enforce a clear direction understood by everyone. ## Let’s do customer interviews tomorrow It isn’t easy to find customers to interview in a B2B business. It requires time and networking. So even if we knew that this was crucial, we delayed to “prepare ourselves” more. We created dynamic interview sheets, did lots of market research, then did more research. But didn’t schedule any interviews. The team also fell short of actually reaching out to customers. We started this mission as a team of 2, one PM/Designer and one Engineer. But none of us had the related network to reach out to customers. Among our stakeholders are our CEO, John, and CBO, Steve. They helped us move forward, but the simple fact that these actors aren’t inside our team made the whole process very slow and it took us months prior to having our first interviews. I do think that even in a best practice case, it would take weeks to get a first interview scheduled. But in our case, things got delayed out of proportion and our team suffered from this as we couldn’t move forward without these precious insights. ### Takeaways Never ever, ever-ever-ever delay customer interviews while in the product discovery process. Do it today. Particularly for a B2B product, getting a customer to agree to an interview can take time. So start with that, contact potential customers, even if you don’t have anything ready for it yet. You’ll have time to prepare. ## We have super-powers and can multitask Nope. No magic here. I was the first to pile up responsibilities, being in charge of this new mission team and also leading the Design team. On top of that, frequently supporting design works for teams with no design resources. You can use all the tricks you know, Product discovery takes time. Actually, more than I could afford even at full-time. But I anyway decided to (or had to?) keep my previous responsibilities. The result is that delays occurred all along the way because the context switch had a hefty cost on my resources. Also because people tend to prioritize what’s impacting others first, or at least I do. Today I still have these multiple responsibilities, and I know that this is a problem. But the team grew in numbers and it is now more manageable (not an excuse, more like a fact). ### Takeaways Product discovery requires incredible focus and dedication. Particularly when there is nothing done before when you need to start a completely new product or focus on a new customer segment. Securing this focus is an absolute priority to ensure an efficient discovery phase. We didn’t and it took us more time than what we initially planned, actually nearly twice the time we first allocated ourselves. ## Believing in other data than your own We made this mistake a couple of times. While researching, and after struggling to find market insights for some time, it is always tempting to believe in a report given by another company. But in all cases, after finding more compelling technics, we found out that the given data wasn’t accurate or even completely wrong. Getting the data wrong from the start alters your assumptions and eventually makes you draw false hypothesis. Hypotheses that could lead some of your product directions later on… ### Takeaways Always, always trust YODA (Your Own DAta)! And try to rely on external sources the least possible. Obviously, it is difficult to completely strikethrough every source of insights you can find on the Web, but at least put them in italic, and try to verify them as soon as this is possible. And of course, never entrust these insights to make product or business decisions! ## Bonus: things we’ve done right! We’ve made many things wrong, but we also did a few things correctly. - Do not spend time with tools. Drop Jira and all the other management tools. Discovery is a demanding process, but that doesn’t require tons of preparation and management. Keep it light, we used Figjam for pretty much everything! - Forget about your cubicle! Discovery is the time to chat, debate and move forward as a group. Plan frequent ideation/brainstorming sessions and let the compulsiveness of your group do the rest! - Forget about specialty! Well, not completely, of course, but during Discovery, engineers won’t code (or maybe for the sake of an experiment), PMs won’t manage, and Designers might design, but for conceptual purposes only. Be the expert you are, but participate in this product discovery as a member of your mission. Let everyone be curious about everything! - Onboard new members closely. Starting a new mission isn’t easy. Joining a new mission after it just started, before knowing what we would build is super hard. So we took all the necessary time to ensure that our new friends know exactly what we are trying to do and where we are at. ## Re…tro…spect All the mistakes we’ve made were discovered soon enough through frequent retrospectives (on a monthly basis, plus punctually when we could identify something’s really wrong). So we turned these mistakes into insights and lessons. We obviously lost time, but we were also aware that this might happen from the start, as all of us were new to product discovery. Our story showcases the importance of running frequent retrospectives. There is absolutely nothing wrong with making mistakes, as long as you can identify them fast and learn from them. If you don’t, this will have long-lasting consequences on your team’s performance, and ultimately on your product too. --- ## [Core Values of a Global Team](https://tech.buzzvil.com/blog/core-values-of-a-global-team) Date: 2022-09-22 | Author: Sophie Cui | Category: Culture ## Working in a global team in Seoul It’s always very special to work within a diverse team, even more in Seoul. When I say the BIC team is diverse, I **truly** mean it. We are a growing group led by our Team Leader **Max** (French), Market Researcher **Mason** (returning from the US), and Product Designer **Sophie** (Hungarian-born Chinese). But what does BIC even mean? BIC stands for Business Incubation Center, a new mission team within Buzzvil set out on product discovery in order to find new opportunities to expand Buzzvil services. In our team, we work with team core values and quarterly OKRs to frame how we collaborate and to direct our focus on priorities that matter. ## Core values that really work Core values are only as powerful as the work put into up-keeping them. And that comes with truly understanding and resonating with what they mean. In the BIC Team, the core values are born from the collection of attitudes that each of our members strongly believes in. Through a workshop session we developed 5 values that we think play key roles in our success as a team: #### Problem-centric🤔, Go Beyond💥, Autonomous⚙️, Adventurous🧗‍♀️ and Lean Everything🧘‍♂️. Let’s dive into what each one of them means to the BIC team.


## Taking values into action

### Problem-centric

> We don’t focus on the solution but on the customer problem.

![Conducting a customer interview](https://tech.buzzvil.com/blog/core-values-of-a-global-team/problem-centric.jpg)

As a team aiming to discover new possibilities, where we put our focus can make or break the outcome. Listening closely to our customers and their needs guides us to the most urgent and challenging problems we can solve.

👉How we put this into actual action:

The team is stubborn about doing as many interviews and research on our customers as possible to make sure we are looking at the right problem space and coming up with the correct helpful solutions.

### Go Beyond

> We always aim to provide more than expected, going the extra mile for our customers.


![We always list action items after meetings](https://tech.buzzvil.com/blog/core-values-of-a-global-team/go_beyond.jpg)

We always list action items after meetings

Opportunities lie within opportunities. We do not stop at providing only the bare minimum that our customers request. Aiming beyond the expected results can often lead to unexpected opportunities.

👉How we put this into actual action:

For every meeting, we concretize our discussions into action items that have to be fulfilled within the deadline. Often times action items are planned one step or a few steps ahead of our current stage.

### Autonomous

> We work independently from other teams. We manage our tasks and time freely based on clear objectives.

![Autonomous](https://tech.buzzvil.com/blog/core-values-of-a-global-team/autonomous.jpg)

In our team, we consider delivery the most important, as long as you can fulfill your tasks it does not matter where and how you work. Instead of waiting for tasks to be assigned to us we actively define our own tasks.

We have the freedom to move fast without having to wait for approval or feedback from other teams. We adhere to the company vision retaining ownership over our own team mission. BIC team essentially functions as “[a startup inside a startup](https://tech.buzzvil.com/blog/building-a-startup-inside-the-startup/)”.

👉How we put this into actual action:

We sync up every day during our daily scrum and share our daily tasks and objectives. This is to ensure everyone is on the same page and to have action items we need to complete for the day.

(We are free to leave the office to go to a coffee shop( there are so many pretty coffee shops around Buzzvil) to work.)

### Adventurous

 > We fail often through bold leads and fast iterations. We are product discovery experts.

![Discussing out-of-box ideas](https://tech.buzzvil.com/blog/core-values-of-a-global-team/adventurous.jpg)

In this team, nothing is too outrageous and all ideas are welcome. Unprecedented ideas are seriously considered as we believe great ideas can come from anywhere.

We do not fear failure. We take on bold challenges and reach out of the box which naturally will result in unsatisfying results. As long as we can move fast and pivot to new directions, failing is just part of the process.

👉How we put this into actual action:

Every week we have an out-of-box time where we spend 2 hours ideating and iterating on some bold challenges.

### Lean Everything

> We communicate clearly and effectively. We don’t waste time with unnecessary meetings and processes.
Discussing out-of-box ideas

![Lean everything](https://tech.buzzvil.com/blog/core-values-of-a-global-team/lean_everything.jpg)

Communication is always a danger zone. Delays in communication, error, and misunderstandings can cause a halt to projects. We put emphasis on fast and effective communication by responding fast and going straight to the point.

There are no forced meetings and we go straight to the point when discussing topics. We do not spend time on unnecessary formalities.

👉How we put this into actual action:

Meetings are always to the point, and we stick to the agenda and end them with either a decision or action item depending on the contents discussed. Meetings also never go overtime unless there is an urgent situation; if we book an hour-long meeting, we aim to finish all agendas within the hour.

## Ever-growing BIC Team

All these core values are fundamental to how the BIC team works together. We want to evolve into a flexible team that can accommodate quick changes and pivots in direction.

If you would like to learn more about our members you can read about [Max’s story here](https://tech.buzzvil.com/blog/max-story/).

---
## [Building a Startup Inside a Startup: Story of the BIC Team](https://tech.buzzvil.com/blog/building-a-startup-inside-the-startup)
Date: 2022-09-15 | Author: Mason Yun | Category: Culture


### First, what’s a startup?

What first comes to your mind when you think of the word “startup''? Entrepreneurship, small teams, funding, creative disruption, the k-drama… The list probably goes on and on.

According to Wikipedia (yeah, I know. I’m actually quoting Wikipedia, but I think the definition does the term justice), a startup refers to “a company… undertaken by entrepreneur[s] to seek, develop, and validate a scalable business model.”

### buzzvil

So according to the definition, buzzvil is definitely a startup. It was founded by entrepreneurs who disrupted the Ad-tech industry through original and innovative products. The business model has continuously evolved and grown ever since, through the efforts of some 120 employees. But as the company grew in size and business became more mature, there came the need for more swift innovation. Such needs called for the creation of the Business Incubation Center(BIC), meant to act like a startup inside a startup.

### Introducing the BIC

The BIC is a small team with 3 members so far. Max is the lead and PM of the BIC team. He has been with buzzvil ever since he moved from France to Korea 8 years ago 🇫🇷🇰🇷. He is also the Head of Design at the company. Mason, who is the author of this article, is the team’s market researcher. He lived in the US for 11 years 🇺🇸🇰🇷. Aspiring to learn more about Product Management, he joined buzzvil in July 2022 and is thoroughly satisfied with the company, team, and environment of continuous growth. Jia, the product designer and the newest member of the team, is Hungarian-born Chinese who speaks 4 languages 🇭🇺🇨🇳🇬🇧🇰🇷 (she can communicate with more than a third of the world’s population!) Before joining buzzvil, she worked at a Korean B2B startup as a product designer. As you can see, the team is very dynamic and global, and collaboration happens in English.

### What makes the BIC more startup-like?

So what makes the BIC a startup inside a startup? Part of this could be answered by this question: What’s the most important aspect of a startup?

The business model? The funding amount? Its culture?


These are indeed all very indispensable factors of a startup but none of these is possible without the people who started the startup. These people are entrepreneurs, who are adventurous and take ownership of the business and its products. And the members of the BIC team do just this– they develop new products and take ownership and responsibility for their success and failure. Along the way, the team continuously ideates, researches, develops, validates, and iterates for the success of that new product.

As such, the BIC team is, by nature, meant to be adventurous. Because it’s small, it makes decisions more autonomously and quickly. Communication is direct and seamless, without unnecessary paperwork or pretenses. While being autonomous, the team keeps in check through daily scrums and what’s called the weekly PPP's, which are dedicated to sharing the week’s Progress, Problems, and the coming week’s Plans. Through such exercises, everyone stays on the same page and focuses on one common objective, generating higher focus and delivering results more quickly.

### So what is the BIC’s goal?

The team’s objective is to create new products that will increase the company’s customer reach by 2-3 times. Yes, it’s a bold goal as buzzvil already has many clients. But the team receives a lot of support within the company and it has firm belief in achieving this goal through the best practices mentioned above. Despite knowing it’s going to be challenging, being a part of creating an entirely new product and watching it succeed will be a feeling like no others.


---
## [Max’s story: From Design to Product leadership](https://tech.buzzvil.com/blog/max-story)
Date: 2022-08-30 | Author: Maxence Mauduit | Category: Culture


Yesterday, or to be exact, 8 years ago, I was joining Buzzvil as a product designer. In late 2014 Buzzvil counted a little more than 20 members and the design team was only 3 members including me. There were no PMs and all our focus was on our B2C app called Honeyscreen.
Today, or 8 years later, our company counts over 120 members and we offer Monetization and Engagement booster solutions to dozens of publishers. We still manage Honeyscreen too, but we use the service as the best showcase of our tech.
Today I also evolved along with our company growth, to be a product team leader. I also manage our design team, which now counts 8 talented members.

So how did all this happen?

## Growing inside a growing organization

Growing as an individual can be done anywhere I guess. But if you are joining a startup that redefines itself permanently and grows fast, your chances to have opportunities to grow along with it are of course higher.
I consider Buzzvil as the best school I’ve ever had (and the bar is high as I am also very proud of my design school back in France). Working here for many years transformed me as much as the company changed. And the reason why I am still here is that the company still finds ways to reinvent itself even today. It pushes me to do the same, permanently reinventing myself by learning and doing.

### Mentorship is all

While the organization grew, I was asked to take on more responsibilities. And of course, I said yes (because why not?). Each time, this new challenge came with a couple of stressful weeks, as I had to perform things I had never done before. But this was possible thanks to the great mentorship I got from my upper leads. So today, I also do my very best to provide the same level of guidance for my teammates to perform well in their new R&Rs. I spare a considerable amount of my time into coaching others in order to reach their short-term goals. And even more important, I also do my best to mentor others on career goals and directions. And I hope they do the same later on within our organization or another (preferably ours 😅).

## From Design to product

It’s no news that Product people and Designers are vowed to interact a lot. And the more a Designer gains seniority, the more their understanding of the business reaches enough maturity to reduce the gap with what a PM does.

### The PMs and their super-powers

I believe that all PMs have a superpower. A specialty that they are bringing with them when moving to the PM role. This super-power can be various depending on the person’s background and interest. For instance, technical PMs will generally come from engineering or associated fields such as QA management. As for designers, their superpowers are usually their customer-centric mindset and their capacity to visually communicate an idea or concept.
So my superpower was just that, I could comfortably visualize what an early concept would look like, even during the customer problem definition space. I was able to start from nothing and advocate leads for the team to discuss and build upon. If you don’t see it, it doesn’t exist.

### Filling the gap

Of course, moving to a PO, and then a PM position required some serious learning curves. And as we are a startup, the need for results is now, not tomorrow. So I learned by doing, and by reading books recommended by more experienced leaders in our company at the same time (you’ll find the list at the bottom!).

#### Managing Cross-functional teams

The first big gap to fill was to learn how to lead and guide very different members in a mission team. For this, you need to learn enough so that you can communicate with all of them. It’s about understanding what’s at stake and being able to discuss it with the related members. You don’t need to bring solutions to the table, but to understand and prioritize the problem.

#### A stronger business understanding

The way I define product design is including business knowledge. The design of a product should acknowledge business goals and include them in the solution. Yet, there is a gap between what a PM needs to know. It’s not only about acknowledging but manipulating and setting up business goals (through KPIs, OKRs,…). It’s about controlling the risks and performances with proper trackers, it’s about reading through the data to grasp the insights the team needs to move forward.

#### Aiming at outstanding leadership skills

I think that being a good leader isn’t just enough. The leadership style and methods will have a direct impact on the performance of an entire team. So good won’t make it. The more leaders have people with them, the more they should be outstanding at everything that touches leadership.
From the recruiting strategy, to the mission and vision setup, daily coaching, and long-term mentoring, I try to put most of my efforts to empower and support my teammates. Accepting a leadership position is accepting the fact that you can achieve more with a team than what you used to do as an individual. But for this to be true, you are required to learn new skills and not just continue to be the specialist you once were.

## Today’s challenges and tomorrow’s dream

Here I am, after leading a large team in charge of our monetization products, I recently started an entirely new mission. We need to find our next business opportunity.
It’s for me another chance to connect the dots with Design practice as we now deep dive into product discovery.
This new mission team needs to find a model that can expand our monetization products’ customer reach, and this, within 2 years from now. From this goal, pretty much everything is up to the team. We function as a startup inside the startup and would get more “funding” from our CEOs only if we can prove that our model is viable.
As we move toward confidence, the team already unlocked a few more resources to hire our core members.
Tomorrow’s dream is for the team to set a dual-track development where both product discovery and delivery stages can co-exist inside our team workflow. In a way, this is about closing the loop and merging design/discovery and product/delivery.

#### Recommended reads about leadership & management (as promised):
- Multipliers by Liz Wiseman
- High Output Management by Andrew Grove
- Coaching for performance by Sir John Whitmore


---
## [배포를 빠르게 - DIY(Deploy It Yourself)](https://tech.buzzvil.com/blog/배포를-빠르게)
Date: 2022-07-05 | Author: Noah Lee | Category: DevOps


**이 글에선 버즈빌에선 어떻게 배포의 속도를 높였는지 소개합니다.**

- [배포를 우아하게 - 원-클릭 배포](/blog/배포를-우아하게/)
- [배포를 안전하게 - 카나리 배포 전략, 롤백](/blog/배포를-안전하게/)
- **배포를 빠르게 - DIY(Deploy It Yourself)**

이전 글들에서 버즈빌의 배포 파이프라인의 세부적인 내용들을 설명했습니다. 이번 글에선 사용자들이 배포 파이프라인을 DIY(~~Do It Yourself,~~ Deploy It Yourself)로 구축하는데 중요한 내용들을 설명하고자 합니다. 일반적으로 배포 사용자는 배포 파이프라인의 구체적인 내용을 알고 싶어하지 않고 단지 서비스를 빨리 고객에게 전달하고자 합니다. 이를 위해선 앞서 설명한 배포 파이프라인을 누구든지 쉽게 만들 수 있어야만 합니다. 

## 템플릿 활용

 이전 글 - 배포를 안전하게 - 에서 쿠버네티스 메니페스트 생성을 위해 헬름(Helm)을 사용하고 있다고 말씀드렸습니다. 헬름은 이미 여러 회사에서 채택해서 사용하고 있고 굉장히 성숙한 도구입니다. 하지만 모든 엔지니어가 헬름을 알고 있진 않아 **헬름이 낯선 엔지니어는 배포 파이프라인을 위해서 새로운 도구를 배워야만 합니다**. 그리고 이 과정에서 배포의 속도는 매우 느려지게 되며, 이는 조직의 속도에 영향을 미치게 됩니다. 

 버즈빌은 이 문제를 해결하기 위해서 **단일 헬름 차트를 운영하고 있습니다**. 일반적으로 각 서비스는 비슷한 인프라 구성을 가지고 있어 단일 차트로 배포가 가능했습니다. 또한 엣지 케이스(Edge Case)가 있는 경우에만 조건문을 추가하거나 너무 복잡해지는 경우 차트를 따로 만들고 있습니다 (아직 별도의 차트를 만든 적이 없습니다). 따라서 사용자는 배포 파이프라인을 위해서 새로운 헬름 차트를 만들 필요가 없습니다. 단순히 가이드를 따라서 `values.yaml` 파일만 작성하면 됩니다.

 또한 단일 헬름 차트는 이미 최적화된 설정을 기본 값으로 가지고 있어 `values.yaml` 파일에서 `enabled` 필드로 활성화/비활성화로 최적화된 리소스를 배포할 수 있었습니다. 

 버즈빌은 헬름뿐 아니라 배포 도구인 스피네이커(Spinnaker)에서도 같은 방식으로 작업했습니다. 스피네이커는 [파이프라인 템플릿](https://spinnaker.io/docs/guides/user/pipeline/pipeline-templates/create/)을 제공하고 있습니다. 파이프라인 템플릿은 스피네이커 파이프라인을 쉽게 생성해주며 템플릿 변수로 커스텀(custom)이 가능합니다. **저희는 상황에 맞는 파이프라인 템플릿 - 롤링 업데이트, 카나리 배포 - 을 미리 생성해 두고 이를 이용해서 파이프라인 생성할 수 있도록 했습니다**. 따라서 사용자는 단 몇 번의 클릭으로 파이프라인을 생성할 수 있었습니다.

파이프라인 템플릿 변수 예시 ![Spinnaker](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%eb%b9%a0%eb%a5%b4%ea%b2%8c/spinnaker-template-var.png)

## 팀 간의 의존성 끊기 일반적으로 하나의 배포 파이프라인을 구축할 때 일부분 인프라 조직의 지원이 필요합니다. 따라서 각 팀은 배포를 위해서 인프라팀의 도움을 요청하게 되며 인프라팀은 하던 작업을 멈추고 요청을 처리하게 됩니다. 이 과정에서 팀 간의 의존성이 생기게 됩니다. 그리고 조직의 규모가 커질수록 팀 간의 의존성은 조직의 속도를 지속해서 저하하게 됩니다. 버즈빌은 팀 간의 의존성을 끊기 위해 먼저 작업 공간을 분리했습니다. 특히, 배포 파이프라인에 필요한 설정 파일들을 서비스의 레포지토리에 위치시켜 **하나의 레포지토리에서만 작업이 가능하도록 했습니다**. 예를 들어, 앞서 말한 헬름 차트의 `values.yaml` 파일은 각 서비스 레포지토리의 `release/` 라는 디렉토리에 위치합니다. 하지만 그럼에도 모든 작업을 엔지니어 스스로 할 순 없었습니다. 주로 높은 권한이 필요한 작업이나 컨택스트 이해가 필요한 작업이 이슈가 됐습니다. 예를 등러, 버즈빌에선 데브옵스 팀이 직접 쿠버네티스 네임스페이스 생성해야 했습니다. 버즈빌은 이 문제는 슬랙(Slack) 워크플로우로 해결했습니다. 배포 파이프라인 작업 전에 엔지니어는 슬랙 워크플로우로 데브옵스 팀에 작업 시작을 공유를 합니다. 그후 엔지니어가 그다음 작업을 진행하는 동안 데브옵스 팀은 비동기적으로 사전 작업을 진행합니다.
슬랙 워크플로우 ![Slack Workflow](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%eb%b9%a0%eb%a5%b4%ea%b2%8c/slack-workflow.png)

저희가 직접 요청(슬랙 팀 맨션)을 피한 이유는 두 가지입니다. 첫 번째는 슬랙 워크플로우는 **구체적인 사전 작업 내용을 추상화합니다**. 만약 직접 요청해야 한다면 엔지니어는 구체적으로 무엇이 필요한지 알아야 하며 이는 불필요한 러닝 커브를 만들어 냅니다. 두 번째는 **자동화에 대한 준비**입니다. 슬랙은 훅(Hook)을 제공하고 있어 나중에 사전 준비 작업을 자동화할 수 있게 합니다. ## 마무리하며 배포 파이프라인은 각 조직 (또는 팀)의 철학이나 상황에 맞게 구성됩니다. 버즈빌의 배포 파이프라인도 그렇습니다. **버즈빌은 확장성, 재사용성, 팀 간 의존성 제거 등 조직의 속도를 높이기 위한 고민을 하고 있으며 이런 철학이 작업에도 반영됐습니다**. 이번 블로그 시리즈를 통해 그동안 버즈빌이 고민한 내용들을 공유하게 됐습니다. 만약 비슷한 고민을 하는 조직이나 팀이 있다면 많은 도움이 되었으면 좋겠습니다. 그리고 앞으로는 모니터 시스템, 보안 등 더 다양한 주제로 공유할 수 있도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. *만약 글의 내용 중 피드백이 있으신 분은 이메일 부탁드립니다.* 🙂 [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [상품 추천 알고리즘 Item-CF의 최적화 여정](https://tech.buzzvil.com/blog/optimizing-itemcf) Date: 2022-06-30 | Author: Peter Kim | Category: Data & ML **만 30개의 이커머스을 광고주로 삼고 있는 버즈빌. 이들을 위한 리타겟팅 광고 솔루션을 고도화하기 위해 도입된 상품 추천 알고리즘: Item-CF. 이 알고리즘 최적화와 적용 과정에 대해서 알아보겠습니다.** ## 버즈빌의 리타겟팅 광고 솔루션 버즈빌에서는 매출의 20% 이상을 리타겟팅 광고 상품으로 발생시키고 있습니다. 전통적으로는 리타겟팅 광고는 유저가 이커머스 플랫폼에서 관심을 보였던 상품을 그대로 광고 지면에 보여줘서 광고 클릭과 상품 구매를 유도하는 광고 솔루션입니다. ![retargeting-example](https://tech.buzzvil.com/blog/optimizing-itemcf/retargeting-example.png)

리타겟팅 광고의 예시. SSG에서 유저가 상품을 장바구니에 담고, 버즈빌의 광고 지면에 들어오게 된다면 동일한 상품을 광고 소재로 보게 된다.

버즈빌에서는 홈쇼핑부터 쿠팡 등의 대형 종합 커머스 몰까지 30개에 이르는 다양한 이커머스를 대상으로, 맞춤형 리타겟팅 광고 솔루션을 제공하고 있습니다. 이런 개인화된 광고를 보여주기 위해, 각 커머스에서부터 유저와 상품 간의 활동 이력을 3rd-party 데이터로 받고 있습니다. 이 데이터를 기반으로, 유저가 본 상품을 위 이미지처럼 그대로 보여주는 전통적인 방법보다, 추가 구매를 유도할 수 있는 다른 상품을 찾아 보여주는 방법이 있는지 고민해봤습니다. 결국, 버즈빌에서는 리타겟팅을 추천 문제로 삼아, 머신러닝으로 이를 풀어볼 수 있는지 연구와 개발을 꾸준히 해오고 있습니다. 그 과정에서 도입된 Item-CF 알고리즘을 Python으로 구현한 과정과, 대규모의 이커머스들에게도 Scale-out이 가능하도록, 최적화 방안들에 대해서 소개해드리겠습니다. ## Item-CF란? Item-CF는 Collaborative Filtering (CF) 이라는 추천 알고리즘의 Item 중심적인 유형입니다. CF나 Item-CF의 간단한 이해를 위해 유저와 아이템 간의 Interaction Matrix를 상상해보겠습니다. 가상의 이커머스에 대한 Interaction Matrix를 예시를 들어 설명해보겠습니다. ![user-item-interaction-matrix-example](https://tech.buzzvil.com/blog/optimizing-itemcf/user-item-interaction-matrix-example.png)

3명의 유저와 5개의 상품만을 가진 가상의 이커머스의 Interaction Matrix. 예시로 상품 2와 5의 벡터를 초록새으로 표시.

각 유저가, 각 상품을 샀으면 1, 안 샀으면 0으로 각 셀을 기입을 했습니다. 각 행은 각 유저를 뜻하는 벡터이고, 각 열은 각 아이템을 뜻하는 벡터가 됩니다. 여기서 CF의 직관은 비슷한 유저는 비슷한 상품 구매 패턴을 가질 것이다라는 것입니다. 그러면 한 유저에 대해 추천을 하려면, 그 유저와 가장 비슷한 유저들을 찾아, 그 사람들이 구매한 상품을 보여줍니다. 이와 달리 Item-CF는 비슷한 유저들이 아닌 비슷한 아이템들을 찾는 기법입니다. 각 아이템에 대하여, 제일 비슷한 아이템들을 기록하는 Item Similarities 테이블을 만들고, 실제 추천 상황일 때, 유저가 이전에 구매한 아이템들로 테이블을 조회해서 나오는 가장 비슷한 아이템들을 보여줍니다. Item-CF는 아마존에서 20년 전에 널리 퍼뜨린 알고리즘으로 유명합니다. 더 디테일한 CF와 Item-CF에 대한 정보를 얻기 위해서는 아마존의 [원본 논문](https://www.cs.umd.edu/~samir/498/Amazon-Recommendations.pdf)을 참고하는 것을 추천합니다. 다음 섹션에서는, 이 논문에서 공유한 Item Similarities 테이블을 만드는 알고리즘을 구현하고 최적화한 것에 대해서 다루겠습니다. ## Python으로 재구현하기 ```python {lineNos=true, hl_lines=9} similar_items_table = defaultdict(list) for product in product_catalog: products_bought_together = set() for user in users_who_purchased_product[product]: for product_bought_together in products_purchased_by_user[user]: products_bought_together.add(product_bought_together) for product_bought_together in products_bought_together: score = cosine_similarity(product, product_bought_together) similar_items_table[product].append((product_bought_together, score)) ``` 논문에서는 Pseudocode로 공유를 한 것을 Python으로 재구현했습니다. 이 코드는 위 예시의 커머스나 버즈빌의 가장 작은 이커머스들 대상으로는 문제없이 돌아갔습니다. 하지만 유저들이 60만 명, 상품들이 40만 개가 되는 저희 중간 스케일의 이커머스에 대해서는 같은 말을 할 수 없었습니다. 알고리즘이 걸리는 시간이 무려 300일이 예상되었습니다. ### 최적화 #1: Sparse Vectors 10줄 이내의 Python 코드를 보면 최적화할 수 있는 부분이 쉽게 보이지는 않았습니다. 사실상 For-Loop들 이외에 최적화 할 수 있는 부분은 라인 9, 즉 두 아이템 벡터 간의 Similarity를 계산하는 부분이었습니다. Similarity를 계산하는 수식을 보면서 생각해보겠습니다. ![cosine-similarity-equation](https://tech.buzzvil.com/blog/optimizing-itemcf/cosine-similarity-equation.png) 분자를 구할 때 A와 B 벡터의 각 디멘션을 (i) 참조를 해야 하는 상황입니다. 하지만, 각 벡터의 디멘션 중 하나라도 0이라면 곱해지는 현상 때문에 마지막 결과에 0을 더하는 케이스가 돼버립니다. 0을 더하는 경우는 굳이 계산을 안 해도 된다는 인사이트와 함께, A와 B 벡터에서 같은 디멘션이 둘 다 0이 아닐 경우에만 연산하면 됩니다. 따라서, 벡터에 0이 많을수록, 즉 벡터가 Sparse 할수록, Cosine Similarity 계산의 Time complexity는 줄어드는 것입니다. 좋은 소식은 실제 이커머스들의 유저와 아이템 간의 Interaction Matrix를 보면, 대부분의 유저와 아이템 벡터들은 매우 Sparse 합니다. 전형적인 커머스에서는 대부분의 유저는 전체 상품 중 극소수의 상품들을 구매하며, 마찬가지로 대부분의 아이템은 극소수의 유저들에게만 구매가 됩니다. (물론, 생수 같은 상품은 정말 많은 사람에게 구매될 수가 있어, 해당 아이템 벡터는 Dense 합니다. 이러한 Dense 벡터는 하지만 전체 벡터 중 1%도 안 되었습니다) 결과적으로, 아이템 벡터들을 Sparse Vector 형식으로 저장하고, Cosine Similarity 계산 로직을 Sparse Vector 들을 대상으로 빠르게 계산할 수 있도록 로직을 바꾸었습니다. ```python from scipy.sparse import csr_matrix from sklearn.metrics.pairwise import cosine_similarity # Use sparse vector format a = csr_matrix(...) # instead of np.array([0,0,...,0]) b = csr_matrix(...) similarity_score = cosine_similarity(a, b) # 6x faster! ``` 다행히 Sparse Vector 형태나 실제 Similarity 계산 로직을 재구현할 필요는 없었습니다. 기존에 Scipy와 Scikit-Learn 라이브러리들에서의 Sparse Vector 포맷을 쓰기만 하면, Cosine Similarity 계산은 내부적으로 알아서 최적화를 해주었습니다. 평균적으로, 6배 정도 빨라졌습니다. 이제 300일이 아닌, 50일밖에(?) 안 걸렸습니다. 하지만, 실제 광고 서비스에 도입하기에는 아직 너무 느렸습니다. ### 최적화 #2: Embarrassingly Parallel Workload 첫 번째 줄에 있는 For-Loop은 순차적으로 돌아야 하는지 확인해봤습니다. Item Similarities 테이블을 위해서 아이템 1번의 가장 Similar 한 아이템 벡터들을 구하는 작업을 할 때, 2번 아이템에 대한 작업은 굳이 1번에 대한 작업이 끝난 후 시작할 필요는 없었습니다. 모든 아이템은 독립적으로 Similarity 계산을 할 수 있다는 인사이트를 얻었습니다. Item-CF 알고리즘은 [Embarrassingly Parallel](https://en.wikipedia.org/wiki/Embarrassingly_parallel)한 Workload이었습니다. 따라서, 알고리즘의 Distributed 버전을 써서 최적화를 진행했습니다. Python 개발 환경 안에서 가장 쉽게 적용해볼 수 있는 Distributed Computing Framework를 찾는 와중, Ray라는 Framework를 적용했습니다. ```python import ray ray.init() # Put variables into shared memory using `put` users_who_purchased_product_ref = ray.put(users_who_purchased_product) products_purchased_by_user_ref = ray.put(products_purchased_by_user) @ray.remote # Decorator turns regular function into asynchronous function def compute_partial(partial_product_catalog, users_who_purchased_product, products_purchased_by_user): partial_table = defaultdict(list) for product in partial_product_catalog: products_bought_together = set() for user in users_who_purchased_product[product]: for product_bought_together in products_purchased_by_user[user]: products_bought_together.add(product_bought_together) for product_bought_together in products_bought_together: score = cosine_similarity(product, product_bought_together) partial_table[product].append((product_bought_together, score)) return partial_table # Split products e.g. If you had 10 products, you can call compute_partial() twice - first for 1~5, second for 6~10 futures = [compute_partial(partial_product_catalog_part_1, users_who_purchased_product_ref, products_purchased_by_user_ref), compute_partial(partial_product_catalog_part_2, users_who_purchased_product_ref, products_purchased_by_user_ref), ...] ray.get(futures) # After this line, `futures` has all partially computed similarities. ``` 다른 Framework 후보들로 Spark와 Dask를 고려했지만, Spark는 Python 밖에서 JVM 클러스터 환경을 배포와 운영을 해야 한다는 큰 셋업 비용 때문에 피했고, Dask는 자체 Dask Dataframe, Dask Array 등의 데이터 타입을 써야 한다는 제약 때문에 피했습니다. 위 코드 블록에서 보이듯이 Ray를 썼을 때는 기존 알고리즘을 최소한의 변경으로 분산 처리 가능한 버전으로 전환할 수 있었습니다. 결론적으로, Item-CF 알고리즘을 CPU 코어 개수만큼 분산 처리가 가능하여지도록 만들어 런타임을 50일에서, EC2의 대규모 r5.12xlarge 인스턴스를 써서, 4시간으로 줄일 수 있었습니다! ![48-cores-singing](https://tech.buzzvil.com/blog/optimizing-itemcf/48-cores-singing.png)

r5.12xlarge의 48개의 코어들이 열심히 달리고 있는 모습 :running:

## Single-core vs Multi-core 최적화 지금까지, Item-CF에 대한 간략한 소개와 해당 알고리즘을 Python으로 재구현하면서, 버즈빌의 리타겟팅 광고 시스템에 도입하기 위한 2가지의 최적화 방안을 소개했습니다. Sparse Vector를 쓸 수 있으면 이에 관여하는 로직의 공간 복잡도와 시간 복잡도를 줄일 수 있다는 것을 봤습니다. 더불어, Ray를 통해, 비교적 간단하게, Custom 한 Python 알고리즘을 Distributed 버전으로 치환했습니다. 이번 Item-CF를 최적화하고 상용화하는 과정을 전체적인 관점에서 돌이켜봤을 때, 다시 한번 Distributed 알고리즘의 개발 및 디버깅의 높은 난이도를 체감하게 됬습니다. Ray처럼 아무리 비교적 쉬운 Framework를 쓰더라도, 분산 처리의 최적화 작업은 Single-core 레벨에서의 최적화 작업에 비해 개발 시간이 배로 더 많이 들었습니다. 이를 삼아, 알고리즘 최적화는 Single-core 레벨부터 먼저, 그다음에 Multi-core 관점으로 접근하시는 것을 추천해 드립니다. ## 추천은 복잡한 문제 마지막으로, 버즈빌의 독특한 위치에 기반하여, Item-CF가 다른 추천 ML 알고리즘에 비해 주었던 장점에 대해서 소개해보겠습니다. 동일한 상품 추천이라는 문제를 풀고 있지만, 버즈빌은 광고 회사이지, 이커머스 회사가 아닙니다. 버즈빌의 ML 팀은 하나의 상품 카탈로그에 대해서 추천 문제를 풀고 있지 않고, 무려 30개의 카탈로그에 대한 문제를 풀려고 하고 있습니다. 따라서, 저희가 도입하는 ML 모델이나 추천 솔루션의 제일 중요한 성질 중에 하나는 Robustness입니다. 버즈빌의 추천 시스템은 하나의 광고주가 아닌, 모든 광고주를 만족시킬 수 있어야 합니다. 특히, 만년 리소스 부족이라는 스타트업 환경에서는, 다른 도메인에 적용할 때도 추가 비용 없이 도입할 수 있는 ML 모델들이 절실합니다. Item-CF는 이런 니즈를 만족했습니다. 유저와 상품 간의 Interaction 들만을 가지고 모든 이커머스에 대해서 높은 질의 추천 결과를 만들어낼 수 있었습니다. 유저 성별과 나이, 상품 가격, 상품 카테고리, 등, 다른 메타데이터를 피쳐로 쓰지 않고 있습니다. 물론, Netflix나 Spotify 같은 타회사들의 사례처럼, 유저와 아이템 간의 Interaction 들과 더불어 이런 메타데이터를 썻을 때 추천 성능을 더 높일 수는 있습니다. 하지만, 이런 회사들에 비해 버즈빌이 다른 점은 바로 메타데이터의 데이터 퀄리티가 보장이 안 되어 있다는 것입니다. Interaction 데이터에 비해 메타데이터의 퀄리티는 제공해주는 이커머스마다 큰 차이가 있는 게 현실입니다. 버즈빌 추천 모델의 성능 관점에서는 이런 메타데이터는 긍정적인 첨가물보다는 병목의 원인에 더 가깝습니다. 결론적으로, 추천이라는 문제는 각 회사의 비즈니스 특색과 데이터 퀄리티에 기반하여 풀어야 하는 문제임을 한 번 더 강조하고 싶습니다. Applied ML은 Research ML과 달리, 순수 성능과 더불어, Robustness 같은, 더 실용적인 측면도 측정해야 합니다. 또한 SOTA를 달성하는 화려한 논문들에 현혹되지 말고, 모든 것을 다 해결할 수 있는 만능 추천 모델은 없다고 얘기하고 싶습니다. Item-CF도 절대 그런 모델은 아니니까요 :) 필요한 피쳐 대비 높은 질의 추천 퀄리티를 보장해주는 일명 가성비가 높은 모델이라고 생각하시기 바랍니다. 버즈빌에서 광고와 상품 추천의 문제가 흥미롭께 느껴진다면: [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [배포를 안전하게 - 카나리 배포, 롤백](https://tech.buzzvil.com/blog/배포를-안전하게) Date: 2022-06-21 | Author: Noah Lee | Category: DevOps **배포에서 속도와 안전성 두 마리 토끼를 잡기란 쉽지 않습니다. 이 글에선 버즈빌에선 어떻게 안전성 문제를 해결했는지 소개합니다.** - [배포를 우아하게 - 원-클릭 배포](/blog/배포를-우아하게/) - **배포를 안전하게 - 카나리 배포 전략, 롤백** - [배포를 빠르게 - DIY(Deploy It Yourself)](/blog/배포를-빠르게/) ## 배포를 안전하게 - 카나리 배포 전략, 롤백 이전 글 - 배포를 쉽게 - 에서 버즈빌의 배포 시스템 구조를 설명했습니다. 이번 글에선 버즈빌의 주요 런타임 환경인 AWS EKS 기반의 쿠버네티스(Kubernetes)에 배포 과정과 구현 방법을 자세하게 설명합니다. 또한 고급 배포 전략인 카나리 배포와 롤백에 대한 구현도 설명하도록 하겠습니다. ### 깃헙에서 쿠버네티스까지 ![Architecture](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%ec%95%88%ec%a0%84%ed%95%98%ea%b2%8c/architecture.png) 쿠버네티스 배포는 다음 두 가지 단계로 이루어져 있습니다. 1. 도커 이미지 빌드한다. 2. 쿠버네티스 메니페스트(manifest) 생성 후 반영한다. 커밋 SHA는 유니크(unique) 하지만 길고 읽기 어려우며 빌드 환경을 표현하지 못한다는 단점이 있습니다. 버즈빌의 경우 UI 상에서 SHA 값을 지정할 수 있고 빌드 환경에 영향을 받지 않기 때문에 SHA 값을 선택했습니다 ([참고](https://docs.microsoft.com/ko-kr/azure/container-registry/container-registry-image-tag-version#unique-tags)). 버즈빌은 도커 이미지를 모든 커밋 별로 빌드하고 있습니다. 푸쉬 이벤트가 발생하면 CI에서 도커 이미지를 빌드하고 이미지를 저장소에 저장합니다. 이미지 태그는 **커밋 SHA 값을 사용해 유니크(unique)함을 보장합니다**. 또한 **각 커밋별 이미지 존재 여부는 커밋 상태에 표기해서 배포 시 확인할 수 있습니다** (i.e, `docker-image` 컨택스트).
깃헙 상태 ![Commit Status](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%ec%95%88%ec%a0%84%ed%95%98%ea%b2%8c/commit-status.png)

이미지 빌드 후 쿠버네티스 메니페스트 생성이 필요합니다. 저희는 쿠버네티스 메니페스트 생성 도구로 헬름(Helm, [문서](https://helm.sh/docs/intro/using_helm/#three-big-concepts) 참고)을 선택해서 사용하고 있습니다. 그리고 **모든 마이크로서비스는** **동일한 헬름 차트로 쿠버네티스 메니페스트를 생성**하며, 각 서비스별로 다른 메니페스트를 생성하기 위해 `values.yaml` 파일과 `helm` 의 `--set`옵션으로 오버라이드를 합니다. 특히, **`--set` 옵션은 위에서 빌드한 이미지 태그를 동적으로 지정할 수 있게 합니다**. 헬름 커맨드 예시는 아래와 같습니다.
헬름 커맨드 예시 ```bash helm upgrade \ buzzvil-microservice \ buzzvil-chartmuseum/buzzvil-chart \ # buzzvil-chartmusuem: 차트 저장소, buzzvil-chart: 단일 헬름 차트 --install \ -f release/values.yaml \ --set global.version=COMMIT_SHA # 커밋 SHA 값을 이미지 태그로 지정 ```

내부적으로 버즈빌은 직접 헬름 명령어를 실행하지 않고 배포 도구인 스피네이커(Spinnaker, 공식 [문서](https://spinnaker.io/docs/guides/user/kubernetes-v2/deploy-helm/))를 통해 간접적으로 실행하고 있습니다. 스피네이커 파이프라인은 헬름 차트는 버즈빌 내부 차트 저장소에서, `values.yaml` 파일은 각 마이크로서비스의 깃헙 레포지토리에서 가져오고 있습니다. 마지막으로 **도커 이미지 태그 - 커밋 SHA - 는 깃헙 배포 이벤트를 통해서 받습니다**. 그렇다면 스피네이커 파이프라인은 어떻게 배포 이벤트를 수신할까요? 스피네이커(Spinnaker) 파이프라인은 여러 종류의 트리거를 제공하며, 그 중 **웹훅 트리거를 제공하고 있습니다** ([문서](https://spinnaker.io/docs/guides/user/pipeline/triggers/webhooks/) 참고). 버즈빌은 웹훅 트리거 설정을 통해 깃헙 [배포 이벤트](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#deployment)로 파이프라인이 트리거 되도록 하였습니다. 배포 이벤트는 레포지토리 정보, 배포 정보 등을 가지고 있어 웹훅 트리거의 제약 조건(constraint)으로 사용하기 용이합니다. 버즈빌은 레포지토리 이름( `repository.name`)과 배포 환경(`deployment.environment`)을 트리거 제약 조건으로 사용하여 특정 마이크로서비스가 배포되면 파이프라인이 트리거되도록 합니다.. 참고로, 배포 테스크(`deployment.task`)도 사용하는데 다른 런타임 환경에 대한 배포와 구분하기 위한 용도로 사용하고 있습니다.
스피네이커 트리거 ![Spinnaker Trigger](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%ec%95%88%ec%a0%84%ed%95%98%ea%b2%8c/spinnaker-trigger.png)

또한, 스피네이커는 파이프라인 표현식을 통해 전달 받은 페이로드에 접근이 가능합니다([문서](https://spinnaker.io/docs/guides/user/pipeline/expressions/) 참고). **버즈빌은 파이프라인 표현식을 통해 배포 된 커밋 SHA을 가져올 수 있으며, 동적으로 이미지 태그 설정이 가능해 집니다**(i.e., `${trigger['payload']['deployment']['sha']}`).
스피네이커 템플릿 ![Spinnaker template](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%ec%95%88%ec%a0%84%ed%95%98%ea%b2%8c/spinnaker-template.png) ```bash # 템플릿과 동일한 명령어 helm upgrade \ buzzvil-microservice \ buzzvil-chartmuseum/buzzvil-chart \ --install \ -f release/values.yaml \ --set global.version=${trigger['payload']['deployment']['sha']} ```

마지막으로 스피네이커는 동적으로 생성된 쿠버네티스 메니페스트를 배포하게 됩니다. ### 카나리 배포 ![Canary Demo](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%ec%95%88%ec%a0%84%ed%95%98%ea%b2%8c/canary-demo.gif) 버즈빌은 두 가지 배포 전략을 - 롤링(rolling) 업데이트와 카나리 배포 - 선택할 수 있습니다. 롤링 업데이트란 점진적으로 서버를 새로운 버전으로 업데이트하는 전략이며 쿠버네티스트 Deployment 리소스가 기본으로 제공하고 있습니다. 반면 카나리 배포란 일부 서버에만 새로운 버전을 배포하고, 일부 트래픽을 새 버전으로 분산하는 방법입니다. 카나리 배포는 A/B 테스트를 가능하게 하고 피해 반경을 좁혀 배포의 안정성을 높여줍니다. 하지만 안타깝게 **카나리 배포는 쿠버네티스에서 지원하고 있지 않아 직접 구현이 필요합니다**. 카나리 배포는 다음 단계로 이루어져 있습니다. 1. 카나리 릴리즈를 생성한다. 2. 카나리 릴리즈로 일부 트래픽을 분산한다. 3. 카나리 릴리즈를 정리한다. 먼저 분산된 트래픽을 처리하기 위한 카나리 릴리즈를 준비합니다. 버즈빌은 카나리 릴리즈도 위에서 설명한 것과 동일한 방법으로 배포합니다. 동일한 헬름 차트와 동일한 `values.yaml` 파일이 사용됩니다. 다만, 카나리 릴리즈 설정을 위해 헬름 차트 `global.canary.enabled`를 제공하며, 이는 **내부적으로 쿠버네티스 리소스의 이름에 `-canary`란 접미사를 붙여서 메인(main) 릴리즈와 구분합니다**. 예를 들어, 메인 릴리즈의 Deployment 리소스 명이 `microservice-prod` 라면 카나리 릴리즈는 `microservice-prod-canary` 란 이름을 갖습니다.
스피네이커 카나리 템플릿 ![Spinnaker template 1](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%ec%95%88%ec%a0%84%ed%95%98%ea%b2%8c/canary-spinnaker-template.png) ```bash # 템플릿과 동일한 명령어 helm upgrade \ buzzvil-microservice \ buzzvil-chartmuseum/buzzvil-chart \ --install \ -f release/values.yaml \ --set global.version=${trigger['payload']['deployment']['sha']} \ --set golbal.canaryMode.enabled=true \ --set global.canaryMode.weigth=0 \ --set replicaCount=2 ```

이제 새로운 버전의 카나리 릴리즈가 준비됐고 트래픽 일부를 카나리 릴리즈로 전달해야 됩니다. 버즈빌은 서비스 매쉬(Service Mesh)로 이스티오(Istio)를 사용하고 있으며 이스티오는 트래픽 시프팅(Traffic shifting, [문서](https://istio.io/latest/docs/tasks/traffic-management/traffic-shifting/) 참고) 기능을 지원하고 있습니다. 트래픽 시프트를 위해선 이스티오의 VirtualService 리소스가 필요하며, VirtualService 리소스에서 기존 릴리즈와 카나리 릴리즈로 보낼 트래픽 비중(`weight`)을 설정하게 됩니다. 버즈빌의 단일 헬름 차트도 VirtualService 리소스를 포함하고 있으며 트래픽 비중을 지정할 수 있도록 `global.canaryMode.weight` 를 제공하며, 내부적으로 **하나의 VirtualService만 생성해서 메인 릴리즈와 카나리 릴리즈로 요청을 분산할 수 있도록 했습니다**. 즉, VirtualService만은 `-canary` 접미사를 붙이지 않고 두 릴리즈가 공유하도록 했습니다.
스피네이커 카나리 템플릿 2 ![Spinnaker template 2](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%ec%95%88%ec%a0%84%ed%95%98%ea%b2%8c/canary-spinnaker-template-2.png) ```bash # 템플릿과 동일한 명령어 helm upgrade \ buzzvil-microservice \ buzzvil-chartmuseum/buzzvil-chart \ --install \ -f release/values.yaml \ --set global.version="${trigger['payload']['deployment']['sha']}" \ --set golbal.canaryMode.enabled=true \ --set global.canaryMode.weigth=5 --set replicaCount=2 ```

VirtualService 예시 ```yaml apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: microservice-prod namespace: microservice spec: hosts: - microservice-prod http: - name: primary route: - destination: host: microservice-prod weight: 95 - destination: host: microservice-prod-canary weight: 5 ```

위에서 말한 것 같이, 카나리 배포는 A/B 테스트를 용이하게 합니다. 버즈빌은 모니터 도구로 데이터독을 사용하고 있고 데이터독은 각 버전별로 메트릭을 제공하고 있습니다. 이 과정을 통해 **사용자는 두 버전 간에 비교로 새 버전을 검증할 수 있습니다**. 또한 **새 버전에서 이슈가 확인돼도 일부 요청만 영향이 있기 때문에 피해 반경을 좁힐 수 있습니다**. 검증이 끝난 후 트래픽을 다시 메인으로 옮기고 카나리를 릴리즈를 정리하도록 합니다. 그리고 사용자는 전체 배포를 진행할지 또는 배포를 취소할지 결정하도록 합니다.
스피네이커 카나리 템플릿 3 ![Spinnaker template 3](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%ec%95%88%ec%a0%84%ed%95%98%ea%b2%8c/canary-spinnaker-template-3.png) ```bash # 템플릿과 동일한 명령어 helm upgrade \ buzzvil-microservice \ buzzvil-chartmuseum/buzzvil-chart \ --install \ -f release/values.yaml \ --set global.version=${trigger['payload']['deployment']['sha']} \ --set golbal.canaryMode.enabled=true \ --set global.canaryMode.weigth=0 \ --set replicaCount=0 ```

추가적으로, 버즈빌은 배포하는 시점에서 카나리 릴리즈의 레플리카 수와 트래픽 비중을 선택할 수 있도록 합니다. 해당 설정 값은 깃헙 이벤트의 `deployment.payload` 필드로 전달합니다. 버즈빌은 깃플로이(Gitploy)의 다이나믹 페이로드(dynamic payload) 기능 - 배포 시점에서 동적으로 `payload` 를 전달 - 을 통해서 동적으로 설정합니다. 그리고 배포 이벤트를 수신한 스피네이커에서 페이로드에 접근해 값을 가져와서 사용하고 있으며, 만약 값이 없을 시 기본 값을 사용하도록 되어 있습니다.
깃플로이 설정 ```yaml envs: - name: prod task: deploy:kubernetes required_context: ['docker-image'] serialization: true dynamic_payload: enabled: true inputs: canaryEnabled: type: select options: - "true" - "false" required: true description: Deploy in a canary strategy. default: "true" canaryWeight: type: number required: false description: Set traffic weight to canary. default: 5 canaryReplicaCount: type: number required: false description: Set canary replica count. default: 2 ```

스피네이커 파이프라인 표현식 - replicaCount: `${ trigger['payload']['deployment']['payload']['canaryReplicaCount']?: "2" }` - weight: `${ trigger['payload']['deployment']['payload']['canaryWeight']?: "5" }`

### 롤백 ![Rollback Demo](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%ec%95%88%ec%a0%84%ed%95%98%ea%b2%8c/rollback-demo.gif) 롤백(Roll-back)이란 이전 버전으로 되돌아가는 것을 의미합니다. 일반적으로, 현재 버전에 이슈가 있는 경우 롤백을 통해서 상태를 빠르게 회복시킬 수 있습니다. **버즈빌은 롤백을 배포 히스토리의 커밋 SHA으로 구현하고 있습니다**. 배포 히스토리는 각각의 커밋 SHA을 포함하고 있고 이를 다시 배포하는 방식입니다. 커밋 SHA를 이용한 이유는 쿠버네티스 메니페스트 생성을 결정하는 `values.yaml` 파일이 각 레포지토리에 포함되어 버저닝되어 있기 때문입니다. 즉, **이전 버전의 `values.yaml` 파일은 이전과 동일한 쿠버네티스 메니페스트를 생성하게 됩니다**. 아래 스피네이커 템플릿을 보면 깃헙 레포지토리에서 `values.yaml` 파일을 가져올 때 커밋을 동적으로 지정하는 것을 볼 수 있습니다.
스피네이커 깃헙 템플릿 ![Spinnaker template](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%ec%95%88%ec%a0%84%ed%95%98%ea%b2%8c/rollback-spinnaker-template.png)

일반적으로 우리는 롤백을 위해서 코드 리버트(revert)하며 피해를 장기화시킵니다. 또한, 메신저로 “배포를 잠시 멈춰주세요!”라는 경고를 보냅니다. 하지만 **이런 방식은 문제를 더 가중시키며 근본적인 문제를 해결하지 못합니다**. 버즈빌도 동일한 문제를 경험했습니다. 그러나 현재는 한 번의 클릭으로 롤백이 가능하며 문제 해결 동안 락(Lock)을 통해 배포하지 못하도록 막고 있습니다. ## 마치면서 버즈빌은 지속적으로 조직이 더 자신감 있고 안전하게 배포 가능하도록 시스템을 개선해 왔습니다. 현재는 이전 보다 편리하면서도 안정적으로 배포가 이루어지고 있습니다. 만약 여러분의 팀이 속도와 안정성 사이에서 고민하고 있다면 이 글이 도움이 되길 바랍니다. 긴 글 읽어주셔서 감사합니다. :) ## 참고 - [카나리 테스트와 함께하는 안전한 서버 배포](https://engineering.vcnc.co.kr/2021/04/canary/) - [The road to a production release](https://deliverybot.dev/2019/11/20/road-to-production/) [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [배포를 우아하게 - 원-클릭(one-click) 배포](https://tech.buzzvil.com/blog/배포를-우아하게) Date: 2022-06-14 | Author: Noah Lee | Category: DevOps **일반적으로 런타임 환경이 늘어날 수록 배포 시스템의 복잡성은 높아지기 마련입니다. 이 글에선 버즈빌에선 어떻게 문제를 해결했는지 소개합니다.** - **배포를 우아하게 - 원-클릭 배포** - [배포를 안전하게 - 카나리 배포 전략, 롤백](/blog/배포를-안전하게/) - [배포를 빠르게 - DIY(Deploy It Yourself)](/blog/배포를-빠르게/) ## 배포를 우아하게 - 원-클릭 배포 ![Demo](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%ec%9a%b0%ec%95%84%ed%95%98%ea%b2%8c/demo.gif) **현재 버즈빌은 사용자가 UI에서 단 몇 번만의 클릭만으로 서비스를 배포하고 있으며, 어플리케이션의 런타임(runtime) 환경과 상관없이 동일한 방법으로 배포하고 있습니다.** 런타임 환경이란 프로그램이 실행되는 환경을 의미합니다. 버즈빌은 런타임 환경으로 AWS EKS 기반의 쿠버네티스(Kubernetes), AWS 람다(Lambda), AWS S3등을 사용하고 있고 각 런타임 환경 별로 다른 배포 도구를 사용하고 있습니다. 그러나 여러 배포 도구를 사용하고 있음에도 사용자는 동일한 방식으로 배포합니다. 일반적으로 런타임 환경 별로 배포 도구가 다르기 마련입니다. 버즈빌은 쿠버네티스는 스피네이커(Spinnaker), AWS S3는 드론(Drone) CI를 사용하고 있습니다. 그리고 배포 도구 별로 다른 배포 방식을 가지게 됩니다. 따라서 사용자는 각각의 배포 도구 사용에 익숙해져야 하며, 배포 파이프라인의 구체적인 내용을 이해하고 있어야만 합니다. **즉, 런타임 환경이 늘어나면 배포 시스템의 복잡성도 커지게 됩니다.** 단순한 해결책으로는 한 개의 배포 도구만 운영하는 방법이 있습니다. 하지만 단 하나의 배포 도구는 각 런타임 환경에서 필요한 기능들을 모두 만족하기 어렵습니다. **버즈빌은 깃헙(GitHub) [배포 API](https://docs.github.com/en/rest/deployments/deployments#about-the-deployments-api)를 통해서 배포 파이프라인을 추상화해서 이 문제를 해결했습니다.** 사용자는 직접 배포 도구와 상호작용 없이 깃헙 API만을 통해 배포를 트리거하고 배포 상태을 확인할 수 있습니다. 따라서 사용자는 동일한 방식으로 배포가 가능해집니다. 다음은 깃헙 배포 API를 알아보고 버즈빌에선 어떻게 이용하고 있는지 살펴보도록 하겠습니다. ### 깃헙 배포 API 버즈빌은 소스 코드 관리 도구로 깃헙(GitHub)을 이용하고 있으며, 깃헙에서 제공하고 있는 [배포 API](https://docs.github.com/en/rest/deployments/deployments#create-a-deployment)를 사용하고 있습니다. 배포 API는 특정 `ref`(브랜치, 커밋 또는 태그)에 대한 배포를 깃헙에 요청합니다. 그리고 깃헙에선 배포 API가 호출되면 이벤트를 배포 도구에 전달하게 됩니다. 또한, 깃헙은 [배포 상태 API](https://docs.github.com/en/rest/deployments/statuses#create-a-deployment-status)를 제공하는데 외부에서 배포의 상태를 업데이트할 때 사용됩니다. 배포 상태 API는 여러 상태를 표현할 수 있도록 `status` 파라미터를 제공하고 있으며, 선택적으로 `description`, `log_url` 파라미터를 통해 배포 상태를 자세하게 설명할 수 있도록 합니다. 아래는 버즈빌의 도구들이 깃헙과 어떻게 상호작용하는지 보여주는 다이어그램입니다. 사용자는 `Trigger` 만을 통해서 깃헙과 통신합니다. 그리고 깃헙은 `3rd Party` 배포 도구 - 스피네이커나 드론 CI - 를 통해서 배포합니다. ``` +---------+ +--------+ +-----------+ +-------------+ | Trigger | | GitHub | | 3rd Party | | Server | +---------+ +--------+ +-----------+ +-------------+ | | | | | Create Deployment | | | |--------------------->| | | | | | | | Deployment Created | | | |<---------------------| | | | | | | | | Deployment Event | | | |---------------------->| | | | | Deploys | | | |-------------------->| | | | | | | Deployment Status | | | |<----------------------| | | | | | | | | Deploy Completed | | | |<--------------------| | | | | | | Deployment Status | | | |<----------------------| | | | | | ``` 배포 API는 배포를 안전하고 유연하게 할 수 있도록 다양한 파라미터를 제공하고 있습니다. 다음은 배포 API에서 제공하고 있는 주요 파라미터를 소개하고 버즈빌에서 어떻게 사용을 하고 있는지 설명하겠습니다. `ref` 파라미터는 브랜치 명, 태그 명 또는 커밋 SHA 값이 될 수 있습니다. 버즈빌에선 지속해서 배포하는 어플리케이션은 브랜치 또는 커밋 SHA 값을 이용하고 있고, 시맨틱 버저닝을 사용하는 어플리케이션을 태그를 사용하고 있습니다. `environment` 파라미터는 배포될 환경을 지정하는 데 사용됩니다. 버즈빌에선 목적에 따라서 몇 가지 환경 - `dev`, `staging`, `prod` - 을 제공하고 있습니다. 엔지니어는 작업 중인 브랜치를 특정 환경에 배포해서 어플리케이션을 검증합니다. 또한 환경별로 배포 기록을 수집해서 변경 내용을 추적하고 있습니다. `auto_merge` 파라미터는 선택된 `ref` 가 레포지토리의 기본 지점보다 앞에 있는지 검증하는 데 사용됩니다. 만약 `ref`가 레포지토리의 기본 지점보다 뒤에 있다면 깃헙에서 자동으로 머지합니다. 버즈빌은 `auto_merge` 를 통해 운영 환경에 배포 시 메인 브랜치가 반영될 수 있도록 합니다. `required_contexts` 파라미터는 `success` 여야만 하는 커밋의 컨택스트(context)을 지정할 수 있습니다. 예를 들어, 아래와 같이 두 개의 커밋 컨택스트 중 하나만 선택한다면 `required_contexts` 로 지정이 가능합니다. 버즈빌에서 쿠버네티스로 배포하는 경우 해당 커밋에 도커 이미지의 존재 여부를 검증하기 위해서 사용하고 있습니다(i.e., `required_contexts: ['docker-image']`). ![커밋 컨택스트](https://tech.buzzvil.com/blog/%eb%b0%b0%ed%8f%ac%eb%a5%bc-%ec%9a%b0%ec%95%84%ed%95%98%ea%b2%8c/commit-contexts.png) `payload` 파라미터는 배포 도구에 추가로 필요한 정보를 전달하는 데 사용됩니다. 버즈빌은 payload 를 통해 사용자가 배포하는 시점에 배포 전략을 선택할 수 있게 합니다. 현재는 두 가지 배포 전략 - 롤링 업데이트와 카나리 배포 -를 제공하고 있으며, 배포 시 샐렉트(Select) 박스로 선택이 가능합니다. ## 이벤트 기반 배포 시스템 저희가 깃헙 배포 API를 중심으로 배포 시스템을 구축한 이유는 **배포 도구를 느슨하게 결합하기 위해서입니다**. 많은 경우 배포 시스템은 배포 도구와 강력하게 결합이 되며, 배포 시스템은 배포 도구에 의존성을 가지게 됩니다. 만약 필요에 의해 배포 도구를 변경하고 싶어도 변경할 수 없는 상황이 되는 것입니다. 버즈빌은 문제를 해결하기 위해 **깃헙 배포 API를 통해서 각 도구의 사용법을 추상화했습니다.** 추상화를 통해서 사용자는 배포 도구들과 상호작용하는 것이 아닌 깃헙 배포 API와 상호작용함으로 항상 동일하게 배포할 수 있습니다. 시스템 구축은 여러 도구를 가져와서 조합함으로 자체적인 개발 없이 빠르게 구성이 가능했습니다. 먼저 깃헙 배포 API와 통신 - 다이어그램에서 `Trigger` - 하기 위해서 깃플로이([Gitploy](https://www.gitploy.io/))를 선택했습니다. 깃플로이는 직관적인 UI를 제공하고 있어 낮은 러닝(learning) 커브를 보장할 수 있었습니다. 그리고 배포 도구 - 다이어그램의 `3rd Party` - 로 쿠버네티스는 스피네이커([Spinnaker](https://spinnaker.io/)), AWS S3나 람다 는 드론([Drone](https://www.drone.io/)) CI를 선택했습니다. 또한 사용자는 롤백, 락(Lock), 리뷰 등의 배포에 필수적인 기능을 동일하게 실행할 수 있게 됐습니다. 기존에는 각 배포 도구별로 사용법을 익혀야 했다면 더 이상 도구를 배우는 데 시간을 소비하지 않을 수 있었습니다. ## 마치면서 저는 깃헙 배포 API가 코드(code)에서 인터페이스(interface)와 비슷하다고 생각합니다. 코딩에서 인터페이스가 복잡성을 낮추고 확장성을 높이는 역할을 하듯이, 배포 시스템에선 깃헙 배포 API를 통해서 같은 효과를 경험할 수 있었습니다. 그리고 이를 통해 버즈빌의 배포 시스템은 훨씬 유연해질 수 있었습니다. 만약 여러분이 확장성 있는 배포 시스템을 고민하고 있다면 이 글이 도움이 됐으면 좋겠습니다. *아쉽게도 해당 글에서 시스템 구현을 포함하지 못했습니다. 하지만 다음 글 - 배포를 안전하게 - 에서 실제 구현과 과정에 배운 점들을 공유할 수 있도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 다음 글도 기대해주세요!* [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [Elasticsearch 검색에서 확률 사용하기](https://tech.buzzvil.com/blog/probability-in-es-search) Date: 2022-06-07 | Author: Isac Yoo | Category: Data & ML 이 블로그에서는 Elasticsearch에서 검색할 때 효과적으로 각 Document에 확률을 사용하는 두 가지 방법을 설명하고, 각 방법의 간략한 성능 비교와 장단점을 설명합니다. 독자는 Elasticsearch의 Search API에 간단한 쿼리를 (`term`, `bool` 등) 작성할 수 있다고 가정합니다. ### 서론 버즈빌에서는 빠른 광고 서빙을 위해서 Elasticsearch에 광고를 캐싱하고 있습니다. RPS가 높으므로 모든 광고를 inverted index에 올려놓고 광고 요청이 왔을 때 매칭되는 광고를 빠르게 찾아내는 것이죠. 광고 서버에서는 다양한 이유로 각 광고의 랜덤성을 조절해야 할 필요가 있습니다. 예를 들어 광고 A1, A2가 있고, 요청 R1, R2이 있을 때, R1 요청을 분석해보니 A1과 A2는 각각 20%와 80% 확률로 나가야 하고, R2 요청은 A1과 A2가 각각 70%, 100% 확률로 나가야하는 경우가 발생합니다. 이러한 동적인 정보는 사전에 알 수 없으므로 inverted index에 미리 색인할 수 없습니다. 그 때문에 `term`, `range` 같은 기본적인 쿼리로는 이 목적을 달성할 수 없고, Elasticsearch에서 제공하는 다른 기능을 활용해야 합니다. ### 가정 핵심만 전달하기 위해 서론의 상황보다 간소화된 환경을 가정하겠습니다. 각 광고는 `alloc_rate`이라는 필드를 가지며 0~100의 값을 가질 수 있습니다. 이 필드는, "해당 광고는 모든 요청에서 `alloc_rate`% 의 확률로 할당된다"라는 의미입니다. 그 때문에 100이 설정되어있다면 모든 요청에 항상 할당되고, 10가 설정되어있다면 요청마다 10%의 확률로 할당되는 것입니다. #### Elasticsearch Elasticsearch의 Index는 아래와 같이 설정되어있다고 가정합니다. 도메인에서 정의한 것과 같이 0~100의 값을 저장합니다. ```json { "settings": { ... }, "mappings": { "properties": { ..., "alloc_rate": { "type": "short", "index": true }, ... } } } ``` ### 방법 0. range 틀린 방법이지만 (글쓴이 포함) 쉽게 빠지는 함정이 있어서 가장 먼저 설명합니다. 바로 애플리케이션 코드에서 0~100의 랜덤한 값을 구한 후 ES query에 상수로 넣는 것입니다. 예를 들어 Python 광고 서버의 경우 아래와 같이 구현될 것입니다. ```python query = { "range": { "alloc_rate": { "gt": random.randint(0, 99) } } } es_client.search(query) ``` 어떤 문제가 발생할까요? 예를 들어 광고가 총 3개 있는데 모두 `alloc_rate`이 10이라고 가정했을 때, `randint()`가 10 초과일 경우 항상 3개의 광고가 모두 할당됩니다. 반대로 `randint()`가 10 이하일 경우 항상 3개 광고가 모두 할당되지 않습니다. ### 방법 1. script 첫 번째 방법은 [Script query](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-script-query.html)를 사용하는 것입니다. ```json { "script": { "script": "doc['alloc_rate'].value > Math.random() * 100" } } ``` 쿼리 자체만 봤을 때 직관적이고 쉽게 이해할 수 있습니다. 각 Document(ES에 색인된 광고)에 대해서 `[0, 1)`의 랜덤값을 구하고 `alloc_rate`과 비교함으로써 랜덤한 할당을 구현하는 것입니다. Document마다 랜덤값을 새로 계산하기 때문에 방법0의 문제를 해결할 수 있습니다. 하지만 [Script query 공식 문서](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-script-query.html)의 최상단에 적혀있는 것처럼, script query는 성능이 좋지 않습니다. [script의 성능에 관한 공식 문서](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/scripts-and-search-speed.html)에 성능 문제를 회피할 수 있는 몇 가지 방법들이 적혀있지만, 저희 경우를 해결할 수 있을법한 방법은 없습니다. > `[`는 열린구간, `)`는 닫힌구간이고 `[0, 1)`는 0부터 1까지의 실수 중 0 포함 1 제외이고, `(0, 1]`은 0 제외 1 포함을 의미합니다. ### 방법 2. function_score 방법 0, 1의 문제를 모두 해결한 방법은 [Function score query](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-function-score-query.html)를 사용하는 것입니다. > [공식 문서의 `random_score` 설명](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-function-score-query.html#function-random)에서 서술했듯 > `seed`와 `field`를 설정하는 게 필요할 수 있습니다. 저희의 경우 모든 광고 할당에 서버에서 생성한 seed를 쿼리로 주입해주고 있습니다. ```json { "function_score": { "functions": [ { "random_score": { "field": "_seq_no", "seed": ${some_random_value} } }, { "field_value_factor": { "field": "alloc_rate", "modifier": "none", "factor": 0.01, "missing": 0 } } ], "score_mode": "sum", "boost_mode": "replace", "min_score": 1 } } ``` `function_score`는 기본적으로 Document의 score 계산 방식을 조작할 수 있게 해주는 쿼리입니다. 이 기능을 이용해서 각 Document의 score를 랜덤하게 계산하고, 그 랜덤값을 `alloc_rate`과 비교하는 것입니다. 방법1도 기능만 봤을 때는 요구조건을 만족하기 때문에, `function_score`를 사용하는 방법 또한 같은 기능을 합니다. 쿼리를 부분별로 나눠서 역할 설명 후 전체적인 의미를 설명하겠습니다. #### function_score.functions 현재 `function_score`는 score 계산 방식의 선택지로 아래 4가지를 제공하고 있습니다. `function_score.script_score`처럼 하나의 선택지만 사용할 수 있지만, `function_score.functions`에 배열 형태로 여러 선택지를 동시에 선택하고, `score_mode`로 선택지들의 결괏값을 어떻게 조합할지 정할 수 있습니다. - `script_score` - [공식 문서](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-function-score-query.html#function-script-score) - Script query와 같이 script를 각 Document의 score를 계산하는 painless 스크립트를 정의할 수 있습니다. - Script query와 마찬가지로 성능이 좋지 않습니다. - `random_score` - [공식 문서](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-function-score-query.html#function-random) - `[0, 1)`의 랜덤한 값을 생성합니다. - `field_value_factor` - [공식 문서](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-function-score-query.html#function-field-value-factor) - Document의 특정 필드를 score로 사용할 수 있습니다. - decay functions: `gauss`, `linear`, `exp` - [공식 문서](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-function-score-query.html#function-decay) - 어떤 기준을 정하고, Document의 특정 필드가 기준과 얼마큼 거리가 있는지를 (값이 차이 나는지) score로 사용합니다. - 거리 계산 방식에 따라서 `gauss`, `linear`, `exp`로 나뉩니다. #### function_score.score_mode `function_score.functions`에 정의한 함수들이 각자의 score 값을 계산해낼 텐데, 그 값을 어떻게 조합해서 최종 score를 만들 것인지를 정합니다. Python 코드로 묘사하면 아래와 같습니다. (이해를 위해 `doc.score` 계산 과정에서 `boost_mode`를 생략했습니다. 아래 해당 항목에서 최종 계산식을 확인해주세요.) ```python for doc in documents: doc.score = score_mode(f(doc) for f in function_score.functions) ``` [공식 문서](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-function-score-query.html)에서 각각 어떤 `score_mode`가 있는지 확인할 수 있고, 여기서는 각 함수의 score 계산값을 모두 합하는 `sum`을 사용했습니다. #### function_score.boost_mode 사실 `function_score.score_mode`가 최종 score가 되는 것은 아니고 `function_score.boost_mode` 연산을 한 번 더 거칩니다. 공식 문서를 보면 `functions_score.query`가 있어서 다른 쿼리를 중첩할 수 있고, 정의하지 않는다면 `{ "match_all": {} }`와 같은 의미가 있습니다. Elasticsearch에서는 아주 기본적인 `term` 쿼리만 날려도 score를 계산하기 때문에, `query` 또한 매칭되는 Document 들이 score를 가지게 됩니다. 그래서 `score_mode`의 결괏값과 `query`의 score 값 사이에도 연산이 필요할 수가 있고, `boost_mode`가 이때 사용됩니다. Python 코드로 표현하면 아래와 같습니다. ```python for doc in documents: query_score = ... # `function_score.query`에서 `doc`의 score doc.score = boost_mode( score_mode(f(doc) for f in function_score.functions), query_score, ) ``` [공식 문서](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-function-score-query.html)에서 각각 어떤 `boost_mode`가 있는지 확인할 수 있고, 여기서는 `function_score.query`의 score는 무시하고 `function_score.functions`의 결과만 사용할 것이기 때문에 `replace`를 사용했습니다. #### function_score.min_score `function_score.boost_mode`까지 거쳐서 각 Document의 최종 score가 결정됐다면, 그 값을 기반으로 Document를 결과에서 제외할지 아닐지를 결정할 수 있는 옵션입니다. 여기서는 `min_score`로 1을 설정했고, 최종 score가 1 미만인 Document는 쿼리 결과에서 제외한다는 의미입니다. #### 정리 위의 지식 파편들로 쿼리의 동작을 설명하겠습니다. 결국 방법2는 방법1의 최적화일 뿐 의미 자체는 동일 해야 하므로 해당 부등식을 `functions_score`에 적용할 수 있도록 조금 손봐야 합니다. 방법1에서 조건식은 아래와 같습니다. `(alloc_rate / 100) > rand` (`rand`는 `[0, 1)`를 반환하는 함수라고 정의합니다) 여기서 양변에 `(1 - rand)`를 더하면 아래와 같습니다. `(alloc_rate / 100) + (1 - rand) > rand + (1 - rand)` 최종적으로 우변을 정리하면 아래와 같습니다. `(alloc_rate / 100) + (1 - rand) > 1` 여기서 `rand`의 정의상 `[0, 1)`를 반환하기 때문에 좌변의 `(1 - rand)`는 `(0, 1]`의 값을 가질 수 있습니다. 미묘하게 다르긴 하지만 `rand`와 거의 같은 값의 범위를 가진다고 볼 수 있습니다. 식과 ES 쿼리를 매핑해보면, 식에서 `> 1` 부분을 `"min_score": 1`로 해결합니다. 또한 `functions`의 `random_score`가 `(1 - rand)` 부분을 의미하고, `field_value_factor`는 `(alloc_rate / 100)`을 의미합니다. Document의 `alloc_rate`는 0~100의 값이기 때문에, 이것을 0~1로 스케일을 줄이기 위해서 `factor`로 0.01을 넣었습니다. ##### 부연설명 사실 `"min_score": 1`는 `> 1`이 아니라 `>= 1`로 동작합니다. 그리고 `(1 - rand)`는 `(0, 1]`이기 때문에, 식만 보면 `alloc_rate`이 0일 때에도 할당 가능성이 있어 보입니다. (대입하면 `0 / 100 + 1 >= 1`이고, 참이기 때문에 할당 가능) 하지만 실제로는 `(1 - rand)` 대신 `rand`를 쓰고 있고, `[0, 1)`의 범위이기 때문에 그렇한 문제가 발생할 여지가 없습니다. 총정리하면 위의 쿼리는 `(alloc_rate / 100) + rand >= 1`을 의미합니다. ##### 부록: `random_score`가 0이 나올 확률 `random_score`는 균등 분포이기 때문에 `[0, 1)` 구간의 모든 값이 같은 확률로 나올 수 있습니다. 그 때문에 해당 구간에서 발생할 수 있는 모든 수의 개수를 N으로 구하면 1 / N으로 0의 등장 확률을 알 수 있습니다. 수학적으로만 생각하면 해당 구간은 실수이기 때문에 구간에 등장할 수 있는 가짓수는 무한입니다. 하지만 [Elasticsearch의 `random_score` 구현 코드](https://github.com/elastic/elasticsearch/blob/master/server/src/main/java/org/elasticsearch/common/lucene/search/function/RandomScoreFunction.java#L55-L79)를 보면, 24bit 길이 정수를 기반으로 `[0, 1)` 랜덤값을 결정하는 것을 알 수 있고, 그 때문에 N은 `2 ^ 24 = 16,777,216`임을 알 수 있습니다. 결국 대략 `1 / 10,000,000` 확률로 0이 나온다는 것을 알 수 있습니다. ### 방법 3. function_score + linear decay 방법2로도 충분히 성능을 챙기면서 기능을 구현할 수 있습니다. 하지만 `field_value_factor` 말고 `linear` decay를 사용해서 같은 기능을 서빙할 수 있고, 또 다른 용례도 있어서 소개합니다. ```json { "function_score": { "functions": [ { "random_score": { "field": "_seq_no", "seed": ${some_random_value} } }, { "linear": { "alloc_rate": { "origin": 100, "scale": 50, "offset": 0, "decay": 0.5 } } } ], "score_mode": "sum", "boost_mode": "replace", "min_score": 1 } } ``` `linear`의 입출력 값을 예시로 설명하면, `alloc_rate` 값이 0일 때 0을 score로, 100일 때 1을 score로 반환하며, 20일 때 0.2를 score로 반환합니다. [공식 문서](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-function-score-query.html#_supported_decay_functions)에도 그림으로 잘 설명되어있지만, `alloc_rate`과 `score`의 관계식으로 표현하면 아래와 같습니다. (지금은 i, ii만 보시면 됩니다) ``` ┌─ 0 i) alloc_rate < 0 or alloc_rate > 200 │ score = ┼─ alloc_rate / 100 ii) 0 <= alloc_rate <= 100 │ └─ 2 - alloc_rate / 100 iii) 100 < alloc_rate <= 200 ``` 때문에 방법2와 같이 `(alloc_rate / 100) + rand >= 1`를 평가합니다. #### 또 다른 사용 사례 0일 때 할당을 막고 100일 때 할당하는 `alloc_rate`과 반대로 100일 때 할당을 막고 0일 때 할당해야하는 경우에는 어떻게 쿼리 해야 할까요? 예산의 소진 상태가 그러한 값을 가질 수 있습니다. 0%라면 예산이 하나도 쓰이지 않은 상태이고, 100%면 모든 예산을 소진했다는 의미입니다. 당연히 0%일 때는 ES 검색 결과에 나와야 하지만 100%라면 더 이상 소진이 되지 않도록 결과에 포함되면 안 됩니다. 위의 할당 조건을 부등식으로 표현하면 아래와 같습니다. `1 - budget / 100 > rand` 부등식을 요리조리 수정하려 해도 Elasticsearch에서는 음수 score를 지원하지 않기 때문에 `field_value_factor`만으로는 해당 조건문을 구현할 수 없습니다. 이때 [공식 문서](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/query-dsl-function-score-query.html#_supported_decay_functions)의 linear 그래프를 보면, origin보다 값이 큰 경우 기울기가 음수임을 알 수 있습니다. 이것을 이용해서 origin을 0으로 설정하면, `budget`이 0일 때 1의 score를, 100일 때 0의 score를 얻을 수 있습니다. ```json { "function_score": { "functions": [ { "random_score": { "field": "_seq_no", "seed": ${some_random_value} } }, { "linear": { "budget": { "origin": 0, "scale": 50, "offset": 0, "decay": 0.5 } } } ], "score_mode": "sum", "boost_mode": "replace", "min_score": 1 } } ``` ### 성능 비교 실제 프로덕션 광고 할당에 쓰이는 쿼리 일부분을 방법3의 linear decay와 방법1의 script의 성능을 비교해보겠습니다. 비교하기에 앞서 Elasticsearch는 접근 패턴과 데이터의 형태에 따라서 성능이 달라질 수 있음을 먼저 밝힙니다. 저희의 경우 총 라이브 광고가 엄청 많지 않고, write 대비 read가 압도적으로 큰 상황입니다. > 아래의 결과는 [Kibana의 Query Profiler](https://www.elastic.co/guide/en/kibana/current/xpack-profiler.html)를 사용했습니다. #### 방법1의 script 전체 쿼리의 수행시간 중 33%가 script 쿼리에 사용됐으며, 가장 오래 걸리는 쿼리가 됐습니다. ![전체 쿼리의 수행시간 중 script가 차지하는 비율이 33%](https://tech.buzzvil.com/blog/probability-in-es-search/performance_script.png) ![좀 더 자세한 성능 분석](https://tech.buzzvil.com/blog/probability-in-es-search/performance_detail_script.png) #### 방법3의 linear decay script 대비 40배 넘게 최적화됐고, 소요 시간이 쿼리 중에서도 하위권에 속합니다. ![전체 쿼리의 수행시간 중 linear decay가 차지하는 비율이 1.3%](https://tech.buzzvil.com/blog/probability-in-es-search/performance_linear_decay.png) ![좀 더 자세한 성능 분석](https://tech.buzzvil.com/blog/probability-in-es-search/performance_detail_linear_decay.png) ### 장단점 `script` vs `function_score`로 봤을 때, `function_score`가 항상 성능이 좋습니다. 하지만 `function_score`를 거치면 각 Document score가 `[0, 1)`로 계산되는 단점이 있습니다. 저희의 경우 scoring을 사용하지 않는 filter context를 사용하고 있어서 이런 단점은 문제가 되지 않았습니다. ### 마치며 `function_score.query`까지 커스텀하면 서론에서 설명했던 좀 더 복잡한 확률 시나리오도 효율적으로 구현해낼 수 있습니다. 실제 광고 할당 서버에서는 이 쿼리를 거의 모든 확률 쿼리에 사용하고 있습니다. 성능 비교 문단에도 적었지만, 인덱싱 구성과 접근 패턴에 따라서 ES는 상당히 다른 성능 결과를 볼 수 있습니다. 심지어 어떤 경우는 script를 사용하는 게 일반 쿼리를 조합하는 것보다 더 빠른 경우도 있습니다. 사용 사례에 따라서 다양한 쿼리와 인덱싱 구성을 테스트해 보시고 적용하는 것을 추천해 드립니다. --- [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [asyncio 뽀개기 3 - SIGTERM (CTRL+C) 올바르게 처리하기](https://tech.buzzvil.com/blog/asyncio-no-3-sigterm) Date: 2022-05-30 | Author: Isac Yoo | Category: Backend asyncio를 사용하는 서버라면 graceful shutdown을 할 수 있어야합니다. Eventloop에 task를 등록하는 구조이기 때문에 graceful shutdown을 하지 않으면 유저 혹은 다른 서버의 요청이 버려지는 현상이 발생할 수 있습니다. 이전 포스트들에서 asyncio의 핵심 요소들의 동작 방식을 이해했다면, 이번 포스트에서는 signal을 이해하고 Eventloop에 signal handler를 추가함으로써 응용 방법을 살펴볼 예정입니다. - [asyncio 뽀개기 1 - Coroutine과 Eventloop]() - [asyncio 뽀개기 2 - Future의 활용]() - (본글) **asyncio 뽀개기 3 - SIGTERM (CTRL+C) 올바르게 처리하기** - (WIP) asyncio 뽀개기 4 - APScheduler에서 graceful shutdown하기 --- ### signal 이란? Unix에서 signal이란 프로세스에 특정한 의미를 담아 보내는 inter-process communication (IPC) 입니다. 의미에 따라서 signal의 종류가 나뉘어있습니다. POSIX 호환 OS라면 (Linux, MacOS도 포함) 터미널에서 `kill -l`를 실행해서 현재 OS가 지원하는 signal의 종류를 확인할 수 있습니다. signal은 프로세스가 실행 중이라면 **언제든** 발생할 수 있고, 발생 시 해당 프로세스는 실행을 멈추고 signal 처리를 하게 됩니다. 여기서 **언제든**이 중요한데, 서버가 이제 막 켜져서 사용할 DB의 health check 중간에도 발생할 수 있고, 사용자 요청을 처리하는 중에 발생할 수도 있고, 심지어 signal handling을 하는 중에도 발생할 수 있습니다. #### 로컬에서 signal 테스트해 보기 로컬에서 `kill` 커맨드로 확인해보실 수 있습니다. `kill -KILL `를 실행하면 process id가 ``인 프로세스에 SIGKILL을 보낼 수 있습니다. CLI에서 `KILL`대신 `kill -l`에 나오는 이름을 적으면 그 signal을 보낼 수 있습니다. 몇몇 signal은 `kill` 없이도 TTY로 보낼 수 있는데, 터미널에서 프로세스 종료할 때 쓰이는 `CTRL + C`가 그 예시이고 SIGINT를 의미합니다. ### 대표적인 signal [Linux의 signal manpage](https://man7.org/linux/man-pages/man7/signal.7.html)를 보면 아주 다양한 signal이 존재합니다. 하지만 서버 개발자가 신경 써야 할 signal 3개만 꼽자면 SIGTERM, SIGKILL과 SIGINT입니다. #### SIGTERM 프로세스에게 종료를 부탁하는 의미로 생각할 수 있습니다. 이 signal을 받은 프로세스는 처리를 마무리하고 정상 종료해야 하기 때문에 graceful shutdown에 적합합니다. `docker stop`과 `kubectl delete pod` 모두 즉시 SIGTERM을 보냅니다. #### SIGKILL 프로세스에게 시간을 주지 않고 즉시 종료할 때 사용합니다. `docker kill`을 실행하면 컨테이너에 즉시 SIGKILL을 보냅니다. `docker stop`과 `kubectl delete pod` 모두 처음에는 SIGTERM을 보내지만, 만약 CLI에서 설정한 시간 동안 프로세스가 종료되지 않으면 SIGKILL을 다시 보내서 즉시 종료합니다. #### SIGINT 위에서 설명한 것처럼 터미널에서 프로세스를 종료하고자 Ctrl + C를 입력할 때 사용됩니다. Docker나 Kubernetes에서 이 시그널이 활용되진 않지만, 로컬에서 개발할 때 자주 사용하기도 합니다. gunicorn에서 즉시 종료할 때 사용되기도 합니다. ### signal handler signal이 발생했을 때 실행할 코드를 의미합니다. OS에서 소유하는 default signal handler라는 게 있고, signal 종류마다 서로 다른 handler를 가지고 있습니다. 예를 들어 SIGKILL의 default signal handler는 프로세스를 종료시키는 동작을 합니다. 프로세스가 직접 자신만을 위한 signal handler를 정의할 수 있는데, 이것을 user defined signal handler라고 합니다. OS는 해당 프로세스에 user defined handler가 있으면 그것을 실행하고, 없다면 default handler를 실행합니다. 단 두 가지 예외가 있는데, SIGKILL과 SIGSTOP은 항상 default handler가 사용됩니다. #### 내 서버는 signal handler를 추가하지 않았는데? 웬만한 서버 프레임위크들은 자체적으로 signal handler를 가지고 있습니다. gunicorn을 사용한다면 기본적인 핸들링을 해주고, FastAPI, Django 등에서도 서버 종료 시점에 핸들러를 실행할 수 있도록 지원하고 있습니다. 이런 프레임워크들은 signal 자체를 추상화해서 프레임워크 기능으로 제공하지만, 웹 프레임워크를 쓰지 않는 서버의 경우 직접 signal을 신경 써야 합니다. ### Python에서의 signal Python에서도 asyncio 이전부터 signal 관련 기능을 [signal 모듈](https://docs.python.org/ko/3.10/library/signal.html)에서 지원하고 있었습니다. 또한 SIGINT (Ctrl + C)가 발생할 때 Python 코드 내부에서 `KeyboardInterrupt` 에러를 발생시킵니다. `KeyboardInterrupt`는 `BaseException`의 자식 클래스이기 때문에 `try-catch`로 잡아서 핸들링할 수 있습니다. asyncio에서는 [loop.add_signal_handler](https://docs.python.org/ko/3.10/library/asyncio-eventloop.html#asyncio.loop.add_signal_handler)가 추가됐고, asyncio를 사용하는 경우 `signal.signal()`대신 이 함수를 사용해야 합니다. [내부 코드](https://github.com/python/cpython/blob/9912b3d989b0cb442e9f9d55fbdd30d55591e2fc/Lib/asyncio/unix_events.py#L88-L131)를 보면, 결국 `signal.signal()`을 사용하지만 Eventloop가 깨어나는 것을 보장하는 장치도 포함되어있기 때문입니다. ### `KeyboardInterrupt`만 사용하면 안될까? > ⚠ `KeyboardInterrupt`은 터미널에서 프로그램 종료를 위해 사용하는 Ctrl + C(SIGINT)에만 반응합니다. > SIGTERM, SIGKILL과는 관련이 없지만, signal handler의 필요성과 signal의 특성을 설명하기 위해서 예시로 사용합니다. ```python import asyncio loop = asyncio.new_event_loop() try: loop.run_until_complete(some_coroutine_function()) except KeyboardInterrupt: print('Interrupted') some_terminating_task() finally: loop.close() print('Eventloop closed') ``` signal handler없이 `KeyboardInterrupt`를 이용해서 Ctrl + C 입력을 대비하는 프로그램입니다. 간단하고 명료하므로 문제가 없어 보이지만, 사실 signal을 처리하기엔 충분하지 않습니다. signal은 **언제든** 발생할 수 있다는 특징이 있으므로 except 내부인 `terminating_some_tasks()` 실행 중이나 finally 내부에서도 발생할 수 있습니다. 그렇다면 상술한 가능성을 차단하기 위해서 except나 finally 내부를 또다시 `try-except KeyboardInterrupt`로 감싸면 해결이 될까요? 불행하게도 그 except 구문 내부에서도 signal이 발생할 수 있습니다. 결국 `KeyboardInterrupt`로는 SIGINT를 안전하게 처리할 수 없습니다. ### asyncio에서 signal 위 예제의 SIGINT를 포함해서 production 환경에서 lifecycle에 사용되는 SIGTERM를 제대로 처리하려면 [loop.add_signal_handler](https://docs.python.org/ko/3.10/library/asyncio-eventloop.html#asyncio.loop.add_signal_handler)를 사용해야 합니다. ```python import asyncio import signal loop = asyncio.new_event_loop() def sig_handler(): print('Shutdown') some_terminating_task() for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, sig_handler) try: loop.run_until_complete(some_coroutine_function()) finally: loop.close() print('Eventloop closed') ``` 중요한 것은 `loop.add_signal_handler`를 최대한 빨리 실행하는 것입니다. signal은 언제든 발생할 수 있으므로 `loop.add_signal_handler` 이전에 시간이 소요되는 초기화 작업이 존재한다면, 초기화 도중에 발생하는 signal은 signal handler에서 처리할 수 없기 때문입니다. #### 활용 1: signal handler에서 Eventloop 조작하기 signal handler는 파라미터를 받을 수 있으므로 Eventloop을 넘겨서 조작할 수 있습니다. 위 예제의 일부분만 수정해보겠습니다. ```python def sig_handler(loop): print('Shutdown') loop.stop() some_terminating_task() for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, sig_handler, loop) ``` #### 활용 2: signal handler로 async 함수 등록하기 기본적으로 signal handler는 `async` 키워드가 없는 일반 함수여야 합니다. 하지만 만약 handler 내부에서 `await` 키워드를 쓰고 싶다면 `loop.create_task`를 사용할 수 있습니다. ```python async def sig_handler(loop): pass for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, lambda l: l.create_task(sig_handler(l)), loop) ``` 다만 꼭 알아둬야 할 점은, 일반 함수를 handler로 등록하면 signal이 발생했을 때 곧바로 handler 내용이 실행되지만, `loop.create_task`를 사용했을 경우 곧바로 실행되지 않을 수도 있다는 것입니다. [asyncio 뽀개기 1편](())에서 설명한 것처럼, `loop.create_task`는 파라미터로 주어진 코루틴을 바로 실행하는 게 아니라 Eventloop에 실행을 위한 등록만 합니다. 그 때문에 만약 Eventloop에 등록된 task가 너무 많거나 CPU intensive 한 task가 실행권을 양보하고 있지 않다면, signal 발생 시점부터 handler 실행까지 딜레이가 있을 수 있습니다. #### 활용 3: 현재 eventloop에서 실행중인 모든 task 취소하기 SIGTERM과 SIGINT는 종료를 위해 사용되기 때문에 현재 Eventloop에서 실행 중인 모든 task를 종료하고 싶은 때도 있습니다. ```python async def sig_handler(loop): tasks = [] for t in asyncio.all_tasks(): if t == asyncio.current_task(): continue tasks.append(t) t.cancel() await asyncio.gather(*tasks) loop.stop() ``` `t.cancel()` 호출 시 해당 task에 `asyncio.CancelledError`가 발생하므로 task는 그에 맞는 에러 핸들링을 해야 합니다. `asyncio.gather`에서는 task들이 `asyncio.CancelledError`를 다 처리하고 코루틴을 끝내길 기다립니다. #### 총정리: 실행 가능한 예제 위의 활용 방안들을 종합해서 실행할 수 있는 예제를 하나 소개합니다. 아래 스크립트는 1초에 한 번 `sleep`이라는 메시지를 출력하는 무한루프를 실행합니다. 여기에 SIGINT 혹은 SIGTERM의 handler를 추가하는데, 그 무한루프의 종료시키는 코드가 포함되어있습니다. 추가로 실행만으로 출력을 확인할 수 있게 `send_sigterm_after_5_seconds`를 추가했습니다. 이 함수는 실행 후 5초 이후에 스크립트 자신에게 SIGTERM을 보냅니다. 만약 SIGTERM의 handler가 잘 설정되어있다면 5초 이후에 `stopping loop...`를 볼 수 있습니다. ```python import asyncio import signal import os shutdown: bool = False def sig_handler() -> None: """ signal handler. `print_and_sleep`의 무한루프를 종료시킨다. """ print('stopping loop...') global shutdown shutdown = True async def print_and_sleep(loop) -> None: """ 1초에 한번 `sleep`이라는 문자를 출력하는 무한루프 """ while not shutdown: print('sleep') await asyncio.sleep(1) loop.stop() async def send_sigterm_after_5_seconds() -> None: """ 5초 이후에 스크립트 자신에게 SIGTERM을 보낸다. """ await asyncio.sleep(5) os.kill(os.getpid(), signal.SIGTERM) def main() -> None: loop = asyncio.new_event_loop() for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, sig_handler) loop.create_task(print_and_sleep(loop)) loop.create_task(send_sigterm_after_5_seconds()) try: loop.run_forever() print('loop stopped') finally: loop.close() print('loop closed') if __name__ == '__main__': main() # SIGINT 없이 스크립트 실행 후 5초 동안의 출력 # sleep # sleep # sleep # sleep # sleep # stopping loop... # loop stopped # loop closed ``` ### 컨테이너 사용시 주의사항 Dockerfile을 작성할 때 ENTRYPOINT를 잘못 작성하거나 script를 사용할 때 애플리케이션이 signal을 받지 못하는 문제가 발생할 수 있습니다. [https://hynek.me/articles/docker-signals/](https://hynek.me/articles/docker-signals/)에 자세한 원인과 가이드가 나와 있습니다. ### 마치며 이번 포스트에서는 signal에 대해서 알아보면서 asyncio에서 올바르게 signal을 처리하는 방법을 살펴봤습니다. 다음 포스트에서는 지금까지의 포스트 내용을 기반으로 실제 프로덕션 서버에서 graceful shutdown을 구현했던 사례를 소개하겠습니다. --- [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- #### 참고문서 - https://en.wikipedia.org/wiki/Signal_(IPC) - https://www.acritelli.com/blog/kubectl-delete-sigkill/ - https://www.roguelynn.com/words/asyncio-graceful-shutdowns/ --- ## [버즈빌 전문연구요원과의 인터뷰](https://tech.buzzvil.com/blog/postech-lab-zine-interview) Date: 2022-05-02 | Author: James Kang | Category: Culture Claud는 버즈빌 Ad.Display팀에서 Engineering Manager로 함께하고 있습니다. 전문연구요원으로서 3년간 복무를 마치고, 이후 시니어 개발자로서 다양하게 버즈빌에 기여해주시고 계신데요. 버즈빌 전문연구요원에 관심있어하시는 분들을 위해서 POSTECH LAB ZINE과 인터뷰를 진행했고, 도움이 될만한 내용을 버즈빌 기술 블로그에 함께 공유합니다. 관심있는 분들의 일독을 권합니다! ![photo 1](https://tech.buzzvil.com/blog/postech-lab-zine-interview/1.png) ### 안녕하세요? 현재 어떤 일을 하고 계신지 소개해 주실 수 있나요? 저는 현재 버즈빌에서 운영하는 다양한 제품의 백엔드 엔지니어링을 담당하고 있습니다. 광고 캠페인을 관리하고 할당하는 광고 서버 개발을 담당하여 진행했고, 최근에는 효율적인 광고 제공과 운영을 위하여 다양한 추천 모델을 제품에 녹여낼 수 있도록 시스템을 설계하고 개발하는 업무를 주로 진행하고 있습니다. ### 다양한 전문 연구기관 중에서, 버즈빌을 최종 선택하게 되신 이유는 무엇일까요? 저는 지인 소개를 통해 버즈빌에 대해 알게 되었는데요. 대용량 트래픽을 처리하는 광고 서버를 개발하고 운영하는 업무가 매력적으로 느껴졌습니다. 또한 자율적이고 다양한 기술에 개방적인 점과, 개발 문화가 잘 자리잡고 있는 부분에서 회사와 함께 빠르게 성장할 수 있을 것 같다고 생각하여 버즈빌에 합류하게 되었습니다. ### 버즈빌에 근무하시면서 가장 좋았던 점은 무엇인가요? 자율적으로 일할 수 있도록 장려하는 분위기가 가장 좋았던 것 같습니다. 분기별로 플래닝을 할 때 직접 의견을 내서 본인이 진행할 과제를 결정하고 있고, 근무 시간에도 바 테이블이나 포커스룸, 루프탑 등등 다양한 업무 공간이 마련되어 있어서 편하게 일할 수 있고, 업무 효율도 더 좋았습니다. ### 하루 일과를 간단히 요약해 주실 수 있나요? 출근한 후 자리에 앉으면 웜업으로 코드 리뷰를 진행합니다. 그리고 오전마다 진행하는 팀 스탠드업 미팅이나 커피 챗 시간에 체크인을 통해 각자의 컨디션을 체크하고 과제 진행 상황을 공유합니다. 이후 시간에는 의사결정을 위한 미팅에 참여하거나 개발 과제 진행에 집중할 수 있는 포커스 타임을 확보하며 일하고 있습니다. 점심 시간에는 식사 후 석촌 호수 산책을 하거나, 스터디 모임에 참여하기도 합니다. ![photo 2](https://tech.buzzvil.com/blog/postech-lab-zine-interview/2.png) ### 개발자로서의 성장을 위해, 버즈빌에선 어떤 활동을 할 수 있나요? 버즈빌의 핵심 가치 중 하나는 성장이며, 그에 맞게 스터디 그룹 등 성장을 위한 다양한 활동을 지원하고 있습니다. 몇 가지 사례를 들어보면, 저는 개인적으로 클라우드 환경에 관심이 많아서 같은 관심사를 가진 엔지니어들과 함께 관련 동향을 팔로우업하는 세션에 매주 참여하고 있습니다. 또한 저희 팀에서는 ML 엔지니어 분들이 많이 계셔서 모델을 잘 설계하고 평가하기 위하여 논문을 통해 파악한 내용을 공유하는 세션을 진행합니다. 또한, 효율적으로 ML 워크로드를 운영하기 위해 MLOps와 관련된 스터디도 지속적으로 진행되고 있습니다. ### 대학원 때 연구 주제와 현재 다루고 있는 주제는 비슷한가요? 대학원 때는 컴퓨터 네트워크 및 보안을 주로 공부하였는데, 현재는 주로 어플리케이션 개발 업무를 진행하다보니 직접적으로 연관이 있지는 않습니다. 하지만 클라우드에 웹 서비스를 배포하고 운영하는 과정에서 대학원 때 공부했던 기반 지식이 큰 도움이 된다고 생각하고 있습니다. ![photo 3](https://tech.buzzvil.com/blog/postech-lab-zine-interview/3.png) ### Claud의 10년 후의 모습은 어떨 것이라고 생각하나요? 10년 뒤에도 계속 개발을 하고 있을 것 같아요. 다양한 경험을 쌓고 동료에게 긍정적인 영향력을 줄 수 있는 엔지니어가 되어서 많은 사람들에게 즐거움을 줄 수 있는 서비스를 만들어가고 싶습니다. 개인적으로 읽었던 글에서 stay consistent 라는 문구가 기억에 많이 남습니다. 제가 전공한 컴퓨터 공학을 포함하여 과학 기술계에서 일하는 사람들은 끊임없이 새로운 것을 받아들여야 하는 입장인 것 같아요. 전문연구요원을 찾아보시는 많은 분들이 있을 텐데요. 작게라도 꾸준히 필요한 일을 하다보면 결국 좋은 결과가 있을거라 믿습니다. 마지막으로, 버즈빌과 함께 성장하고 싶은 분들은 아래 링크를 통해서 지원해 주세요! 감사합니다! [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [asyncio 뽀개기 2 - Future의 활용](https://tech.buzzvil.com/blog/asyncio-no-2-future) Date: 2022-04-04 | Author: Isac Yoo | Category: Backend Future를 잘 활용하면 단순히 `await` 하는 용도보다 더 다양한 흐름 제어를 할 수 있습니다. 이전 포스트에서는 asyncio의 핵심 컴포넌트인 코루틴과 Eventloop을 소개했습니다. 이번 포스트에서는 Future를 만드는 방법, Callback을 등록해서 활용하는 방법 등을 살펴볼 예정입니다. - [asyncio 뽀개기 1 - Coroutine과 Eventloop]() - (본글) **asyncio 뽀개기 2 - Future의 활용** - [asyncio 뽀개기 3 - SIGTERM (CTRL+C) 올바르게 처리하기]() - (WIP) asyncio 뽀개기 4 - APScheduler에서 graceful shutdown하기 --- ### Future란? Future는 Python에 국한된 개념이 아니라 비동기 프로그래밍에서 널리 사용되는 개념이며, 주로 비동기 연산의 결과를 저장하는 객체를 나타냅니다. Java나 Javascript에서 접해보셨다면 이미 익숙한 개념일 것 같습니다. 동기 함수라면 함수가 blocking 후 종료했을 때 결과를 반환하기 때문에 그 값을 사용하면 됩니다. 하지만 비동기 함수의 경우, 호출하는 곳에서 의도적으로 연산 종료를 기다리지 않는 이상 반환 값을 바로 받아볼 수 없습니다. 그래서 Future는 비동기 함수가 호출한 곳에 "지금은 반환 값이 없는데 나중에 이곳에 값을 채워줄게" 라는 목적으로 만들어서 주는 것입니다. #### 꼭 Future를 알아야할까? [공식 문서](https://docs.python.org/ko/3.10/library/asyncio-future.html#asyncio.Future)에서 설명한 것처럼, Future 객체를 외부에 노출하는 것은 권장되지 않기 때문에 고수준에서만 asyncio를 사용하면 Future를 만날 기회가 적습니다. 하지만 비동기 프로그래밍에서 중요한 개념이고, 저수준에서 asyncio를 다루려면 필수적으로 알아야 효율적인 코드를 작성할 수 있습니다. #### Python에서의 Future Python에는 사실 2개의 서로 다른 Future 클래스가 있습니다. 하나는 [concurrent.futures.Future](https://docs.python.org/ko/3.10/library/concurrent.futures.html#future-objects)이고, 다른 하나는 [asyncio.Future](https://docs.python.org/ko/3.10/library/asyncio-future.html#future-object)입니다. **이후에 등장하는 모든 Future는 후자임을 먼저 밝힙니다.** 두 클래스는 서로 다른 시기에 다른 목적을 위해 만들어졌기 때문에 서로 호환되지 않습니다. 대신 [asyncio.wrap_future()](https://docs.python.org/ko/3.10/library/asyncio-future.html#asyncio.wrap_future)를 이용하면 전자를 후자로 바꿀 수 있습니다. #### asyncio에서의 Future 위에서 설명한 것처럼 asyncio 모듈에서 Future가 직접 노출되는 부분은 적습니다. [loop.create_future()](https://docs.python.org/ko/3.10/library/asyncio-eventloop.html#asyncio.loop.create_future)와 [asyncio.create_task()](https://docs.python.org/ko/3.10/library/asyncio-task.html#asyncio.create_task)가 가장 흔한데, 후자는 Eventloop에 코루틴을 등록하는 역할을 하므로 실제로 조작하는 경우는 흔치 않습니다. #### Future의 기능 독자의 편의를 위해 간략하게 특징/기능을 나열하고 넘어가겠습니다. - Awaitable이기 때문에 `await` 키워드로 결과를 기다릴 수 있음 - `done()`으로 연산이 끝났는지 확인 가능 - `set_result()`로 연산의 결과를 지정 - `cancel()`로 연산을 취소 가능 - `add_done_callback()`으로 콜백함수 등록 가능 ### Future 활용 #### 활용 1. 콜백 [이전 포스트](())의 요구사항에 몇 가지 조건을 추가 후 구현하면서 Future를 활용해보겠습니다. ##### 요구사항 기존 - 10초마다 Job이 생성되는데, 각 Job은 랜덤한 시간을 소요함 - 이전 Job의 수행시간이 10초를 넘든 안 넘든 상관없이 10초마다 새로 생성되어야 함 - Job들은 concurrent 하게 실행됨 새로 추가되는 항목 - 각 Job이 끝났을 때 총 수행 시간을 출력함 - Job이 성공/실패(예외 발생) 여부와 상관없이 출력돼야 함 ##### 구현 1. Job 수정 가장 간단한 방법은 Job이 직접 출력을 하도록 수정하는 것입니다. ```python3 import asyncio import time from random import randint async def run_job() -> None: before = time.time() try: delay = randint(5, 15) await asyncio.sleep(delay) # 5~15초 동안 잠자기 finally: after = time.time() print(f'Job duration: {after - before} sec') async def main() -> None: while True: asyncio.create_task(run_job()) await asyncio.sleep(10) asyncio.run(main()) ``` 위와 같은 간단한 예제에는 이렇게 구현해도 큰 무리가 없지만, 애플리케이션에 사용하기엔 문제가 있을 수 있습니다. 대부분은 이렇게 구현하면 SOLID의 단일 책임 원칙을 어기는 코드를 만들게 됩니다. 만약 이 예제 코드가 주기적으로 Job을 스케줄링하는 라이브러리라고 생각하면, 모니터링 기능을 위해서 사용자가 Job을 수정해야 하는 상황이 발생하는 것이죠. ##### 구현 2. 콜백 ```python3 import asyncio import functools import time from random import randint async def run_job() -> None: delay = randint(5, 15) await asyncio.sleep(delay) # 5~15초 동안 잠자기 def print_wall_time(before: float, _: asyncio.Future[None]) -> None: after = time.time() print(f'Job duration: {after - before} sec') async def main() -> None: while True: before = time.time() future = asyncio.create_task(run_job()) future.add_done_callback(functools.partial(print_wall_time, before)) await asyncio.sleep(10) asyncio.run(main()) ``` Future에서 제공하는 콜백기능을 이용해서 다시 구현했습니다. `run_job()`은 이전 포스트의 구현 그대로 변한 것이 없고, `main()`에 모니터링을 위해 콜백을 등록하는 코드가 추가됐습니다. 또한, 모니터링을 위한 로직이 하나의 함수 `print_wall_time()`으로 분리되었습니다. [Future.add_done_callback()](https://docs.python.org/ko/3.10/library/asyncio-future.html#asyncio.Future.add_done_callback)은 Future가 정상 종료, 예외 발생, `cancel()`을 통한 취소 등 종료만 된다면 항상 실행되기 때문에 `try-finally` 같은 코드가 없이도 목적을 달성할 수 있습니다. Future는 콜백함수 파라미터로 객체 자신을 넘겨주기 때문에 원한다면 아래와 같이 Job의 결과까지 모니터링할 수 있습니다. ```python import asyncio import functools import time from random import randint async def run_job() -> int: delay = randint(5, 15) await asyncio.sleep(delay) # 5~15초 동안 잠자기 return randint(0, 1000) # 임의의 연산 결과 def print_wall_time(before: float, future: asyncio.Future[int]) -> None: after = time.time() if future.exception() is not None: print(f'Error occurred: {future.exception()}, duration: {after - before} sec') else: print(f'Result: {future.result()}, duration: {after - before} sec') async def main() -> None: while True: before = time.time() future = asyncio.create_task(run_job()) future.add_done_callback(functools.partial(print_wall_time, before)) await asyncio.sleep(10) asyncio.run(main()) ``` ##### 유의사항 **그렇다면 Future에 추가된 콜백함수는 언제 어떤 순서로 호출될까요?** [공식 문서](https://docs.python.org/ko/3.10/library/asyncio-future.html#asyncio.Future.add_done_callback)를 봐도 그러한 명세는 찾을 수 없습니다. 그리고 Future는 Eventloop에서 자신에게 최적화된 구현체를 가질 수 있기 때문에 어떤 규칙을 보장받을 순 없을 것 같습니다. 다만 [기본 구현체](https://github.com/python/cpython/blob/a5c90784be0bf70287bac4195573e85e956c2332/Lib/asyncio/futures.py#L159-L171)는 콜백 등록에 [loop.call_soon()](https://docs.python.org/ko/3.10/library/asyncio-eventloop.html#asyncio.loop.call_soon) 메소드를 사용하고 있는데, Future 실행이 끝난 이후에 다음 Eventloop iteration에 콜백 함수 실행을 등록하며, 콜백함수가 등록된 순서대로 실행을 시작할 것 같습니다. 결론적으로 명세상 보장받을 수 없고, 순서나 시점을 보장받아야 하는 경우 별도의 장치가 필요합니다. **async 함수(코루틴 함수)를 콜백으로 등록하고 싶으면 어떻게 해야 할까요?** `Future.add_done_callback()`은 코루틴 함수가 아닌 일반 함수를 기대하기 때문에 코루틴 함수를 실행할 수는 없습니다. 대신 코루틴 실행을 Eventloop에 등록할 수는 있는데, `Future.add_done_callback(lambda fut: asyncio.create_task(some_async_func(fut)))` 같이 사용하시면 됩니다. `asyncio.create_task()`를 사용했기 때문에 당연하게도 코루틴이 종료되는 것을 `await`으로 기다릴 수 없습니다. 그 이유에 대해서 궁금하시면 [이전 포스트](())를 참고해주세요. #### 활용 2. Future 발행 asyncio 모듈의 많은 유틸들이 (Queue, gather, as_completed 등) 내부적으로는 Future 기반으로 작성되어있습니다. 그중에서도 [Semaphore](https://ko.wikipedia.org/wiki/%EC%84%B8%EB%A7%88%ED%8F%AC%EC%96%B4)를 간단하게 구현해보면서 Future의 또 다른 활용법을 살펴보겠습니다. 아래 인터페이스를 먼저 제시해 놓았기 때문에 먼저 직접 구현해보신 이후에 코드를 확인해보시길 추천해 드립니다. ##### 인터페이스 & 테스트 코드 ```python import asyncio from abc import ABCMeta from datetime import datetime # 인터페이스 class Semaphore(metaclass=ABCMeta): _value: int def __init__(self, initial_value: int = 1) -> None: self._value = initial_value async def acquire(self) -> None: raise NotImplementedError def release(self) -> None: raise NotImplementedError # 테스트 코드 async def run_job(sem: Semaphore, job_id: int) -> None: await sem.acquire() print(f'{datetime.now()} - start job {job_id}') await asyncio.sleep(1) print(f'{datetime.now()} - job {job_id} finished') sem.release() async def main() -> None: sem = SomeSemaphore(2) # 구현체로 변경 필요 await asyncio.gather( # 한번에 2개의 Job 묶음이 1초 간격으로 실행되어야함 run_job(sem, 1), run_job(sem, 2), run_job(sem, 3), run_job(sem, 4), run_job(sem, 5), ) asyncio.run(main()) ``` ##### 구현 1. busy waiting busy waiting 방식이 가장 쉽게 떠올릴 수 있는 구현일 것 같습니다. ```python class BusyWaitingSemaphore(Semaphore): async def acquire(self) -> None: while self._value <= 0: await asyncio.sleep(0.1) self._value -= 1 def release(self) -> None: self._value += 1 ``` 당연하게도 글 제목인 Future가 코드에 없으므로 문제가 있는 코드라는 것을 알 수 있습니다. 이 코드는 비효율적인데, sleep을 사용해 주기적으로 release가 있었는지 확인하는 polling 기반이기 때문입니다. 운이 좋지 않으면 release가 호출됐더라도 0.1초 이후에야 acquire가 반환되는 상황이 발생할 수 있는 것이죠. 그러한 딜레이를 해결하기 위해서 sleep delay를 0.01로 바꾸면 polling 주기가 잦아지기 때문에 Eventloop에 오버헤드가 생기게 됩니다. ##### 구현 2. Future 발행 Future를 사용하면 busy waiting 방식의 polling을 걷어내고 비동기적으로 훨씬 더 효율적인 Semaphore를 만들 수 있습니다. 지금까지는 asyncio 모듈이나 관련 라이브러리에서 반환 값으로 주는 Future 객체에 `await`을 붙이거나 콜백을 붙였다면, 이번에는 직접 Future 객체를 생성하고 관리하는 것입니다. ```python import asyncio from collections import deque class FutureSemaphore(Semaphore): _waiters: deque[asyncio.Future[None]] def __init__(self, initial_value: int = 1) -> None: super().__init__(initial_value) self._waiters = deque() async def acquire(self) -> None: if self._value <= 0: loop = asyncio.get_running_loop() future = loop.create_future() self._waiters.append(future) await future self._value -= 1 def release(self) -> None: self._value += 1 if len(self._waiters) > 0: fut = self._waiters.popleft() fut.set_result(None) ``` `BusyWaitingSemaphore.acquire()`에서 while loop이 Future를 사용하도록 변경됐습니다. 직접 비어있는 Future 객체를 만들고 `await` 키워드로 Future가 끝나길 기다리는 것이죠. 그렇다면 언제 해당 Future가 `await`에서 깨어나면 될까요? 누군가 `release()`를 호출해서 Semaphore에 공석이 생겼을 때 기다리고 있는 Future를 깨워주면 되는 것이죠. 그것을 위해서 `acquire()`에는 생성한 Future 객체를 `_waiters` 변수에 추가해서 기다리고 있다는 사실을 알리고, `release()`에서는 함수 끝에 기다리고 있는 Future가 있는지 찾아서 `set_result()`로 Future를 끝내는 것입니다. 실제로 [asyncio.Semaphore](https://docs.python.org/ko/3.10/library/asyncio-sync.html#asyncio.Semaphore) 또한 [유사한 방식](https://github.com/python/cpython/blob/a5c90784be0bf70287bac4195573e85e956c2332/Lib/asyncio/locks.py#L333-L405)으로 구현되어있습니다. (물론 위의 예제 코드로는 해결하지 못하는 복잡한 상황을 처리하기 위해 조금 더 복잡하긴 합니다.) ##### 유의사항 **Future 객체 생성 방식** [공식 문서](https://docs.python.org/ko/3.10/library/asyncio-future.html#asyncio.Future)에 나와 있는 것처럼 `asyncio.Future()`와 같이 직접 객체를 생성하기보다, `loop.create_future()`로 생성해야 합니다. Eventloop이 자신에게 더 최적화된 구현체를 제공할 수도 있기 때문입니다. **예제 코드의 thread-safety** 위의 예제 코드는 모두 thread-safe 하지 않지만, 싱글 스레드에서는 항상 안전합니다. 이것은 `asyncio.Semaphore`도 마찬가지인데, Eventloop은 하나의 스레드에서 실행되기 때문입니다. `release()`의 `_value`와 `_waiters` 수정이 atomic 하지 않아도 싱글 스레드에서 안전한 이유는, [이전 포스트](())의 Cooperative multitasking 부분을 참고해주세요. ### 마치며 이번 포스트에서는 Future를 단순히 `await` 하는 것 보다 조금 더 복잡한 용례를 살펴봤습니다. 다음 포스트에서는 Eventloop의 signal handling 방법을 살펴보면서 SIGINT, SIGTERM 같은 종료 시그널을 올바르게 처리하는 방법을 설명하겠습니다. --- [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [asyncio 뽀개기 1 - Coroutine과 Eventloop](https://tech.buzzvil.com/blog/asyncio-no-1-coroutine-and-eventloop) Date: 2022-03-30 | Author: Isac Yoo | Category: Backend 이 시리즈의 목적은 asyncio의 컴포넌트들과 활용법을 소개하는 것입니다. 최종적으로는 실제 production에 쓰이고 있는 graceful shutdown을 구현하는 것을 목표로 하며, 그 과정에서 필요한 asyncio 지식을 여러 포스트에 걸쳐 설명할 예정입니다. Python에 국한된 내용도 있지만 코루틴, Future 등은 Asynchronous Programming에서 공통으로 사용되는 개념이기 때문에 Python을 사용해서 해당 개념을 익힌다고 생각할 수도 있습니다. - (본글) **asyncio 뽀개기 1 - Coroutine과 Eventloop** - [asyncio 뽀개기 2 - Future의 활용]() - [asyncio 뽀개기 3 - SIGTERM (CTRL+C) 올바르게 처리하기]() - (WIP) asyncio 뽀개기 4 - APScheduler에서 graceful shutdown하기 --- ### 서론 Eventloop은 Python asyncio에서 핵심 컴포넌트라고 할 수 있고, 코루틴은 asyncio에서 기본 실행 단위라고 할 수 있습니다. 이번 포스트에서는 코루틴이 어떤 것인지, 어떻게 하면 Eventloop에서 코루틴을 실행하는지를 가상의 요구사항을 정의하고 코드로 구현하면서 설명하겠습니다. 독자는 Python 공식 문서에서 async, await가 사용되는 기본 예제들을 이해할 수 있다고 전제하고 있습니다. ### 요구사항 - 10초마다 Job이 생성되는데, 각 Job은 랜덤한 시간을 소요함 - 이전 Job의 수행시간이 10초를 넘든 안 넘든 상관없이 10초마다 새로 생성되어야 함 - Job들은 concurrent 하게 실행됨 요구사항 중에서도 `10초를 넘든 안 넘든 상관없이 10초마다 새로 생성되어야 함`이 핵심이라고 할 수 있습니다. 요구사항이 적기 때문에 아래 코드를 보기 전에 직접 한번 작성해보시고 어떤 어려움이 있는지 고민해보시길 추천해 드립니다. ### 구현 위의 가정을 어떻게 하면 외부 라이브러리 없이 asyncio 모듈만 사용하여 구현할 수 있을까요? 전체 코드 이후에 하나씩 설명하겠습니다. ```python3 import asyncio from datetime import datetime from random import randint async def run_job() -> None: delay = randint(5, 15) print(f'{datetime.now()} sleep for {delay} seconds') await asyncio.sleep(delay) # 5~15초 동안 잠자기 print(f'{datetime.now()} finished ({delay} sec)') async def main() -> None: while True: asyncio.create_task(run_job()) await asyncio.sleep(10) asyncio.run(main()) # 2022-03-27 00:55:59.585462 sleep for 13 seconds # 2022-03-27 00:56:09.590352 sleep for 14 seconds # 2022-03-27 00:56:12.588084 finished (13 sec) # 2022-03-27 00:56:19.598264 sleep for 13 seconds # 2022-03-27 00:56:23.593074 finished (14 sec) # 2022-03-27 00:56:29.605253 sleep for 6 seconds # 2022-03-27 00:56:32.602354 finished (13 sec) # 2022-03-27 00:56:35.608464 finished (6 sec) ``` 출력 예시를 보면 13초 sleep이 끝나기 전에 14초 sleep이 먼저 실행된 것을 볼 수 있습니다. while loop에서 첫 번째 `run_job()`이 13초 동안 수행되는 것을 기다리지 않고 다음 iteration을 수행했기 때문에 이런 결과가 나왔다고 볼 수 있고, 이것의 핵심은 `asyncio.create_task`입니다. ##### `asyncio.create_task`는 왜 쓰는 걸까? `await run_job()`을 대신 쓰면 안될까? 간단한 `await run_job()`이 안되는 이유부터 설명하면, `await`을 붙이는 순간 `run_job`이 return 할 때까지 `main()`이 실행권을 갖지 못하기 때문입니다. 쉽게 말해서 `run_job()`이 8초 걸리는 job이었다면 8초가 지나서야 `await asyncio.sleep(10)`을 실행하게 되는 것이죠. 이 때문에 해당 iteration은 총 18초가 걸리며, "10초마다 Job을 생성함" 이라는 규칙을 어기게 됩니다. 각 Job의 수행 시간과 상관없이 10초마다 계속 생성하려면 `await`과 Eventloop에 대해서 좀 더 자세히 알 필요가 있습니다. `await`은 사실 내부적으로 두 가지 동작을 한다고 볼 수 있습니다. 첫 번째로 `await` 뒤에 있는 코루틴을 (좀 더 정확히는 [Awaitable 객체](https://docs.python.org/ko/3.10/library/collections.abc.html#collections.abc.Awaitable)) Eventloop에 실행해달라고 등록하고, 두 번째로 등록한 코루틴이 끝날 때 돌려받길 기대하며 실행권을 Eventloop에게 반환합니다. Eventloop은 등록한 코루틴이 종료되거나 에러가 발생한 이후에 실행권을 돌려줍니다. Eventloop은 이런 식으로 여러 코루틴 사이에 실행권을 주고받으며 [Cooperative multitasking](https://en.wikipedia.org/wiki/Cooperative_multitasking)을 달성하는 것이죠. 저희의 경우 전자의 동작은 필요하지만, 후자의 동작은 필요가 없습니다. 이때 사용할 수 있는 게 바로 `asyncio.create_task()`입니다. 해당 함수의 역할은, 파라미터로 들어오는 코루틴을 Eventloop에 등록하고 코루틴이 끝났을 때 결과를 받아볼 수 있는 Future 객체를 반환합니다. 반환되는 Future 또한 Awaitable 객체이기 때문에 `await`을 앞에 붙이면 Eventloop에 실행권을 넘기면서 코루틴의 종료까지 기다릴 수 있지만, 지금은 필요하지 않기 때문에 의도적으로 `await` 없이 넘어갔습니다. 그 때문에 실행권을 뺏기지 않은 채로 다음 while iteration을 위해 10초간 기다릴 수 있습니다. > 사실 `asyncio.create_task()`가 반환하는 것은 [Task 객체](https://docs.python.org/ko/3.10/library/asyncio-task.html#task-object)이고, > 이 객체는 [Future](https://docs.python.org/ko/3.10/library/asyncio-future.html#asyncio.Future)를 상속받기 때문에 동일하게 Awaitable합니다. ##### `await` 없이 `run_job()`만 쓰면 안될까? `await`이 기다리는 동작을 내포하기 때문에 `asyncio.create_task(run_job())` 대신 `run_job()`으로 변경해도 괜찮을까요? 실제로 테스트해보면 `RuntimeWarning: coroutine 'run_job' was never awaited`이라는 warning과 함께 아무것도 출력되지 않는 것을 확인할 수 있습니다. 이유는 간단한데, `run_job`은 일반 함수가 아니라 "코루틴 함수" 이며, `run_job()`은 "코루틴 객체" 를 생성해서 반환할 뿐 실제로 코루틴을 실행하지 않기 때문입니다. 특별한 동작 같아 보일 수 있지만, 사실 이미 Python에는 비슷한 동작이 아주 흔하게 쓰이고 있습니다. 당장 쉘을 열어서 `(print(n) for n in [1, 2, 3])`을 실행하면 ` at ...>` 같은 출력만 보일 뿐, 아무 숫자도 출력되지 않습니다. `list(print(n) for n in [1, 2, 3])` 혹은 `next(print(n) for n in [1, 2, 3])` 같은 방법으로 순회해야 비로소 출력된 숫자를 볼 수 있습니다. 다시 예제로 돌아오면, `run_job()`은 코루틴 객체를 반환하는데, 이 객체는 generator 객체와 동일하게 지금 실행하는 게 아니라 추후에 실행 가능한 객체입니다. 또한, 앞서 설명한 것처럼 코루틴을 실행하기 위해서는 Eventloop에 등록해야 하므로 `run_job()`으로 코루틴 객체를 생성한다고 해서 코루틴을 실행하는 게 아닙니다. 즉 코루틴을 실행하려면 `asyncio.create_task`나 `await` 등을 통해 Eventloop에 등록하는 것이 필수입니다. > 참고로 숫자 출력 예시 같은 표현식을 [generator expression](https://docs.python.org/ko/3.10/glossary.html#term-generator-expression)이라고 하며, > 해당 표현식이 반환한 객체를 [generator iterator](https://docs.python.org/ko/3.10/glossary.html#term-generator-iterator) 라고 합니다. > 또한, generator iterator를 반환하는 함수를 [generator](https://docs.python.org/ko/3.10/glossary.html#term-generator)이라고 부르며, > 함수 내부에서 `yield` 키워드를 사용하면 자동으로 generator를 정의하는 것이 됩니다. ##### 용어 정리 - 코루틴과 Future Awaitable, 코루틴, Future, Task 여러 키워드가 등장해서 한번 정리하고 넘어가는 게 좋을 것 같습니다. 우선 계층구조를 확인해보면 아래와 같습니다. ``` ┌──Coroutine │ Awaitable◄───┤ │ └──Future◄────────Task ``` `await` 키워드를 붙일 수 있는 최소 조건이 Awaitable입니다. 그래서 3가지 모두 `await`으로 실행이 끝나길 기다릴 수 있습니다. 하지만 코루틴은 Eventloop에 등록되지 않으면 실행되지 않기 때문에, `await`을 붙이거나 Future나 Task로 감싸야 합니다. ### 동시 실행 위의 코드는 요구사항 중 하나인 동시 실행을 만족합니다. 그렇다면 어디서 어떻게 동시 실행되고 있는 걸까요? 어디는 당연하게도 Eventloop에서 돌아가겠죠? 문제는 Eventloop이 어떻게 동시 실행을 구현하느냐입니다. 이것을 이해하려면 Eventloop이 Cooperative multitasking을 하는 방식을 이해해야 합니다. asyncio에 공리가 하나 있는데, **"스레드당 실행 중인 Eventloop은 하나"** 라는 제약조건입니다. 즉, 아무리 많은 코루틴을 하나의 Eventloop에서 동시 실행해도 결국 single thread로 동작한다는 의미입니다. (사실 예외가 있는데 그건 아래에서 설명하겠습니다.) Eventloop의 구현체마다 다를 수 있지만, cpython의 경우 내부 queue에 등록된 코루틴들을 기억하면서 한 번에 하나씩 번갈아가며 실행하고 있습니다. 어떻게 번갈아가면서 실행하길래 동시에 실행 중이라는 착각을 만들 수 있을까요? 답은 `await` 키워드와 코루틴이라는 단어에 있습니다. #### 코루틴 vs 일반 함수 우선 코루틴과 일반 함수(루틴 혹은 서브루틴)의 차이점을 실행권의 흐름을 기준으로 보겠습니다. ``` Caller Function Caller Coroutine ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ▼ │ │ │ │ │ │ │ │ │ │ │ │ │ │ call └──────┼────┼───────┐ │ │ call └──────┼─────┼──────┐ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ┌──────┼─────┼──────┘ │ │ │ │ │ │ │ │ │ │ suspend│ │ │ │ │ │ │ ▼ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └──────┼─────┼──────┐ │ │ │ │ │ │ │ resume │ │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ┌──────┼─────┼──────┘ │ │ │ │ │ │ │ │ │ │ suspend│ │ │ │ │ │ │ ▼ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └──────┼─────┼──────┐ │ │ │ │ │ │ │ resume │ │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ │ │ │ │ │ │ │ │ ┌──────┼────┼───────┘ │ │ ┌──────┼─────┼──────┘ │ │ │ │ │ return │ │ │ │ │ return │ │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ▼ │ │ │ │ │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ``` 일반 함수는 호출될 때 stack에 올라간 후 실행권을 가지고 body를 수행합니다. 수행이 끝나면 실행권을 호출한 곳으로 돌려주고 stack에서 사라집니다. 하지만 코루틴은 언제든 실행권을 반환할 수 있습니다. 즉 일반 함수는 시작점과 반환점이 항상 처음과 끝으로 고정된 코루틴의 일종이라고 볼 수 있습니다. #### Eventloop이 코루틴을 실행하는 방식 ``` Eventloop │ Coroutine 1 │ │ ┌─────────────┐ │ │ │ │ │ │ │ │await │ │ Coroutine 2 └─────────────┘ │ ┌─────────────┐ : │ │ │ : │ │ │ : │ │await │ ┌─────────────┐ │ └─────────────┘ │ │ │ : │ │ │ : │await │ │ : └─────────────┘ │ : : │ ┌─────────────┐ : │ │ │ : │ │ │ : │ │return │ ┌─────────────┐ │ └─────────────┘ │ │ │ │ │ │ │return │ │ └─────────────┘ │ │ ▼ ```

Eventloop이 2개의 코루틴을 번갈아가며 실행

내부에 `await`을 각각 2번, 1번씩 사용하는 코루틴 2개가 Eventloop에 등록되어있을 때 어떤 방식으로 번갈아서 실행하는지를 예로 들었습니다. 앞서서 `await` 키워드가 실행될 때 실행권을 Eventloop에 넘긴다고 했는데, 이때 해당 코루틴이 Suspend 되면서 다른 코루틴을 마저 실행합니다. OS에서 사용되는 프로세스 스케줄러와 비슷해 보이는데, 가장 큰 다른 점은 선점 여부입니다. 대부분의 프로세스 스케줄러는 선점형이기 때문에 프로세스의 실행권을 뺏을 수 있지만, Eventloop은 비선점형이기 때문에 실행 중인 코루틴이 `await`을 사용하거나 `return` 해서 실행권을 Eventloop에 돌려주지 않는다면 실행권을 뺏어서 다른 코루틴을 실행할 방법이 없습니다. 바로 이 이유 때문에 [requests](https://pypi.org/project/requests/) 같은 blocking 코드를 코루틴 내부에서 실행하지 못하도록 가이드 하는 것입니다. ###### 구현의 예제 출력을 도식으로 재구성 ![Score](https://tech.buzzvil.com/blog/asyncio-no-1-coroutine-and-eventloop/flow_of_example.png) `main()` 코루틴은 `asyncio.sleep` 이외에는 `await` 하는 곳이 없으므로 10초마다 실행됩니다. `run_job()` 코루틴들은 `main()`코루틴에서 `asyncio.create_task`로 Eventloop에 등록했기 때문에, `main()` 코루틴이 멈춘 직후에 실행을 시작합니다. 또한, 내부에서 `await`으로 실행권을 반납하며 sleep 하기 때문에 `delay`가 아무리 크더라도 다른 코루틴의 실행이 미뤄지지 않습니다. #### 또 다른 동시 실행 방법 [Python 공식 문서](https://docs.python.org/ko/3.10/library/asyncio-task.html#running-tasks-concurrently)를 보면 `gather`와 `wait` 같은 함수들을 사용할 수 있습니다. 하지만 이 함수들은 기다릴 코루틴(혹은 Future)을 파라미터로 넣어줘야 하기 때문에 개수를 사전에 알고 있어야 한다는 제약이 있습니다. MySQL을 읽으면서 동시에 Redis를 읽을 때 같은 용례가 있을 것 같습니다. 각각을 코루틴으로 만들고, `gather`나 `wait`으로 모든 코루틴 종료를 기다리는 것이죠. 또 [as_completed()](https://docs.python.org/ko/3.10/library/asyncio-task.html#asyncio.as_completed)를 사용하면 여러 코루틴을 동시 실행하면서 끝나는 순서대로 결과를 받아볼 수도 있습니다. 이번 포스트의 경우, 무한루프는 프로그램 수행시간을 알 수 없으므로 세 가지 함수 모두 사용할 수 없습니다. #### Eventloop와 multi thread 사실 항상 모든 코루틴이 하나의 Eventloop에서 실행되진 않습니다. "스레드당 실행 중인 Eventloop은 하나" 라는 제약이 있다는 말은, Eventloop을 하나 더 만들고 싶으면 스레드를 하나 더 만들면 된다는 의미이기도 합니다. 그래서 [run_coroutine_threadsafe](https://docs.python.org/ko/3.10/library/asyncio-task.html#asyncio.run_coroutine_threadsafe)를 사용하면 코루틴을 어떤 스레드에서 실행할지 선택할 수도 있고, [run_in_executor](https://docs.python.org/ko/3.10/library/asyncio-eventloop.html#asyncio.loop.run_in_executor)를 사용하면 Eventloop을 하나 더 만들지 않더라도 다른 스레드에서 함수를 실행할 수 있습니다. 다른 스레드가 개입되고, 공유변수를 스레드에 걸쳐 사용한다면 접근제한을 고민해야 합니다. [asyncio 모듈에서 제공하는 Synchronization primitive](https://docs.python.org/ko/3.10/library/asyncio-sync.html) 들은 thread-safe 하지 않기 때문입니다. ### 마치며 이번 포스트에서는 asyncio에서 가장 기본이 되는 Eventloop과 코루틴에대해서 알아봤습니다. 다음 포스트에서는 Future에 대한 조금 더 자세한 설명과 활용법을 설명하겠습니다. --- [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [DynamoDB를 사용하는 Go 서비스의 응답 시간 최적화 #2 TLS Handshake](https://tech.buzzvil.com/blog/dynamo-go-latency-optimization-2-tls-handshake) Date: 2022-03-29 | Author: Raf Kim | Category: Backend 안녕하세요. 버즈빌의 데이터 엔지니어 Raf입니다. 이전 포스팅([DynamoDB를 사용하는 Go 서비스의 응답 시간 최적화 #1 AWS Credential Token](https://tech.buzzvil.com/blog/dynamo-go-latency-optimization-1-aws-credential-token/))에 이어, Go 서비스에서 DynamoDB를 사용하면서 응답 시간 최적화를 시도한 경험을 공유드리도록 하겠습니다. 이번 편은 해결책을 만들지 못했지만, DynamoDB와 Go HTTP Client를 사용하면서 배우게 된 것에 대해 공유하고자 합니다. ### DynamoDB의 응답 시간이 길게 잡히는 이슈 리워드 서비스를 런칭하고 난 뒤 몇 개월이 지나면서 트래픽은 점점 늘어났습니다. 어느 순간부터 극히 일부의 gRPC 요청이 높은 응답시간을 가지는 Datadog 알람을 몇 번씩 보게 되었습니다. DynamoDB의 쿼리 응답시간 대시보드를 보았더니 최댓값이 100ms를 넘기는 경우를 보게 되었습니다.

text 그림 1) DynamoDB 쿼리 응답시간 대시보드

[DynamoDB Metrics and Dimensions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/metrics-dimensions.html)에 따르면, 위의 대시보드는 네트워크, 클라이언트의 시간이 아닌 서버에서의 요청 처리 시간만 포함된다고 합니다. 로드테스트를 수행하던 때에는 DynamoDB의 응답시간은 짧다고 가정하였고 DynamoDB에서도 응답 시간이 짧게 나오는 상황은 생기지 않았지만, 이번에는 DynamoDB의 응답 시간이 길어졌으므로 이전과는 완전히 다른 문제로 판단하였습니다. 검색을 해보니, [Tuning AWS Java SDK HTTP request settings for latency-aware Amazon DynamoDB applications](https://aws.amazon.com/ko/blogs/database/tuning-aws-java-sdk-http-request-settings-for-latency-aware-amazon-dynamodb-applications/) 문서에서 DynamoDB 요청에 타임아웃을 걸어두고 타임아웃이 지나면 재시도를 하는 Java AWS SDK의 설정에 대해 소개가 되어 있었습니다. 하지만 Go AWS SDK에서는 지원하지 않고 있으므로, 아래처럼 타임아웃이 지나면 재시도를 하는 로직을 구현하여 사용해 보기로 하였습니다. 30ms의 타임아웃으로 최대 3회까지 요청을 시도하며, 재시도를 해도 똑같은 에러가 나올 수밖에 없는 에러는 즉시 리턴해주고 타임아웃을 포함한 재시도 가능한 에러는 재시도를 수행합니다. 이 코드를 배포하였을 때 기대하는 것은 Datadog을 통해 gRPC 요청이 100ms 이내로 완료되는 것입니다. ### TLS Handshake가 긴 응답시간을 만들게 되었다. 이 코드를 배포한 뒤 응답 시간이 긴 gRPC 요청에 대해서 모니터링을 하였습니다. 하지만 일부 gRPC 요청에서 DynamoDB로 가는 요청에 타임아웃이 발생하면 다음 요청도 타임아웃이 발생하는 문제가 보였습니다. 제가 기대했던 것은 간헐적인 네트워크 등의 이슈로 인해 응답시간이 길어질 수 있지만, 재시도를 하게 되면 낮은 응답시간을 가질 수 있을 것이라 생각했습니다. 하지만 이런 상황은 간헐적이라기보다는 인과가 있는 듯한 모습을 보였습니다. 따라서 `httptrace.ClientTrace`를 통해 몇 가지 trace를 심어보기로 하였습니다. `httptrace.ClientTrace`에 대해선 첫 번째 포스팅([HTTP connection pool in Go explained](http://localhost:1313/blog/http-connection-pool-in-go-explained/))을 참조 부탁드립니다.

그림 2) Datadog APM

Trace를 심은 후 확인해 본 결과, 대체적으로 첫 시도에 타임아웃이 발생하면 두 번째 시도에서 TLS Handshake를 하고 있었습니다. 이 중 100ms를 넘어가는 gRPC 요청들은 모두 TLS Handshake를 수행하고 있었으며 이 중 일부는 TLS Handshake에 100ms가 넘는 시간을 쓰고 있었습니다. APM에서 TLS Handshake가 타임아웃 이후에도 발생하는 이유는 다른 Go routine으로 커넥션을 생성하기 때문입니다. TLS Handshake는 커넥션을 맺을 때에만 필요한 과정이므로 긴 응답시간을 가지는 요청들은 대부분 새로운 커넥션을 만들려고 시도한다는 것을 확인하였습니다. TLS Handshake의 응답시간을 줄일 수만 있다면, 문제는 쉽게 해결할 수 있을 것이라 보여 TLS Handshake 과정에 대해 알아보았습니다.

그림 3) TLS Handshake 과정 (출처: TLS Handshake)

TLS Handshake에서 처음으로 주고받는 메시지인 `Client Hello, Server Hello`는 서로 커넥션을 맺기 위한 메타데이터를 공유하게 됩니다. 특히 [TLS Session Resumption](https://blog.cloudflare.com/tls-session-resumption-full-speed-and-secure/)은 TLS Handshake 과정을 간소화하여 응답시간을 줄일 수 있습니다. 최초 한 번의 Handshake 이후, 새 TLS 커넥션을 만들 때마다 처음에 받은 Session을 재활용할 수 있습니다. Session ID Resumption은 서버에서 Session을 캐싱하고 있으며, Session Ticket Resumption은 서버에서 캐싱 하지 않는 방식이라고 합니다. 이런 특징을 활용해서 여러 대안을 생각해 보고 이것들이 실현 가능한지 확인해 보았습니다. #### 1) TLS를 사용하지 않을 수 있을까? 하지 말아야 할 방법이지만 가장 간단한 방법이라 먼저 실현 가능성을 확인해 보았습니다. [Infrastructure Security in Amazon DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/network-isolation.html) 문서에 따르면, DynamoDB는 TLS 1.0 이상을 필수로 사용해야 하며, TLS 1.2 이상을 권장하고 있습니다. 따라서 TLS를 사용하면서 응답시간을 줄여야 합니다. #### 2) TLS Handshake의 응답시간을 줄일 수 있을까? 두 번째로 고민한 것은 hop을 최대한 줄이는 것입니다. Handshake의 응답시간이 10ms 정도가 일반적이고 에러가 나는 일부 요청에서만 100ms 가량 나왔으므로 Handshake에서 생기는 연산과정보다는 네트워크 응답시간이 주요 병목이 될 것이라 추정했습니다. DynamoDB는 기본적으로 퍼블릭 인터넷을 통해 접근하지만, 보안 목적으로 퍼블릭 게이트웨이를 거치지 않고 AWS 내부망에서 통신을 하도록 VPC Endpoint를 설정할 수 있습니다 ([Using Amazon VPC Endpoints to Access DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/vpc-endpoints-dynamodb.html)). 하지만 Amazon EKS를 사용해서 리워드 서비스를 띄우고 있었으므로 VPC 엔드 포인트를 사용하기 위해 필요한 인프라 설정이 필요하지만 응답시간을 줄일 수 있을지 확신이 없어 유보하게 되었습니다. 이후 AWS 솔루션 아키텍트 분들과 함께 가진 DynamoDB 클리닉 세션에서 위와 같은 상황에 대해 VPC 엔드 포인트를 사용하는 것이 어떤지 의견을 여쭤보았으나, 응답시간을 짧게 하려는 목적으로 VPC 엔드 포인트를 사용했을 때 효과는 크지 않을 것이라는 답변을 해 주셨습니다. #### 3) TLS Handshake를 하지 않게 만들 수 있을까? 위에서 설명한 Session Resumption 기능을 활용할 수 있다면 큰 문제 없이 응답시간을 최적화할 수 있습니다. 하지만 이것에 대해 공식적으로 보이는 답변은 찾지 못하였고, Case Open을 통해 DynamoDB에서 Session Resumption 지원 여부를 물어보았습니다. 답변이 오는 동안 `ServerHello` 메시지에서 Session Resumption 지원 여부를 클라이언트에게 알려주는 점에 착안하여, Go의 TLS 계층에서 이 정보를 로그로 찍어보았습니다. 확인해 보니 `ClientHello` 메시지의 `ticketSupported` 필드에 `true`를 넣어서 보내도 `ServerHello` 메시지는 `false`를 리턴하였습니다. 또한 Session ID 방식은 [Go에서 지원하지 않는 것](https://github.com/golang/go/issues/18607)으로 확인하였습니다. 이후 AWS에서 아래와 같은 답변을 받았습니다. - TLS sessions are honored only on same DynamoDB IP the initial TLS session was created on. We do not support TLS session resumption across DynamoDB IPs. - We do not support Extended Master Secret RFC 7627. If the client does use Extended Master Secret TLS session resumption will not operate. 따라서 TLS Handshake를 피할 방법은 없다고 판단하였습니다. DynamoDB가 낮은 응답시간을 강조하고 있었음에도 실제 응답시간에 큰 영향을 미치는 TLS Handshake에 대해 Session Resumption을 지원하지 않는 것은 아쉬웠습니다. #### 4) TLS Handshake의 빈도를 최소화하기 위해 커넥션을 재활용하면 되지 않을까? TLS Handshake를 하는 것이 불가피하므로 TLS Handshake의 빈도를 최대한 줄이려면 커넥션을 최대한 재사용 해야 합니다. 하지만 재시도 상황에서 TLS Handshake를 시도하는 것을 보면 커넥션을 재활용하지 못하는 것처럼 보였습니다. 첫 번째 포스팅에서 커넥션이 커넥션 풀로 반납되는 과정에 대해 설명하였습니다. 하지만 이번 케이스는 커넥션이 커넥션 풀로 반납되지 않은 상황이므로 어떤 경우에 커넥션 풀로 반납되지 않을 수 있는지 확인해 보기로 하였습니다. `tryPutIdleConn()`이 `idleConn`에 커넥션을 넣지 못하면 에러를 리턴하게 되고, `persistConn.close()` 메소드를 호출해서 커넥션을 닫게 됩니다. `petsistConn.close()` 메소드는 `readLoop()`, `writeLoop()` Go routine이 종료되도록 합니다. 커넥션을 닫아주는 `persistConn.close()`를 호출하는 함수들을 따라가 보았습니다. context의 타임아웃이 발생되는 경우 요청을 처리하는 `readLoop(), writeLoop()` Go routine이 타임아웃에 대한 정보를 받아 `persistConn.close()`를 호출하는 것을 확인하였습니다. 결과적으로 TLS Handshake를 지속적으로 만드는 원인은 짧은 타임아웃 값으로 인해 재시도가 생겨 오히려 역효과를 만드는 것이었습니다. 따라서 재시도가 자주 일어나지 않도록 하면서, 재시도가 동작할 경우 TLS Handshake가 성공할 수 있도록 타임아웃을 설정해 줘야 합니다. 저는 어느 정도 느린 DynamoDB의 응답시간에 대해 용인할 수 있도록 타임아웃을 70ms로 높였습니다. 70ms로 설정하면 재시도에서 TLS Handshake의 응답시간이 100ms 정도 될 경우 두 번째 시도에서도 실패하게 될 것입니다. 하지만 커넥션을 생성하는 과정은 gRPC 요청을 처리하는 Go routine과 다른 Go routine으로 동작합니다. 커넥션이 생성되고 난 뒤 커넥션을 사용할 Go routine이 없으면 이 커넥션은 유휴 커넥션이 되어 커넥션 풀로 들어가게 됩니다. 따라서 세 번째 요청에서 커넥션을 또다시 생성하려 하지만, 그 사이에 유휴 커넥션이 생겨 DynamoDB로 요청을 보낼 수 있을 것입니다. #### 5) 커넥션 풀에 충분히 많은 커넥션 풀을 유지할 수 있을까? 커넥션 풀에 충분히 많은 유휴 커넥션을 유지하여 일부 커넥션이 닫힐 때 바로 유휴 커넥션을 사용하도록 만드는 방법도 생각할 수 있습니다. 첫 번째 포스팅([HTTP connection pool in Go explained](http://localhost:1313/blog/http-connection-pool-in-go-explained/))에서 `MaxIdleConnsPerHost`와 `IdleConnTimeout` 값을 조정하면 커넥션 풀에 서버가 실제로 사용하는 만큼의 커넥션을 넣어둘 수 있습니다. 그런데 `IdleconnTimeout`의 기본값은 90초이지만, 리워드 서비스는 10분에 한 번씩 트래픽이 급증하는 경향을 가지고 있어 10분이 적절한 값이지만 이만큼 높이는 것은 합리적인 설정이 아니라고 보았습니다. 또한 커넥션 풀은 LRU로 동작하고 Round-Robin과 같은 옵션을 제공하지 않아 모든 커넥션을 살아있는 상태로 오랜 시간 동안 유지하기는 어렵다고 판단하였습니다. 이 문제에 관해 AWS 솔루션 아키텍트에게 여쭤보니, `IdleConnTimeout`을 늘리더라도 DynamoDB 서버에서 커넥션을 끊어버릴 수 있어 실제로 잘 동작하지 않을 것이라고 말씀해 주셨습니다. 나중에 Amazon DynamoDB의 아키텍처에 대해 깊게 설명한 영상을 보면서 DynamoDB가 수많은 인스턴스가 존재하는 Request Router가 요청을 받고 파티션을 가진 Storage Node에 요청을 전달하는 구조임을 알았습니다. 이 구조를 보고 DynamoDB 클라이언트가 특정한 Request Router 인스턴스와 통신하는 것이 아니므로 클라이언트의 커넥션을 임의로 끊는 것이 서버의 가용성을 높게 유지할 수 있지 않을까 생각하게 되었습니다. AWS에서 제안하는 방법으로는 [커넥션을 통해 더미 트래픽을 보내 커넥션을 유지하는 것](https://aws.amazon.com/ko/premiumsupport/knowledge-center/dynamodb-high-latency/)도 해결책이 될 수 있다고 합니다. 하지만 이 방식은 코드의 복잡도를 높이는 방식이 될 것 같아 채택하지 않았습니다. ### 마치며 처음 목표는 gRPC 요청의 타임아웃을 100ms 이내로 만드는 것이었지만, 간헐적인 요청들에 대해서까지 모두 최적화하려다 보니 오히려 커넥션 풀에 악영향을 줄 수도 있는 결과를 보았습니다. 적절한 타임아웃을 설정하여 응답시간이 긴 요청의 대다수는 해결했지만, 계속해서 긴 응답시간을 보이는 극히 일부의 요청은 아쉽게도 버려질 수밖에 없었습니다. 원인을 파악하기 위해 Go의 HTTP 커넥션 풀과 DynamoDB를 깊게 보면서 매우 값진 경험을 하게 되었습니다. 특히 DynamoDB를 더 관심을 가지게 되었고 아키텍처가 어떤 식으로 구성되어 있는지, 현재 지원하는 기능들은 어떤 방식으로 구현했는지 찾아볼 수 있게 되었습니다. DynamoDB의 아키텍처가 궁금하시다면 아래 두 개 영상을 추천드리며 마치겠습니다. 긴 글 읽어주셔서 감사합니다! - **[AWS re:Invent 2018: Amazon DynamoDB Under the Hood: How We Built a Hyper-Scale Database (DAT321)](https://www.youtube.com/watch?v=yvBR71D0nAQ)**: DynamoDB의 아키텍처와 주요 기능에 대해서 소개합니다. 특히 [Dynamo](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf)(SOSP ‘07)에 나온 아키텍처와는 다르고, 오히려 구글의 [Spanner](https://www.usenix.org/system/files/conference/osdi12/osdi12-final-16.pdf)(OSDI '12)에 가까운 아키텍처라고 느껴졌습니다. - **[FAST '19 - Transactions and Scalability in Cloud Databases —Can’t We Have Both?](https://www.youtube.com/watch?v=CK6h48zOY9k)**: DynamoDB가 어떻게 트랜잭션을 지원하고 있는지 설명합니다. 트랜잭션 또한 구글의 Spanner에 소개된 TrueTime처럼 실제 타임스탬프를 활용하여 트랜잭션을 지원하고 있는 것으로 보입니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [AWS GameDay Microservice Magic에서 3위! 버즈빌 A팀 인터뷰](https://tech.buzzvil.com/blog/aws-gameday-2021) Date: 2022-03-17 | Author: Diane Rha, Jacob Yu | Category: Culture 안녕하세요! 저희는 Buzzvil Culture Committee의 Diane과 Jacob입니다. 버즈빌은 지난 번 AWS GameDay에 참여하여 **Microservice Magic에서 3위**라는 뛰어난 성적을 거두었는데요! 버즈빌 개발팀의 높은 실력을 증명한 시간이 아니었나 싶습니다. 참가하신 다섯 분의 개발자 Liam, Noah, Aiden, Claud, Isac의 목소리를 듣고자, 인터뷰를 진행했습니다. GameDay에 관심 있는 개발자분들은 재미있게 읽어주세요! ![Score](https://tech.buzzvil.com/blog/aws-gameday-2021/gameday-score.jpeg) ### Q: 이번에 진행했던 AWS GameDay를 간단히 소개해주신다면? Liam - 장애 상황 대응이란 분/초를 다투는 일이고, 대응에 실수가 있거나 지연되면 그만큼 매출이건 고객과의 관계가 됐건 악영향을 끼치게 됩니다. 개발자에게 있어 장애 상황을 직접 겪고 대응해보는 것만큼 확실히 대응 경험을 쌓을 수 있는 방법은 없겠지만, 훈련을 하기 위해서 장애를 일으킬 순 없죠. GameDay는 실제 환경과 유사한 모의 환경을 구성하고 의도적으로 장애를 유발하는 실패 요소를 주입해서 대응 훈련을 하는 것을 의미합니다. 이번 AWS GameDay에서는 이런 장애 대응 모의 훈련에 팀 간 경쟁 요소를 가미하여 가상의 회사의 직원이 되어 모노리스 운영 환경을 마이크로서비스 환경으로 개선하는 주제로 진행되었습니다. ### Q: GameDay에는 어떻게 참여하시게 되었나요? Aiden - 버즈빌 합류 전에는 Devops 관련 경험이 없었어요. 버즈빌 Devops팀에 합류 후 1년 정도 경험을 쌓은 뒤 GameDay를 참여하게 되었는데, 스스로의 능력을 확인해볼 수 있던 기회라고 생각했습니다. Isac - 버즈빌 Devops edge 프로그램 참여 중에 GameDay에 대한 이야기를 들었습니다. 처음엔 Devops 지식이 많지 않아서 어려울 줄 알았는데, 편하게 참여할 수 있다고 말씀 주셔서 참석해보았습니다. Noah - 클라우드를 잘 사용하는 기업들에서는 장애 모의 대응과 같은 문화가 있다는 걸 알고 있었어요. 작은 기업에서는 직접 하기는 어려운 와중에, AWS에서 가상 환경을 잘 제공해주어서 참여해보고 싶었습니다. GameDay가 소개된 지 얼마 안 되기도 했습니다. 모두가 첫 번째 참석이었어요. ### Q: 각자 맡으신 역할을 간단히 소개해주신다면? Aiden - 마침 마이크로서비스가 총 3개 있어서 Aiden, Claud, Isac이 각각 한 개씩을 담당했습니다. 그리고 게임 진행과 관련된 정보나 가이드를 멤버들에게 전파하는 역할을 Noah가 맡았고, 각 마이크로서비스로 요청을 전달하는 API gateway를 Liam이 맡았습니다. ### Q. GameDay에서 다룬 시나리오와 버즈빌의 MSA 개선 과정을 비교해본다면 어떤 점이 비슷하고 어떤 점이 달랐을까요? Isac - GameDay에선 코드 변경은 없었고 인프라와 관련된 버그를 고치는 정도였어요. 저희 회사와 다르게 코드레벨에서 접근하는 경우는 GameDay에 없었습니다. 현실에서 일어나기 힘든 장애, 한 번도 경험해보지 못한 말도 안 되는 장애들이 생긴다는 게 당황스럽기도 했어요. 버즈빌에서는 코드로 인프라를 관리하고 다른 사람들의 리뷰를 받기 때문에 이런 장애가 일어나기 어려워서, 이런 점이 차이점이기도 했어요. 이번 기회를 통해 어떻게 해야 할지 배울 수 있었기도 했습니다. Noah - 비슷한 점은, 트래픽 처리가 과도해져서 트래픽을 더 잘 처리하기 위해 마이크로서비스로 전환한다는 점이었어요. 그리고 저희가 게임을 진행하면서 각각 마이크로서비스를 운영할 때 서비스 오너를 지정해서 했는데, 버즈빌에서도 서비스별로 서비스 오너를 지정해서 하기 때문에 이 점도 비슷했습니다. 게임에도 현실을 많이 반영했다는 생각이 들었어요. Claud - 각 마이크로서비스에 대한 관리 부담은 줄어들지만, 전체적인 운영 코스트와 복잡성이 늘어나기 때문에 이 부분을 신경써야 하는 점이 비슷하다고 느꼈습니다. 가장 달랐던 점은 GameDay에서는 어떻게 구현했는지도 모르는 다른 팀의 서비스를 서비스 메트릭만 보고 가져다 사용할 수 있다는 것이었는데, 이 부분도 흥미로웠습니다. ![Slide](https://tech.buzzvil.com/blog/aws-gameday-2021/gameday-slide.jpeg) ### Q: 좋은 성과를 거둘 수 있었던 버즈빌 팀의 특별한 전략이나 역량이 있다면 무엇이었을까요? Isac - 우선, 버즈빌에서 원래 AWS를 많이 쓰고 있다는 점이 있을 것 같습니다. Aiden - 다들 AWS console이 익숙하다보니, 다루지 않았던 제품들도 빠르게 핸들링 할 수 있었습니다. 그리고 이것도 나름의 게임이다보니 노하우가 좀 있더라구요. 그 전에 1위한 팀을 보니 이미 경험이 있었던 분들이 포함되어 있어서 빠르게 적응하실 수 있었던 것 같아요. 저희는 중반쯤에 요령이 생겼던 것 같습니다. 특히, 로그를 확인할 수 있는, CloudTrail이라는 서비스를 후반에서 적극적으로 활용하면서 점수를 높일 수 있었습니다. Noah - 초반에는 당황스러워서 어떻게 해야 할지 모르고 손발을 놓은 적도 있어요. 혼자라면 포기했을 텐데, 옆에서 누군가 원인을 찾다 보니 저도 포기하지 않고 열심히 하게 되었습니다. 이런 식으로 게임 진행에 현실적인 부분이 많이 반영된 것 같아요. Liam - 마이크로서비스, 마켓플레이스 모니터링, API gateway 대응 등 초반에 각자의 역할을 잘 나누어서 진행하다 보니 각 요소에서 발생할 수 있는 문제 상황에 대한 readme를 읽고 미리 예측하거나 빠르게 발견해서 대응할 수 있었습니다. ### Q: 실무 경험이 GameDay에서도 도움이 되었을 것 같은데요, 버즈빌에서 하시는 일이 어떤 식으로 도움이 되셨을까요? 예를 들면, 버즈빌에서 이용하고 있는 서비스 중 GameDay에서도 유용하게 쓰신 서비스가 있으실까요? Noah - 우선 버즈빌에서도 Auto Scaling Group, load balancer를 사용하고 있다 보니 쉽게 설정하고 사용할 수 있었습니다. 그리고 Lambda 익숙해서 운영도 어렵지 않았구요. Isac이 맡으셨던 ECS(Elastic Container Service), Claud가 맡으셨던 Elastic Beanstalk는 버즈빌에서 잘 사용하지 않기 때문에 처음에는 다루기가 어려웠습니다. 여러 서비스를 접하게 하려는 AWS의 의도도 있었던 것 같아요. Liam - 다양한 AWS 콘솔 사용자가 있는 환경에서 보안 설정을 변경한다거나 누군가가 실수로 리소스를 삭제해서 의도치 않은 장애가 일어난다거나 하는 일은 현실 상황에서도 충분히 발생할 수 있는 일이에요. GameDay에서는 AWS 계정에서 일어난 이벤트가 수집되는 Cloudtrail을 활용하면 누가 언제 어떤 리소스를 편집했는지 알 수 있기 때문에 장애 해결의 중요한 열쇠가 되는 경우가 많았는데요, 실제로 버즈빌에서도 갑자기 DNS 레코드가 바뀌었다거나 어떤 리소스가 갑자기 안 보인다거나 하는 일이 발생하면 가장 먼저 찾게되는 서비스입니다. ![승리 전략](https://tech.buzzvil.com/blog/aws-gameday-2021/gameday-description.jpeg) ### Q: GameDay를 진행하며 특별히 느낀 것이나 배운 점이 있을까요? Claud - 예상치 못한 이슈 대응을 위해 audit log가 중요하다는 것을 뼈저리게 느꼈습니다. Aiden - 게임 시나리오 안에는 갑자기 인프라 관련 설정이 뜬금없이 바뀌는 경우가 포함되어있었는데, 운영자 입장에서 변경사항을 파악을 잘하지 못했습니다. 평소에 잘 사용하지 않은 제품이다 보니까. 인프라 담당자로서, 인프라를 AWS에서 계속 운영한다면 관련 제품들을 계속 공부를 해야겠다고 느꼈습니다. Isac - Devops Edge에 참석하면서 Devops를 꾸준히 공부하고 있지만, 개발자로 일하다보니 아는데 한계가 있긴 한 것 같아요. 여전히 블랙박스처럼 보이긴 하지만, 어떤 걸 다루고 있는지는 잘 느꼈던 것 같습니다. 특히, 코드 뒤에서 일어나는 일들이 정말 다양하다는 걸 느꼈습니다. Noah - GameDay가 회사 내부적으로도 있으면 좋을 것 같다는 생각을 했어요. 이런 문화가 있으면 엔지니어 입장에서는 재미있게 느낄 수 있다고 생각합니다. ### Q: GameDay 중 생겼던 일 중 어떤 일이 가장 기억에 남으시나요? Aiden - 마지막 5분 정도 시간이 남았을 때, 3위와 4위 스코어 차이가 크게 나지 않았어요. 게임 막바지라 뭔가 더 개선을 하진 않고 운명에 맡기고 있었습니다. 스코어보드를 보면서 다 같이 기도를 하면서 마지막 순간을 보냈는데, 결국 순위권에 들어서 재미있고 기억에 남는 경험이 되었어요. 해커톤을 하는 느낌이었어요. Noah - 테이블에 앉아서 AWS에서 보내준 간식을 먹으면서 작업을 집중해서 재미있게 했던 것이 좋았습니다. 흔히 그리는 스타트업의 모습과 가까웠던 것 같아요. 예를 들면 영화 소셜네트워크에 나올법한? 개발자의 낭만을 느낄 수 있는 모습이었던 것 같습니다. Isac - “제발 고장 나지 마라”고 하면서 스코어보드 보고 있던 순간이 기억에 납니다. 간식도 별로 안 먹을 정도로 집중했던 것 같아요. 그리고 중간에 답을 잘 못찾고 있었을 때, 문제가 무엇인지 파악도 안 되어서 무력감을 느꼈는데 그때도 인상 깊은 순간으로 기억에 남습니다. Aiden - 어떤 장애가 발생했는데, 다른 팀들은 잘 처리하지 못하는 와중에 우리 팀이 잘 처리를 하면서 스코어가 쭉 올라갔어서 순위권에 들 수 있었습니다. 그리고 어디가 문제였는지 잘 모르겠을 때, Liam이 승부욕을 갖고 계속 문제를 찾아주셨던 모습도 기억에 남습니다. Claud - Aiden이 특정 서비스가 사용하는 API가 당일중에 스펙이 변경된 v2로 업데이트될 것이라는 정보를 빠르게 확인해주셔서 해당 변경사항을 다른 팀보다 빠르게 적용하여 스코어를 많이 획득했던 것이 생각나네요 ### Q: Claud, Isac은 Devops팀이 아님에도 참가를 하셔서 좋은 성적을 내셨습니다. 평소에 AWS/인프라 관련 역량을 어떻게 키우고 계신가요? Isac - AWS를 엄청 열심히 공부를 하고 있지는 않지만, 회사에서 새롭게 쓰는 기능은 항상 살펴보고 있어요. 인프라에 관심이 있어서 쿠버네티스, 클라우드 관련 자료들을 찾아보고 있구요. 라즈베리파이를 하나 사서 취미용 쿠버네티스 클러스터를 만들고 있는데요, 이런 걸 하면서 배포를 어떻게 하면 좋을지 같은 걸 찾아보고 공부하고 있습니다. Claud - AWS는 오래전에 자격증 시험을 보면서 공부했던 기초 지식이 도움이 되는 것 같아요. 현재는 업무 중 사용하는 서비스에 대해서만 팔로우업하는 편입니다. 인프라/클라우드는 관심 분야여서 개인적으로 구성한 k8s 클러스터를 playground로 하여 이런저런 놀이를 하고 있습니다. 이외에도 CNCF 프로젝트를 구경하거나 관련 블로그, 유튜브 채널을 구독하고 있어요. ### Q: 추후 AWS GameDay에 참가하실 버즈빌리언 or 개발자분들께 한마디 하신다면? Aiden - 사전에 준비를 해야 한다거나 필수적으로 필요한 역량은 없으니 부담 없이 즐기는 마음으로 참여하면 좋은 것 같습니다. AWS를 잘 쓰는 사람보다는 모두를 위한 게임인 것 같아요. Isac - 저도 사전 역량이 없는 상태로 참가를 한 거였는데, 닥치면 배우게 되는 것 같습니다! Noah - 공부해서 가면 더 얻을 게 많은 것 같아요. 수준이 높지 않기 때문에 누구든지 참여할 수 있는 것도 맞습니다. 맡은 서비스에 대해 기본적인 정보를 알고 가면, 그걸 기반으로 적용하고 응용하면서 좋은 경험을 얻을 수 있을 것 같습니다. Claud: 생소한 AWS 제품이 있으면 한번 훑어보고 참가하면 도움이 될 것 같네요. 이번에 나온 거는 ECS(Elastic Container Service), ElasticBeanStalk... Liam - 초반에는 문제가 동시다발적으로 발생하지 않았던 것 같아요. 각 서비스의 디테일을 충분히 미리 파악해두면 나중에 분명히 도움이 될 것이라 생각합니다. readme가 괜히 readme가 아니다. ![단체사진](https://tech.buzzvil.com/blog/aws-gameday-2021/gameday-buzzvil.jpeg) 지금까지 AWS GameDay에 참여하신 버즈빌 개발자분들의 이야기를 들어보았습니다. 인터뷰를 진행한 입장에서 느낀 점을 공유하자면, 우선 첫 참가인데도 3등을 하셨고, 또 실제로 활용되는 스킬들을 써보면서 모의 장애 상황에 대응하는 모습이 다들 대단하다고 느껴졌습니다! 다들 해커톤 느낌으로 재미있게 즐기신 것 같아서 저희도 나중에 꼭 참여해봐야겠다는 생각이 들었어요. 이처럼 버즈빌은 많은 개발자들이 각자의 영역에서 전문성을 높이고 성장할 수 있도록, 적극적으로 지원하고 있습니다. 버즈빌과 함께 성장하고 싶은 분들이 있다면 아래 링크를 통해서 적극 지원 부탁드립니다! 감사합니다!
[버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform)
**버즈빌 기술 블로그의 다른 콘텐츠도 구경오세요!** [주니어 개발자가 만난 클린 아키텍처]() [버즈빌 백엔드 기술 스택을 소개합니다]() [버즈빌 CTO가 들려주는 AWS 리인벤트(re:Invent) 2021]() --- ## [DynamoDB를 사용하는 Go 서비스의 응답 시간 최적화 #1 AWS Credential Token](https://tech.buzzvil.com/blog/dynamo-go-latency-optimization-1-aws-credential-token) Date: 2022-03-15 | Author: Raf Kim | Category: Backend 안녕하세요. 버즈빌의 데이터 엔지니어 Raf입니다. 이전 포스팅에서 Go의 HTTP 커넥션 풀에 관해 설명해 드렸습니다 ([HTTP connection pool in Go explained](https://tech.buzzvil.com/blog/http-connection-pool-in-go-explained/)). 이 포스팅에서는 Go 서비스에서 HTTP 요청에 대한 Trace를 남겨 DynamoDB 요청의 응답 시간에 큰 영향을 미치는 Credential Token 발급 로직을 찾아내고 최적화한 내용을 담았습니다. Amazon DynamoDB는 낮은 응답 시간과 strong consistency를 지원하는 NoSQL 데이터베이스입니다. 사용자에겐 노출되지 않지만 읽기, 쓰기 요청이 진행 중일 때에도 수평적 확장이 가능하므로 확장성, 가용성이 뛰어납니다. DynamoDB의 데이터 모델은 파티션 키와 정렬 키뿐만 아니라 다른 정렬키를 사용하는 LSI, 다른 파티션 키를 사용하는 GSI까지 지원하기 때문에, 다양한 접근 패턴에도 유연하게 대응을 할 수 있습니다. 버즈빌에서는 주로 Write intensive 한 테이블에 대해 DynamoDB를 활용하고 있습니다. 이런 워크 로드에서 DynamoDB를 사용하면 MySQL보다 Migration 같은 운영적인 부담이 훨씬 줄어드는 이점이 있습니다. 이 중 유저가 광고를 볼 때 받아 가는 리워드에 대한 적립 기록을 관리하는 리워드 테이블은 Write intensive 하면서도 낮은 응답 시간, 높은 처리량을 요구하여 DynamoDB를 사용하기로 하였습니다. 하지만 새로 리워드 서비스를 구현하면서 그동안 인지하지 못했던 에러들을 보았습니다. ### 적은 트래픽에서 간헐적으로 높은 응답 시간을 보이는 현상 리워드 서비스를 구현하면서 로드 테스팅 프레임워크인 [Locust](https://github.com/locustio/locust)를 같이 도입하였습니다. Locust는 파이썬이나 Go로 코드를 작성할 수 있어 버즈빌의 기술 스택과 일치하므로 도입하는데 부담이 적었습니다. 또한, 단순히 HTTP 요청을 보내는 것뿐만 아니라 GRPC 요청을 하거나 메시지 큐에 이벤트를 전달하는 것 등 로드테스트를 실행하는 대상에 대한 제약이 없어서 Locust를 선택하였습니다. 로드테스트는 트래픽을 점진적으로 늘리면서, 1000 RPS까지 안정적으로 처리하는 서비스를 만드는 것을 목표로 하였습니다. Locust에서 동작하는 테스트 에이전트가 리워드 서비스를 호출할 수 있도록 간단한 코드를 작성한 뒤 3 RPS 정도의 낮은 트래픽을 보내면서 모니터링을 하였습니다. 그런데 한두 시간에 한 번씩 응답 시간이 2초 가까이 튀는 요청이 한두 개씩 발생했습니다. 트래픽이 점점 늘어난다면 높은 응답 시간을 가지는 요청의 비중이 점점 더 커질 위험도 있었으므로 원인을 파악해야만 했습니다. ### Trace를 심어 원인을 파악해보자 먼저 DynamoDB에 대한 CloudWatch 대시보드를 확인했습니다. 최대 응답 시간이 이 정도로 나온다면 DynamoDB 안에서 요청을 처리하는 데 많은 시간이 걸렸다고 판단을 할 수 있습니다. 하지만 대시보드에서는 2초나 되는 응답 시간을 가지는 요청은 없었습니다. 그렇다면 문제는 DynamoDB에 요청을 보내는 리워드 서비스에서 많은 시간을 소요했다고 볼 수 있습니다. 버즈빌에서는 서버의 각 레이어에 Trace를 심고 Datadog APM을 통해 볼 수 있어, 쉽게 긴 실행 시간을 가지는 함수를 파악할 수 있습니다. 따라서 DataDog이 제공하는 AWS Trace를 활용하여 AWS Session에 Trace를 심어 APM을 확인하였습니다. 그림 1은 Trace를 심어 DataDog APM을 통해 본 결과입니다 (당시의 스크린샷이 없어 그림으로 대체하였습니다). 리워드 서비스의 repository 레이어에서 DynamoDB의 Query 커맨드를 호출할 때 남긴 Trace의 수행시간은 대략 2초였지만, DynamoDB의 커맨드가 수행되는 시간은 대략 20ms 미만으로 나왔습니다. 즉, Go AWS SDK 내부에서 2초에 가까운 시간동안 커맨드 수행이 아닌 다른 일을 하고 있었던 것입니다.

text 그림 1) DynamoDB에 Trace를 심고 난 후 DataDog APM

따라서 Go AWS SDK가 실제로 요청을 처리하는 로직을 살펴보았습니다. 주로 응답 시간이 길게 잡힐 것 같다고 추정되는 구간은 1) DynamoDB에 보내는 HTTP 요청과 2) SDK 내부에서 임시 자격 증명(토큰)을 재발급하기 위한 HTTP 요청이 있었습니다. 그림 1에서 DynamoDB에 보내는 HTTP 요청은 20ms 미만의 응답시간을 보이는 것을 확인하였으므로 인증 토큰 발급 로직이 원인이라 추정했습니다. 또한, SDK 내부에서 토큰이 만료될 때만 HTTP 요청을 보내고 있었으므로 “간헐적으로 응답 시간이 튀는 현상"을 설명하기에도 충분했습니다. 이를 직접 확인하기 위해, 토큰 발급 로직이 높은 응답 시간을 만든다는 것을 확인하기 위한 Trace를 심기로 하였습니다. Go에서는 HTTP 요청이 진행되는 과정의 몇몇 구간에 대해 Trace를 심을 수 있으며, Go AWS SDK 또한 `http.Client` 주입을 통해 이 기능을 지원합니다. `http.Transport`를 감싸는 구조체를 만들어 `http.Client`에 대해 Trace를 심었습니다. `http.Client`에 Trace를 심는 방법은 이전 포스팅 ([HTTP Connection Pool in Go Explained](https://tech.buzzvil.com/blog/http-connection-pool-in-go-explained/))를 확인하시면 됩니다.

text 그림 2) http.Client에 Trace를 심고 난 후 Datadog APM

그림 2는 `http.Client`에 Trace를 심고 난 후 응답 시간이 긴 요청의 Datadog APM입니다. 예상하던 것처럼, 토큰을 재발급하기 위한 HTTP 요청이 2초에 가까운 시간을 보이는 것을 확인하였습니다. 따라서 토큰 재발급 로직에 대해 리서치를 해보기로 하였습니다. ### 백그라운드로 토큰을 업데이트해 보자 AWS SDK의 토큰 발급에 대해 검색을 해보니 STS 엔드 포인트를 전역 엔드 포인트 대신 리전별 엔드 포인트로 쓰면 응답 시간이 줄어든다는 문서([Managing AWS STS in an AWS Region](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html)) 가 있어 이를 바로 적용해 보았습니다. 적용하는 방법은 간단합니다. 환경 변수에 `AWS_STS_REGIONAL_ENDPOINTS=regional`을 추가하면 됩니다.

text 그림 3) 리전별 엔드 포인트를 사용하도록 변경한 뒤 Datadog APM

2초까지 나오던 토큰 재발급 요청의 응답 시간이 100ms 정도로 많이 줄어들긴 했지만, 여전히 서비스가 GRPC 요청을 처리하는 도중에 토큰 재발급 로직이 포함되면 꽤 높은 응답 시간을 보였습니다. 간헐적으로 나오는 상황이었으므로 평균 응답 시간은 문제가 없었지만, 토큰이 만료될 때마다 GRPC API의 타임아웃이 발생할 가능성이 훨씬 커지므로 잠재적인 위험을 안고 있다고 생각했습니다. 이 문제는 만료되지 않는 1) Access Key 기반의 토큰을 사용하거나, 2) 토큰의 만료 시간을 없애거나, 3) 토큰을 백그라운드로 발급받는다면 이 문제를 해결할 수 있을 것입니다. 하지만 만료되지 않는 인증 방식을 쓰는 것은 보안에 대한 취약점도 생기므로 불가능한 선택지라고 볼 수 있습니다. 세 번째에 대해 찾아보니 Go AWS SDK에서도 비슷한 [이슈](https://github.com/aws/aws-sdk-go/issues/561)가 올라와 있었고, [코멘트](https://github.com/aws/aws-sdk-go/issues/561#issuecomment-185974563)에서는 백그라운드로 토큰을 재발급하여 해결하는 방법이 있었습니다. 다만 코드에 락을 사용하고 있어 로직에 대해 이해를 한 뒤 사용하기로 하였습니다. 다시 Go AWS SDK의 토큰 발급 구조를 보면서 이슈 코멘트에서 제안한 코드가 잘 동작할지 확인해 보기로 했습니다. AWS SDK에서 토큰이 발급되는 구조는 아래와 같습니다. - [`credentials.Credentials`](https://github.com/aws/aws-sdk-go/blob/main/aws/credentials/credentials.go#L120): [`credentials.Provider`](https://github.com/aws/aws-sdk-go/blob/main/aws/credentials/credentials.go#L100) 인터페이스를 통해 토큰 발급 요청과 토큰 만료 여부를 확인합니다. - [`stscreds.WebIdentityProvider`](https://github.com/aws/aws-sdk-go/blob/main/aws/credentials/stscreds/web_identity_provider.go#L51): [`credentials.Provider`](https://github.com/aws/aws-sdk-go/blob/main/aws/credentials/credentials.go#L100)의 구현체로, 임시 자격 증명 방식의 토큰입니다. 이 구현체는 환경 변수에 들어간 값을 바탕으로 SDK 초기화 시점에 결정됩니다. 환경 변수로 주입한 Access Key를 활용하는 구현체는 [`credentials.EnvProvider`](https://github.com/aws/aws-sdk-go/blob/main/aws/credentials/env_provider.go#L30)이며, 토큰이 만료되지 않습니다.

text 그림 4) stscreds.WebIdentityProvider를 사용하는 토큰 발급

그림 4는 타임라인에 따라 각 컴포넌트의 상호작용과 토큰의 상태를 나타내었습니다. `credentials.Credentials`에서 토큰이 만료되면 `stscreds.WebIdentityProvider`를 통해 토큰을 발급받고, `stscreds.WebIdentityProvider`는 AWS STS 엔드 포인트로 HTTP 요청을 보냅니다. `stscreds.WebIdentityProvider`는 내부 메소드로 만료 시각을 설정하고, 토큰을 리턴합니다. [백그라운드로 토큰을 발급하는 코드](https://github.com/aws/aws-sdk-go/issues/561#issuecomment-185974563)는 다음과 같이 동작하게 됩니다. `credentials.Provider`를 감싸는 구조체인 `RefreshProvider`는 Go routine을 통해 토큰을 주기적으로 발급받아 구조체에 캐싱 해 둡니다. 그리고 `credentials.Credentials`에서 토큰을 발급받기 위해 `Provider.Retrieve()` 메소드를 호출하면 구조체 내부에서 들고 있는 토큰을 리턴합니다. 이 코드는 실제 토큰 발급 로직이 리워드 서비스의 요청과 분리되도록 해주기 때문에 문제를 해결할 수 있을 것이라 보였습니다. #### 언제나 하나의 토큰만 활용하도록 만들자 실제로 배포를 하고 테스트를 실행해 보니 다른 에러가 발생했습니다. `InvalidSignatureException` 에러가 간헐적으로 발생했었고 다른 변경사항은 없었으므로 원인은 위의 코드가 분명했지만, DynamoDB 서버에서 에러를 내려주고 있어 근본적인 원인을 찾지는 못했습니다. 문서를 찾아보니 만료된 서명인 경우([Troubleshooting AWS Signature Version 4 errors](https://docs.aws.amazon.com/general/latest/gr/signature-v4-troubleshooting.html)) `InvalidSignatureException` 에러가 발생한다고 설명되어 있었습니다. 혹시 리워드 서비스가 동시에 두 개의 토큰을 가지고 있으므로 STS 서버에서 이전에 발급받은 토큰에 대해 만료 처리를 하지 않았을까 생각했습니다. 코드에서도 `RefreshProvider`는 새 토큰을 발급받고 있었지만 즉시 쓰이지는 않고, `credentials.Credentials`에서 이전 토큰을 사용하고 있다가 이전 토큰이 만료되면 `RefreshProvider`가 캐싱 해 둔 새 토큰으로 바꾸는 구조입니다. 즉 그림 5처럼, Go routine(파란색 화살표)을 통해 새 토큰을 발급받아도 `credentials.Credentials`는 가진 토큰이 만료될 때까지 긴 시간 동안 자신이 가진 토큰을 사용하게 됩니다.

text 그림 5) 동시애 두 개의 만료되지 않은 토큰 중, 이전 토큰을 사용하는 상황

따라서 리워드 서비스가 하나의 토큰만 쓰도록 수정해 보기로 하였습니다. `RefreshProvider.periodicRefresh()`에서 토큰을 발급받자마자 `credentials.Credentials`에서 새 토큰을 사용하도록 변경한다면, 리워드 서비스가 동시에 두 개의 토큰을 유지하지 않게 됩니다. 아래 그림 6은 제가 수정한 내용을 도식화하였습니다. 변경된 Go routine(파란색 화살표)는 10분에 한 번씩 토큰 발급을 요청한 뒤, 토큰 발급에 성공하면 `Credentials.Expire()`를 호출하여 `credentials.Credentials`가 가진 토큰을 만료시키고, `RefreshProvider`를 통해 캐싱 된 토큰을 받아 가도록 하였습니다. 이 코드는 [Github Gist](https://gist.github.com/moonsub-kim/9fb3eb3b72f2cafff51e6fa94ccaaaf3)에 올려두었습니다.

text 그림 6) RefreshProvider가 새 토큰을 발급받자 마자 credentials.Credentials의 토큰 만료 처리

며칠 정도 지켜보면서 `InvalidSignatureException` 이 발생하지 않는 것을 확인하고 토큰 발급 문제는 해결되었다고 판단하였습니다. 구현하면서 다소 아쉬웠던 점은 `credentials.Credentials` , `credentials.Provider` 둘 다 토큰을 캐싱하고 있고 이 둘을 새 토큰으로 업데이트하기 위해 강제로 `Expire()`를 호출한다는 점이 Circular dependency를 가지는 것 같아 좋은 구조는 아니라고 생각이 들었습니다. Datadog APM을 활용하여 `http.Client`에 Trace를 심어 그동안 알지 못했던 AWS SDK의 토큰 발급 로직을 찾아내고, 이를 RPC 호출의 Critical Path에 포함되지 않게 하여 간헐적으로 생기는 높은 응답 시간을 없앨 수 있었습니다. 이후 며칠 동안 로드테스트의 트래픽을 점점 늘려보면서 서비스가 더 많은 요청에도 안정적으로 동작하는지 확인한 뒤, 로드테스트를 끝내고 서비스를 릴리즈하게 되었습니다. 하지만 릴리즈 이후 몇 개월 동안 트래픽이 서서히 늘어나며 DynamoDB에 저장되는 레코드들이 많아지면서 또다시 높은 응답 시간이 생기는 문제를 보았습니다. 이 이슈는 다음 포스팅을 통해 공유드리도록 하겠습니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [HTTP connection pool in Go explained](https://tech.buzzvil.com/blog/http-connection-pool-in-go-explained) Date: 2022-03-07 | Author: Raf Kim | Category: Backend 안녕하세요. 버즈빌의 데이터 엔지니어 Raf입니다. 버즈빌에서는 Go 마이크로 서비스에서 Elasticsearch와 DynamoDB 등의 AWS SDK를 사용하거나, 내부 서비스가 아닌 애드 네트워크나 앱 퍼블리셔 등의 HTTP로 통신해야 하는 외부 서비스를 연동하는 경우 HTTP 요청을 사용하고 있습니다. 저는 DynamoDB의 응답시간을 낮추기 위해 최적화를 하던 도중 HTTP 커넥션 관리에 대해 집중적으로 분석을 하게 되었습니다. 이 포스팅에서는 Go에서 HTTP 통신에 사용되는 `http.Transport`를 분석하며 **Go의 `http.Client`가 커넥션을 관리하는 방법, 효과적으로 쓰기 위해 설정할 수 있는 파라미터, `http.Client`의 내부로직에 trace를 심는 방법** 등을 소개하려고 합니다. 먼저 간단하게 `http.Client`를 사용하여 웹사이트에 GET 요청을 보내는 예제를 가져왔습니다. 커넥션 풀을 10으로 설정하여 요청을 보내는 예시입니다. Go의 HTTP에 대해 파라미터를 직접 수정할 일이 없다면 `http.Get()` 함수를 호출해서 간단하게 사용 할 수도 있습니다. ### http.Client, http.Transport 이 둘은 각각 어떤 역할을 하는가? Go의 net/http 패키지에서 `http.Client`가 선언된 부분을 보면 아래와 같이 설명이 붙어있습니다. *A Client is higher-level than a RoundTripper (such as Transport) and additionally handles HTTP details such as cookies and redirects.* 이것을 보면 `http.Client`는 통신에 대해서는 직접적으로 관여하는 것처럼 보이진 않고 헤더 등의 필요한 데이터를 채워 넣는 일 등을 할 것으로 추측할 수 있습니다. 또한, `http.Client`는 `http.RoundTripper` 인터페이스를 통해 `http.Transport`를 가지고 있습니다. `http.Transport`에는 아래와 같은 설명이 있습니다. *Transport is an implementation of RoundTripper that supports HTTP, HTTPS, and HTTP proxies. By default, Transport caches connections for future re-use.* 이를 통해 Transport가 실제로 요청을 주고받는 역할을 하며 커넥션 풀도 관리한다는 것을 알 수 있으므로 `http.Client`는 보지 않고 `http.Transport`를 보도록 하겠습니다. ### http.Transport #### 커넥션 풀 관리에 쓰이는 공유변수와 파라미터 `http.Transport`가 가지는 필드 중, 커넥션 풀과 관련된 것들만 보면 아래와 같습니다. 아래 그림 1은 위의 필드 중, 커넥션 풀과 연관된 것들을 도식화하였습니다.

text 그림 1) 커넥션 풀 공유변수

먼저, 그림에서 굵게 표시된 것들은 모두 `http.Transport`가 가지는 필드입니다. Go를 사용해 보신 분들은 아시겠지만, 소문자로 시작하는 필드는 private이므로 애플리케이션 코드에서 접근할 수 없고, 대문자로 시작하는 필드는 public이므로 접근 가능한 필드입니다. 오른쪽 아래의 PersistConn은 하나의 커넥션을 가진 구조체이며, `writeLoop`, `readLoop` 이라는 두 개의 Go routine을 가지고 있습니다. 또한 다른 그림에서 PC라고 표현된 것은 모두 PersistConn을 의미합니다. 왼쪽 박스의 *Idle Connections*는 커넥션 풀에 관련한 필드들입니다. idleConn은 코드에 표현된 것처럼, 키, 값을 각각 호스트 정보(`connectMethodKey`)와 PersistConn 슬라이스를 가지는 맵입니다. 각 PersistConn은 타임아웃 시간이 설정되어 있으며 빨강색 숫자로 커넥션이 사라질 때까지의 남은 시간을 표현하였고, 슬라이스의 뒤쪽 인덱스에 있을수록 최근에 사용한 PersistConn 입니다. PersistConn의 타임아웃값은 IdleConnTimeout을 통해 설정 할 수 있습니다. 또한 MaxIdleConnsPerHost 값을 통해 호스트마다 가질 수 있는 최대 유휴 커넥션 수를 조정 할 수 있습니다. idleLRU는 전체 커넥션 풀 크기를 제어하기 위한 큐입니다. 오른쪽에 위치할수록 최근에 사용된 커넥션입니다. 전체 유휴 커넥션 수는 MaxIdleConns로 조정 할 수 있습니다. 오른쪽 위 박스 *Wating Requests*는 요청이 들어왔을 때 커넥션 풀에서 커넥션을 받지 못한 경우 대기하고 있는 요청을 나타냅니다. idleConnWait은 커넥션 풀에 커넥션이 없어서 Go routine을 통해 커넥션이 생성되기를 잠시 기다리는 상태이며 idleConn 처럼 호스트마다 슬라이스를 유지합니다. connsPerHostWait은 모든 커넥션이 사용되고 있는 경우 요청을 기다리도록 하는 큐입니다. *# of Connections* 박스에 있는 MaxConnsPerHost 필드로 호스트마다 가질 수 있는 최대 커넥션 수를 제한 할 수 있습니다. connsPerHostMaxConnsPerHost를 위해 호스트마다 가지고 있는 커넥션 수를 저장하고 있습니다. #### 요청 관점에서 커넥션 풀 다시 보기 위 그림을 통해 간략하게 커넥션 풀 제어를 위해 사용되는 공유변수와 파라미터를 보았습니다. 이 섹션에서는 애플리케이션 코드에서 HTTP 요청을 보냈을 때, 커넥션 풀을 활용하며 커넥션을 생성/반납하는 과정에 대해 알아보겠습니다.

text 그림 2) 커넥션을 가져오는 과정

그림 2는 요청에 사용할 커넥션을 가져오는 과정입니다. 시작지점은 위의 `http.Client.Do()`가 호출하는 `Transport.roundTrip()` 메소드입니다. `roundTrip()`은 커넥션을 얻어오기 위해 `getConn()` 메소드를 호출합니다. `getConn()`은 `wantConn` 객체를 만듭니다. `wantConn` 객체는 실제 커넥션에 해당하는 PersistConn을 담을 수 있게 되어있으며, `tryDeliver()` 메소드를 통해 다른 Go routine이 PersistConn을 전달 할 수 있습니다. `wantConn` 객체를 만든 뒤 `queueForIdleConn()` 메소드에 `wantConn`을 전달합니다. `queueForIdleConn()`은 공유변수인 idleConn에 접근하여 필요로 하는 호스트에 대해 PersistConn을 가져오려고 시도합니다. idleConn에서 PersistConn을 가져올 때는 가장 최근에 쓴, 즉 인덱스의 뒤쪽에 있는 PersistConn을 가져와 `wantConn` 객체의 `tryDeliver()` 메소드를 호출할 때 전달합니다. 커넥션 풀에 커넥션이 있는 경우엔 위처럼 커넥션을 가져오게 됩니다. idleConn에서 가져올 PersistConn이 없으면 PersistConn을 생성해야 합니다. PersistConn을 생성하는 과정은 Go routine을 통해서 수행합니다. 먼저 idleConnWait 큐에 `waitConn`을 넣어둡니다. 커넥션을 생성하는 과정은 Handshake 등으로 오랜 시간이 걸리기 때문에 커넥션이 생성되는 동안 다른 유휴 커넥션이 생기게 되면 빠르게 `waitConn`에게 전달해 주기 위해 idleConnWait을 활용합니다. 그리고 `queueForDial()` 메소드를 호출하여 새 Go routine 띄워 커넥션을 만들고 `wantConn`의 `tryDeliver()` 메소드를 호출하여 커넥션이 전달되도록 합니다. `getConn()`을 수행 중인 Go routine은 `wantConn` 객체에 PersistConn이 들어올 때까지 기다리게 됩니다. `wantConn` 객체에 PersistConn이 들어오면 `getConn()` 메소드가 끝나고 PersistConn을 활용해 호스트로 요청을 보내게 됩니다. `dialConnFor()` Go routine이 커넥션을 생성하는 동안 반납되는 다른 커넥션이 먼저 `wantConn`에 전달될 수 있습니다. 이때 `dialConnFor()` Go routine이 만든 커넥션은 `wantConn`에게 커넥션 전달에 실패하므로 커넥션을 반납해야 합니다.

text 그림 3) 커넥션을 반납하는 과정

그림 3은 커넥션 반납을 수행하는 `tryPutIdleConn()` 메소드의 동작을 표현하였습니다. `tryPutIdleConn()` 메소드가 호출되는 시점은 1) 위의 `dialConnFor()` 에서 `wantConn` 에게 커넥션 전달에 실패했을 때, 2) `wantConn` 이 커넥션을 받았으나 컨텍스트에서 cancel이 생겼을 때, 3) HTTP 요청이 처리되고 난 뒤 애플리케이션 코드에서 `res.Body.close()`를 호출하여 PersistConn의 `readLoop()` Go routine이 종료될 때입니다. `res.Body.Close()`를 호출해주지 않으면, `PersistConn`이 유휴상태가 되지 못하고 계속 살아있게 되어 메모리 릭이 발생하게 되는 것을 알 수 있습니다. `tryPutIdleConn()` 메소드는 커넥션 풀로 커넥션을 반납하기 전에 idleConnWait에 저장된 `wantConn`을 하나씩 꺼내어 `tryDeliver()` 메소드를 호출하여 커넥션 전달을 시도합니다. 이를 통해 wantConn이 새 Go routine을 띄워 `PersistConn`을 생성하는 것을 기다리지 않고 빠르게 커넥션을 얻어 갈 수 있습니다. idleConnWait에 `wantConn`이 없으면 커넥션 풀인 idleConn에 커넥션을 저장하면서 커넥션 타임아웃을 설정하게 됩니다. #### 커넥션 풀을 효과적으로 사용하기 위한 파라미터 Go는 위와 같이 커넥션 풀을 관리하여 커넥션을 재사용하도록 도와주며, 그림 1에 나온 몇 가지 파라미터를 조정하여 커넥션 풀을 더 효과적으로 사용 할 수 있습니다. 커넥션 풀과 관련한 파라미터와 default 값을 나열해 보면 아래와 같습니다. - MaxIdleConns: 유지 가능한 최대 유휴 커넥션 수, default: 100 - MaxIdleConnsPerHost: 호스트마다 유지 가능한 최대 유휴 커넥션 수, default: 2 - IdleConnTimeout: 유휴 커넥션 타임아웃, default: 90초 - MaxConnsPerHost: 호스트마다 사용 가능한 최대 활성/유휴 커넥션 수, default: 0 (무제한) 이 중 MaxIdleConnsPerHost의 default 값은 호스트마다 유지하는 유휴 커넥션 수가 최대 두 개까지 되는 것을 알 수 있습니다. 이 값을 고려했을 때, 트래픽이 줄어들면 빠르게 유휴 커넥션을 제거하여 메모리를 확보 할 수 있습니다. 하지만 이 값은 한 호스트로 요청을 보내는 트래픽이 갑자기 증가할 때 응답시간에 악영향을 미칠 수 있습니다. 단위 시간당 사용 중인 커넥션이 유휴 커넥션으로 돌아오는 속도보다, 커넥션을 사용하려는 요청이 더 많게 되면 커넥션을 생성하려고 할 것입니다. 따라서 요청의 Critical path에 커넥션 생성 과정이 포함되어 응답 시간이 늘어나게 되므로 커넥션 풀 활용의 이점이 사라지게 됩니다. 이처럼 서비스가 참조하는 호스트의 수가 일정하게 정해져 있다면 MaxIdleConnsPerHost 값을 바꾸어 사용하는 것을 권장 드립니다. ### httptrace.ClientTrace `getConn()` 메소드를 통해 커넥션을 가져온 뒤엔 이 커넥션을 통해 실제 통신을 하는 과정이 남아있습니다. 이 내용은 요청이 진행되는 과정에 trace를 심을 수 있는 `httptrace.ClientTrace` 와 함께 소개해드리고자 합니다. `httptrace.ClientTrace`는 아래와 같은 함수들을 정의하여 trace를 심을 수 있습니다. godoc의 [httptrace 패키지](https://pkg.go.dev/net/http/httptrace)를 보면 위 trace들이 언제 호출되는 지 주석을 통해 알 수 있습니다. 하지만 저는 HTTP 통신 과정을 자세히 알지 못하다 보니 어떤 순서로 호출되는지 알 수 없어서 실제로 사용할 때 모든 trace를 심어 로그를 보면서 대강 순서를 파악했습니다. 그래서 저처럼 HTTP 통신 과정을 잘 알지 못해도 호출되는 순서를 통해 trace를 심을 곳을 쉽게 파악 할 수 있도록 통신 과정 사이에 호출되는 trace를 도식화하였습니다. 그림 4은 커넥션을 얻는 과정, 그림 5는 서버와 통신하는 과정입니다. 타임라인에 따라 Go routine이 하는 행동을 설명해두었고, Go routine을 호출하거나 채널을 통해 데이터를 주는 경우는 점선 화살표로 표시하였습니다. 또한 동작하는 중에 `httptrace.ClientTrace`의 메소드를 호출하는 경우는 밑줄로 표시해두었습니다. 타임라인에서 굵게 칠해진 선들은 채널을 통해 기다리고 있는 상태를 의미합니다.

text 그림 4) 커넥션을 얻는 과정에서 호출되는 trace

 

text 그림 5) 통신하는 과정에서 호출되는 trace

이 중 가장 유용했던 것은 `TLSHandshakeStart()`와 `TLSHandshakeDone()`을 통해 TLS handshake 과정이 응답시간에 큰 영향을 미치는 것을 확인한 것이었습니다. DynamoDB를 사용하던 서비스에서 평소에는 10ms 정도의 빠른 응답시간으로 처리되던 요청이 가끔 100ms를 넘긴 적이 있었는데, trace를 심고 나서 TLS Handshake 과정으로 인해 많은 시간이 소요되는 것을 확인했었습니다. `httptrace.ClientTrace`는 아래처럼 사용 할 수 있습니다. 위와 같이 `http.Request`에 Context를 주입하여 trace를 심을 수 있습니다. 하지만 HTTP 요청이 생기는 코드마다 이 로직을 심는 것 대신, 아래처럼 `http.RoundTripper` 구현을 통해 요청을 생성하는 코드로부터 분리 할 수 있습니다. 또한 HTTP 요청에서 리디렉션 등으로 여러 번의 실제로 HTTP 요청이 생기는 것으로 추정되면, `RoundTrip()` 메소드 호출 전/후에도 trace를 심어 각각의 HTTP 요청에 대한 수행시간을 확인 할 수 있습니다. 버즈빌에서는 trace 함수에 Datadog을 연동하여 flame graph로 trace를 보고 있으며, 오픈 소스 대체재로는 opentelemetry, jaeger 등이 있습니다. ([버즈빌 백 엔드 기술 스택을 소개합니다](http://tech.anstjq.ml/blog/buzzvil-backend-tech-stack/)) ### 마치며 `httptrace.ClientTrace`를 활용하여 커넥션 생성과 HTTP 요청 중 병목이 되는 구간을 파악 할 수 있었고, 서비스의 HTTP 요청 패턴에 따라 `http.Transport`의 파라미터를 조정하여 커넥션 풀을 더 잘 활용할 수 있게 되었습니다. 사실 DynamoDB의 응답 시간을 최적화하기 위해 여러 시도를 해보다가 `http.Transport` 까지 내려와서 코드를 보게 되었는데, 다음 포스팅에서는 AWS SDK를 사용하면서 숨겨진 HTTP 요청이 간헐적으로 느린 응답 시간을 보이는 문제를 해결했던 이야기를 공유해 드리도록 하겠습니다. 작성한 내용은 http 패키지의 모든 내용을 자세히 확인한 것은 아니니 다소 부족한 점이 있을 수 있습니다. 혹시 알고 계시는 것과 다른 것을 발견한다면 [Github Gist](https://gist.github.com/moonsub-kim/6246802d85e16d56609da06af4083065)에 댓글을 남겨주시거나, [Linkedin Profile](https://www.linkedin.com/in/%EB%AC%B8%EC%84%AD-%EA%B9%80-b5242912b/)로 메시지를 보내주시면 감사하겠습니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [제품 전략 피라미드 - 버즈빌 제품팀이 일하는 법#2](https://tech.buzzvil.com/blog/product-strategy-pyramid) Date: 2022-01-24 | Author: Whale Lee | Category: Product 버즈빌 CPO 웨일입니다. 2021년을 회고하기 위해 시작한 시리즈였는데 어느새 2022년이 되어버렸네요. 작년 한 해는 버즈빌에게는 정말 많은 시도와 회고, 성장이 있었던 한 해였습니다. 이 포스팅을 통해 2021년 한 해 동안 버즈빌 제품팀이 제품과 비즈니스 성장을 위해 실행해 온 방식들을 소개해 보겠습니다. ## 시리즈 1. **[고객 중심(Customer-centric) 전략: 그룹 분할, NPS](../how-buzzvil-product-team-works-2021-first/)** 2. **[제품 비전, 전략, 그리고 로드맵: 제품 전략 피라미드](..//product-strategy-pyramid/)** 3. **[다양한 제품 개발 방법론](../how-buzzvil-product-team-works-3/)** ## 제품 정체성 제품이 성장하면서 주요 고객이 얼리어답터 그룹을 지나고 캐즘(초기 시장과 주류 시장 사이에 나타나는 수요 하락 또는 정체 현상)을 건너 실용주의자 그룹으로 들어서면 제품을 처음 만들 때는 마주하지 못했던 다른 성격의 문제를 마주하게 됩니다. 얼리어답터 그룹은 기존 제품에 대한 애정과 애착을 가지고 변화를 거부하며 좁지만 더 전문적인 제품을 요구하는 한편, 새로 들어오는 실용주의자 그룹은 보편적으로 필요한 기능의 확대와 그에 맞는 제품의 변화를 요구하기도 합니다. 이처럼 고객이 다양해지고 고객 요청이 많아지면 고객 요청 간 충돌이 발생합니다. 이슈 별로 논의하며 급하게 대응을 하다 보면 어느새 제품의 지향점이 어디인지 헷갈리기도 하죠. 이전 글에서 얘기한 것처럼 고객 중심이 아니라 고객 중시 관점에서 이슈를 해결하게 되면 수많은 고객 요청을 처리하다가 제품이 키메라처럼 변해버리기도 합니다. 버즈빌 역시 성장과 함께 이런 문제를 마주하곤 했습니다. 잠금화면 서비스 허니스크린으로 시작한 버즈빌 제품은 고객의 서비스에 잠금화면 기능을 추가할 수 있게 도와주는 B2B 제품으로 확장하게 되었고, 이후 잠금화면뿐만 아니라 서비스 내부 지면까지 제품군을 확장하며 리워드 광고 플랫폼으로 자리를 잡아갔습니다. 이 과정에서 누군가는 잠금화면 사용성에 더 집중하기를 원했고, 다른 누군가는 서비스 수익을 더 늘려주기를 원했죠. 고객을 중심으로 그룹을 나누기로 했기에 우리는 제품의 정체성을 검토하고 재정의해야 했습니다. ## 제품 로드맵: 시간은 우리를 기다려주지 않는다 한편 눈앞에 있는 일들은 제품이 어느 방향으로 나아갈지 정의될 때까지 기다려주지 않았습니다. 조직 변경 이전에 진행하던 일, 운영을 위해 어쩔 수 없이 해야 하는 일, 해왔던 일이기에 해야 하는 일이 계속 발생하고 있었죠. 모든 일을 멈추고 제품을 재정비할 시간이 있었다면 좋았겠지만, 스타트업은 비행 중인 비행기에서 날개를 조립할 필요가 있었습니다. 조직 변경 이후 일을 처리하기 위해 가장 먼저 시도한 방법은 제품의 로드맵을 구성하는 일이었습니다. 조직 변경과 함께 과제별 오너십이 굉장히 많이 변했습니다. 팀은 팀대로 어떤 일이 우선인지 결정하기 어려웠고, 유관 팀은 기다리고 있던 일이 언제 어떻게 진행될지 알기 어려웠습니다. 팀별로 분기 제품 로드맵을 구성하고, 공유하고, 조율함으로써 눈 앞에 펼쳐진 엉킨 실타래를 풀어냈습니다. 제품 로드맵을 구성하는 방법은 굉장히 다양합니다. 최종 결과물은 비슷하게 보일지라도 로드맵의 목적에 따라 구성 방법이 다양하죠. 아래 설명한 로드맵 종류 외에도 포트폴리오 로드맵, 전략 로드맵 등 정말 많은 로드맵 정의가 존재합니다. * **내부 로드맵(Internal Roadmap)**: 내부 운영을 위한 로드맵입니다. 주요 독자에 따라 개발 로드맵, 세일즈 로드맵, 경영 로드맵으로 나뉠 수 있으며, 각 로드맵에 따라 담아야 하는 내용이 다릅니다. * **외부 로드맵(External Roadmap)**: 고객을 위한 로드맵입니다. 고객에게 제공될 다음 제품이나 기능이 어떤 것인지 공유합니다. 고객을 설레게 만드는 로드맵이어야 합니다. * **목표 중심 로드맵(Goal-oriented Roadmap)**: 목표를 기준으로 정리된 로드맵입니다. OKR을 운영할 경우 Objective 혹은 Key Result 기준으로 로드맵을 관리할 수 있습니다. * **기능 기반 로드맵(Feature-based Roadmap)**: 기능을 기준으로 로드맵을 관리합니다. 거시적인 관점에서 로드맵을 관측하기는 어렵지만 세부 기능이 언제 개발되고 출시될 예정인지 확인할 수 있습니다. * **릴리즈 로드맵(Release Roadmap)**: 외부 로드맵 중 하나로, 주요 릴리즈 일정과 릴리즈 별로 담고 있는 제품 및 기능을 포함합니다. 우선 발등에 붙은 불을 끄기 위해 한 분기에 해당하는 기능 기반 로드맵을 구성했습니다. 팀별로 해야 하는 일을 나열하고, 유관 팀과 조율을 통해 우선순위를 결정했습니다. 딱 한 분기 로드맵을 구성하는데도 상당한 소통이 필요했습니다. 다행이 한 분기 짜리 계획이 만들어진 뒤에는 각 팀이 스스로 계획에 맞춰 움직일 수 있게 되었습니다. 한 분기짜리 로드맵을 구성하자 조금이나마 이후 계획을 위한 여유가 생겼습니다. 여유를 틈타 1년 치 로드맵을 구성하려고 보니 기능 기반 로드맵은 너무 세부 내용을 담고 있어서 계획하기 어려울 뿐만 아니라 지키기도 어려워 보였습니다. 무엇보다 기능 기반 로드맵은 제품팀이 어떤 방향으로 나아가는지 표현하기에는 너무 세부적이어서 한계가 있었죠. 그렇다고 계획을 세우지 않으면 분기마다 중심이 이리저리 흔들릴 것 같았습니다. 여러 논의 끝에 분기 정도 단위의 릴리즈 로드맵을 구성하기로 했습니다. 주나 월 기준이 아니라 분기를 기준으로 어떤 제품 혹은 기능을 출시할 것인지 뭉뚱그려 정리했죠. 계획은 변경될 가능성이 높았고 일정 역시 신뢰도가 낮았으나 한 해를 어떻게 보내려고 하는지는 감을 잡을 수 있는 로드맵이 구성되었습니다. 한 해가 지나고 돌아보니 역시 일정은 많은 부분 변경이 있었으나 전체적인 진행 방향은 크게 어긋나지 않았음을 확인할 수 있었습니다. 한편 한 해짜리 릴리즈 로드맵이 구성되었음에도 여전히 내부에서는 우리 제품이 어느 방향으로 나아가는지 잘 모르겠다는 자성의 목소리가 계속 들려왔습니다. 로드맵만으로는 한계가 있음이 느껴졌죠. ## 제품 전략 피라미드 제품 로드맵은 계획을 중심으로 움직인다는 장점이 있지만, 핵심 가치를 중심으로 움직이기 어렵고 변화에 약하다는 단점이 있습니다. 로드맵을 구성한 뒤에도 언제든 계획은 변경될 수 있다는 점을 인정하고 유연하게 움직이려고 노력했지만, 여전히 실행 도중 계획은 변경하는 일은 심리적으로 큰 부담이 있었습니다. 실행 도중 발견한 통찰과 좋은 기회가 있더라도 갑자기 진행 방향을 바꾸는 것은 쉬운 일이 아니었죠. 로드맵은 제품의 중심을 잡아줄 수 없었습니다. 제품의 중심은 제품 비전과 제품 전략이 잡아줘야 했죠. 조직이 변경되고 고객이 구체적으로 정의됐으니 제품의 비전도 다시 검토해봐야 했습니다. 발등의 불을 끄느라 로드맵을 먼저 구성했지만, 조금 늦었더라도 제품의 중심을 잡아줄 제품 비전과 전략이 준비되어야 했습니다. ![제품 전략 피라미드](https://tech.buzzvil.com/blog/product-strategy-pyramid/product-strategy-pyramid.png) ### 제품 비전 제품 비전은 제품을 통해 만들고자 하는 미래를 나타냅니다. 제품의 비전은 조직이 회사의 미션을 어떻게 달성할 것인지 좀 더 구체적으로 계획할 수 있게 정의되어야 합니다. 제품 비전은 영감을 줄 수 있어야 하고, 방향은 명확하되 방법은 유연해야 하며, 해결책보다는 문제에 집중해야 합니다. 한두 줄의 문장을 정의하기 위해 정말 많은 고민과 논의가 동반되어야 하죠. 디맨드 그룹과 서플라이 그룹은 각자의 고객을 중심으로 제품의 비전을 정리했습니다. 제품의 비전은 통상적으로는 2~5년 기간 안에 만들고자 하는 미래를 나타내지만, 조직 개편 이후 불확실성을 줄이기 위해 각 그룹은 1년 정도의 기간을 염두에 둔 상태로 비전을 정의했습니다. 당연하다고 생각했던 제품의 비전을 고객을 중심으로 다시 정의해보니 디맨드 그룹과 서플라이 그룹의 정의가 완전히 달랐습니다. 디맨드는 애드-테크(Ad-tech) 시장에서 광고주 고객에게 더 많은 가치를 제공하기 위한 제품 비전을 정의했고, 서플라이는 디지털 서비스 고객들이 자사의 서비스를 성장시키는 데 도움을 줄 수 있는 제품 비전을 정의했죠. 달라진 비전만큼 해야 하는 일도 달라졌습니다. ### 제품 전략 제품 전략은 제품 비전을 실현하기 위한 계획을 표현합니다. 누구를 위해 어떤 제품을 언제 어떻게 활용할지 정의합니다. 제품 전략을 구성하는 방법은 매우 다양합니다. 하지만 좋은 전략이라면 대부분 고객과 문제가 분명하게 잘 정의되어 있을 것입니다. 그리고 대부분 한 시점에는 하나의 시작 혹은 고객에 집중하고 있을 것이고요. 서플라이 그룹은 고객의 서비스 성격을 기준으로 고객을 세 가지로 나누었습니다. 큼지막한 기준으로 고객을 나누려고 시도했음에도 기준을 정의하기 위해 한 달 이상을 논의했습니다. 정보를 조사하고, 특정 기준을 정의한 뒤 고객 그룹을 나누고, 구분 기준이 의미 있는지 검토한 뒤, 이를 반복했습니다. 겹치지 않으면서 빠짐없이 나누기(Mutually Exclusive and Completely Exhaustive, MECE)를 희망했지만, 완벽한 기준을 정하기는 어려웠죠. 긴 논의 끝에 비즈니스와 제품, 경영 관점에서 동의하는 그룹을 정의했습니다. 고객 그룹을 정의한 뒤에는 그중 주요 고객 그룹을 정의했습니다. 돌아보면 고객 그룹 정의는 논의의 시작에 불과했죠. 팀 간, 파트 간 맥락과 시각의 차이로 인해 현재 집중해야 하는 고객 그룹을 어디까지로 봐야 하는지 다들 의견이 달랐습니다. 이를 합의하기 위해 더 많은 조사와 논의, 검증, 실험이 진행되었습니다. ‘치열하게 논의하고 결정된 것은 믿고 따른다’라는 ‘버즈빌이 일하는 방식’에 따라 치열한 논의 끝에 집중할 고객 그룹을 정의하고 실행했습니다. 한편 시기에 따라 집중해야 하는 고객은 변경되거나 확대될 수 있기에, 꾸준히 주요 고객 정의를 검토하고 논의하고 있기도 합니다. ## 회고 제품 로드맵이 없었다면 2주 후에 무슨 일을 해야 할지, 한 달 뒤 우리 제품은 어떤 모습일지 알기 어려웠을 것입니다. 제품 비전이 없었다면 우리가 무엇을 달성하고자 하는지 짐작할 수 없었을 것이고, 제품 전략이 없었다면 우리가 지금 이 제품을 왜 만들고 있는지 알 수 없었을 것입니다. 제품 비전과 전략, 로드맵 덕분에 조직 개편 이후 전사적으로 혼란스러웠던 시기를 극복하고 빠른 제품 성장과 비즈니스 성장까지 만들어낼 수 있었습니다. 하지만 이런 노력에도 여전히 우리가 어디를 향하는지 모르겠다는 내부의 목소리가 종종 들려왔습니다. 관련 구성원들의 논의와 몇 번의 내부 발표, 어딘가에 존재하는 비전/전략/로드맵 문서는 모든 사람이 같은 생각을 가지게 만들기에는 충분하지 않았죠. 비전, 전략, 로드맵 순서로 구성되는 전략 피라미드는 아래에서 위로 갈수록 추상적이고 포괄적인 내용을 담게 됩니다. 구체적이지 않은 내용일수록 더 자세히 소개하고 더 자주 강조할 필요가 있습니다. 2021년으로 다시 돌아간다면 더욱더 좋은 전략 피라미드를 구성하기 위해 더 많은 의견을 받고, 더 치열하게 논의하고, 더 자주 소개하는 자리를 가지게 될 것 같습니다. 언제나 시간과 사람은 항상 모자랍니다. 모든 것을 하고자 하면 아무것도 하지 못하게 되죠. 버즈빌은 제품 비전과 제품 전략을 바탕으로 무엇에 집중해야 하는지 끝없이 고민하고, 논의하고, 개선하고 있습니다. 버즈빌의 좋은 동료들 덕분에 제품팀은 두려움 없이 다양한 도전을 시도하고 이를 바탕으로 빠르게 성장하고 있죠. 물론 여전히 더욱 원대한 비전과 더 확실한 전략을 구성하기 위해 해야 할 일이 많이 남아 있습니다. 버즈빌의 멋진 여정을 함께할 동료를 찾는다는 소식을 전하며 글을 마무리합니다. 다음 글에서는 과제의 우선 순위를 정하기 위해 버즈빌 제품팀이 실행해온 방법을 소개해 보겠습니다. [버즈빌 PM 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [버즈빌 안드로이드 개발자는 이렇게 일합니다](https://tech.buzzvil.com/blog/how-buzzvil-android-dev-works-2022-jan) Date: 2022-01-24 | Author: Dio Heo | Category: Frontend 안녕하세요, 버즈빌 Output 팀의 Dio입니다. 버즈빌의 주요 제품 중 하나는 광고 지면을 모바일 플랫폼(안드로이드, iOS)에 쉽게 적용할 수 있는 SDK이고 이를 통해 Native, Interstitial, Feed, Lockscreen 등의 지면을 제공합니다. 버즈빌의 안드로이드, iOS 개발자는 바로 이 SDK의 개발을 책임지게 됩니다. 일단 앞서 소개해 드린 광고 지면을 개발하며 UI/UX를 개선하는 것이 첫 번째 역할이고, 새로운 광고 지면이나 UI도 주기적으로 탐색합니다. 그리고 이런 기능을 모바일 디바이스에서 효율적으로 제공하기 위한 노력도 지속하며, 동시에 매체사의 다른 개발자나 기획자 같은 SDK의 고객이 어떻게 하면 SDK를 편하게 사용할 수 있을지도 계속 고민합니다. 또한 이런 지면들을 활용해 버즈빌에서 직접 만든 앱인 허니스크린도 운영하고 있고, 이를 통해 실제 앱 사용자 경험도 개선합니다. 이번 글에서는 버즈빌의 안드로이드 개발자가 이런 제품들을 개발하기 위해 어떻게 일하는지를 소개해 드리려고 합니다. 제가 주로 담당하는 안드로이드 개발 과정을 중심으로 서술한 글이지만, iOS 개발자의 일하는 방식도 이와 흡사하기 때문에 iOS 개발자분들께도 흥미로운 내용이 많으리라 생각합니다.
Feed 지면의 예시


## SDK 개발 프로세스 버즈빌 안드로이드 개발자들은 주로 스프린트 방법론을 기반으로 한 과정으로 업무를 진행합니다. 스프린트는 2주 주기로 진행되며, 한 스프린트가 종료되면 실행 가능한 SDK와 앱을 빌드합니다. 빌드가 나온 후에는 QA 과정이 이어지며, QA가 완료된 이후에는 버즈빌에서 직접 운영하는 앱에서부터 실제 배포를 진행합니다. ### 스프린트 진행 버즈빌에서는 계층 기반으로 팀이 구성되어 있습니다. 그래서 안드로이드 개발자들도 제품에 따라 여러 팀에 나뉘어 소속됩니다. 각 팀에서는 팀의 장단기 목표에 맞게 매 스프린트마다 할 일을 계획하고, 개발자들도 팀원으로서 계획에 참여하며 각자의 목표를 합의하고 설정합니다. 목표가 정해지면 2주간 스프린트를 진행하게 되며, 스프린트가 완료되면 그다음 주에 새로운 버전의 빌드를 만들고 QA 단계로 넘어가게 됩니다. ### 배포 앱 빌드와 SDK 배포는 백엔드에서의 배포와 마찬가지로 ChatOps를 활용합니다. 슬랙에서 예약된 커맨드를 입력하면 [botkit](https://github.com/howdyai/botkit) 기반으로 개발된 내부 챗봇 서비스에 해당 요청이 전달되어 CI 서비스를 통해 앱 빌드나 SDK 배포가 시작됩니다. ![슬랙에서의 챗봇을 이용한 배포 예시](https://tech.buzzvil.com/blog/how-buzzvil-android-dev-works-2022-jan/slack_deploy_example.png) 버즈빌의 안드로이드 프로젝트는 하나의 저장소에 여러 모듈이 포함된 <[모노 리포 - 멀티 모듈]()> 구조로 구성되어 있습니다. 이 과정에서 Pull Request를 테스트하거나 SDK 라이브러리를 배포하는 데에는 고려할 사항이 매우 많은데요, (예시: [멀티 모듈 SDK 버전 관리]()) 이런 부분에 필요한 로직을 직접 구성한 Gradle task나 CI 서비스, 그리고 배포 파이프라인에 직접 개발하여 사용하고 있습니다. ## 개발 문화 앞서서 언급했듯이 기본적인 팀 구조는 계층 기반으로 구성되어 있지만 같은 직무의 개발자들끼리도 항상 교류하며, 안드로이드 그리고 iOS 개발자들도 가깝게 소통합니다. 1~2주에 한 번씩 정기적인 미팅을 통해 코드의 중요한 변경 점이나 흥미로운 이슈 및 해결 과정, 그리고 설계에 대한 고민 등을 공유하며 논의합니다. 새로운 기술에 관한 관심도도 전반적으로 높아서, 정기적인 미팅에서 개발자들이 소개하거나 자유롭게 스터디를 조직해서 조금 더 깊게 새로운 기술을 탐색합니다. 이렇게 학습한 기술은 치열한 검토 후에 실제 제품에도 적용됩니다. 코드 리뷰도 버즈빌의 중요한 개발 문화입니다. 개인적으로는 활발한 코드 리뷰를 운영하는 데 가장 중요한 것은 규칙보다도 문화라고 생각하는데, 버즈빌은 이 문화가 아주 잘 정착되어 있습니다. Pull Request 작성자는 코드 리뷰를 철저히 받기 위해 최대한 많은 정보를 Pull Request에 담으려 하고, 리뷰 요청을 받은 개발자도 꼼꼼한 리뷰를 하기 위해 최대한 노력합니다. 코드 구조나 디자인 패턴에 대한 고민과 설계가 필요한 상황이라면, Draft Pull Request를 만들거나 간단한 Design Document를 작성해 코드 리뷰 요청 이전에 주변 개발자 동료들과 먼저 논의를 시작하기도 합니다. ![신규 입사자의 첫 PR을 격렬히 환영하는 의미로 88개의 댓글이 달리며 코드 리뷰가 진행되는 현장](https://tech.buzzvil.com/blog/how-buzzvil-android-dev-works-2022-jan/pr_reviews.png) 코드 리뷰 규칙에 대해서도 간단히 소개해보겠습니다. 버즈빌 코드 리뷰의 규칙은 기본적으로는 구글에서 제시하는 [코드 리뷰 가이드](https://google.github.io/eng-practices/review/)를 따릅니다. 하지만 이 가이드의 모든 규칙을 엄격하게 따른다기보다는 그 안에 담긴 철학과 원칙을 중요하게 생각합니다. 또한 버즈빌 내부 상황에 맞게 수정하거나 추가해서 사용하는 규칙도 있으며, 각 팀의 상황에 맞게 일부 변형한 관례를 적용하기도 합니다. 예를 들어 위의 스크린샷에서 “[OUT-840]”은 Pull Request를 Jira card와 연결하기 위해 Pull Request 제목에 붙이는 접두사입니다. ## 마치며 지금까지 버즈빌 안드로이드 개발자가 일하는 방식에 대해 간략하게 소개해드렸습니다. 짧은 글을 통해 저희가 일하는 방식을 온전히 소개해드리는 것은 어려운 일이지만, 그래도 이 글을 통해 버즈빌 개발자의 일하는 방식을 조금이나마 느끼셨으면 좋겠네요. 애드테크, SDK 개발 등 일반적으로 접하기 힘든 일들을 해나가는 저희의 일하는 모습을 앞으로도 자주 소개해드리도록 하겠습니다. 지금까지 읽어주셔서 감사합니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [Pytest-bdd와 Selenium을 이용한 웹 UI 테스트 자동화](https://tech.buzzvil.com/blog/qa-web-ui-automation) Date: 2022-01-09 | Author: Erica Gwon | Category: DevOps 안녕하세요, 버즈빌 QA 매니저 Erica입니다. 버즈빌에서는 Dash라고 불리는 광고 관리 보드를 운영하고 있습니다. QA 파트에서는 이 중 광고주 대상 시스템 영역에 대해 웹 UI 테스트 자동화를 진행하고 있습니다. 이 글을 통해 현재까지 진행된 테스트 자동화 사례를 소개하며 테스트 자동화를 처음 시작할 때 고려해야 할 점을 함께 나눠보고자 합니다. ## UI 자동화의 대상과 프로젝트 소개 광고 관리 보드에서는 광고주의 광고 항목에 대한 설정 기능을 지원합니다. 이 기능 안에서 광고를 보여줄 이미지나 동영상 설정, 광고가 나가는 지면 설정, 광고 예산 설정, 광고 집행 시간과 타겟팅 설정, 성과 보고서 생성 등 다양한 세부 기능에서 여러 변경 사항이 발생합니다. 이로 인해 기존 기능이 영향을 받았는지 확인하기 위한 회귀 테스트를 매번 진행하고 있는데요, 이번 자동화 프로젝트는 보드 웹 UI 상에서 보이는 광고 항목의 회귀 테스트 자동화를 목표로 하고 있습니다. TMI. 현재 진행 중인 웹 UI 테스트 자동화 프로젝트의 이름은 ‘라떼’입니다. 프론트엔드에서는 자스민, 모카, 차이 등 차 이름을 딴 테스트 프레임워크가 많다 보니 이전에도 녹차라는 이름의 자동화 프로젝트가 진행되었었고 자연스레 같은 맥락에서 프로젝트 이름을 짓게 되었습니다. ‘예전에는 자동화 없이 수동으로 테스트했었다’라는 뜻에서 라떼이기도 합니다 :) ## UI 자동화는 어떻게 시작해야 할까? 라떼 프로젝트는 처음에 파일럿 프로젝트로 시작했습니다. 파일럿 프로젝트는 Selenium과 pytest로 BDD 기반 자동화 구현이 쉽게 가능한지, 자동화 프로젝트와 테스트케이스를 관리하고 있는 Testrail과의 연동이 가능한지 테스트하는 용도로 3 달여 남짓 진행했습니다. 파일럿 프로젝트를 진행해 보니 이대로 전체 영역을 대상으로 한 자동화 구현이 가능하겠다는 확신을 얻게 되어 이후 Selenium, pytest-bdd, pytest-testrail-client의 조합으로 본 프로젝트에 돌입할 수 있었습니다. 이와 같이 테스트 자동화 프로젝트 도입을 고민하는 단계에서는 파일럿 프로젝트를 통해 현재 선정한 툴의 조합으로 프로젝트를 진행해도 될지를 가늠해 볼 수 있습니다. 파일럿 프로젝트의 또 다른 장점은 처음부터 완벽한 구조를 고민하지 않아도 된다는 데 있습니다. 라떼 파일럿 프로젝트의 경우, 처음에는 Selenium의 WebDriver API를 통해 크롬 브라우저를 열고 로그인 창의 아이디와 패스워드 Input 필드를 선택하고 값을 넣는 코드에서 시작했습니다. 그러다가 page, element, locator 클래스를 도입해 구조를 잡아나가고 pytest-bdd를 적용해 시나리오와 자동화 스크립트를 디렉토리 단위로 분리해 냈습니다. 자동화 스크립트 중에서도 특정 기능이나 전체 세션에서 공통으로 쓰이는 기능은 conftest.py 내에 fixture로 다시 분리해 내고, pytest에 대한 지식이 늘어나면서 pytest hook과 pytest.ini에서의 환경 설정 코드도 하나둘씩 추가되었습니다. 최종적으로 pytest-testrail-client 플러그인을 통해 Testrail에 테스트 결과를 전송할 수 있게 되었을 때에는 이미 작은 영역에 대해 프로젝트 전체 구조가 잡혀 있었기 때문에 망설임 없이 본 프로젝트에 같은 구조를 적용할 수 있었습니다. ![Testrail로 전송된 자동화 프로젝트 수행 결과](https://tech.buzzvil.com/blog/qa-web-ui-automation/qa-web-ui-automation_testrail.png) ## UI 자동화를 할 때 어떤 고민이 들까? 웹 UI 테스트 자동화를 처음 진행할 때는 어디까지 테스트 자동화의 대상으로 삼을 것인지, 어떤 툴을 쓰는 게 좋을지, 프로젝트 구조를 어떻게 잡을 것인지에 대해 고민하게 됩니다. ### 구조 다행히 이번 프로젝트의 경우 ‘광고 항목의 회귀 테스트’라는 대상이 처음부터 설정되어 있었기 때문에 첫 번째 사항에 대해서는 크게 고민하지 않았습니다. 다만 대상 영역 중 어떤 부분을 먼저 자동화할 것인지에 대해서는 대개 ‘수동으로 확인하기 번거롭거나 어려운 부분’을 먼저 자동화해야겠다는 목표가 있었습니다. 프로젝트 구조에 대해서는 파일럿 프로젝트를 진행하다 보니 크게 테스트 시나리오, 테스트 스크립트, 웹 페이지 객체로 자연스럽게 상위 디렉토리 구조가 나뉘게 되었습니다. ```bash ├── features │ ├── account │ │ └── advertiser.feature │ ├── ad │ └── ad_group ├── step_definitions │ ├── account │ │ ├── conftest.py │ │ └── test_advertiser.py │ ├── ad │ └── ad_group ├── page_objects │ ├── locators │ ├── page.py │ └── element.py ├── pytest.ini └── conftest.py ``` ### 툴(tool) 툴에 대해서는 프론트엔드에서 사용하는 자바스크립트 기반으로 갈지 아니면 코드 구현이 직관적이고 배우기 쉬운 파이썬 기반으로 갈지에 대한 고민이 있었습니다. 라떼에서는 후자를 선택해 진행했고 결과적으로 파이썬의 직관성과 pytest와 연동되는 풍부한 라이브러리를 활용할 수 있게 되었습니다. ### 유지 보수성 자동화 프로젝트의 뼈대를 잡아가며 가장 많이 신경 쓴 부분은 앞으로의 유지 보수 가능성이었습니다. 라떼 이전에도 진행되었던 자동화 프로젝트가 있었지만 광고 관리 보드 UI의 업데이트에 따라 웹 페이지 요소 객체들이 변경되면서 스크립트의 상당 부분이 깨져 있는 상태였습니다. 라떼는 두 번째로 진행되는 프로젝트인 만큼 이 부분을 보강하기 위해 테스트 자동화에 사용할 요소 객체에 최대한 id를 많이 부여하고 xpath의 상대 경로를 통해 요소 객체를 선택함으로써 UI 변경 사항에 최대한 덜 민감해지도록 했습니다. 자동화 스크립트 작성도 쉽고 실용적이며 직관적이어야 한다는 판단하에 page object 내에 정의된 클래스를 활용해 요소 객체는 이미 정의된 클래스를 불러서 쓰고 스크립트 작성 시에는 필요한 동작을 잘 구현하는 데 집중할 수 있도록 했습니다. ![시나리오와 대응되는 테스트 스크립트](https://tech.buzzvil.com/blog/qa-web-ui-automation/qa-web-ui-automation_maintainability.png) ### 확장성 추가적으로 확장성을 고려해 개발 순서는 BDD를 기반으로 테스트 시나리오를 먼저 작성하고 테스트 시나리오에 대응하는 스크립트를 짜는 방식으로 가져갔습니다. BDD를 실제 적용해 보니 시나리오가 이미 정의되어 있기 때문에 테스트 스크립트를 짜기도 쉬워지고, 시나리오를 작성할 때 현재 광고 관리 보드의 QA를 맡고 계신 QA 매니저 Amy의 도움을 받을 수 있어서 프로젝트를 진행하기 한결 수월하다는 인상을 받았습니다. ![pytest-bdd로 작성된 테스트 시니리오](https://tech.buzzvil.com/blog/qa-web-ui-automation/qa-web-ui-automation_pytest-bdd-scenario.png) ## UI 자동화의 끝은 어디일까? 자동화 프로젝트의 개발 방식이 자리 잡아감에 따라 프로젝트 자체도 조금씩 개선해 나가고 있습니다. 처음에는 광고 관리 보드 시스템에 접속하기 위한 로그인 정보를 콘솔에서 입력하다가 불편하다고 느껴서 pytest.ini로 로그인 정보를 옮기게 되었고, 크롬 브라우저 버전이 올라갈 때 수동으로 업데이트해야 했던 크롬 드라이버를, 현재는 라이브러리를 적용해 크롬 브라우저 버전에 맞게 크롬 드라이버 버전을 자동으로 맞춰주도록 변경했습니다. 그 외에 아직 적용되지 못한 과제로는 로그인 세션이 브라우저에 걸쳐 유지되는 테스트 병렬 실행 적용, 서버에 자동화 프로젝트를 올려 누구나 사용 가능할 수 있게 만드는 작업 등이 있습니다. 그런 와중에 코드가 늘어갈수록 리팩토링 해야할 코드도 보이고 광고 관리 보드의 UI가 지속적으로 업데이트되면서 이미 작성된 테스트 코드도 수정하거나 추가해야 할 부분이 늘어나고 있습니다. 결과적으로 자동화 프로젝트도 결국 코드를 다루는 행위이기에 끊임없이 유지 보수하지 않으면 도태될 운명이 아닐까 합니다. 그러니 반대로 지속적인 유지 보수를 통해 테스트 대상과 보조를 맞춰 진행해 나간다면, 테스트에 대한 부담감을 줄이고 자신 있게 프로덕트를 내놓는 데 도움이 되리라고 확신합니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [고객 중심 전략 - 버즈빌 제품팀이 일하는 법#1](https://tech.buzzvil.com/blog/how-buzzvil-product-team-works-2021-first) Date: 2021-12-21 | Author: Whale Lee | Category: Product 버즈빌에서 CPO를 맡고 있는 웨일입니다. 어느새 벌써 2021년이 훌쩍 흘러갔네요. 버즈빌에게는 정말 많은 시도와 회고, 성장이 있었던 한 해였습니다. 이번 시리즈를 통해 올 한 해 동안 버즈빌 제품팀이 제품과 비즈니스 성장을 위해 실행해 온 방식들을 소개해 보려고 합니다. ## 시리즈 1. **[고객 중심(Customer-centric) 전략: 그룹 분할, NPS](../how-buzzvil-product-team-works-2021-first/)** 2. **[제품 비전, 전략, 그리고 로드맵: 제품 전략 피라미드](..//product-strategy-pyramid/)** 3. **[다양한 제품 개발 방법론](../how-buzzvil-product-team-works-3/)** ## 고객 중심(Customer-centric) 전략 고객 중시(Customer-focused)와 고객 중심(Customer-centric)은 서로 의미가 다릅니다. 고객 중시 전략은 고객을 만족시키기 위해 최선의 서비스를 제공하려는 방향에 가깝다면, 고객 중심 전략은 고객을 이해하고 그들의 입장에서 생각함으로써 고객이 겪는 문제와 필요로 하는 것을 찾아내는 방향에 가깝죠. 고객 중시는 밖에서 안으로 들어가는 접근이라면 고객 중심은 안에서 밖으로 나가는 접근이라고 할 수 있습니다. 예를 들어 디지털카메라를 사러 온 고객에게 현재 마주한 상황(e.g., 워킹맘이 바쁜 와중에 가족에게 손쉽게 아이 사진을 공유하고 싶다)을 물어본 뒤 고객에게 필요한 제품을 추천하는 것이 고객 중심 전략이라면, 곧바로 고객이 원하는 카메라 해상도가 몇인지 물어보고 그에 맞는 제품군을 추천해주는 것은 고객 중시 전략이라고 할 수 있습니다. Best Buy는 실제로 고객 중시에서 고객 중심으로 나아감으로써 9% 매출 상승을 경험했다고 합니다. 버즈빌의 올 한 해 키워드를 뽑는다면 ‘고객 중심’이라는 키워드는 빠질 수 없을 것입니다. 제품팀은 항상 수많은 가설과 요청, 산더미 같은 아이디어와 백로그들 사이에서 어떤 일이 중요한 일인지 고민합니다. 이런 상황에서 고객 중심 전략은 버즈빌 제품팀이 이리저리 흔들리지 않고 고객의 관점에서 중요한 문제들을 파악하고 해결하는 데 큰 도움을 주었습니다. ## 그룹의 분할과 팀 구성 방식 2021년 올해 초, 버즈빌은 고객 중심 사고를 강화하기 위해 고객을 기준으로 그룹을 분할했습니다. 아직 전 직원이 100여 명 정도인 스타트업이지만 여전히 전사 조직을 크게 둘로 나뉘는 일은 엄청나게 큰 변화였습니다. 제품팀 입장에서는 모든 제품팀을 재편성해야 할 필요가 있었죠. 팀을 재편성하는 과정은 큰 고통이 따랐습니다. 언제나처럼 시간은 촉박했고 의견은 다양했죠. 그룹이 나뉘기 전에 있던 제품과 서비스들의 오너십을 나누기도 쉽지 않았습니다. 버즈빌 시스템은 콘웨이 법칙(Conway’s Law)에 따라 조직의 구성을 반영하고 있었습니다. 조직이 변경되면서 어떤 서비스는 오너십을 잃어버렸고, 어떤 서비스는 양쪽에 속하는 상태가 발생했죠. 팀을 구성하기 위한 원칙이 필요했습니다. 크게 보면 다섯 가지 정도의 원칙으로 팀을 나눌 수 있었죠. * **제품 기반 팀 구성**: 각 제품을 중심으로 팀을 구성한다. e.g., 액션형 광고 팀, 수익화 도구 팀 * **계층 기반 팀 구성**: 제품의 계층(layer)을 기준으로 팀을 구성한다. e.g., SDK 팀, 엔진 팀 * **고객 기반 팀 구성**: 고객 그룹을 기준으로 팀을 구성한다. e.g., 커머스 광고 팀 * **기능 기반 팀 구성**: 같은 일을 하는 사람들을 한 팀으로 구성한다. e.g., 기획 팀, 서버 팀 * **목표 기반 팀 구성**: 달성하고자 하는 KPI를 기준으로 팀을 구성한다. e.g., PV 팀, ARPU 팀 ![팀 구성 방식](https://tech.buzzvil.com/blog/how-buzzvil-product-team-works-2021-first/team-structure.png) 구성 방법마다 일장일단이 있었습니다. 예를 들어 고객 기반 팀 구성은 고객 중심 사고에 부합한다는 장점이 있었지만 각 팀에서 비슷한 구현을 각자 해야 하는 사일로 현상이 발생할 가능성이 컸고, 목표 기반 팀 구성은 추구해야 하는 목표가 뚜렷하다는 장점이 있었지만 구현하는 기능마다 어느 팀에서 오너십을 가져가야 하는지 정하기가 어려웠습니다. 기능 기반 팀은 목적 중심으로 움직이기 어려웠고, 계층 기반 팀은 목적 달성을 위해 여러 팀의 협업이 필요했으며, 제품 기반 팀은 회색 영역이 다수 존재했죠. 역시나 One size fits all 은 없었습니다. 고민 끝에 처음부터 바로 청사진을 만들 수 없다는 상황을 인정하고 한 단계씩 조직 성숙도를 높여가기로 하였습니다. 조직의 구성을 여러 단계로 나눈 뒤 각 단계별로 성격과 다음 단계로 넘어가기 위한 조건을 명시했죠. 디맨드 그룹은 계층 기반으로 팀을 구성했습니다. 제품을 광고 상품, 광고 엔진, 광고 관리 도구로 나눈 뒤 각 제품을 담당하는 팀을 구성했죠. 한 분기 정도 학습을 해나가며 조직을 안정화했고, 이후 광고 상품 팀은 액션형 광고 팀과 노출형 광고 팀으로 분할되어 운영되었습니다. 서플라이 그룹은 KPI를 중심으로 팀을 구성했습니다. 한 팀에서 지표 하나를 끝까지 책임지고 달려 나갈 수 있는 장점이 있었죠. 한편 제품 오너십이 애매한 과제들이 종종 나타나기도 했습니다. 여러 시도 끝에 현재는 계층 기반 팀이 구성되었고, 제품 기반 팀으로 변화해 나가고 있습니다. 2022년에는 팀 토폴로지(Team Topologies)에서 제안하는 팀 구성을 적용하려고 준비 중입니다. 이 내용은 시리즈 마지막에서 좀 더 다뤄보겠습니다. ## 고객 추천 지수 (Net Promoter Score, NPS) 2020년부터 고객 만족도를 측정하기 위해 고객 추천 지수(NPS)를 활용하자는 얘기가 종종 나왔습니다. 2021년 전사 차원에서 고객 중심 전략을 시도하게 되면서 본격적으로 NPS 수집을 시작하게 되었죠. 기존에는 광고 플랫폼으로써 퍼블리셔, 광고주, 사용자 세 고객을 모두 고려해야 했기에 오히려 집중하기가 어려웠던 한편, 고객을 중심으로 그룹을 나누고 보니 해야 할 일이 명확해졌습니다. 우선 디맨드 그룹에서 먼저 시도를 하게 되었습니다. 영의 열렬한 지지 하에 세일즈 팀에서 주도하여 NPS 조사를 실행했고, 분석한 결과를 다 함께 논의했습니다. NPS 문항과 더불어 추천 이유와 비추천 이유를 함께 물어봄으로써 고객이 가진 생각을 더 자세히 알아볼 수 있었습니다. 덕분에 고객이 가진 주요 문제를 빠르게 파악할 수 있었고, 이를 해결함으로써 NPS를 7점에서 최대 40점까지 올려볼 수 있었습니다. 제품팀이 고객의 목소리에 귀를 기울이는 것은 매우 중요합니다. 이 사실은 모두가 잘 알지만 실행하기는 생각보다 쉽지 않습니다. 고객 개발 팀이 별도로 있지 않은 이상 - 있다고 하더라도 - 어떻게든 직접 고객과 소통하고 이를 통해 그들의 문제를 해결해야 합니다. 자칫하면 고객이 요구하는 것을 제공하려다가 그들이 실제로 필요한 것을 놓쳐버리는 경우도 발생하기 때문에 주의해야 하죠. NPS가 고객 개발 관련 과제를 모두 해결해주지는 못합니다. 하지만 고객의 목소리를 직접적으로 들을 수 있는 좋은 수단임에는 틀림없습니다. 더불어 고객 관점에서 우리가 잘하고 있는지 측정할 수 있는 좋은 지표가 됩니다. 고객 관련 지표를 측정할 수 있다는 것은 많은 의미를 가집니다. 피터 드러커의 유명한 말처럼, 측정할 수 없으면 관리할 수 없으니까요. ## 마무리 > “Ideas are easy. Execution is everything.” > > \- Venture Capitalist John Doerr 아이디어는 쉽습니다. 고객 중심으로 움직이자고 말하는 것 또한 쉽습니다. 결국에는 그것을 어떻게 실행할 것인지에 달려있습니다. 버즈빌은 한 달 만에 전체 조직을 두 그룹으로 나누고 한 분기 만에 이를 안착시킬 정도로 빠른 실행력을 가졌습니다. 저도 함께 실행했지만, 아직도 우리가 어떻게 잘 해낼 수 있었는지 궁금할 정도입니다. 영과 존 두 CEO의 비전과 모든 버즈빌리언의 실행력이 함께했기에 이뤄낼 수 있었다고 생각합니다. 한편 개선점은 항상 존재하고, 여전히 가야 할 길은 많이 남아있습니다. 내년에는 또 어떤 변화를 만들 수 있을지 두근거리네요. 다음 편에서는 조금 더 제품 팀 관점에서 제품 전략을 세우고 실행하기 위해 어떤 노력을 들였는지 소개해 보겠습니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [버즈빌 CTO가 들려주는 AWS 리인벤트(re:Invent) 2021](https://tech.buzzvil.com/blog/reinvent-2021-zune) Date: 2021-12-16 | Author: Zune Seo | Category: Culture 리인벤트는 AWS에서 개최하는 콘퍼런스 중 가장 큰 행사입니다. 미국 라스베이거스에서 열리며 티켓값만 무려 200만 원에 달합니다. 그만큼 콘퍼런스가 매우 크게 열리며 호텔 하나에서는 감당이 안 되어 라스베이거스의 여러 호텔에서 동시에 발표가 진행됩니다. 키노트에서는 AWS에서 1년간 준비해온 새로운 주요 서비스들이 한꺼번에 소개가 됩니다. 그동안 리인벤트가 열릴 때마다 AWS의 RSS를 통해 새로 출시되는 서비스가 뭐가 있나 챙겨보곤 했는데 드디어 리인벤트에 직접 참여할 기회가 생겼습니다. 코로나로 인해 해외 출장이 쉽지 않은 상황이었지만 무사히 다녀올 수 있었고 이후에 다른 분들이 리인벤트에 참석할 때 도움이 됐으면 하는 마음에서 사내 공유용으로 후기를 작성하였습니다. 작성하고 보니 다른 분들에게도 공개하면 좋겠다는 생각이 들어 블로그에 올리게 되었습니다. 회사 공식 블로그에 올리기에는 말투가 약간 가벼운데(?) 감안하고 읽어주시면 감사하겠습니다. ## 세션 예약 리인벤트는 티켓을 구입하고 나면 미리 세션들을 예약할 수 있습니다. 하지만 리인벤트 행사 참여 결정이 늦게 되면서 이미 대부분의 세션이 예약이 불가능한 상태였습니다. 울며 겨자 먹기로 남은 세션들로 예약을 했는데 리인벤트 웹사이트의 예약페이지는 예약 가능한 세션만 보여주는 기능이 없어 2천 개에 달하는 세션 중에서 예약 가능한 세션 찾는 작업이 어려웠습니다. 나중에 알게 됐지만, AWS events라는 앱을 받으면 여기서는 필터링 기능이 있어 쉽게 예약이 가능합니다. ## 키노트 키노트는 매일 있는데 그중에서도 화요일 오전에 AWS CEO인 Adam Selpsky가 진행한 키노트가 메인입니다. 듣던 대로 규모가 엄청났습니다. 키노트에서는 새로 나온 주요 서비스가 발표됩니다. 서비스 발표를 위해 왜 이런 서비스가 나왔는지 밑밥을 엄청나게 깔아놓고 서비스 소개를 합니다. 각 주제의 전환도 유연하게 이어진다는 느낌을 받았습니다. 이렇게 많은 사람 앞에서 막힘없이 술술 발표를 진행하는 CEO가 대단해 보이기도 했습니다. 초청 연사로 나스닥 CEO가 중간에 발표했는데 나스닥이 AWS를 적극적으로 활용하고 있는 모습을 보니 AWS에 대한 신뢰감이 더 커지는 것을 느낄 수 있었습니다. AWS CEO가 나스닥 CEO에게 연락해서 이번 키노트 때 AWS 자랑 좀 해달라고 부탁하는 모습도 상상을 해봤습니다. ![키노트](https://tech.buzzvil.com/blog/reinvent-2021-zune/keynote.jpg) 키노트 때 발표됐던 새로운 서비스 중 가장 눈에 띄는 것은 Arm 기반의 인스턴스인 그래비톤의 세 번째 버전이 나온 것과 머신러닝과 관련된 많은 새로운 기능들이 출시된 것입니다. 그래비톤은 2018년 말에 처음 소개가 된 것으로 알고 있는데 AWS에서 거의 매년 새로운 버전의 그래비톤을 출시하면서 3년 사이 많이 변화가 일어난 것 같습니다. 그래비톤을 사용하면 같은 성능대비 비용이 20%~30% 절감되다 보니 도입하는 회사가 빠르게 늘고 있습니다. RDS나 Elasticache 같은 관리형 서비스는 정말 아무것도 할 필요 없이 인스턴스 타입을 바꾸기만 하면 됩니다. 웹 애플리케이션의 경우 Arm 아키텍처에 맞게 다시 빌드를 해서 운영을 해야 하는 약간의 장벽이 있습니다. 예전에는 로컬 개발 머신은 인텔인데 실제 배포환경은 Arm이라 서로의 아키텍처가 다른 점이 약간은 불편한 요소라고 생각했는데 애플에서 Arm 기반의 맥북인 M1을 출시하면서 오히려 상황이 바뀌었습니다. 이제 개인용 컴퓨터, 서버 가릴 것 없이 Arm으로 천하통일 되는 것이 거스를 수 없는 흐름이 아닌가 하는 생각이 들었습니다. 이렇게 빠른 주기로 그래비톤 3이 출시됐다는 것은 그러한 생각을 더 굳게 만들어준 것 같습니다. 키노트 때 발표된 머신러닝과 관련된 새로운 제품들은 주로 SageMaker와 관련된 것들이었습니다. AWS에서 머신러닝 제품군을 강화하기 위해 노력하는 것이 느껴졌고 SageMaker를 밀어주고 있고 실제로 프로덕션에 사용하는 사례도 꽤 있다고 느꼈습니다. 미국의 큰 자산운용사인 뱅가드나 오퍼월 운영사인 탭조이에서 주요 머신러닝 시스템을 SageMaker로 운영하고 있다고 합니다. 세션 중에 SageMaker 실제로 쓰고 있는 사람 손 들어보라고 하면 약 15% 정도의 사람들은 손을 들었던 것 같습니다. 머신러닝 시스템 운영을 위한 옵션으로 꼭 검토해볼 만한 서비스인 것 같습니다. 목요일에 있었던 AWS CTO인 Werner Vogels의 키노트는 개발자의 키노트답게 기술적인 이야기로 많이 채워졌습니다. API 호출 인증을 위한 사이닝 키를 어떤 조합으로 생성하는지 등의 디테일한 부분까지 이야기를 하셨는데 생각보다 너무 디테일한 내용이어서 키노트에서 저런 이야기도 할 수 있구나 하고 생각이 들었습니다. Werner Vogels는 분산 시스템의 대가라고 알고 있습니다. 최근에 “Data intensive application”이라는 책을 사내에서 스터디하고 있는데 분산 시스템 주제를 많이 다루고 있다 보니 Werner Vogels를 직접 보게 되어 영광이었습니다. ## 워크숍 ### Get rolling with machine learning fast on AWS DeepRace 미리 알아보니 일반 세션은 나중에 녹화본으로 볼 수 있으나 워크숍 세션은 그렇지 않기 때문에 워크숍을 가보는 것이 좋다고 해서 예약이 남은 세션 중에서도 워크숍 위주로 예약을 했습니다. 첫 워크숍 세션은 “Get rolling with machine learning fast on AWS DeepRace” 이었습니다. 사실 딥레이서에 관심이 있는 것은 아니지만, 예약 가능한 것 중에 그나마 들을만해 보이는 것이 딥레이서였고 워크숍 세션이었기에 가봤습니다. 워크숍은 6명 정도가 함께 앉을 수 있게 책상을 모아놓은 형태로 배치가 되어 있었습니다. 한 15분 정도 설명을 해주시더니 갑자기 이제 공유한 링크로 들어가서 각자 알아서 해보라고 합니다. 워크숍이라 뭔가 서로 인터랙티브한 진행을 기대했는데 살짝 당황했습니다. 계정도 각자 개인 계정에서 진행해야 했고 대신 크레딧을 지급해줬습니다(다른 워크숍에서는 별도의 지급된 계정에서 진행했습니다). 회사 계정으로는 할 수는 없어서 개인 계정을 급하게 만들어서 튜토리얼을 따라 했습니다. 리인포스먼트 러닝의 리워드 함수 부분만 작성하면 나머지는 알아서 해주기 때문에 쉽게 동작하는 모델까지 만들어볼 수 있는 것이 인상 깊었습니다. 하지만 모델 학습에 약 1시간이 걸리다 보니 기본 예제 돌려놓고 나서 시간이 뜨는 상황이 벌어졌습니다. 학습 결과를 보고 나서 하이퍼 파라미터 튜닝을 하려면 결국 하나 돌려놓고 기다려야 했는데 학습 전략을 세우기 위해 공식 문서를 하나씩 읽다 보니 내가 원하던 워크숍은 아닌 것 같아서 중간에 나왔습니다. 하지만 워크숍의 좋은 점도 있습니다. 특히 막혔을 때 도와주실 가이드분들이 여러 명 참석하여 도움이 필요하면 언제든지 물어볼 수 있습니다. 이번 딥레이서 워크숍의 경우 그 전에 미리 어느 정도 공부를 해놓고 질문할 것들을 마련해와 참석했더라면 훨씬 유용했을 것 같습니다. ![워크숍](https://tech.buzzvil.com/blog/reinvent-2021-zune/workshop.jpg) ### Introduction to Working backwards 최근에 워킹 백워드라는 책을 읽었는데 마침 같은 주제로 워크숍이 있어 참석하였습니다. 예약은 못 했지만 워크 업으로 쉽게 참석할 수 있었습니다. 워크숍은 우리의 고객이 누구인지 정의하고 그들의 문제를 나열해보고 그중 하나의 문제에 대한 솔루션을 생각해보는 것입니다. 최종 결과물로 press release 형식으로 customer quote, idea summary, headline을 작성하게 됩니다. 이때 PR을 읽는 독자의 관점에서 관심이 갈 수 있도록 글을 쓰는 것이 중요합니다. 진행 방식은 옆에 앉은 사람과 중간중간 결과물에 대해서 서로 설명하고 피드백 주는 방식입니다. 저는 미국 정부 기관에 솔루션을 제공해주는 회사에 근무하시는 분과 이야기를 했습니다. 시간이 부족해서 맘에 드는 결과를 만들지는 못했지만 그래도 기대했던 워크숍의 모습대로 참여자들과 인터랙티브하게 진행 할 수 있어서 좋았습니다. 사내 Employee experience 팀의 제임스가 가끔 해주시는 세션 때 서로 논의하면서 진행하는 때도 있는데 그때랑 비슷한 느낌입니다(제임스 짱). 워크숍이 끝나고 발표자분께 슬라이드 발표자료 받을 수 있는지, 회사 내에서 워크숍 진행해보려면 어떻게 해야 하는지 여쭤보니 어카운트 매니저에게 도움을 받아보라고 하셨습니다. 그리고 아마존 내에서는 실제로 어떻게 워킹 백워드의 문화를 만들어가냐고 물어보니 상위 리더가 계속해서 강조하고 도움을 주는 것이 중요하다는 이야기를 해주셨습니다. ![워킹 백워드 슬라이드](https://tech.buzzvil.com/blog/reinvent-2021-zune/workingbackwards.jpg) ## 광고 관련 세션 광고 관련 세션들은 개수도 적고 그렇게 인기가 많지는 않았던 것 같습니다. 세션 장소도 다른 세션 대비 소규모였고 그나마도 빈자리가 조금씩 보였습니다. ### Under the hood at Amazon Ads 리인벤트에서 들었던 광고 세션 중에 가장 유용했습니다. 초반에 들었던 세션이고 나중에 다시 보기 할 생각으로 메모를 제대로 안 했더니 자세한 내용이 기억나지는 않네요. 3명이 돌아가면서 압축적으로 내용을 설명하는데 첫 번째 발표자는 use case 위주의 대략적인 설명을, 두 번째 발표자는 ad serving system을 좀 더 디테일하게 설명, 마지막 발표자는 ML에 관해 설명을 해줬습니다. 광고 캠페인 관리를 위한 데이터베이스로 RDS를 쓰다가 스케일 문제로 다이나모디비로 갈아탄 것(소스는 다이나모디비이지만 검색을 위해서는 우리와 마찬가지로 엘라스틱서치 사용), 그리고 인퍼런스 전용 칩을 사용하여 BERT 모델을 실시간으로 인퍼런스하는 부분이 인상 깊었습니다. 초당 수만 요청에 수억 개 광고 스코어링을 하는데 지연시간 20ms 99p라고 하네요. 두 번째 발표하신 분께 궁금한 점들이 있어서 따로 질문하러 갔습니다. 그런데 저 말고도 질문 있으신 분들이 많이 있으셔서 거의 30분가량을 발표자를 코너에 몰아넣고 여러 사람들이 질문 공세를 펼쳤습니다. 답변을 완벽히 이해하지 못해서 추가적인 질문을 하고 싶어 링크드인에서 검색할 수 있는 이름을 받아두었습니다. 원래는 한국에 돌아가서 온라인으로 물어보려다가 다음날 링크드인에 친구 추가하고 질문을 했더니 바로 오프라인으로 만나서 이야기하자고 해서 행사장에서 만나 이야기를 나눴습니다. 빠르게 연락해보기를 잘했다는 생각이 들었습니다. 물어본 내용은 다음과 같습니다. 광고 서치 인덱스로 엘라스틱서치 대신 Solr를 쓴 이유는? : 엘라스틱서치는 여러 샤드에 요청을 보내 결과를 다 수집한 다음에 완성이 되면 한 번에 리턴이 되고 이 과정에서 끼어들 수가 없지만 Solr는 이러한 부분에 중간에 끼어들어 커스터마이징이 가능하다. 그리고 Solr는 요청 결과가 스트리밍되기 때문에 중간 결과만 받고 처리하는 것도 가능하다. Solr에서의 latency는? : 20~30ms이다. 그리고 쿼리 결과가 캐싱 되기 때문에 결과적으로는 대부분의 요청이 캐시에서 처리된다. 요청 파라미터가 한두 개도 아니고 그러면 unique query가 엄청나게 많은데 그걸 다 캐싱하나? : 다 캐싱한다. 그래서 캐싱 된 데이터가 300GB이다. 캐시 invalidation이 실시간으로 안 되기 때문에 application logic에서 필터링을 한 번 더 한다. Solr에서 리턴되는 광고 개수가 1000개 정도 된다. 예산 제어 delay가 1초라는데 어떻게 한 건가? : Kinesis analytics로 쏜 데이터를 aggregation 해서 다이나모디비에 atomic count operation으로 예산을 차감하는 방식이다. aggregation window가 기본적으로 길게 되어 있지만 남은 예산이 10달러 이하면 해당 광고들에 대해서는 1초마다 aggregation을 한다. 아마존에서의 광고 시스템에 대해서 알아볼 수 있는 다른 자료들이 더 있나? 구글은 [advertising platforms arthitecture](https://cloud.google.com/architecture/infrastructure-options-for-building-advertising-platforms)도 공개했던데 AWS에서도 만들어주면 좋을 것 같아. : 내부 비밀 정보들이라 더 밝힐 수 없다. 그리고 너희 구글 스케일 아니잖아. 괜히 구글 프랙티스 따라 하지 말고 실제로 지금 당장 해결해야 할 문제들을 해결하는 게 중요해. 나도 일단 만들고 문제 생기면 고치고 하면서 일하고 있어~ (동의는 하지만 그래도 미래에 어떻게 될지 큰 그림을 알고 싶어서 물어본 거라고 변명했습니다.) ![발표자분과 함께 찍은 사진](https://tech.buzzvil.com/blog/reinvent-2021-zune/amazonad.jpg) 발표자분은 정말로 실용주의의 극치를 달리시는 분 같았고 그만큼 그의 설명에서 아마존의 광고 시스템이 성급한 최적화보다는 필요한 기능을 빠르게 구현하는 형태로 발전해왔다는 것을 알 수 있었습니다. 버즈빌에서 개발해온 광고 시스템이 아마존의 시스템과 닮은 부분이 많다는 것도 흥미로운 부분이었습니다. 광고 서빙을 위해 범용 검색엔진을 검색 인덱스로 사용하는 점, 예산 제어를 위해 Kinesis analytics를 사용하는 점(다만 버즈빌은 다이나모디비 대신 레디스를 예산 관리 데이터베이스로 쓰고 있습니다)이 그러한 부분입니다. 아마존과 크게 다른 부분이 광고 검색 인덱스의 결과를 캐싱하는지입니다. 캐싱을 사용하면 검색 속도를 빠르게 해주는 장점도 있지만 300GB에 달하는 캐시를 관리해야 하는 비용이 발생하고 실시간으로 동기화하지 못하기 때문에 애플리케이션 로직에서 추가적인 필터링 로직을 구현해야 하는 등 단점도 존재합니다. 버즈빌에서는 캐싱보다는 광고 검색 인덱스를 최적화하는 방향으로 발전하고 있습니다. 버즈빌에서 어떠한 기술 스택을 활용하고 있는지는 최근에 올라온 [버즈빌 백엔드 기술 스택을 소개합니다](https://tech.buzzvil.com/blog/buzzvil-backend-tech-stack/) 포스팅에서 확인할 수 있습니다. ### Best practices for cloud-based real-time ad platforms 마케팅/광고 헤드 1명과 실무자 2명이 나와서 나눠서 발표를 진행했습니다. 꽤 도움 되는 내용도 많았고 40분 정도 발표하고 나서 남은 20분은 Q&A로 진행된 발표였습니다. 세션 타입이 일반이 아니라 chalk-talk이라서 발표시간이 짧았을 수 있겠네요. Best practice는 다음과 같습니다. **Low cost** - 100% spot으로 운영하는 것을 목표로 한다. - Aerospike 쓴다. - Data transfer 줄인다. 외부 서버와 통신이 많으면 private pricing 알아보거나 PrivateLink 활용. Region cross replication 안 하는 등 region 간 통신 최소화. **Low latency** - SO_REUSEPORT 켠다 - 1 bidder 64 CPU 대신 32 bidder 64 CPU (2 CPU per bidder) → 이유를 설명해줬으나 이해를 못 했습니다 ㅠ - FastJSON 사용 - fastHTTP 사용 - Non-blocking data stream - Kinesis에 Zstandard compression 사용 - Optimize Prometheus summaries 아마존도 광고 서버로 golang을 사용하는 듯했습니다. 많은 회사에서 Aerospike를 쓴다는 이야기가 나와서 왜 다이나모디비 대신 Aerospike 쓰는지 질문을 했는데 짧은 영어 듣기실력 때문에 다 이해는 못 했지만 대략적으로는 managed와 self-service의 차이 정도로 이해를 했습니다. 우연히도 제 양쪽에 한국인분이 앉으셨는데 제 질문이 끝나자마자 거의 동시에 저한테 한국인이시냐면서 명함을 주셨습니다. 세션 끝나고 다 같이 이야기를 잠시 했는데 두 분 다 광고업계 분이셔서 재미있게 이야기 할 수 있었습니다. ### Applying Amazon SageMaker to real-time advertising workloads 아까 나온 마케팅/광고 헤드 1명과 탭조이에서 1명이 발표를 진행했습니다. 역시나 이 세션도 40분 발표하고 남은 20분은 Q&A로 진행하는 같은 패턴이었습니다. 탭조이 발표의 마지막쯤 슬라이드에 핵심적인 내용이 있네요. 인상 깊은 점은 SageMaker의 inference 기능을 쓰는데 따로 lambda 띄우는 것이 아니라 bidder server에서 client SDK 활용해서 바로 SageMaker로 inference 요청을 날리는 부분입니다. Inference 서버 따로 구축을 안 해도 되니 좋은 것 같습니다. 그리고 탭조이 전체 워크로드의 70%를 SageMaker로 처리하고 있는 부분이 눈에 띄었습니다. - ~5 single-model and ~4 multi-model Amazon SageMaker endpoints handling ~70% of our production ad requests - Using ~40 c5.2xlarge instances - Model training and hyperparameter tuning achieved on Amazon SageMaker - Inference via Client SDK - didn’t use AWS Lambda; no need to expose inference calls publicly - Model deploys and retraining is performed manually - Couchbase used to serve user-level features, contextual features stored in memory of our bidding engine ## 엑스포 엑스포 행사에 가면 정말 많은 회사가 있습니다. 첫날에 갔다가 티셔츠만 엄청나게 수집을 했는데 많이 모아놓고 나니 너무 티셔츠에만 관심을 가진 게 아닌가 하는 생각이 들었습니다. 마지막 날에 엑스포에 한 번 더 갔는데 티셔츠 욕심이 사라진 상태에서 보니 어떤 회사들이 있는지 눈여겨보게 되었습니다. 회사들이 너무 많다 보니 어떤 부스를 볼 것인지 판단하기 위해서 회사 이름과 한 문장 정도의 캐치프레이즈만 보고 빠르게 결정을 내려야 했습니다. 만약 버즈빌이 이곳에 부스를 차렸다면 한 문장으로 어떻게 메시지를 만들어야 할지 고민하게 만드는 포인트였습니다. ![Expo 입구](https://tech.buzzvil.com/blog/reinvent-2021-zune/expo.jpg) 엑스포를 둘러보며 정말 대략적인 감이지만 많이 보였던 회사들의 종류가 두 가지가 있었습니다. 하나는 클라우드 운영을 쉽게 해주는 솔루션(eg. FinOps 관련 회사들)을 제공하는 회사들이 보였고 다른 회사들로는 데이터 분석 플랫폼(eg. Firebolt)을 제공하는 회사들이었습니다. FinOps와 관련해서는 우리가 쓰고 있는 Spot by NetApp(Spotinst가 NetApp에 인수됨)이라는 회사에서 제공하는 솔루션도 보였고 Apptio가 부스가 커 보여서 데모를 구경했습니다. 비용이 궁금해서 물어보니 전체 청구 비용의 2%를 지불해야 한다고 합니다. 2%면 꽤 큰 비용인데 과연 그만큼 효과가 있을지 의문이 들었지만, 가격만 좀 더 저렴하면 유용할 것 같다는 생각이 들었습니다. 그리고 생각보다 제가 몰랐던 데이터 분석 플랫폼 회사들이 많은 것을 알게 되었습니다. 최근 급부상 중인 Snowflake나 Databricks와 같은 회사들은 치열한 경쟁을 이겨내고 성장한 회사들이었구나 하는 생각이 들었습니다. 엑스포에는 AWS 부스도 따로 마련이 되어 있는데 여기에 AWS SA 분들이 깔려있습니다. EC2, Container, AI/ML 등등 각각의 주제로 부스들이 나뉘어있고 궁금한 내용을 바로 물어볼 수 있습니다. 화이트보드가 있어서 SA 분들께서 그림을 그려가면서 설명을 해줍니다. 아쉽게도 질문할 거리가 생각나지 않아서 활용하지 못했습니다. 만약에 리인벤트에 참석할 계획이라면 평소에 궁금했던 부분들을 잘 정리해놨다가 가서 이것저것 물어보면 유용할 것 같다는 생각이 들었습니다. ## 메가존 행사 버즈빌은 메가존이라는 MSP 회사와 함께 협력하고 있습니다. 메가존에서 펍을 빌려서 저녁에 공짜로 술을 마시며 네트워킹 할 수 있는 자리가 두 번 있습니다. 이때 다른 회사에서 오신 분들과 이야기할 기회가 있었습니다. 리인벤트 행사가 워낙 크다 보니 세션 중인 낮에는 회사에서 같이 가신 분들과도 마주칠 일이 별로 없었고 그렇기에 다른 회사에서 오신 분들을 만날 기회도 적었습니다. 메가존이 주최해주신 행사를 통해 한국의 다른 회사 분들을 만나는 기회를 많이 만들 수 있었습니다. ## 기타 주저리 세션이 많기 때문에 호텔 여러 군데서 동시에 세션이 진행됩니다. Venetian이라는 호텔이 메인 호텔입니다. 여기서 키노트도 진행됩니다. 이번에는 코로나로 세션이 줄어서 Venetian 외에는 Wynn, Encore, Caesars forum까지 총 네 군데서 진행이 됐습니다. Venetian에서 키노트 장소 쪽으로 걸어 나가면 Caesars forum으로 바로 이어진 육교가 보여 걸어서 갈 수 있는데 나머지 호텔은 Venetian 호텔 1층에서 셔틀을 타고 가는 게 좋습니다. 저는 이동하기 귀찮아서 Venetian과 Caesars forum에 있는 세션 위주로 들었습니다. 세션 하는 행사장에서 아침, 점심을 나눠줍니다. 음식의 퀄리티가 꽤 좋다고 느꼈습니다. 특히 Caesars forum에 있는 점심 장소는 야외에 있어서 바깥 공기를 쐬며 식사를 할 수 있어서 좋았습니다. ![Caesars forum 점심 장소](https://tech.buzzvil.com/blog/reinvent-2021-zune/lunch.jpg) 모든 좌석이 예약제로 운영되는 것이 아니기 때문에 꼭 듣고 싶은 세션이 있다면 미리 가서 줄 서고 있으면(walk-up) 들을 수 있습니다. 인기 세션이 아니라면 미리 갈 필요도 없지만 인기 세션들은 30분~1시간 전에 미리 가서 기다려야 할 수 있습니다. 저는 EKS 관련 세션을 워크 업으로 갔다가 사람이 많아서 못 들었는데 다른 대부분 세션은 그냥 가서 들을 수 있었습니다. 요즘에 kubernetes에 대한 관심이 많다는 것을 느낄 수 있었습니다. AWS 분들이 직접 하시는 발표에서는 [아마존의 리더십 원칙](https://aws.amazon.com/ko/careers/culture/)을 발표내용에 녹여내는 경우가 꽤 있었습니다. 특히 첫 번째 리더십 원칙인 “**Customer Obsession**”은 여러 번 언급되었습니다. 덕분에 아마존의 비전이 “**to be Earth’s most customer-centric company**, where customers can find and discover anything they might want to buy online, and endeavors to offer its customers the lowest possible prices.”라는 것이 머리에 각인되었습니다. 회사의 리더십 원칙을 실제로 실천하기 위해 노력하는 모습들은 배울만한 점이었습니다. ![발표 세션 중의 working backwards](https://tech.buzzvil.com/blog/reinvent-2021-zune/amazonad-workingbackwards.jpg) re:Play라는 목요일 저녁에 하는 뒤풀이 행사가 있는데 유명한 DJ(Zedd)가 디제잉을 했습니다. 뒤에 크게 스크린이 있어서 화려한 애니메이션이 나왔는데 중간에 오징어 게임 애니메이션이 나와서 한국의 위상이 올라간 것을 느낄 수 있었습니다. 참고로 뒤풀이 행사장은 큰 천막 3개로 이루어져 있는데 술은 당연히 공짜고 한 천막에서는 음식을 나누어 줍니다. 그래서 굳이 저녁 먹지 않고 가도 행사장에서 주는 음식으로 때워도 됩니다. ## 새로운 서비스 버즈빌에서 관심 있어 할 만한 새로운 서비스들을 나열해봤습니다. 너무 많아서 설명은 생략합니다. 그리고 SageMaker 서비스들도 새로 나온 것들이 많아서 주의 깊게 보면 좋을 것 같은데 이것도 종류가 많아서 SageMaker 캔버스만 포함했습니다. - [Amazon ECR, 풀스루 캐시(pull through cache) 리포지토리 발표](https://aws.amazon.com/about-aws/whats-new/2021/11/amazon-ecr-cache-repositories/) - [AWS Karpenter v0.5, 이제 정식 버전 제공](https://aws.amazon.com/ko/about-aws/whats-new/2021/11/aws-karpenter-v0-5/) - [Apache Iceberg 기반의 Amazon Athena ACID 트랜잭션(평가판) 발표](https://aws.amazon.com/ko/about-aws/whats-new/2021/11/amazon-athena-acid-apache-iceberg/) - [AWS Graviton3 프로세서 기반의 새로운 Amazon EC2 C7g 인스턴스 발표](https://aws.amazon.com/about-aws/whats-new/2021/11/amazon-ec2-c7g-instances-aws-graviton3-processors/) - [Amazon EC2 Trn1 인스턴스 평가판 발표](https://aws.amazon.com/ko/about-aws/whats-new/2021/11/amazon-ec2-trn1-instances/) - [Amazon MSK Serverless 공개 평가판 소개](https://aws.amazon.com/ko/about-aws/whats-new/2021/11/amazon-msk-serverless-public-preview/) - [Amazon Redshift Serverless(평가판) 발표](https://aws.amazon.com/ko/about-aws/whats-new/2021/11/amazon-redshift-serverless/) - [Amazon Kinesis Data Streams 온디맨드 발표](https://aws.amazon.com/ko/about-aws/whats-new/2021/11/amazon-kinesis-data-streams-on-demand/) - [Amazon EMR Serverless 평가판 소개](https://aws.amazon.com/ko/about-aws/whats-new/2021/11/amazon-emr-serverless-preview/) - [정확한 기계 학습 모델을 구축하는 시각적이며 코드 없는 인터페이스인 Amazon SageMaker 캔버스 소개](https://aws.amazon.com/ko/about-aws/whats-new/2021/11/amazon-sagemaker-canvas-machine-learning-models/) - [Amazon DynamoDB, 최대 60%까지 DynamoDB 비용을 절감해 주는 새로운 Amazon DynamoDB Standard-Infrequent Access 테이블 클래스 발표](https://aws.amazon.com/ko/about-aws/whats-new/2021/12/amazon-dynamodb-standard-infrequent-access-table-class/) - [Amazon S3, 세 가지 스토리지 클래스에 대해 최대 31%의 가격 인하 발표](https://aws.amazon.com/ko/about-aws/whats-new/2021/11/amazon-s3-price-reduction-storage-classes/) ## 마치며 리인벤트는 끝나고 나면 일정 기간 동안 온라인에서 다시 보기가 가능합니다. 사실 저는 오프라인에서 발표를 듣는 것보다는 온라인 발표를 듣는 것을 더 선호합니다. 불필요한 소개 부분은 스킵하고 배속을 올려서 보면 짧은 시간에 훨씬 많은 정보를 얻을 수 있기 때문입니다. 그래서 리인벤트에 참석해서는 직접 가봐야지만 경험할 수 있는 것들을 경험해보기 위해 노력했습니다. 워크숍도 많이 참석해보고 발표 끝나고 질문도 적극적으로 했습니다. 덕분에 발표자분과 따로 만나 이야기도 해보고 정보도 더 많이 얻는 등 의미 있는 시간을 보낸 것 같습니다. 언젠간 또 기회가 있기를 바라며 이상 글을 마칩니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [버즈빌 신입 개발자는 이렇게 성장합니다](https://tech.buzzvil.com/blog/버즈빌-신입-개발자는-이렇게-성장합니다) Date: 2021-12-05 | Author: Claud Choi | Category: Culture 안녕하세요, 버즈빌 Ad Display 팀의 Claud입니다. 갓 커리어를 시작한 주니어 개발자라면 앞으로 엔지니어로서 어떻게 성장할 수 있을지에 대해 이런저런 고민이 많을 것 같습니다. 최근 저희 팀에 새로 합류한 개발자가 빠르게 성장하는 모습을 바라보며 버즈빌에서 신입 개발자가 어떻게 성장하는지 공유해보려 합니다. ### 온보딩 - 시작 버즈빌에 새로 합류한 개발자는 버즈빌의 소프트웨어 아키텍처와 [기술 스택]()에 익숙해질 수 있도록 온보딩 과정에 참여합니다. 기본적으로 동료 엔지니어나 온보딩 담당자가 전체적인 구조를 공유하는 세션을 진행하고 있으며, 스스로 학습할 수 있도록 필독 서적을 선정하여 제공하고 있습니다. 추천 서적을 통해 비즈니스 요구사항을 빠르게 달성하면서도, 관리가 용이하고, 확장 가능하며, 안정적인 소프트웨어를 작성하기 위한 원리와 기반 지식을 학습할 수 있습니다. | 책 | 저자 | | - | - | | Clean Architecture: A Craftsman's Guide to Software Structure and Design | Robert C. Martin | | Domain-driven design | Eric Evans | | Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems | Martin Kleppmann | 온보딩 과정을 통해 기반이 되는 지식을 쌓은 후에는 팀에서 진행하는 프로젝트나 내부적으로 관리하는 개발자 스킬 로드맵을 통해 우선순위를 정하여 기술 스택을 학습하게 됩니다. ### 개발 과제 - 설계부터 배포까지 버즈빌 개발자들은 팀에서 담당하는 다양한 프로젝트에 참여하여 개발 과제를 진행합니다. 버즈빌의 시스템은 마이크로서비스 아키텍처(MSA, Microservice Architecture)로 구성되어 있기 때문에 새로운 비즈니스 요구사항이 발생하는 경우 다양한 마이크로서비스에 기여하게 됩니다. 마이크로서비스를 개발하는 과정에는 항상 설계 문서(Design document)를 작성하는 것을 원칙으로 하고 있습니다. 새로운 개발자가 기존 서비스에 기여하게 되는 경우 설계 문서를 통해 해당 서비스의 목적이나 역할에 대해 쉽게 파악할 수 있습니다. 또한 서비스 설계 과정에서 구조적으로 고민했던 부분이나 엔지니어링 관점의 선택에 대한 히스토리를 쉽게 파악할 수 있기 때문에 동료 엔지니어의 경험을 자연스럽게 공유받을 수 있고, 엔지니어링 역량을 키우기 위한 기반을 쌓을 수 있습니다. 신규 마이크로서비스를 개발하는 경우에는 개발자가 서비스 설계부터 개발, 배포 과정까지 오너십을 가지고 진행하고 있습니다. 단순히 코드를 작성하는 것을 넘어, 스스로 문제에 대해 고민하고 해결 방법을 찾아가는 과정을 경험할 수 있습니다. 개발 과제를 진행하고 변경사항을 반영하기 전에는 항상 코드 리뷰 과정을 거치고 있습니다. 신입 개발자들은 리뷰 과정에서 좋은 코딩 스타일이나 효율적인 코드를 작성하는 방법에 자연스럽게 익숙해지게 됩니다. 또한 경험이 풍부한 동료들의 리뷰를 통해 아키텍처나 구조에 대해 한번 더 고민할 수 있는 기회가 되기도 합니다. ![코드 리뷰](https://tech.buzzvil.com/blog/%eb%b2%84%ec%a6%88%eb%b9%8c-%ec%8b%a0%ec%9e%85-%ea%b0%9c%eb%b0%9c%ec%9e%90%eb%8a%94-%ec%9d%b4%eb%a0%87%ea%b2%8c-%ec%84%b1%ec%9e%a5%ed%95%a9%eb%8b%88%eb%8b%a4/code-review.png) ### 개발 세미나 - 경험 공유의 장 버즈빌 개발자들은 일주일에 한 번씩 [개발 세미나](/seminar)를 통해 관심있는 주제나 진행했던 과제에 대한 경험을 공유하고 있습니다. 새로운 기술이나 깊이 알지 못했던 영역에 대한 지식을 얻거나, 동료들이 문제를 해결하는 과정에서 어떤 고민을 하고 어떤 선택을 했는지 생각해볼 수 있습니다. 또한 세미나 프리젠테이션을 진행하는 경우, 준비 과정에서 발표 주제에 대해 더 깊게 이해하게 되고, 효과적으로 정보를 전달하기 위한 방법에 대해 고민해볼 수 있는 계기가 됩니다. ### 그룹 활동 - 전문가로의 성장 발판 버즈빌에서는 같은 관심사를 가진 개발자들 간의 그룹 활동이 활발하게 이루어지고 있습니다. 신입 개발자는 자율적으로 그룹에 참여하여 동료와 함께 관심 분야에 대한 역량을 키울 수 있습니다. Developer's Edge 프로그램은 기존에 담당하는 직무를 넘어서 새로운 스킬셋을 갖추기 위한 목적으로 많은 개발자들이 참여하고 있습니다. 현재 다음과 같은 edge 팀이 운영되고 있으며 각 팀에서는 공통 관심사에 관하여 스터디를 하거나 관련 업무를 나누어서 수행하는 등 다양한 활동을 진행하고 있습니다. | 팀 | 관심 분야 | | - | - | | DevOps Edge | SRE, Cloud Native | | Data Engineering Edge | 데이터 처리 및 분석 | | Scalability Edge | 대용량 트래픽, Scalability | Technical Expert 프로그램은 각각의 구성원에게 버즈빌에서 사용하는 다양한 기술에 대한 오너십을 부여하여 전문성을 유지하기 위한 목적으로 운영되고 있습니다. 각 분야의 Expert는 맡은 기술에 대해 지속적으로 학습하고, 구성원들의 기술 수준 향상을 위한 교육을 진행하는 등의 활동을 진행합니다. 대표적으로 Python, Golang expert 그룹이 활발하게 활동하고 있으며, 각 프로그래밍 언어에 대한 Best practice를 정리하거나 프로젝트 템플릿을 관리하는 등 많은 기여를 하고 있습니다. ### 마치며 이전에 작성했던 코드를 보고 있으면 뭔가 아쉽고, 다시 작성하면 더 잘 만들 수 있을 것 같다는 생각을 하곤 합니다. 부족했던 과거에 대해 반성하면서도 한편으로는 성장하고 있다는 사실에 안도하게 되는 것 같습니다. 어떤 분야든 성장을 위해서는 꾸준히 시간을 투자하여 다양한 경험을 쌓는 것이 중요합니다. 개인적으로는 주도적으로 문제를 해결하며 배우고 느꼈던 경험을 동료와 서로 공유하는 과정에서 자신이 가장 빠르게 성장할 수 있다는 생각을 가지고 있어서, 그만큼 함께 일하는 동료를 중요하게 생각합니다. 버즈빌에서는 함께 성장할 동료들을 찾고 있습니다. [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [주니어 개발자가 만난 클린 아키텍처](https://tech.buzzvil.com/blog/how-to-design-software-at-code-levels) Date: 2021-11-29 | Author: Damon Gong | Category: Backend 안녕하세요. 버즈빌 신입 개발자 Damon입니다. 해당 게시글에서는 버즈빌의 신입 개발자가 마주한 개발 설계 과정에서의 문제점을 설명합니다. 그리고, 개발자 JD가 저를 가이드하여 설계가 개선된 경험을 공유하고자 합니다. # 좋은 설계의 중요성 **Robert C. Martin**의 **Clean Architecture**에서 "좋은 설계란 시스템을 개발하고 유지 보수하는 데 필요한 인력, 비용, 시간을 줄이는 것"이라고 정의했습니다. 시스템은 개발하는 과정도 중요하지만 개발 이후 유지 보수의 과정도 매우 중요합니다. 소프트웨어에 대한 요구사항은 계속해서 변경되고, 새로운 기능이 추가되기도 합니다. ![HW와 SW 유지보수 비용 (SE, Maintenance, Hans & Vilet, 2008)](https://tech.buzzvil.com/blog/how-to-design-software-at-code-levels/sw_hw_cost_graph.png) 위 그래프를 보면 소프트웨어의 유지보수 비용은 소프트웨어의 개발 비용과 비슷하거나 그 이상입니다. 그만큼 소프트웨어의 유지 보수는 중요합니다. ‘Clean Architecture’, ‘DDD’, ‘SOLID’ 등과 같은 설계 개념은 좋은 설계를 통해 개발 및 유지 보수 과정에서 소요되는 물리적인 비용을 줄일 수 있습니다. # SRP, OCP, DIP를 기반으로 한 설계 SOLID 원칙은 좋은 설계를 돕는 대표적인 원칙 중 하나입니다. 그중 SRP, OCP, DIP를 예시로 들어 어떻게 좋은 디자인을 할 수 있는지 소개합니다. SRP, OCP, DIP는 다음과 같습니다. - SRP(Single Responsibility Principle): 단일 책임 원칙으로, 한 클래스는 하나의 책임만 가져야 한다. - OCP(Open/Closed Principle): 소프트웨어 컴포넌트는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다. - DIP(Dependency Inversion Principle): 의존 관계를 맺을 때, 변하기 쉬운 것보다 변하기 어려운 것에 의존해야 한다. ## 현재 상황 저는 Unit/UI 테스팅 라이브러리를 개발하고 있었습니다. 당시 다음과 같은 소스 코드를 작성했습니다. - `TestRunner` 클래스는 테스트에 필요한 유틸리티 클래스를 주입받는다. - `RUN_TEST`는 인자로 전달받은 테스트 블록(block)을 수행하는 함수이다. - `TestUtil` 클래스는 `RUN_TEST`에서 테스트 시작 전과 테스트 종료 후에 수행된다. `TestUtils.kt` ```Kotlin interface TestUtils { fun runBeforeTest() fun runAfterTest() } ``` `TestRunner.kt` ```Kotlin class TestRunner( private val testUtils : List ) { fun RUN_TEST(block : () -> Unit) { testUtils.forEach(TestUtil::runBeforeTest) //RUN TEST testUtils.forEach(TestUtil::runAfterTest) } } ``` ## 시나리오 - 클라이언트는 테스트의 수행시간을 시스템 로그로 볼 수 있는 기능을 추가하고자 한다. - 다음과 같은 클래스를 추가하여 클라이언트의 요구사항을 만족시켰다. `ExecutionTimeSystemLogger.kt` ```Kotlin class ExecutionTimeSystemLogger() : TestUtil { private var startTime : Long = 0 override fun runBeforeTest() { startTime = System.nanoTime() } override fun runAfterTest() { val endTime = System.nanoTime() val executionTIme = endTime - startTime println("TIME: $executionTime") } } ``` 해당 설계는 클라이언트의 요구사항을 완벽하게 반영하고 있습니다. 하지만, 개발자 JD는 저의 코드를 보고 다음과 같은 리뷰를 남깁니다. 1. 단일 책임 원칙(SRP)를 위반한다. - 실행 시간을 계산하는 책임 - 출력하는 책임 2. 개발 폐쇄 원칙(OCP)를 위반한다. - 클라이언트가 출력을 File 시스템을 통해서도 원한다면, 기존 코드를 수정해야 한다. 3. 의존성 역전 원칙(DIP)를 위반한다. - TestRunner는 `ExecutionTimeSystemLogger`라는 구상 클래스에 의존한다. 위와 같은 문제는 소프트웨어의 변화를 매우 어렵게 만듭니다. 예를 들어, 위 코드가 실제 코드에 적용된 후, 클라이언트가 시스템 로그가 아닌 파일을 통해 로그를 남기고 싶다고 요청하는 경우에 어떻게 될까요? 아마 저는 `ExecutionTimeFileLogger`를 만들게 될 것입니다. 이 클래스는 기존의 `ExecutionTimeSystemLogger`의 많은 내용이 중복됩니다. 또한, 시간을 계산하거나, 출력하는 포맷이 변경된다면 두 클래스 모두를 수정해야 될 것입니다. 만약, 시스템, 파일 외 다른 형태로 출력하는 기능이 계속해서 추가된다면 이런 시스템을 유지 보수하는 것은 매우 어려워질 것입니다. ### 요구사항을 만족하는 유연한 소프트웨어 개발 이러한 상황을 대응하고 유연하게 소프트웨어를 설계하는 대표적인 원칙 중 하나가 'SOLID'입니다. 다음은 개발자 JD가 저를 가이드 하여 SOLID 원칙을 준수하게끔 코드를 개선한 결과입니다. `Logger.kt` ```Kotlin interface Logger { fun log(message: String) } ``` `SystemLogger.kt` ```Kotlin class SystemLogger() : Logger { override fun log(message: String) { println(message) } } ``` `ExecutionTimer.kt` ```Kotlin class ExecutionTimer( private val logger: Logger ) : TestUtil { private var startTime : Long = 0 override fun runBeforeTest() { startTime = System.nanoTime() } override fun runAfterTest() { val endTime = System.nanoTime() val executionTIme = endTime - startTime val message = "TIME: $executionTIme" logger.log(message) } } ``` 위를 통해 다음과 같은 사항이 개선되었습니다. - `ExecutionTimeSystemLogger`는 `ExecutionTimer`, `Logger`로 분리되어 각각이 할 일에 대해 책임집니다. - **각 클래스는 하나의 책임을 진다.** - `SystemLogger`는 `Logger`를 구현한다. 만약, 클라이언트가 System 로그가 아닌 File로 로그를 남기고 싶을 때는 단순히 `FileSystemLogger`를 구현하여 대응하면 됩니다. - **확장에 대해 열려있다.** - `ExecutionTimer`는 `Logger`에 의존합니다. `Logger`는 바뀔 가능성이 매우 적기 때문에 그로 인해 `ExecutionTimer`가 변경될 가능성도 적어집니다. - **변하기 쉬운 것이 아닌, 변하지 않는 것에 의존한다.** 이제 아래와 같은 클라이언트의 요구에 다음과 같이 `FileSystemLogger`를 추가하여 대응할 수 있습니다. > "File로 실행 시간을 출력하는 기능을 추가해 주세요." ```Kotlin class FileSystemLogger() : Logger { override fun log(message: String) { //File System Logging.. } } ``` 이렇게 설계하면 실행 시간을 계산하는 로직이나 출력의 포맷이 변하더라도 영향을 받지 않게 됩니다. 그리고, 하나 더 중요한 점은 `ExecutionTimeSystemLogger`가 `ExecutionTimer`와 `Logger`로 분리 덕분에 다른 곳에서도 `Logger`를 재활용할 수 있게 되었습니다. # 글을 마무리하며 버즈빌의 신입 개발자가 가이드를 통해 코드를 개선해 나아가는 과정을 설명드렸습니다. 다음엔 더 재밌는 포스팅으로 찾아오겠습니다. 추가로, 버즈빌의 신입 개발자로 채용되시면, 많은 개발자들이 코드 리뷰를 통해 어떻게 소프트웨어를 유연하게 설계할 수 있는지 자세하게 설명해 드립니다! [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [버즈빌 백엔드 기술 스택을 소개합니다](https://tech.buzzvil.com/blog/buzzvil-backend-tech-stack) Date: 2021-10-30 | Author: Liam Hwang | Category: Backend 안녕하세요, 버즈빌 DevOps 팀의 Liam입니다. 버즈빌은 리워드 기반 애드테크 플랫폼으로서 월간 2천만 이상의 오디언스에게 최적화된 광고를 노출하고 성과를 극대화하기 위해 노력하고 있습니다. 최적화된 광고 서빙을 위해 다양한 데이터 소스로부터 수집된 오디언스의 행동 데이터를 바탕으로 오디언스가 더 관심을 가질 광고를 추천하며, 보상을 활용하여 최종적인 구매까지 도달하기 위한 여정의 허들을 낮추고자 노력하고 있습니다. 버즈빌에서는 시간이 지남에 따라 다뤄야 할 트래픽과 데이터의 양과 속도가 매우 빠르게 늘어났습니다. 이를 더 유연하고 효율적으로 구현하기 위해 수년에 걸쳐 기존의 모노리식 서비스를 수십 개의 마이크로서비스로 분리해 왔습니다. 버즈빌에서는 이러한 복잡한 광고 시스템을 운영하기 위해 매우 다양한 기술 스택을 활용하고 있으며 [스택쉐어](https://stackshare.io/buzzvil/buzzvil)에도 공유되어 있습니다. 하지만 활용 중인 기술 스택의 종류가 매우 다양하고 각 기술이 회사에서 실제로 어떻게 사용되고 있는지에 관해서 확인하는 데에는 한계가 있다고 생각합니다. 이번 글에서는 버즈빌에서는 이런 복잡하고 대용량의 트래픽을 처리하는 광고 시스템을 운영하기 위해 어떤 백엔드 기술 스택을 활용하고 있는지 간략하게 소개해 드리고자 합니다. ## 프로그래밍 언어 백엔드 마이크로서비스는 대부분 Python 또는 Go로 작성되고 있습니다. 내부 서비스 카탈로그에는 2021년 10월을 기준으로 45개의 마이크로서비스가 작성되어 있습니다(백엔드 서비스만 포함). 언어별로는 Python으로 작성된 서비스는 21개, Go는 16개, Node.js 7개 등이 있습니다. 제품팀 내에서 주로 사용되는 Python과 Go의 채택률은 엇비슷한 편이지만 마이크로서비스 아키텍처를 중심으로 하는 개발 문화가 보편화한 이후에는 Go 언어로 작성된 gRPC 기반의 서비스가 근소한 차이로 더 활발히 생성되고 있습니다. ![언어, 프로토콜별 마이크로서비스 개수를 세어보았습니다. Python, Go로 작성된 마이크로서비스가 대다수를 차지하고 있습니다.](https://tech.buzzvil.com/blog/buzzvil-backend-tech-stack/language-protocol.png) Python은 버즈빌이 가장 오랫동안 주력으로 채택해오고 있는 언어이고, 프레임워크는 Django를 주로 활용하고 있습니다. Django는 훌륭한 ORM이 제공되며 다양한 모델을 중심으로 한 비즈니스 로직 작성이 용이하여 사내에서 여전히 인기가 많은 프레임워크입니다. 마이크로서비스의 경우 특정 도메인 로직에 집중하고 Django가 제공하는 다양한 기능들이 필요 없는 경우가 많기 때문에 비교적 가벼운 Flask나 FastAPI를 활용하기도 하며, 최근에는 gRPC 기반의 Python 서비스가 더 많이 작성되고 있습니다. 버즈빌에서는 기존에 광고 할당을 포함한 대부분의 API를 Django 기반의 Python 웹서버에서 서빙하고 있었지만, 광고 트래픽이 늘어나면서 인프라 이용량이 많이 증가하였습니다. 그중 가장 큰 비중을 차지하는 광고 및 콘텐츠 할당 로직을 Go로 포팅하여 큰 폭의 성능 개선을 끌어내었습니다([Go 서버 개발하기](/blog/tech-blog-go-서버-개발하기/)). 그 이후로 고성능 API가 필요한 경우 Go를 많이 활용하고 있습니다. Go는 단순함을 추구하는 버즈빌의 개발 철학과도 잘 맞으며, 고루틴을 통한 동시성 프로그래밍에 대한 지원이 뛰어나 매우 인기가 많은 선택지입니다. 프로젝트별로 언어를 선택하는 절대적인 기준은 없습니다만, 성능과 리소스 사용에 대한 고려가 더 많이 필요한 경우 Go를 권장하고 있습니다. 언어별로 해당 언어를 선호하는 개발자들이 모여 전문가 그룹을 구성하여, 추천하는 라이브러리나 프로젝트 구조 등에 대한 best practice를 고민하고 스타일 가이드나 빌드 파이프라인 등을 개선하기 위해 노력합니다. 마이크로서비스를 지향하다 보니 신규 서비스 개발이 잦은 빈도로 발생하였고 서비스를 처음부터 배포까지 빠르게 완료할 수 있도록 서비스 템플릿을 유지보수하고 있습니다. 이 템플릿에는 각 언어별로 권장되는 구성을 반영하여 해당 언어에 대해 이해도가 낮은 구성원이라도 손쉽게 프로젝트를 구성할 수 있도록 돕고 있습니다. 템플릿 프로젝트는 Cookiecutter라는 프로젝트 템플릿 도구를 활용하며, 추후 템플릿으로부터 손쉽게 프로젝트를 생성할 수 있도록 [Backstage와도 연동할 계획](https://backstage.io/docs/features/software-templates/software-templates-index)이 있습니다. ## IDL 저장소 마이크로서비스 간 통신을 위해 gRPC를 활용하기로 했는데, 단순히 성능적인 이득을 얻기 위함만은 아니었습니다. 마이크로서비스 아키텍처의 청사진을 그릴 때부터 수많은 마이크로서비스가 생성될 것이 예상되었고, 매번 API 문서를 작성하는 것으로는 변화무쌍함을 감당하기 어려울 것이라 생각했습니다. gRPC는 기본적으로 protocol buffer라는 인터페이스 정의 언어(Interface Definition Language, IDL)을 지원하고 있습니다. 새로 생성할 서비스의 인터페이스와 각 RPC의 메시지 명세를 protobuf로 정의해서 IDL 저장소에 PR을 생성합니다. 이 PR에서 신규 기능을 제공하는 팀과 사용하는 팀이 함께 리뷰하면서 회의나 디자인 문서에서 합의된 내용이 잘 반영되었는지 검증합니다. API 정의는 시간이 지나면서 변하기 마련인데, [Buf](https://buf.build/)라는 도구를 활용하여 새로운 변경이 하위 호환성을 깨뜨리진 않는지 등을 검증하고 있습니다. IDL 저장소의 CI는 새로 제안된 API 정의가 머지되면 각 언어별 codegen을 통해 클라이언트 및 서버 코드를 생성하며 언어별 패키지 저장소에 푸시합니다. Go의 경우 Go Module로 패키징하여 GitHub에 태그를 직접 푸시하며, PyPI, NPM 패키지 등은 사내 패키지 저장소에 배포합니다. gRPC를 활용하기 어려운 환경이나 레거시 클라이언트를 위해 gRPC를 JSON API로 transcoding하는 설정을 추가하고 있는데, 이때 필요한 proto descriptor set 또한 생성하여 S3로 업로드 합니다([gRPC를 쓰면 REST가 공짜!?]( ) 참조). ## 데이터베이스 데이터베이스로 MySQL(RDS), DynamoDB, Elasticsearch, Redis(Elasticache) 등을 주로 활용하고 있습니다. 각 마이크로서비스는 각자의 자체적으로 해당 도메인의 데이터를 소유하기 위해 중앙 데이터베이스를 공유하지 않고, 각 마이크로서비스가 직접 데이터베이스를 관리하는 것을 원칙으로 하고 있습니다. 대부분의 마이크로서비스에서 특별한 요구사항이 없다면 관계형 데이터베이스인 RDS for MySQL을 주로 사용하고 있습니다. 모니터링에는 [pmm-server](https://github.com/percona/pmm-server)와 CloudWatch를 활용하고 있고, 무중단 스키마 변경을 위해 GitHub에서 개발된 [gh-ost](https://github.com/github/gh-ost)를 활용하고 있습니다. 광고 최대 게재빈도 제어, 리워드 발급 등 높은 읽기/쓰기 성능이 필요하거나 큰 규모로 확장이 예상되는 워크로드의 경우 완전 관리형 데이터베이스인 DynamoDB를 적극적으로 활용하고 있습니다. 프로비저닝 된 읽기/쓰기 용량(provisioned read/write capacity), 쿼리 쓰로틀링 여부 등만 잘 모니터링해주면 확장 축소가 용이하고 운영 코스트가 최소화되기 때문에 다양한 사용처에서 활용되고 있습니다. 사용자의 광고 요청에 대해 수많은 타게팅 조건에 부합하는 후보 광고 목록을 빠르고 효율적으로 선택하기 위해 Elasticsearch를 활용하고 있으며, 캐싱이나 실시간 통계 제공을 위해 Redis를 활용하고 있습니다. ## 서비스 프로비저닝 AWS Lambda로 구성된 일부 엔드포인트를 제외하면 대부분의 서비스가 Kubernetes 클러스터에 배포되고 있습니다. 따라서 각 서비스가 내부적으로 어떤 기술을 사용하고 있는지 크게 상관하지 않고 컨테이너 기반으로 빌드만 되면 배포가 가능한 상태가 되는 것을 지향하고 있습니다. 이를 위해 Kubernetes에 배포되는 리소스의 manifest를 일관적으로 관리하기 위해 권장 설정이 포함된 Helm chart 템플릿을 관리하고, Kubernetes로의 배포를 위해 개발자가 챙겨야 하는 중요한 설정(Graceful shutdown, readiness/liveness probe 등)에 대한 가이드를 하고 있습니다. Helm chart는 IDL 저장소와 유사하게 전사 차트를 모노리포 형태로 단일 저장소에서 관리하고 있으며, 각 서비스 개발자는 템플릿 차트를 기반으로 애플리케이션의 요구사항에 맞게 차트를 작성하면 DevOps 엔지니어가 리뷰하는 프로세스를 가지고 있습니다. ![Spinnaker Pipeline](https://tech.buzzvil.com/blog/buzzvil-backend-tech-stack/spinnaker.png) 배포는 인하우스 [Spinnaker](https://spinnaker.io/)를 이용해 팀별로 다양한 배포 방식을 설정하고 있으며, Managed Pipeline Template 기능을 활용해 환경별 배포 파이프라인을 구성, 관리하는 부담을 덜고 있습니다. ![ChatOps 기반 배포](https://tech.buzzvil.com/blog/buzzvil-backend-tech-stack/chatops-deployment.png) 배포의 트리거는 ChatOps를 활용합니다. [botkit](https://github.com/howdyai/botkit) 기반으로 개발된 내부 챗봇에 배포 트리거를 요청하면 배포된 변경사항에 대한 요약과 배포 상태가 슬랙을 통해 리포팅 됩니다. ChatOps를 활용하면 배포에 대한 가시성이 확보되고, 배포 슬랙 채널을 살펴보는 것만으로도 배포 과정에 대한 온보딩이 자연스럽게 이뤄진다는 장점이 있습니다. ## 서비스 메쉬 서비스 메쉬로 Istio를 활용하고 있으며, 이는 마이크로서비스 아키텍처를 처음 도입하는 시점부터 활용하기로 하였는데요. 마이크로서비스 간 커뮤니케이션을 통제하는 복잡한 클라이언트 로직을 최대한 인프라 레벨로 내리고 서비스 레벨에서는 핵심 비즈니스 로직에만 신경 쓸 수 있게 하기 위해서였습니다. 특히 HTTP/2에 기반한 gRPC를 적극적으로 활용하기로 했지만 Kubernetes의 Service는 영구 연결(long-lived connection)에 대한 부하 분산에 대한 지원이 미비하기 때문에([참고](https://learnk8s.io/kubernetes-long-lived-connections)) 클라이언트 코드에서 부하 분산을 처리하지 않고 그에 대한 고민을 Service Mesh로 넘길 수 있다는 장점이 매우 크게 작용했습니다. 그 외에도 [gRPC-JSON 트랜스코더](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter), [트래픽 전환(shifting)](https://istio.io/latest/docs/tasks/traffic-management/traffic-shifting/), [미러링](https://istio.io/latest/docs/tasks/traffic-management/mirroring/) 등의 고급 트래픽 제어 기능을 활용할 수 있기 때문에 신규 기능을 테스트하거나 Django 모노리스의 일부 기능을 새로 작성한 마이크로서비스로 전환할 때 유용합니다. 신규 마이크로서비스로 기존 기능을 옮기고 나면, 기존 트래픽을 미러링하여 양쪽 서비스가 동일한 결과를 만들어내는지 검증하거나 기존 트래픽의 부하에 대응이 가능한지 확인합니다. 그리고 트래픽 전환을 활용해 트래픽의 일부 비율을 점진적으로 신규 마이크로서비스로 유입 시켜 최종적으로 이전을 완료하고 있습니다. ## 관측성 스택 ![Grafana, Prometheus, Loki 기반의 관측성 스택](https://tech.buzzvil.com/blog/buzzvil-backend-tech-stack/grafana.png) 버즈빌은 관측성 스택으로 Datadog과 Prometheus 및 Grafana를 활용하고 있습니다. Kubernetes 환경을 구축하면서부터 Prometheus와 Grafana를 적극적으로 활용해오고 있지만, 전체 인프라 중 Kubernetes로 운영하는 워크로드 비율이 높아지면서 Prometheus가 처리하는 메트릭의 label cardinality가 너무 높아져서 메모리 사용량에 대한 관리가 매우 어려졌으며 OOM Kill 되는 현상이 빈번하게 발생하였습니다. 이를 해결하기 위해 [Thanos](https://thanos.io/)를 기반으로 Prometheus HA 구성을 하고, 메트릭을 오브젝트 스토리지(S3)에 저장하고 오래된 시계열 데이터는 compaction을 통해 압축 및 다운 샘플링을 하게 되었습니다. 하지만 여전히 관측성 스택을 인하우스에서 관리하는 부담이 매우 높았고, 마이크로서비스가 추가되면서 서비스 간 호출에 대한 가시성, 즉 분산 트레이싱에 대한 요구사항이 증가하였습니다. Istio와 기존에 활용 중이던 Grafana와도 궁합이 잘 맞는 Jaeger도 고려했지만, 직접 운영하는 부담을 줄이기 위해 관리형 모니터링 솔루션인 Datadog을 전면 도입하였습니다. 특히 Datadog APM(Application Performance Monitoring)을 적극적으로 활용하면서 마이크로서비스 호출 간 병목 파악, SLO 메트릭 관리, 이상 감지 등의 모니터링이 매우 수월해졌습니다. 그 외에도 장애 관리를 위한 Incident Management, Pagerduty 통합 기능 등을 활용하고 있습니다. ## 로깅 버즈빌은 로그를 광범위하게 활용하고 있습니다. 서비스 가시성 및 디버깅을 위한 로그에서부터 핵심 비즈니스 메트릭 수집까지 다양한 로그가 존재하며 매일 1TB 이상의 로그가 생성됩니다. 각 서비스는 다양한 지표를 로그 스트림으로 출력하고 있고, Kubernetes에 배포된 서비스들을 위해 로그를 포워딩하는 몇 가지 메커니즘을 운용하고 있습니다. Kubernetes를 적극적으로 활용하기 이전에 구성된 서비스들은 fluentd를 사이드카 컨테이너 형태로 파드에 함께 배치하여 S3나 Kinesis Data Streams 등으로 로그를 포워딩하고 이를 Data lake에 적재하고 있습니다. Kubernetes 도입 이후 생성된 서비스는 [Kubernetes Logging Architecture](https://kubernetes.io/docs/concepts/cluster-administration/logging/)에 제안된 것처럼 stdout, stderr로 모든 로그를 출력하고, 각 노드에 배포된 fluent-bit이 fluentd deployment로 로그를 포워딩하여 최종적으로 S3에 로그를 적재하고 있습니다. fluent-bit은 로그 스트림 중 JSON structured log line이 있다면 예약된 type 키에 기반하여 Data lake의 약속된 경로로 라우팅하고 있습니다. 그 외에 수일 이내의 단기 로그 저장 및 조회를 위해 Loki stack을 운영하고 있으며 주로 서비스 디버깅, 트러블슈팅 용도로 활용하고 있습니다. ## 마치며 버즈빌의 백엔드를 구성하는 기술 스택에 대해 간단하게 살펴보았습니다. 복잡한 광고 시스템을 도메인 기반의 마이크로서비스 아키텍처로 풀어나가면서 직면한 여러 문제를 해결해 왔습니다. 이번 포스팅에서는 전반적인 기술 스택에 대해 소개해드렸지만, 기회가 된다면 각 영역에 대해 디테일한 예시와 깊이 있는 소개를 해드리도록 하겠습니다. 버즈빌에는 여전히 해결해야 하는 문제들이 산적해 있고, 이런 고민들을 함께 해나갈 수 있는 동료를 찾고 있습니다. 관심이 있으시다면 편하게 티타임을 요청 주세요! [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [글머리 기호(Bullet point) 중독 현상](https://tech.buzzvil.com/blog/bullet-point-addiction) Date: 2021-10-27 | Author: Whale Lee | Category: Product > **TL;DR** 무분별하게 사용된 글머리 기호(Bullet point)는 건강에 해롭습니다. 요새 들어서 글머리 기호를 과도하게 사용하는 글이 많이 보입니다. 자세한 설명이 필요할 때도 보이고, 비교가 필요할 때도 보이고, 심지어 문장 한 줄을 표현할 때 글머리 기호를 사용하는 경우도 보입니다. 혹시 글을 작성할 때 글머리 기호를 아래처럼 사용하고 계시지는 않나요? 다음 예시를 봤을 때 불편함이 느껴지지 않는다면 이 글을 더 읽지 않아도 괜찮습니다. ![Bad example](https://tech.buzzvil.com/blog/bullet-point-addiction/bad-example.png) ## 글머리 기호 작성 가이드 글머리 기호는 잘 사용하면 정보와 논지를 전달하는데 좋은 도구지만, 잘못 사용하면 오히려 글을 이해하기 어렵게 만들기도 합니다. 글머리 기호를 사용할 때 몇 가지 규칙만 지켜도 예쁘고 읽기 좋은 글이 됩니다. 다음은 여러 아티클에서 공통으로 추천하는 글머리 기호 구성 방법입니다. - 서로 관련 있는 아이템으로 구성한다. - 각 아이템은 짧게 구성한다. - 모든 아이템은 비슷한 형식으로 표현한다. - 모든 아이템은 병렬적으로 나열한다. - 완벽한 문장이 되는 경우에만 마침표를 찍는다. 각 아이템에 부연 설명이 필요할 경우 다음과 같은 형태로 표현할 수도 있습니다. - **단순하게 구성한다.** 불필요한 복잡성은 줄이고, 가능하면 하위 불릿 사용은 피한다. - **과한 사용을 피한다.** 글이 쇼핑 리스트처럼 보이지 않도록 주의한다. 막연히 글머리 기호를 줄이려고 시도하기에는 조금 어려울 수도 있습니다. '한 문장짜리 문단을 구성해도 괜찮을까?', '불필요한 서술을 추가하여 핵심 내용을 전달하기 어렵게 만드는 것은 아닐까?' 이런저런 걱정이 들기도 하죠. 억지로 문단을 구성하려다가 비슷한 문장을 여러 번 사용하게 되기도 합니다. 아래 대안을 활용하면 핵심을 명확하게 전달하면서 무분별한 글머리 기호 사용을 줄일 수 있습니다. | 대안 | 설명 | 장점 | 단점 | | - | - | - | - | | 두괄식 문단 | 자세한 설명이 필요할 때 활용한다. | 핵심 문장과 부연 설명이 구분되고 정보가 잘 전달된다. | 좋은 문단을 구성하는 일은 어렵다. | | 테이블 | 비교 항목들이 여러 개 있을 때 활용한다. | 항목별로 정보 전달이 수월하다. | 내용이 길어지면 오히려 읽기가 어려워진다. | | 숫자 리스트 | 순서가 있거나 아이템을 지칭할 때 활용한다. | 순서를 명확하게 전달할 수 있다. | 글머리 기호와 마찬가지로 단순하게 구성해야 한다. | ### 마무리 아마존은 미팅에서 파워포인트를 없애고 6 페이저(6-Pager, i.e 6-page narrative)를 도입했습니다. 제프 베조스는 사람의 뇌가 하드 데이터로 도배된 글머리 기호보다 서사를 더 잘 처리하기 때문에 이런 방식을 도입했다고 합니다. 이런 6 페이저를 구성할 때조차도 글머리 기호는 목표(Goals) 섹션을 포함해서 단 두 군데에서만 허용된다고 하네요. 옛날 대기업에서는 정보 전달의 효율을 높이고자 '음' 체(e.g., -되었음, -했음)를 사용하도록 강제했다고 합니다. 이 얘기를 들었던 학창 시절에는 '뭐 이런 걸 다 강제하나' 싶었는데, 그 의도 자체는 6 페이저와 마찬가지로 '효율적이고 정확하게 정보를 전달하고 이를 바탕으로 올바른 의사 결정을 내리기 위한 노력'으로 보입니다. 여전히 '음'체 사용을 강제하는 것은 반대하지만, 더 좋은 커뮤니케이션을 위한 방법은 꾸준히 고민해볼 가치가 있다고 생각합니다. 노션, 컨플루언스 등에서 과도한 글머리 기호에 노출되어 중독된 모든 분과 함께 이런 중독 현상을 개선해보고자 제 생각을 짧게 적어 보았습니다. 저를 포함한 모든 중독자 여러분, 화이팅입니다 😉 ### 참고 자료 - [How to Write Powerful Bullet Points](https://www.grammarly.com/blog/bullet-points/) - [The Anatomy of an Amazon 6-pager](https://writingcooperative.com/the-anatomy-of-an-amazon-6-pager-fc79f31a41c9) - [Best Practices for Bullet Points](https://www.businesswritingblog.com/business_writing/2005/12/the_best_of_bul.html) [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) --- ## [버즈빌 개발문화를 소개합니다.](https://tech.buzzvil.com/blog/hr-blog-develop-culture) Date: 2021-09-30 | Author: James Kang | Category: Culture 안녕하세요, 버즈빌 EX팀 James입니다. 버즈빌에 지원하시는 후보자분 중에서, 조직 문화뿐만 아니라 개발 문화를 궁금해하는 분들도 많은데요. 버즈빌리언들의 실제 목소리를 그대로 전달 드리고자, 개발자 대상 설문을 진행했습니다. 앞으로 개발 문화뿐만 아니라 기술 스택, 일하는 방법, 기술 로드맵 등 다양한 이야기를 공유하며 소통하도록 하겠습니다. 기대해 주세요! ### 첫 번째, 믿을 수 있고 뛰어난 동료 개발자! 🤲 스타트업에서 일하는 개발자에게 가장 중요한 가치는 아마도 '**빠른 성장**'일 것입니다. 이러한 성장을 가능하게 하는 비결은 무엇일까요? '**믿을 수 있고 뛰어난 동료**'라고 볼 수 있는데요. 넷플릭스에서는 높은 인재 밀도를 유지하는 것이 자신들의 경쟁력이라고 밝힌 바 있습니다. 미국 샌타모니카 연구소에서 진행 된 실험에서도 우수 개발자와 그렇지 않은 개발자의 성과가 적게는 10배에서 많게는 20배까지 차이난다는 결과가 존재합니다. 뛰어난 인재가 곧 경쟁력이자, 성장의 원천입니다 버즈빌이 2021년 실시한 내부 개발자 대상 설문조사에서 가장 많이 언급된 단어 역시 성장과 동료입니다. 버즈빌에 다니는 이유로 가장 중요한 요소가 ‘금전적인 보상'이나 '복지'가 아닌 '**같이 일하는 동료**'라는 사실이 이를 뒷받침하는데요. 버즈빌은 까다로운 인터뷰 과정을 통해 높은 인재 밀도를 유지하고 있으며, 개발자들의 빠른 성장을 위해 끊임없이 노력하고 있습니다. ![](https://tech.buzzvil.com/blog/hr-blog-develop-culture/images/pic_0.png) “*같이 일할 수 있는 좋은 개발자가 많아서 개인이 성장할 기회가 많은 것 같다.*” “*어느 기업을 가도 좋은 개발자, 잘하는 개발자는 많겠지만, 이렇게 밀도가 높은 곳을 찾긴 어렵다.*” “*동료 개발자들 중에서 같이 일하기 어려운 분들은 거의 없고 서로에게 좋은 영향을 주고 받으면서 성장해 나갈 수 있다.*” ### 두 번째, 대용량 트래픽과 최신기술을 경험할 수 있는 개발 환경 📈 많은 개발자들이 중요하게 생각하는 것이 ‘**다른 누군가에게 영향을 미치는 것**’인데요. 버즈빌 개발 환경은 B2B 플랫폼으로서 B2C 서비스와는 또 다른 매력을 갖고 있습니다. 특히 디지털 광고의 특성상 광고주와 퍼블리셔 양측으로부터 수많은 트래픽이 발생하는데, 이러한 대용량 트래픽을 짧은 시간 안에 소화할 수 있는 시스템을 구축하고 관리하는 경험은 개발자들이 빠르게 성장할 수 있는 발판이 됩니다. 도메인 주도 설계(DDD, Domain-Driven Design) / Clean architecture의 적극적인 도입뿐만 아니라 안정적 DevOps를 통한 효과적인 인프라 운영도 버즈빌 개발 문화의 매력 중 하나입니다. 버즈빌은 2년 전 Kubernetes의 국내 인지도가 낮을 때부터 적용을 시작했고, 현재 전체 서비스의 95% 이상을 모두 Kubernetes에서 운영하며 인프라를 최적화하고 있습니다. 새로운 기술을 도입해볼 기회는 늘 열려있죠! ![](https://tech.buzzvil.com/blog/hr-blog-develop-culture/images/pic_1.png) “*다양한 인프라 기술들을 접목시켜 대용량 트래픽을 다룰 수 있는 환경을 운영하고 개선할 수 있다.*” ”*Microservice 기반의 아키텍처. DDD/Clean architecture 적극적인 도입. kubernetes, AWS, golang, python 등 최신 기술 사용이 강점이다.*” “*새로운 기술들을 도입하는 데 큰 어려움이 없고, 최신 기술 스택들을 많이 사용해서 배울 것들이 넘쳐난다.*” ### 세 번째, 잡플래닛 4.3의 자율적이고 수평적 조직문화! 👍 버즈빌은 개발자들이 성장할 수 있도록 지원하면서도, 전사적으로 일하기 좋은 문화를 만들어가기 위해서 노력합니다. 특히, 지시를 통해서 일하는 것이 아닌, 회사의 방향과 맥락 공유를 통해서 **자율적이고 책임감 있게 업무를 수행**하고 있습니다. 또한 수평적인 문화 위에서 합리적으로 일할 수 있도록 업무 프로세스를 회고하고 발전시킵니다. 최근에는 좀 더 목적 지향적으로 움직이고자 조직 구조를 개편하고, 일하는 방식을 만드는 등 버즈빌과 버즈빌리언이 함께 성장하기 위한 다양한 시도를 해나가고 있습니다. ![](https://tech.buzzvil.com/blog/hr-blog-develop-culture/images/pic_2.png) “*수평적이고 서로 존중하는 조직문화와 높은 생산성 및 퀄리티를 지향하는 개발문화를 갖고 있다.*” “*조직문화가 유연하며 불필요한 서류작업 및 절차가 없다.*” “*수평적이고 자율적이다. 성장이라는 키워드가 중요하게 여겨지는 문화를 유지하고 있다.*” ### 더 나은 개발 문화를 위해서! 🔥 #함께 성장하기 이번 개발자 대상 설문에서 ‘개발자로서 느끼는 만족도’ 그리고 ‘다른 친구에게 얼마나 추천할 수 있는지’를 물었을 때, 4점 이상 응답이 각각 82.1%와 75%로 높게 나왔습니다. 전반적으로 긍정적인 응답이었지만, 만족하지 못한 분들도 분명 존재합니다. 특히, 구글과 같은 거대 테크 기업과 비교하면 아직 해야할 일도 많고, 최근 목적 지향적으로 움직이는 과정에서 쌓이게 되는 기술 부채를 비롯하여 풀어야 할 문제들도 다수 언급되었습니다. 그럼에도 불구하고, 버즈빌은 꾸준히 회고하며 개선하며 앞으로 나아가고자 합니다. 개발자로서 폭발적으로 성장하며 더 나은 개발문화를 함께 만들어보고 싶은 분들은 주저하지 말고 버즈빌에 지원해 주세요! [버즈빌 개발자 지원하기 (클릭)](https://buzzvil.career.greetinghr.com/) [버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)](https://docs.google.com/forms/d/e/1FAIpQLSdu-AHgTmGGfNSmr0bwEO4ubvnVFzHsZ7Dq-b4_Y7mghB772Q/viewform) ![](https://tech.buzzvil.com/blog/hr-blog-develop-culture/images/pic_3.png) --- ## [setup.py 멈춰!](https://tech.buzzvil.com/blog/setup.py-멈춰) Date: 2021-09-13 | Author: Isac Yoo | Category: Backend 파이썬 코드를 다른 사람도 사용할 수 있게 공유하거나, 불필요한 파일들을 제거하고 프로젝트를 배포하려면 패키징을 하는 것이 가장 효율적입니다. 파이썬은 나름 긴 역사와 함께 패키징 또한 여러 방식을 거쳐 발전하고 있습니다. 이 블로그에서는 파이선 패키징의 문제점 2개를 설명하고, 작성 시점에서 가장 권장되고 표준인 패키징 방식을 설명하려고 합니다.
## 패키징에 어떤 역사가 있었을까? 제가 알고 있던 역사를 정리하려고 찾아보니 좋은 글이 있어서 그 글을 소개하는 것으로 대체하려고 합니다. https://ryanking13.github.io/2021/07/11/python-packaging.html
## 나는 지금 legacy를 사용하는 건가? 다음 사항에 포함된다면 legacy를 사용하고 있다고 볼 수 있습니다. - setup.py 존재 - 최종 패키징을 python setup.py ...로 시작되는 커맨드 사용 중 Poetry를 사용하고 있다면 legacy가 아닙니다. 다만 아래 부록을 참고하면 배포를 좀 더 쉽게 만들 수 있습니다.
# 문제 1: setup.py가 없으면 패키징이 안된다. ## setup.py는 뭐가 문제인가? ### 대체재가 없다 파이썬에서 패키지 포맷은 Binary Distribution인 wheel과 Source Distribution으로 이미 표준이 있습니다. 파일 이름과 위치에 맞춰서 넣기만 하면 setuptools나 Poetry로 패키징한 패키지와 동일하게 설치할 수 있습니다. 하지만 패키징 자체는 표준이 없고 대신 setuptools와 setup.py가 사실상 표준으로 사용되고 있었고, setuptools는 third party library입니다. setup.py를 읽기 위해서는 setuptools 패키지가 필요하기 때문에 무슨 패키징 툴을 개발하든 setuptools를 의존해야 합니다. ### build-time dependency 관리는 누가? runtime dependency는 패키지에 적혀있기 때문에 pip이 알아서 같이 설치를 해줍니다. 만약 패키징할 때 setup.py에서 특정 라이브러리를 import 해서 버전 관리를 따로 한다면? 해당 패키지 설치 없이 `python setup.py`는 당연히 `ImportError`가 발생할 것이고, 파일을 열어 dependency를 확인한 후 직접 설치해야 합니다.
## 해결방법: 스크립트 대신 설정 파일 setup.py 스크립트를 사용했을 때 코드 실행 때문에 위와 같은 의존성 문제가 발생한다면, 스크립트를 없애자! ### `pyproject.toml` [PEP-518](https://www.python.org/dev/peps/pep-0518/) 에서 build-time dependency를 선언적으로 관리하기 위해 추가된 파일. 최근에는 용도가 확장돼서 pytest, mypy, 다양한 linter 등 다양한 툴들의 설정 파일과 project metadata를 저장하는 용도로 사용되고 있습니다. ### 예시 #### setuptools 를 사용해서 wheel 패키지를 만들 때 ```toml [build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" ``` #### poetry를 사용할 때 ```toml [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ``` --- 위의 예시 내용이 `pyproject.toml`에 적혀있고 [PEP-517](https://www.python.org/dev/peps/pep-0517/) 를 만족하는 패키징 툴이 있다면, setuptools나 poetry가 없는 환경에서도 필요한 build system과 dependency들을 설치하고 패키징하는 것을 자동화할 수 있습니다. 찾아보기론 현재 PEP-517 만족하는 툴은 [build](https://pypi.org/project/build/) 와 [pep517](https://pypi.org/project/pep517/) 인데, 후자는 deprecate 되었고 전자를 대부분 사용하는 것 같습니다. Poetry 또한 PEP-517을 지원하는데, 테스트해보니 backend가 poetry인 경우만 동작하는 것으로 보입니다. [build](https://pypi.org/project/build/) 의 경우 아래의 두 줄로 패키징할 수 있습니다. (poetry 또는 setuptools 등 backend의 사전 설치 불필요) ```shell pip install build python -m build . ```
# 문제 2: 메타데이터에 대한 표준이 없다. ## 패키지 메타데이터는? 위의 설정만으로 패키징이 끝나는 것은 아닙니다. 필수 메타데이터인 패키지 이름, 버전, dependency 이외에도 파이썬 패키지에는 readme, tag, entrypoint 등 다양한 메타데이터를 심을 수 있습니다. 이 부분 또한 표준화되어있지만, 아쉽게도 비교적 최근에 표준화되어서 ([PEP-621](https://www.python.org/dev/peps/pep-0621/): 2021/3/1에 [Final로 변경](https://github.com/python/peps/pull/1857)) 아직은 주요 빌드툴에서 지원되지 않습니다. | build system | status | link | |:------------: |:------: |---------------------------------------------------------------------------------- | | setuptools | 논의 중 | https://github.com/pypa/setuptools/issues/1688 | | Poetry | 논의 중 | https://github.com/python-poetry/poetry/issues/3332 | | Flit | 지원 | https://flit.readthedocs.io/en/latest/pyproject_toml.html#pyproject-toml-project | 그 때문에 각 빌드툴 specific한 포맷을 따라서 메타데이터를 작성해야 합니다. (setuptools - `setup.cfg`, Poetry - `pyproject.toml`의 `[tool.poetry]`) 아래는 각 독자 규격 샘플과 문서를 나열했고, 마지막은 미래에 모든 빌드툴 공통으로 쓰일 표준 포맷 샘플을 작성했습니다. ### setuptools 공식 문서: https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html#declarative-config `setup.py`에 있던 모든 필드를 ini 포맷의 `setup.cfg`에 선언적인 방식으로 기록합니다. ```ini [metadata] name = my_package version = attr: src.VERSION description = My package description long_description = file: README.rst, CHANGELOG.rst, LICENSE.rst keywords = one, two license = BSD 3-Clause License classifiers = Framework :: Django License :: OSI Approved :: BSD License Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 [options] zip_safe = False include_package_data = True packages = find: scripts = bin/first.py bin/second.py install_requires = requests importlib; python_version == "2.6" [options.package_data] * = *.txt, *.rst hello = *.msg [options.entry_points] console_scripts = executable-name = package.module:function [options.extras_require] pdf = ReportLab>=1.2; RXP rest = docutils>=0.3; pack ==1.1, ==1.3 [options.packages.find] exclude = src.subpackage1 src.subpackage2 [options.data_files] /etc/my_package = site.d/00_default.conf host.d/00_default.conf data = data/img/logo.png, data/svg/icon.svg fonts = data/fonts/*.ttf, data/fonts/*.otf ``` ### Poetry 공식 문서: https://python-poetry.org/docs/pyproject/ setuptools와 다르게 최근에 개발됐기 때문에 처음부터 `setup.py`대신 `pyproject.toml`을 사용했습니다. 다만 PEP-621가 accept 되기 전에 개발됐기 때문에 `[project]`대신 `[tool.poetry]`에 PEP-621과는 약간 다른 독자 포맷으로 기록합니다. ```toml [tool.poetry] name = "poetry" version = "1.2.0a2" description = "Python dependency management and packaging made easy." authors = [ "Sébastien Eustace " ] license = "MIT" readme = "README.md" include = [ { path = "tests", format = "sdist" } ] homepage = "https://python-poetry.org/" repository = "https://github.com/python-poetry/poetry" documentation = "https://python-poetry.org/docs" keywords = ["packaging", "dependency", "poetry"] classifiers = [ "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules" ] [tool.poetry.build] generate-setup-file = false # Requirements [tool.poetry.dependencies] python = "^3.6" poetry-core = "^1.1.0a6" cleo = "^1.0.0a4" crashtest = "^0.3.0" requests = "^2.18" cachy = "^0.3.0" requests-toolbelt = "^0.9.1" cachecontrol = { version = "^0.12.4", extras = ["filecache"] } pkginfo = "^1.5" html5lib = "^1.0" shellingham = "^1.1" tomlkit = ">=0.7.0,<1.0.0" pexpect = "^4.7.0" packaging = "^20.4" # temporarily clamped due to https://github.com/pypa/pip/issues/9953 virtualenv = ">=20.4.3,<20.4.5" keyring = ">=21.2.0" entrypoints = "^0.3" importlib-metadata = {version = "^1.6.0", python = "<3.8"} dataclasses = {version = "^0.8", python = "~3.6"} [tool.poetry.dev-dependencies] pytest = "^6.2" pytest-cov = "^2.8" pytest-mock = "^3.5" pre-commit = { version = "^2.6", python = "^3.6.1" } tox = "^3.0" pytest-sugar = "^0.9" httpretty = "^1.0" zipp = { version = "^3.4", python = "<3.8"} deepdiff = "^5.0" [tool.poetry.scripts] poetry = "poetry.console.application:main" ``` ### 미래: PEP-621과 Flit 공식 문서 - PEP-621: https://www.python.org/dev/peps/pep-0621/#details - Python Packaging User Guide: https://packaging.python.org/specifications/declaring-project-metadata/ - Flit: https://flit.readthedocs.io/en/latest/pyproject_toml.html#pyproject-toml-project setuptools와 Poetry가 PEP-621를 지원하면 아래 포맷으로 메타데이터를 작성하면 어떤 툴을 쓰더라도 상호 교차적으로 사용할 수 있습니다. 하지만 위의 표처럼 setuptools와 Poetry는 아직 표준을 따르도록 구현되지 않았기 때문에 아쉽게도 지금은 사용할 수 없습니다. 지금 아래의 포맷으로 패키징을 하고싶다면 유일한 선택지는 [Flit](https://flit.readthedocs.io/en/latest/pyproject_toml.html#pyproject-toml-project) 입니다. ```toml [project] name = "spam" version = "2020.0.0" description = "Lovely Spam! Wonderful Spam!" readme = "README.rst" requires-python = ">=3.8" license = {file = "LICENSE.txt"} keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"] authors = [ {email = "hi@pradyunsg.me"}, {name = "Tzu-Ping Chung"} ] maintainers = [ {name = "Brett Cannon", email = "brett@python.org"} ] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python" ] dependencies = [ "httpx", "gidgethub[httpx]>4.0.0", "django>2.1; os_name != 'nt'", "django>2.0; os_name == 'nt'" ] [project.optional-dependencies] test = [ "pytest < 5.0.0", "pytest-cov[all]" ] [project.urls] homepage = "example.com" documentation = "readthedocs.org" repository = "github.com" changelog = "github.com/me/spam/blob/master/CHANGELOG.md" [project.scripts] spam-cli = "spam:main_cli" [project.gui-scripts] spam-gui = "spam:main_gui" [project.entry-points."spam.magical"] tomatoes = "spam:main_tomatoes" ```
## 부록: Poetry 프로젝트에서 배포를 좀 더 쉽게하기 ### TL;DR Dockerfile이나 CI pipeline에서는 `potery` 커맨드 대신 `python -m build` 를 사용하자 --- [Poetry](https://python-poetry.org/) 를 사용하면 패키징 이외에도 virtual env 관리, dependency resolving, 패키지 업로드 등 개발에 필요한 기능들을 한 번에 해결할 수 있습니다. 다만 Dockerfile이나 CI pipeline에서 패키징을 위해 `poetry` 커맨드를 사용하려고 하면 위의 기능을 모두 설치해야 합니다. 여기서 사소한 문제는 `poetry` 패키지의 dependency가 많아서 `poetry` 패키지를 설치하는 데만 시간이 좀 걸린다는 것입니다. 특히 alpine 이미지를 사용할 경우 복잡해지는데, dependency 중에는 C 코드를 포함하는 패키지가 (cryptography) 있어서 각종 빌드 프로그램들을 설치하고 컴파일도 해줘야 합니다 (현재 alpine은 wheel 포맷을 지원하지 않기 때문). 그 때문에 로컬의 호스트 머신에서 사용할 게 아니라면 Poetry로 관리한다고 하더라도 위에 설명한 `build` 커맨드를 사용하는 것이 가장 편리합니다. 아래는 Dockerfile을 예시로 작성했습니다. #### Before ```dockerfile FROM python:3.9-alpine WORKDIR /src COPY . . RUN apk --update add gcc musl-dev openssl-dev libffi-dev cargo # 아래가 rust, c 코드 컴파일로 몇분이 소요됨 RUN pip install poetry RUN poetry build ``` #### After ```dockerfile FROM python:3.9-alpine WORKDIR /src COPY . . RUN pip install build RUN python -m build ``` --- ## [그리디 알고리즘을 이용한 중복 콘텐츠 클러스터링](https://tech.buzzvil.com/blog/그리디-알고리즘을-이용한-중복-콘텐츠-클러스터링) Date: 2021-01-19 | Author: Dominic Lim | Category: Data & ML 버즈빌이 제공하는 서비스 중 하나는 버즈빌과 제휴를 맺은 여러 콘텐츠 퍼블리셔들의 콘텐츠를 크롤링하여 유저들에게 서빙하는 것이 있습니다. 이렇게 크롤링한 콘텐츠들은 비슷한 내용을 가지는 중복 콘텐츠가 있기 마련입니다. 특히 뉴스 타입의 콘텐츠를 제공하는 퍼블리셔들은 그때그때 화제가 되는 주제를 가지고 콘텐츠를 만들다 보니 서로 비슷한 내용의 콘텐츠가 발행되는 경우가 많습니다. 그렇기에 크롤링한 콘텐츠 중에 서로 중복인 것들을 구분하고 중복된 다수의 콘텐츠 중 하나를 선택하여 유저에게 제공할 필요가 있습니다. 이 과정은 2가지 단계로 나눌 수 있습니다. 1. 어느 콘텐츠가 어느 콘텐츠와 중복인지 분류한다. 2. 중복인 콘텐츠 중에 어느 것을 대표로 선택할지 정한다 ## 중복 분류와 중복 관계 그래프 생성 유저들에게 노출할 대표 콘텐츠를 선택하기 전에 먼저 어떠한 콘텐츠들이 서로 중복인지 판단할 필요가 있습니다. 콘텐츠의 중복 여부를 판단하는 프로세스는 이 포스팅의 주제는 아니지만 버즈빌에서 사용하는 방식은 간략히 설명하자면 다음과 같습니다. 1. 각 콘텐츠의 이미지와 텍스트를 각각 적절한 알고리즘을 사용해 임베딩 벡터로 변환 2. 모든 콘텐츠 사이의 이미지 임베딩 벡터의 cosine similarity와 텍스트 임베딩 벡터의 consine similarity 값을 구한다. 3. 2번에서 두 스코어 중 하나라도 각 스코어의 threshold 값을 넘는 두 콘텐츠는 서로 중복으로 판단한다. 4. 중복 여부를 판단하기 위해 모든 콘텐츠 간에 위 프로세스를 실행시킵니다. 두 콘텐츠 간의 중복인가 아닌가를 판단할 수 있는 프로세스가 존재한다면 콘텐츠 간의 중복 관계는 비 가중 그래프로 표현할 수 있습니다. 콘텐츠는 그래프의 노드가 되고, 서로 중복으로 판단되는 콘텐츠는 엣지로 연결됩니다. | ![](https://tech.buzzvil.com/blog/%ea%b7%b8%eb%a6%ac%eb%94%94-%ec%95%8c%ea%b3%a0%eb%a6%ac%ec%a6%98%ec%9d%84-%ec%9d%b4%ec%9a%a9%ed%95%9c-%ec%a4%91%eb%b3%b5-%ec%bd%98%ed%85%90%ec%b8%a0-%ed%81%b4%eb%9f%ac%ec%8a%a4%ed%84%b0%eb%a7%81/base-graph.png) | |:--:| | Figure 1. 콘텐츠들 간의 중복 Relation 그래프 예시 | ## 중복 클러스터링 각 콘텐츠 간의 중복 관계 그래프가 완성된 후 어떻게 대표 콘텐츠를 선택하는 것이 바람직할까요? 이것은 일종의 그래프 클러스터링 문제라고 볼 수 있습니다. 서로 거리가 가까운 노드들을 한 클러스터로 묶고, 각 클러스트에서 대표 노드를 선택하는 것이지요. 생각해볼 수 있는 간단한 방법의 하나는 그래프에서 서로 간의 경로가 존재하는 모든 노드를 같은 중복 클러스터로 묶고 각 클러스터에 속한 노드 중에 하나를 대표로 선택할 수도 있겠습니다. 그러나 이것은 대표 콘텐츠와 중복이라고 판단되지 않는 콘텐츠가 같은 중복 클러스터에 속하게 됩니다. 한국인이라면 아래의 동요가 익숙할 것입니다. > 원숭이 엉덩이는 빨~개 > > 빨가면 사과 > > 사과는 맛있어 > > 맛있으면 바나나 > > 바나나는 길어 > > 길면 기차 > > 기차는 빨라 > > 빠르면 비행기 > > 비행기는 높아 > > 높으면 백두산 원숭이가 사과와 비슷하다는 문구와 사과는 바나나와 비슷하다는 문구는 납득이 될 수도 있습니다. 어쩌면 사람에 따라서는 원숭이는 바나나와 비슷하다는 것까지도 납득될 수도 있겠죠. 그러나 원숭이가 백두산과 비슷하다는 주장까지 나온다면 이것은 납득하기 어려울 것입니다. 만약 경로가 존재하는 모든 노드를 한 클러스터로 묶는 방식을 사용한다면 원숭이는 백두산과 같은 클러스터에 속하게 될 것입니다. 원숭이를 구경한 유저들은 백두산이 원숭이와 중복이라는 이유로 구경하지 못하게 되는 일이 일어나게 되죠. ## 중복 클러스터링의 필수 원칙과 약한 원칙 유저들에게 중복이 아닌 콘텐츠를 효율적으로 골라서 보여주기 위해 중복 클러스터링 알고리즘은 다음과 2개의 필수적인 원칙을 지켜야 합니다. 1. 그래프의 각 노드는 대표로 선택되었거나 대표로 선택된 노드와 이웃이어야 한다. 2. 대표로 선택된 노드들은 서로 이웃이어선 안된다. 위 원칙을 지키고 대표 콘텐츠를 선택하여 유저에게 보여줄 경우, 모든 콘텐츠는 유저에게 노출될 수 있거나, 대표로 선택된 콘텐츠와 중복이 됩니다. 위 2개의 필수 원칙에 아래의 2개의 원칙 중 하나를 정책으로 추가할 수 있습니다. - 가능한 최소한의 수의 클러스터를 생성해야 한다. (최소 수 대표 원칙) - 가능한 최대한의 수의 클러스터를 생성해야 한다. (최대 수 대표 원칙) 위 “원숭이 엉덩이는 빨개” 노래에서 원숭이에서 바나나까지의 관계만 그래프로 표현한다면 관계도는 다음과 같습니다. > 원숭이--사과--바나나 최소 수 대표 원칙을 따른다면 사과를 대표로 선택하면 됩니다. 원숭이와 바나나 둘 다 사과의 클러스터에 속하게 되며, 클러스터의 개수는 1개가 됩니다. > (원숭이--**사과**--바나나) 최대 수 대표 원칙을 따르고자 한다면 원숭이와 바나나를 대표로 선택하면 됩니다. 클러스터는 2개가 되며 두 대표인 원숭이와 바나나는 서로 중복이 되지 않습니다. 사과는 원숭이나 바나나 둘 중 하나의 클러스터에 속하게 하면 됩니다. 아래의 예시는 사과가 원숭이가 대표로 있는 클러스터에 속한 경우입니다. > (**원숭이**--사과)--(**바나나**) 보시다시피 최소 수 대표 원칙에서 선택되는 대표 콘텐츠는 더 많은 수의 콘텐츠들과 중복이며 그렇기에 가능한 많은 수의 콘텐츠를 대표한다고 할 수 있습니다. 대신 대표들의 수는 더 적습니다. 반면에 최대 수 대표 원칙에서 선택되는 대표 콘텐츠는 더 적은 수의 콘텐츠를 대표하고 더 많은 수의 콘텐츠가 대표로 선택되어 유저들에게 노출됩니다. 그러나 최소 수 원칙과 최대 수 원칙을 지킬 수 있는 알고리즘은 어려워 보입니다. 원숭이 엉덩이는 빨개 노래처럼 모든 노드가 일렬로만 연결된 단순한 그래프만 존재하는 것이 아니고서는 이것은 복잡한 그래프 조합 문제가 됩니다. 하지만 1, 2번 원칙과 달리 두 선택적 원칙은 반드시 지켜야 할 필요가 있지 않습니다. 1, 2번 원칙만 지켜지면 어쨌든 한 콘텐츠는 유저에게 노출될 수 있거나, 혹은 해당 콘텐츠와 중복되는 콘텐츠가 유저에게 노출될 수 있으니까요. 그래서 위의 선택 원칙을 가능한 한 지키면 좋지만, 반드시 지켜야 할 필요는 없는 약한 원칙으로 변경합니다. - 될 수 있으면 적은 수의 클러스터를 생성해야 한다. (적은 수 대표) - 될 수 있으면 많은 수의 클러스터를 생성해야 한다. (많은 수 대표) 이렇게 하면 위의 두 약한 원칙 중 하나를 선택하여 그리디 알고리즘으로 쉽게 구현이 가능합니다. ## 그리디 알고리즘 **적은 수 대표** 약한 원칙을 따르는 그리디 알고리즘은 다음과 같습니다. 1. 그래프에서 가장 엣지가 많은 노드를 선택, 대표로 지정하여 클러스터를 만든다 2. 1번에서 선택한 노드와 이웃인 노드를 모두 1번에서 만든 클러스터에 넣는다. 3. 그래프에서 클러스터에 속한 노드, 그리고 노드와 연결된 엣지도 지운다. 4. 모든 노드가 사라질 때까지 1~3번을 반복한다. 만약 **많은 수 대표** 약한 원칙을 따르고자 한다면 위 단계에서 1번을 다음과 같이 바꾸면 됩니다. 1. 그래프에서 가장 엣지가 적은 노드를 선택, 대표로 지정하여 클러스터를 만든다 다음 두 그림은 Figure 1 그래프에 각각 적은 수 대표, 많은 수 대표를 적용한 그리디 알고리즘의 결과입니다. 파란색 노드가 대표로 선택된 노드를 가리킵니다. | ![](https://tech.buzzvil.com/blog/%ea%b7%b8%eb%a6%ac%eb%94%94-%ec%95%8c%ea%b3%a0%eb%a6%ac%ec%a6%98%ec%9d%84-%ec%9d%b4%ec%9a%a9%ed%95%9c-%ec%a4%91%eb%b3%b5-%ec%bd%98%ed%85%90%ec%b8%a0-%ed%81%b4%eb%9f%ac%ec%8a%a4%ed%84%b0%eb%a7%81/less-clusters.png) | |:--:| | Figure 2. Figure 1번의 그래프에 **적은 수 대표** 원칙 탐욕 알고리즘을 적용한 결과| | ![](https://tech.buzzvil.com/blog/%ea%b7%b8%eb%a6%ac%eb%94%94-%ec%95%8c%ea%b3%a0%eb%a6%ac%ec%a6%98%ec%9d%84-%ec%9d%b4%ec%9a%a9%ed%95%9c-%ec%a4%91%eb%b3%b5-%ec%bd%98%ed%85%90%ec%b8%a0-%ed%81%b4%eb%9f%ac%ec%8a%a4%ed%84%b0%eb%a7%81/more-clusters.png) | |:--:| | Figure 3. Figure 1번의 그래프에 **많은 수 대표** 원칙 탐욕 알고리즘을 적용한 결과 | ## 배치 프로세스 그리고 찬탈 콘텐츠는 언제나 새롭게 생성되고 수명이 존재합니다. 아직 콘텐츠의 수명이 남아 있더라도 버즈빌에서는 유저들에게 최신 콘텐츠가 더 잘 보이도록 하므로 오래된 콘텐츠일수록 노출 빈도는 낮아집니다. 버즈빌에서는 위에서 설명한 중복 클러스터링 프로세스를 30분 간격으로 과거 6시간 동안의 콘텐츠에 배치로 적용합니다. 그렇기 때문에 한 번 대표로 선택된 콘텐츠는 30분 뒤에 다른 콘텐츠에 대표 자리를 찬탈당할 수도 있습니다. 만약 찬탈을 원하지 않는다면 위의 그리디 알고리즘을 실행하기 전에 이미 대표로 선택된 콘텐츠와 이웃한 콘텐츠의 노드는 모두 지운 뒤에 남은 노드만을 가지고 그리디 알고리즘을 실행하면 됩니다. 이미 대표로 선택된 콘텐츠와 이웃인 콘텐츠는 대표들의 클러스터에 흡수 된 것이 됩니다. 아래 그래프는 Figure 2에서 30분이 지났을 때의 상황을 보여줍니다. 새로운 콘텐츠가 들어오고, 오래된 콘텐츠는 사라집니다. | ![](https://tech.buzzvil.com/blog/%ea%b7%b8%eb%a6%ac%eb%94%94-%ec%95%8c%ea%b3%a0%eb%a6%ac%ec%a6%98%ec%9d%84-%ec%9d%b4%ec%9a%a9%ed%95%9c-%ec%a4%91%eb%b3%b5-%ec%bd%98%ed%85%90%ec%b8%a0-%ed%81%b4%eb%9f%ac%ec%8a%a4%ed%84%b0%eb%a7%81/rebellion.png) | |:--:| | Figure 4. Figure 2번에서 30분 뒤 | | 새로 들어온 콘텐츠(초록색) | | 6시간이 지나 더는 그래프에 존재하지 않는 콘텐츠(점선) | | ![](https://tech.buzzvil.com/blog/%ea%b7%b8%eb%a6%ac%eb%94%94-%ec%95%8c%ea%b3%a0%eb%a6%ac%ec%a6%98%ec%9d%84-%ec%9d%b4%ec%9a%a9%ed%95%9c-%ec%a4%91%eb%b3%b5-%ec%bd%98%ed%85%90%ec%b8%a0-%ed%81%b4%eb%9f%ac%ec%8a%a4%ed%84%b0%eb%a7%81/usurpers.png) | |:--:| | Figure 5. 반란과 찬탈 허용 | 찬탈을 허용한다면 Figure 5와 결과가 나옵니다. 과거의 대표는 다른 노드에 대표 자리를 찬탈당할 수도 있습니다. | ![](https://tech.buzzvil.com/blog/%ea%b7%b8%eb%a6%ac%eb%94%94-%ec%95%8c%ea%b3%a0%eb%a6%ac%ec%a6%98%ec%9d%84-%ec%9d%b4%ec%9a%a9%ed%95%9c-%ec%a4%91%eb%b3%b5-%ec%bd%98%ed%85%90%ec%b8%a0-%ed%81%b4%eb%9f%ac%ec%8a%a4%ed%84%b0%eb%a7%81/no-usurping.png) | |:--:| | Figure 6. 반란 불가 | 찬탈을 허용하지 않는다면 Figure 6에 보이듯이 한번 대표로 선택된 콘텐츠는 영원히 대표로 남습니다. 한번 대표로 선택된 콘텐츠가 찬탈당하는 혼란을 원하지 않는다면 찬탈을 금지해야합니다. 그때그때 가장 적절한 콘텐츠가 대표가 되는 것을 원한다면 찬탈을 허용할 필요가 있습니다. ## 개선점 위에서 이야기했듯이 배치로 실행되는 중복 클러스터링은 새로 생성된 콘텐츠가 다음 배치 실행 전까지 대기해야 한다는 단점이 있습니다. ### 현재의 버즈빌의 방식 현재의 버즈빌의 중복 콘텐츠 프로세싱은 30분마다 배치로 돌아가고 있습니다만, 새로 생성된 콘텐츠가 유저에게 보이기까지 30분이 걸리지는 않습니다. 이는 대표가 아닌 중복 콘텐츠를 유저에게 노출되지 않도록 하는 방식이 아니라 페널티를 줘서 노출빈도가 줄어들도록 하기 때문입니다. 또한 콘텐츠는 최신 콘텐츠 일수록 노출 보너스가 붙기에 최신 콘텐츠가 무조건 오래된 대표 콘텐츠보다 노출 빈도가 낮아지지 않습니다. 최신 콘텐츠는 일단 아직 대표로 분류되지 않더라도 어느 정도는 노출이 되다가 대표로 선택되면 더 잘 노출이 되게 하고 있습니다. ### 매트릭 트리를 이용한 스트리밍 프로세스 배치 방식을 스트리밍 방식으로 바꾸기 위한 개선방식 하나는 매트릭 트리를 사용하는 것입니다. 각 콘텐츠의 중복 여부를 판단하기 위해 생성한 임베딩 벡터를 매트릭 트리에 저장합니다. 매트릭 트리를 이용해 새로 생성된 콘텐츠와 중복 관계인 콘텐츠를 빠르게 찾고 중복 관계 그래프에 새 콘텐츠 노드를 빠르게 추가하는 것이 가능해질 것입니다. - 찬탈을 허용할 경우 1. 새로 추가된 콘텐츠와 경로가 존재하는 모든 콘텐츠를 가져옵니다. 2. 그리디 알고리즘을 적용합니다. - 찬탈을 허용하지 않는 경우 1. 새로 추가된 콘텐츠와 이웃인 대표 콘텐츠를 가져옵니다. 2. 대표 콘텐츠와 이웃인 새 콘텐츠는 모두 대표 콘텐츠의 클러스터에 흡수됩니다. 3. 남은 새 콘텐츠에 그리디 알고리즘을 적용합니다. 찬탈을 허용하지 않는 경우는 경우 새로 추가된 콘텐츠와 이웃인 콘텐츠만 가져오면 되지만, 찬탈을 허용하면 추가로 그래프 경로가 존재하는 모든 콘텐츠를 가져와야 하므로 시간이 더 걸릴 것입니다. 스트리밍 프로세스를 구현하기 위해서 생성되는 콘텐츠를 메시지 큐 형식의 버퍼에 보관해서 버퍼에 일정 수 이상의 콘텐츠가 쌓여있거나 일정 시간이 지났을 때마다 클러스터링 프로세스가 실행되도록 하는 것도 생각해 볼 수 있습니다. ## 되돌아보며 처음 중복 콘텐츠 문제를 접했을 때 이것을 그래프 문제로 해석한 뒤에 어떻게 해결할지 방침을 정했었습니다. 처음에는 약한 원칙 없이 필수적인 원칙만을 정하고 이를 해결하기 위한 알고리즘을 구현하고자 했습니다만, 그러면 복잡하고 시간이 오래 걸리는 알고리즘의 도입이 필요하다는 것을 깨닫고 약한 원칙과 필수 원칙을 구분하여 그리디 알고리즘을 적용하는 방식을 생각해 냈습니다. 그리고 클러스터링 알고리즘을 배치로 적용하였습니다. 현재 클러스터링 프로세스는 단순히 30분 주기의 배치로 적용되고 있고 이것만으로도 충분히 만족할 만한 퍼포먼스를 보여줍니다. 그러나 사실 콘텐츠의 유입은 데이터 스트림이라고 볼 수 있고, 클러스터링 프로세스도 배치가 아닌 스트리밍으로 적용할 수도 있습니다. 만약 클러스터링 프로세스를 스트림으로 적용하고자 한다면 이것을 지원하기 위한 메트릭 트리의 퍼포먼스, 콘텐츠를 보관하는 큐의 정책 등에 대한 고려가 필요할 것입니다. --- ## [우리 팀의 OKR은 괜찮을까요](https://tech.buzzvil.com/blog/how-do-we-manage-okr) Date: 2020-10-30 | Author: Whale Lee | Category: Product 존 도어의 OKR 혹은 그외 유명한 OKR 책들을 읽어보면 왠지 쉽게 적용할 수 있을 것 같은 느낌이 듭니다. 간혹 Objective와 Key Result 라는 개념을 가지고 무슨 책까지 쓰나 생각하시는 분들도 있을 것 같습니다. 헌데 배운대로 열심히 적용하다보면 비슷한 질문에 다다르곤 합니다: **우리는 OKR을 잘 운영하고 있을까요?** Objective는 일단 제쳐두고 Key Result만 생각해 보도록 하겠습니다. Key Result를 정할 때 특히 신경써야 할 두 가지 기준이 있습니다. - Key Result는 측정 가능해야 한다. - Key Result가 모두 달성되면 해당 Objective가 달성 되었다고 간주할 수 있어야 한다. ## 잘못된 예시 위 기준을 바탕으로 다음 미션 팀 KR을 한번 살펴보죠. - Objective - 사용자들이 매일 사용하는 서비스로 만든다. - Key Results - 출석 체크 기능을 만든다. - 정기적인 푸시 알림을 구현한다. 뭔가 위 기준에 부합하는 것 같지는 않습니다. '출석 체크 기능을 만든다'는 것은 측정 가능할 수 있지 않느냐는 질문을 할 수도 있을 것 같습니다. 가능하죠. 사용자가 해당 기능을 사용할 수 있다는 사실 여부를 바탕으로 1 혹은 0이 될 수 있습니다. 헌데 보통 분기 마무리가 될 때까지 완성이 안되면 이렇게 기록하고는 합니다. "아직 런칭은 못 했지만 기능은 거의 구현 되었고, QA하고 배포만 준비하면 되니까 0.8 정도로 합시다." 이게 뭔가요. 두 번째 기준에서도 애매합니다. 두 가지 기능이 있다면 우리 서비스는 사용자들이 매일 사용하는 서비스라고 볼 수 있을까요? 해당 기능만이 해결책일까요? 분기 중간에 얻은 인사이트를 바탕으로 다른 기능을 개발하는 것은 지양해야 할까요? 다 만들었는데 사용자들이 매일 안쓰면 어떻게 되나요? 뭔가 우리의 OKR은 괜찮아 보이지 않았습니다. ## 어떻게 해야 할까요 우선 조악한 예시로 시작하는 점 양해 부탁 드립니다. - Objective - 사용자들이 매일 사용하는 서비스로 만든다. - Key Results - 평균 재방문 주기를 48시간에서 24시간으로 줄인다. - 월 Retention Rate 하락을 2% 이하로 방어한다. 위 Key Result 들은 실제 현실성과는 별개로 OKR 기준에 어느정도 부합해 보입니다. 측정 가능하고, 해당 Key Result 들이 모두 달성되면 어느정도 Objective를 달성했다고 볼 수도 있겠죠. 하지만 이렇게만 OKR을 작성하려고 하면 무언가 빠진 것 같은 느낌을 받습니다. **팀에서는 OKR에 우리가 '무엇'을 할 것인지 적어야 할 것 같은 부담을 느끼죠.** 우리가 Key Result에 해당 분기에 진행할 하는 과제를 나열하게 되는 이유 중 하나는 OKR을 공유 용으로 사용하기 때문입니다. 간혹 '보고'라는 단어의 부정적인 의미 때문에 '공유'의 가치가 평가절하 되기도 하지만, 분명히 OKR은 공유의 목적을 가지고 있습니다. ## 그렇다면 그것도 적자 이런 고민을 하고 있을 때 버즈빌 애자일코치 이마무께서 명쾌한 답변을 주셨습니다: "그래서 해당 분기에 진행할 과제에 대한 항목을 추가하기도 합니다." - Objective - 사용자들이 매일 사용하는 서비스로 만든다. - Key Results - 평균 재방문 주기를 48시간에서 24시간으로 줄인다. - 월 Retention Rate 하락을 2% 이하로 방어한다. - Tasks - 출석 체크 런칭 - 정기 푸시 알림 전송 - 재방문 주기 측정 이렇게 구성하니 팀에서 하고자 하는 목표와 측정 기준도 명확하고, 해당 목표를 달성하기 위해 진행할 과제에 대한 정보도 포함하고 있으며, 추가적인 과제를 만들거나 과제를 변경할 수 있는 유연함도 얻을 수 있었습니다. 목표 달성을 위해 A/B 테스팅을 수행할 수도 있고, 푸시 알림 대신 정기 메일을 활용해볼 수도 있습니다. 수단과 별개로 Key Result를 달성하는 것이 최종 목적이니까요. ## 마무리 IMHO, OKR을 운영하는 목적은 목표를 명확하게 하고 팀과 회사, 개인 모두가 같은 방향으로 움직이게 하는 것입니다. 그저 보고 혹은 공유 만이 목적은 아니죠. OKR 경험이 그리 많지 않은 제 입장에서 봤을 때 "이것을 왜 하는가"에 대한 질문을 스스로에게 계속 물어보지 않으면 그저 서류 작업의 연장선에 그치고 말 가능성이 높습니다. 이미 OKR을 잘 운영하고 있는 팀들 입장에서는 "뭔 당연한 소리야" 혹은 "뭔 X소리야" 라고 들으실 수도 있을 것 같습니다. 하지만 분명히 저처럼 계속해서 고민하고 있는 분들도 있다고 믿으며, 약간의 도움이라도 되고자 생각을 정리합니다. 비슷한 고민이 있거나 이견이 있으시다면 많은 피드백 부탁드립니다. --- ## [다시 만난 버그](https://tech.buzzvil.com/blog/tech-blog-arabic-number-string-format) Date: 2020-09-18 | Author: Summer Kwak | Category: Backend # 버그 발생 개발자에게 가장 피하고 싶은 순간 중 하나는 주말에 슬랙에서 나를 급하게 찾는 알람이 울릴 때입니다. 이런 불상사를 피하고자 금요일 배포 금지, 금요일 롤아웃 금지 등의 불문율이 존재합니다. 하지만 지난주 일요일 오후에 슬랙에서 클라이언트 팀을 전체 멘션하는 알람이 울렸습니다. 두바이에 있는 파트너사에서 피드에 보이는 컨텐츠를 클릭해도 랜딩이 안된다고 연락이 온 것인데요, 버즈빌에서는 컨텐츠와 리워드형 광고를 보여주는 제품을 만들어 파트너사에 제공하고 거기서 나오는 광고수익을 나누어 가지는 사업을 하고 있습니다. 그 중 컨텐츠는 광고만 보이는 피드가 아니라 읽을만한 피드, 사용자에게 또 다른 가치를 제공해 주고 제품에 남아있을 수 있도록 도와주는 접착제 같은 역할을 하고 있습니다. 이런 상황에서 컨텐츠가 랜딩이 안되는 상황은 큰 문제였죠. 불행 중 다행인 것은, 이 문제가 파트너사가 QA 중에 발생한 이슈였고, 일요일에 연락이 온 것은 아랍권 국가에서는 일요일에도 일을 하기 때문이었습니다. 참고로 그들도 일주일 내내 휴일 없이 일하는 것은 아니고, 금 토를 쉬고 일요일부터 목요일까지 일한다고 하네요. # 원인 파악하기 문제를 파악하기 위해 여러 개발자들이 모여들었습니다. 원인 파악을 해봐야겠죠! 컨텐츠를 클릭하면 서버로 redirect되는 주소를 요청하면서 파라미터로 checksum을 넘기고 있습니다. 제대로 된 요청인지 무결성을 검증하기 위해 유저 정보, 캠페인 정보, 리워드 등의 파라미터를 암호화 알고리즘을 사용하여 checksum으로 만들로 있습니다. 문제는 이 checksum이 잘못 넘어가고 있다는 것이었습니다. 서버에서 계산한 값은 - `6371ab08199c6fcaf73cdc5ed17c712d` 인 데 클라이언트에서 계산해서 보내는 값은 - `ed85f6f40927a1b61e0702cc43c9dad1` 으로 엉뚱한 값이 넘어오고 있었습니다. 파트너사의 메일에 따르면 아랍어로 된 컨텐츠, 혹은 디바이스 언어가 아랍어로 설정된 경우에 발생하고 있었습니다. 1. 첫 번째 가설은 checksum을 만들 때 사용하는 컨텐츠 제목의 인코딩이 잘못되어 checksum이 잘못 생성되고 있다는 것이었습니다. 하지만 파트너사에서 문제가 된다고 보내온 컨텐츠는 제목이 영어로 되어있어서 제목의 인코딩 문제는 아닌 것으로 밝혀졌습니다. 2. 두 번째 가설은 whitespace등에 대한 escaping 처리가 클라이언트와 서버간 차이가 난다는 것이었는데, 이 문제도 가능성은 작아 보였습니다. checksum을 만드는 부분에 브레이크 포인트를 잡고 디버깅을 시작해 보았습니다. 다음은 버즈빌에서 checksum을 만드는 예시입니다. ```java public static String generateChecksum(String deviceId, String contentsName, String userToken, int reward) { String str = String.format("example:%s:%s:%s:%d", deviceId, contentsName, userToken, reward); return encrypt(str); } ``` 디바이스 아이디, 컨텐츠 이름, 유저 토큰, 리워드 값 등을 이용해 만들고 있는데, 이 파라미터에 파트너사에서 문제가 된다고 보내온 컨텐츠와 유저 정보, 리워드 값을 넣고 checksum을 만들어 보았습니다. - `example:20014fea6bcc820c:[tumblr] tumblr Pictures:2ahUKEwjrmqr5pv:1` 이런 파라미터를 만들어 계산해보니 아무런 문제 없이 서버에서 계산한 것과 정확히 같은 값이 나왔습니다. 두 번째 가설처럼 space를 지워보기도 하고 이스케이프 문자를 넣어보기도 했지만, 클라이언트에서 보내오는 잘못된 값과 동일한 결과를 얻을 수 없었습니다. 그러다가 언어를 아랍어로 설정하면 발생하는 버그라는 점에서 힌트를 얻어, 테스트 디바이스의 언어를 아랍어를 변경하고 다시 시도해 보았습니다. 아랍어로 변경하자마자 클라이언트에서 계산한 checksum은 파트너 사에서 리포트 해온, 잘못 보내고 있다는 그 값을 반환했습니다. - `ed85f6f40927a1b61e0702cc43c9dad1` 분명히 아까와 같은 파라미터 들인데, 결과가 다르다니 이상한 일이었습니다. 살펴보니, 암호화 함수의 인자로 들어가는 string의 값이 조금 달라졌습니다. - `example:20014fea6bcc820c:[tumblr] tumblr Pictures:2ahUKEwjrmqr5pv:١` 혹시 파라미터로 사용된 두 string의 차이가 보이시나요? 디바이스가 한국어로 설정되어 있을 때는 맨 끝의 숫자 1이 정상적으로 1로 출력되지만, 디바이스가 아랍어일 때는 처음 보는 특수문자가 들어있습니다. 혹시나 하는 마음으로 google에서 arabic number로 검색해보니, 검색 결과로 저 특수문자와 비슷한 문자가 나옵니다. 클릭해 보니 Arabic Numeral과 English Numeral을 비교해 놓은 표가 있었습니다. ![image](https://tech.buzzvil.com/blog/tech-blog-arabic-number-string-format/arabic_number.png) 아랍에서 사용하는 1, 2, 3 숫자는 사실 ٠,١,٢ 이렇게 생겼다는 것을 알게 되었습니다. 아랍에서 사용하는 숫자가 문제가 되어 checksum 계산이 틀렸던 것입니다. 원인을 알아냈으니 수정하는 일만 남았습니다. 안드로이드 클라이언트에서 `String.format`을 이용하여 숫자를 string으로 변환하여 사용하고 있었는데 이 숫자가 잘못 들어갔던 것이죠. `String.format()`을 사용할 때 로케일을 설정하지 않으면 사용자가 설정한 로케일이나 시스템 기본 로케일을 사용하여 문자열이 구성됩니다. 그런데 로케일이 아랍어로 경우 숫자를 문자열로 변환하는 과정에서 우리가 알고 있는 0, 1, 2 등의 서구 아라비아 문자가 아니라 ٠,١,٢ 등의 문자처럼 보이는 숫자가 들어가게 되어 예상과 전혀 다른 결과를 내고 있었던 것입니다. 참고로 이 숫자들은 동부 아라비아 숫자라고 합니다. 만약 사용자에게 보여줘야 하는 문자열이었다면 아랍 국가에서는 동부 아라비아 숫자로 표기하는 것이 아무런 문제가 없고 오히려 권장해야 하는 상황입니다. 하지만 checksum과 같이 어떤 상황에서든 일정해야 하는 값이 로케일에 따라 해석하는 방법이 달라지니 이런 버그가 발생했습니다. # 해결 방법 이를 해결하는 방법은 두 가지가 있습니다. - (권장) Locale을 설정한다 ```java String.format(Locale.US, "%d", 1); ``` - %s로 받는다. 이렇게 해도 숫자가 로케일에 상관없이 1로 string이 만들어집니다. ```java String.format("%s", 1); ``` 글로벌 서비스를 하다 보면 종종 생각지도 못하는 문제를 맞닥뜨리게 됩니다. 덕분에 아라비아 숫자에 동부 버전이 있다는 사실도 배우게 되었습니다. 버그를 찾으며, 다른 언어의 문자 체계에 대해 알게 된다는 게 너무 재밌지 않나요? 사실 이 버그는 생각보다 빠르게 발견할 수 있었는데, 브라이스께서 이전 직장에서 정확히 똑같은 버그를 만나서 해결할 적이 있었다고 합니다. 이제 버즈빌에 근무하시는 분들이 나중에 아라비아 숫자로 인한 버그를 만난다면 금방 해결할 수 있게 되겠죠? 이 글을 보는 여러분도 마찬가지일 테고요. 아니, 버그가 생기기 전에 로케일 없이 사용하는 String.format은 위험할 수 있다고 인지하신다면 가장 좋을 것 같네요. 프로그래머로 일을 하다 보면 보기만 해도 무서운 코드들이 점점 늘어나는데, 그만큼 구멍에 빠지지 않고 조심성있게 작업할 수 있게 되었다고 생각합니다. 다시 만난 버그를 또다시 만나지는 않기를 바라며 이 글을 마칩니다. --- ## [안드로이드 11의 "패키지 공개 상태" 변경 사항 정리](https://tech.buzzvil.com/blog/tech-blog-package-visibility-in-android-11) Date: 2020-08-05 | Author: Dio Heo | Category: Frontend 안드로이드 11의 출시가 얼마 남지 않았습니다. [공식 문서](https://developer.android.com/preview/overview#timeline)에 따르면 2020년 3분기 안에 출시가 될 에정이니, 1~2달 안에 정식 버전이 출시될 것 같습니다. 지금까지의 메이저 버전 업데이트가 항상 그랬듯, 이번 안드로이드 11에서도 중요한 변경사항이 많고 이에 기존 앱들은 큰 영향을 받을 수 있습니다. 그중에서도 "패키지 공개 상태"에 대한 변경은 버즈빌 안드로이드 제품에도 많은 영향을 주는 부분이이서, 이에 대해 조사하며 실제로 테스트해본 결과를 공유하고자 합니다. # "패키지 공개 상태" 변경사항 targetSdkVersion이 안드로이드 11 이상인 앱에서는 디바이스에 설치된 다른 앱 목록을 알 수 없고, 미리 매니페스트 파일에 지정한 앱의 정보만 가져올 수 있습니다. 쿼리 하고 싶은 앱을 지정하려면 `AndroidManifest.xml`에 `` 요소를 추가해서 쿼리할 패키지 이름 또는 인텐트 필터 서명을 포함해야 합니다. ## 패키지 이름 지정 방법 ```xml ... ``` ## 인텐트 필터 지정 방법 ```xml ... ``` 예외적으로 몇몇 사용 사례(런처, 접근성, 브라우저, 보안 앱 등)에서는 기기에 설치된 모든 앱을 쿼리하거나 상호작용해야 할 수 있습니다. 이를 위해 안드로이드 11에는 `QUERY_ALL_PACKAGES` 권한이 도입되었습니다. 공식 문서에서는 `QUERY_ALL_PACKAGES` 권한에 대해 Google Play에서의 관련 가이드라인을 제공할 예정이라고 설명하고 있습니다. 구글에서는 대부분의 상황에서 `` 요소를 통해 앱이 작동하는 데 필요한 패키지 공개 상태를 최소한으로 요청하여 앱의 기존 동작을 유지할 수 있다고 보고 있는 것 같아, 아마 위에 언급한 예외적인 사용 사례를 제외하면 이 권한의 사용을 어떤 방식으로든 제한할 것으로 보입니다. 더 자세한 내용은 [안드로이드 개발자 페이지의 공식 문서](https://developer.android.com/preview/privacy/package-visibility)를 읽어보시기를 추천해 드립니다. # 영향받는 API 공식 가이드 문서에는 예시로 든 API는 `PackageManager.queryIntentActivities()` 뿐이지만, [구글 안드로이드 11 밋업](https://developersonair.withgoogle.com/events/a11meetup-korea/watch?talk=meetup1)에서는 "패키지 공개 상태" 변경에 영향을 받는 다른 API도 소개하고 있습니다. - `PackageManager.getPackageInfo("com.another.app", 0)` - 앱 설치 여부와 상관없이 `NameNotFoundException` 예외 발생 - `PackageManager.getInstalledPackage(0)` - 자기 자신과 적은 수의 시스템 필수 패키지 목록만 반환 - `PackageManager.queryIntentActivities(intent, 0)` - 자기 자신과 적은 수의 시스템 필수 패키지 목록만 반환 - 명시적 intent를 통한 방법 - 예외 발생 # 동작 테스트 변경에 영향을 받을 것으로 예상되는 API를 실제로 테스트해보기 위해 샘플 앱을 생성해 안드로이드 11 가상 디바이스에서 돌려보았습니다. Android 11 Beta 2.5 이미지를 사용했고, 샘플 앱의 코드는 [여기서](https://github.com/hseung/PackageVisibilityTest) 확인할 수 있습니다. 테스트 대상 API는 위의 구글 밋업에서 소개된 API 4개, 그리고 추가로 이와 비슷한 역할을 API 2개를 선정했습니다. ## 1. PackageManager.getPackageInfo() - `` 요소를 지정하지 않으면, `NameNotFoundException`이 발생합니다. - 매니페스트 파일이 아래와 같으면, "com.android.chrome"을 인자로 호출한 API가 정상적으로 동작합니다. ```xml ... ``` ## 2. PackageManager.queryIntentActivities(intent, 0) 아래의 샘플 코드로 테스트한 결과입니다. ```kotlin Intent(Intent.ACTION_VIEW, Uri.parse("https://www.google.com")).let { intent -> val list = packageManager.queryIntentActivities(intent, 0) } ``` - `` 요소를 지정하지 않으면 빈 리스트가 반환됩니다. - 매니페스트 파일에 1번 예제처럼 "com.android.chrome"를 명시적으로 지정하면, 해당 패키지만 반환됩니다. - 매니페스트 파일에 아래와 같이 인텐트 필터를 사용하면, 설치된 모든 브라우저 앱이 반환됩니다. ```xml ... ``` ## 3. Intent.resolveActivity(packageManager) 2번 예제의 intent 그대로 테스트를 진행한 결과입니다. - `` 요소를 지정하지 않으면 null이 반환됩니다. - 매니페스트 파일에 1번 예제처럼 "com.android.chrome"를 명시적으로 지정하면, 해당 패키지가 반환됩니다. - 매니페스트 파일에 2번 예제와 같은 인텐트 필터를 지정하면, 실제 설치된 브라우저 중 가장 적합한 패키지가 반환됩니다. ## 4. PackageManager.getInstalledPackages(0) - `` 요소를 지정하지 않으면 자기 자신과 몇몇 시스템 필수 패키지만 반환됩니다. - `` 요소에 특정 패키지 혹은 인텐트 필터를 지정하면, 해당하는 패키지도 추가로 반환됩니다. ## 5. PackageManager.getInstalledApplications(0) - `` 요소를 지정하지 않으면 자기 자신과 몇몇 시스템 필수 패키지만 반환됩니다. - `` 요소에 특정 패키지 혹은 인텐트 필터를 지정하면, 해당하는 패키지도 추가로 반환됩니다. ## 6. 명시적 Intent를 이용한 `Context.startActivity(intent)` 아래의 샘플 코드로 테스트한 결과입니다. ```kotlin Intent(Intent.ACTION_VIEW, Uri.parse("https://www.google.com")).let { i -> i.flags = Intent.FLAG_ACTIVITY_NEW_TASK i.setPackage("com.android.chrome") context.startActivity(i) } ``` - `` 요소를 지정하지 않으면 `ActivityNotFoundException`이 발생합니다. - 매니페스트 파일에 1번 예제처럼 "com.android.chrome"를 명시적으로 지정하면, 크롬 앱이 정상적으로 실행됩니다. - 매니페스트 파일에 2번 예제처럼 인텐트 필터를 지정해도 크롬 앱이 정상적으로 실행됩니다. # 마치며 지금까지 안드로이드 11의 변경사항 중 "패키지 공개 상태"에 대해 자세히 알아보았습니다. 안드로이드 11의 출시가 얼마 남지 않은 만큼, 다른 앱 개발자 여러분들도 자신의 앱을 테스트해보시고 빠르게 변경사항에 대한 호환성을 확보하시기 바랍니다. --- ## [제품을 대하는 개발자의 자세](https://tech.buzzvil.com/blog/developers-attitude-towards-products) Date: 2020-07-01 | Author: Whale Lee | Category: Product ## 개발자에게 프로덕트 관점은 왜 중요할까요 최근 인터뷰에서 개발자로서 성장하기 위해서 어떻게 하면 좋겠느냐는 질문을 받았습니다. 프로그래밍 언어를 더 깊게 파는 것이 좋을까요? 알고리즘을 잘 풀어야 할까요? CS 지식? 클라우드 환경 운영? 머신러닝 엔지니어링? 무엇 하나 명확한 답이 되기에는 어려움이 있습니다. '개발자'라는 한 단어로 분류하기에 개발 분야는 너무 넓고 많으니까요. 이 질문은 서비스를 만들고 운영하는 입장에서 개발자가 무엇을 통해 성장해야 하는지에 대한 고민으로 이어졌습니다. 여전히 하나의 궁극적인 답은 없겠지만, 제품에 대한 고민이 개발자의 성장에 어떤 의미를 가지는지에 대해 생각을 정리해보려고 합니다. ### PM/PO의 덕목 [PM/PO가 되어서는 안되는 사람](https://brunch.co.kr/@hyungsukkim/121) 얼마전 "PM/PO가 되어서는 안되는 사람"이라는 글이 커뮤니티에서 활발하게 돌아다녔습니다. PM/PO로서 필요한 시각과 능력을 정의하기 위해 반례를 나열한 글인데요, 제목만 참고하면 아래와 같습니다. 1. 이 일을 왜 하는가를 이해하지 못하는 사람 2. 납기와 최소 스펙 개념이 없는 사람 3. 전체 판을 보지 못하는 사람 4. 우선 순위를 설정하지 못하는 사람 5. 수면 위에 드러난 것만 보는 사람 6. 질문을 하지 못하는 사람 7. 문제를 인식하지 못하는 사람 8. 압박을 견디지 못하는 사람 9. 끝을 보지 않는 사람 10. 실패했다는 것을 알지 못하는 사람 이를 요약해보면 "해야 하는 일을 목적과 이유를 전체적인 관점에서 파악하되 커뮤니케이션을 통해 우선 순위를 정하고, 이를 바탕으로 단계별 상세 스펙과 스케쥴을 산정한 뒤 끝을 볼 때까지 최선을 다하는 사람" 입니다. 이 항목들이 PM/PO에게만 필요한 덕목일까요? ### 개발자의 덕목 "Software Engineering Economics"의 저자 Barry Boehm은 개발자를 평가할 때 다음 항목이 순서대로 높은 중요도를 가진다고 합니다. 1. 문제를 분석하고 어떻게 해결할지 결정할 능력 2. 날 것(Raw)의 개발 능력 3. 문제를 분석하고 해결한 경험 4. OS 혹은 어플리케이션 도메인을 포함한 소프트웨어 구동 환경에 대한 경험과 지식 5. 특정 언어의 경험과 지식 순서에서 알 수 있듯이 언어에 대한 경험과 지식은 가장 덜 중요한 항목입니다. 훌륭한 개발자라면 새로운 언어는 금세 배울 수 있지만 어플리케이션 도메인에 대한 능력과 경험은 그렇지 않기 때문이라고 합니다. 반면에 특정 도메인에서 문제를 분석하고 해결해 온 경험을 바탕으로 문제를 정의/분석하고 해결해 나갈 수 있는 능력이 중요하다고 이해할 수 있습니다. (제가 좋아하는 문구를 인용해 보자면) 역사적으로 장인 정신은 경제학의 논리를 이긴 적이 없다고 합니다. 그런 의미에서 경제학의 관점으로 개발자가 프로덕트 이해도를 높이는 것이 왜 중요한지 나열해 보죠. - 프로덕트 이해도가 높을 수록 문제 분석 및 해결 능력이 증가한다. - 해당 능력이 좋아질 수록 평가가 좋아진다. - 평가가 좋아지면 몸 값(?)이 높아진다. 참 쉽죠? ## 프로덕트의 이해도 그래서 무엇을 어떻게 하라는 것 일까요? 우리는 이미 개발하고 있는 프로덕트를 잘 이해하고 있는 것 같은데 말이죠. 앞서 나온 PM/PO의 덕목 중에 '질문을 잘 하는 사람'을 참고해서 질문을 한번 던져 볼까요? ### 프로덕트 관점 - 이 제품/기능의 목적은 무엇인가? - 얼마나 많은 사람들이 얼마나 자주 사용하게 되는가? - 문제가 발생했을 때 어떤 일이 생기는가? ### 사용자 관점 - 누가 언제 왜 사용하게 되는가? - 해당 기능의 이전 행동과 이후 행동은 무엇인가? - 사용 도중 예외 사항은 없는가? 있다면 어떻게 동작해야 하는가? ### 비지니스 관점 - 이 제품은 우리의 비지니스에서 어떤 의미를 가지는가? - 사용자/매출/수익 관점에서 어떤 영향을 어떻게 미치는가? - 기능의 성공 혹은 실패 시 어떤 리스크가 있는가? 어떤가요? 위 질문에 쉽게 대답할 수 있나요? 어쩌면 많은 좋은 개발자 분들의 경우 이미 이런 질문이 체득 되어서 보는 즉시 대답을 하실 수도 있을 것입니다. 한편 '대답을 할 수 있다'와 '대답을 해봤다' 사이의 간극도 고려해야 합니다. 즉흥적으로 답을 내릴 수 있는 능력의 중요함 뿐만 아니라, 개발 전에 스스로 질문하고 대답해 보았는가 역시 중요하다는 의미 입니다. 위 질문을 듣고 다음과 같은 의문이 생길 수도 있습니다: #### Q. 이미 위에서 고려되어 내려온 것인데 왜 개발자가 또 생각해야 하나요? A. 문제를 분석하는 시각이 달라질 수 있습니다. 주어진 요구 사항 또한 잘 구성된 것인지 스스로 검증해야 합니다. 다시 한 번 언급하자면, 문제를 잘 분석하고 해결책을 만들 수 있어야 여러분의 몸 값이 올라갑니다(?). 그리고 일단 위라는 개념을 버리세요. #### Q. 사용자 관점에서의 고민은 PM의 역할 아닌가요? A. 개발자는 제품의 첫 번째 사용자입니다. 사용자의 관점에서 제품을 바라보면 미처 생각하지 못한 부분들이 발견되기 마련입니다. 로그인 화면 구현 요청이 왔는데 비밀번호 가리기 요구 사항이 전달되지 않았다고 비밀번호를 노출하는 일을 하지 마세요. #### Q. 사용자를 위해 기능을 개발하는데 매출을 왜 생각하나요! (버럭) A. 모든 것은 연결되어 있습니다. 회사에게 돈은 공기와 같고, 비지니스 목적에서 수익 추구는 빠질 수 없습니다. 비영리 단체도 운영 유지를 위해선 수익이 필요하죠. 돈, 수익 만을 목적으로 만들라는 의미가 아닙니다. 내가 지금 하는 일이 비지니스에 어떤 의미를 가지는지 이해해야 제대로 우선 순위를 정하고, 문제를 더 잘 분석하고 해결해 나갈 수 있다는 의미입니다. ## 버즈빌의 각 프로덕트는 어떤 의미를 가질까요 수영을 잘 하기 위해서 어떤 것들이 필요할까요? 수영 선수들은 어깨가 엄청 넓고 어깨 근육이 매우 잘 발달해 있죠. 팔 동작이 수영 속도에 큰 영향을 미치는 것은 자명합니다. 하지만 팔 동작만 잘 한다고 속도가 나지 않겠죠. 주기 함수에서 팔 동작이 진폭을 의미한다면, 발 동작은 y축 평행 이동을 책임집니다. 발차기를 통해서 꾸준한 속도를 만들어 낼 수 있죠. 이런 모든 동작은 기초 체력과 코어 근육으로 부터 나옵니다. 기본기가 되어 있어야 성장할 수 있죠. (제가 수영 전문가는 아니니 메타포로서 대충 이해 부탁 드립니다.) ![Swimming metaphor](https://tech.buzzvil.com/blog/developers-attitude-towards-products/swimming.jpg) 우리 회사 내의 제품군과 내가 개발하고 있는 기능들이 어떤 역할을 하는지 생각해 보면 비지니스에 대한 이해도와 제품 및 기능의 의미를 좀 더 명확하게 느낄 수 있습니다. (각 버즈빌 프로덕트의 의미는 좋은 내부 토론 주제 입니다. 이 글에서는 설명 없이 넘어가도록 하겠습니다.) ## 우리의 프로덕트가 버즈빌 성장에 어떻게 영향을 미칠까요 서비스의 확장성(Scalability)과 관련해서 스케일 큐브(Scale Cube)라는 개념이 있습니다. 수평적으로 값을 복제하여 사용하는 X축, 기능 별로 묶어서 제공하는 Y축, 그리고 비슷한 것들을 묶어서 파티셔닝하는 Z축 확장을 모두 활용하면 이론적으로 무한한 확장이 가능하다고 합니다. ![Scale cube](https://tech.buzzvil.com/blog/developers-attitude-towards-products/scalecube.png) 이 개념은 기술적인 확장성 외에도 생각보다 많은 경우에 적용이 가능합니다. 서비스 관점에 이를 적용해 볼까요? 우리는 현재 이뤄낸 성공을 복제하면서 X축 성장을 도모하고, 우리가 가진 기능과 효율을 늘려감으로써 Z축 성장을 도모할 수 있으며, 우리 시스템이 가진 독립적인 기능을 따로 제공함으로써 Y축으로도 성장할 수 있습니다. ![Buzzvil scale cube](https://tech.buzzvil.com/blog/developers-attitude-towards-products/bzscalecube.jpg) ## 마무리 대학교 재학 중 사진 동아리 활동 시절, 생각보다 많은 동아리 친구들이 사진이 아닌 카메라에 빠지곤 했습니다. 카메라의 성능에 관심이 많았고 카메라 조작 기술에 심취했죠. 좋은 사진을 찍는데 있어서 좋은 카메라와 좋은 기술은 매우 중요한 재료지만, 사진 자체에 대한 고민과 깊은 이해가 없다면 어느 순간 길을 잃게 되지 않을까요? 이 글이 우리가 개발자로서 무엇을, 왜, 어떻게 하고 있는지 고민하는데 조금이나마 도움이 되기를 바랍니다. 모든 개발자 여러분 화이팅 👍 --- ## [안드로이드 개발자의 서버 개발기](https://tech.buzzvil.com/blog/tech-blog-developing-server-as-android-developer) Date: 2020-05-29 | Author: Summer Kwak | Category: Frontend 안녕하세요. 곽서현입니다. 저는 버즈빌에서 안드로이드 개발자로 일하고 있습니다. 이번 글에서는 제가 서버 개발을 시작하면서 겪었던 시행착오와 제가 서버 개발에 적용할 수 있도록 해준 버즈빌의 가이드에 대한 소개를 해보려고 합니다. 주니어 개발자를 온보딩 시키거나 타직군에서 넘어오는 경우 어떻게 실무에 적응 시킬 수 있는지 고민하시는 분들에게 도움이 되길 바랍니다. # 0. Motivation 안드로이드 개발을 하고 있는 제가 이번에 서버 개발을 하게 된 이유는 모든 회사가 항상 겪고 있는 고질병인 리소스 부족이었습니다. 물론 서버 개발할 사람이 부족하니 "너가 해!" 해서 이렇게 된 것은 당연히 아닙니다. 개발해야 할 일이 생겼고 마침 서버 개발을 하고 싶었던 제가 일을 맡게 되었습니다. 신규 서비스를 개발 중인 저희 팀은 안드로이드 개발자 2명, 디자이너 1명, PM 1명으로 이루어진, 작지만 전형적인 스타트업 같은 팀입니다. 팀에서 실험적인 기능 하나를 기획했지만 서버를 개발할 서버 엔지니어가 없었습니다. 하지만 우리 팀은 저를 포함하여 당장 이 기능을 런칭해서 데이터를 보고 싶어하는 조급한 이들로 이루어져 있었기에 '안되면 내가 하지 뭐' 라는 단순한 생각으로 시작하게 되었습니다. 서버 개발을 만만하게 본 것은 아니었지만, 예상 사용자 수가 DAU의 10% 정도로 작은 기능 - 물론 바람은 더 높습니다만 - 이었기 때문에, 기존 서버 개발자분들의 도움을 받는다면 가능할 것 같았습니다. 버즈빌에서는 빠르게 개발하고 빠르게 테스트해 보는 문화를 장려하기 때문에 난데없이 안드로이드 개발자가 서버 개발을 한다고 했을 때 말리는 사람은 아무도 없었습니다. 다른 직군에서 보면 개발자는 다 똑같이 까만 화면에 알록달록한 글씨를 타이핑하는 사람들이겠지만 사실 개발 분야는 꽤 다양하게 나누어져 있고 분야 간 이동도 적습니다. 하지만 개발자들은 자기 서비스를 처음부터 끝까지 자기 손으로 만들어 보고 싶다는 욕구를 다들 조금씩은 가지고 있기 마련입니다. 지난여름 회사 내의 개발자와 디자이너들과 새로운 서비스를 만들어 보는 모임을 만들었는데 (버즈빌에는 스터디 활동이 정말 활발합니다. 요새는 코로나 때문에 못 모이고 있네요.) 거기서 go project와 react를 써볼 일이 있었고 꽤 재밌었습니다. 버즈빌에서는 개발자들의 자기 개발을 적극적으로 지원하고 있고, 개발자가 원한다면 다양한 직무를 맡을 수 있도록 장려하고 있습니다. # 1. 시작하기 일단 제가 서버개발을 시작한다고 하니 서버 개발자 한분이 도와주시겠다고 했습니다. 그리고 저에게 가이드 문서를 주셨습니다. 처음 써보는 것들이 많아 명확히 이해하진 못했지만, 전체적인 개발 플로우를 익힐 수 있었습니다. 다음은 버즈빌 서버 개발을 하면서 쓴 툴, 언어들입니다. - Visual Studio Code - Go - Docker, docker compose - cookiecutter - swagger - kubectl ## Design Canvas 자, 이제 서버 개발을 해 봅시다. 그런데 잠깐, 바로 코딩하는 게 아니네요? 첫 번째 할 일은 디자인 캔버스 작성입니다. 버즈빌에서는 마이크로서비스 아키텍처를 지향하고 있고 신규 서비스는 새로운 마이크로 서비스로 만들어 배포하고 있습니다. 새로운 서비스 작성자는 해당 서비스가 왜 필요하고 어떤 기능을 하는지 등을 포함한 디자인 캔버스를 작성한 후, 다른 서버 개발자, PM 분들과 함께 리뷰를 진행합니다. 다음은 디자인 캔버스에 포함되는 사항입니다. - 서비스 목적 - 이 서비스에서 다루는 범위 / 다루지 않는 범위 - API - 마일스톤 - dependency가 있는 다른 서비스 - 예상 request e.g. high throughput ![](https://tech.buzzvil.com/blog/tech-blog-developing-server-as-android-developer/images/designcanvas.png) 디자인 캔버스를 작성함으로써 마이크로 서비스의 목적과 범위를 명확히 하고 작성자와 팀 간에 무엇을 어떻게 만들지 합의를 보게 됩니다. 팀 내에서 다 같이 기획을 했기 때문에 범위를 정하는 건 수월했는데, request 예상은 어떻게 해야 할지 감을 잡기 힘들어서 다른 분들이 작성하신 디자인 캔버스를 많이 참고했습니다. 디자인 캔버스를 작성하고 저 포함 5명 정도의 사람을 모아 리뷰를 진행했습니다. 디자인 캔버스의 리뷰에는 이 서비스를 왜 만들어야 하는지 뿐 아니라 model 이름과 db schema에 대한 대략적인 방향까지 포함됩니다. 수정을 거쳐 드디어 통과됐습니다! ## API 설계 그다음 단계는 디자인 캔버스를 통해 합의한 API를 protobuf 로 작성해야 합니다. protobuf란 grpc로 통신하기 위해 필요한 API를 작성하는 방식입니다. protobuf 나 API first approach 같은 부분은 버즈빌의 수요 데브 세미나에서 여러 번 다루어졌던 주제라 당황스러운 내용은 없었습니다. 버즈빌은 protobuf로 작성되는 API를 하나의 리포에서 관리합니다. pr이 마스터에 머지되면 해당 패키지에 있는 API는 swagger를 통해 protobuf 와 java로 된 http클라이언트로 빌드하고 배포합니다. 그럼 안드로이드에서는 http 클라이언트를 가져다 쓰면 되니, 안드로이드 쪽에서 API 관련 코드를 작성하는 수고가 없어집니다. API가 작성되면 가져다 쓰던 안드로이드 개발자 시절에는 별생각 없었는데 직접 API를 작성해 보니 생각해야 할 게 꽤 많네요. 그래도 봐 오던 게 있으니 부딪혀 봅니다. 서당개 삼년이면 풍월을 읊는다고 남들이 어떻게 하는지 잘 관찰하는 것도 자신의 성장에 큰 도움이 됩니다. 그리고 풍월을 잘 읊으려면 좋은 서당에 있어야겠죠? - API 이름이 간결하고 의미를 잘 전달하는가 - 리소스 이름이 restful한가 - 앱에서 호출하는 API 인지, 내부용 API 인지 - 불필요하게 중북된 리소스는 없는지 API를 작성하면서 좋았던 것은 클라이언트 개발자 입장에서 어떤 API를 호출해서 어떤 기능을 수행하고, 화면이 어떻게 구성될지, 어떻게 바뀔지 그려볼 수 있다는 점이었습니다. protobuf 함수 이름은 디자인 캔버스 작성할 때 이미 합의를 했기 때문에 별문제 없었는데 restful한 리소스 이름 정하는 게 고민이 됐습니다. 애매한 지점이 있을 때 일단 pr을 만들고 리뷰어 분과 논의해서 정했습니다. 몇번의 핑퐁 끝에 API 작성도 클리어했습니다. # 3. Go project 개발 ## Bootstrap 이제 진짜 go project를 시작할 준비가 되었습니다! 뭐든지 부트스트랩이 가장 헷갈리잖아요? 어디서부터 시작하지? 하는 것이 처음 도전할 때 가장 큰 허들 일 것 같아요. 안드로이드 개발은 대체로 Android Studio를 설치하면 준비가 완료되지만 서버 개발은 뭐부터 해야 할지 막막했습니다. 하지만 버즈빌에서는 처음 해보는 사람이 당황하지 않도록 템플릿이 준비되어 있습니다. 버즈빌 개발팀에서 cookiecutter를 이용해 프로젝트 템플릿을 미리 설정해 두었고 저는 템플릿을 이용해 리소스 이름, 패키지 이름, 프로젝트 이름만 넣으면 go project를 생성할 수 있었습니다. cookiecutter는 프로젝트 템플릿을 설정할 수 있는 유틸리티입니다. 프로젝트를 열어 보면 cmd, internal 패키지와 그 아래 usecase, repo, entity 파일, 그리고 go mod와 docker compose yml 까지 다 들어있네요! 저는 정해진 위치에 필요한 로직만 추가하면 됩니다. 참고로 IDE는 VSCode를 사용했고 docker compose를 이용해 devcontainer를 띄워 그 위에서 작업했습니다. VSCode의 Remote Container extension을 이용해 컨테이너 내부에 개발 환경을 설정할 수 있습니다. ([여기서](/blog/vs-code로-컨테이너-안에서-개발하기/) 자세히) docker compose는 개발용 환경설정 및 개발에 필요한 database를 도커에 만들어주는 등 컨테이너 실행에 필요한 옵션을 관리해 주는 역할을 합니다. ## Presentation layer development cookiecutter로 생성된 고 프로젝트는 presentation layer, domain layer, data layer로 나누어져 있고 각각의 이름은 server, usecase, repo 입니다. 개발자 마다 다르겠지만 저는 server 부터 작업을 했습니다. repo는 가장 마지막에 작업했고 따라서 local db 세팅도 가장 나중에 했습니다. presentation layer의 작업은 크게 많지는 않았습니다. protobuf를 미리 정해 놓았기 때문에 API에 맞도록 코드를 작성하면 됩니다. cookiecutter로 main 함수의 대부분이 설정되어 있기 때문에 첫 pr은 비교적 쉽게 날릴 수 있었습니다. ## Test pr을 보내기 전에 먼저 잘 돌아가는지 테스트를 해봐야겠죠? docker-compose.test.yml을 만들고 docker 설정을 해주면 test환경을 만들어 줄 수 있습니다. 서버를 실제 띄워 본 것은 아니고 테스트를 작성해서 검증했습니다. TDD로 개발하는 게 정말 편하기도 했지만 이래도 되는 걸까 하는 마음이 동시에 들었습니다. 안드로이드에서는 integration test가 시간이 오래 걸려서 디바이스에 직접 실행해 보는 것이 더 편합니다. 물론 안드로이드 팀에서도 테스트를 중요시하고 있지만 integration test, ui test가 느리고 힘들기도 하기 때문에 디바이스 한번 켜보지 않고 이 코드는 잘 돌아간다고 쉽게 말하기는 힘들 것 같습니다. 그래서 TDD가 서버 개발에 있어서 가장 적응이 안 되면서도 가장 편하다고 생각했던 부분이었습니다. 테스트를 처음 돌리기까지 많은 삽질이 있었습니다. docker를 실행시키고 vscode에서 test 환경의 docker image를 빌드해서 그 위에서 작업했는데 이런 개념에 익숙해지는데 시간이 걸렸습니다. 그래도 최근에 만들어진 다른 마이크로 서비스들이 제가 작업하는 것과 비슷한 환경으로 개발되었기 때문에 참고할 수 있는 것이 정말 감사했습니다. ## Pull request 모든 팀의 PR이 그렇겠지만 서버팀에 보낸 PR에서도 다른 개발자분들이 많은 꿀팁을 전수해 주셨습니다. error 핸들링은 어떻게 할지, 컨벤션이 어떤 건지 등등 다양한 의견이 있습니다. 저희 팀에 주니어분이 오시면 저도 더 상세하게 리뷰를 해야겠다는 생각도 듭니다. ![](https://tech.buzzvil.com/blog/tech-blog-developing-server-as-android-developer/images/group2.png) ![](https://tech.buzzvil.com/blog/tech-blog-developing-server-as-android-developer/images/frame1.png) 그리고 첫번째 pr이라고 축하도 받았습니다! ![](https://tech.buzzvil.com/blog/tech-blog-developing-server-as-android-developer/images/group1.png) 참고로 커피는 아직 못 받았습니다. 하하. ## Domain, Data layer development 이제 domain layer와 data layer 작업만 남았네요. domain layer 작업을 마치고 마지막 data layer 작업을 위해 도커로 db를 세팅했는데 정말 편했습니다. db 설정이 몇 줄 설정만 가지고 된다는 게 정말 좋네요. 디비로 mysql을 설정해 놓았는데 orm을 뭘 쓸지, migration은 어떻게 할지 정해야 합니다. 당시 서버팀에서 gorm과 migrate에서 다른 툴로 옮겨 가려고 실험할 때라 모든 서비스가 사용하는 툴이 통일되어 있진 않았는데 논의 끝에 raw SQL과 goose를 쓰기로 일단 합의가 되었습니다. raw SQL은 딱히 더 알아야 할 툴이 없는 것은 편했고 SQL을 다 작성해 주자니 귀찮은 면도 있었습니다. 여기에 대해서는 개발자 분마다 생각이 다를 것 같습니다. repo 작업까지 마치고 pr에 대한 리뷰를 받아보니 제가 작업하면서 이게 도메인 로직인지 아닌지에 대한 고민이 좀 부족했구나 하는 것이 느껴졌습니다. 예를 들어 모델을 데이터베이스에서 가져오는 작업을 수행하는데, 가져올 때 없으면 생성하고 있으면 저장된 걸 꺼내오는 로직을, 저는 데이터 레이어에서 할 일이라고 생각했습니다. 그런데 리뷰해주신 분과 얘기하다 보니 그건 도메인 레이어로 보는 것이 맞는 것 같았습니다. Upsert를 하는 과정에서 데이터 레이어의 interface가 지저분해진 면도 있었고요. 그 부분을 도메인 레이어로 옮기니 데이터 레이어가 한결 깔끔해집니다! interface에서 `GetLotteryTicket` 이 세 가지 interface로 분리되었지만, implementation에서는 정리가 더 잘 되었습니다. 도메인 로직을 잘 분리하는 것은 안드로이드 개발에서도 중요한 부분이기 때문에 이번에 이렇게 더 배우게 되어 좋았습니다. 안드로이드는 뷰까지 가세해 코드가 더 복잡하고 코드의 양이 훨씬 많아서 레이어를 나누는 부분에 있어서 더 신중하게 고민해야겠다는 생각이 듭니다. | Before ```go type Repo interface { GetLotteryTicket(accountID int64, appID int64, lotteryKey string) (*LotteryTicket, error) } ``` | After ```go type Repo interface { GetLotteryTicket(accountID int64, lotteryID int64) (*LotteryTicket, error) GetLotteryByKey(key string) (*Lottery, error) SaveLottery(lottery *Lottery) error } ``` # 4. Deployment 마지막으로 배포는 제가 거의 신경을 쓰지 않았는데, 그 이유는 버즈빌의 devops 팀에서 다 알아서 해주셨기 때문입니다.(고마워요 ㅠㅠ) CI설정, production/staging db 세팅 등 코드가 실제 배포되기까지의 수많은 일을 담당해 주셨는데 이 부분에서 좋았던 점은 많은 부분이 템플릿화가 되어 있어서 각 서비스에 맞게 조금씩만 수정해 주면 처음부터 세팅하지 않아도 된다는 점이었습니다. 그러고 보면 서버 개발의 많은 부분이 템플릿화가 되어 있어서 휴먼에러를 최대한 줄여주고 있습니다. 이런 점은 서버 개발을 하면서 큰 도움이 되었습니다. 이 모든 것은 셋업해주신 버즈빌 개발자분들 만세! # 5. 회고 ## 좋았던 점 작업을 하면서 가장 감사했던 부분은 다른 개발자분들의 리뷰와 관심이었습니다. 특히 제가 작업한 서비스와 전혀 관계없는 개발자분도 이렇게 주말 동안 저의 코드를 읽고 직접 pr까지 보내주셨습니다. 비록 merge는 되지 못했지만, 덕분에 go로 작업하는 부분에 힌트를 많이 얻을 수 있었습니다. ![](https://tech.buzzvil.com/blog/tech-blog-developing-server-as-android-developer/images/group3.png) 좋았던 점 하나를 더 꼽자면 서버 개발, 배포 과정과 로그를 볼 수 있는 방법들을 알게 되었다는 점입니다. 나중에 안드로이드 개발하다가 뭔가 버그가 있는 것 같을 때 간단한 트러블슈팅 정도는 할 수 있게 된 것 같아 뿌듯하네요. 다른 서비스들에도 PR 날려보고 싶어졌습니다. ## 힘들었던 점 서버 개발을 하면서 힘들었던 점은 인프라 권한을 받는다거나 데이터베이스 마이그레이션 하는 툴은 따로 설치해야 한다거나 하는 식의 미리 설정해 둬야 하는 부분이 많다는 점이었습니다. 컨플루언스로 정리가 잘 되어 있어서 따라 하는 데 큰 무리는 없었지만, 간혹 막히면 막막해졌습니다. 문서에는 트러블슈팅이 함께 들어가면 좋을 것 같습니다. ## 그래서 앞으로는? 한동안 클라이언트 개발만 하다가 오랜만에 서버 작업을 하니 개인적으로 꽤 재밌는 시간이었습니다. 속도가 아주 빨랐다고는 할 수 없지만 그래도 계획한 시간과 크게 틀어지진 않았던 것으로도 만족합니다. 오지랖일 수도 있지만, 팀에서 필요한 서비스가 있다면 제가 하겠다고 해볼 수 있을 것 같습니다. 회사 내의 여러 개발자분과 협업하고 배울 수 있는 시간은 뭐든 소중합니다. 앞으로 또 기획부터 개발, 배포까지 혼자서 하게 될 일이 또 있을지 모르겠지만 또 하게 된다면 이번 배움을 바탕으로 더 잘 할 수 있을 것 같습니다. 그러려면 이번에 겪었던 일들을 문서로 잘 정리해 두어야겠죠. 정리된 문서는 개발팀 내부에 공유할 예정입니다. 서버 개발자분이 안드로이드 개발을 맡게 되시거나 주니어 개발자분이 팀에 합류하게 될 때 잘 해드려야겠다는 생각도 듭니다. 문서정리도 중요하지만, 같이 일하고 서비스를 만들어나가는 느낌을 받는 게 좋았거든요. 개발팀은 드라이하게 일한다는 생각을 할 수도 있겠지만 개발팀의 문화가 개발자 개개인에게 큰 영향을 주기도 합니다. 하고 싶은 게 있을 때 해보라고 기회를 주고 서로에게 배우려는 문화가 있는 조직에서 일하게 되어 다행이라는 생각이 듭니다. --- ## [VS Code로 컨테이너 안에서 개발하기](https://tech.buzzvil.com/blog/vs-code로-컨테이너-안에서-개발하기) Date: 2019-11-12 | Author: Whale Lee | Category: DevOps 컨테이너 환경은 날이 갈 수록 더 많은 사람에게 사랑 받고 있죠. 처음부터 Docker, Kubernetes 등을 활용해 컨테이너 기반의 배포를 사용하기 시작한 사람들은 capistrano나 fabric 같은 도구를 사용한 배포 환경에서 나타날 수 있는 정말 다양하고 예상하기 힘든 문제들이 어떤 지옥인지 상상하기 어려울 것입니다. 컨테이너는 배포 환경과 개발 환경을 최대한 맞춰주는 역할도 수행할 수 있습니다. docker-compose를 잘 활용하면 큰 무리 없이 실제 배포 환경과 거의 비슷한 환경을 구성할 수 있죠. 로컬 환경에서 실행 된다면 배포 시점에도 실행될 것이라는 자신감을 얻을 수 있습니다. 다들 불안해 하면서 개발하고 더 불안해 하면서 배포하기는 정말 싫잖아요? 하지만 사람의 욕심은 끝이 없죠. 다들 메모장으로 개발하지 않는 이상 특정 IDE를 활용하고 있을 텐데요, IDE에서도 컨테이너 환경 안에서 개발할 수 있다면 얼마나 좋을까요? 이 글에서는 **컨테이너 안에서 개발할 수 있는 환경이 필요한 이유**와 **VS Code를 사용해서 컨테이너 내부 개발 환경을 구성하는 방법**을 정리해 보겠습니다. # 컨테이너 환경에서 동작하는 IDE가 왜 필요할까? ## 개발자 사이 개발 환경 통일 보통 Repository를 clone하고 맨 처음 하는 일은 README.md를 정독하는 일입니다. 어떻게 개발 및 테스트를 진행하는지 알아야 하니까요. 다행이 대부분의 언어들이 package manager를 가지고 있죠. gem, npm, pip, mod 등을 활용해서 라이브러리를 가지고 옵니다. `pip install -r requirements.txt` 하면 짜잔, 완료일까요? 에고 Python 버전이 다르네요. Python 3.8.0을 설치해야겠어요. 설치하려고 보니 다른 프로젝트에서는 여전히 python 2를 사용 중입니다. 프로젝트 왔다 갔다 할 때마다 python을 설정해 줄 수 없으니 virtualenv를 사용해야 겠군요. rvm, nvm, ... 뭐 이리 해야 될 것들이 많은지. 애써 Docker를 사용해서 컨테이너화 시켰지만 아직 많은 경우 이와 같은 행동을 반복합니다. 컨테이너 안에 개발 및 디버깅에 필요한 도구들이 들어있지 않거든요. IDE에서 지원하는 intellisense 기능을 사용하려면 필요하기도 하구요. 컨테이너는 만들었지만 로컬 환경에서 개발할 때는 정작 쓰지를 못하는군요. ## 테스트 환경 구성 docker-compose up으로 프로그램 실행은 시켜도 테스트는 여전히 로컬 테스트 환경을 사용하기도 합니다. 개발 환경과 배포 환경 사이의 의존성 차이 때문에 컨테이너 내부에서 테스트 환경이 구성되어 있지 않을 수도 있고, 디버거를 붙여서 디버깅 하기 어렵기도 합니다. Remote debugger를 지원하는 언어들도 있지만 테스트를 실행하고 나서 디버거를 연결해야 한다거나, IDE에서 테스트 버튼 하나 눌러서 디버깅을 하기 어려운 등의 난관이 있습니다. 반면 로컬 테스트 환경을 사용할 경우 integration test, acceptance test 진행 시 외부 디펜던시 연결이 상당히 귀찮은 문제가 발생합니다. docker-compose를 잘 구성해두면 커맨드 하나로 test db들을 다 띄워서 손쉽게 연결할 수 있는 반면, 로컬 환경을 사용하면 mysql 띄우고, redis 띄우고, es 띄워서 해당 host들 환경 변수로 다 따로 설정해줘야 하죠. 그거 귀찮다고 unit test만 구성할 수는 없잖아요? # 구성 방법 자세한 내용은 [https://code.visualstudio.com/docs/remote/containers](https://code.visualstudio.com/docs/remote/containers "https://code.visualstudio.com/docs/remote/containers") 문서에서 확인할 수 있습니다. 이 글에서는 기본적인 설정 방법만 간단하게 적어보겠습니다. VS Code의 Remote Container extension을 사용하면 컨테이너 내부에 개발 환경을 설정할 수 있습니다. docker-compose와 비슷한 방식으로 Dockerfile을 활용해서 개발 환경을 설정하는 방식이죠. 프로젝트 루트에 `.devcontainer` 폴더를 만들고 `devcontainer.json` 파일을 추가해서 개발 환경을 설정합니다. { "name": "Python 3", "dockerComposeFile": [ "../docker-compose.yml", "docker-compose.yml" ], "service": "test", "workspaceFolder": "/app", "settings": { "python.pythonPath": "/usr/local/bin/python", "python.linting.flake8Enabled": true, "python.testing.pytestEnabled": true }, "postCreateCommand": "apt-get update && apt-get install -y git && pip3 install flake8", "extensions": [ "ms-python.python", "littlefoxteam.vscode-python-test-adapter" ] } 위 파일은 python 프로젝트를 위한 devcontainer.json 파일 예시입니다. 프로젝트 실행을 위한 `docker-compose.yml` 를 추가하고, 개발 시 특정 값을 덮어쓰기 위해 `.devcontainer/docker-compose.yml` 도 함께 설정합니다. `settings` 필드에는 공통적으로 사용할 vs code 설정 값을 추가합니다. 개발자마다 다르게 설정할 수 있는 값은 각자 로컬 설정 파일에 추가하면 됩니다. `postCreateCommand` 를 사용해서 개발에 필요한 디펜던시들을 추가합니다. 이 부분은 당연히 배포를 위한 Docker 이미지에는 포함되지 않습니다. `extensions` 에는 VS Code에서 사용할 extension을 추가합니다. 컨테이너 내부에서 실행되는 환경이기 때문에 vs code 실행 후 설치한 extension 들은 해당 이미지를 다시 빌드시 다 사라집니다. 이 곳에 추가해두면 이미지를 빌드할 때마다 해당 extension 들을 설치한 상태로 vs code가 실행되게 됩니다. ## 실행 방법 extension이 잘 설치되어 있고 .devcontainer 폴더가 잘 구성했다면 **Remote-Container: Reopen in Container** 커맨드를 실행합니다. ![](https://tech.buzzvil.com/blog/vs-code%eb%a1%9c-%ec%bb%a8%ed%85%8c%ec%9d%b4%eb%84%88-%ec%95%88%ec%97%90%ec%84%9c-%ea%b0%9c%eb%b0%9c%ed%95%98%ea%b8%b0/1_oi_iujgkzbntwm_jbjsq1w.png) 짜잔, 컨테이너 환경에서 VS Code가 실행되었습니다! # Conclusion 해당 기능은 2019년 5월에 처음 나온 따끈따끈한 기능입니다. 그만큼 아직 불안정한 부분도 있습니다. 하지만 테스트 결과 충분히 시도할 만한 가치가 있습니다. 프로젝트에 신규 개발자가 들어올 때마다 일일이 가서 세팅해주는 일은 이제 슬슬 필요 없을 때가 된 듯 합니다. ## 컨테이너 기반 IDE 설정의 장점 * rvm, virtualenv 등 프로젝트별 개발 환경 설정이 따로 필요하지 않다. * DB 등 외부 서비스 의존성이 있는 테스트 진행이 매우 수월하다. * 개발자 사이 개발 환경을 손쉽게 맞출 수 있다. ## 컨테이너 기반 IDE 설정의 단점 * 자원을 많이 사용한다. * 따라서 배터리 소모가 크다. * Intellisense 등의 기능이 약간 느릴 수 있다. --- ## [gRPC를 쓰면 REST가 공짜!?](https://tech.buzzvil.com/blog/tech-blog-grpc를-쓰면-rest가-공짜) Date: 2019-09-01 | Author: Whale Lee | Category: Backend gRPC는 구글에서 만들고 오픈 소스로 운영 중인 RPC(Remote Procedure Call) 프레임워크입니다. Protocol Buffers를 사용하여 서비스를 쉽게 정의하고 사용할 수 있습니다. HTTP/2를 기반으로 구성되어서 상당히 빠르며, 양방향 스트리밍도 가능합니다. 다양한 장점에도 불구하고 익숙한 HTTP/JSON을 뒤로한 채 낯선 gRPC를 도입한다는 것이 쉬운 결정은 아닙니다. Protocol buffers 도 공부해야 하고, 서버 구성도 바꿔야 하고, 에러 로깅, 디버깅 방법 등 고려해야 할 것들이 정말 많죠. 버즈빌은 많은 고민 끝에 gRPC를 도입하기로 결정했고 다양한 곳에서 gRPC를 사용하고 있습니다. 이 글에서는 gRPC를 도입한 이유와 방법, 그 과정에서 배운 것들을 공유하겠습니다. # 왜 gRPC? 버즈빌 시스템은 마이크로서비스 아키텍처(MSA, Microservice Architecture)를 적용하고 있습니다. 버즈빌에서 왜 마이크로서비스 아키텍처를 사용하는지는 [https://medium.com/@ssowonny/](https://medium.com/@ssowonny/ "https://medium.com/@ssowonny/")[버즈빌-소프트웨어-아키텍트-팀-8f1278c9d455](https://medium.com/@ssowonny/%EB%B2%84%EC%A6%88%EB%B9%8C-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%95%84%ED%82%A4%ED%85%8D%ED%8A%B8-%ED%8C%80-8f1278c9d455) 글에 설명해 두었습니다. MSA를 구성하다보면 다양한 문제들을 맞이합니다. 중요한 허들 중 하나는 네트워크 속도입니다. 모놀리식(Monolithic) 아키텍처에서 모듈 간의 통신은 하나의 프로세스 안에서 이루어집니다. 반면 MSA 에서는 다양한 서비스들이 다른 프로세스, 다른 호스트에서 돌아가고 있으며, 각 서비스 사이의 통신이 매우 빈번하게 일어납니다. Protocol Buffers 를 사용하는 gRPC는 일반적으로 HTTP/JSON 요청보다 빠른 속도를 보입니다. 물론 요청 한 번에 엄청나게 많은 시간을 절약해 주는 것은 아닙니다. 많은 지식과 코드가 필요한 것에 비해서 얻는 속도는 몇 ms 없을테니까요. 다만 여전히 하나의 요청에 많은 내부 요청을 사용하는 마이크로서비스 아키텍처를 위해서는 네트워크 요청 시간을 조금이라도 더 줄이는 것이 매우 중요합니다. 서비스 간의 통신 규약을 정하는 것도 매우 중요합니다. 서비스 사이 의존성이 낮아지면서 얻은 장점 대신, 각 서비스가 변경 되면서 발생할 수 있는 문제들이 생겨나게 되었죠. JSON을 통한 직렬화(Serialization)는 서비스 사이에서 쉽게 규약이 어긋나거나 실수하기 쉽고, API Route 역시 변경 사항을 널리 적용하기 어렵습니다. 물론 잘 구성된 문서를 통해 규약을 맞추거나, OpenAPI(Swagger) 같은 도구를 사용하는 방법도 있습니다. 하지만 이 역시 학습 비용이 필요합니다. 어차피 학습해야 한다면, 성능이 더 좋은 것을 학습하는 것이 좋지 않을까요? # gRPC를 구성해보자. gRPC는 Protocol Buffers를 통해 클라이언트 코드와 서버 인터페이스 코드를 생성합니다. 옵션에 따라 생성하는 언어를 변경할 수 있죠. 하나의 `proto` 파일을 사용해서 go, python, java, swift 등 다양한 언어의 서버/클라이언트 코드를 생성할 수 있습니다. 버즈빌에서는 Protocol Buffers로 씌여진 서비스 정의를 모아두기 위해 `buzzapis` 레포지토리를 구성했습니다. 해당 repository 안에는 버즈빌에서 구성한 모든 마이크로서비스의 proto 파일이 들어 있습니다. 덕분에 한 눈에 어떤 서비스가 어떤 API를 제공하는지 알 수 있죠. ```sh packages ├── auth │ ├── auth.proto │ └── package.json ├── geo │ ├── geo.proto │ └── package.json ├── reward │ ├── reward.proto │ └── package.json ... ``` `buzzapis`에서 API 변경이 일어나면 자동으로 배포를 진행합니다. Pull Request를 통해 변경 사항을 리뷰하고 `master` 브랜치에 머지되면, 변경된 package는 구성되어 있는 CD(Continous Delivery)를 통해서 각 언어들로 빌드됩니다. 이렇게 빌드된 라이브러리들은 각자의 패키지 매니저(e.g PyPI, bintray)로 배포되죠. 듣기에 간단한 작업이지만 각 스텝마다 해결해야 할 문제 상황들이 발생합니다. # Protocol Buffers를 컴파일해보자. `protoc`를 설치한 뒤 아래 명령만 수행하면 해당 언어로 컴파일 됩니다. 참 쉽죠? $ protoc -I=packages/auth --go_out=build/go/auth auth.proto # 어떻게 변경된 것만 빌드하지? 마지막으로 Makefile 을 건드린게 언제인지 기억 나시나요? 대학교를 졸업하면 잘 안 건드리는 것 중 하나가 Makefile 이죠. 헌데 서비스가 많아지고 다양한 언어와 환경을 손쉽게 조절하려고 생각해보면 갑작스레 Makefile 이 생각날 때가 있습니다. protoc를 사용해서 변경된 proto 파일을 컴파일하기에 Makefile 은 정말 유용한 도구 입니다. ``` build/go/%.pb.go: %.proto protoc -I=$(dir $<) -f $(notdir $<) --go_out=$(dir $@) ``` # 배포는 어떻게 하지? Protocol Buffers를 사용해 서비스를 쉽게 정의했다면, 정의한 서비스를 쉽게 구성하고 사용하는 것 또한 중요하겠죠. 앞서 언급 했듯이 Protocol Buffers로 정의된 서비스는 각 언어에서 사용하는 Package Manager로 자동 배포됩니다. 각 언어로 빌드 된 라이브러리를 자동으로 배포 하려면 아래와 같은 조건이 충족되어야 합니다: 1. 최근 배포 이후 수정 사항이 있는 서비스만 배포할 수 있다. 2. 각 서비스별로 버전을 관리할 수 있다. 3. 언어별로 라이브러리 배포에 필요한 파일(e.g [setup.py](http://setup.py/), .gemspec, build.gradle)을 생성할 수 있다. ## **lerna** 버즈빌에서는 `lerna`를 활용해서 이런 작업들을 좀 더 쉽게 수행합니다. lerna는 npm 관리를 위해 제작된 툴이지만, 모노리포에서 여러 프로젝트를 손쉽게 관리하는데 유용하게 사용될 수 있습니다. `lerna`를 사용하면 각 폴더 안에 존재하는 `package.json`을 바탕으로 버전을 관리할 수 있습니다. ```sh $ npx lerna list -l lerna notice cli v3.13.3 lerna info versioning independent auth v0.1.2 packages/auth config v0.1.12 packages/config ... ``` 또한 변경 사항이 있는 package 들만 버전을 올릴 수도 있죠. ```sh $ npx lerna version patch --no-push --yes ``` `Gomplate`와 같은 템플릿 도구를 사용하면 package.json 파일 내용을 바탕으로 언어별 라이브러리 배포에 필요한 파일들을 생성할 수 있습니다. `Makefile`을 활용하면 이 같은 도구들을 사용해서 수정 사항이 있는 패키지들만 빌드하고, 배포에 필요한 파일을 생성한 뒤, 각 언어별 라이브러리 레포지토리에 배포할 수 있습니다. # 보안은 어떻게 하지? 회사 내부에서 사용하는 서비스 정의와 서버 인터페이스, 클라이언트 라이브러리를 외부로 공개할 수는 없겠죠. 이를 위해서 Private Package Manager 관리가 필요합니다. * Python 은 PyPI 서버를 설치하여 관리하고 있습니다. Pip을 사용해 설치할 때 — external-index-url 옵션을 사용해서 private PyPI 서버를 지정하게 할 수 있습니다. 이 PyPI 서버의 접근만 제어할 수 있다면 라이브러리를 외부로 공개하지 않으면서 손쉽게 디펜던시를 관리할 수 있습니다. * Java 라이브러리는 고맙게도 Bintray가 권한 관리 부분을 해결해주고 있습니다. * Go 의 경우 go mod를 사용하고 있는데, go mod 자체가 github 등의 소스 관리 툴을 Package Manager 로 사용하고 있기 때문에 해당 repository에 대한 접근 권한만 관리해주면 됩니다. 그 외에도 대부분의 Package Manager는 private library를 배포하는 방식을 제공하고 있으니 해당 가이드에 따라서 설정이 가능합니다. # 그래도 여전히 HTTP/JSON이 필요하다. 여전히 RESTful API는 대세입니다. 특히 JSON 형태로 API를 제공하지 않는 곳을 보면 왠지 뒤쳐져 있는 서비스 같은 느낌을 받기도 합니다. 내부 서비스들 끼리는 gRPC를 사용하더라도, 같은 기능을 외부에 HTTP/JSON 형태로 제공해주고 싶은 욕심이 생깁니다. 좋은 것은 나눌 수록 신나니까요. 그렇다고 HTTP 서버를 따로 띄우자니 관리 비용이 걱정입니다. 로직 변경이 있을 때 두 가지를 모두 변경하는 것은 여간 고통스러운 일이 아니죠. 클린 아키텍처 등을 적용해 비지니스 로직을 최대한 따로 두어 재활용 한다고 해도, 힘든 것은 여전히 힘든 것입니다. 헌데 다행이도 우리는 [gRPC를 HTTP/JSON 변환](https://cloud.google.com/endpoints/docs/grpc/transcoding)할 수 있습니다! Protocol Buffers로 적절하게 HTTP/JSON 형태 선언만 해주면 하나의 gRPC 서버로 두 형태 모두 대응할 수 있는 것이죠. 공짜로 HTTP/JSON을 지원할 수 있다는 점 역시 버즈빌이 gRPC를 사용하기로 결정하는데 큰 역할을 했습니다. ```proto rpc GetShelf(GetShelfRequest) returns (Shelf) { option (google.api.http) = { get: "/v1/shelves/{shelf}" }; } message GetShelfRequest { int64 shelf = 1; } ``` 이렇게 설정해두면 `GetShelf` RPC도 사용할 수 있고, `/v1/shelves/{shelf}` REST API도 사용할 수 있게 되는 것이죠. 서버 구현 쪽은 전혀 변경한 것이 없었습니다. # 근데 공짜 점심은 없더라. 세상에 간단한 일은 없더군요. 이 모든 과정을 쉽고 자연스럽게 개발하고 배포하려면 CI/CD를 잘 구성해야 합니다. 버즈빌에서는 Kubernetes와 Istio를 통해 Transcoder를 구성하고, Helm과 Spinnaker를 활용해서 CD를 구성하였습니다. 이 부분은 좋아요가 많이 달리면 다음 글에서 좀 더 자세히 풀어보도록 하겠습니다. # 시도할 가치가 있었다. 변화는 항상 두려움을 동반합니다. 그럼에도 그 두려움을 뚫고 새로운 시도를 할 수 있는 조직에 있다는 것은 매우 즐거운 일입니다. gRPC 도입은 그저 프로토콜의 변화만이 아닌 API 우선 법칙을 포함한 개발 문화와 방법론의 변화까지 가지고 오게 된 좋은 선택이었습니다. 여전히 넘어야 할 산들이 많이 남아있지만, 이런 산들을 넘어가면서 더욱 안정적이고 빠르고 변화에 유연한 멋진 시스템이 될 것이라고 믿습니다. --- ## [버즈빌의 소프트웨어 아키텍트는 어떤 일을 할까?](https://tech.buzzvil.com/blog/버즈빌의-소프트웨어-아키텍트는-어떤-일을-할까) Date: 2019-07-08 | Author: Whale Lee | Category: Product **버즈빌**에는 ATF(**Architecture Task Force**) 라는 이름의 팀이 있습니다. 이름대로 소프트웨어 아키텍처(Software architecture)를 고민하고 실행하는 소프트웨어 아키텍트(Software architect)가 모인 조직입니다. 2019년 1월 만들어졌으니 이제 막 만 0.5살이 되었네요. 이 글에서는 ATF 팀이 어떤 사람들로 구성되어 있고, 주로 어떤 일을 하는지 소개하려고 합니다. > _“Talk is cheap. Show me the code.” > — Linus Torvalds_ 아쉽게도 이 글에서는 코드가 등장하지 않습니다. 용어도 가능한 비 개발자가 읽기 쉽게 써볼 생각입니다. 개발자는 코드로 대화한다는 말이 있죠. 어느 정도 공감하는 말입니다만, 가끔은 코드를 내려놓고 개념을 다시 생각해보는 것도 꽤 즐겁습니다. # 소프트웨어 아키텍처, 그게 뭐야? 소프트웨어 아키텍처에 대한 내용은 이전 글 [Software architecture: The important stuff](https://medium.com/@ssowonny/software-architecture-the-important-stuff-a0c01fb9d774)에서 간단히 소개한 적이 있습니다. 간단하게 정리해 보면 소프트웨어 아키텍처는 소프트웨어를 구성하기 위해서 고려해야 하는 시스템 디자인을 포함한 중요한 결정들이라고 생각하시면 될 것 같네요. # 소프트웨어 아키텍트는 뭐야? 일단 이름은 멋지네. ![](https://miro.medium.com/max/700/0*N2zrnne39aG-fLa9) 소프트웨어 아키텍트는 소프트웨어 아키텍처를 고민하는 사람들입니다. 객체 지향 프로그래밍(OOP, Object-oriented programming)이 널리 사용되던 1990년대 후반에 등장해서 자리잡힌 역할입니다. OOP 덕분에 더 크고 복잡한 애플리케이션 구현이 가능해졌고, 그에 따라 고수준(high-level) 애플리케이션과 시스템 관리가 필요해졌기 때문이죠. **어느 회사나 소프트웨어 아키텍트는 있습니다.** 직군 이름에 소프트웨어 아키텍트라고 적혀 있는 사람이 없더라도 누군가 그 역할을 맡고 있을 가능성이 높죠. 규모가 작은 스타트업의 경우 보통 CTO가 해당 역할을 맡고 있습니다. 소프트웨어가 전체적으로 어떤 구조를 가지고 구성될 지 고민하고, 결정하고, 실행하죠. 규모가 커지거나 해당 역할이 따로 필요한 경우 엔터프라이즈 아키텍트(Enterprize architect) 혹은 치프 아키텍트(Chief architect)라는 이름으로 분리되어 아키텍트 팀을 리드하기도 합니다. # 스타트업인 버즈빌은 왜 ATF가 있지? ## 디지털 광고 시장의 복잡성 디지털 광고 시장은 생각한 것보다 훨씬 복잡합니다. > _“그까이꺼(?) 그냥 사용자한테 광고 이미지 하나 보여주면 되는 게 뭐가 복잡하냐!”_ 라고 생각하실 수도 있죠. 디지털 광고가 생겨나고 성장하면서 생겨난 개념과 기술 등을 차근차근 설명하고 넘어가면 좋겠지만, 이 주제는 추후 따로 한 번 다뤄보도록 하겠습니다. 우선 이 글에서는 애드 테크 분야에 어떤 복잡성이 있는지 간단히 나열해 보죠. Mobvista 에서 제작한 Ad Tech 101 영상을 보면 감을 잡는 데 도움이 될 수도 있겠네요. 디지털 광고, 특히 프로그래매틱 광고는 **디맨드 사이드(Demand-side)**와 **서플라이 사이드(Supply-side)**의 필요가 한 군데 얽혀서 돌아갑니다. 디맨드 사이드는 광고를 집행하고자 하는 측, 즉 광고주 측입니다. 서플라이 사이드는 자신들의 사용자에게 광고를 보여줌으로써 수익을 만들고자 하는 측으로, 일반적인 퍼블리셔(Publisher, 서비스 운영 측)가 이에 속합니다. 광고주는 광고를 보여주고 싶고 퍼블리셔는 수익을 만들고 싶으니 둘의 필요성이 딱 맞아떨어진다고 할 수 있죠. 문제는 두 그룹이 서로를 필요로 하고는 있지만 추구하는 방향은 정반대라는 것 입니다. 광고주는 가능한 한 저렴하게 광고를 집행하고 싶어 하지만, 퍼블리셔는 같은 사용자를 대상으로 가장 비싸게 광고를 집행하고 싶어 하니까요. 이를 해결하기 위해 다양한 방법들이 등장합니다. 실시간 광고 지면 입찰(Real-time bidding) 등을 통해서 경쟁을 시킨다던가, 애드 네트워크(Ad-network)를 통해서 광고와 지면을 최대한 모은 뒤 활용하기도 하죠. 이 와중에 양 쪽을 모두 만족시킬 수 있는 방법은 뭐가 있을까요? 바로 **광고의 효율을 높여주는 것**입니다. 광고주의 궁극적인 목표는 ‘광고 집행’이 아니라 광고를 통한 구매와 같은 전환, 즉 컨버젼(Conversion)에 있으니까요. 광고를 한 번 보여줄 때 단가가 좀 높을지라도, 전체 지출한 광고 비용 대비 얻은 매출(ROAS, Revenue Over Ad Spending)이 높으면 기꺼이 더 높은 금액을 지출합니다. 조직은 정말 가끔을 제외하고는 합리적인 존재니까요. 퍼블리셔 입장에서도 각 광고 노출을 최적으로 할수록 단가가 높아지니 효율이 높으면 행복해집니다. 광고 효율은 어떻게 높일 수 있을까요? 어제 이커머스 사이트에서 검색했던 제품이 어디를 가나 따라다니며 등장하던 경험 있으시죠? 관심 있는 제품을 다시 보여주면 구매 확률이 높아진다는 광고 상품(리커머스, Recommerce)의 효율이 어느 정도 검증되었거든요. 사용자를 알면 알수록 어떤 광고를 보여줬을 때 전환 가능성이 높을지 더 잘 측정할 수 있습니다. 이를 위해 두 가지 문제를 해결해야 하죠: a) **사용자를 어떻게 잘 알 수 있을까,** b) **사용자 정보를 바탕으로 어떻게 광고를 선택해야 할까**. ![](https://miro.medium.com/max/500/0*JV8HAHSTXYK8JCJt) 사용자를 어떻게 잘 알 수 있을까요? 사용자를 추정하고, 활동을 기록하면 됩니다. 참 쉽죠? 이를 위해 핑거 프린팅(Finger printing), DMP(Data Management Platform) 구성, 사용자 관심사 분석, 트랙킹 픽셀(Tracking pixel) 등 다양한 기술이 필요합니다. 정보를 바탕으로 관심사를 추출하는 기술 역시 이에 포함됩니다. 이 중 하나의 분야만 집중적으로 수행하는 회사들도 꽤 많을 정도입니다. 정보가 모였으면 다음은 추천이죠. 정보를 바탕으로 언제 어떤 광고를 보여줘야 최적의 효율을 만들 수 있을지 고민해야 합니다. 사용자가 어떤 컨텐츠에 반응을 보였는지, 해당 사용자와 비슷한 사용자들은 어떤 관심사를 좋아하는지 등의 정보를 바탕으로 최적의 광고를 찾아내죠. 추천 기술은 역사가 꽤 깊은 기술이면서도 여전히 머신러닝 기술이 빛을 발할 수 있는 정말 중요한 분야입니다. 광고가 잘 노출되었다면, **광고의 효율을 잘 측정**하는 것도 중요하겠죠. 일단 보여주고 “잘 됐겠지” 생각하는 것은 모든 시험 문제 답을 5번으로 찍어 놓고 “이 정도면 괜찮겠지” 하는 것과 비슷합니다. 최소한 몇 번 답이 가장 많이 나오는지 정도는 되돌아봐야 다음번 찍을 때 성적이 오르죠. 좋은 사용자에게 적절한 광고를 보여줘도 어떤 이미지를 보여주느냐에 따라서 효율이 달라지기도 합니다. 어떤 지면(배너, 컨텐츠 사이, 비디오 중간 등)에서 보여주느냐에 따라서도 아주 다르죠. 심지어 언제 보여줘야 최적의 효율인지도 다릅니다. 최적의 효율을 내려면 A/B 테스팅은 필수 입니다. 똑똑한 광고주는 효율을 높이기 위해 최선을 다하고, 애드 네트워크 등의 중간을 이어주는 업체는 이런 필요를 충족 시켜주기 위해 열심히 기능을 개발합니다. ## 버즈빌 비지니스의 복잡성 일반 사용자 입장에서 버즈빌을 바라보면 “잠금화면 앱 회사”라고 인식하게 됩니다. 한국과 일본에서 잠금화면 앱인 허니스크린을 운영하고 있고, 미국에서는 Slidejoy 라는 이름으로 잠금화면 앱을 운영하고 있으니까요. 하지만 조금만 더 깊게 살펴보면 버즈빌의 비지니스 범위는 그보다 훨씬 더 넓습니다. ![](https://miro.medium.com/max/700/0*8reXaYis808mqTB_) 버즈빌은 SDK를 만들어서 비지니스 파트너에게 제공함으로써 **서플라이 사이드**의 역할을 맡고 있습니다. 퍼블리셔(i.e 파트너사)는 버즈빌 SDK를 자사 서비스에 설치함으로써 손쉽게 광고 지면을 추가하고 수익을 올릴 수 있죠. 퍼블리셔만의 잠금화면을 만들 수 있을 뿐만 아니라, 앱 내 혹은 외부에 다양한 형태의 광고를 추가할 수 있습니다. 이를 통해 버즈빌은 자사의 앱만으로 이루어진 사용자 풀이 아니라 더 넓고 다양한 프리미엄 사용자 풀을 구축할 수가 있죠. 또한 버즈빌은 많은 광고주와 직간접적으로 파트너십을 맺으면서 **디맨드 사이드**의 역할 또한 맡고 있습니다. 직접 계약을 통해 더 좋은 효율과 적절한 과금을 진행하기도 하고, 다양한 네트워크를 통해 더 넓은 광고주와의 접점을 가져가기도 합니다. 광고 효율이 높아야 하는 것은 두 번 강조해도 모자라죠. 다양한 퍼블리셔와 광고주를 서로 연결해주는 역할을 해야 하니 **사용자와 광고를 적절히 연결**해주는 것이 매우 중요합니다. 퍼블리셔마다 다르게 제공되는 정보를 취합하고, 사용자의 관심사를 추측하고, 적절한 시점에 관심이 갈만한 광고를 추천하려면 앞서 언급한 **핑거프린팅, DMP, 추천, 효율 검증** 등의 모든 기술과 데이터가 종합적으로 필요합니다. 그냥 있으면 되는 게 아닙니다. 정말 잘 해야 하고, 더 잘 해져야 하죠. 버즈빌이 가지고 있는 독점적인 장점 중 하나는 **로열티 프로그램**(e.g 포인트 시스템)을 활용한다는 것입니다. 견물생심이라고, 보통 사람이라면 백화점에 데려다 놓기만 해도 물건이 사고 싶어집니다. 저는 펀샵([funshop.com](http://funshop.com/))에 들어가면 몇 시간 동안 나오지를 못해요. 사용자가 관심을 가질만한 광고에 적절한 포인트를 지급함으로써 사용자의 관심을 끌어올 수 있습니다. 버즈빌은 많은 내부 실험을 통해서 **보상을 통한 광고의 효율을 검증**했습니다. 구글 또한 이미 보상 비디오 광고(Rewarded Video Ad)을 서비스하고 있고, 더 나아가 보상 광고(Rewarded Ad)로 범위를 늘리려는 움직임을 보입니다. 헌데 이게 또 골치가 아파요. 앞서 말한 내용에 회색 영역(Gray area)이 꽤 많거든요. “사용자가 관심을 가질만한”게 뭘까요. “적절한 포인트”에서 ‘적절한’은 어떻게 정의할 수 있을까요. 포인트는 얼마나 자주 줘야 할까요? 이런 회색 영역들은 모두 연구를 수반한 기술이 뒷받침 되어야 하죠. ## 애드 테크(Ad-tech) 회사로서의 버즈빌 ![](https://miro.medium.com/max/500/0*HVZe5DGJ1hgVdSi0) 지난 6월 약 90명의 모든 버즈빌 멤버와 함께 사이판으로 워크샵을 다녀왔습니다. 포스퀘어(Foursquare)가 광고 회사로서 피봇함으로써 기울어지던 회사를 되살리고 성장 중인 것을 알고 계신가요? 핀터레스트(Pinterest)의 광고 관련 기술이 매우 뛰어나며, 페이스북이 페이스북 오디언스 네트워크(FAN, Facebook Audience Network)로서 광고 업계에서 매우 잘 나가고 있다는 것을 알고 계신가요? 버즈빌은 잠금화면 서비스를 운영할 뿐만 아니라, **로열티 프로그램**을 적극 활용하고 **독창적인 지면**들을 소유한 높은 기술력을 가진 **애드 테크 회사**입니다. 눈에 보이는 가벼워 보이는 빙산 밑에 얼마나 크고 복잡한 것들이 숨어 있을지 먼 거리에서 넓게 바라보지 않는 이상 쉽게 알아차리기 어려운 법이죠. # 그래서 아키텍트는 뭐 하는데? 애드 테크 분야는 슬쩍 봐도 스테이크홀더(Stakeholder, 이해당사자)가 많죠. 퍼블리셔, 광고주를 기점으로 광고 관리자, 지면 관리자, 광고 제작자, 데이터 관리자, 분석가 등 많은 사용자 그룹이 존재합니다. 사용자가 많은 시스템은 필연적으로 기능이 많아지고 복잡해질 수밖에 없습니다. 다들 바라는 게 정말 다르니까요. 복잡한 시스템일수록 소프트웨어 아키텍처가 중요해집니다. 시스템이 변화에 유연해야 하고, 관련 있는 것들은 비슷한 곳에 모으면서도 (high-cohesion) 서로 다른 것들이 서로에게 너무 의존해 있으면 안되죠 (low-coupling). 제품군이 많아지니 재사용성에 대한 고민도 많아지고, 외부 파트너가 사용하는 API를 구성해야하니 더욱더 깔끔하고 안정적인 개발이 중요해집니다. 아키텍트는 기술만을 보는 직군이 아닙니다. 비지니스를 살피고 비지니스에 필요한 기술이 무엇인지 파악합니다. 다른 글에서 얘기했듯 모든 경우에 적용 가능한 단 하나의 정답 따위는 없으니까요. **아키텍트는 비지니스에 따라 전체적인 그림을 살펴보고 어떤 아키텍쳐가 적합할지 고민하고 실행해야 합니다.** 아키텍트는 전체적인 서비스의 구성을 그리고, 서비스 사이의 통신 규약을 정하고, 서비스의 역할을 규정하거나 규정하는 규칙을 수립하기도 합니다. 서비스 간의 구성뿐만 아니라 **서비스 내의 소프트웨어 아키텍쳐에도 관여**하죠. 프로젝트에 따라서 어떤 아키텍처를 가져가는 것이 좋을지 프로젝트 멤버와 함께 고민하고 논의합니다. 클린 아키텍처를 어떻게 적용해야 할지, 서버리스가 적절할지, MVVM, MVI, RIB 등 어떤 아키텍처를 적용할지, 리액티브 프로그래밍을 어디에 어떻게 활용할지, 서비스 메시는 무엇을 써야 할지, e2e 테스트는 어떻게 수행할지 같은 수많은 고민과 결정 사항들이 놓여 있죠. 또한 **아키텍트는 사내 교육 역시 고려해야 합니다.** 소프트웨어 엔지니어와 함께 올바른 방향으로 나아가기 위해 아키텍트는 계속해서 방향을 찾고, 공유하고, 함께 논의하고, 진행 상황을 점검하고 이를 다시 반복해야 합니다. 아키텍트 없이 엔지니어만 있으면 열심히 달리는데 방향이 자꾸 엉뚱해질 수 있고, 엔지니어 없이 아키텍트만 있다면 방향은 명확한데 당최 걷지를 않을 수도 있으니까요. # 버즈빌 ATF 멤버 소개 ![](https://miro.medium.com/max/700/0*8tg2KMAr2duWGugo) 무엇이든 열심히 잘하는 ATF 멤버들 버즈빌의 짱짱한 ATF 멤버를 소개합니다. 버즈빌 ATF는 비슷한 듯 참 다른 각자의 강점이 한 군데 어우러져서 열심히 달려나가고 있습니다. ![](https://miro.medium.com/max/500/0*zI1-rJkArEbDBdGr.png) ## Ben 벤은 매우 도전적이며 실행이 겁나 빠른 아키텍트이자 훌륭한 엔지니어입니다. 벤이 언제나 시스템을 깊게 이해하고 있는 덕분에 변경에 있어서 고려할 사항들을 명확하게 정의할 수 있죠. 그 와중에 마음에 들지 않는 부분이 보이면 벤은 언제든 달려들어 뜯어 고칩니다. 든든한 벤과 함께면 정말 거칠 것이 없죠. *** ![](https://miro.medium.com/max/500/0*Ww-xJZNwf5GM8NEg.png) ## Liam 리암은 기술 분야에 모르는 것이 없을 정도로 넓고 깊은 지식과 경험을 보유하고 있습니다. 게다가 변화에도 민감하여 최신 트렌드 역시 꽉 잡고 있죠. 덕분에 거의 모든 경우에 정답에 가까운 솔루션을 제시합니다. 급하게 결론에 도달하지 않고 깊은 생각을 바탕으로 방향을 제시하기에 모두 리암의 의견을 귀담아듣습니다. 심지어 디자인까지 잘하는 것은 엄청난 보너스! *** ![](https://miro.medium.com/max/500/0*JJIHVzIh_9hjU-ZT.png) ## Ethan 언제나 열정적인 이든은 파고 파고 더 파고드는 깊이 있는 아키텍트입니다. 항상 성장을 추구하며 새로운 정보를 습득하는 데 적극적입니다. 이든은 변경에 앞서 시스템을 분석하고 파헤치며, 올바른 방향으로 가기 위한 구체적인 방법을 제안합니다. 사내 교육까지 담당하고 계신 이든은 몸이 한 개가 아닌 것이 분명합니다. *** ![](https://miro.medium.com/max/500/0*oTCBOcfx0QzfOsmB.png) ## Jerry 제리는 사용자에 대한 이해가 특출난 아키텍트입니다. HCI(Human Computer Interaction)를 전공할 정도로 사람과 컴퓨터에 대한 깊은 관심을 가지고 있습니다. 그 덕분인지 정확하게 사람들의 생각을 분석하고 필요성을 정의합니다. 콘웨이의 법칙이 말하듯 시스템은 조직의 모습을 반영하게 되어있고, 조직의 모습은 사람들의 생각을 반영하게 되어있죠. 제리의 뛰어난 능력과 통찰력 덕분에 사용자의 필요성을 명확하게 정의할 수 있습니다. *** ![](https://miro.medium.com/max/500/0*TyKpnN945A76eBA8.png) ## Whale 은 접니다. 제가 스스로 생각하는 웨일(저)은 항상 긍정적인 아키텍트입니다. 긍정적으로 미래를 그리고, 그 미래를 현실로 만들기 위해 필요한 것들을 생각하죠. 비지니스가 나아가야 할 방향과 시스템이 나아가야 할 방향을 맞추고(alignment), 그 방향으로 나아가기 위한 필수 요소를 정의하고, 팀과 함께 고민하고, 논의하고, 결정합니다. *** # 마치며 아키텍트의 일은 목표 지점까지의 도달이 짧은 시간에 이루어지는 것도 아니고, 그 일들이 당장 사용자에게 직접적으로 맞닿는 분야도 아닙니다. 전사적인 공감대와 지지가 없다면 흐지부지되기 쉬운 분야이기도 하죠. 그럼에도 아키텍트의 역할은 서비스와 시스템이 더 크게 성장하기 위해 필수적이며, 엔지니어링의 체질을 건강하게 유지하는 기반이 됩니다. 이 글이 조금이나마 소프트웨어 아키텍트의 역할을 이해하는데 도움이 되기를 바랍니다. 앞으로도 많은 이해와 깊은 응원 부탁드립니다! ![](https://miro.medium.com/max/1000/0*TNGWNAbJsuiBPN7w) 사이판 워크샵 해변 댄스 파티 :) ## 홍보 버즈빌은 애드 테크 분야에서 빠르게 성장 중이며, 더 좋은 서비스와 시스템을 만들기 위해 오늘도 열심히, 즐겁게 달려나가고 있습니다. 더 굳건하고 빠르게 성장하기 위해 앞서 언급한 거의 모든 분야에 대해 **훌륭한 인재들을 채용**하고 있습니다. 함께 새로운 시장을 개척하고, 어려운 문제에 도전하고, 성취를 통해 한 걸음 더 나아가고 싶으신 분이라면 언제든 연락 주세요 :) \#직원의꿈을지원하는회사 #도서무제한구매가능 #동호회지원 #스터디지원 #매년해외워크샵 #매달특색있는문화생활 #[인재추천30만원](https://www.buzzvil.com/ko/2019/08/16/buzzvil-culture-버즈빌-인재추천-상품권-30만원-이벤트-진행/) --- ## [Design system at Buzzvil](https://tech.buzzvil.com/blog/design-system-at-buzzvil) Date: 2019-05-16 | Author: Maxence Mauduit | Category: Design Design system vs startup. Takeaways. We built a Design system. It took us time, resources and a lot of energy, involving pretty much everyone in our design team. And it’s just the beginning of a long story. If you are working at a startup and are considering building a design system, you might find clues reading about our experience. First off, we can’t ignore how time consuming building a system can be. Naturally some of the worries we had were to know if a design system was really worth it. And can a startup get any advantage from it? ## Long story short: Yes. ## A quick background Back when we decided to start building our system we were a 5–6 years old startup counting about 50+ members, including 3 product and 2 visual designers. The business was at that time aggressively expanding, our Business Development team got a few big partners that brought us to an all new level and our product team needed to react fast, scaling up our headcount, hiring lots of engineers to support our growing needs. In terms of design, we had 2 options, either hire a lot of designers to support in a linear way each integration or keep a core and highly efficient team to build up a system that could cover up all our needs in a centralized way. We went with option 2. To understand what’s at stake, let’s have a closer look at the problems we are trying to solve. We, at [Buzzvil](https://www.buzzvil.com/), are building a scalable platform for multiple partners in Korea, Japan and the US. We are young but we also build complex products that have to be permanently maintained and constantly developed. In that sense, even if you are startup, you should not be afraid of building a design system that will help you meet your needs. I will intentionally avoid giving details about our products as I hope this article to be generic enough, focusing more on the thinking, less on the making. ### That’s our first and main problem, answering our business needs: ## How can we organize our design to cover our diverse and growing needs with a modest team? ![](https://cdn-images-1.medium.com/max/2048/0*sTpMV60bs5WVWyDM.png) Two in-house apps, 15+ white labeled app and a bunch of SDK and API to manage (not counting all the necessary marketing work that comes on top of it, or even mentioning the dashboard to manage and operate all that.) It sounds like a pretty big product line already, especially for a startup with only a handful of designers. Well, systems are all about connecting things together, allowing these “things” to be reused at different places. And that’s what we’ve been doing, (re)factoring our design assets into reusable components. *Easy peasy*, at least in theory. **Technical debt also applies to design. We don’t spend time doing things twice, saving up time to think more.** This was our starting point. A major problem that we want to solve throughout the construction of a system. We started working on this about a year ago and on the way, different issues were solved. We are a small company that dreams big. Big enough to have clients all over the world. Being a global company is awesome, but it makes you realize that your design needs to fit people, markets with different cultural backgrounds and various levels of appreciation and understanding toward technology and aesthetics. ### That’s our second problem, being understood by anyone: ## How can we design scalable global products that fit a wide spectrum of people? ![](https://cdn-images-1.medium.com/max/2048/0*o1XBqN7Wxh9oquSS.png) Fortunately, we aren’t the only ones in this situation. Designers who once tackled the same problem like ours created standards, style guides and more recently the notion of Design System became pretty popular and many wrote their thoughts or [shared their Design System](https://github.com/alexpate/awesome-design-systems) online. Complex problems usually require simple solutions. Ironically, if you have experience designing products, you will probably agree saying that making things simple.. is pretty complex. 🔁 When designing an application, we carry a visual semantic. It is a form of language that is expected to be understood by people using our service. By reducing the complexity of that language we expect it to be understood by more people. Minimalism in design comes from this mindset. Simplicity is key. By keeping things simple, more people will understand what you want to express. But what does simplicity has to do with a systematic approach? **Principles**. Working with a systemic approach means setting principles and procedures in order to organize our workflow around some common grounds and values. In our case and to answer our business requirements, minimalism is one of our key principles to fit the diverse markets we are in and to be manageable to a small design team. Our team started from Google [Material](https://material.io/) Design and built our own components and principles from there so we wouldn’t have to start from scratch. There were a few solid reasons why we started building our own system this way. First of all, Material is an incredible design language that constantly evolves. And second, Material is already widely spread, reused, and used by millions of users throughout Android OS and major apps that follow Material Design guidelines. Buzzvil internally manages Slidejoy (US, Global) and HoneyScreen (Korea, Japan & Taiwan) as our two in-house (B2C) clients. But we also develop, support and manage partner’s mobile applications through our SDK. Each of them have very particular needs when it comes to express their brand identity. How can our system provide enough flexibility to accommodate such ecosystem? ### This leads to our third problem: meeting various brand needs. ## How can we manage multiple brands across our different services? ![](https://cdn-images-1.medium.com/max/2048/0*u07lVYifBuPAkEVW.png) To this dilemma, we needed an answer that supports third party brand integration in order to fit each of our partners’ needs. A lot of theories and practical examples are out there as systems are spreading across IT companies, but I found [Daniel Eden’s approach](https://daneden.me/2017/07/17/design-system-structure/) most suitable for our needs. Eden has an interesting way of organizing and structuring a design system. Everything starts by separating your design assets into 2 layers, **pattern** and **expression.** ### The pattern layer ![](https://cdn-images-1.medium.com/max/2048/0*y-nLLu7-fWp5-iv4.png) Patterns are our layout’s blocks. They give exact specifications on how our components should be executed. Patterns are carrying components made of sub-components and so on. They are expressionless They don’t carry any message whatsoever. Components are reusable within a service and even across our entire scope. A great system factorizes components to an optimal amount, getting rid of replicated ones across our entire design scope. The goal here is to make our life simpler. ### The expression layer ![](https://cdn-images-1.medium.com/max/2048/0*zbAA2R461P-T8rsl.png) The expression layer carries a message and is the root level of our components. In other words, they aren’t composed by any other sub-components. We generally refer to them as *Atoms (see [atomic design](http://atomicdesign.bradfrost.com/chapter-2/) from Brad Frost for more infos).* This is where our branding, our tone of voice is contained. Expression layers are colors, text strings, icons and illustrations, photos and videos. They express a message and are ruled by a specific brand guideline, independent from our UI structure. Now you probably start to understand how scalable this approach is. By identifying everything and thanks to awesome tools such as [Figma](https://www.figma.com) or [Sketch](https://sketchapp.com/), it is now possible to connect pattern and expression layers with ease. System’s weakness is on how flexible they are. Balancing between a too rigid or too soft structure isn’t an easy thing. Cutting it into these two chunks made things much easier for us. Blocks just like expression aren’t always from designer’s responsibility as blocks will most likely be integrated by developers and expressions are gonna be handled by marketers to set the brand’s tone of voice. These two chunks will need different types of communication as you won’t translate the same message to a developer or a marketer. ## The difficulty of reaching a great design is to connect the different stakeholders over one clearly identified Design concept. Patterns need to follow devs guidelines while expressions need to be flexible enough for a brand to be loud and clear or a content to be properly displayed. And in the middle of all this, our work is to connect the dots to make sure that in the end, everything is cognitively affordable to people. In Daniel Eden’s article mentioned earlier, another layer called concept is mentioned. This is how we communicate things to our different stakeholders and where our design is evaluated. ### This is our fourth and last problem, our design needs: ## How do we communicate and evaluate our design concepts? ![](https://cdn-images-1.medium.com/max/2048/0*bzy4RUYms4Na4usJ.png) Concepts tell a story. A concept is about communicating an abstract idea by any possible mean. This is where designers must make a difference and **the reason why we have been working on a system: to save up resources in order to be able to spend the necessary time on visualizing/materialize ideas over concepts.** To rephrase Eden’s words, ## if the expression layer is our alphabet, the pattern layer our words and sentences, then the concept layer is our story. Building an alphabet and a dictionary are foundations to work on what matters the most: the story we are gonna tell to people. The quality of our foundations affects the clarity of our message while the quality of our story affects the overall experience of our products. But then, how can we tell if a design is good? Concepts are made of assumptions that are based on theoretical and practical research. Empathy cannot be easily controlled in advance and testing things out is definitely a great way to judge either your design answers its purpose or not. And to objectively judge something, metrics are key. In our system, each concept carries a set of metrics that will tell us after testing if our assumptions were right. Just like the entire system, metrics aren’t written in stone. Things are flexible, and can always be adjusted after the first tests. If the results aren’t as expected after a few loops, it means that our concept wasn’t right and needs to be fixed. We assume that our core components are pretty robust and won’t be the main cause of a poor test result, but their assemblage on the other hand, might be in cause. Another way to say it: your words are all orthographically correct but your sentence isn’t properly constructed and what you need to do is to switch words around to make the grammar right. If that doesn’t work either, it probably means that your entire concept needs to be thought over as the message isn’t answering people’s expectations. ## Thoughts Having a design system is a great way to deliver a design. Yet, we can’t expect everyone around us to think and work like designers. But what we can definitely do is informing them and allowing everyone access our resources to build up rough concepts from pre-made blocks. We have been doing that over the last 6 months, letting people know how this works via some design classes within our company and giving access to everything to everyone. And recently we saw mockups and intentions coming from some of our PM’s or Marketers giving us a huge satisfaction after going through all this. Great ideas come from every mind, designers have the necessary tools to easily express these ideas, whereas our co-workers might not. Sharing our assets is a way to help not just our co-workers but anyone communicate on an idea. Sharing our early concepts, principles and design culture is for everyone to understand our process and to understand how sharing ideas is never a useless thing. In the end, a single concept could resonate in a different way to someone else, bringing up new leads. Like many others, we will soon share our design system to everyone after cleaning up the mess a bit :). Thanks for taking the time to read me until the end, I’ve decided to keep this article away from our actual products and components to keep it more generic, but I would be happy to answer any question you’d have! --- Featured illustration by [Jetty Cho](https://www.linkedin.com/in/jettycho/). --- ## [Clean Architecture Packaging Strategy](https://tech.buzzvil.com/blog/tech-blog-clean-architecture-packaging-strategy) Date: 2019-05-15 | Author: Whale Lee | Category: Backend ### TL; DR > **Clean Architecture** 를 구성할 때에도 **Package by Feature** 방식을 고려해봅시다. ### What is Package? (일반적으로) 사람들은 잘 정돈된 책상을 보면 기분이 좋아집니다. 물론 사람에 따라 책상을 보는 것 자체가 기분이 안 좋을 수도 있지만, 여전히 펜과 커피잔, 컴퓨터와 칫솔이 마구잡이로 놓여있는 책상을 보는 것보다는 잘 정돈된 책상이 기분이 좋을 것입니다. 잘 정돈된 책상에서는 원하는 물건을 바로 찾을 수 있고, 새로운 물건을 배치할 때 어디에 배치해야 하는지도 쉽게 알 수 있습니다. **우리 코드도 그렇습니다.** package 는 필요한 코드의 묶음입니다. 비슷한 코드를 한군데 두어서 찾기 쉽고 수정하기 쉽게 만들어 줍니다. 언어에 따라서 package 의 정의와 관리 및 사용하는 방법이 다르지만, package 라는 개념 자체에 대한 접근은 거의 비슷합니다. 이후 사용할 package 라는 단어가 java 의 package 와 동일한 개념은 아니지만, 많은 부분 적용 가능한 개념이라고 볼 수 있습니다. 책상 정리에 여러 방법이 있듯이, package 구성 방법도 여러 방법이 있습니다. 대표적으로 비교되는 방식이 package by layer 와 package by feature 방식입니다. 각각이 어떤 방식인지에 대해서는 folder 구조를 가볍게 비교하는 정도로 설명하고 넘어가겠습니다. ### Package by Layer Class 의 역할에 따라서 package를 나누는 방식입니다. rails 를 사용하는 (혹은 사용했던) 개발자들에게는 매우 익숙한 형태입니다. ``` controllers/ ArticlesController.java UsersController.java models/ Article.java User.java adapters/ ArticlesAdapter.java UsersAdapter.java ``` ### Package by Feature Class 의 기능에 따라서 package 를 나누는 방식입니다. ``` article/ Article.java ArticlesAdapter.java ArticlesController.java user/ User.java UsersAdapter.java UsersController.java ``` ### Package by Feature is Better, in most cases. 모든 선택이 tradeoff 가 있듯이 두 방식 모두 각자의 장단점을 가지고 있습니다만, feature 중심의 packaging 방식이 진영 싸움에서 우위를 가져가고 있습니다. Package by feature 방식이 [Package Principles](https://en.wikipedia.org/wiki/Package_principles) 들을 더 잘 지키는 방향이고, 이를 통해 얻을 수 있는 이점이 아주 많기 때문입니다. ### Package Principles * Reuse-release Equivalence Principle (REP) * Common-Reuse Principle (CRP) * Common-Closure Principle (CCP) * Acyclic Dependencies Principle (ADP) * Stable-Dependencies Principle (SDP) * Stable-Abstractions Principle (SAP) 프로그램이 커지고 복잡해짐에 따라 package by layer 가 가지는 한계 역시 package by feature 를 선호하는 이유 중에 하나이기도 합니다. 프로그램이 커지다 보면 점차 아래와 같은 모습을 띠게 되고, 점점 뭔가 잘못되고 있다는 느낌을 받게 됩니다. ``` controllers/ ArticlesController.java CampaignsController.java LoginController.java PreferencesController.java UsersController.java ... models/ Article.java Campaign.java Session.java Preferences.java User.java ... adapters/ ArticlesAdapter.java UsersAdapter.java ... ``` ### Wait a minute, I’m used to package by layer than feature. 우리는 개념을 익힐 때 기능별로 공부하는 것이 아니라 역할별로 공부합니다. 예를 들어 Adapter Pattern 을 익힌다고 했을 때 User Adapter 와 Article Adapter 를 각각 공부하지 않죠. Adapter 를 공부한 뒤에 User 와 Article 에 적용합니다. 우리가 배운 방식대로 코드에 적용하려니 자연스럽게 package by layer 가 익숙하게 느껴집니다. “Controller 는 어디서 찾지? Controller folder 안에서 찾아야지.” 초기에 프로젝트를 구성하면 이런 방식이 어색하게 느껴지지 않습니다. 직관적이니까요. 덕분에 처음 이해가 쉬운 장점이 있습니다. 하지만 프로젝트가 커짐에 따라 하나의 기능 수정을 위해 여러 package 를 수정해야 하는 상황들이 자주 발생하고, package 간에 dependency 가 명확하지 않기 때문에 tightly-coupling 된 구성을 가지게 됩니다. ### Clean Architecture Packages Robert C. Martin 이 제안한 Clean architecture 는 layer 를 명확하게 나누고 implementation 과 interface 를 확실하게 구분함으로써 유연하고 깔끔한 소프트웨어 구현을 가능하게 합니다. 유행하는(?) framework 들이 직접적으로 제안하거나 미리 준비를 해주지 않기 때문에 대다수의 사람에게 친근하게 사용되고 있지는 않지만, 잘 사용되었을 경우 더욱 깨끗하고 아름다우며 변화에 유연하고 안정적인 구현을 가능하게 도와줍니다. 한편 실제 구현하는 방식에서 ‘이걸 따라 하면 된다’ 하는 정답에 가까운 가이드라인은 없습니다. Class 를 어떤 식으로 나눠야 하는지, Usecase 를 모아놔야 할지 아니면 기능마다 파일을 만들어야 할지, **package 를 어떻게 구성해야 하는지** 같은 것들 말이죠. ### Clean Architecture: Package by Layer 그래서 그런지 Clean architecture 를 구성한 예제 코드들을 보면 package by layer 로 구성된 경우가 종종 보입니다. domain, data, presentation layer package 를 나누고 Use case, Repository interface 등을 domain package 안에 넣고, repository 구현체는 data package 안에 넣는 방식입니다. (e.g [go-cleanarchitecture](https://github.com/manuelkiessling/go-cleanarchitecture)) _(아래에서는 좀 더 명확한 차이를 보기 위해 package 사용 구분이 명확하고 활용도가 높은 go project 를 예시로 들겠습니다.)_ ``` domain/ repo/ article_repo.go user_repo.go usecase/ article_usecase.go article_usecase_test.go user_usecase.go user_usecase_test.go data/ article_pgrepo.go article_pgrepo_test.go user_pgrepo.go user_pgrepo_test.go ``` 개념을 이해한 대로 package 를 구성했기 때문에 clean architecture 를 잘 적용한 것으로 보이지만, 위에서 언급한 package by layer 의 문제점이 존재하게 됩니다. ### Clean Architecture: Split by Feature, Package by Feature 한편 feature 단위로 나눈 뒤에 내부 구성을 package by layer 로 구성한 예제들도 있습니다. (e.g [go-clean-arch](https://github.com/bxcodec/go-clean-arch)) ``` article/ usecase/ article_usecase.go article_usecase_test.go repo/ pgrepo.go entity.go repo.go usecase.go user/ usecase/ user_usecase.go user_usecase_test.go repo/ pgrepo.go entity.go repo.go usecase.go ``` 이 경우 root package 가 feature 단위로 되어있기 때문에 package by feature 형식으로 구성된 것처럼 보입니다. 또한 내부 package 가 layer 단위로 나뉘어 있기 때문에 개념적으로 이해도 쉬워 보이죠. 하지만 이렇게 구성될 경우 실질적으로 use case 나 repo folder 안에 두 개 이상의 구현이 들어갈 일이 거의 없게 될 것을 추측할 수 있습니다. 이미 feature 단위로 나누었기 때문에 내부 folder 안에 여러 구현체가 들어갈 일이 별로 없는 것이죠. 혹, 오히려 여러 구현체가 들어가야 하는 상황이 될 경우 다시 package by layer 의 문제가 발생합니다. package 를 사용하는 시점에도 여전히 문제가 발생합니다. layer 이름으로 package 를 import 해야 하기 때문에 어떤 feature 를 사용하는지 알 수 없습니다. method 이름으로 구분하는 방법(e.g `usecase.NewArticleUsecase()`)을 선택할 수도 있지만, 여전히 package 이름이 겹치기 때문에 alias 를 해야만 하는 문제도 있습니다. ```go r := repo.New() // What kind of repository implementation? u := usecase.New(r) // Which usecase? ``` ### Clean Architecture: Package by Feature Clean architecture 의 개념은 명확합니다. 잘 적용되면 깨끗하고 유연한 코드를 작성할 수 있습니다. 하지만 package 가 직접적으로 그 개념을 표현해야만 하는 것은 아닙니다. 오히려 개념이 명확하기 때문에 package 를 layer 단위로 구분해 주지 않아도 충분히 표현이 가능합니다. ``` article/ pgrepo/ pgrepo.go entity.go repo.go usecase.go usecase_test.go user/ pgrepo/ pgrepo.go entity.go repo.go usecase.go usecase_test.go ``` 위 구성에서는 feature 중심으로 packaging 하고 내부 구성은 파일 이름으로 분류합니다. repository 는 interface 로 구성하고, 실제 implementation 은 따로 package 를 구성하여 그 둘의 구분을 분리합니다. interface 와 implementation 을 구분함으로써 data layer 의 수정이나 변경이 domain layer 에 영향을 미치지 않게 합니다. pgrepo 는 postgres 로 구성한 repository 의 구현 package 입니다. sqliterepo, memrepo, mockrepo 등 다른 구현체를 구성할 수 있습니다. data layer package 를 따로 구성하지 않고 package 안쪽에 배치함으로써 package 간의 dependency 를 더 명확하게 의도할 수 있습니다. package 를 사용하는 입장에서도 불필요한 이름이 반복되지 않고 명확하게 의도를 전달할 수 있습니다. ```go r := pgrepo.New() u := article.NewUsecase(r) ``` ### **Consideration** pgrepo 라는 package 이름은 postgres 라는 기능적인 의미를 가지고는 있지만 article 의 pgrepo 와 user 의 pgrepo 간의 구분이 이루어지지 않습니다. 때문에 package 를 import 하는 시점에 alias 를 해야만 구분하여 사용 가능합니다. 그렇다고 userpgrepo 라는 이름을 쓰기에는 user package 내부 package 에 user 라는 이름이 반복적으로 사용되는 아쉬움이 남습니다. (e.g [https://github.com/gogs/gogs/tree/master/pkg/auth](https://github.com/gogs/gogs/tree/master/pkg/auth) 같은 경우를 살펴보면 auth 안에 github 이라는 이름의 package 가 github 을 사용하는 auth 구현을 담고있지만, github 이라는 package 이름이 auth 를 담고 있지는 않습니다.) ### Clean Architecture: Split by Layer, Package by Feature 한편 제일 상위 레이어를 layer 기준으로 구분하고, 내부 packaging 을 feature 를 기준으로 작성하는 방법도 있습니다. ``` domain/ article/ entity.go repo.go usecase.go usecase_test.go user/ entity.go repo.go usecase.go usecase_test.go data/ articlerepo/ repo.go repo_test.go userrepo/ repo.go repo_test.go ``` 이 방식은 java 를 사용할 때 더 장점이 두드러집니다. 상위 layer 구분을 java package — 앞에서 사용하던 package 와 같은 단어는 아닙니다 — 대신 module 로 구성하고 내부는 package 로 구성함으로써 java package 가 물리적으로 나누지 못하는 경계를 나눠주게 됩니다. (e.g module 간에는 circular dependency 가 불가능합니다.) 사용하는 시점에도 좀 더 명확하게 의미를 전달해줄 수 있습니다. ```go r := articlerepo.New() u := article.NewUsecase(r) ``` 다만 이렇게 구성할 경우 feature 단위의 구성이 여러 군데로 흩어지는 아쉬움이 남을 수 있습니다. ### Conclusion 개념을 이해하는 것과 코드로 옮기는 것 사이에는 분명한 간극이 있습니다. 문서를 읽을 때는 명확해 보이던 것들도 직접 코드로 옮기다 보면 많은 예외 상황을 마주하게 되죠. 같은 생각을 구현할 때 다양한 방법이 존재하고, 방법에 따른 tradeoff 가 발생합니다. 직관적으로 보이는 방법이 항상 옳은 방법은 아닙니다. 마치 [Square 가 Rectangle 을 상속 받으면 안되는 것](https://medium.com/@ssowonny/keep-principles-in-mind-f8576fe7e626) 처럼 말이죠. 많은 software architecture 가 계속해서 제안되고 개선되고 있죠. 어떤 architecture 를 사용할 것인가도 중요하지만, 그 architecture 를 구현하는 시점에도 역시 많은 고민과 선택이 필요합니다. 물론 지금 내린 선택이 시간이 흐르고 나면 그른 선택이 될 수도 있고 내일 생각이 달라져서 다른 선택을 하고 싶은 마음이 들 수도 있습니다. 하지만 지금 하고 있는 이 고민과 결정이 분명히 팀과 개인 모두의 성장을 가져올 것이라고 믿습니다. ps. Buzzvil 에서는 Package Principles 를 충분히 고려하면서 상황에 맞는 packaging 전략을 선택하고 있습니다. 물론 여전히 많은 경우 하나의 유일한 답이 나오지는 않습니다. 하지만 깊은 고민을 통해 하나씩 원칙을 세워 나가며 더욱 직관적이고 깨끗한 구현을 향해 달려가고 있습니다. 즐거운 여정을 함께 할 인재를 찾고 있으니 언제든 마음껏 연락 부탁 드립니다. #### References * [Package by feature, not layer](http://www.javapractices.com/topic/TopicAction.do?Id=205) * [PBF(Package by Feature), no more PBL(Package by Layer)](https://medium.com/mindorks/pbf-package-by-feature-no-more-pbl-package-by-layer-50b8a9d54ae8) * [Package Principles](https://en.wikipedia.org/wiki/Package_principles) * [Package Oriented Design](https://www.ardanlabs.com/blog/2017/02/package-oriented-design.html) * [https://github.com/android10/Android-CleanArchitecture/tree/core/refactor-project-structure](https://github.com/android10/Android-CleanArchitecture/tree/core/refactor-project-structure) * [https://github.com/bxcodec/go-clean-arch](https://github.com/bxcodec/go-clean-arch) * [https://github.com/manuelkiessling/go-cleanarchitecture](https://github.com/manuelkiessling/go-cleanarchitecture) --- ## [Compare Software Architectures: Monoliths, SOA and Microservices](https://tech.buzzvil.com/blog/tech-blog-compare-software-architectures-monoliths-soa-and-microservices) Date: 2019-03-11 | Author: Whale Lee | Category: Backend 요즘 Software architecture 라는 단어를 들으면 아마도 Client engineer 분들은 MVC, MVP, MVVM 이 먼저 떠오를 것이고, Server engineer 분들은 Microservice architecture 를 먼저 떠오를 것 같네요. Clean architecture 나 Event-driven architecture 등을 떠올리는 분들도 계실 것 같구요. Software architecture 를 어떻게 정의할 수 있을지에 대해서는 [Software architecture: The important stuff](https://medium.com/@ssowonny/software-architecture-the-important-stuff-a0c01fb9d774) 에 적어 봤으니 여기에선 넘어가도록 하죠. \[caption id="" align="aligncenter" width="495"\]![](https://cdn-images-1.medium.com/max/600/0*bVGkCRm9A73ujHOi.png) https://mherman.org/blog/developing-microservices-node-react-docker/\[/caption\] Microservice architecture 는 대세라고 말할 수 있습니다. Netflix, Amazon 등 굴지의 기업들이 성공적으로 적용해서 운영하고 있고, 국내 기술적으로 뛰어난 많은 기업들 역시 이미 적용했거나 시도하고 있습니다. “남들 다 하는데 이러다 도태 되는거 아냐?” 라는 생각이 들 정도로 말이죠. 그러나 이전 글에서 얘기했듯이 정답은 없으며, Microservice architecture 역시 예외는 아닙니다. 모든 선택에는 Tradeoff 가 있고, Microservices 는 다른 architecture 에 비해 어떤 장점이 있는지 살펴봐야 합니다. 이와 관련하여 정말 많은 좋은 글들이 이미 있으니, 이 글에서는 몇 가지 Software architecture 들을 가볍게 정리 및 비교해 보도록 하겠습니다. ### Monolithic Architecture Monolithic architecture 는 Microservice architecture 의 장점을 얘기할 때 반드시 언급될 정도로 대척점에 있는 architecture 입니다. Monolithic architecture 는 하나의 큰 덩어리로 구성되어 있고, 모든 기능이 하나의 프로젝트에 집중되어 있습니다. 쉽게 구성이 가능하고 초기에 기능을 빠르게 추가하기에 용이하나, 복잡도가 늘어날수록 기능 추가 속도가 느려지고 문제가 발생할 가능성이 높습니다. PoC(Proof of Concept)를 위한 가벼운 프로젝트나 아주 초기 프로젝트에 적용 가능합니다. ### Semi-Monolithic Architecture Monolithic architecture 보다는 작지만, 여전히 기능들이 몇 개의 프로젝트에 집중되어 있는 architecture 입니다. 예를 들어 frontend 와 backend 프로젝트를 나누었지만 각 프로젝트가 monoliths 인 경우 semi-monolithic architecture 라고 볼 수 있습니다. 다만 Semi-monoliths 의 경우 몇 군데에서 언급한 것을 볼 수 있지만, 일반적으로 사용되는 architecture 용어는 아닌 듯하고, Semi-monoliths 로 구분될 수 있는 경우 Monolithic architecture 라고 분류할 수 있을 듯합니다. 단순 frontend / backend 보다 좀 더 많은 수의 service 로 분할된 architecture 를 구성하더라도 각 service 가 monoliths 로 구분될 수 있다면 여전히 monolithic architecture 를 구성하고 있다고 할 수 있습니다. ### Service-Oriented Architecture 여러 조직이 다수의 application 사이에서 로직과 데이터를 공유하기 위해 제안된 architecture 입니다. Monolithic architecture 와 달리 기능을 나눠서 여러 개의 서비스로 구성하고, 서비스 사이는 API 를 통해서 통신합니다. Microservice architecture 와 Service-oriented architecture (SOA) 를 비교하기 위해 Enterprise Service Bus (ESB)가 많이 언급됩니다. ESB는 Enterprise Application Interface (EAI) 와 대조적으로 가볍고 흔한 통신을 위해 제안되었으나, 통제와 관리를 위해 점점 무거운 방향으로 진행되면서 최초의 의도와 달라졌습니다. SOA 가 무거워짐에 따라 최초의 의도였던 빠른 적용, 민첩한 개발 및 적은 통합 비용과 멀어지게 되면서 자연스럽게 도태되었습니다. 서비스 사이에 데이터베이스를 공유할 수 있느냐 아니냐로 Microservice 와 구분을 짓는 의견도 있습니다만, SOA의 정의가 넓어서 이 부분에 대해서는 이견들이 있습니다.   \[caption id="" align="aligncenter" width="800"\]![](https://cdn-images-1.medium.com/max/800/0*hBnSAZA5m_02RDmD) https://dzone.com/articles/microservices-vs-soa-2\[/caption\] SOA가 넓은 범위에서 정의됐기 때문에 ESB 나 DB 공유 여부로 SOA 를 규정 짓기는 어렵습니다. 정의 상으로 보면 Microservice architecture 역시 SOA 의 일종이라고도 볼 수 있습니다. Microservice 의 예시로 자주 등장하는 Netflix 와 Amazon 역시 Microservice 라는 단어가 사용되기 전에는 스스로의 시스템을 SOA 라고 지칭했습니다. [Microservice Architecture: The O’Reilly Book](https://www.apiacademy.co/articles/2016/03/microservice-architecture-the-oreilly-book) 의 공동 저자 Matt McLarty 는 [Learn from SOA: 5 lessons for the microservices era](https://www.infoworld.com/article/3080611/application-development/learning-from-soa-5-lessons-for-the-microservices-era.html) 라는 글에서 SOA 와 Microservice architecture 가 같은가 다른가는 그다지 중요한 것이 아니며, **우리가 SOA 로부터 어떤 것들을 배웠는가**가 중요하다고 강조합니다. ### Microservice Architecture Microservice architecture는 규모가 빠르게 커져도 제품 생산 속도를 빠르게 유지하고 안정성을 가질 수 있는 architecture 입니다. 충분히 작은 서비스들이 서로 통신하면서 기능을 수행합니다. Microservice architecture 를 SOA의 잘 구현된 형태라고 보는 시각도 있지만, micro 라는 단어가 SOA 에서 정의하는 서비스보다 작은 크기의 서비스임을 명시적으로 표현하기 때문에 매우 다르다는 의견 역시 있습니다. Microservice architecture 는 각 서비스의 크기를 작고 가볍게 유지함으로써 더 깔끔하고 명확하게 서비스를 유지할 수 있습니다. 잘 구성될 경우 특정 서비스에 장애가 생겨도 다른 서비스에 영향을 적게 미치거나 유연하게 대응할 수 있기 때문에 전체 시스템 오류(e.g Single Point of Failure)를 방지할 수 있습니다. 각 서비스는 독립적으로 배포 및 확장 가능하기 때문에 기능 배포가 빠르고 많은 트래픽에 유연하게 대처할 수 있습니다. 한편 Microsoft architecture 는 구조적인 면에서 복잡도가 증가하며, 많아진 서비스 및 서비스 간 통신에 대한 유지 보수 비용이 추가됩니다. 이를 대응하기 위해서 충분히 자동화되고 잘 구성된 시스템이 필수적으로 필요합니다. ### Conclusion 판단과 결정은 근거를 필요로 합니다. 가끔 감을 믿고 밀어붙여야 할 때(e.g 오늘 점심은 해장국을 먹어야 한다던가)도 있다는 점은 인정합니다. 하지만 그 역시 설득력을 가지지 못하면 하나의 목표를 향해 모두가 미친듯이 달려가기는 어렵겠죠. Software architecture 를 결정하기 위해서는 추구하는 비전과 비지니스를 이해하고 그에 맞는 근거 하에 모든 팀원을 판단하고 설득해야 합니다. 버즈빌 에서는 더 빠르고 큰 성장을 위해 Architecture Task Force 팀을 구성하였습니다. ATF 팀은 버즈빌에 최적인 Software architecture 를 판단하고, 구성하고, 실행하기 위해 바쁘게 움직이고 있습니다. **Buzzvil Services Characteristic:** * 제품이 다양하고 제품별로 제공해야 할 기능이 많다. * 각 제품이 공통적으로 필요로 하는 기능이 많다. * 서비스 혹은 기능별로 대응해야 하는 트래픽이 다르다. * 전체 서비스 장애 발생 시 많은 후속 문제가 발생한다. * 트래픽 변동이 특정 이벤트에 의해 크게 일어날 수 있다. Buzzvil 의 제품과 비지니스는 위와 같은 성격을 가지고 있습니다. 이를 바탕으로 우리는 Microservice architecture 가 가장 적절하다고 판단하였고, 현재 microservices 의 장점을 살리면서 안정적이고 빠르게 우리가 원하는 목표에 도달할 수 있도록 다양한 방면에서 변화를 가져가고 있습니다. ### References * [Learn from SOA: 5 lessons for the microservices era](https://www.infoworld.com/article/3080611/application-development/learning-from-soa-5-lessons-for-the-microservices-era.html) * [Microservices vs. SOA](https://dzone.com/articles/microservices-vs-soa-2) * [On monoliths, service-oriented architectures and microservices](https://odino.org/on-monoliths-service-oriented-architectures-and-microservices/) * [Microservices.io](https://microservices.io/) * [Microservices Resource Guide](https://www.martinfowler.com/microservices) * [Design Microservice Architectures the Right Way](https://www.youtube.com/watch?v=j6ow-UemzBc) * [Developing Microservices - Node, React, and Docker](https://mherman.org/blog/developing-microservices-node-react-docker/) --- ## [Hello, TLS 1.3](https://tech.buzzvil.com/blog/atech-blog-hello-tls-1-3) Date: 2019-02-11 | Author: Brice Bang | Category: Backend ### 들어가며 ![](https://cdn-images-1.medium.com/max/1600/0*MDiyywE36sAZ-39W) 인터넷, 특히 웹에서 사용하는 통신 프로토콜을 HTTP라고 하고, 보안 채널인 TLS를 사용하는 프로토콜을 HTTPS라고 합니다. 2018년, 파이어폭스와 크롬은 그동안 일반적인 통신 방법으로 사용하던 HTTP를 ‘안전하지 않음’으로 표시하기로 하였습니다. 크롬은 이에 더해서 ‘안전함’으로 표시하던 HTTPS를 점진적으로 특별한 표시를 제거하여 일반 사이트로 표시하기로 하였습니다. 이러한 변화는 보안 채널인 TLS가 더는 특별한 것이 아니라, 일반적이고 평범하며, 그리고 항상 적용되어야 함을 시사하고 있습니다. TLS는 서로 멀리 떨어진 두 컴퓨터가 안전하지 않은 네트워크를 통해서 안전하게 대화를 주고받기 위해 만들어진 보안 프로토콜입니다. 각종 사이트에 로그인할 때 입력한 로그인 정보부터 여러분이 인터넷 결제를 할 때 입력하는 정보까지, 묵묵히 그리고 안전하게 우리의 중요한 통신들을 보호해주던 TLS는 이제 ‘당연한 것’이 되었으며, 작년 8월 10일에는 드디어 새로운 버전인 TLS 1.3이 발표되었습니다. 오늘은 이제는 ‘당연한 것’이 되어 버린 TLS와 최신 버전인 TLS 1.3에 대해서 살펴보도록 하겠습니다. ### TLS 알아보기 이 글의 앞부분은 TLS에 대해 잘 모르실 분들을 위해서 TLS의 필요성과 TLS의 기본 원리 등에 대해서 살펴봅니다. TLS에 대해 이해하고 있으며, TLS 1.3에 대해서만 파악하고 싶으신 분은 이 부분을 넘어가서 TLS 1.3 부분을 보셔도 좋습니다. 물론, TLS 1.3에 대해 정확하고 자세하게 알 방법은 [RFC 8446 표준 문서](https://tools.ietf.org/html/rfc8446)를 읽는 것입니다. #### 안전하지 않은 인터넷 TLS라는 보안 프로토콜이 필요한 이유는 인터넷에서 데이터를 전달하는 방식이 안전하지 않기 때문입니다. 인터넷에서 데이터를 전달하는 방식은 ‘수업 시간에 쪽지를 보냈던 경험’을 떠올려보면 좋습니다. 수업 시간에 멀리 떨어진 친구에게 쪽지를 보낼 때는 직접 전달할 수 없기 때문에 중간에 있는 친구들의 도움을 받아야 합니다. 인터넷에서도 마찬가지로, 발신자인 클라이언트와 수신자인 서버는 직접 연결되지 않기 때문에 그 중간에 존재하는 무수한 전달자인 라우터가 데이터를 넘겨주는 방식으로 전달됩니다. 문제는 중간에 있는 라우터를 신뢰할 수 없다는 점에서 발생합니다. #### TLS의 보안 목표 신뢰할 수 없는 라우터들로 인해서 어떤 문제들이 생길 수 있을까요? 수업 시간으로 다시 돌아가서 생각해보면 매우 간단하게 답을 생각할 수 있습니다. 여러분은 아마도 친구에게 전달하기 전에 쪽지를 접었을 것입니다. 그 이유로는 쉽게 전달하기 위한 것도 있겠지만, 중간에 있는 친구들은 열어보지 않기를 바라는 마음도 있었을 것입니다. 그렇기 때문에 보통은 메시지가 보이지 않는 방향으로 쪽지를 접습니다. 여러분은 최소한의 보안을 위해서 쪽지를 접었습니다만, 안타깝게도 여러분의 친구들은 믿을 수 없습니다. ![](https://cdn-images-1.medium.com/max/1600/0*O8IuKjLj5VM7XaDg) 설명하기 편하게 이름을 붙여보겠습니다. A에서 따온 이름을 가진 앨리스(Alice)는 자신의 친구, B에서 따온 이름을 가진 밥(Bob)에게 쪽지를 전하려고 합니다. 그 중간에서, 도청자(Eavesdrop)의 E에서 따온 이름을 가진 이브(Eve)라는 친구가 나쁜 마음을 먹었습니다. 이브는 쪽지가 자신에게 오기만 한다면 이를 열어볼 수도 있고, 앨리스에게 응답을 보내서 밥인 것처럼 속일 수도 있고, 아니면 쪽지의 내용을 지우개로 살짝 지우고 단어 몇 개를 바꿔서 밥에게 전달할 수 있습니다. 쪽지가 자신에게 오지 않는 경우를 대비해서, 주변의 자신과 친한 빨간 친구들과 모의할 수도 있습니다. 이러한 공격을 중간자 공격(Man-in-the-Middle Attack)이라고 합니다. 중간에 있는 믿을 수 없는 친구들 때문에 발생하는 문제입니다. 이제 TLS의 목표를 좀 더 이해하기 쉽게 말할 수 있습니다. **앨리스와 밥이 주고받는 쪽지를 이브가 방해할 수 없게 하는 것**입니다. 한편, 앞에서 말씀드렸던 이브의 공격 방법을 3가지로 정리할 수 있습니다. 1. 이브는 앨리스에게 응답을 보내서 자신을 밥이라고 속일 수 있습니다. 2. 이브는 앨리스가 보낸 쪽지를 읽을 수 있습니다. 3. 이브는 앨리스가 보낸 쪽지를 변경하거나, 위조할 수 있습니다. 그리고 이 세 가지 보안 위협에 대응하기 위한 다음의 보안 목표를 세울 수 있습니다. 1. 인증성 (Authenticity): 앨리스와 쪽지를 주고받는 상대방이 밥임을 인증하는 것 2. 기밀성 (Confidentiality): 앨리스와 밥 만이 서로의 쪽지를 읽을 수 있고, 이브는 불가능하게 하는 것 3. 무결성 (Integrity): 앨리스가 쓴 쪽지가 변경되지 않았음을 보장하는 것 TLS는 이 세 가지 보안 목표를 달성함으로써, 안전하지 않은 인터넷에서 안전한 통신 채널을 만들 수 있습니다. #### TLS의 암호화 모음 (Cipher Suite) TLS는 안전한 네트워크 연결을 위해서 앞에서 언급했던 각각의 보안 목표를 달성하는 암호화 기법을 여러 가지 조합하여 암호화 모음을 만들었습니다. 클라이언트와 서버는 적절한 암호화 모음을 선택하기만 하면, 세 가지의 보안 목표를 달성할 수 있습니다. 암호화 모음은 구별을 위해서 이름을 가지고 있는데, 암호화 모음의 이름은 일반적으로 다음과 같은 구조를 가집니다. TLS_{키 합의 프로토콜}_{인증 방법}\_WITH\_{암호화 기법}_{데이터 무결성 체크 방법} 예를 들어서 TLS_DHE_RSA\_WITH\_AES\_256\_CBC_SHA256의 경우, 키 합의 프로토콜으로는 DHE, 인증 방법으로는 RSA, 암호화 기법으로는 AES\_256\_CBC, 데이터 무결성 체크 방법으로는 SHA256를 사용하는 것을 알 수 있습니다. 그리고 색깔로 구별해드린 것에서 눈치채셨겠지만 인증 방법은 인증성을, 암호화 기법은 기밀성을, 데이터 무결성 체크 방법은 무결성을 담당합니다. ![](https://cdn-images-1.medium.com/max/1600/0*f4-akeTSa9bJ2r-y) 위 그림은 위키피디아에 정리된 TLS가 지원하는 암호화 모음의 목록입니다. 각 항목의 조합으로 정말 많은 암호화 모음을 만들어서 지원하고 있습니다. [TLS1.2를 정의한 RFC5286의 appendix-C](https://tools.ietf.org/html/rfc5246#appendix-C)나 [IANA가 정의한 TLS의 표준 암호화 모음 페이지](http://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-4)에서 더 자세한 목록을 확인할 수 있습니다. #### 암호 기본 요소 (Cryptographic Primitives) 앞서 살펴봤던 암호화 모음은 여러 구성 요소로 이루어져 있었습니다. 마치 레고 작품을 완성하기 위한 레고 조각 같은 이러한 작은 알고리즘을 암호 기본 요소라고 부릅니다. 대표적으로는 대칭키 암호화, 비대칭키 암호화, 키 합의 프로토콜, 암호학적 해시 함수, 메시지 인증 코드 등이 있습니다. 이들이 어떻게 동작하는지 알아보고, 어떤 보안 목표를 달성할 수 있는지 생각해보면, TLS를 더 잘 이해할 수 있을 것입니다. 대칭키 암호화 ![](https://cdn-images-1.medium.com/max/1600/0*FqRQW2Sffylf_lbR) 우선 암호화 과정에서 자주 쓰이는 용어들에 대해서 정리하겠습니다. 전송할 원본 메시지를 평문(plain text), 이를 아무도 읽지 못하게 암호화한 것을 암호문(cipher text)이라고 합니다. 그리고 평문을 암호문으로 바꾸는 과정을 암호화(encryption), 암호문을 평문으로 해독하는 과정을 복호화(decryption)라고 합니다. 이 두 과정에는 다양한 수학적 기법들이 사용됩니다. 암호화/복호화 과정에는 항상 키(key)라는 특수한 데이터가 존재합니다. 이 키가 있어야만 암호화/복호화를 올바르게 수행할 수 있기 때문에, 키는 항상 안전하게 보관해야 합니다. 이름에서 유추할 수 있지만, 금고의 열쇠 같은 역할이라고 생각하면 좋습니다. 대칭키 암호화는 암호화를 하는데 사용하는 키와 복호화를 하는데 사용하는 키가 같은 암호화를 말합니다. 즉 앨리스와 밥은 공통의 키를 만들어두고 이브에게 키를 공개하지 않고 잘 보관하면 안전하게 메시지를 주고받을 수 있습니다. 대칭키 암호화는 수행 속도가 빠르다는 장점이 있고, 키를 아는 사람만 암호화/복호화를 할 수 있다는 점에서 기밀성을, 그리고 단 둘만 키를 가지고 있다면 그 자체로도 약간의 인증성을 제공할 수 있습니다. 대표적인 기법으로는 많이 들어보셨을 AES, 그리고 공인인증서의 암호화 방식으로 유명한 SEED, ARIA, 최근에 주목받고 있는 암호인 ChaCha20 등이 있습니다. 그런데 이 방식에는 한 가지 문제점이 있습니다. 멀리 떨어져 있는 두 대상이 안전하지 않은 네트워크를 사이에 두고 어떻게 같은 키를 공유할 것인가가 바로 그 문제입니다. 이에 대한 해결책은 이후 키 합의 프로토콜에서 살펴보도록 하겠습니다. 비대칭키 암호화 ![](https://cdn-images-1.medium.com/max/1600/0*zd4M7KS7XSUtHKmd) 비대칭키 암호화는 암호화 단계에 사용하는 키와 복호화 단계에 사용하는 키가 다른 암호화를 말합니다. 키는 한 쌍으로 구성되는데, 하나의 키로 암호화한 것은 다른 한쪽의 키로만 복호화할 수 있습니다. 일반적으로 하나는 안전하게 보관하는 개인 키로, 하나는 누구나 가질 수 있게 공개하는 공개 키로 사용합니다. 이 기법은 특정 공개 키에 대한 개인 키의 소유를 증명할 수 있다는 점에서 인증성을 보장해주고, 실제 암호화에 사용할 세션 키를 안전하지 않은 채널을 통해 교환하는 방법으로 사용할 수 있습니다. 공개 키로 암호화한 것은 개인 키를 이용해야만 복호화가 가능하다는 점에서 단방향으로 기밀성을 보장하지만, 암호화/복호화에 드는 비용이 크기 때문에 일반적인 암호화 통신에는 대칭키 암호화를 사용합니다. 대표적인 암호로는 RSA나 DSA가 있습니다. 디지털 인증서 ![](https://cdn-images-1.medium.com/max/1600/0*8YclDbTxqj9B9kDd) TLS에서는 ‘상대방이 통신하고자 하는 대상이 맞음’을 인증하기 위해서 디지털 인증서를 사용합니다. 디지털 인증서는 모두가 신뢰할 수 있는 제삼자인 CA와 비대칭키 암호화가 필요합니다. 밥은 우선 CA에게 자신이 밥임을 다양한 방식으로 증명하고 자신의 공개 키가 밥의 공개 키가 맞음을 인증하는 인증서를 발급받습니다. 이후 앨리스에게 이 인증서를 주면, 앨리스는 자신이 신뢰할 수 있는 CA가 발급한 올바른 인증서인지 확인하고, 맞으면 그 인증서에 포함된 밥의 공개 키로 데이터를 암호화해서 전달합니다. 만약 밥이 이것을 올바르게 복호화한다면 CA가 인증하는 밥의 공개 키에 대응하는 개인 키를 가지고 있다는 것이므로, 이 과정을 통해서 현재 통신하고 있는 상대방이 밥이 맞음을 인증할 수 있습니다. 키 합의 프로토콜 대칭키 암호화를 사용하기 위해서는 안전하지 않은 네트워크상에서 안전하게 키를 교환할 수 있어야 합니다. 이러한 역할을 하는 알고리즘을 key exchange, key agreement, key establishment 등 여러 이름으로 부릅니다. 여기서는 ‘키 합의 프로토콜’이라고 하겠습니다. 크게 두 가지 방법이 있는데, 하나는 앞서 살펴보았던 비대칭키 암호화 중 대표적인 RSA를 기반으로 한 프로토콜이고, 다른 하나는 디피-헬만 키 합의 프로토콜입니다. RSA 키 합의 프로토콜 ![](https://cdn-images-1.medium.com/max/1600/0*HBO_GA0lFw0STHhh) 먼저, 비대칭키 암호화인 RSA를 사용한 키 합의 프로토콜부터 살펴보겠습니다. 앨리스가 밥의 공개 키를 알고 있기 때문에, 앨리스는 앞으로 둘의 통신에 사용할 세션 키를 만들어서 이를 밥의 공개 키로 암호화한 다음 밥에게 전송합니다. 이를 복호화할 수 있는 개인 키를 가지고 있는 사람은 밥 뿐이므로, 제 3자인 이브는 세션 키를 얻을 수 없고, 앨리스와 밥은 안전하게 세션 키를 교환하여 보안 채널을 만들 수 있습니다. 그런데 겉보기에는 안전해보이는 이 방식에는 새로운 보안 문제가 있습니다. 순방향 비밀성 (Forward Secrecy) 밥의 개인 키를 모르는 이브는 우선 둘의 모든 통신을 기록해두기로 합니다. 왜냐하면 언제가 될지는 모르지만, 밥의 개인 키를 알아내기만 하면 그동안 기록해 둔 것을 복호화해서 앨리스가 밥에게 보낸 세션 키를 얻을 수 있고, 세션 키를 얻으면 둘의 대화 내용을 알아낼 수 있기 때문입니다. 실제로, 밥이 가지고 있는 개인 키는 언제든 어떤 이유로 인해 유출될 수 있습니다. 예를 들어, 2014년에 세상을 떠들썩하게 했던 [Heartbleed 버그(CVE-2014–0160)](http://heartbleed.com/)가 있습니다. 이 버그는 OpenSSL 구현의 취약점으로, 공격자가 서버의 메모리상에 존재하지만 허가되지 않은 데이터를 얻을 수 있게 합니다. 이러한 방법이든 아니면 또 다른 방법이든 밥의 개인 키가 유출되고 나면, 앨리스와 밥의 통신은 개인 키가 유출되기 전에 이루어진 것이더라도 안전하지 않게 됩니다. 이와 같은 상황에 대비하여, 현재에만 안전한 것이 아니라 미래에 개인 키가 유출되어도 안전하게 하는 보안 목표를 순방향 비밀성 (forward secrecy), 또는 완전 순방향 비밀성 (perfect forward secrecy, PFS)이라고 합니다. 우리가 살펴보았던 RSA 기반의 키 합의 프로토콜은 순방향 비밀성을 제공하지 않음을 알 수 있습니다. 이제 순방향 비밀성을 제공하는 키 합의 프로토콜에 대해서 알아볼 차례입니다. ![](https://cdn-images-1.medium.com/max/1600/0*q6xB2sDXc8VPx1sd) 디피-헬만(-머클) 키 합의 프로토콜 (Diffie-Hellman(-Merkel) Key Exchange, DHM, DH) 디피-헬만 키 합의 프로토콜이라고 알려졌지만 기본 아이디어를 제공한 머클의 공로를 인정하여 디피-헬만-머클 키 합의 프로토콜으로 불러야 한다는 의견이 있는 위대한 기법을 소개할 순서입니다. 이 기법은 안전하지 않은 채널에서 안전하게 세션 키를 만들 수 있게 하며, 순방향 비밀성도 보장합니다. 이것이 가능한 이유는 네트워크를 통해 세션 키를 전달하는 것이 아니라, 세션 키를 만들 수 있는 힌트만을 네트워크상으로 전달하기 때문입니다. 위키피디아에서 가져온 아래 그림을 보시면 조금 더 쉽게 이해할 수 있습니다. ![](https://cdn-images-1.medium.com/max/1600/0*c41cwd4iFvrDBNI-) 위 그림에서는 키를 색에 비유해서 설명하고 있습니다. 목표는 앨리스와 밥이 안전하지 않은 통신을 통해서 공통의 색을 만드는 것입니다. 여기에는 가정이 하나 있는데, 색깔의 혼합은 일방향함수라는 것입니다. 풀어서 설명하면, 두 색을 섞어서 새로운 색을 만들기는 쉽지만, 섞여 있는 색을 두 개의 원래 색으로 분리하는 것은 어렵다는 것입니다. 우선 앨리스와 밥은 이번 통신에 사용할 기저 색을 공개적으로 정합니다. 이 그림에서는 노란색입니다. 그 후 앨리스는 빨간색, 밥은 청록색을 비밀 색으로 정했습니다. 둘 다 직접 비밀 색을 전송하지 않고, 기저 색인 노란색과 각자의 비밀 색을 섞은 결과인 오렌지색과 파란색을 전송합니다. 앨리스와 밥은 각자 받은 것을 자신의 비밀 색과 섞으면 공통의 색을 만들 수 있습니다. 하지만 이브는 기저 색과 주황색, 파란색을 가지고도 가정에 의해서 공통의 색을 만들어낼 수 없습니다. 이브가 훗날 어느 쪽이든 비밀 색을 알아내면 공통 색을 만들 수 있기 때문에, 앨리스와 밥은 더는 필요 없는 비밀 색인 빨간색과 청록색을 버립니다. 이러한 방식을 수학적으로 구현한 것이 디피-헬만 키 합의 프로토콜으로, 실제로는 이산 로그 문제라는 것을 이용해서 일방향함수를 만들어 사용합니다. 이러한 방식으로 키를 교환하면, 둘의 통신을 기록해두고 훗날 밥의 개인 키를 탈취하더라도 얻을 수 있는 것은 힌트뿐이므로 여전히 복호화를 할 수 없습니다. 그리고 세션 키를 만드는 데에 사용한 비밀 키들은 폐기되었기 때문에, 탈취할 수 없습니다. 짧게 요약하면, 디피-헬만 키 합의 프로토콜은 순방향 비밀성을 제공합니다. 무결성 데이터의 무결성을 제공하기 위한 암호 기본 요소에는 해시 함수(hash function)와 키 있는 해시 함수(keyed hash function)가 있습니다. ![](https://cdn-images-1.medium.com/max/1600/0*6K4KaXV7biUuyeCd) 해시 함수는 어떤 임의의 데이터를 입력으로 받아서 일정한 길이의 데이터로 바꾸어주는 함수를 말하는데, 이때 나오는 결과인 일정한 길이의 데이터를 해시 또는 해시 값이라고 합니다. 이 중에서도 암호학적으로 강점을 가지는 요소들을 가진 해시 함수들을 암호학적 해시 함수라고 합니다. 암호학적으로 강점을 가지는 요소에는 첫째, 해시 값만을 보고서 입력 데이터를 찾기 어려울 것, 둘째, 특정 입력 데이터의 해시 값과 같은 해시 값을 가지는 다른 데이터를 찾기 어려울 것, 셋째, 같은 해시 값을 가지는 서로 다른 두 입력 데이터를 찾기 어려울 것이 있습니다. 이러한 조건들을 만족하는 대표적인 암호학적 해시 함수로는 SHA-256이 있습니다. 암호학적 해시 함수들은 이러한 특성들 덕분에 데이터의 무결성을 보장하는 데에 사용할 수 있습니다. 예를 들어서 특정 메시지에 해시 값을 이어붙여서 전송하면, 전송 중에 메시지가 변경되었을 경우 해시 값이 다르다는 점을 통해서 오염된 메시지임을 알 수 있습니다. 하지만 완벽한 무결성을 제공하지는 못합니다. 왜냐하면 이브도 아무 데이터에 대해서 올바른 해시 값을 계산할 수 있기 때문입니다. 이브가 메시지의 본문을 바꾼 후, 해시 값도 변경된 데이터에 알맞은 것으로 변경하는 방식으로 위조(forgery attack)하면 밥은 지금 받은 데이터가 앨리스가 보낸 데이터가 맞는지 아니면 변경된 것인지 알 수 없습니다. 키 있는 해시 함수는 이러한 문제를 해결할 수 있습니다. ![](https://cdn-images-1.medium.com/max/1600/0*6wBdpls3mTqb71Vo) 키 있는 해시 함수에도 여러 가지 종류가 있는데, 우리가 살펴볼 것은 메시지 인증 코드(message authentication code, MAC)입니다. 메시지 인증 코드는 대칭키 암호화의 개념과 해시 함수를 섞은 형태라고 생각하면 쉽게 이해할 수 있습니다. 키에 따라서 메시지를 넣었을 때 나오는 해시 값이 달라지는 것이 특징입니다. 따라서 키를 모르는 이브는 메시지를 변경하고 그에 맞는 해시 값을 계산하여 붙이려고 해도 올바른 해시 값을 계산할 수 없으므로, 위조나 변조를 할 수 없습니다. 앨리스와 밥은 공통의 키를 가지고 있기 때문에, 같은 해시 값을 계산해 낼 수 있습니다. 키를 알고 있는 사람만이 올바른 해시 값을 만들 수 있다는 점에서 앨리스가 보낸 메시지가 변조되지 않았다는 무결성을 보장할 수 있으며, 해당 해시를 계산하는데 사용할 키를 가지고 있음을 증명하기 때문에 약한 인증성도 제공합니다. 메시지 인증 코드에는 대표적으로 HMAC, CMAC, Poly1305, SipHash가 있습니다. 참고로, python 3.x에서는 2.x 버전과 다르게 해시 함수로 siphash를 사용하기 때문에 실행할 때마다 키가 바뀌어서 사전의 항목 순서가 매번 바뀌게 됩니다. #### TLS 1.2 핸드셰이크 (Handshake) 암호화 모음을 구성하는 암호 기본 요소에 대해서 모두 알아보았습니다. 이제 TLS에서 어떻게 이러한 것들을 준비하는지 알아보도록 하겠습니다. TLS에서 보안 채널을 만들기 위해 실제 데이터를 보내기 전에 서로 합의하며 준비하는 과정을 핸드셰이크라고 합니다. 이 과정에서는 어떤 것들을 합의하고 준비해야 할까요? 우선 나와 지금 핸드셰이크를 시작할 상대방의 신원을 파악해야 합니다. 신원이 파악이 안 되면 다른 것들을 합의하는 것은 의미가 없을 것입니다. 그다음 할 일은 양쪽이 지원하는 암호화 모음을 살펴보고, 가장 적절한 것을 하나 골라야 합니다. 그다음으로 해당 암호화 모음을 사용하기 위해서 필요한 각종 파라미터를 합의하고, 세션 키를 만들면, TLS 보안 채널이 완성됩니다. 마지막으로 만들어진 TLS 보안 채널을 통해서 암호화/복호화를 진행하면서 데이터를 주고받으면 됩니다. 이 과정이 핸드셰이크에서 이루어지는 것들이고, 아래는 간략하게 나타낸 TLS 1.2의 핸드셰이크 과정입니다. ![](https://cdn-images-1.medium.com/max/1600/0*Vp5YBSfbOdKCA7eY) ClientHello에서 클라이언트는 자신이 사용할 TLS 버전, 사용 가능한 암호화 모음 등을 서버에 보냅니다. 서버는 그중에서 암호화 모음 하나를 선택해서 ServerHello에 기입하고 자신의 인증서와 함께 클라이언트에 보냅니다. 클라이언트는 서버의 인증서를 평가한 후, ClientKeyExchange에 세션 키를 만들어서 서버의 공개 키로 암호화해서 전달합니다. ChangeCipherSpec은 이후 보내는 모든 메시지는 암호화될 것임을 상대에 알리는 용도입니다. 지금까지, TLS의 필요성에 대해 살펴보고, TLS의 보안 목표와 이들을 달성할 수 있게 한 기본 암호 모음, 그리고 TLS 1.2의 핸드셰이크까지 살펴보았습니다. 이제부터 최신 버전인 TLS 1.3에서는 어떤 것들이 바뀌었는지 살펴봅시다. ### TLS 1.3 TLS 1.3이 개선한 것들을 정리해보면 크게 속도와 정리 그리고 보안으로 요약할 수 있습니다. #### TLS 1.3의 속도 TLS 1.3은 RTT를 줄여서 속도를 개선하였습니다. RTT는 round trip time을 말하는 것으로, 어떤 데이터를 상대방에게 보내기 시작한 시간부터 그것에 대한 응답을 받기 시작하는 시간까지, 즉 데이터가 네트워크를 한 바퀴 돌아서 다시 돌아오는데 걸리는 시간을 말합니다. TLS는 보안 채널을 구성하기 위해서 핸드셰이크 과정이 필요한데, 이 과정에서 많은 시간이 소요됩니다. 핸드셰이크는 크게 처음 연결하는 경우와 기존에 연결했던 것을 다시 연결하고자 하는 경우가 있는데요, TLS 1.3는 기존과 비교해서 각각 1 RTT만큼을 줄였습니다. 구체적으로는 처음 연결 시 1-RTT가 소요되고, 다시 연결 시에는 0-RTT를 시범적으로 지원합니다. ![](https://cdn-images-1.medium.com/max/1600/0*PbM8y813yS2Xv7B1) ![](https://cdn-images-1.medium.com/max/1600/0*RGV0uymO6Nr53O5s) 왼쪽은 TLS 1.2의 핸드셰이크, 오른쪽은 TLS 1.3의 핸드셰이크를 정리한 것입니다. 왼쪽은 클라이언트-서버-클라이언트-서버의 2 RTT 후에 암호화된 데이터가 송수신되는 것을 볼 수 있지만, 오른쪽은 클라이언트-서버의 1 RTT 후에 Finished와 암호화된 데이터가 같이 전달되는 것을 확인할 수 있습니다. #### TLS 1.3의 정리 #### 암호화 모음 관리 단순화 TLS 1.3에서는 기존의 암호화 모음 관리 방법이 가지던 복잡성을 줄이기 위해서 암호화 모음을 3개의 요소로 분리하고, 이를 조합하는 형태로 바꾸었습니다. 아래 그림을 보면서 설명을 하겠습니다. ![](https://cdn-images-1.medium.com/max/1600/0*njJapA2kBadnauep) ![](https://cdn-images-1.medium.com/max/1600/0*G4T8Pq2bF4dXiBX_) 왼쪽은 TLS 1.2에서 지원하던 암호화 모음의 목록을 일부 가져온 것이고, 오른쪽은 TLS 1.3에서 조합해서 쓰는 형태로 변경한 것을 나타냅니다. 왼쪽은 지원하는 모든 암호화 모음의 조합을 나열하고 있는데, 하나의 암호화 모음은 키 합의 프로토콜, 인증 알고리즘, 암호화 기법, 무결성 체크 기법을 모두 묶은 것으로 한 줄 한 줄에 대해서 구별이 되는 코드값을 부여하고 있습니다. 이러한 방식은 확장성이 매우 떨어지는데, 만약 키 합의 프로토콜에서 새로운 프로토콜이 추가된다면, 다른 모든 조합에 대해서 무수히 많은 새로운 항목을 만들어야 합니다. 이에 반해서 오른쪽 방법은 서로 구별이 되는 요소들인 암호화 기법, 키 합의 프로토콜, 인증 알고리즘으로 분리해서 나타내기 때문에 키 합의 프로토콜에 새로운 프로토콜이 추가되어도 가운데 항목에만 새로운 값을 부여하면 나머지 모든 조합도 자동으로 지원하게 됩니다. 실제 예시를 들어보면 기존에는 TLS\_ECDHE\_ECDSA\_WITH\_CHACHA20\_POLY1305\_SHA256라는 이름의 암호화 모음이 있었고, 이는 0xCCA9라는 값을 가지고 있었습니다. TLS 1.3부터는 와 같이 세 요소의 조합으로 나타낼 수 있습니다. #### TLS 1.3의 보안 TLS 1.2까지는 기존과의 호환성을 위해 지원하던 legacy 기능이 많이 있었지만, 이러한 기능들은 오래된 것들이다 보니 시간이 지나서 보안에 위협이 되기도 했습니다. TLS 1.3에서는 보안을 강화하기 위해서, 기존의 TLS 1.2에서 지원하였던 많은 기능을 삭제하고 정리하였습니다. 주로 상대적으로 더 높은 보안 강도를 가지는 키 길이 (256bit), 짧은 길이로도 더 높은 보안 강도를 높이는 타원 곡선 암호, 미래에도 안전함을 보장하는 순방향 비밀성을 기준으로 선택한 것으로 보입니다. 어떤 항목들을 어떻게 정리하였는지 항목별로 살펴보겠습니다. 순방향 비밀성을 고려한 키 합의 프로토콜 ![](https://cdn-images-1.medium.com/max/1600/0*2pFGqbahVu-A6cjh) ![](https://cdn-images-1.medium.com/max/1600/0*hzb9HqJqMb7LVt_H) 왼쪽은 TLS1.2에서 지원하던 키 합의 프로토콜을 나타내고, 오른쪽은 TLS 1.3에서 지원하는 키 합의 프로토콜을 나타냅니다. TLS 1.3에서는 순방향 비밀성을 보장하지 않는 RSA 등의 다른 방법들은 제거되었고 (EC)DHE, PSK-only, PSK with (EC)DHE만 남았습니다. PSK는 미리 안전한 방법으로 공유된 키를 사용하는 방법인 사전 공유 키(pre-shared key)의 줄임말로, 엄밀히는 순방향 비밀성을 보장하지는 않습니다. PSK를 사용하면서 순방향 비밀성을 보장하고자 할 때는 PSK with (EC)DHE를 사용하면 됩니다. 강한 기법들만 남은 서명 알고리즘 ![](https://cdn-images-1.medium.com/max/1600/0*XC4IU-k1agJjhpd8) 서명 알고리즘에서도 변경이 있었습니다. TLS 1.2에서는 RSA, 디지털 서명 알고리즘(DSA), 타원곡선 디지털 서명 알고리즘(ECDSA)을 지원하고 있었습니다. 2048 bit의 키를 지원하는 RSA와 비교해서 1024 bit이라는 짧은 키를 강제하는 문제가 있었던 디지털 서명 알고리즘은 TLS 1.3에서 삭제되었습니다. 그리고 에드워드 곡선이라는 안전하다고 알려진 새로운 종류의 타원 곡선을 사용하는 에드워드 곡선 디지털 서명 알고리즘(EdDSA)이 추가되었습니다. AEAD만 살아남은 암호화 모음 ![](https://cdn-images-1.medium.com/max/1600/0*6tm-Ly6hyimFWs2o) TLS 1.3에서는 암호화 모음의 정의가 바뀌었습니다. 앞서 ‘암호화 모음 관리 단순화’에서 살펴봤습니다만, TLS 1.2에서 암호화 모음이라는 것은 보안 채널을 만드는데 필요한 모든 암호 기본 요소들의 조합으로, 키 합의 프로토콜, 인증 알고리즘, 암호화 기법, 무결성 체크 기법 등을 포함하는 묶음이었습니다. 하지만 TLS 1.3에서는 키 합의 프로토콜과 인증 알고리즘이 분리되었으므로, 암호화 모음은 암호화 기법과 무결성 체크 기법을 담당합니다. 위 그림은 TLS 1.3에서 지원하는 암호화 모음을 나타낸 것입니다. 기존의 무수히 많았던 것들이 모두 삭제되었고 위 그림의 5가지 암호화 모음이 새로 정의되었는데, 이름은 ‘TLS\_AEAD\_해시 함수’의 형태로 붙였습니다. 해시 함수는 키를 생성하기 위해 사용하는 것으로, SHA256 이상을 사용합니다. AEAD는 처음 들어보는데 무엇일까요? 연관 자료가 있는 인증 암호화(Authenticated Encryption with Associated Data, AEAD) 번역한 이름이 매우 길기 때문에, 이 글에서는 AEAD라고 쓰겠습니다. AEAD의 출발은 AE, 즉 인증 암호화입니다. 기존에는 암호화 기법과 무결성 체크 기법이 분리되어 있었는데, 여기에는 사소하지만 중요한 문제가 있었습니다. 바로 어떤 순서로 각 기법을 적용해야 할 것인가입니다. 암호화와 무결성 체크를 각각 한 다음에 합칠지(Encrypt-and-MAC), 암호화를 먼저 적용하고 무결성 체크를 할지(Encrypt-then-MAC), 무결성 체크를 먼저 하고 암호화를 할지(MAC-then-Encrypt)를 두고 의견이 분분했습니다. 그중 일부는 취약점이 발견되기도 하고, 방법 자체는 문제가 없으나 구현의 실수로 취약점이 생기기도 했습니다. 이러한 혼란을 막기 위해서, 인증 암호화라는 기법이 만들어졌습니다. 내부적으로 암호화와 인증을 올바르게 구현하였기 때문에 사용하는 사람은 쉽게 사용만 하면 됩니다. 연관 데이터(AD)는 무결성 체크에는 필요하지만 암호화할 필요는 없거나, 하면 안 되는 데이터를 말합니다. 예를 들어서 암호화된 패킷에 대한 메타 데이터인 헤더 같은 정보들은 위조는 막아야 하지만 암호화가 되면 안 되므로, 연관 데이터로 AEAD에 넣어주면, 헤더를 자유롭게 아무나 읽을 수 있지만, 헤더가 변경된 경우 감지할 수 있습니다. 대표적인 AEAD로는 AES\_GCM, CHACHA20\_POLY1305 등이 있습니다. 핸드셰이크 전체에 대한 인증 TLS 1.2는 다양한 오래된 기법들을 지원하고 있었기 때문에 [FREAK](https://nvd.nist.gov/vuln/detail/CVE-2015-0204)과 같이 중간에서 낡고 취약한 기법으로 보안 수준을 낮추는 공격에 당하기 쉬웠습니다. TLS 1.3에서는 이렇게 TLS의 헤더 값을 바꾸어서 취약한 기법으로 변경하는 것을 막기 위해서, 특히 첫 번째 핸드셰이크 패킷인 ClientHello에 대해서도 자신이 받은 데이터에 대해 인증하는 과정을 추가하였습니다. ![](https://cdn-images-1.medium.com/max/1600/0*WbJ0DeUG3Hhk855y) 위 그림을 보시면, TLS 1.2에서는 ClientHello에 대한 인증 절차가 없기 때문에 이브가 중간에서 헤더를 변경해서 취약한 암호화 기법만 쓰도록 하여도 서버나 클라이언트가 이를 눈치챌 방법이 없습니다. TLS 1.3에서는 서버는 ClientHello에 대해서 서명(signature)을 생성하여 ServerHello에 추가하여 클라이언트에 보냅니다. 클라이언트는 자신이 보낸 데이터로 생성한 서명과 서버가 보낸 서명을 비교하여 서버가 자신이 보낸 데이터를 변경 없이 올바르게 잘 받았는지 확인할 수 있습니다. 더 이른 단계부터 암호화 지원 ![](https://cdn-images-1.medium.com/max/1600/0*Rb2ciHvDus8Yn2II) TLS 1.3은 TLS 1.2와 비교해서 핸드셰이크 과정이 1-RTT로 줄었습니다. 이와 함께, 서버가 처음 보내는 패킷인 ServerHello부터 응용프로그램의 데이터를 암호화해서 전송할 수 있게 됩니다. 타원 곡선 정리 오늘날 주목을 받는 암호화 기법에는 타원 곡선 암호가 있습니다. 타원 곡선이라는 특수한 집합에서 수학적 연산을 정의하고, 그 정의 위에서 각종 암호화 기법을 구현한 것으로 이산 로그 문제의 한 종류입니다. 타원 곡선 암호는 기존의 소인수 분해의 어려움에 기반한 RSA와 비교했을 때, 키 길이 대비 더 높은 보안 수준을 제공하고, 더 빠르다는 장점이 있습니다. 예를 들어서 RSA로는 2048 bit의 키를 사용해서 얻을 수 있는 보안 수준보다 256 bit의 키를 사용한 타원 곡선 암호의 보안 수준이 훨씬 높으며, 연산 속도는 20배나 빠릅니다. 이러한 타원 곡선 암호는 높은 보안 수준을 가지는 안전한 타원 곡선을 선택하는 것이 중요합니다. 아래는 TLS 1.2와 TLS 1.3에서 지원하는 타원 곡선의 목록을 나타낸 것입니다. ![](https://cdn-images-1.medium.com/max/1600/0*u8wB6U5fHyv69tPW) 기존에 존재하던 많은 타원 곡선들이 대부분 삭제되었고, 안전하다고 알려진 몇 가지 타원 곡선들만 남았습니다. ### TLS의 역사 TLS에 대해서 전반적으로 살펴보았는데, TLS의 역사에 대해서 간단히 살펴보면 좋을 것 같습니다. TLS의 시작은 아주 오래전, 1994년으로 거슬러 올라갑니다. 당시 웹 기술을 선도하던 넷스케이프 사는 인터넷 결제를 위해서는 안전한 프로토콜이 필요하다고 생각하였고, TLS의 전신인 보안 소켓 계층(Secure Socket Layer, SSL)이라는 프로토콜을 발표했습니다. 이후 SSL은 IETF에 의해 국제적인 표준화가 이루어 지면서 전송 계층 보안(Transport Layer Security, TLS)이라는 새로운 이름을 갖게 됩니다. TLS의 발전 역사는 각종 보안 취약점과의 전쟁의 역사라고 생각할 수 있는데, 아래 그림을 보시면 TLS가 각종 보안 취약점과 싸우면서 어떻게 발전해왔는지 개략적으로 알 수 있습니다. 주황색은 각종 보안 취약점을 나타내고, 파란색은 SSL/TLS의 버전을 나타냅니다. 현재도 가장 인터넷에서 널리 쓰이고 있는, 그리고 조금 전까지 알아본 TLS 1.2가 발표된 2008년 이후 상당히 많은 취약점이 발표된 것을 알 수 있습니다. 그리고 2014년, 더는 두고 볼 수 없었던 전문가들은 이러한 취약점들을 개선한 TLS 1.3을 준비하기로 하였고, 4년이 지난 2018년에 드디어 1.3 버전이 확정되게 되었습니다. ![](https://cdn-images-1.medium.com/max/1600/0*W17fcZREd2P1WQv_) #### TLS 1.3을 준비하는데 왜 그리 오래 걸렸을까? TLS 1.3을 준비하는데 걸린 시간은 4년이었는데, 그 이유는 바로 하위 호환성 때문이었습니다. 인터넷에는 컴퓨터를 비롯하여 라우터, 방화벽 등 수많은 미들웨어가 존재합니다. 이러한 미들웨어들이 제대로 TLS를 구현하지 않아서 새로운 버전을 지원하지 않는 문제가 발생했습니다. 헤더에 TLS 1.3을 의미하는 0x0304를 넣으면 기존에는 존재하지 않았던 버전이기 때문에, 잘못된 패킷이나 공격으로 감지하거나, 제대로 동작하지 않는 일이 발생한 것입니다. 이 문제를 해결하기 위해서, TLS 1.3은 기존의 버전을 나타내는 값에는 TLS 1.2와 같은 값을 사용하고, SupportedVersions라는 추가 확장을 통해서 자신이 지원하는 모든 버전을 명시하는 방식으로 변경했습니다. 왜 이런 방식으로 변경했는지에 대해서는 [Cloudflare에서 흥미롭게 다룬 글](https://blog.cloudflare.com/why-tls-1-3-isnt-in-browsers-yet/)이 있으니, 읽어보셔도 좋습니다. ![](https://cdn-images-1.medium.com/max/1600/0*jGjtrQZFuA7F5eND) ### TLS 1.3을 당장 쓸 수 있을까? 열심히 TLS에 대해 공부했고, TLS 1.3에 대해서도 알아봤으니 이제 TLS 1.3을 사용하는 일만 남았습니다. 지금 당장 TLS 1.3을 사용할 수 있는지, 웹 브라우저와 각종 TLS 라이브러리의 지원 상황을 알아보고 서버의 TLS 1.3 설정이 올바르게 되어 있는지 테스트할 방법에 대해 간단히 알아보겠습니다. #### 웹 브라우저의 TLS 1.3 지원 여부 ![](https://cdn-images-1.medium.com/max/1600/0*n9EAZcdVq4XYbJi3) 주요 웹브라우저에 대해서 살펴보면, 2019년 1월인 현재에도 파이어폭스와 크롬, 삼성 인터넷만이 TLS 1.3을 지원하는 것을 확인할 수 있습니다. IE와 Edge, Safari, Opera 등은 아직 TLS 1.3을 지원하지 않습니다. 크롬의 경우는 chrome://flags로 들어가서 TLS 1.3 플래그를 켜야 합니다. 이 플래그를 켜고 TLS 1.3을 지원하는 서버에 접속하면 아래와 같이 TLS 1.3을 사용하는 것을 확인할 수 있습니다. ![](https://cdn-images-1.medium.com/max/1600/0*I0VNgd_ayNiRe4n3) #### TLS 라이브러리들의 TLS 1.3 지원 여부 아래 이미지는 주요 TLS 라이브러리의 목록을 위키피디아에서 찾은 것인데요. 제가 처음 발표자료를 준비했던 작년 9월과 비교해보니 TLS 1.3을 지원하는 라이브러리가 많이 늘었습니다. ![](https://cdn-images-1.medium.com/max/1600/0*49aBe1h__eS2a1W4)   #### HTTP 서버의 TLS 1.3 지원 우선 TLS 1.3을 지원하는 OpenSSL 1.1.1을 사용해야 합니다. apache2의 경우는 2.4.36부터, nginx는 1.13.0부터 TLS 1.3을 지원합니다. 한편, go는 TLS 1.3을 지원하는 1.12 버전이 2019년 2월 현재 beta 상태이기 때문에, 이를 기반으로 하는 Caddy는 아직 TLS 1.3을 지원하는 버전이 공식적으로 배포되지 않았습니다. Caddy에서 TLS 1.3을 사용하려면 [Caddy의 TLS 1.3 지원 이슈에 달린 이 댓글](https://github.com/mholt/caddy/issues/2080#issuecomment-439564485)에서 제안하는 대로 go의 1.12-beta 버전을 직접 빌드한 후, 패치하면 됩니다. #### TLS 1.3 테스트 방법 [SSL Labs의 테스트](https://www.ssllabs.com/ssltest/)를 이용하면 특정 서버가 TLS 1.3을 지원하는지 확인할 수 있습니다. 아래 스크린샷은 TLS 1.3을 지원하는 Cloudflare의 블로그를 테스트한 결과입니다. ![](https://cdn-images-1.medium.com/max/1600/0*atmPnN5-35ylM5Jh) ### DJB TLS 1.3이 선택한 변화들을 살펴보면 DJB의 업적에 대한 언급을 하지 않을 수 없습니다. Daniel Julius Bernstein 이라는 이름의 암호학자인데, 그의 몇 가지 주목할만한 업적을 살펴보면 다음과 같습니다. * 미국 정부를 상대로 암호화 기술 수출 제한 조치에 대해 소송 * 해시 함수에 대한 DDoS 공격을 막기 위해 SipHash 공동 개발: python 3.x 등에서 사용 * 안전하고 빠른 타원 곡선 Curve25519 제안: OpenSSH에서 기본 곡선으로 사용 * 안전하고 빠른 서명 알고리즘 ed25519 개발: TLS 1.3에 추가 * 특히 모바일 기기에서 AES보다 빠른 대칭키 암호화 ChaCha20 개발: 크롬, TLS 1.3에서 사용 * IV를 가지는 MAC Poly1305 개발: TLS 1.3에서 사용 ### 마치며 이상으로 TLS가 필요한 이유부터 TLS가 제공하고자 하는 보안 목표를 살펴보고, 어떤 기법들을 이용해서 TLS가 보안 목표를 달성하는지 살펴보았습니다. TLS 1.2의 핸드셰이크를 간단히 살펴보았고, TLS 1.3에서 여러 변경된 것들을 살펴보았습니다. TLS 1.3은 안전한 기법들을 안전하게 사용할 수 있도록, 그리고 현재의 인터넷에서도 잘 동작할 수 있게 설계되었습니다. 이제는 ‘당연한 것’이 되었지만, 여전히 막연하고 어려운 것으로 생각했을 TLS를 이 글을 통해서 조금은 친해질 수 있었으면 좋겠습니다. 더 많은 사람이 TLS와 최신 버전인 TLS 1.3에 대해 이해하고 사용함으로써 인터넷이 더 안전해지기를 바랍니다. 참고) 발표 자료의 내용을 글로 읽을 때 자연스럽도록 순서를 바꾸거나, 생략하거나, 새로운 정보를 추가하였습니다.   **슬라이드와 글을 작성하는 데에 참고한 자료 모음** 웹 브라우저의 HTTP 안전하지 않음 처리 크롬 * [Emily Schechter. 2018. A milestone for Chrome security: marking HTTP as “not secure”](https://www.blog.google/products/chrome/milestone-chrome-security-marking-http-not-secure/) * [Emily Schechter. 2018. Evolving Chrome’s security indicators](https://blog.chromium.org/2018/05/evolving-chromes-security-indicators.html) 파이어폭스 * [Catalin Cimpanu, 2018. Firefox Prepares to Mark All HTTP Sites “Not Secure” After HTTPS Adoption Rises](https://www.bleepingcomputer.com/news/software/firefox-prepares-to-mark-all-http-sites-not-secure-after-https-adoption-rises/) TLS * [RFC2246: T. Dierks. 1999. The TLS Protocol Version 1.0](https://tools.ietf.org/html/rfc2246) * [RFC5246: T. Dierks. 2008. The Transport Layer Security (TLS) Protocol Version 1.2](https://tools.ietf.org/html/rfc5246) * [Agathoklis Prodromou. 2017. TLS/SSL Explained — A brief history of TLS/SSL, Part 2](https://www.acunetix.com/blog/articles/history-of-tls-ssl-part-2/) * [Nick Sullivan. 2016. Introducing CFSSL 1.2](https://blog.cloudflare.com/introducing-cfssl-1-2/) * [Dokydoky. 2018. HTTPS — 2. HTTPS의 Ciphersuite, Handshake, Key derivation](https://dokydoky.tistory.com/463) * [IANA. 2005–2018. Transport Layer Security (TLS) Parameters](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml) TLS 1.3 * [RFC8446: E. Rescorla. 2018. The Transport Layer Security (TLS) Protocol Version 1.3](https://tools.ietf.org/html/rfc8446) * [Jay Thakkar. 2018. The IETF has FINALLY published TLS 1.3 as RFC 8446](https://www.thesslstore.com/blog/tls-1-3-approved/) * [WolfSSL. 2018. TLS 1.3 Performance Part 2 — Full Handshake](https://www.wolfssl.com/tls-1-3-performance-part-2-full-handshake/) * [Filippo Valsorda. 2016. An overview of TLS 1.3 and Q&A](https://blog.cloudflare.com/tls-1-3-overview-and-q-and-a/) * [Nick Sullivan. 2017. Why TLS 1.3 isn’t in browsers yet](https://blog.cloudflare.com/why-tls-1-3-isnt-in-browsers-yet/) * [Nick Sullivan. 2018. A Detailed Look at RFC 8446 (a.k.a. TLS 1.3)](https://blog.cloudflare.com/rfc-8446-aka-tls-1-3/) 암호 기본 요소 * [SSL2Buy. Symmetric vs. Asymmetric Encryption — What are differences?](https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences) * [Tim Taubert. 2016. THE EVOLUTION OF SIGNATURES IN TLS — Signature algorithms and schemes in TLS 1.0–1.3](https://timtaubert.de/blog/2016/07/the-evolution-of-signatures-in-tls/) * [Cipher suite, Wikipedia](https://en.m.wikipedia.org/wiki/Cipher_suite) * [Diffie–Hellman key exchange, Wikipedia](https://en.m.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange) * [Hash function, Wikipedia](https://en.m.wikipedia.org/wiki/Hash_function) * [Authenticated encryption, Wikipedia](https://en.m.wikipedia.org/wiki/Authenticated_encryption) * [EdDSA, Wikipedia](https://en.m.wikipedia.org/wiki/EdDSA#Software) ChaCha * [Elie Bursztein. 2014. Speeding up and strengthening HTTPS connections for Chrome on Android](https://security.googleblog.com/2014/04/speeding-up-and-strengthening-https.html) * [Nick Sullivan. 2015. Do the ChaCha: better mobile performance with cryptography](https://blog.cloudflare.com/do-the-chacha-better-mobile-performance-with-cryptography/) * [Vlad Krasnov. 2016. It takes two to ChaCha (Poly)](https://blog.cloudflare.com/it-takes-two-to-chacha-poly/) 타원 곡선 암호 * [Andrea Corbellini. 2015. Elliptic Curve Cryptography: a gentle introduction](https://andrea.corbellini.name/2015/05/17/elliptic-curve-cryptography-a-gentle-introduction/) * [Andrea Corbellini. 2015. Elliptic Curve Cryptography: breaking security and a comparison with RSA](https://andrea.corbellini.name/2015/06/08/elliptic-curve-cryptography-breaking-security-and-a-comparison-with-rsa/) * [Kelly Bresnahan. 2016. Elliptic Curve Cryptography](https://www.slideshare.net/KellyBresnahan/elliptic-curve-cryptography-66406021) * [RFC4492: S. Blake-Wilson, et al. 2006. Elliptic Curve Cryptography (ECC) Cipher Suites for Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc4492) * [RFC8422: Y. Nir. 2018. Elliptic Curve Cryptography (ECC) Cipher Suites for Transport Layer Security (TLS) Versions 1.2 and Earlier](https://tools.ietf.org/html/rfc8422) * [Curve25519, Wikipedia](https://en.m.wikipedia.org/wiki/Curve25519) TLS 1.3 지원 여부 * [Apache: Changes with Apache 2.4.36](https://github.com/apache/httpd/blob/2.4.36/CHANGES) * [Nginx: Change Log v1.14](http://nginx.org/en/CHANGES-1.14) * [Go: 이슈 — crypto/tls: add support for TLS 1.3](https://github.com/golang/go/issues/9671) * [Caddy: 이슈 — TLS 1.3](https://github.com/mholt/caddy/issues/2080) DJB * [Jesse Victors. 2016. TLS 1.3 and the future of cryptographic protocols](https://www.synopsys.com/blogs/software-security/tls-1-3/.) * [Daniel J. Bernstein, Wikipedia](https://en.m.wikipedia.org/wiki/Daniel_J._Bernstein) * [Bernstein v. United States, Wikipedia](https://en.m.wikipedia.org/wiki/Bernstein_v._United_States) RFC 표준 문서 * [RFC2246: T. Dierks. 1999. The TLS Protocol Version 1.0](https://tools.ietf.org/html/rfc2246) * [RFC5246: T. Dierks. 2008. The Transport Layer Security (TLS) Protocol Version 1.2](https://tools.ietf.org/html/rfc5246) * [RFC8446: E. Rescorla. 2018. The Transport Layer Security (TLS) Protocol Version 1.3](https://tools.ietf.org/html/rfc8446) * [RFC4492: S. Blake-Wilson, et al. 2006. Elliptic Curve Cryptography (ECC) Cipher Suites for Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc4492) * [RFC8422: Y. Nir. 2018. Elliptic Curve Cryptography (ECC) Cipher Suites for Transport Layer Security (TLS) Versions 1.2 and Earlier](https://tools.ietf.org/html/rfc8422) 기타 * [Transport Layer Security, Wikipedia](https://en.m.wikipedia.org/wiki/Transport_Layer_Security) * [The Heartbleed Bug](http://heartbleed.com/) * [Elie Bursztein. 2017. Understanding the prevalence of web traffic interception](https://blog.cloudflare.com/understanding-the-prevalence-of-web-traffic-interception/) * [Sealpath. Protecting the three states of data](http://sealpath.com/protecting-the-three-states-of-data/) * Jean-Philippe Aumasson 지음, 류광 옮김, 2018, Serious Cryptography 처음 배우는 암호화, 한빛미디어 --- ## [Software architecture: The important stuff](https://tech.buzzvil.com/blog/tech-blog-software-architecture-the-important-stuff) Date: 2019-01-22 | Author: Whale Lee | Category: Backend 마틴 파울러는 Software architecture 를 **“무엇이건 간에 중요한 것들(The important stuff whatever it is)”** 이라고 정의합니다. 조금은 재미있는 정의지만, 그 정의를 도출하기 위해 제시한 다른 정의를 들어보면 고개를 끄덕이게 합니다. * Software architecture 는 전문 개발자들이 같은 생각을 가지고 이해하는 시스템 디자인입니다. * Software architecture 는 이른 시기에 정해져야 하는 디자인 결정들입니다. 혹은 여러분이 “아, 처음부터 좀 더 잘 생각하고 할 껄”이라고 후회하는 바로 그 결정들입니다. * Software architecture 는 또한 바꾸기 어려운 결정들의 집합입니다. 결국 무엇을 중요하게 생각할 것인가, 그것이 Software Architecture 라는 의미입니다. ### Why is it important? 왜 중요한지 설득하지 못한다면 사실 중요하지 않은 것일지도 모르죠. 그래서 왜 Software Architecture 이 중요한지 짚어보고자 합니다. 쿠팡은 Microservice architecture 로 전환하는 여정을 글로 남겼는데요. 블로그 글의 제목을 [“행복을 찾기 위한 우리의 여정”](https://medium.com/coupang-tech/%ED%96%89%EB%B3%B5%EC%9D%84-%EC%B0%BE%EA%B8%B0-%EC%9C%84%ED%95%9C-%EC%9A%B0%EB%A6%AC%EC%9D%98-%EC%97%AC%EC%A0%95-94678fe9eb61) 이라고 지었습니다. (좋은 글이니 읽어보시길!) 다시 말해서, Software Architecture는 개발가자 더 좋은 제품을 만들 수 있는 길이기 때문에 중요하다고 말합니다. 그러나 좋은 Software Architecture를 만드는 일은 쉽지 않습니다. 블로그 글을 인용 해보겠습니다: **"여기 저렴한 제품과 비싼 제품이 있습니다. 비싼 제품은 software architecture 가 잘 고려되어 있고, 저렴한 제품은 시스템 디자인에 대한 고민 없이 구현되어 있습니다. 하지만 두 제품은 겉으로 보기에 차이가 없습니다. 소비자가 보기에 똑같이 보이고, 똑같은 기능이 있으며, 성능 또한 같습니다. 어떤 제품을 사야할까요?"** 소비자는 제품을 만든 개발자의 행복을 위해 더 비싼 제품을 선택하지는 않습니다. 개발자 역시 동료들에게 “내가 행복하려면 시간과 돈이 좀 더 들더라도 좋은 software architecture 를 구성해야 해.” 라고 주장하기엔 설득력이 부족하죠. **Software architecture 가 왜 중요한지 모두가 공감하려면 경제적인 입장에서 그 중요성을 설득해야 합니다.** “내부 품질을 좀 포기하더라도 이번 릴리즈에 더 많은 기능들이 들어가야 해.” 라는 의견에 “안돼 우리(개발자)는 더 전문적으로 구성해야 해.”라는 의견으로 대응하면 항상 질 수 밖에 없습니다. 장인 정신과 경제 논리 사이의 싸움에서는 경제 논리가 항상 이겨왔거든요.   \[caption id="" align="aligncenter" width="618"\]![](https://cdn-images-1.medium.com/max/1600/1*eZFQORi5zQsXL3k7eLgp7g.png) Cumulative functionality over Time\[/caption\] Software architecture 를 고려하지 않으면서 제품을 개발하면 초기에는 기능 추가 속도가 빠를 수 있지만, 시간이 흐름에 따라 제품의 기능 증가 속도는 점차 느려집니다. 이미 구현된 기능들과 코드가 새로운 기능을 추가하는데 걸림돌이 되기 때문입니다. 한편, 좋은 설계를 지속적으로 건강하게 유지하고, 주기적으로 리팩토링을 하고, 코드를 깨끗하게 유지한다면 시간이 흘러도 기능 추가가 느려지지 않을 수 있습니다. 오히려 기능을 추가하기 위해 수정해야 할 곳들이 명확하고 모듈화 또한 잘 되어있기 때문에 시간이 갈 수록 기능 추가가 더욱 빠르게 진행될 수 있습니다. 새로운 개발자가 참여하는 시점에도 시스템을 더욱 빠르게 이해하고, 더 빠르고 안전하게 기능을 추가할 수 있게 됩니다. 결국 장기적으로 더 많은 기능을 생산하고 빠르게 고객에게 전달하기 위해서 개발팀은 좋은 디자인과 설계에 대해 깊게 고민해야 합니다. ### What is the best software architecture? 옳은 software architecture 는 없습니다. 상황에 따라 해답은 다를 수 있습니다. Microservice architecture 가 좋다고 해서 모든 것에 대한 답이 microservice architecture 인 것은 아니고, 마찬가지로 어떤 시스템이 monolithic architecture 로 구현되어 있다고 해서 뒤쳐져 있는 것도 아닙니다. 모든 선택에는 Tradeoff 가 있기 마련이니까요. 유선 통신 시스템을 구성한다고 생각해 볼까요? 우리 나라처럼 인터넷이 잘 구성된 상황에서 Skype 로 할 수 있는 통화는 무료이고, 품질도 좋고, 영상 통화까지 됩니다. “Skype 만세! 인터넷을 통한 통신이 항상 옳습니다!” 라고 외치려던 시점에 정전이 되었습니다. 방금 외친 외침은 멀리 가봐야 옆집 정도 닿겠죠. 한편 기존 유선 전화 시스템은 느리고 화상 통화도 안되지만, 전화선 자체에 전원이 공급되고 있기 때문에 정전 시에도 통화가 가능합니다. 전쟁 상황이나 기타 재난 등에도 반드시 통신이 가능해야 하는 곳은 유선 전화 시스템이 꼭 필요할 것 같습니다. 은행 시스템도 적절한 예시가 될 수 있습니다. 비밀번호 입력, 전화 인증, OTP 확인하는 등 은행 업무는 왜이리도 복잡할까요? 그냥 비밀번호 기억해주고 로그인 유지해주면 참 편할텐데 말이죠. 안전하기 위해서겠죠. 여러분의 자산은 소중하니까요. 사용성(Usability)과 안전성(Security)은 종종 둘 사이를 조절해야 하는 Tradeoff 입니다. 만들려는 제품과 시스템, 환경, 시기와 조건 등에 따라서 적절한 architecture 는 달라집니다. 좋은 architecture 를 선택할때 개발자는 선택한 것의 대척점에 있는 무언가를 포기 해야합니다. 그렇기에 software architecture 는 기술적인 범주 안에서만 고려되면 안되고, 구현하고자 하는 비지니스를 매우 잘 이해하고 고려해서 적용해야 합니다. ### What are you going to do? 이미 구성된 software architecture 를 변경하는 것은 굉장히 어렵습니다. 이미 구성되어 있는 것들을 상세하게 알고 있어야 하고, 비지니스의 요구 사항을 수용해야 하며, 이미 존재하는 기능이 변경 도중 문제 없이 동작해야 합니다. 또한 기존 시스템에 기여한 개발자들과 변경 사항에 대한 공감대를 이뤄야 하며, 겉으로 보기에 당장 변화가 없는 것에 대한 비용에 대해 많은 사람들을 설득해야 합니다. **최근 Buzzvil 에서는 Architecture Task Force 팀을 구성하였습니다.** 이를 통해 전체적인 설계를 정비하고 모든 개발팀이 구조적으로 같은 이해를 할 수 있도록 분석, 조사, 계획 수립, 실행에 옮길 예정입니다. 지속적인 공유를 통해 전사적인 공감대를 유지하고 체계적인 문서화와 가이드라인을 통해 모든 팀원이 함께 실행하며 성장할 수 있는 기반을 준비하게 될 것입니다. 궁극적으로 전사 프로젝트와 모든 팀이 더욱 빨리 움직일 수 있는 software architecture 를 구성하고, 이를 통해 더 많은 기능을 더 빠르게 전달할 수 있게 할 것입니다. 아직 해야할 일들이 많이 남아있지만 제대로 계획하고 빠르게 움직인다면 충분히 좋은 결과를 만들 수 있을 것 같습니다. 당장은 눈에 보이는 변화가 없을지라도, **좋은 디자인에 대한 고민과 실행이 우리가 궁극적으로 바라는 비전과 목표에 한 걸음 더 빠르게 다가가는 올바른 길**이라고 믿습니다. --- ## [PhantomJS를 Puppeteer 전환하며](https://tech.buzzvil.com/blog/tech-blog-phantomjs-pupppeteer-migration) Date: 2019-01-08 | Author: Liam Hwang | Category: Frontend 버즈빌에서는 모바일 잠금화면에 내보내기 위한 광고 및 컨텐츠 이미지를 생성하기 위한 PhantomJS 렌더링 서버를 다수 운영하고 있습니다. 일반적으로 PhantomJS는 웹페이지 캡쳐에 많이 쓰이지만, 기본적으로 headless하게 웹페이지를 렌더링하고 캡쳐할 수 있다는 특성 때문에 동적인 이미지 생성에도 많이 활용됩니다. 버즈빌의 렌더링 서버는 200개 이상의 컨텐츠 프로바이더로부터 실시간으로 잠금화면 컨텐츠 이미지를 생성하고 있어 분당 수백 건의 이미지를 안정적으로 생성하는 것이 가능해야 합니다. ![Buzzscreen](https://tech.buzzvil.com/blog/tech-blog-phantomjs-pupppeteer-migration/buzzvil-games-1-1.jpg) 렌더링 서버의 스케일링 이슈를 해결하기 위해 버즈빌에서는 여러 대의 렌더링 서버를 둬서 횡적으로 확장을 함과 동시에, 개별 서버 내에서도 리소스 사용률을 높이기 위해 [Ghost Town](https://github.com/Buzzvil/ghost-town)이라는 라이브러리를 작성해 PhantomJS 프로세스 풀을 구성하여 사용하고 있었습니다([Scaling PhantomJS With Ghost Town](https://www.buzzvil.com/2014/05/29/scaling-phantomjs-ghost-town/) ). 한편, 시간이 지나면서 잠금화면에서 렌더링하는 이미지 템플릿의 종류가 다양해지고, emoji 및 여러 특수문자를 표현하기 위해 렌더링 서버에 여러 폰트(대표적으로 Noto Sans CJK)를 설치해야 하는 요구사항이 추가됐는데, PhantomJS에서 폰트 렌더링이 일관적이지 않은 문제가 발생했습니다. 이 문제의 정확한 원인은 결국 찾지 못했지만 PhantomJS의 이슈였거나 시스템 상에 폰트가 시간이 지나면서 추가 설치됨에 따라 font cache가 서버마다 일관되지 않은 상태가 되었기 때문인 것으로 짐작하고 있습니다. 다른 워크로드와 마찬가지로 렌더링 서버도 최초에는 packer를 이용해 일관되게 이미지를 빌드하고 업데이트하려고 했지만, 자주 기능이 추가되거나 배포되는 서비스가 아니기에 서버를 오래 띄워놓고 수동으로 유지보수를 한 케이스들이 누적되어 더 이상 packer를 이용해 시스템이나 폰트를 최신 상태로 유지하는 것이 어려운 상태였습니다. 모든 눈꽃송이가 자세히 보면 조금씩 다르게 생겼다는 것에서 비롯된 [snowflake](https://martinfowler.com/bliki/SnowflakeServer.html), 즉 배포된 서버들이 시간이 지남에 따라 조금씩 다른 상태가 된 것입니다. 평소에는 문제가 없어 보이지만, 추가적인 확장성이 필요해 scale out을 하거나 새로운 템플릿을 개발해 배포를 하면 문제가 발생하는 상황이었습니다. 사실 더 큰 문제는 PhantomJS 프로젝트가 더 이상 관리되지 않는다는 점이었습니다. 2017년 Google Chrome 59버전부터 Headless Chrome이 내장되기 시작하였고, 곧바로 Node API인 puppeteer가 릴리즈 되어, 현시점에서 가장 많이 쓰이는 렌더링 엔진을 손쉽게 headless로 사용할 수 있는 환경이 되었습니다. 때문에 PhantomJS 관리자가 사실상의 [중단을 선언](https://groups.google.com/forum/#!topic/phantomjs/9aI5d-LDuNE)하였고, 2018년에는 최초 개발자에 의해 프로젝트가 [아카이브 되었습니다](https://github.com/ariya/phantomjs/issues/15344). 프로젝트가 업데이트되지 않는 것은 템플릿에 최신 CSS 스펙을 사용하지 못한다는 것을 의미하고, 버그 수정도 되지 않기에 어플리케이션의 유지보수가 굉장히 어려워짐을 의미합니다. 현재까지의 문제점을 정리하면 아래와 같습니다. 1. 자주 배포되지 않는 서비스 특성으로 인한 서버들이 snowflake화 되는 현상(특히 폰트) 2. PhantomJS의 개발 중단으로 인해 버그 픽스 및 최신 CSS 속성 사용이 어렵게 되고, 향후 유지보수나 새로운 템플릿 개발이 어려워짐 해결방안은 명확했습니다. 첫번째 문제를 해결하기 위해서는 어플리케이션과 폰트가 설치된 시스템을 통째로 컨테이너로 만들고, CI/CD 파이프라인을 통해 지속적으로 빌드하여 snowflake화 되지 않도록 하면 됩니다. 사실 최초에 packer를 이용해 AMI 이미지를 생성하도록 구성이 되어있었기에, 매 배포마다 AMI를 새로 생성하고 지속적으로 렌더링 서버를 배포하는 환경이기만 했으면 snowflake를 방지할 수 있었을 것입니다. 하지만 자주 기능이 추가되거나 배포되는 서비스가 아닌데다, AMI를 빌드하는 과정이 CI/CD에 통합돼 있지 않고 어플리케이션만 지속적으로 배포하는 환경이었기에 편의상 서버를 종료하지 않고 장기간 관리를 해 오게 되었고, packer로 새로운 AMI 이미지를 빌드하는 것이 어려워 졌습니다. 때문에 AMI 빌드를 통한 배포 대신, 이미 운영 중인 kubernetes 클러스터에 도커 컨테이너를 빌드해 immutable한 형상으로 배포하기로 결정하였습니다. 두번째 문제의 간단한 해결책은 PhantomJS를 puppeteer로 변경하는 것입니다. 이 부분은 생각보다 간단했습니다. 의도했는지는 알 수 없으나 puppeteer의 api는 PhantomJS와 꽤나 비슷합니다. drop-in replacement까진 아니지만, PhantomJS api 호출하는 부분만 살짝 바꿔주는 정도로 교체가 가능하였습니다. 물론 교체만 하였다고 해서 기존에 개발된 템플릿이 의도된 대로 출력되는 것을 보장하지는 않기에, 렌더링 서버가 렌더링하는 수많은 템플릿들을 PhantomJS와 puppeteer로 각각 출력하여 일일히 비교하는 작업이 필요했습니다. 어떤 템플릿이 어떤 인자를 필요로하며 의도된 출력 결과가 무엇인지에 대한 정의가 남아있지 않았기에 템플릿마다 샘플 케이스들을 생성하는 작업이 필요했습니다. 아직까지는 수동으로 결과를 비교해야하는 문제점이 있지만 적어도 직접 확인할 수 있는 것은 큰 도움이 되었습니다. 향후에는 자동화된 테스트 케이스를 구성하여 기능 개발이 좀 더 용이하도록 보완할 계획입니다. 결과는 만족스러웠습니다. 많은 경우 기존과 출력 결과가 달랐지만, 최신의 크롬 웹킷이 사용되면서 오히려 템플릿을 개발할 때 의도했던대로 CSS를 더 정확하게 렌더링하게 된 것이었습니다. ```dockerfile FROM node:10-slim RUN apt-get update && \ apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \ libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \ libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \ ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget unzip && \ wget https://github.com/Yelp/dumb-init/releases/download/v1.2.1/dumb-init_1.2.1_amd64.deb && \ dpkg -i dumb-init_*.deb && rm -f dumb-init_*.deb && \ apt-get clean && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* RUN yarn global add puppeteer@1.10.0 && yarn cache clean ENV NODE_PATH="/usr/local/share/.config/yarn/global/node_modules:${NODE_PATH}" RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser # Set language to UTF8 ENV LANG="C.UTF-8" RUN wget -P ~/fonttmp \ https://noto-website-2.storage.googleapis.com/pkgs/NotoSans-unhinted.zip \ https://noto-website-2.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip \ https://noto-website-2.storage.googleapis.com/pkgs/NotoSansCJKkr-hinted.zip \ https://noto-website-2.storage.googleapis.com/pkgs/NotoSansCJKtc-hinted.zip \ https://noto-website-2.storage.googleapis.com/pkgs/NotoSansCJKsc-hinted.zip \ https://noto-website-2.storage.googleapis.com/pkgs/NotoColorEmoji-unhinted.zip \ && cd ~/fonttmp \ && unzip -o '*.zip' \ && mv *.*tf /usr/share/fonts \ && cd ~/ \ && rm -rf ~/fonttmp WORKDIR /app # Add user so we don't need --no-sandbox. RUN mkdir /screenshots && \ mkdir -p /home/pptruser/Downloads && \ mkdir -p /app/node_modules && \ chown -R pptruser:pptruser /home/pptruser && \ chown -R pptruser:pptruser /usr/local/share/.config/yarn/global/node_modules && \ chown -R pptruser:pptruser /screenshots && \ chown -R pptruser:pptruser /usr/share/fonts && \ chown -R pptruser:pptruser /app # Run everything after as non-privileged user. USER pptruser RUN fc-cache -f -v COPY --chown=pptruser:pptruser package*.json /app/ RUN npm install && \ npm cache clean --force COPY --chown=pptruser:pptruser . /app/ ENTRYPOINT ["dumb-init", "--"] CMD ["npm", "start"] ``` puppeteer를 사용하면서 약간의 권한 문제가 있어서 결과적으로 위와 같은 Dockerfile을 작성하게 되었는데, puppeteer 도커 이미지 작성에 관한 최신 정보는 [여기](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md)서 확인할 수 있습니다. 컨테이너 오케스트레이션(K8s)을 사용하면 process 기반의 스케일링은 컨테이너를 여러대 띄워 로드밸런싱을 손쉽게 할 수 있지만, 개별 컨테이너의 throughput을 향상시키기 위해 기존에 Ghost town을 작성해 PhantomJS 프로세스 풀을 만든 것처럼 크롬 프로세스 풀을 구성하기로 하였습니다. 프로세스 풀 구성에는 [generic-pool](https://github.com/coopernurse/node-pool) 라이브러리를 사용하였으며 아래처럼 구성하였습니다. ```javascript const puppeteer = require("puppeteer"); const genericPool = require("generic-pool"); const puppeteerArgs = [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", ]; const createPuppeteerPool = ({ max = 5, min = 2, maxUses = 50, initialUseCountRand = 5, testOnBorrow = true, validator = () => Promise.resolve(true), idleTimeoutMillis = 30000, ...otherConfig } = {}) => { const factory = { create: async () => { const browser = await puppeteer.launch({ headless: true, args: puppeteerArgs, }); browser.useCount = parseInt(Math.random() * initialUseCountRand); return browser; }, destroy: (browser) => { browser.close(); }, validate: (browser) => { return validator(browser).then((valid) => Promise.resolve(valid && (maxUses <= 0 || browser.useCount < maxUses)) ); }, }; const pool = genericPool.createPool(factory, { max, min, testOnBorrow, idleTimeoutMillis, ...otherConfig, }); const genericAcquire = pool.acquire.bind(pool); pool.acquire = () => genericAcquire().then((browser) => { browser.useCount += 1; return browser; }); pool.use = (fn) => { let resource; return pool .acquire() .then((r) => { resource = r; return resource; }) .then(fn) .then( (result) => { pool.release(resource); return result; }, (err) => { pool.release(resource); throw err; } ); }; return pool; }; module.exports = createPuppeteerPool; ``` ### Caveats PhantomJS에서 puppeteer로 전환함에 있어서 몇가지 주의해야 할 점이 있었는데요. 첫째는 기존에 사용하던 템플릿의 html에 이미지 소스를 `file://` url 프로토콜을 이용해 로드하는 경우가 있었는데, PhantomJS에서는 정상적으로 로드가 되지만 Headless Chrome에서는 보안 정책으로 인해 로컬 파일을 로드할 수 없었습니다([관련 이슈](https://github.com/GoogleChrome/puppeteer/issues/1643)). 때문에 로컬 이미지가 필요한 템플릿은 Express 서버에서 static file serving을 하도록 하고 `http://` 프로토콜로 변경하였습니다. 다음으로 발생한 문제는 PhantomJS을 이용한 기존 구현에서는 jade template을 compile한 후 page 객체의 `setContent` 메소드를 이용해 html을 로드하였는데, puppeteer에서는 `page#setContent` API 호출 시 외부 이미지가 로드될 때까지 기다리지 않는다는 점입니다. [puppeteer 에 올라온 관련 이슈](https://github.com/GoogleChrome/puppeteer/issues/728)에서는 `setContent` 대신 아래와 같이 html content를 data URI로 표현하고 `page#goto`의 인자로 넘기면서 `waitUntil` 옵션을 주는 방식을 해결방법으로 권하고 있습니다. ```javascript await page.goto(`data:text/html,${html}`, { waitUntil: "networkidle0" }); ``` 이 때 주의해야 할 점은 `waitUntil`의 옵션으로 `networkidle0`이나 `networkidle2` 등을 사용하면 외부 이미지가 충분히 로드될 때 까지 기다리는 것은 맞지만, 500ms 이내에 추가적인 네트워크 커넥션이 발생하지 않을 때까지 기다리는 옵션이기 때문에 외부 이미지가 로드되더라도 추가적으로 500ms를 기다리게 됩니다. 때문에 SPA 웹페이지를 캡쳐하는 경우가 아니라 정적인 html을 로드하는 경우라면 `load` 이벤트로 지정하면 됩니다. 이외에도 향후에 프로젝트의 유지관리나 운영 중인 서비스의 모니터링을 위해 Metrics API 엔드포인트를 만들어 prometheus에서 메트릭을 수집할 수 있도록 하고 grafana 대시보드를 구성하였습니다. 이 대시보드는 어떤 템플릿이 실제로 사용되고 있는지, 템플릿 렌더링에 시간이 얼마나 소요되는지 등을 모니터링할 수 있도록 구성하여 사용되지 않고 있는 템플릿을 판단하거나 서비스 지표를 모니터링 하는 데 이용하고 있습니다. ### 마치며 최근에 들어서는 PhantomJS를 사용하던 많은 곳에서 puppeteer로의 전환을 해오고 있어 본 포스팅에서 다루고 있는 내용이 크게 새로운 내용은 아닐 수 있습니다. 하지만 버즈빌에서는 렌더링 서버가 과거에 이미 PhantomJS를 사용하는 것을 전제로 상당한 최적화가 진행되어 왔고, 꽤나 높은 동시 처리량이 요구되는 상황에서 puppeteer로 교체를 해버리기에는 여러 불확실한 요소들이 존재하는 상황이었습니다. 버즈빌의 핵심 비즈니스 중 하나인 잠금화면에 사용되는 이미지를 렌더링하는 서비스가 레거시(개발이 중단된 PhantomJS)에 의존하는 코드베이스 때문에 변경이 어려워지는 것은 향후 꽤나 큰 기술부채로 작용할 것이라 판단하였습니다. 이번 마이그레이션을 진행하면서는 이 부분을 염두에 두고 컨테이너를 사용해 CI/CD 파이프라인을 구축해 지속적으로 컨테이너 기반의 이미지를 생성하도록 변경하였고, 그 결과는 꽤나 만족스러웠습니다. 마이그레이션 이후 그간 밀려 있던 신규 템플릿 개발이나 신규 컨텐츠 프로바이더를 추가하는 과정이 수월해졌기 때문입니다. 빠르게 변화하는 비즈니스 요구사항에 대응하다보면 기술부채는 필연적으로 쌓일 수밖에 없습니다. 개발자에게는 당연히 눈에 보이는 모든 기술부채들을 청산하고 싶은 욕구가 있지만 늘 빚 갚는데 시간을 쓰고 있을 수만은 없는 노릇입니다. 리소스에는 한계가 있으니까요. 어떤 기술부채를 지금 당장 해결해야하는지 의사결정을 하는데 있어 고민이 된다면 일단 “측정”을 해보는 것을 권장합니다. 수치화된 지표가 있다면 당장 의사결정권자나 팀을 설득하는 데 사용할 수도 있지만, 서비스의 핵심 지표들을 하나 둘씩 모니터링 해나가다 보면 서비스에 대한 가시성이 높아지고 미래에 정말로 병목이 되는 지점을 찾아내기 쉬워질 것입니다. ### **참고 자료** - [https://docs.browserless.io/blog/2018/06/04/puppeteer-best-practices.html](https://docs.browserless.io/blog/2018/06/04/puppeteer-best-practices.html) - [https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md) - Icons made by [Freepik](https://www.freepik.com/) from [Flaticon](https://www.flaticon.com/) is licensed by [Creative Commons](http://creativecommons.org/licenses/by/3.0/) BY 3.0 --- ## [Keep Principles in Mind](https://tech.buzzvil.com/blog/tech-blog-keep-principles-in-mind) Date: 2018-12-04 | Author: Whale Lee | Category: Backend 원칙(Principle)은 중요합니다. "난 원칙대로 살지 않겠어!" 라고 외치고 싶더라도, 원칙이 있고 원칙을 충분히 이해하고 있지 않다면 그저 사춘기 소년/소녀의 이유 없는 반항 정도로 밖에 들리지 않을테니까요. 사실 대부분의 이런 경우 원칙 보다는 "규칙(Rule)대로 살지 않겠다"에 가깝지만, 여기에서는 그냥 넘어가도록 하죠. 소프트웨어 개발에도 다양한 원칙들이 존재합니다. 학부 수업에서 잠깐 들었거나 이런 저런 글들을 읽다가 접해 봤을 이런 원칙들은 실제 서비스를 만들면서 바쁘게 기능을 추가하고 버그를 수정 하느라 어느새 기억 속에서 잊혀지곤 하죠. 정신없이 기능을 구현하다가 문득 코드를 돌아봤을 때 '이게 왜 여기에 있지' 라는 의문이 든다면 한 번쯤 원칙을 되새겨 보라는 신호가 아닐까요? 이 글에서는 Clean Architecture 와 Clean Code 등의 저자로 유명한 Uncle Bob(Robert C. Martin)이 얘기하는 S.O.L.I.D Principles 에 대해 얘기해 보려고 합니다. SOLID 원칙은 밥 아저씨가 2000년도 자신의 논문 Design Principle and Design Patterns 에서 OOD(Object-Oriented Design)를 위해서 제안한 5가지 원칙의 앞 글자만 떼서 붙여졌습니다. Object-Oriented Design 을 대상으로 제안된 원칙이지만 Agile 개발 등의 개발 방법론 핵심 철학에도 적용될 수 있는 개념들 입니다. ### S.O.L.I.D Principles **Single Responsibility Principle** Class 는 오직 한 가지의 책임이 주어져야 하고, 오직 한 가지 이유에서만 변경되어야 합니다. 보고서를 편집하고 출력하는 모듈에 대해서 생각해 볼까요. 해당 모듈은 두 가지의 이유로 변경될 가능성이 있습니다. 보고서의 내용이 바뀌었을 때도 변경되어야 하고, 보고서의 형식이 바뀌었을 때도 변경되어야 합니다. 편집 과정 때문에 모듈을 변경하다 보면 해당 변경 사항이 출력 부분에도 영향을 미칠 가능성이 상당히 높습니다. 이 경우 내용을 편집하는 모듈(i.e 내용을 담당하는 모듈)과 출력하는 모듈(i.e 형식을 담당하는 모듈) 두 가지로 나뉘어야 합니다. "할 수 있다고 해서 해야 한다는 뜻은 아닙니다." **Open / Closed Principle** Class, Module, Function 등의 소프트웨어 구성 요소는 확장(extension)에 대해 열려 있어야 하며, 변경(modification)에 대해 닫혀 있어야 합니다. 어떤 모듈이 Data Structure 에 필드를 추가하거나 함수를 추가하는 등 확장이 가능하다면 그 모듈은 확장에 대해 열려 있다고 표현합니다. 반면에 어떤 모듈이 수정 없이 다른 모듈에 의해 사용될 수 있다면 그 모듈은 닫혀 있다고 표현합니다. ```java public class CreditCard {     private int cardType;       public int getCardType() { return cardType; }       public void setCardType(int cardType) { this.cardType = cardType; }          public double getDiscount(double monthlyCost){          if (cardType == 1) {              return monthlyCost * 0.02;          } else {              return monthlyCost * 0.01;          }     } } ``` 위 CreditCard class 에 새로운 카드 타입을 추가하려고 하면 getDiscount 함수를 변경할 수 밖에 없습니다. 이 경우 Open/Closed Principle 을 위반된다고 볼 수 있습니다. "코트를 입기 위해서 개복 수술을 할 필요는 없으니까요." **Liskov Substitution Principle** 프로그램 상의 Object 들은 프로그램의 정확성을 해치지 않으면서 하위 타입의 Instance 로 변경 가능해야 합니다. 하위 타입 함수 인자의 반공변성(Contravariance), 하위 타입 함수 반환 타입의 공변성(Covariance), 상위 타입의 예외를 상속하지 않는 추가적인 예외 발생 금지 등의 요구 사항이 있습니다. OOP 에서 상속 개념을 배울 때 이해를 돕기 위해 주어진 몇 가지 예시들이 있었을텐데, 우습게도 우리가 생각하기에 타당한 상속에 관한 예시들 중 의외로 원칙을 위배하는 경우가 많습니다. Liskov Substitution Principle 을 위반하는 대표적인 예시는 정사각형과 직사각형입니다. 정사각형은 직사각형의 일종이니 Square가 Rectangle을 상속받는 것이 충분이 타당한 것으로 보입니다. 정말 그럴까요? Rectangle 의 넓이를 구하는 함수의 테스트를 구성해 봅시다. ```java Rectangle rect = new Rectangle(); rect.setWidth(10); rect.setHeight(20); assertEquals(200, rect.getArea()); ``` 여기에 `new Rectangle()` 대신에 `new Square()`가 rect 에 할당되면 어떻게 될까요? 넓이는 400 을 반환하기 때문에 테스트는 실패하겠죠. 정사각형이 직사각형을 상속 받으면 Liskov Subsitution Principle 을 위반한다고 볼 수 있습니다. 상속은 문제를 해결하는데 있어서 상당히 유혹적인 방법입니다. 하지만 상당히 많은 경우에 상속을 오용할 가능성이 높습니다. "오리처럼 생기고 오리처럼 꽥꽥 거리더라도, 배터리가 필요하다면 오리가 아닙니다." **Interface Segregation Principle** 많은 것을 아우르고 일반적으로 사용 가능한 하나의 interface 보다 특정 클라이언트를 위한 여러 개의 interface 가 낫습니다. Xerox는 Stapling(프린터기가!?), Fax 등의 다양한 기능이 포함된 신규 프린터 소프트웨어를 개발 도중, 더이상 개발이 불가능할 정도로 프로그램이 번잡 해졌다는 것을 인정하고 밥 아저씨에게 도움을 요청합니다. 문제는 Job Class 하나가 모든 기능을 다 구현하고 있다는데 있었습니다. 이 비대한 Class 는 Client 입장에서 사용되지도 않을 모든 함수를 알 수 있게 구성 되어 있었죠. 이 문제에 대해 밥 아저씨는 Interface Segregation Principle 을 적용하여 각 Client 입장에서 사용해야 하는 함수 만을 가지고 있는 각 interface 들을 따로 만들었습니다. 그리고는 다음에 나올 Principle 인 Dependency Inversion Principle 을 통해서 해당 기능을 구현하게 함으로써 문제를 해결했습니다. **Dependency Inversion Principle** "추상화에 의존해야지, 구체화에 의존하면 안됩니다." 상위 계층의 모듈은 하위 계층의 구현이 아니라 추상화에 의존해야 합니다. 상위 계층이 하위 계층의 구현에 의존하던 전통적인 의존 관계를 역전 시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있습니다. 예를 들어 Dependency Injection 은 이 원칙을 따르는 방법 중 하나 입니다. ### Conclusion 세상에 나쁜 프로그램은 있습니다. 당장 눈에 보이는 기능이 똑같다고 같은 프로그램인 것은 아닙니다. 생각보다 많은 코드들이 '그 곳에 넣을 수 있기 때문에', '그 곳에 넣어도 돌아가기 때문에' 깊은 고민 없이 그 곳에 정착합니다. 당장 좀 더 빠르게 기능을 추가해서 주변 사람들의 박수를 받을 수도 있습니다. 허나 이것들이 쌓이면 더이상 손댈 수 없는 코드가 되고, 문제를 느끼고는 Refactoring을 하자고 다짐하고, 모두 엎은 다음 또 다시 같은 코드를 만들게 되겠죠. 쉬운 코드가 가장 만들기 어려운 코드이고, 그런 좋은 코드는 좋은 원칙으로 부터 나옵니다. 변화에 적응할 수 있는 프로그램, 의도가 쉽게 읽히는 프로그램, 문제 발생 가능성이 적은 프로그램, 쉽게 확장할 수 있는 프로그램 등 좋은 프로그램을 만드는 것은 우리가 실제로 목표하는 것을 달성하기 위해서 정말 중요합니다. 이는 그저 경험이나, Tweak 만으로 이루어지지 않습니다. 다양한 신규 기술들과 Framework 들을 두루 섭렵하면서 활동 반경을 넓히고 경험을 쌓았다면, 가끔은 잠시 서서 원칙에 대해 되돌아 보는 것은 어떨까요?   *버즈빌에서 활기찬 개발자를 채용 중입니다. (전문연구요원 포함) --- ## [Buzzvil's Success Story with Google Vision API](https://tech.buzzvil.com/blog/buzzvil-news-buzzvils-success-story-with-google-vision-api-2) Date: 2018-10-26 | Category: Culture At the Google Cloud for Startups event held in Seoul on Oct. 25, Buzzvil's Yohan was able to share the company's success story with Google Vision API.![](https://tech.buzzvil.com/blog/buzzvil-news-buzzvils-success-story-with-google-vision-api/buzzvil-google-1.png)Buzzvil was also introduced as one of Google Cloud's client at this years Google Cloud Summit held in Seoul on Oct. 25. It is the city's first time to hold the event! Mobile lockscreen media platform provider Buzzvil has invited many partners into its platform from home and abroad to vigorously expand ad inventories for 17 million users across 30 nations. Inventories are filled with ad garnered through ad networks or direct sales. The ads go through our server before reaching our users' mobile lockscreen. We use Google's Cloud Vision to analyze and categorize images to filter out harmful contents. **Why Google Vision API?** With Google Vision's simple technology, we are able to display good content to users at an affordable price in a stable manner. A user looks at his or her phone every 11 minutes. Getting user's trust and maintaining it are essential if we want to bring value to our partners and advertisers. Google's Vision helps us to do that. Yohan's presentation slide is available below. Take a look. ![](https://tech.buzzvil.com/blog/buzzvil-news-buzzvils-success-story-with-google-vision-api/%ec%9a%94%ed%95%9c%eb%8b%98_speech.jpeg) *Yohan rocks the stage!* ![](https://tech.buzzvil.com/blog/buzzvil-news-buzzvils-success-story-with-google-vision-api/buzzvil-google-3.jpeg) *Buzzvil's developers with Google team.* At Buzzvil, our efforts to operate our services in a timely and stable manner continues. If you want to check out what our developers have to say, stay tuned to our [Buzzvil Tech Blog](https://www.buzzvil.com/category/technology-engineering/). --- ## [Buzzvil's Success Story with Google Vision API](https://tech.buzzvil.com/blog/buzzvil-news-buzzvils-success-story-with-google-vision-api) Date: 2018-10-26 | Category: Culture 버즈빌은 지난 10월 25일 Google Cloud for Startups에 참여하여 버즈빌의 Google Vision API를 사용한 Success Story를 공유했습니다. 이날 버즈빌을 대표하여 발표한 인물은 바로 광고 할당을 담당하고 있는 Yohan 서버 개발자였습니다.![](https://tech.buzzvil.com/blog/buzzvil-news-buzzvils-success-story-with-google-vision-api/buzzvil-google-1.png)지난 10월 25일 서울 코엑스에서 개최된 Google Cloud Summit에서 고객사로 버즈빌이 소개되기도 했습니다! 버즈빌은 현재 국내외 유수의 파트너사와 함께하여 광고 인벤토리를 공격적으로 넓혀가고 있으며, 30개국에서 1,700만 유저를 대상으로 서비스 중입니다. 버즈빌은 현재 애드네트워크와 직영업을 통해 잠금화면 지면에 광고를 체우고 있는데요. 이와 같은 광고 물량은 버즈빌의 서버를 걸쳐 유저들의 모바일 잠금화면에 노출되고 있습니다. 이 과정에서 버즈빌은 Google의 Cloud Vision를 활용하여 광고 이미지의 카테고리를 구별하여 인사이트를 얻습니다. 특히 Cloud Vision이 제공하는 세이프서치를 통해 광고 이미지의 선정성을 확인하고 폭력적이거나 유해한 콘텐츠의 노출을 방지합니다. **Why Google Vision API?** 11분에 한 번 꼴로 보는 모바일 첫화면인 만큼 유저의 신뢰를 얻고 유지해야 버즈빌은 파트너사와 광고주에게 큰 가치를 가져다 줄 수 있습니다. Google Vision은 간단한 기술과 합리적인 비용으로 안정적으로 유저에게 좋은 콘텐츠의 노출을 가능하게 합니다. 때문에 버즈빌의 가치를 높이기 위해 아주 유용한 기술로 활용하고 있습니다. Yohan의 발표 내용을 아래 슬라이드를 통해 확인 해주세요. ![](https://tech.buzzvil.com/blog/buzzvil-news-buzzvils-success-story-with-google-vision-api/%ec%9a%94%ed%95%9c%eb%8b%98_speech.jpeg) *열심히 설명하고 있는 Yohan!* ![](https://tech.buzzvil.com/blog/buzzvil-news-buzzvils-success-story-with-google-vision-api/buzzvil-google-3.jpeg) *구글 팀, 뜨거운 관심과 응원 감사합니다!* 버즈빌에서는 서비스를 더 빠르고 안정적으로 운영하기 위한 다양한 고민과 시도를 하고 있는데요. 버즈빌의 개발자들의 고민이나 다양한 개발 이야기들이 궁금하신 분은 [**Buzzvil Tech Blog**](https://tech.buzzvil.com/)를 참고해 주시면 더 다양하고 재미있는 버즈빌의 개발 이야기를 만나보실 수 있습니다. --- ## [How we pipe data](https://tech.buzzvil.com/blog/tech-blog-how-we-pipe-data) Date: 2018-07-31 | Category: Data & ML 버즈빌에서는 미국과 일본을 비롯한 전 세계 30개국에서 1,700만 이상의 유저의 행동에 대한 데이터를 수집하고 있습니다. 이 데이터에는 유저들이 잠금화면에서 어떤 Action을 수행하는지부터 잠금화면에 어떤 광고가 노출되고 유저들이 어떤 광고를 클릭 하는 지 등의 정보들이 포함되는데요. 이러한 데이터는 여러 종류의 다른 소스로부터 오고 각기 다른 종류의 DB (MySQL, DynamoDB, Redis, S3 등등) 에 저장됩니다. 하지만 데이터를 분석하고 활용하기 위해서는 이렇게 흩어져서 저장된 데이터들을 한 곳으로 모으는게 필수적입니다. 그래서 저희 팀에서는 이렇게 다양한 소스로 부터 발생해서 다양한 DB에 저장된 데이터를 어떤 과정을 통해 한 곳으로 모을 것인가에 대해서 고민하게 되었습니다. 그리고 고민 끝에 각각의 DB에 저장된 데이터를 하나의 큰 데이터 스토리지에 모을 수 있는 ‘데이터 파이프라인’을 구축하는 계획을 세우게 되었습니다. 하지만 다양한 소스로부터 수집된 수많은 데이터들을 잘 유지해가며 하나의 큰 DB에 모을 수 있는 데이터 파이프라인을 구축하는 것이 쉽지 않았는데요. 이 포스팅을 통해서 버즈빌에서는 어떻게 각각의 데이터들을 수집하고 저장하는지 또 이런 데이터들을 통합하기 위한 파이프라인을 어떻게 구축했는지 공유하고자 합니다. 본격적인 이야기에 앞서 현재 버즈빌에서 모든 데이터가 모이는 데이터 스토리지로 사용 중인 RedShift에 대해 이야기하고 싶습니다. 개인적으로는 정말 쓰면 쓸수록 감탄이 나오는 데이터 스토리지라고 생각합니다. Redshift는 AWS에서 관리하는 SQL기반의 열기반 스토리지(SQL based columnar data warehouse)이며 복잡하고 대규모의 데이터 분석에 적합합니다. 고객들로부터 생성된 수많은 종류의 데이터를 기반으로 다양한 인사이트를 얻고자 하는 많은 기업들(Yelp, Coursera, Pinterest 등)이 사용하고 있는 솔루션 이기도 합니다. 버즈빌에서는 여러가지 특징을 고려하여 Redshift를 도입하게 되었는데요. 그 이유는 아래와 같습니다. * [**Performance Performance Performance**](https://blog.panoply.io/a-full-comparison-of-redshift-and-bigquery). -------------------------------------------------------------------------------------------------------------- 1.   Column 기반 스토리지 -> 필요한 Column에만 접근한다. 2.   Join이나 aggregation이 많은 복잡한 쿼리도 쉽게 계산할 수 있다. 3.   분산 저장 방식 (Distributed Storage) 4.   Date Ingestion이 빠르다. (Ingest first, index and clean later)   * **Horizontal Scalability** -------------------------- 1. sharding이나 clustering에 추가적인 complexity가 필요하지 않다. 2. 데이터가 원래 노드에 저장되기 때문에 horizontal scaling을 위해서는 그냥 추가적인 노드만 붙이면 된다. 3. 다른 AWS서비스들과 쉽게 연동이 가능하다. (장점 이자 단점) [![](https://tech.buzzvil.com/blog/tech-blog-how-we-pipe-data/2.png)](http://blog.carlesmateo.com/2014/10/13/performance-of-several-languages/) 하지만 몇 개의 아쉬운 점들도 있습니다. : - 다른 RDBMS와 달리 Mutilple indice를 지원하지 않는다. - 1 Distribution Key and 1 Sort Key - MySQL이나 다른 RDBMS처럼 uniqueness나 foreign key constraint를 걸 수 없다.   **모은 데이터를 어떤 방식으로 Redshift로 옮겨야 할까요?** -------------------------------------- 버즈빌이 구축한 데이터 파이프 라인은 크게 3갈래의 메인 루트가 있습니다. [![](https://tech.buzzvil.com/blog/tech-blog-how-we-pipe-data/1.png)](http://blog.carlesmateo.com/2014/10/13/performance-of-several-languages/) **1) Athena Preprocessing Batch job을 통해서 (잠금화면 활동, 광고 할당)** ----------------------------------------------------------- **Why?** 전처리 작업(Preprocessing)이 필요한 가장 큰 이유는 들어오는 데이터의 어마어마한 크기 때문입니다. 또 어떤 데이터들은 너무 raw하기 때문에 애널리스트나 데이터 사이언티스트가 분석에 활용할 수 있는 형태로 바꾸기위해 전처리가 필요하기도 합니다. 버즈빌에서는 이런 데이터들을 처리하기 위해서 AWS Athena를 사용하고 있습니다. Athena는 과금 방식이 Athena 쿼리로 읽은 데이터의 사이즈를 기반으로 하기 때문에 다른 EMR이나 MapReduce solution들을 사용했을때보다 상대적으로 적은 비용으로 활용할 수 있다는 장점이 있습니다. **How?** 1. 먼저 S3로 데이터를 보냅니다. 2. 그 후, Athena를 활용하여 데이터를 가공/처리합니다. 3. 가공된 데이터를 읽어서 Redshift로 보냅니다. (COPY command 활용) **Pros?** 1. 서버를 따로 가질 필요가 없습니다. (EMR 클러스터나 서버를 관리할 필요가 없음) 2. 경제적입니다. (S3에서 1TB를 읽을때마다 $5 정도의 비용) **Cons?** 1. 사용량이 몰리는 시간대 (12:00 AM UTC)에는 일부 쿼리가 실패할 수 있습니다. -> 중요하고 필수적인 데이터는 Athena가 아닌 다른 방법을 통해 처리하는것이 적합합니다. 2. PRESTO DB의 기능을 (아직은) 온전히 활용할 수 없습니다.   **2) Firehose를 통해서 (Impression, Clicks, Device, Events)** --------------------------------------------------------- **Why?** Kinesis Firehose는 Redshift, Elasticsearch, S3와 같은 최종 목적지까지 다양한 데이터들을 안정적으로 옮길 수 있는 파이프라인을 제공할 뿐 아니라 Fluentd와 매끄럽게 잘 연동된다는 점에서 굉장히 뛰어난 서비스 입니다. Fluentd는 서버로부터 firehose까지 데이터가 안정적이고 꾸준하게 전달 될 수 있도록 도와줍니다. 따라서 firehose와 fluentd의 연동을 통해서 따로 두개의 파이프라인 ( SERVER -> S3, S3 -> Redshift) 을 관리할 필요 없이 데이터 소스부터 최종 저장소까지 이어지는 하나의 파이프 라인만 관리할 수 있게 됩니다. **How?** ![](https://tech.buzzvil.com/blog/tech-blog-how-we-pipe-data/543y743ethf-1.png) ([https://docs.aws.amazon.com/firehose/latest/dev/what-is-this-service.html](https://docs.aws.amazon.com/firehose/latest/dev/what-is-this-service.html)) 1. 적절한 data format과 원하는 ingestion period를 설정하여 Firehose delivery stream을 만듭니다. ```python conf["user_activity"] = { "DataTableName": "user_activity", "DataTableColumns": "user_id, app_id, activity_type, timestamp", "CopyOptions": "FORMAT AS JSON "s3://buzzvil-firehose/sample/user_activity/jsonpaths/user_activity_log-0001.jsonpaths" gzip TIMEFORMAT AS "YYYY-MM-DDTHH:MI:SS" ACCEPTINVCHARS TRUNCATECOLUMNS COMPUPDATE OFF STATUPDATE OFF", "jsonpaths_file": "buzzvil-firehose/sample/user_activity/jsonpaths/user_activity_log-0001.jsonpaths", } configuration = { "RoleARN": "arn:aws:iam::xxxxxxxxxxxx:role/firehose_delivery_role", "ClusterJDBCURL": "jdbc:redshift://buzzvil.xxxxxxxxx.us-west-2.redshift.amazonaws.com:5439/sample_db", "CopyCommand": { "DataTableName": sample_table, "DataTableColumns": conf[type]["DataTableColumns"], "CopyOptions": conf[type]["CopyOptions"], }, "Username": db_user, "Password": db_password, "S3Configuration": { "RoleARN": "arn:aws:iam::xxxxxxxxxxxx:role/firehose_delivery_role", "BucketARN": "arn:aws:s3:::firehose_bucket", "Prefix": "buzzvil/user_activity/", "BufferingHints": { "SizeInMBs": 64, "IntervalInSeconds": 60 }, "CompressionFormat": "GZIP", "EncryptionConfiguration": { "NoEncryptionConfig": "NoEncryption", } } } ``` 2\. Fluentd docker containers을 각각의 서버에서 세팅하고 실행합니다. ``` @type tail path /var/log/containers/buzzad/impression.json pos_file /var/log/containers/td-agent/impression-json.pos format none tag firehose.impression @type kinesis_firehose region us-west-2 delivery_stream_name "prod-buzzad-impression-stream" flush_interval 1s data_key message ``` 3\. Firehose에서 데이터를 잘 모아서 Redshift 문제없이 보내고 있는지 모니터링 합니다. ![](https://tech.buzzvil.com/blog/tech-blog-how-we-pipe-data/789u-1.png) 1. 빠르고 안정적인 데이터 전송이 가능합니다. 2. 모니터링이 편합니다. **Cons?** 1. Schema가 자동으로 바뀌지 않습니다.( Redshift의 Schema를 수동으로 일일히 변경해주어야 합니다.)   **3) MySQL Asynchronous Loads를 통해 (Ads, Contents, Ad Provider, Ad Publishers)** ------------------------------------------------------------------------------- **Why?** 여러대의 RDS MySQL DB로부터오는 데이터간의 sync를 맞춰가며 Redshift로 데이터를 복제하기 위해서는 3가지의 테크닉을 활용해야만 합니다. (이 방법은 소개하고 있는 세 메인 루트 중에서 가장 매력도가 떨어지는 방법입니다..) **How?** * FULL_COPY * MySQL 테이블 전체를 복사해서 SQL insert를 통해서 Redshift에 복사합니다. * INCREMENTAL_COPY * 이전에 복사한 가장 마지막 Primary key부터 시작해서 새로생긴 row들을 읽어서 Redshift로 복사합니다. * UPDATE\_LATEST\_COPY * 이전에 복사한 가장 마지막 타임스탬프부터 시작해서 새로 생성되거나 업데이트된 row들을 Redshift로 복사합니다.(중복된 값은 삭제). **Pros?** 1. 데이터의 특징에 맞게 잘 조정된 방법입니다. 2. binary log를 통한 Replication보다 훨씬 다루기 쉽습니다. **Cons?** 1. MySQL을 잘 조정하기 위해 여러대의 서버나 lambda를 다루어야만 합니다. -> Redshift sync task를 위해서 2. 안정적인 schema altering을 할 수 있을 만큼 Redshift의 ORM이 발전된 상황은 아닙니다.. 어떤 데이터를 다루는지에 따라서 위에서 소개한 3가지 방법 중 어떤 방법을 활용해야할지가 달라진다고 할 수 있습니다. 예를 들어 Transactianl log 같은 데이터들의 경우에는 firehose를 통해 전달하는 방법이나 먼저 aggregate하는 과정을 거친 후에 Redshift에 저장하는 식으로 처리를 해야 합니다. 그리고 MySQL에 저장된 fact table같은 데이터들은 CDC (change data capture) sync method를 통해서 Redshift에 데이터를 전달하고 동기화를 하는 과정이 필요합니다. 버즈빌에서는 위에서 소개해드린 3가지 방법을 적절히 조합해가면서 BD 매니저나 애널리스트들이 서비스간 플랫폼간의 데이터분석을 쉽게 할 수 있는 데이터 환경을 구축하기 위해서 노력하고 있습니다. --- ## [A/B Testing - Sampling부터 Interpretation까지](https://tech.buzzvil.com/blog/tech-industry-a-b-testing-sampling부터-interpretation까지) Date: 2018-06-14 | Category: Data & ML 안녕하세요. 모바일 첫화면 미디어 플랫폼 버즈빌의 Business Intelligence Manager Elia 입니다. 저희 버즈빌에서는 여타 모바일 환경에서 작업하는 회사와 마찬가지로 무수히 많은 a/b 테스팅을 통해 프로덕트를 개선하고 있습니다. 이번 포스팅에서는 a/b 테스팅을 실제로 수행할 때 주로 발생하는 궁금증에 대해서 경험적으로 느낀 바를 공유하려 합니다. 먼저 이 글을 써야겠다고 생각하게 된 것은, 옆자리에 앉은 PM Jim의 질문에서 시작되었습니다.   _"a/b 테스팅할 때, 샘플 사이즈를 어떻게 정해요?"_   이 질문은 스타트업 관련, 데이터 관련 컨퍼런스에서 항상 등장하는 주제라서, 한 번 정리해 보면 (hopefully) 많은 사람들한테 도움이 될 수 있을 것 같았습니다. 그리하여 이왕 하는 김에 a/b testing 관련해서 그간 생각해왔던 이것저것을 sampling과 interpretation으로 나눠서 정리해보려 합니다. ### **1\. Sampling** 보통 a/b testing에 들어가는 통계 기법(?)은 t-test입니다. 그룹 a의 평균값과 그룹 b의 평균값이 같은 것인지 다른 것인지를 추정하는 테스트입니다. (group A와 group B의 값은 쉽게 관찰 가능한 것인데, 둘이 같은지 다른지를 판단한다는 것이 헷갈리실 수 있겠지만 이는 interpretation 부분에서 다루겠습니다) 일반적으로 학계에서 많이 쓰이는 알파 에러율 5%와 테스팅 파워 95%를 위해서는 샘플이 120 정도만 되면 됩니다. 이는 실제 인더스트리에서 행해지는 테스트의 샘플 사이즈에 비하면 크지 않은 숫자이므로, 모바일 환경에서 테스트를 진행할 때에는 크게 걱정할 필요가 없는 수준입니다. (반대로 만약 샘플 사이즈가 120보다 작다면, 많은 어려움에 봉착할 수 있습니다.) ![](https://tech.buzzvil.com/blog/tech-industry-a-b-testing-sampling%eb%b6%80%ed%84%b0-interpretation%ea%b9%8c%ec%a7%80/%e1%84%80%e1%85%b3%e1%84%85%e1%85%b5%e1%86%b71.png) 샘플 사이즈가 충족되었다면, 그 다음에 알아봐야 할 문제가 진짜 문제입니다. 보통 a/b testing이 혼선을 겪는 부분이 이 부분부터입니다. #### _**샘플이 랜덤하게 그룹지어져 있는가?**_ 첫번째는 샘플들이 충분히 랜덤하게 추출되어야 한다는 부분입니다. 여기서 랜덤 샘플링이 필요한 이유는, a/b testing에서 확인해보고자 하는 stimulus가 오롯이 잘 표현되어야 하기 때문입니다. 예를 들어서 모바일 환경에서의 a/b testing 중 가장 많이 등장하는 KPI 중 하나인 CTR을 생각해봅시다. 샘플을 500명씩 뽑아서, group A에게는 초록색 글자를 보여주고 group B에게는 파란색 글자를 보여줘서, 파란색과 초록색 중 CTR이 높은 쪽으로 폰트 색깔을 바꾸는 테스트를 가정해 봅시다. group A와 group B가 랜덤으로 배정되어야만 두 그룹의 유일한 차이점은 폰트 색깔이 될 것이고, 그럴 때에 폰트 색깔이 만들어내는 차이점을 온전히 관찰할 수 있습니다. _만약 group A에 초록색을 좋아하는 유저들이 모여있다면?_ _만약 group A에 폰트 색깔과 관계없이 CTR이 높은 유저들이 모여있다면?_ 그럴 경우에는 결과가 ‘group A = 3% vs group B = 1%’라고 나온다고 하더라도, ‘초록색이 만들어내는 CTR > 파란색이 만들어내는 CTR’이라고 결론지을 수 없습니다. 그럼 어떻게 랜덤 샘플링을 할 수 있을까요? 일반적으로는 유저를 가입 시점으로 줄을 세워서 자연수를 1부터 N까지 부여한 뒤에 mod function을 이용해서 나머지값을 이용해서 그룹을 지을 수 있습니다. 그룹이 두 개인 경우엔 mod(X, 2)로 만들어서 그룹 0과 그룹 1을 만들 수 있습니다. 서비스에 가입하는 순서의 홀짝 숫자에 따라서 유저군의 특성이 갈릴 위험이 없으므로, 이 방식으로 대부분의 노이즈를 없앨 수 있습니다. 즉, 90% 이상 random sampling이 가능해집니다. 남아있는 10%는 경험에서 체득한 팁이므로 서비스의 특성에 따라 적용되지 않을 수도 있습니다. #### **_sample은 population의 축소판이고, 축소판이어야 합니다_** ![](https://tech.buzzvil.com/blog/tech-industry-a-b-testing-sampling%eb%b6%80%ed%84%b0-interpretation%ea%b9%8c%ec%a7%80/%e1%84%80%e1%85%b3%e1%84%85%e1%85%b5%e1%86%b72.png) sample은 population을 반영해야 합니다. population을 랜덤하게 반영해야 한다는 것이 위에 기술된 내용이었고, 이번 단락에서는 sample이 population을 반영해야 한다는 보다 근본적인 이야기를 다뤄보려 합니다. sample이 population을 온전히 반영하지 못하면 실망스러운 결과가 발생할 수 있습니다. 실제 버즈빌에서 있었던 예를 들어서 설명드리겠습니다. 사용된 숫자는 이해를 돕기위해 예시로 사용하였습니다. 버즈빌에서는 랜덤 샘플링의 이론에 충실하게 NRU만을 대상으로 테스트를 진행할 때가 있습니다. 위의 폰트 색깔의 예시를 이어나가면, 초록색 글씨와 파란색 글씨의 차이를 구분하기 위해서, 유저들이 아무런 선입견 없이 파란색과 초록색의 차이에 어떻게 반응하는지를 확인해보기 위해 샘플을 NRU로 한정한 적이 있습니다. 오래된 유저들은 기존의 색깔인 초록색에 익숙한 경험이 있을테니, 새로운 색인 파란색에 더 민감하게 반응할 것이라는 가정이 있었고, 이러한 “노이즈"를 없애기 위하여 샘플을 NRU로 한정한 것이었습니다. 그리하여 테스트를 통해 ‘초록색 = 3% vs 파란색 = 3.6%’로 20% 발전이라는 야심찬 계획을 가지고 파란색을 전체 유저를 대상으로 구현한 결과! 기존의 3% CTR이 3.15%로 5% 발전하는 실망스러운 경험을 하게 되었습니다. 왜 이런 일이 발생했을까요? 여러 이유가 있겠지만, sample이 population을 반영하지 못한 부분이 가장 큽니다. 우리의 전체 유저군을 population으로 봤을 때, NRU는 특정한 유저군을 의미합니다. 그들은 새로 서비스에 들어왔기 때문에 지금까지 이뤄진 learning도 없고, 그러므로 폰트 색깔에 대한 선입견도 없습니다. 그렇지만 선입견이 없다는 점이 문제였습니다. 테스트 자체만을 위해서는 깔끔한 샘플링인 것처럼 보였지만, 사실 우리가 테스트를 통해 향상시키고자 하는 목표는 전체 유저의 CTR이지 NRU의 CTR이 아니었습니다. 전체 유저 중 NRU를 제외한 유저들은 각각 서비스 경험에 따라 학습된 부분이 다르고, 그러므로 새로운 font color에 반응하는 정도도 다릅니다. population의 CTR을 높이고 싶었다면 샘플링 역시 population을 대상으로 수행되었어야 했고, 서비스 이용시간에 따른 learning이 걱정되었다면 learning의 정도가 두 그룹에 고르게 들어가서 상쇄될 수 있도록 가입 순서대로 차례차례 샘플링하면 해결되었을 것입니다. sampling은 noise를 상쇄할 수 있도록 random하게 수행되어야 하지만, 내가 달성하고자 하는 KPI의 대상이 되는 population을 충실히 반영해야 한다는 대전제를 반드시 기억해야 합니다. ### 2\. Interpretation 이제 샘플링도 끝났고 각각의 그룹에 각각의 stimulus를 주었습니다. 그러면 각 그룹이 어떤 반응을 보였는지 결과를 해석해야 합니다. 다시 글씨 색깔의 예로 돌아가 봅시다. population을 대표하는 random sample group A 500명과 group B 500명에게 각각 초록색 글씨와 파란색 글씨를 보여주는 실험을 2주일간 진행하였습니다. 결과는 group A는 CTR 3%이고 group B는 CTR 4%를 기록하였습니다. 그렇다면 group B가 더 좋은 결과를 보인 것인데, 이 결과를 믿어도 되는 것일까요? #### _**t-test가 무엇을 판별하는 테스트인지 이해해야 합니다**_ group B의 CTR 4%가 group A의 CTR 3%보다 높은 것이 믿을만한 것이라는 질문은 무슨 뜻일까요? 이를 이해하기 위해서 우리는 우선 group A의 CTR 3%가 고정되어있는 숫자가 아니라, 유저별로 혹은 시간이나 날씨와 같은 외부적 환경에 따라 조금씩 바뀔 수 있는 숫자라는 점을 알아야 합니다. group A의 CTR 3%는 group A에 속한 CTR 평균 10%를 보여주는 유저, 평균 0.1%를 보여주는 유저, 그리고 새벽 3시에 최고로 졸릴 때 글자를 접한 유저, 오후 10시 프라임 타임에 글자를 접한 유저의 모든 경우의 수를 평균해서 보여주는 숫자입니다. 그러므로 똑같은 유저들에게 정확히 같은 조건으로 2주간 실험을 다시 하면 3.1%가 될 수도 있고 2.7%가 될 수도 있습니다. 다만 이번 실험에서 3%를 보였다는 의미입니다. 이는 group B가 보여준 4%도 마찬가지입니다. 다음 실험에서는 3.5%가 나올 수도 있고, 4.2%가 나올 수도 있습니다. 다시 원래의 질문으로 돌아가면, “group B의 CTR 4%가 group A의 3%보다 높은 것이 믿을만한 것인가?”라는 질문은, 다시 말하면 “지금 보여주는 차이 1%가, 과연 다음 실험에서도(그래서 전체에게 적용되면 영구적으로) 반복될 것인가?”라는 질문과 마찬가지입니다. 위 질문을 통계적으로 해석하면 과연 3%와 4%라는 값이 같은 분포에서 나왔는지 다른 분포에서 나왔는지를 묻는 것입니다. ![](https://tech.buzzvil.com/blog/tech-industry-a-b-testing-sampling%eb%b6%80%ed%84%b0-interpretation%ea%b9%8c%ec%a7%80/%e1%84%80%e1%85%b3%e1%84%85%e1%85%b5%e1%86%b73.png) 만약 위와 같이 3%와 4%가 같은 분포에서 나왔다면, 다음에 똑같은 조건으로 실험을 한다면 2.5% vs 4.5%가 나올 수도 있고, 이와 같은 확률로 3% vs 2%가 나올 수 있습니다. 이런 경우에는 원래 질문인 “믿을만한 것인가?”에 “믿을 수 없다"는 대답이 나오는 것이고, 이는 결국 “다음 실험에도 지금과 같은 결과가 나올 것이라고 장담할 수 없다"는 것입니다. 3%와 4%가 정말 의미가 있는 차이를 가지려면 둘이 다른 분포를 가져야 합니다. ![](https://tech.buzzvil.com/blog/tech-industry-a-b-testing-sampling%eb%b6%80%ed%84%b0-interpretation%ea%b9%8c%ec%a7%80/%e1%84%80%e1%85%b3%e1%84%85%e1%85%b5%e1%86%b74.png) 위와 같이 3%와 4%가 서로 다른 분포에서 나온 것이라면, 다음 실험에서도 3%와 4%의 차이가 유지될 확률이 높고, 그러할 때에  “group B의 CTR 4%가 group A의 3%보다 높은 것이 믿을만한 것이다”라고 할 수 있습니다. 문제는, 우리는 둘이 같은 분포에서 나왔는지 다른 분포에서 나왔는지 알 길이 없다는 것입니다. 단지 우리가 알 수 있는 것은 group A는 3%를 보였다는 것이고 group B는 4%를 보였다는 사실 뿐입니다. 이 정보를 가지고 우리는 통계적인 추정을 해야하는 것이고, 그것이 a/b testing에서 하고자 하는 t-test가 도와주는 부분입니다. 결과를 보는 방법은 매우 쉽습니다. 구글에 조금만 찾아보면 많은 계산기가 등장하므로 아무 것이나 찾아서 하셔도 좋고, 이 글에서 조금 편의를 제공하자면 [**https://abtestguide.com/calc/**](https://abtestguide.com/calc/)** 를 이용하셔도 좋습니다. (버즈빌과는 아무 관계가 없는 사이트입니다) 어차피 프로그램이 매우 편하게 계산해 주지만, 혹시 궁금하신 분을 위해서 약간의 설명을 추가하자면, 기본적으로 그룹의 크기와 관계가 있습니다. 직관적으로 생각해도 각각 10명에게 테스트를 해서 3%와 4%의 차이가 나오는 것과 10,000명에게 테스트해서 3%와 4%의 차이가 나는 것은 신뢰도가 다를 수밖에 없을 것입니다. 각각 10,000명의 샘플을 가지고 3%와 4%가 나온다면, 두 그룹이 다른 분포를 형성하고 있을 확률이 보다 높다고 할 수 있을 것입니다. ![](https://tech.buzzvil.com/blog/tech-industry-a-b-testing-sampling%eb%b6%80%ed%84%b0-interpretation%ea%b9%8c%ec%a7%80/%e1%84%80%e1%85%b3%e1%84%85%e1%85%b5%e1%86%b75.png) 저희의 예를 위에다 입력해보니, significant 하지 않다는 결과가 도출되었습니다. 이는 즉, 다음 번에 똑같은 조건으로 실험을 하면 group A의 결과가 group B의 결과보다 작게 나오는 것이 반복될 확률이 충분히 크지 않다는 의미입니다. confidence를 95로 설정했으므로 보다 정확히 말하면 “실험을 무한히 반복할 때, group A의 CTR이 group B의 CTR보다 낮은 경우가 95% 이하로 발생할 확률이 점점 커진다"라는 말입니다. 물론 개인적으로는 a/b testing을 할 때 신뢰도 95%에 너무 집착할 필요가 없다는 입장입니다. 무한히 했을 때 95% 이상 같은 결과가 반복되지 않는다 하더라도 회사로서는 충분히 좋은 의미가 있을 수 있으니, 이 부분에 관해서는 사용자가 스스로 설정하면 될 것 같습니다. #### **_매일매일을 독립적인 테스트라고 생각하면 훨씬 간단할 수 있습니다_** 버즈빌에서는 테스트를 진행할 때 많은 경우 결과를 일별로 끊어서 그래프를 확인합니다. group A와 group B가 2주일간 보여준 결과를 하나의 실험이라고 본 것이 위에 적어놓은 내용이라면, 2주일 동안 매일매일을 새로운 실험으로 보고 14개의 실험을 한 경우라고 생각해본다고 가정해봅시다. 매일매일 group B가 group A에 비해서 높은 결과를 보였다면, 사실 위에서 계산한 과정을 거치지 않아도, 둘이 확실히 다르다고 확신하셔도 무방합니다. 동전을 던져서 앞면이 나올 확률은 50%지만, 동전을 14번 던져서 14번 앞면이 나올 확률은 거의 0%에 가깝기 때문인 것과 같은 이치입니다. 테스트를 한 번 수행해서 나온 값 3%와 4%는 통계적으로 유의미한 결과인지 생각해봐야 하지만, 14번동안 일관되게 “group A’s CTR < group B’s CTR”이라면 group B의 CTR이 유의미하게 높다고 봐도 무방할 것입니다. 결과가 매일매일 엎치락뒤치락 하다면, 위와 같이 전체 값을 놓고 판별해보는 과정이 필요할 것이고, 14번 동안 10번은 B가 높고  4번은 A가 높은 수준이라면, 아마 더 깊이 분석해보지 않아도 높은 확률로 실제로 B가 높을 것입니다. 진짜 현실적으로 판단하기 어려운 때가 매일매일의 값은 일반적으로 B가 높은데, 어떤 특수한 날 A의 값이 이상한 정도로 높은 때, 즉 아웃라이어가 있을 때입니다. 그럴 때는 보통 1. 아웃라이어를 만들어내는 유저를 찾아보고 샘플에 포함시키는 것이 합당한지 판단해본다. 2. 실험의 외부적인 요소가 그 아웃라이어를 만들어내지 않았을지 알아본다. (데이터의 인풋 자체가 잘못됐을 가능성 조사) 등을 통해 문제를 해결할 수 있습니다. 데이터를 시간별, 일별, 주간별로 쪼개어 보면 하나로 합쳐져 있을 때 보이지 않았던 많은 인사이트를 얻을 수 있고, 운이 좋으면 통계 분석을 건너뛰고도 정확한 결론을 내릴 수 있는 가능성이 있으므로 반드시 추천하는 팁입니다. 한 번의 큰 테스트도 좋지만, 14번의 작은 테스트 역시 더 많은 인사이트를 줄 수 있는 것과 같은 이치입니다.이상으로 a/b 테스팅과 관련된 포스팅을 마치겠습니다. 읽다가 궁금하신 사항이나 맞지 않는 정보가 담겨있다고 생각하시는 부분 있으면 언제든지 [**elia.rho@buzzvil.com**](mailto:elia.rho@buzzvil.com)으로 문의주시기 바랍니다. 독자 여러분의 a/b 테스팅에 조금이나마 도움이 되기를 바랍니다.   감사합니다. --- ## [버즈빌의 AWS Summit 2018 발표 참관기](https://tech.buzzvil.com/blog/buzzvil-news-버즈빌의-aws-summit-2018-발표-참관기) Date: 2018-05-04 | Category: Culture AWS에서는 세계 각국의 주요도시에서 매년 AWS Summit을 개최하고 있습니다. 서울에서도 2015년을 시작으로 매년 AWS Summit Seoul을 개최해 오고 있는데요. 올해로 4회가 되는 [**AWS Summit Seoul 2018**](https://aws.amazon.com/ko/summits/seoul/)은 지난 4월 18일, 19일 양일간 클라우드 컴퓨팅의 미래를 조망할 기조연설을 포함하여 100여개의 다양한 강연, 파트너 전시 부스 및 각종 부대 행사에 이르기까지 다양한 내용들로 진행되었습니다. 버즈빌은 개발과 관련된 거의 모든 인프라를 AWS에서 운영하고 있습니다. EC2, S3, Cloudfront, RDS, DynamoDB, Elasticache, Redshift, Athena, Kinesis 등 AWS의 여러가지 서비스를 사용하고 있는 만큼 AWS Summit에 대한 많은 관심을 가지고 있는데요. 특별히, 이번 AWS Summit 에는 버즈빌 Product팀의 Senior Engineer인 Ben이 커뮤니티 섹션의 발표자로 참여하게 되어 그 관심이 더욱 뜨거웠습니다. 이 포스팅을 통해 **“AWS에서 Kubernetes 실전 활용하기”** 라는 주제로 진행되었던 Ben의 발표 내용에 대해 간략하게 공유하고자 합니다.![](https://tech.buzzvil.com/blog/buzzvil-news-%eb%b2%84%ec%a6%88%eb%b9%8c%ec%9d%98-aws-summit-2018-%eb%b0%9c%ed%91%9c-%ec%b0%b8%ea%b4%80%ea%b8%b0/dsc07456.jpg)[**Kubernetes**](https://kubernetes.io/)는 Google에 의해 설계되었고 현재 리눅스 재단에 의해 관리되고 있는 Container Orchestration Tool 입니다. Ben은 버즈빌에서의 Kubernetes의 도입 배경과 필요성에 대해 설명하며 Infra Engineer가 따로 없는 대부분의 스타트업들이 여러 Instance 내의 container들을 통합 관리하기란 쉽지 않으며 이러한 상황에서 Container Orchestration Tool을 도입하여 잘 활용한다면 큰 도움을 받을 수 있다고 설명했습니다. 뿐만 아니라 Docker에서 새로운 Container를 생성할 때 Node의 개수를 최적화 해주는 **Automatic binpacking**, 사전에 정한 metric에 따라 Deployment를 자동으로 scaling해주는 **Horizontal scaling**, 특정 Container 나 Node가 죽었을때 자동으로 복구시켜주는 **Self-healing** 등 서비스를 보다 안정적으로 운영할 수 있게 도와주는 Kubernetes의 유용한 기능들도 발표를 통해 소개 되었습니다. 이어서 AWS 환경에서 Kubernetes를 잘 활용할 수 있게끔 도와주는 [**kops(Kubernetes Operations)**](https://github.com/kubernetes/kops)에 대한 소개와 함께 청중들의 실제적인 이해를 돕기 위해 kops를 실제로 활용한 간단한 데모도 진행하였습니다.   Kubernetes의 도입배경과 각종 기능들, 실제 Kubernetes를 활용하는 법에 대한 데모까지 포함된 Ben의 발표에 대한 보다 자세한 내용은 아래의 자료를 통해 확인하실 수 있습니다. - [“AWS에서 Kubernetes 실전 활용하기” 발표자료](https://www.slideshare.net/awskorea/practical-usage-of-kurbernetes-on-aws-yoo-byeongu) - [Demo github](https://github.com/urunimi/kube-sample) ![](https://tech.buzzvil.com/blog/buzzvil-news-%eb%b2%84%ec%a6%88%eb%b9%8c%ec%9d%98-aws-summit-2018-%eb%b0%9c%ed%91%9c-%ec%b0%b8%ea%b4%80%ea%b8%b0/dsc_2103.jpg) ![](https://tech.buzzvil.com/blog/buzzvil-news-%eb%b2%84%ec%a6%88%eb%b9%8c%ec%9d%98-aws-summit-2018-%eb%b0%9c%ed%91%9c-%ec%b0%b8%ea%b4%80%ea%b8%b0/dsc_2120.jpg) ![](https://tech.buzzvil.com/blog/buzzvil-news-%eb%b2%84%ec%a6%88%eb%b9%8c%ec%9d%98-aws-summit-2018-%eb%b0%9c%ed%91%9c-%ec%b0%b8%ea%b4%80%ea%b8%b0/dsc_2106.jpg) 버즈빌에서는 서비스를 더 빠르고 안정적으로 운영하기 위한 다양한 고민과 시도를 하고 있는데요. Kubernetes를 도입하게 된 것도 그 고민의 결과물이었다고 할 수 있습니다. 특별히 이번에는 AWS Summit이라는 좋은 기회를 통해 버즈빌이 고민해온 내용들에 대해 함께 나눌 수 있었기에 더욱 의미있었던 것 같습니다.버즈빌의 개발자들의 고민이나 다양한 개발 이야기들이 궁금하신 분은 [**Buzzvil Tech Blog**](https://tech.buzzvil.com/)를 참고해 주시면 더 다양하고 재미있는 버즈빌의 개발 이야기를 만나보실 수 있습니다. --- ## [Go 서버 개발하기](https://tech.buzzvil.com/blog/tech-blog-go-서버-개발하기) Date: 2018-02-12 | Category: Backend **Go 서버 개발을 시작하며** ------------------   ### 특정 API만 다른 언어로 구현해서 최대의 성능을 내보자! 저희 서버는 대부분 Django framework 위에서 구현된 광고 할당 / 컨텐츠 할당 / 허니스크린 앱 서비스 이렇게 나눌 수 있는데 Python 이라는 언어 특성상 높은 성능을 기대하기가 어려웠습니다. 하지만 세가지 서비스에서 락스크린에서 어떤 컨텐츠나 광고를 보여줄지 결정하는 Allocation(할당) API 가 가장 많이 호출되고 있었는데 빈도로 보면 80% 정도로 높은 비중을 차지하고 있어서 이 Allocation API 들을 성능이 좋은 다른 언어로 구현하면 어떨까 하는 팀내 의견이 있었습니다. ### Why Go? 저는 예전부터 Java,  C# 등의 컴파일 언어에 익숙해서 기존 Java 와 C, 그리고 Go 라는 최근에 새로 나온 언어 중에서 아래 블로그글과 같이 여러 reference 들을 통해 성능이 좋다는 Go 로 이 API 들을 포팅하는 작업을 시작하게 되었습니다. Go 에 대한 첫 인상은 Java, C계열 언어보다 덜 verbose 보였고 python 보다는 strongly-typed, encapsulated 하다보니 자유도를 제한해서 코드를 보기 쉽게 하는 것을 선호하는 저의 성격과도 잘 맞는 언어였습니다. ![](https://tech.buzzvil.com/blog/tech-blog-go-%ec%84%9c%eb%b2%84-%ea%b0%9c%eb%b0%9c%ed%95%98%ea%b8%b0/golang.png) [출처: Carles Mateo, Performance of several languages](http://blog.carlesmateo.com/2014/10/13/performance-of-several-languages/) **서버 개발 환경** ------------   ### Server design #### How to import libraries * GVT ([https://github.com/FiloSottile/gvt](https://github.com/FiloSottile/gvt)) \- Go 는 vendering tool 을 통해 dependency 를 관리할 수 있습니다. GVT 의 경우 처음 도입했을 때 별로 유명하지 않았는데 사용법이 간단해서 도입하게 되었습니다. 아래와 같이 참조하고 있는 revision 을 관리해주며 update 통해서 최신 소스를 받아 올수 있습니다. ``` { "version": 0, "dependencies": [ { "importpath": "github.com/Buzzvil/go-env", "repository": "https://github.com/Buzzvil/go-env", "vcs": "git", "revision": "2d8489d40184a12c4d09d09ce1ff717e5dbb0745", "branch": "master", "notests": true }, .... ``` #### Design pattern ![](https://tech.buzzvil.com/blog/tech-blog-go-%ec%84%9c%eb%b2%84-%ea%b0%9c%eb%b0%9c%ed%95%98%ea%b8%b0/designpattern.png)Go 언어에서는 package level cycling dependency 를 허용하지 않아서 좀더 명확한 구조를 만들기 좋았습니다. 예를들어 Service 에서는 Controller 를 참조할수 없고 Model 에서는 Controller / Service / DTO 등을 참조할수 없도록 강제했습니다. 모든 API 요청은 Route 를 통해 Controller 에게 전달되고 이 때 생성된 DTO (Data transfer object) 들을 Controller 가 직접 혹은 Service layer 에서 처리하도록 하였고 DB 에 접근할 때는 모델을 통해 혹은 직접 접근하도록 했지만 추후 구조가 복잡해지면 DB 쿼리 등을 담당하는 DAO (Data access object) 를 도입할 계획입니다 ### Libraries 요소 이름 선택 이유 Network [Gin](https://github.com/gin-gonic/gin) Web 서버이다 보니 네트워크 성능을 최우선으로 고려, [벤치마크](https://github.com/gin-gonic/gin/blob/master/BENCHMARKS.md) 표를 보고 이 라이브러리를 선택 Redis & cache [go-redis](https://github.com/go-redis/redis) 역시 성능을 가장 중요한 지표로 보고 이 라이브러리 선택 Mysql [Gorm](https://github.com/jinzhu/gorm) ORM 없이는 개발하기 힘든 시대이죠. 여러 Database를 지원하고 ORM 중에서도 method chaining 을 사용하는 Gorm 을 선택 Dynamo [guregu dynamo](https://github.com/guregu/dynamo) AWS에서 제공하는 Dynamo 패키지를 그대로 사용하면 코드 양이 너무 많아지고 역시 method chaining 을 지원해서 선택 Environment variables [caarlos0 env](https://github.com/caarlos0/env) Go 에서는 tag 를 이용하면 좀더 코드를 간결하고 읽기 쉽게 사용할수 있는데 이 라이브러리가 환경변수를 읽어오기 쉽도록 해줌 #### Redis cache ```golang func SetCache(key string, obj interface{}, expiration time.Duration) error { err := getCodec().Set(&cache.Item{ Key: key, Object: obj, Expiration: expiration, }) return err } func GetCache(key string, obj interface{}) error { return getCodec().Get(key, obj) } ``` #### Mysql ```golang var config model.DeviceContentConfig env.GetDatabase().Where(&model.DeviceContentConfig{DeviceId: deviceId}).FirstOrInit(&config) ``` #### Dynamo ```golang if err := env.GetDynamoDb().Table(env.Config.DynamoTableProfile).Get(keyId, deviceId).All(&profiles); err == nil && len(profiles) > 0 { ... } ``` #### Environment variables ```golang var ( Config = ServerConfigStruct{} onceConfig sync.Once ) type ( ServerConfigStruct struct { ServerEnv string `env:"SERVER_ENV"` LogLevel string .... } ) func LoadServerConfig(configDir string) { onceConfig.Do(func() {//최초 한번반 호출되도록 env.Parse(&Config) } } ``` **Unit test** ------------- ### 환경 구성 Test 환경에는 Redis / Mysql / Elastic search 등에 대한 independent / isolated 된 환경이 필요해서 이를 위해 docker 환경을 따로 구성하였습니다. Test case 작성은 아래와 같이 package 를 분리해서 작성했습니다. ```golang package buzzscreen_test var ts *httptest.Server func TestMain(m *testing.M) { ts = tests.GetTestServer(m) // 환경 시작 tearDownElasticSearch := tests.SetupElasticSearch() tearDownDatabase := tests.SetupDatabase() code := m.Run() // 여기서 작성한 TestCase 들 실행 // 환경 종료 tearDownDatabase() tearDownElasticSearch() ts.Close() os.Exit(code) } ``` Mock server는 은 http.RoundTripper interface 를 구현해서 http.Client 의 Transport 멤버로 설정해서 구현했습니다. 아래는 Test case 작성 예제입니다. ```golang httpClient := network.DefaultHttpClient mockServer := mock.NewTargetServer(network.GetHost(MockServerUrl)) .AddResponseHandler(&mock.ResponseHandler{ WriteToBody: func() []byte { return []byte(mockRes) }, Path: "/path", Method: http.MethodGet, }) clientPatcher := mock.PatchClient(httpClient, mockServer) defer clientPatcher.RemovePatch() ``` Unit test 관련해서는 내용이 방대해서 추후 다른 포스트를 통해 자세히 소개하도록 하겠습니다. **Infra** --------- ### API 요청 분할 #### AWS Application load balancer 여러 API 중에서 할당 API 를 제외한 요청은 기존의 Django 서버로 요청을 보내고 할당요청에 대해서만 Go서버로 요청을 보내도록 구현하기 위해 먼저 시도 했던 것은 AWS Application load balancer (이후 ALB) 였습니다. ALB 의 특징이 path 로 요청을 구별해서 처리할수 있었기 때문에 Allocation API 만 Go 서버 로 요청이 가도록 구현했습니다. [![](https://dmhnzl5mp9mj6.cloudfront.net/application-management_awsblog/images/img2.png)](https://aws.amazon.com/ko/blogs/devops/introducing-application-load-balancer-unlocking-and-optimizing-architectures/) [출처: Amazon Devops Blog, Introducing Application Load Balancer](https://aws.amazon.com/ko/blogs/devops/introducing-application-load-balancer-unlocking-and-optimizing-architectures/) 하지만 이렇게 오랫동안 서비스 하지 못했는데 그 이유는 서버 구성이 하나 더 늘어나고 앞단에 ALB 까지 추가되다 보니 이를 관리하는데 추가 리소스가 들어가게 되어서 어떻게 하면 이러한 비용을 줄일수 있을까 고민하게 되었습니다.   #### Using docker & nginx ![](https://tech.buzzvil.com/blog/tech-blog-go-%ec%84%9c%eb%b2%84-%ea%b0%9c%eb%b0%9c%ed%95%98%ea%b8%b0/docker-1024x662.png) Go로 작성된 서버가 독립적인 Micro service 냐 아니면 Django 서버에서 특정 API 를 독립시켜 성능을 강화한 모듈이냐 의 정체성을 두고 생각해봤을때 후자가 조금더 적합하다보니 Go / Django 서버는 한 묶음으로 관리하는 것이 명확했습니다. Docker 를 도입하면서 nginx container 가 proxy 역할을 하고 path를 보고 Go container / Django container 로 요청을 보내는 구성을 가지게 되었습니다. **글을 마치며** ---------- ### 시작은 미약하였으나 끝은 창대하리라 하나의 API를 이전했음에도 불구하고 Allocation API 에 대해서는 약 1/3, 서버 Instance 비용은 1/2.5 수준으로 감소했습니다. ![](https://tech.buzzvil.com/blog/tech-blog-go-%ec%84%9c%eb%b2%84-%ea%b0%9c%eb%b0%9c%ed%95%98%ea%b8%b0/screen-shot-2016-12-07-at-3.28.08-pm.jpg) 설명: 기존 4개의 Django 인스턴스의 CPU 사용률이 모두 13% 정도 감소, Go 인스턴스의 CPU 사용율은 17% 정도   17 / (13 * 4)  ≒ 1 / 3 충분히 만족할만한 성과가 나와서 그 뒤로 몇가지 API도 Go 로 옮겼고 새로 작성하는 API 는 Go 환경 안에서 직접 구현하는 중입니다. 처음에는 호출이 많은 하나의 API 를 다른 언어로 포팅하기 위해 시작한 작업이었는데 Container 기술을 도입하는 등 서버 Infra 까지 변경하면서 상당히 큰 작업이 뒤따르게 되었습니다. 하지만 이 작업을 하면서 많은 동료들의 도움과 조언이 있었고 결국 완성할수 있었습니다. 이렇게 실험적인 도전을 성공 할수 있는 환경에 여러분을 초대하고 싶습니다! Go언어에 대한 문의나 좋은 의견도 환영합니다. --- ## [아마존 에코를 활용한 음성 인식 에어컨 제어](https://tech.buzzvil.com/blog/아마존-에코를-활용한-음성-인식-에어컨-제어) Date: 2017-09-27 | Category: Backend 에어컨 제어를 위한 회로 설계 및 LIRC 사용 -------------------------- > 이 포스팅은 하드웨어 대한 지식이 부족한 독자도 이해할 수 있도록 하는 것을 목표로 하여 작성되었습니다. 국민학교 시절에 문방구에서 팔던 사이렌, 라디오 조립 키트를 기억하시나요? 어렸을적 과학시간에 배운 꼬마전구 회로를 만드는 것에 흥미를 느껴서 사이렌 조립키트를 산 적이 있었습니다. 하지만 납땜을 하려면 납이 필요하다는 사실을 모른 채 인두만 사서 납땜을 시도하다가 실패한 가슴아픈 추억이 있습니다. 진짜 납땜은 10년이 지나 학부 논리설계수업에서 처음 해보게 되었습니다. 지금은 회사에서 고수준 언어인 파이썬을 활용해 잠금화면 광고 플랫폼을 만들고 있지만 여전히 마음 한켠에는 하드웨어에 대한 로망을 간직하고 있었습니다. 그래서 이번 기회에 이왕이면 회사에 도움이 될 만한 하드웨어 작품을 만들어보고자 고민하다가 음성인식으로 에어컨을 제어하는 시스템을 만드는 아이디어를 떠올리게 되었습니다. 버즈빌 사무실에는 한 층에 3대의 에어컨이 있습니다. 여름에는 무려 이 3대의 에어컨을 일일히 켜주어야 하는 불편함이 있었습니다. 심지어 2대는 삼성 나머지 한대는 LG 에어컨으로 모델이 달라서 리모콘도 두 개나 필요합니다. 한번에 에어컨을 모두 켤 수 있게 하면 사람들이 좋아하지 않을까, 그리고 간지나게 음성명령으로 제어하면 좋겠다는 생각을 했습니다. 요즘엔 세상이 참 좋아져서 라즈베리파이, 아마존 에코 등을 활용하면 비교적 싼 가격에 원하는 것을 만들 수 있습니다. 프로젝트의 목표는 다음과 같이 정했습니다. * "Alexa, turn on the AC" * "Alexa, could you please turn off the AC?" 처럼 말하면 알아서 에어컨을 끄고 켜주는 것입니다. 회사에서 슬랙을 메신저로 쓰고 있는데 커스텀 커맨드 기능을 지원하니 아래처럼 슬랙에서도 에어컨을 제어하는 것도 목표로 삼았습니다. * /acon - 에어컨 끄기 * /acoff - 에어컨 켜기 * /acwarm - 에어컨 약하게 * /acmedium - 에어컨 중간 * /accool - 에어컨 세게 에어컨을 직접 제어하는 것은 언뜻 어려울 것 같기도 하지만 의외로 쉬운 방법이 있습니다. 에어컨 리모컨의 동작을 그대로 흉내내는 것입니다. 리모컨은 전자기기와 통신할 때 적외선을 이용합니다. 리모컨의 앞쪽을 보면 조그만한 LED머리가 튀어나와 있는 것을 볼 수 있는데 여기서 적외선이 발생합니다. 이 적외선 LED의 깜빡이는 패턴을 가지고 원하는 신호를 전달하게 됩니다. 아마 가시광선 LED로도 리모컨을 만들 수 있었겠지만 리모컨 버튼 누를때마다 빛이 깜빡인다면 좋아하는 사람은 아무도 없을 것 같습니다. 적외선 LED를 이용하면 에어컨 제어가 가능하다는 것을 알았으니 아래와 같이 전체적인 구조도를 그릴 수 있습니다. 가장 먼저해야 할 것은 회로도를 만드는 것입니다. 리모컨의 동작을 흉내내려면 버튼을 누를 때 어떤 신호가 나오는지 분석이 필요합니다. 이를 위해서 적외선 수신기가 필요합니다. 따라서 만들어야하는 회로는 적외선 발신기와 적외선 수신기 두 부분으로 나누어져 있습니다. 회로는 아래와 같습니다. ![](https://tech.buzzvil.com/blog/%ec%95%84%eb%a7%88%ec%a1%b4-%ec%97%90%ec%bd%94%eb%a5%bc-%ed%99%9c%ec%9a%a9%ed%95%9c-%ec%9d%8c%ec%84%b1-%ec%9d%b8%ec%8b%9d-%ec%97%90%ec%96%b4%ec%bb%a8-%ec%a0%9c%ec%96%b4/air-conditioner-schematic-1.png) 적외선 수신기로는 TSOP38238을 선택했습니다. 전원 공급을 위한 3V(+)핀과 GND(-)핀을 연결하고 수신 결과를 받기 위한 핀을 라즈베리파이의 GPIO와 연결합니다. GPIO는 General Purpose Input/Output의 약자로 전압값의 유무를 이용해 출력을 내보내거나 반대로 외부의 값을 읽어들이는 용도로 사용하는 핀입니다. 각각의 핀은 Input 또는 Output 기능중에 하나만 선택하여 사용할 수 있습니다. GPIO는 디지털 회로이기 때문에 0 또는 1만 인식할 수 있습니다. 라즈베리파이의 GPIO 전압은 3.3V 입니다. 따라서 전압이 0V이면 0이고 3.3V이면 1을 의미합니다. GPIO로 사용 가능한 핀이 어떤 것인지는 [pinout.xyz](https://pinout.xyz/) 에서 확인 가능합니다. 어떤 GPIO핀을 사용할지는 아무거나 마음대로 선택해도 됩니다. 이왕이면 그라운드(0V), 전원(3.3V, 5V)와 가까운 핀을 선택하는 것이 케이블 연결하기가 편합니다. 또 다른 팁은 다른 특수 기능이 함께 포함되어 있는 GPIO핀은 가능하면 사용하지 않는 것입니다. 주로 시리얼 통신을 지원하기 위한 기능이 많이 있는데 이런것들은 일반적인 기능이 아니기 때문에 모든 핀에 구현되어 있지 않고 특정한 몇몇 핀에만 구현되어 있습니다. 실제로 나중에 빛 감지 센서를 추가로 달려고 보니 이미 사용하던 핀과 충돌이 발생해서 옮기는 작업을 따로 하는 불상사가 발생하였습니다. 적외선 발신기로는 IR333C를 선택했습니다. 발신기 회로는 스위치가 달린 전구 회로와 비슷합니다. 차이점이 있다면 LED에 과전류가 공급되지 않도록 전류를 제한하기 위해 직렬로 68옴 저항을 달았고 물리적인 스위치 대신에 트랜지스터를 달았다는 것입니다. 트랜지스터를 이용하면 전자적으로 전류를 통과시키거나 차단시키는 것이 가능합니다. 라즈베리파이의 GPIO핀과 트랜지스터의 베이스 핀을 연결시켜 LED On/Off 제어가 가능하도록 했습니다. 어떤분은 GPIO에서 3.3V가 나오니 이 출력을 그대로 LED와 연결 시키면 트랜지스터가 없어도 LED 제어가 가능하지 않냐는 의문을 가질 수도 있을 것 같습니다. 하지만 GPIO는 단순히 신호를 전달하기위한 용도로 만들어져있어 LED가 필요로 하는 전력을 공급하기에는 충분하지 않게 설계되어 있습니다. 따라서 전력 공급을 위해 만들어놓은 5V, 3.3V 등의 핀을 이용해 LED에 전력공급을 하고 On/Off 제어는 트랜지스터를 이용해서 구현합니다. 회로에 아직 설명하지 않은 2.2k옴 저항(베이스 저항), 10k옴 저항(풀 다운 저항)이 있는데 이게 왜 필요하고 어느 정도의 저항값을 써야하는지에 대해서는 저의 능력이 부족하여 자세히 설명하기 쉽지 않은 것 같습니다. 덕분에 이 글을 쓰면서 다시 한 번 공부하는 시간을 가졌습니다. 회로를 보호하고 안정적으로 동작하게 하기 위해서 사용한다고 이해하면 좋을 것 같습니다. "LED", "BJT", "Mosfet", "Base resistor", "Gate resistor", "Pull-down resistor" 등의 키워드로 검색해보면 자료들을 찾을 수 있습니다. 트랜지스터가 mosfet이냐 BJT냐에 따라서 해당 저항이 필요한 이유가 조금씩 다르므로 구분해서 자료를 찾아보는 것이 좋습니다. 이 회로를 바탕으로 아래와 같이 빵판을 활용해 프로토타입 회로를 완성하였습니다. 빵판도 따로 구매를 해야하는데 인터넷에 "라즈베리파이 입출력 키트"로 찾아보시면 약 만 오천원 가량에 빵판과 커넥터, 점퍼케이블, 종류별 저항등을 한번에 구입할 수 있습니다. 이것저것 알아보기 귀찮으신 분들에게 딱 맞는 제품인 것 같습니다. ![](https://tech.buzzvil.com/blog/%ec%95%84%eb%a7%88%ec%a1%b4-%ec%97%90%ec%bd%94%eb%a5%bc-%ed%99%9c%ec%9a%a9%ed%95%9c-%ec%9d%8c%ec%84%b1-%ec%9d%b8%ec%8b%9d-%ec%97%90%ec%96%b4%ec%bb%a8-%ec%a0%9c%ec%96%b4/dsc04316-1.jpg) 적외선 LED는 종류마다 발신 각도에 차이가 있습니다. 발신각도가 좁을수록 LED가 정확하게 에어컨을 향하고 있어야만 신호가 제대로 전달이 됩니다. IR333C는 발신 각도가 40도 입니다. 에어컨이 천장에 달려 있기 때문에 회로가 완성이 되면 에어컨 아래에 위치한 책상에 둘 예정입니다. 하지만 완전히 고정되어 있지 않기 때문에 다른 사람들이 건드려서 위치가 옮겨지게 되면 동작을 하지 않을수도 있습니다. 조금 더 안정적인 동작을 하기 위해서는 발신각도가 더 넓은 LED를 구입하면 됩니다. 전자 부품을 구매하는데 매우 유용한 [**digikey**](https://www.digikey.com/products/en/optoelectronics/infrared-uv-visible-emitters/94)라는 사이트에서 검색을 해봤습니다. 필터조건을 다음과 같이 설정합니다. > Type = IR > Wavelength = 940nm > Viewing Angle >= 80도 > Mounting Type = Through Hole 이렇게 하니 발신각도 140도 짜리 SLED-56-16639 라는 제품을 찾을 수 있었습니다. 국내 사이트에 찾아보니 팔고 있습니다. 하지만 가격이 IR333C보다 많이 비싸서 시험삼아 하나만 구입해봤습니다. 회로를 완성했으니 이제 라즈베리파이에서 이 회로를 제어할 수 있도록 설정하는 것이 필요합니다. 고맙게도 리눅스에서는 LIRC(Linux Infrared Remote Control)라는 패키지가 존재해서 쉽게 적외선 제어와 관련된 프로그램을 만들 수 있도록 도와줍니다.   lirc는 아래 명령을 이용하면 설치가 가능합니다. ```shell sudo apt-get install lirc ``` mode2, irsend, irrecord등 적외선 제어에 필요한 커맨드들이 설치되고 lircd라는 데몬도 함께 설치됩니다. lircd는 유닉스 도메인 소켓통신을 이용해 적외선 신호를 주고받을 수 있게 해주는 데몬입니다. 프로그램에서는 소켓통신만 하면 뒷단은 알아서 처리해주는 고마운 프로그램입니다. 설치가 완료되고 나면 핀 설정을 해야합니다. 핀을 설정하기 위해 수정해야하는 파일은 다음과 같습니다. > /boot/config.txt ``` dtoverlay=lirc-rpi,gpio\_in\_pin=23,gpio\_out\_pin=21 ``` > /etc/modules ``` lirc_dev lirc_rpi gpio_in_pin=23 gpio_out_pin=21 ``` > /etc/lirc/hardware.conf ``` LIRCD_ARGS="--uinput" DRIVER="default" DEVICE="/dev/lirc0" MODULES="lirc_rpi" ``` 파일을 수정하고 나면 라즈베리파이를 재부팅해줍니다. 적외선 신호 캡쳐를 위해 아래와 같이 irrecord 커맨드를 사용했습니다. ```shell irrecord -d /dev/lirc0 lircd.conf ``` 이 커맨드를 실행하면 캡쳐하고 싶은 리모컨의 버튼을 누르면 적외선 수신기를 통해서 캡쳐한 결과를 lircd.conf 파일에 기록해줍니다. 캡쳐 과정은 크게 두 단계로 진행됩니다. 첫번째는 프로토콜을 파악하는 부분입니다. 앞서 정보를 적외선 깜빡임의 패턴을 통해서 전달한다고 했는데 같은 정보를 표현하기 위한 패턴도 여러 종류가 있을 수 있습니다. 이러한 프로토콜을 파악하기 위해 irrecord커맨드에서는 반복적으로 여러 종류의 버튼을 눌러달라고 합니다. 버튼을 수십번 누르다 보면 마침내 프로토콜 파악이 완료되고 실제로 원하는 버튼의 신호를 캡쳐하는 두 번째 단계가 나타납니다. 여기서 버튼의 이름을 입력하고 리모컨의 버튼을 누르면 lircd.conf파일에 프로토콜 명세와 각 버튼의 패킷값이 기록됩니다. lircd.conf 파일의 포맷은 아래와 같습니다. ``` begin remote name lg-ac bits 20 flags SPACE_ENC|CONST_LENGTH eps 30 aeps 100 header 3204 9803 one 575 1497 zero 575 462 ptrail 575 pre_data_bits 8 pre_data 0x88 gap 106773 toggle_bit_mask 0x0 begin codes BTN_0 0xC0051 0xFF312 # 끄기 BTN_1 0x00314 0xFF345 # 냉방/18도/1바람 BTN_2 0x00303 0xFF345 # 냉방/18도/2바람 BTN_3 0x00347 0xFF345 # 냉방/18도/4바람 end codes end remote ``` 윗 부분에는 flags, header, gap등 프로토콜에 대한 복잡한 설정값들이 명세되어 있고 뒷부분에 BTN\_0, BTN\_1 로 정의되어 있는 부분이 실제로 캡쳐한 버튼에 대한 패킷값입니다. 적외선 프로토콜을 공부하는것이 이번 프로젝트의 목적은 아니기 때문에 이 부분은 irrecord가 만들어준대로 그대로 쓰고 각 버튼의 패킷값을 분석하는데 집중하도록 하겠습니다. 리모컨과 관련해서 기억해야할 아주 중요한 특징이 하나 있습니다. 바로 단방향 통신이라는 것입니다. 리모컨은 에어컨으로 신호를 전달할 수는 있지만 반대로 에어컨의 정보를 받아올 수는 없습니다. 리모컨의 전원 On 버튼을 눌러보면 LCD에 현재 설정된 온도가 표시되어 있습니다. 실제로 여기에 표시되어 있는 값은 에어컨에서 받아온 값이 아닌 리모컨 자체가 보관하고 있던 값이 표시됩니다. 그렇다면 리모컨의 상태와 에어컨의 상태가 일치하지 않는 경우가 발생할 수 있다고 예상할 수 있습니다. 예를 들어, 에어컨 신호가 닿지 않는 곳에서 온도 올리는 버튼을 세번 누르고 다시 에어컨 신호가 닿는 곳에서 온도 올리는 버튼을 한번 누른다면 에어컨과 리모컨의 온도값이 3도가 차이날 수 있습니다. 이러한 문제를 해결하기 위해 리모컨의 프로토콜은 온도를 올려라는 "행위" 보다는 지금 세팅되어야할 온도값의 "상태"를 전송하는 형태로 설계되어 있습니다. 심지어 이 온도의 상태값은 온도 조절 버튼을 누를때만 가는게 아니라 전원 On 버튼을 눌렀을때도 전송이 됩니다. 전원을 켰는데 리모컨과 에어컨의 상태값이 다르면 안되기 때문입니다. 따라서 에어컨 리모컨의 프로토콜의 패킷 구조는 "누른 버튼의 코드 값" + "에어컨의 모든 상태값(온도, 바람 세기, 바람 방향 등)" 과 같이 되어 있다고 예상해볼 수 있습니다. LG 에어컨을 기준으로 캡쳐한 결과는 아래와 같습니다. ``` begin codes BTN_0 0xC0051 0xFF312 # 끄기 BTN_1 0x00314 0xFF345 # 냉방/18도/1바람 BTN_2 0x00303 0xFF345 # 냉방/18도/2바람 BTN_3 0x00347 0xFF345 # 냉방/18도/4바람 BTN_4 0x00617 0xFF345 # 냉방/21도/1바람 BTN_5 0x00606 0xFF345 # 냉방/21도/2바람 BTN_6 0x0064A 0xFF345 # 냉방/21도/4바람 BTN_7 0x0091A 0xFF345 # 냉방/24도/1바람 BTN_8 0x00909 0xFF345 # 냉방/24도/2바람 BTN_9 0x0094D 0xFF345 # 냉방/24도/4바람 BTN_10 0x00C1D 0xFF345 # 냉방/27도/1바람 BTN_11 0x00C0C 0xFF345 # 냉방/27도/2바람 BTN_12 0x00C50 0xFF345 # 냉방/27도/4바람 BTN_50 0x04B10 0xFF345 # 난방/26도/1바람 BTN_51 0x04B0F 0xFF345 # 난방/26도/2바람 BTN_52 0x04B43 0xFF345 # 난방/26도/4바람 BTN_53 0x04D12 0xFF345 # 난방/28도/1바람 BTN_54 0x04D01 0xFF345 # 난방/28도/2바람 BTN_55 0x04D45 0xFF345 # 난방/28도/4바람 BTN_56 0x04F14 0xFF345 # 난방/30도/1바람 BTN_57 0x04F03 0xFF345 # 난방/30도/2바람 BTN_58 0x04F47 0xFF345 # 난방/30도/4바람 end codes ``` 버튼은 끄기 버튼과 켜기 버튼 딱 두가지만 사용합니다. 켜기 버튼에서도 항상 온도 상태값이 전달된다는 것을 활용하면 온도 조절이 가능합니다. 패킷은 10자리로 구성되어 있는데 왼쪽 5자리는 에어컨의 상태값, 오른쪽 5자리는 누른 버튼의 코드값이라는 것을 알 수 있습니다. 끄기 버튼은 0xFF312, 켜기 버튼은 0xFF345 입니다. 그리고 왼쪽의 상태값에서 0x00303을 더하면 항상 3도씩 올라가는 것을 알 수 있습니다. 이렇게 몇 번 캡쳐를 하고 패턴을 보면 나머지 버튼들은 실제로 캡쳐를 해보지 않아도 어떤 값인지 알 수 있습니다. LG 리모컨의 캡쳐작업은 이렇게 쉽게 끝났습니다. 하지만, 삼성 리모컨은 어째서인지 irrecord를 이용해서 캡쳐시도를 해도 계속해서 오류가 나면서 캡쳐가 되지 않았습니다. 아무래도 표준적이지 않은 형태의 프로토콜을 사용하는 것이 아닌가 의심이 갔고 체크섬 로직이 있다는 이야기도 있어서 분석이 쉽지 않아보였습니다. 하지만 프로토콜이 아무리 복잡하다고 해도 결국에는 LED의 깜빡임으로 모두 표현이 되는 것이기 때문에 raw waveform을 캡쳐해서 그대로 내보낼 방법만 있으면 됩니다. 다행히 mode2라는 유틸리티가 있어 이를 이용하면 수신한 raw waveform을 lircd.conf 포맷으로 출력시킬 수 있습니다. 명령은 아래와 같습니다. ```shell sudo mode2 -m -d /dev/lirc0 > lirc.conf ``` 이렇게 해서 완성한 삼성 에어컨용 lircd.conf 파일은 아래와 같습니다. 파일이 너무 크기때문에 앞쪽에 일부분만 첨부했습니다. ``` begin remote name samsung-ac flags RAW_CODES eps 30 aeps 100 gap 8929 begin raw_codes name BTN_0 632 17684 3048 8887 541 444 552 1440 545 441 556 436 547 444 550 441 552 439 554 439 556 435 550 1438 551 440 554 436 557 1455 535 1452 545 443 551 1435 552 1429 548 1437 551 1432 556 1427 550 441 553 439 555 437 558 434 549 443 551 467 527 465 529 463 542 450 544 448 553 440 547 444 550 442 553 439 554 438 556 436 548 444 549 443 552 440 554 438 556 436 549 443 550 442 552 440 554 464 530 462 ... ``` LG 에어컨과는 다르게 버튼 하나 정의하는데 많은 정보가 필요한 것을 알 수 있습니다. 완성한 두개의 파일 내용을 /etc/lirc/lircd.conf 에 넣어두면 lircd 데몬이 시작할 때 읽어들여 명령어를 등록해놓게 됩니다. 각각의 에어컨은 lg-ac, samsung-ac 로 이름을 지정하였습니다. 파일 하나에 두 디바이스의 명령을 한번에 관리하려니 불편하여 파일을 두개로 쪼개고 싶었습니다. 그래서 lircd.conf 파일의 내용을 아래와 같이 변경하였습니다. ``` include "conf.d/samsung-ac.conf" include "conf.d/lg-ac.conf" ``` 그리고 conf.d 디렉토리를 만들고 이 안에 각각 모델에 대한 정의가 들어있는 파일을 넣어두니 원하는대로 동작하였습니다. 사실 include "conf.d/*" 처럼 glob pattern을 적용하고 싶었지만 어째서인지 동작하지 않아서 어쩔 수 없이 각각의 파일 이름을 지정하였습니다. 이제 irsend 명령을 이용하면 적외선 발신기를 제어할 수 있습니다. 예제 명령은 아래와 같습니다. ``` irsend SEND_ONCE lg-ac BTN_1 irsend SEND_ONCE samsung-ac BTN_2 ``` 이렇게해서 라즈베리파이의 쉘에서 irsend 커맨드를 이용해 에어컨을 마음대로 제어할 수 있게 되었습니다. 궁금하신 분들을 위해서 재료비를 공개합니다. * 라즈베리파이3 + 공식케이스 + 방열판 = 53,350원 * SD카드 = 4,410원 * 트랜지스터 = 700원 * 적외선 LED = 400원 * 저항 = 100원 * 만능기판 = 1,300원 * 만능기판 다리 = 100원 * 점퍼 케이블 = 300원 * **총 = 60,660원** 올해 초에 RASPBERRY PI ZERO W라는 제품이 나왔습니다. 라즈베리파이3에서 몇가지 기능을 빼서 싸게 만든 제품인데 해외에서 10불에 판매하고 있습니다. 한국에서는 이것저것 끼워서 비싸게 팔고 있네요. 이번 프로젝트에서 필요로 하는 기능은 다 갖추고 있기에 기회가 되면 구매해서 테스트해볼 예정입니다. 만능기판의 경우 PCB 재질이 여러 종류가 있는데 가장 저렴한 재질인 종이 페놀은 미관상 좋지 못해서 조금 더 예쁘고 튼튼한 에폭시 재질로 구매했습니다. 누런 색 기판이 페놀 재질이고 초록색 기판이 에폭시 재질입니다. 저항은 허용 전력에 따라서 크기가 다르게 나옵니다. 1/2W는 크기가 너무 크고 1/4W가 일반적으로 쓰기에 무난합니다. 더 작은 1/8W을 써도 됩니다. LED가 잠시 깜빡일때만 전류가 흐르므로 허용 전력은 충분합니다. 아래는 납땜을 위한 준비물 비용입니다. * 라즈베리파이 입출력 키트 = 14,600 * 테프론 와이어 AWG30 = 5,380원 * Kester 유연납 1.0mm /50g = 4,400원 * 멀티미터 FLUKE-101 = 43,500원 * HAKKO FX-888D 디지털인두기 = 143,550원 * 교체용인두팁 T18-K = 11,880원 * 니퍼/핀셋 = 집에 있는 것 * **총 = 223,310원** 납땜 준비물 구매하는데 많은 돈을 썼는데 저처럼 충동구매로 비싼 인두와 멀티미터를 사지만 않으면 싼 가격으로 장비들을 마련할 수 있습니다. 혹시나 회로가 동작하지 않을 때 문제를 빠르게 파악하기 위해서 멀티미터를 꼭 구입하는 것을 추천합니다. 고급 제품인 FLUKE사 모델이 비싸다면 VC99라는 모델이 가성비가 좋다고 합니다. 지금까지 에어컨 제어를 위한 회로 설계 및 LIRC 사용에 대해 알아봤습니다. 이후의 서버 네트워크 구축 및 아마존 에코/슬랙 연동과 배포 및 테스트 자동화에 대한 프로젝트의 자세한 정보는 [GitHub repository](https://github.com/Buzzvil/raspberry-pi-ac-controller)에 공개되어 있습니다. 긴 글 읽어주셔 감사합니다. ### References - [Setting Up LIRC on the RaspberryPi](http://alexba.in/blog/2013/01/06/setting-up-lirc-on-the-raspberrypi/) - [라즈베리파이로 에어컨 제어하기](http://pickykang.tistory.com/37) - [버섯돌이의 허큘렉스 다루기 \- IR 입문](http://www.icbanq.com/pbloger/board_View.aspx?number=657) --- ## [안드로이드 파편화(Fragmentation)에 대하여](https://tech.buzzvil.com/blog/tech-blog-안드로이드-android-파편화-fragmentation) Date: 2017-08-08 | Category: Frontend ### 1\.  글을 시작하며 저는 버즈빌에서 안드로이드 개발을 하고 있는 초보 개발자 Evan입니다. 버즈빌에서 개발을 시작한지 이제 1년정도 되었는데요, 작년 이맘때와 비교하면 회사의 훌륭한 개발자분들과 같이 일하면서 스스로 많이 성장했구나 라는 생각을 하면서도, 한편으로는 아직도 부족한 점이 많다고 느껴지기도 합니다. 1년여간의 안드로이드 개발 경험을 뒤돌아보면서 어려웠던 점들에 대해서 생각해보면 여러 가지가 있겠지만, 그중 하나를 꼽자면 파편화 문제가 아닐까 싶습니다. 그래서 스스로 정리도 할 겸 제가 맡고 있는 안드로이드의 파편화에 대해서 글을 남겨보고자 합니다. #### 파편화 안드로이드 파편화에 들어가기에 앞서, 파편화에 대해 간략히 설명드리겠습니다. 파편화란, 기기의 앱 구동 환경이 제각각 분열되어 있는 것을 이야기하는데, 크게 OS 측면의 파편화와, 하드웨어 측면의 파편화로 나눌 수 있습니다. 하드웨어와 OS 버전의 다양성이 많지않은 iOS에 비해 안드로이드는 파편화 정도가 심해서 여러 가지 문제를 발생시키고 있습니다.  #### OS 파편화 OS 파편화는 기기들이 수많은 OS 버전에서 동작할 수 있는 것을 이야기합니다. 아래의 그림은 구글 안드로이드 공식 사이트에서 제시하고 있는 통계 자료입니다. 2017년 7월 초 기준으로 해당 버전을 사용하는 기기의 비율을 나타내고 있는데요(0.1% 미만 제외), Gingerbread에서 Nougat까지 다양한 버전이 존재하는 것을 알 수 있습니다. 최근에는 Nougat 다음인 O 버전의 Preview가 나오고 있는 상황입니다. ![](https://tech.buzzvil.com/blog/tech-blog-%ec%95%88%eb%93%9c%eb%a1%9c%ec%9d%b4%eb%93%9c-android-%ed%8c%8c%ed%8e%b8%ed%99%94-fragmentation/screen-shot-2017-08-03-at-5.20.02-pm.png) ###### [_지정된 버전의 Android 플랫폼을 실행하는 기기 분포 _](https://developer.android.com/about/dashboards/index.html?hl=ko) 위와 같이 동일한 기기들이 다양한 OS 환경에서 동작할 수 있기 때문에, 한 가지 기능을 개발하더라도 이러한 OS 파편화로 문제가 발생할 수 있는 가능성을 항상 염두에 두어야 합니다. 대부분의 앱에서 minSdkVersion과 targetSdkVersion 설정을 통해 대응해야 하는 문제의 경우의 수를 줄이고 동작 안정성을 높이고 있지만, 최대한 많은 기능을 제공하기 위해서는 지원 가능한 버전의 범위를 넓히고 각 버전에 맞게 대응을 하며 개발해야 합니다. 또한, 새로 나오는 버전에 대해서도 어떤 API가 바뀌고 사라지는지 관심을 가지고 있어야 새 버전 출시시 기존에 잘 되던 기능이 동작하지 않는 문제를 예방할 수 있습니다. 간략히 OS 파편화에 따른 문제점을 살펴보았지만, 구글이 제공하는 문서에 맞게 개발한다면 예상하지 못한 버그가 생길 가능성은 낮기 때문에 OS 파편화는 그리 큰 문제가 아닐 수도 있습니다. 더 큰 문제는 하드웨어 파편화입니다. #### 하드웨어 파편화 OS 파편화보다 더 큰 문제는 수많은 제조사에 의해 수많은 종류의 기기가 나온다는 것입니다. 아래 그림은 Open Signal 사이트에서 2015년 제시한 기기 파편화에 대한 그림입니다. 사이트에 따르면, 최소 24,000종류가 넘는 기기가 세상에 존재한다고 합니다. ![](https://tech.buzzvil.com/blog/tech-blog-%ec%95%88%eb%93%9c%eb%a1%9c%ec%9d%b4%eb%93%9c-android-%ed%8c%8c%ed%8e%b8%ed%99%94-fragmentation/screen-shot-2017-08-09-at-3.11.50-pm.png) ###### _[기기 파편화 (Open Signal, 2015)](https://opensignal.com/reports/2015/08/android-fragmentation/)_ 안드로이드는 오픈 소스이기 때문에 수많은 제조사에서 안드로이드 기반으로 기기를 개발할 수 있고, 이에 따라 안드로이드 기기의 다양성은 점점 더 늘어나고 있는 추세입니다. 기기의 다양성으로 인한 문제 중 하나는 해상도에 관한 것입니다. 너무나 다양한 사이즈의 기기가 존재해서 UI 작업시에는 여러 크기의 화면에서 어떻게 보일지 항상 고민해야 합니다. 특히 그래픽이 중요한 게임 개발시에는 더욱더 해상도 문제가 개발 시간과 비용을 증가시키는 원인으로 작용하고 있습니다. 또한 제조사들은 개발시에 구글의 기본 가이드라인만 준수하면 자신들의 입맛에 맞게 변화가 가능하기 때문에 기능과 서비스, UI 등을 customize해서 제품을 출시하게 됩니다. 또한 같은 모델이라도 통신사에 따라 customize되어 출시되는 경우도 있습니다. 따라서 이러한 customize가 만들어내는 문제가 있을시, 특정 모델에서만 앱이 제대로 동작하지 않는 경우도 생기게 됩니다. 중요한 점은 기기가 너무 다양해서 이러한 문제는 사전에 알고 대처하기 힘들다는 것입니다. #### 파편화가 발생하는 이유 이러한 파편화의 원인에 대해서 좀 더 자세히 알아보겠습니다. 아래 그림은 안드로이드 기기의 제작 과정을 나타낸 것입니다. 아래와 같은 5가지 과정을 거치게 되는데요. 1. 안드로이드 팀이 오픈소스 코드를 릴리즈 2. 반도체 제조사에서 자신들의 하드웨어에 맞게 릴리즈된 버전 수정 3. 기기 제조사에서 출시할 기기에 맞게 릴리즈된 버전 수정 4. 기기 제조사와 통신사에 의한 검증 5. 실제 유저에게 릴리즈 ![](https://tech.buzzvil.com/blog/tech-blog-%ec%95%88%eb%93%9c%eb%a1%9c%ec%9d%b4%eb%93%9c-android-%ed%8c%8c%ed%8e%b8%ed%99%94-fragmentation/screen-shot-2017-08-03-at-5.59.12-pm.png) ###### [_안드로이드 기기 제작과정 _](https://android-developers.googleblog.com/2017/05/here-comes-treble-modular-base-for.html) 이렇게 구글의 업데이트에 맞추어 각 제조사별로 다시 기기에 맞는 customize를 해주게 되는데, 이 과정에서 하드웨어 파편화가 발생하게 되는 것입니다. 또한 이는 OS 파편화의 원인이 되기도 하는데, 새 버전이 릴리즈되었다고 해도 위 과정에 드는 시간이 상당히 오래 걸리기 때문에 제조사마다 새 OS버전 업데이트 가능 시점을 예측하기가 어렵기 때문입니다. #### 대응 그렇다면 이러한 파편화 문제에 대해서 어떻게 대응할 것인가에 대해서 생각해보려고 합니다. 스타트업에서는 파편화에 대한 대응을 완벽하게 할 수 있는 환경이 갖춰져 있지 않을 가능성이 높기 때문에, 대응에 드는 리소스를 최소화하는 방향으로 개발하는 것이 바람직할 것입니다. 이는 버즈빌도 마찬가지입니다. 버즈빌에서도 테스트 담당자가 따로 있지 않기 때문에, QA가 필요할 시에는 해당 서비스와 관련된 개발팀, 운영팀 인원이 함께 테스트하는 방식을 취하고 있습니다. 테스트 기기도 마찬가지입니다. 실제 테스트를 모든 기기에서 할 수는 없으므로, 버즈빌에서는 서비스를 제공하는  국가에서 주로 사용하는 기기를 파악한 뒤 대표적인 모델들을 중심으로 테스트를 진행하고 있습니다. 또, 가능하다면 최대한 버전분기를 하지 않는 방향으로 개발하는 것도 하나의 방법입니다. 버전분기가 들어갈수록 해당 기능이 원하는 방식대로 동작할지 알아보기가 힘들고, 유지보수비용도 증가하기 때문입니다. 새로운 버전, 새로운 기기에 대한 호환성 체크도 필요합니다. 위에서도 잠깐 언급했었지만, 새로 릴리즈될 버전에서 어떤 기능이 추가,제거,변경되었는지 조사하고 수정해야 할 기능이 없는지 확인하는 것은 필수입니다. 안드로이드 O 버전에서도 많은 변화가 있을 예정인데, 암시적 브로드캐스트에 대한 제한이 추가됨에 따라 기존에 이 기능을 사용하던 부분에 대한 수정을 해야 하는 것도 한 가지 예입니다. 특이한 스펙의 기기가 나왔을 경우에도 마찬가지입니다. 얼마 전 갤럭시 S8과 같이 가로:세로 비율이 1:2에 가까운 기기들이 출시되었는데 이에 따른 UI상의 예외처리도 한 가지 예라고 할 수 있습니다. 구글에서도 당연히 이러한 파편화 문제를 인지하고 있고, 해결하기 위해 여러 방안을 내놓고 있습니다. 넥서스 등의 레퍼런스폰을 지속적으로 출시하고, 안드로이드의 새로운 버전을 1년 주기로 정하고, 핵심 기능들의 업데이트는 플레이 스토어를 통해서 가능하도록 하여 제조사의 커스터마이징 부담을 줄여주었습니다. 또한 Android O부터는 [**Project Treble**](https://source.android.com/devices/architecture/treble)을 통해 제조사가 더 쉽고 빠르게 OS 업데이트에 대응할 수 있도록 지원하게 되었습니다. 따라서 제조사와 개발자가 가지고 있는 안드로이드 파편화에 대한 부담은 점점 줄어드는 추세로 나아가고 있습니다. ### **2\. 마무리하며** 안드로이드 파편화 문제는 안드로이드 초창기부터 꾸준히 제기되었던 문제이고, 여러 이해관계자들이 이 문제를 해결하기 위해 노력한 결과 현재는 좀더 나은 환경에서 안드로이드 개발을 할 수 있게 되었습니다. 하지만 지금도 파편화는 개발시에 고려해야 할 중요한 문제 중 하나입니다. 다양한 환경에서 안정적이고 일관되게 구동하는 안드로이드 앱을 만들기 위해서는 항상 이 문제를 신경써야 할 것입니다. 기존 버전과의 호환성을 고려하고, 새로운 환경에서도 잘 동작할 수 있는 서비스를 만들기 위해서 저도 공부를 게을리 하지 않아야겠습니다. --- ## [오픈소스를 쇼핑하는 엔지니어](https://tech.buzzvil.com/blog/tech-blog-오픈소스를-쇼핑하는-엔지니어) Date: 2017-05-12 | Category: Backend ### 1\.  글을 시작하며 오픈소스를 많이 사용하게 되는 스타트업 엔지니어는 항상 고민을 합니다. 쇼핑을 할 때 가격을 비교하고 사용기를 읽어보는 것처럼, 오픈소스를 선택할 때에도 자신의 기준에 맞춰서 여러가지 비교를 해보고 다른 분들의 사용기를 참고하기도 하구요. 가끔은 쇼핑중독처럼 어떤 오픈소스가 좋은지 비교하는데서 즐거움을 느끼기도 합니다(?). 허니스크린 서버를 개발하기 시작했을 때에도 당연스레 많은 고민을 했었고, 그 와중에 좋은 참고가 됐던 자료는 인스타그램의 서버구조에 관한 글이었습니다. 인스타그램에서는 아래의 세가지 원칙으로 시스템을 선택했습니다. ##### \- 아주 간단하게 유지하라 ##### \- 바퀴를 재 발명하지 마라 ##### \- 가능하면 증명되고 안정된 기술을 사용하라 인스타그램의 천만명이 넘는 서비스를 단 3명의 엔지니어가 유지하고 있었다는 사실에 놀라지 않을 수 없었습니다. 스타트업의 특성상 한정된 자원으로 퍼포먼스를 내야하는 제약이 따르기 때문에 이러한 접근 방법은 정말 유용하다는 생각이 들기도 하구요. 위의 세 가지 원칙은 결국에는 하나의 의미로 해석할 수 있습니다. **시스템을 최대한 간단하게 유지하는 것(최소한의 커스터마이징).** 이를 위해서는 이미 존재하는 기술을 잘 가져다 써야 합니다. 그리고, 가져다 쓸 기술을 선택할 때에는 증명되고 안정된 것이면 좋다는 것입니다. 제가 궁금했던 점은 그러한 바퀴를 어떻게 발굴해서 잘 장착했느냐 였는데요. 바퀴를 가져다 쓰려면 이 바퀴가 썩은 바퀴인지 튼튼하고 용도에 맞는 바퀴인지 구별을 해야하는데 말처럼 쉬운 일은 아니었습니다. 더불어, 가장 첫 순간에 바퀴를 잘 선택하는 것도 매우 중요합니다. 이미 자동차가 출발해서 달리는 중에는 다른 바퀴로 갈아끼우기가 매우 힘들기 때문입니다. 서비스를 시작하고나서 서비스의 중단 없이 뭔가를 바꾸는 것이 시스템을 운영하는데 있어서 매우 어려운 부분 중에 하나라는 건 모두가 공감하실 점이라 생각합니다.  **그렇다면 좋은 바퀴는 어떻게 찾아내야 할까요?** 주변에 물어볼 사람이 있다면 좋겠지만 대부분은 인터넷 검색을 통해서 찾아냅니다. 무지한 개발자에게 항상 한 줄기 빛이되어주는 구글에게 감사한 마음을 가지며 제가 바퀴를 고르는 기준에 대해서 소개 해드리고자 합니다.  #### **검색 키워드의 선정** 검색에 있어서 가장 중요한 것은 키워드의 선정입니다. 검색 키워드의 중요성에 대해서 처음으로 느꼈던 때는 학부생 시절 특허수업에서였습니다. 하루는 특허관련 회사에서 일하시는 외부 강사를 초청해서 수업을 들었는데, 선행 특허를 찾아내기 위한 노하우를 알려주시면서 이리저리 다양한 키워드를 조합하여 검색하는 모습이 매우 인상이 깊었습니다. **검색에 있어서 가장 중요한 것은 키워드의 선정**이라는 어찌보면 당연한 사실을 깨닫게 되는 계기였습니다. 만약 비슷한 기능을 하는 A와 B라는 라이브러리가 있다고 가정할 때, 처음에 시도해보는 몇 가지 키워드를 아래와 같이 추려보았습니다.\[blockquote\]- A B - A vs B - A B 비교 - A B which is better - A alternative - Why A sucks - Why moved from A - Switch from A \[/blockquote\]유치해 보이지만 의외로 vs로 검색해보면 좋은 비교자료가 많이 나옵니다. 그리고 alternative와 같은 단어를 활용하면 A와 비슷한 기능을 하는 새로운 B를 찾는데 도움이 됩니다. 검색을 통해서 이미 알고 있는 후보군들을 비교하는 것은 물론 알지 못했던 후보를 발견하기도 합니다. 검색을 잘 하려면 어휘력이 좋아야 합니다. 어느 키워드로 검색하면 내가 원하는 답을 쉽게 찾을 수 있을까 항상 고민해야 합니다. #### **치명적인 단점은 없는지** 위의 검색 키워드 중에 "Why A sucks"에 해당하는 이야기입니다. 예를 들면, 주변에 핸드폰을 선택 할 때 아이폰은 다 좋은데 배터리 분리가 안되서 안 쓴다는 사람이 많습니다. 선택하는데 있어서 단점이 중요한 역할을 한 사례입니다. 대부분 어떤 오픈소스를 소개하는 페이지에 가보면 좋은점만 나열되어있습니다. 따라서 단점에 대해서 따로 파악할 필요가 있습니다. 원하는 기능이 있어서 기술을 도입했는데 알고보니 단점이 용납할 수 없는 것이었다면 나중에 후회하게 됩니다. 그리고 단점을 찾다보면 주의해야할 사항들에 대해서도 잘 알 수 있게됩니다. 예를들면 레디스 같은 경우 성능은 막강하지만 싱글스레드로 동작하고 데이터가 유실될 가능성에 대해 인지를 하고 있어야 합니다. 그리고 리플리케이션 구성에서 장애발생시 fail-over를 잘못했다가 데이터를 날려먹는 경우가 종종 있습니다. 강대명님의 "레디스, 잘못쓰면 망한다"에 잘 나와 있는 내용이네요. 실제로 장애로 일부 데이터가 소실된 [트윌로의 사례](http://www.twilio.com/blog/2013/07/billing-incident-post-mortem-breakdown-analysis-and-root-cause.html)도 있습니다. #### **활발히 개발중인지** 소스 레파지토리의 커밋 로그등을 확인해서 계속 유지보수가 되고 있는지를 체크합니다. 예를들어, 마지막 커밋이 6개월 이상 됐다면 죽은 프로젝트라고 볼 수 있습니다. 당장은 괜찮을지 몰라도 나중에 버그가 발견되거나 다른 라이브러리를 버전업 하는데 있어서 호환성 문제로 걸림돌이 될 수 있습니다. 아주 간단한 라이브러리(JSON serializer 같은)들은 유지보수 할 것이 많지 않고 다른 것으로 갈아 타기도 쉬우니 예외입니다. 지금은 유지보수가 되고 있는데 조만간 문 닫을 가능성도 있습니다. 사실 이정도까지 파악하려면 감으로 할 수 밖에 없습니다. 소스 커미터들의 프로필, 레파지토리의 분위기(?)등 알아서 잘 판단합니다. 개인이 만들어서 혼자 운영하는 작은 라이브러리들은 유지보수가 잘 안될 가능성이 높습니다. #### **버전이 1.0 이상인지** 하지만, 무조건 1.0 이상이라고 안전한 것은 아니고 반대로 1.0 미만이라고 불완전한 것도 아닙니다. 하지만 1.0 버전이 나왔다면 그만큼 실 환경에서 써도 된다고 저자가 자신감을 표현했다는 것으로 이해하면 좋을 것 같습니다. 0점대 버전임에도 프로덕션 환경에서 많이 사용하는 오픈소스의 예로는 Node.js, SQLAlchemy가 있습니다. 다만 나중에 1.0으로 버전업이 되면서 하위호환성이 보장 안 될 수는 있습니다. 버전 넘버와 함께 라이브러리가 얼마나 오래된 것인지도 함께 보고 판단 합니다. SQLAlchemy 같은 경우 거의 10년간 0점대 버전을 사용하고 있었습니다. 버전과 관련된 한가지 팁이 있는데 만약 마지막 두 개의 릴리즈가 3.9.28, 4.0.0 이라면 저는 최신버전을 사용 안하고 3.9.28버전을 사용합니다. 버전이 4.0.0이 됐다는 것은 큰 변화가 있었다는 뜻이고 버그가 있을 가능성도 매우 높습니다. 갓 나온 최신버전 보다는 안정된 버전을 선택하는 것이 좋습니다. 메이저 버전이 나온지 6개월정도는 지나야 어느정도 안심하고 사용할 수 있는 것 같습니다. GitHub의 issues 페이지도 한 번 둘러보면 특별한 문제가 없는지 확인하는데 도움이 됩니다. #### **레퍼런스가 충분한지** 잘 알려진 곳에서 사용하는것이 확인되면 우선 그 라이브러리는 어느정도 검증이 되었다는 뜻입니다. 허니스크린의 경우 웹프레임워크로 Django와 Flask를 검토 했었는데 Django를 선택한 큰 이유중에 하나가 바로 레퍼런스의 유무였습니다. Django는 Disqus, Instagram, Pinterest, Eventbrite 등 많은 곳에서 성공적으로 사용하고 있는 반면 Flask는 딱히 어디서 사용중이라는 정보를 얻을수가 없었습니다. Flask도 많은 사람들이 사용하고 있지만 Django만큼의 대형 사이트에서의 레퍼런스가 찾기 힘들었습니다. (물론 Flask가 좋지 않다는 이야기는 절대 아니며 위의 내용은 2013년에 비교를 했을 당시 기준이었습니다.) #### **구글 트렌드 검색** 이래도 A와 B중에 선택을 못하겠다고 하면 [구글 트렌드](http://www.google.com/trends/)에 가서 A와 B로 구글에서 검색되는 빈도를 확인해봅니다. 추이를 그래프로 볼 수 있기 때문에 선택 기준의 객관적인 자료로 활용할 수 있습니다. ### **2\. 마무리하며** **어떤 오픈소스를 사용할 것인가**에 대한 호불호는 갈릴 수 있습니다. 좋은 오픈소스를 잘 고른다고해서 좋은 개발자라고 말할수도 없습니다. 하지만 사용하는데 문제가 있을 만한 오픈소스를 가려낼 필요는 있습니다. 프로덕션 환경에서는 적어도 예상치 못한 문제를 일으킬 확률을 줄이고 나아가서 안정적이고 빠른 속도로 개발해 나갈 수 있도록 처음의 선택을 잘 하면 좋을 것 같습니다. 글을 쓰다보니 남이 만들어놓은 오픈소스를 가져다 쓰기만 하는 것 같아서 죄책감이 드네요. 언젠간 저도 오픈소스에 기여하는 날이 올 수 있도록 노력해야겠습니다. --- ## [효과적인 LTV 활용기](https://tech.buzzvil.com/blog/tech-industry-효과적인-ltv-활용기) Date: 2017-04-18 | Category: Data & ML 안녕하세요. 모바일 잠금화면 애드네트워크 버즈빌의 Business Intelligence Manager, Elia입니다. 저희 버즈빌에서는 LTV(Lifetime Value)를 많은 곳에 사용하고 있습니다. LTV를 이용한 UA(User Acquisition)채널 관리도 그 중 하나입니다. 이번 포스팅에서는 버즈빌이 그동안 LTV를 구하기까지 겪었던 시행착오와 함께 LTV 계산의 효과적인 응용방법에 대해서 소개해드리고자 합니다.  ![](https://tech.buzzvil.com/blog/tech-industry-%ed%9a%a8%ea%b3%bc%ec%a0%81%ec%9d%b8-ltv-%ed%99%9c%ec%9a%a9%ea%b8%b0/screen-shot-2017-04-18-at-3.24.00-pm.png) #### **마케팅은 비용이다? 투자다!?** 모바일 광고 인더스트리에서는 공통적으로 중요시 여기는 지표들이 있습니다. NRU(New Registered User), DAU(Daily Active User), 그리고 retention rate은 모바일 광고 플레이어라면, 그리고 특히 마케터라면, 하나라도 놓쳐서는 안되는 필수적인 지표들입니다. 여러 채널들을 통해서 유입되는 NRU를 관리하고, 이렇게 유입된 유저들의 retention rate을 관찰하면, 그 비지니스의 DAU가 결정되게 됩니다. 일반적인 마케터라면 NRU와 retention rate 관리에 필요한 비용은 어느정도 감을 잡고 있을 것입니다 (그렇지 않으면 안됩니다). 여기까지를 잘 해내기만 해도 준수한 마케터라고 할 수 있겠지만, 이것만으로 최고의 마케터가 될 수는 없습니다. 왜냐하면 사업 전체적인 관점에서 봤을 때 보다 중요한 질문이 있기 때문입니다. 마케팅 분야의 저명한 학술지 Journal of Marketing Research의 지난 수십년간의 논문에 대해서, 텍스트 마이닝을 통해 가장 빈번한 키워드가 무엇이었는지를 발표한 흥미로운 논문이 있습니다. 그 중에서 4위를 차지한 키워드가 바로 **‘measurement’**입니다. 이는 마케터의 영원한 딜레마에 대해서 학자들이 얼마나 많이 연구했는지를 보여주는 부분입니다. 그 딜레마라 함은 바로 **“그래서 그거 돈 돼?”** 라는 CEO의 질문입니다. 마케터가 Facebook posting으로 10만 share를 만들어내고, Super Bowl 광고를 통해서 트위터가 본인의 회사의 이름으로 도배가 되어도 CEO의 질문은 본질적으로 변하지 않습니다. “그래서 그거 돈 돼?” 입니다. **여기서 중요하게 생각해야할 점이 마케팅 활동도 전사적 입장에서 보면 단지 비용이 아니라 투자라는 점입니다.** 모든 투자는 ROI를 따져서 그 적합성과 성과를 따지고, 마케팅 활동 역시 ROI를 살펴봐야 할 대상입니다. #### **LTV 계산은 ROI 분석의 첫발** 마케터의 노력의 산물인 NRU와 retention rate을 ROI의 관점에서 분석할 수 있게 해주는 metric이 바로 LTV 입니다. Lifetime Value의 줄임말인 LTV의 중요성은 이미 예전부터 인식되어 왔고 기업들의 LTV관리를 위한 노력은 주위에서 쉽게 찾을 수 있습니다. 커피 전문점에서 10개를 사면 1개를 무료로 주는 프로그램이 그 전형적인 예입니다. 그들은 왜 그런 프로그램을, 얼핏보면 손해라고 느껴질수도 있는 프로그램을 운영하는 것일까요? 첫번째 해답은 retention rate에 있고, 보다 궁극적인 답으로는 LTV에 있다고 할 것입니다. 커피 전문점의 예를 들면, 쿠폰을 찍게 함으로써 다음 구매에 대한 확률을 높일 수 있습니다. 여기서 다음 구매에 대한 확률, 고객이 다시 이 커피 전문점을 방문해서 구매할 확률이 retention rate이라고 볼 수 있습니다. 그렇지만 단지 우리 매장과 서비스를 다시 방문하는 것만이 중요한 것이 아니라, 비지니스의 보다 원초적인 질문은 “그 고객의 재방문이 우리의 사업에 도움이 되느냐?” 입니다. 물론 Profit을 무시한채 revenue를 쫓아야 할 때도 있지만, 사업의 기본은 뭐니뭐니해도 profit입니다. 만약 커피를 판매하는데서 오는 비용이 수익보다 많다면, 고객이 다시 우리 매장을 찾으면 오히려 손해가 커지는 구조일테니, retention이라는 것 자체가 의미가 없어지게 됩니다. 그렇기 때문에 마케터가 봐야할 궁극의 지표중에 하나로 저는 LTV가 있다고 생각합니다. ![](https://tech.buzzvil.com/blog/tech-industry-%ed%9a%a8%ea%b3%bc%ec%a0%81%ec%9d%b8-ltv-%ed%99%9c%ec%9a%a9%ea%b8%b0/cost-benefit-balance.jpg) #### **LTV 계산의 시행착오** 버즈빌에서는 LTV를 계산하려는 시도를 사업 초창기부터 해왔습니다. LTV 계산에 대해서 잘 정리되어있는 [“조성문의 블로그: 고객 생애 가치(Customer Lifetime Value) 이해하기](https://sungmooncho.com/2011/11/21/customer-lifetime-value/)"를 참고하여 LTV 계산의 첫발을 내딛었습니다. 해당 포스팅에서는 LTV를 아래와 같이 정의합니다. ![](https://tech.buzzvil.com/blog/tech-industry-%ed%9a%a8%ea%b3%bc%ec%a0%81%ec%9d%b8-ltv-%ed%99%9c%ec%9a%a9%ea%b8%b0/screen-shot-2017-04-18-at-5.01.03-pm.png)위와 같은 정의에 따라 LTV를 계산하려던 저희의 시도는 안타깝게도 크게 의미있는 결과를 만들어내지 못했습니다. 그 이유는 크게 두가지입니다. 1. Data가 부족하다. 2. Business cycle상 LTV를 계산하기에는 시기상조이다. 위의 식대로 LTV를 계산하려면 고객 1인당 평균 매출, retention rate, acquisition cost가 어느정도 정확하게 측정이 되어야 합니다. 그러나 모바일 광고시장의 복잡성으로 인해, 유저 한 명이 창출하는 매출이 어느 정도인지 평균적으로 측정한다는 것이 쉽지 않았습니다. 광고 매출은 임프레션당 발생하는 매출, 클릭당 발생하는 매출, 인스톨이나 실행과 같은 특정 조건이 만족되었을 때 발생하는 매출 등으로 다양하기 때문에 총 매출을 유저의 수로 나누는 것은 너무 많은 정보를 놓쳤습니다. 그렇다고 따로따로 하나씩 측정하기에는 유저들의 행동을 정확히 판단할만한 데이터가 부족하였습니다. 데이터의 부족을 실감하고 난 뒤 언제 어디서 무슨 데이터를 수집할 것이고, 수집된 데이터를 어떤 형태로 보관하고 어떤 형태로 이용할 수 있게 할 지를 매우 꼼꼼하게 점검한 뒤 실행에 옮겼습니다. 또한, retention rate과 같은 지표는 어느정도의 시간이 지나야 안정적인 데이터가 나오기도 합니다. Business cycle상 초창기에는 매출, retention rate, 그리고 acquisition cost 모두 불안정하기에 적정한 값을 추정해서 식에 대입하기가 힘듭니다. 사업의 특성상 지표들이 안정되기까지 필요한 시간이 다 다르겠지만 버즈빌의 경우엔 최소 1년은 필요했습니다. #### **Buzzvil의 LTV 계산법** 그렇게 시간이 흘러서 지표가 안정되고 데이터가 쌓이면서 LTV를 계산할 수 있게 되었습니다. LTV는 국가별로 계산할수도 있고 전체의 평균을 계산할 수도 있지만, 버즈빌에서는 가능한 한 최대로 작은 단위로 계산을 하려고 꾸준히 노력했고 결국 개인별 LTV까지 계산할 수 있게 되었습니다. 먼저 버즈빌의 LTV 계산법을 살펴보고 그렇게 산출된 LTV를 활용한 마케팅 운용의 예를 살펴보겠습니다. 우선 사용된 LTV의 공식은, ![](https://tech.buzzvil.com/blog/tech-industry-%ed%9a%a8%ea%b3%bc%ec%a0%81%ec%9d%b8-ltv-%ed%99%9c%ec%9a%a9%ea%b8%b0/screen-shot-2017-04-18-at-4.59.28-pm.png)로 간단히 표현할 수 있습니다. 위 공식은 간단한 한 줄이지만 많은 내용을 내포하고 있습니다. 먼저 시간단위 t는 retention rate과 관계가 깊습니다. User i 의 retention rate이 낮으면 t의 범위가 하루, 이틀로 줄어들 것이고, 반대로 retention rate이 높으면 t의 범위가 1년, 2년을 넘어 길어질 것입니다. 매 t당 revenue가 cost보다 크다는 전제 하라면, retention rate의 증가는 LTV 증가에 필수적인 요소가 될 것입니다. Revenue와 cost 모두 i와 t에 따라 값이 다르다는 부분도 중요한 부분입니다. 위에서도 말했듯, 마케팅의 큰 관심 중에 하나가 바로 measurement입니다. Measurement가 제대로 선행되지 않는다면 user i가 time t에 만들어낸 revenue와 cost를 추적하기가 쉽지 않습니다. 레스토랑을 한 번 예로 들어보겠습니다. 고객의 LTV를 측정하려면 특정한 고객의 방문 히스토리가 기록되어 있어야 하는데, 대부분의 레스토랑엔 그런 기능이 존재하지 않습니다. 그렇기 때문에 로얄티 프로그램을 만들어서 고객의 히스토리를 정확히 기록하고자 하는 것입니다. 그렇게 되면 특정 고객이 어느 때에 레스토랑을 방문해서 무엇을 주문하고 얼마를 냈는지, 그리고 그 때에 그 음식의 마진이 어느정도였는지가 기록되어있다면 LTV를 계산할 수 있을 것입니다. 데이터의 양이 무궁무진해진 온라인 광고 시장에서는 고객의 아주 사소한 행동까지도 데이터로 추적이 가능하기 때문에, 위와 같은 식의 LTV 계산이 가능해집니다. 시간 t는 서비스의 종류에 따라 보다 세밀하게 시간 단위로 나눌 수도, 아니면 더 큰 단위로 나눌 수도 있을 것입니다. #### **LTV를 이용한 채널관리** LTV의 계산이 이뤄졌다면 이제 이 계산된 LTV를 어디에 활용할 수 있을지 생각해봐야 합니다. LTV는 그 자체만으로 매우 방대한 양의 정보를 담고 있기에 많은 부분에 사용될 수 있습니다. 시간의 흐름에 따라 어느 시점에 LTV의 감소가 가장 큰지를 파악해서 마케팅 이벤트를 통해 수익을 증대시킬 수도 있고, 유저별 revenue - cost 값을 살펴보고 세그멘테이션을 통한 수익구조 개선도 생각해볼 수 있습니다. 버즈빌에서 LTV를 이용하며 큰 수익구조 개선이 있었던 부분은 user acquisition 부분입니다. 게임 앱 시장 등에서 활발히 이뤄지고 있는 influencer 마케팅이 그 좋은 예가 될 수 있습니다. 각 채널별로 보유하고 있는 influencer들이 다르고, 각각의 influencer들이 앱이나 서비스를 홍보하는 방식도 차이가 있습니다. 그렇기 때문에 채널별로 유입되는 유저들의 수익성에 많은 차이가 있을 수 있습니다. 예를 들어서 “어떻게든 설치만 하고 바로 지워도 됩니다"라는 메세지를 보내는 influencer가 있고, “정말 좋은 앱이니까 설치하고 한 번 써보시면 후회하지 않습니다"라는 메세지를 보내는 influencer가 있다고 하면, 두 채널을 통해서 유입된 유저는 retention rate 등에서 차이가 나게 됩니다. 전자의 메세지를 전달하는 채널이 acquisition cost가 유의미하게 저렴하다면 retention rate이 낮음에도 불구하고 더 좋은 수익성을 보일 수 있고, 반대로 cost가 비슷하다면 retention rate이 수익성을 가르는 핵심 지표가 될 수 있습니다. 하지만 retention rate 말고도 유저의 활동성도 수익에 큰 영향을 끼치기에 retention rate 하나만으로 채널의 좋고 나쁨을 판단하는 것은 위험합니다. 그렇기 때문에 LTV를 고려하면 자동적으로 retention rate과 활동성이 반영되는 지표가 만들어지고, 이 지표를 acquisition cost와 비교하면 수익성을 판단하는 일이 매우 간단한 일이 됩니다. #### **마치며..** 모바일 광고 시장에서 데이터를 살펴보는 일을 하다보면, 똑같은 한 명의 유저라도 그 유저의 행동에 따라 상당히 많은 수익성 차이가 있을 수 있는 것을 알 수 있습니다. 활동성이 낮은 유저와 높은 유저는 CTR에서부터 impression까지 많은 차이가 있습니다. 버즈빌은 위와 같은 LTV계산을 통해서 기본적으로 활동성이 우수한 알찬 DAU를 구성할 수 있었습니다. 이렇게 구성된 DAU를 가지고 타게팅 고도화 등의 작업을 통해서 보다 알맞은 광고를 유저들에게 연결시켜주기 위해 노력하고 있습니다. 이 글이 독자 여러분들이 좋은 마케터가 되는 데에, 그리고 여러분의 회사가 더 좋은 수익성을 갖게 되는 데에 조금이나마 도움이 될 수 있으면 좋겠습니다 :) --- ## [개발자의 입장에서 본 버즈빌의 개발 문화: 애자일 소프트웨어 개발](https://tech.buzzvil.com/blog/tech-blog-개발자의-입장에서-본-버즈빌의-개발-문화-애자일) Date: 2017-03-31 | Category: Culture ### 1\.  글을 시작하며 개발자가 가장 행복할 때는 개발에 집중할 수 있을 때입니다. 화창한 아침 기분 좋게 출근해서 어제 작업하던 화면을 하나씩 불러옵니다. 모니터에 코드가 적인 편집툴이 하나씩 올라오고 머리속에도 해당 기능개발을 위한 자료구조와 알고리즘이 로드 됩니다. 이 데이터는 이쪽으로 보내고 이 부분은 이렇게 로직을 짜봐야지. 집중해서 코드를 짜고 있으면 어느새 모니터 외에는 아무것도 보이지 않습니다. 누군가 어깨를 톡톡 노크합니다. “점심 먹으러 가요” 많은 개발자가 이러한 순간을 행복해 합니다. 집중해서 일할 때의 엔돌핀은 초콜릿 다섯개를 한꺼번에 입에 넣을 때 보다 더 샘솟습니다. 상황을 좀 바꾸어 볼까요. 집중해서 코드를 짜려는 그때, 누군가가 어깨를 톡톡 노크합니다. ‘회의합시다’. ‘요구사항 명세서는 다 작성하셨나요?’. ‘새로 개발해야 하는 기능이 있는데 언제까지 가능한가요? 사실, 다음주까지 완료해야 합니다만'. ‘저번에 말한 그 기능에서 이것 좀 추가해줄 수 있어요?’. ‘아아…’ 어깨를 톡톡 노크한 그분의 심정을 이해 못 하는 것은 아닙니다. 상황이 어쩔 수 없다는 것도 이해 못 하는 것은 아닙니다. 소프트웨어가 가진 본래적 특성에 따라 그 복잡성을 다루는 것이 쉽지가 않기에, 개발자가 행복하게 개발할 수 있는 상황을 만들기가 쉽지는 않습니다. 하지만, 계속해서 이렇게 힘들게 살아야 하나요? 아마도 아닐 것 같습니다. 그동안 많은 분들이 같은 문제를 겪어왔고 상황을 타개하고자 노력해왔습니다. 많은 개발 방법론과 도구가 등장하였고 개발 패러다임 또한 끊임없이 진화해 왔습니다. 자, 그렇다면 우리도 우리 선조들이 고생해서 일구어 놓은 유산을 통해서 좀 더 나은 환경을 만들 수 있겠죠? ### **2\. 배경: 애자일 소프트웨어 개발** 개발자 혹은 관련 분야 종사자라면 한번쯤 이 쿨내 진동하는 단어를 들어봤을 겁니다. ‘기민한' 소프트웨어 개발이라니 이 얼마나 멋집니까. 하지만 사실 이 쿨한 이름과 달리 애자일에 대한 이야기는 꽤 오래 되었습니다. 어떻게 하면 이 지긋지긋한 소프트웨어의 복잡성을 덜어낼 수 있을까 하는 고민들이 각기 다른 이름으로 하나씩 정리되어 오다가 2001년에 켄트 벡, 마틴 파울러, 로버트 마틴, 제프 서덜런드 등 이름만 들어도 쟁쟁한 리더분들끼리 한 스키장 리조트에서 모여 애자일 선언문을 작성합니다. > **Agile Manifesto** (참고 자료: [애자일 선언문](http://agilemanifesto.org/)) > > 우리는 소프트웨어를 개발하고, 또 다른 사람의 개발을 도와주면서 소프트웨어 개발의 더 나은 방법들을 찾아가고 있다. 이 작업을 통해 우리는 다음을 가치 있게 여기게 되었다: > > 공정과 도구보다 개인과 상호작용을 포괄적인 문서보다 작동하는 소프트웨어를 계약 협상보다 고객과의 협력을 계획을 따르기보다 변화에 대응하기를 가치 있게 여긴다. > > 이 말은, 왼쪽에 있는 것들도 가치가 있지만, 우리는 오른쪽에 있는 것들에 더 높은 가치를 둔다는 것이다. ‘애자일' 이라는 이름에서도 알 수 있듯이 애자일 선언문은 속도에 집중합니다. 다만, 속도에만 집중하는 것이 아닌 일을 되게 만드는 속도에 집중합니다. 간단히 식사하려고 분식집에 김밥먹으러 온 손님에게 최고의 식사를 대접한다며 재료손질부터 시작한다면 손님은 이미 떠나가고 없을 겁니다. 분명 최고의 김밥일지라도요. 애자일을 이야기할때 사람들이 많이 혼돈하는 것이 있습니다. 애자일은 방법론인가? 프로세스인가? 그렇지는 않습니다. 애자일은 RUP나, SPICE, CMMI process와 깉이 정형화된 개발 단계와 산출물이 존재하는 프로세스는 아닙니다. 위의 애자일 선언문에서도 볼 수 있듯이 소프트웨어를 개발함에 있어서 어디에 더 중요한 가치를 두어야 하는가에 대한 철학이라고 생각할 수 있습니다. 말이 어렵네요. 저처럼 어려워하는 사람들을 위해서 애자일 선언문을 작성한 멋진 구루들은 아래와 같은 12가지 원칙들로 다시 설명해주고 있습니다. > **애자일 선언 이면의 원칙** > > 우리는 다음 원칙을 따른다: 우리의 최우선 순위는, 가치 있는 소프트웨어를 일찍 그리고 지속적으로 전달해서 고객을 만족시키는 것이다. > > 비록 개발의 후반부일지라도 요구사항 변경을 환영하라. 애자일 프로세스들은 변화를 활용해 고객의 경쟁력에 도움이 되게 한다. > > 작동하는 소프트웨어를 자주 전달하라. 두어 주에서 두어 개월의 간격으로 하되 더 짧은 기간을 선호하라. > > 비즈니스 쪽의 사람들과 개발자들은 프로젝트 전체에 걸쳐 날마다 함께 일해야 한다. > > 동기가 부여된 개인들 중심으로 프로젝트를 구성하라. 그들이 필요로 하는 환경과 지원을 주고 그들이 일을 끝내리라고 신뢰하라. > > 개발팀으로, 또 개발팀 내부에서 정보를 전하는 가장 효율적이고 효과적인 방법은 면대면 대화이다. > > 작동하는 소프트웨어가 진척의 주된 척도이다. > > 애자일 프로세스들은 지속 가능한 개발을 장려한다. 스폰서, 개발자, 사용자는 일정한 속도를 계속 유지 할 수 있어야 한다. > > 기술적 탁월성과 좋은 설계에 대한 지속적 관심이 기민함을 높인다. > > 단순성이 \-\- 안 하는 일의 양을 최대화하는 기술이 \-\- 필수적이다. > > 최고의 아키텍처, 요구사항, 설계는 자기 조직적인 팀에서 창발한다. > > 팀은 정기적으로 어떻게 더 효과적이 될지 숙고하고, 이에 따라 팀의 행동을 조율하고 조정한다. 그렇다면, 십계명과 같은(애자일은 십이계명이네요) 이 원칙들에 따라서 버즈빌은 어떻게 일하고 있는가 살펴보기로 했습니다. 사실, 우리 훌륭한 PM(Young과 Eric에게 감사를 - 버즈빌은 수평적인 소통을 위하여 영어이름을 사용하고 있습니다.)께서 이미 애자일 마스터로서 스크럼과 칸반에서 좋은 프랙티스들을 따와서 전도해주셨습니다. 우리가 실천하고 있는 좋은 경험들은 아래 그림에 마크해 두었습니다. 굳이 이 내용들을 하나씩 언급하면서 교과서적인 내용을 서술하기 보다는 실제로 우리가 12원칙에 얼마나 가까운지 살펴보는게 더 의미가 있을 것 같아서 아래 그림으로 대체하겠습니다. 빨간색 네모 박스는 하고 있거나 도입중인 부분입니다. 구체적인 필요하신 분들은 구글에만 검색해봐도 깔끔하게 정리된 자료가 무궁무진하게 많아요. ![](https://tech.buzzvil.com/blog/tech-blog-%ea%b0%9c%eb%b0%9c%ec%9e%90%ec%9d%98-%ec%9e%85%ec%9e%a5%ec%97%90%ec%84%9c-%eb%b3%b8-%eb%b2%84%ec%a6%88%eb%b9%8c%ec%9d%98-%ea%b0%9c%eb%b0%9c-%eb%ac%b8%ed%99%94-%ec%95%a0%ec%9e%90%ec%9d%bc/image1.png) _\[Image Source: _[_Subway Map to Agile Practices_](https://www.agilealliance.org/agile101/subway-map-to-agile-practices/)_\]_ ### 3. 버즈빌 애자일 소프트웨어 개발 12계명 ##### 1. 우리는 다음 원칙을 따른다: 우리의 최우선 순위는, 가치 있는 소프트웨어를 일찍 그리고 지속적으로 전달해서 고객을 만족시키는 것이다. 소프트웨어가 필요한 이유는 고객이 존재하기 때문입니다. 아무리 멋지고 훌륭한 제품을 만들더라도 그것을 사용할 고객이 존재하지 않는다면 아무 가치가 없습니다. 버즈빌의 고객은 크게보면 넷으로 나눌 수 있습니다: 잠금화면 유저, 광고주, 퍼블리셔, 오퍼레이터. 각각의 고객에게 필요한 가치는 여러가지가 있습니다. 질 좋은 광고/컨텐츠를 통한 유저 경험의 극대화, 고도화된 타게팅을 통한 광고 효율 향상, 안정적인 서비스를 통한 수익창출, 오퍼레이션의 편리성 등 다양한 니즈를 가지고 있습니다. 고객은 너그럽지 않습니다. 우리의 제품이 이러한 고객의 가치를 온전히 제공할 수 없다면 고객은 기다려주지 않습니다. 유저는 우리 서비스에서 떠나갈 것이며 광고주는 더이상 우리에게 광고를 주지 않을 것입니다. 퍼블리셔는 우리 서비스를 믿을 수 없어서 사업을 포기할 수도 있고 오퍼레이터는 지루하고 복잡한 작업으로 심지어 버즈빌을 떠나버릴 수도 있습니다. 따라서 우리는 해당 가치를 측정할 수 있는 데이터를 수치화해서 실시간으로 모니터링 하고 있으며, 빠른 배포와 피드백을 통해서 지속적으로 그들을 만족시키려 노력하고 있습니다. 그 어느것도 이보다 우선시될 수는 없습니다. ![](https://tech.buzzvil.com/blog/tech-blog-%ea%b0%9c%eb%b0%9c%ec%9e%90%ec%9d%98-%ec%9e%85%ec%9e%a5%ec%97%90%ec%84%9c-%eb%b3%b8-%eb%b2%84%ec%a6%88%eb%b9%8c%ec%9d%98-%ea%b0%9c%eb%b0%9c-%eb%ac%b8%ed%99%94-%ec%95%a0%ec%9e%90%ec%9d%bc/image2.png) _\[Image Source: 버즈빌 데이터 대쉬보드\]_ ##### 2. 비록 개발의 후반부일지라도 요구사항 변경을 환영하라. 애자일 프로세스들은 변화를 활용해 고객의 경쟁력에 도움이 되게 한다. 개발자의 입장에서 요구사항의 변경은 그리 반가운 일은 아닙니다. 기껏 애써가며 설계와 구현을 진행 했는데 그간의 노력이 까만 화면위의 글자 뿐인 아무 가치 없는 낙서로 남게 되는 상황을 아무도 좋아하지 않을 것입니다. 하지만, 요구사항은 변경되기 마련입니다. 고객이 스스로 뭘 원하는지 모를 수도 있고 비지니스 상황에 따라서 더이상 필요 없거나 바꿔야 할 수도 있습니다. 인정합시다. 요구사항은 자주 변합니다. 심지어 제품이 출시되고 나서도 변할 수 있습니다. 버즈빌은 이 현실을 인정합니다. 그렇다면 이 현실을 극복하기 위해서 어떻게 해야 할까요? 문제를 나눠보겠습니다.   * 요구사항의 인입 및 정제 요구사항 인입 초기에 서로간의 이해가 조금 더 분명하다면 오해는 줄일 수 있고 요구사항 변경 또한 덜 빈번해질 것 같습니다. 개발팀으로 요청되는 모든 요구사항은 PM(Product Manager)를 통해서 이루어집니다. 제품 관점에서 가장 폭 넓게 이해하고 있는 PM은 고객의 요구사항이 정확하게 무엇인지 다시 한번 정제하고 필요하다면 개발자와 상의해서 구현가능한지 검토합니다. 이를 통해서 요구사항은 좀 더 구체화되며 불확실성과 모호성은 제거됩니다. 버즈빌의 PM은 총 네명인데, 각각 엔드유저, 광고주 및 광고 시스템, 퍼블리셔, 제품 고도화에 관련된 제품을 맡고 있습니다. 각 PM 이 대응하는 고객에 따라서 요구사항의 인입과 정제방식이 다르며 정리된 요구사항은 매 스프린트 계획 회의 전에 다시 한번 정리되어 프로덕트 백로그에 우선순위에 따라 나열됩니다.   * 요구사항의 개발 및 확인 프로덕트 팀에 요구사항을 제기한 사람은 본인이 요청한 사항이 어떻게 진행되고 있는지 항상 확인할 수 있습니다. 개발팀의 스크럼보드는 개발팀 뿐만 아니라 모두에게 공개되어 있습니다. 스크럼보드는 Trello를 통하여 관리하고 있는데, 각 카드는 구체적인 내용을 얼마든지 담을 수 있고 카드를 통해서 얼마든지 논의할 수 있습니다. 개발자가 개발중인 내용은 카드를 통해서 진척사항을 기록하고 있으며 피엠 및 요구사항 발주자는 해당 카드를 통해서 개발사항에 대해서 질문하거나 요구할 수 있습니다. 큰 틀을 벗어나지 않는 요구사항의 변화는 이 카드를 통해서 적응해 나갈 수 있습니다. ![](https://tech.buzzvil.com/blog/tech-blog-%ea%b0%9c%eb%b0%9c%ec%9e%90%ec%9d%98-%ec%9e%85%ec%9e%a5%ec%97%90%ec%84%9c-%eb%b3%b8-%eb%b2%84%ec%a6%88%eb%b9%8c%ec%9d%98-%ea%b0%9c%eb%b0%9c-%eb%ac%b8%ed%99%94-%ec%95%a0%ec%9e%90%ec%9d%bc/image3.png) _\[Image Source: 버즈빌 업무툴 '트렐로'\]_ ##### 3. 작동하는 소프트웨어를 자주 전달하라. 두어 주에서 두어 개월의 간격으로 하되 더 짧은 기간을 선호하라 앞서 이야기한 것처럼 고객의 요구사항은 변화무쌍하여 언제 어떻게 바뀔지 모릅니다. 따라서 우리는 가능한 빠르게 고객의 피드백을 받고 이를 다시 제품에 반영할 수 있어야 합니다. 이를 위해서 배포하는 방법은 쉽고 자동화 되어 있어야만 합니다. 지속 통합과 나아가 지속 배포가 그 해답이 될 수 있습니다. 모든 소프트웨어 자산은 github을 통해서 형상관리를 하고 있으며 각 브랜치는 마스터에 머지되기 전에 Jenkins를 통해 필수적인 테스트를 자동으로 거치고 머지됩니다. 물론 새로운 기능에 대한 유닛 테스트도 새롭게 작성될 수 있으며 CI과정에 포함됩니다. 무중단 배포, 빌드 버전, 라이브러리 패치 등 대부분의 배포 스크립트는 Fabric을 통해서 자동화되어 있으며 개발자가 할 일은 개발을 마친 뒤에 fab deploy 명령어만 실행하면 됩니다. 최근에는 형상관리 시스템에 코드를 푸쉬하는 것만으로 배포까지 될 수 있도록 자동화하는 작업을 진행하고 있습니다. 배포가 간결하고 쉽기 때문에 작은 변경에 대한 배포를 자주 할 수 있고 그 만큼 고객의 피드백은 더 빠르게 받을 수 있습니다. ![](https://tech.buzzvil.com/blog/tech-blog-%ea%b0%9c%eb%b0%9c%ec%9e%90%ec%9d%98-%ec%9e%85%ec%9e%a5%ec%97%90%ec%84%9c-%eb%b3%b8-%eb%b2%84%ec%a6%88%eb%b9%8c%ec%9d%98-%ea%b0%9c%eb%b0%9c-%eb%ac%b8%ed%99%94-%ec%95%a0%ec%9e%90%ec%9d%bc/image4.png) _\[Image Source: 시스템 업데이트 푸쉬메세지\]_ ##### 4. 비즈니스 쪽의 사람들과 개발자들은 프로젝트 전체에 걸쳐 날마다 함께 일해야 한다. 버즈빌에는 특이한 문화가 있습니다. 2달마다 한번씩 제비뽑기를 해서 이후 2달동안 근무할 자리를 정하는데, 예외없이 모두에게 해당합니다. 때문에 버즈빌에는 팀 간의 경계가 없습니다. 바로 옆자리 사람이 디자이너가 될 수도 있고, 비지니스 담당자가 될수도 있습니다. 같은 공간안에서 밀접하게 일할 수 있기 때문에 커뮤니케이션의 비용은 상당히 적고 서로의 언어를 더 잘 이해할 수 있습니다. 다만, 비지니스/파트너쉽 팀은 나머지 팀과 공간이 분리되어 있습니다. 업무의 특성상 비지니스/파트너쉽 은 전화통화와 방문자가 많습니다. 해당 팀은 좀 더 자유롭게 소통할 수 있고 나머지 팀은 좀 더 집중할 수 있는 환경을 구성하기 위해서 공간을 나누어 쓰고 있습니다. 물론 비지니스/파트너쉽 팀원들도 해당 공간에서 2개월마다 자리배치는 랜덤입니다. ![](https://tech.buzzvil.com/blog/tech-blog-%ea%b0%9c%eb%b0%9c%ec%9e%90%ec%9d%98-%ec%9e%85%ec%9e%a5%ec%97%90%ec%84%9c-%eb%b3%b8-%eb%b2%84%ec%a6%88%eb%b9%8c%ec%9d%98-%ea%b0%9c%eb%b0%9c-%eb%ac%b8%ed%99%94-%ec%95%a0%ec%9e%90%ec%9d%bc/image5-2.jpg) _\[Image Source: 버즈빌 사무실 전경\]_ ##### 5. 동기가 부여된 개인들 중심으로 프로젝트를 구성하라. 그들이 필요로 하는 환경과 지원을 주고 그들이 일을 끝내리라고 신뢰하라. Trust & Respect. 버즈빌 문화 중 가장 좋아하는 문화 입니다. 우리 모두는 최고의 팀원과 함께하고 있으며 각 팀원이 스스로 올바른 결정을 내리고 책임감있게 일하고 있다고 믿습니다. 우리는 서로를 상호 존중합니다. 주니어라 할지라도 의견을 개진하는데 있어서 주저하지 않으며, 시니어라 할지라도 본인의 의견만 관철시키려 하지 않고 모두가 모두에게 배울것이 있다고 생각합니다. 매 스프린트 시작시에 일정 산정과 계획회의를 하는데 각 일감에 대한 조금 더 관련된 개발자가 있을 수는 있으나 어떤 일감을 가져갈지는 오로지 각 개발자의 몫입니다. 본인의 판단하에 일감을 정하고 각자는 일감을 해결할 수 있도록 스스로 계획해서 일합니다.   ##### 6. 개발팀으로, 또 개발팀 내부에서 정보를 전하는 가장 효율적이고 효과적인 방법은 면대면 대화이다. 버즈빌에서는 다양한 협업 도구를 이용합니다. Slack, Trello, Github, GoogleCalendar, GoogleDocs, Email, Skype 등등. 모두 훌륭한 커뮤니케이션 수단입니다. 하지만, 한계는 분명히 존재합니다. 면대면으로 이야기하는 것보다 더 효율적인 의사소통 수단은 없습니다. 하지만, 이슈가 있을 때 매번 상대를 간섭하는 것은 부담스러운 일입니다. 때문에 서로 편하게 의견을 나눌 수 있는 시간을 정했습니다. 매일 아침 개발팀은 약 15분 정도의 데일리스크럼을 진행하는데, 이는 일반적인 스크럼에서의 그것과 크게 다르지 않습니다. 어제 하던일, 오늘 할일, 이슈나 도움이 필요한 사항에 대한 브리핑을 약 1분간 짤막하게 공유합니다. 이때 간단한 질문은 할 수 있지만, 구체적인 논의는 데일리스크럼이 끝난 직후에 당사자들만 모여서 다시 진행합니다. 때로는 필요에 따라 짝 프로그래밍도 진행합니다. 주로 새로 합류한 개발자 혹은 기존 개발자 중에서도 주로 다루지 않았던 코드에 대한 작업이 필요할 때 진행합니다. ![](https://tech.buzzvil.com/blog/tech-blog-%ea%b0%9c%eb%b0%9c%ec%9e%90%ec%9d%98-%ec%9e%85%ec%9e%a5%ec%97%90%ec%84%9c-%eb%b3%b8-%eb%b2%84%ec%a6%88%eb%b9%8c%ec%9d%98-%ea%b0%9c%eb%b0%9c-%eb%ac%b8%ed%99%94-%ec%95%a0%ec%9e%90%ec%9d%bc/image6-1.jpg) _\[Image Source: 개발팀 데일리 스크럼\]_ ##### 7. 작동하는 소프트웨어가 진척의 주된 척도이다. 매 스프린트 리뷰 시간에는 각자 약 5분씩 그간 개발해온 기능을 가볍게 공유합니다. 필요하다면 소스코드를 같이 보기도 하고, 실제 동작하는 모습을 데모하기도 합니다. 서로 박수로 칭찬하기도 하고 궁금한 점을 질문하기도 합니다. 물론 기능 적인 구현만 공유하는게 아니라 비기능적인 구현도 공유합니다. 리팩토링을 통해서 훨씬 이해가 쉽고 확장성있는 코드를 만들었다던가 새로운 기술에 대한 연구 및 고찰에 대한 내용을 공유하기도 합니다.   ##### 8. 애자일 프로세스들은 지속 가능한 개발을 장려한다. 스폰서, 개발자, 사용자는 일정한 속도를 계속 유지 할 수 있어야 한다. 각 스프린트에 개발할 과제는 개발자 스스로 결정합니다. 보통 하루에 개발에만 집중할 수 있는 시간을 5시간이라고 했을 때 2주일을 기준으로 50시간 정도의 업무량을 가진다고 가정합니다. 개발 과제의 난이도나 작업량을 고려하여 스스로 업무량을 산정하고 장애처리나 예상치 못한 상황에 대한 버퍼를 고려하여 30~40 시간정도의 일감을 스프린트에 올립니다. 각 제품의 피엠은 스프린트 중에 계속하여 각 개발자의 로드를 확인하여 과도한 업무가 가중되지 않도록 리스크를 관리합니다. 스프린트라는 단어의 의미에서 오해가 생길 수 있습니다. 스프린트는 목표를 정하고 집중하자는 의미이지 체력적으로 모든걸 쏟는 전력질주는 아닙니다. 밤을 세워서 개발하는 것은 절대로 자랑스러운 일이 아닙니다. 밤을 세울 수 밖에 없다면 스스로 계획을 잘못 세웠거나 사장님이 나쁜겁니다. 단기간의 성과 보다 지속적이고 예측가능한 성과가 장려되어야만 합니다. 개발자는 엉덩이로 개발하기보다 머리로 개발해야 합니다. 집중할 수 있는 시간에 집중해서 일하고 쉴 때는 과감하게 쉴 수 있어야 합니다. 버즈빌에는 탁구대를 테이블로 쓰고 있는 회의실이 있는데, 점심시간 즈음에는 항상 사람들로 북적입니다. 휴게실에는 커다란 티비와 플레이스테이션이 있습니다. 때로 위닝일레븐 대회가 열리기도 합니다. ![](https://tech.buzzvil.com/blog/tech-blog-%ea%b0%9c%eb%b0%9c%ec%9e%90%ec%9d%98-%ec%9e%85%ec%9e%a5%ec%97%90%ec%84%9c-%eb%b3%b8-%eb%b2%84%ec%a6%88%eb%b9%8c%ec%9d%98-%ea%b0%9c%eb%b0%9c-%eb%ac%b8%ed%99%94-%ec%95%a0%ec%9e%90%ec%9d%bc/image7.png) _\[Image Source: 점심시간 탁구 시합\]_ ##### 9. 기술적 탁월성과 좋은 설계에 대한 지속적 관심이 기민함을 높인다. 버즈빌의 서버는 하루에도 수백만의 유저로부터 수백건의 요청을 받습니다. 따라서 단순히 기능적으로 동작하는 것 뿐만 아니라 다양한 아키텍쳐가 고려되어야만 합니다. 광고 할당 로직을 위한 최적화에서부터 타게팅을 고도화 하기 위한 검색에 대한 고려, 데이터의 수집과 가공에 대한 고려, 열거하자면 수도 없이 많습니다. 따라서 설계에 대한 고민은 지속적인 요구사항의 구현과 더불어 당연히 계속되어야만 합니다. 최근에는 복잡성을 줄이기 위해서 마이크로 아키텍쳐로의 진화, 성능을 고려하여 일부 병목이 되는 기능을 Golang으로 구현하는 최적화 등을 진행하고 있습니다. 필요한 사항은 어떻게든 시작을 합니다. 나중으로 미루면 절대로 시작하지 않습니다. 그럼에도 불구하고 기술부채가 남아 있을 수 있습니다. 이를 위해서 3회의 스프린트가 끝나면 이러한 고민을 집중할 수 있는 유지보수 스프린트를 1회 진행합니다. 이때는 잠시 새로운 요구사항의 인입을 보류하고 기술 부채를 해결하기 위하여 시스템을 보수합니다. 대체로 프레임워크나 서버 등의 인프라 관리, 보안과 모니터링 등과 같이 평소에 진행하기 어려운 과제 위주로 진행 됩니다.   ##### 10. 단순성이 필수적이다 = 안 하는 일의 양을 최대화하는 기술이 필수적이다. 요구사항을 이해하고 구현하는데 있어서 최대한 시간을 절약하고 단순화 하는게 중요합니다. 개발 일정에 대한 추정과 진행사항 확인을 위해서 복잡한 방법을 쓰거나 보고 하는 일이 필요할까요? 중요한 것은 일을 되게 만든다는데 있습니다. 일정 산출과 추정을 위해서는 단순하게 트렐로 카드에 얼만큼의 시간이 필요하고 얼만큼 진행되었다고 적는게 나아 보입니다. 개발일정에 대해서 해당 개발자만큼 잘 아는 사람은 없을 것 같아요. 아래 그림은 구글 캘린더에서 캡쳐한 실제 저의 일반적인 스프린트 동안의 일정입니다. 스터디, 꿀다방 정기모임(드립커피 소모임입니다.)는 개인적인 일정이고,  StrategyTalk는 한달에 한번 있는 전사 모임입니다. 이들을 제외하면 프로덕트 팀의 회의는 스프린트계획/리뷰 미팅, 매일 있는 15분간의 스크럼 미팅, 개발리더와의 1:1미팅이 전부입니다. 소모적인 회의는 없으며, 보고를 위한 미팅도 없습니다. 누구도 개발자를 마이크로 매니지 하지 않고, 누구도 보고를 원하지 않습니다. 스프린트계획/리뷰 미팅과 데일리 스크럼을 통해서 충분히 소화할 수 있습니다.   ![](https://tech.buzzvil.com/blog/tech-blog-%ea%b0%9c%eb%b0%9c%ec%9e%90%ec%9d%98-%ec%9e%85%ec%9e%a5%ec%97%90%ec%84%9c-%eb%b3%b8-%eb%b2%84%ec%a6%88%eb%b9%8c%ec%9d%98-%ea%b0%9c%eb%b0%9c-%eb%ac%b8%ed%99%94-%ec%95%a0%ec%9e%90%ec%9d%bc/image4-2.png) _\[Image Source: 버즈빌 업무툴 'Google Calendar'\]_ ##### 11. 최고의 아키텍처, 요구사항, 설계는 자기 조직적인 팀에서 창발한다. 누가 시켜서 하는일은 재미 없습니다. 만약에 누군가 저에게 ‘다른 일은 넘보지 마시고 이 일만 집중해서 하세요' 라고 했다면 회사를 뛰쳐나갔을지도 모릅니다. 실제로 전 직장에서 뛰쳐나온 가장 큰 이유중의 하나가 제가 재미있어하고 좋아하는 일을 빼앗아가버렸기 때문이었습니다.(팀 매니저가 되면서 개발할 수 있는 시간보다 회의와 보고해야 하는시간이 너무 많아졌어요) 개발자들이 개발할 대부분의 개발 과제는 백로그에서 스스로 가져옵니다. 하고 싶은 일에 집중할 수 있을때에 생산성이 높은 것 같습니다. 본인이 스스로 일감과 스케줄을 계획하고 진행하기 때문에 책임감을 가질 수 밖에 없습니다. 코드 한줄을 작성하더라도 이 기능이 왜 필요한지 다시 한번 확인하고 다음번 수정을 위해서 유연하게 변경가능하지 검토합니다. 우리는 코드에 Ownership이 없다는 원칙에 동의하지만 그렇다고 책임질 수 없는 코드를 작성하지는 않습니다.   ##### 12. 어떻게하면 더 효과적일지 정기적으로 숙고하고 팀을 조율한다. 매 스프린트 마무리에 각 일감에 대한 리뷰와 데모가 끝나고 나면 스프린트동안 잘 했던 일들과 개선이 필요한 사항에 대해서 이야기 합니다. 그 중 몇가지를 나열해보면 아래와 같습니다.   * 과제에 대한 정의가 명확하지 않다 -\> 과제 인입되기 전에 PM을 통해서 구체화 한다. * 장애나 긴급 이슈로 인해서 스프린트 초기 설정한 계획이 무용지물이 된다 -\> 버퍼를 고려한다. * 코드 리뷰 요청이 일부 개발자에게만 집중된다 -\> 리뷰가 업무를 토스 할 수 있는 방안 마련 * 테스트 폰 관리가 어렵다 -\> 테스트 폰에 라벨링 하여 사물함에 보관하고 필요시 명함을 꽂아넣고 사용함 * 유닛 테스트 코드 활용방안 마련 -\> 중요한 코드에 대해서 먼저 작성될 수 있도록 가이드 * 코드 리뷰가 밀리는 경우가 발생한다 -\> 슬랙 봇을 이용하여 아침출근 직후 점심 식사 직후에 알람을 보내도록 설정하고 다른 업무 전에 리뷰 먼저 처리하도록 가이드 * 스탠딩 데스크 활용 방안 -\> 먼저 4개의 스탠딩 데스크를 구매하여 시범 사용 후 확대 방안 논의 * 개발과 관련된 지식정보 모으기 -\> github에 프로젝트 생성하여 위키로 활용 이와 같이 회고 시간에는 다양한 의견을 나눕니다. 의견이 수렴되면 바로 액션 아이템으로 삼아서 실행합니다. 버즈빌 개발 팀은 계속해서 적응해 나갑니다. 심지어 데일리 스크럼 시간의 분위기가 자칫 무거워질 수 있으므로 잔잔한 음악을 깔아놓고 해보는 것은 어떤가 하는 이야기도 있습니다. 유지보수 스프린트에 대한 논의는 뜨거운감자입니다. 유지보수 할 일감을 미뤄두고 하는게 맞는 것인가 아니면 일반적인 스프린트에 같이 진행하는 것이 맞는 것인가. 우리는 계속해서 논의하고 있으며 아마도 적절한 답을 찾아갈 것 같습니다. ### 4. 결론 다시 이야기 하지만 애자일 소프트웨어 개발은 프로세스나 방법론이 아닙니다. 물론 흔히 알려진 스크럼이나 익스트림 프로그래밍, 칸반과 같은 정리된 도구들이 존재하긴 합니다만, 사람들이 좋다고 말한다하여 그대로 적용하는건 바보같은 일입니다. 사람의 성격과 성향이 다양하게 많은 것처럼 팀의 성격과 성향 또한 다양합니다. 어떤 팀은 구조화된 조직을 가지고 있는가 하면 다른 어떤 팀은 수평적인 조직일 수 있습니다. 또한 어떤 팀은 결함을 용납할 수 없는 Mission Critical한 제품을 만드는 반면 다른 어떤 팀은 예측하기 어려운 환경에서 빠른 피드백이 필요할 수도 있습니다. 각 팀은 자기 팀에게 적절한 방법을 생각해볼 수 있어야 합니다. 그리고 서로간의 신뢰를 바탕으로 새로운 방법들에 적응할 수 있어야 합니다. 최근에 버즈빌은 슬라이드조이와의 합병으로 프로덕트 팀의 문화가 한층 더 성숙하고 있습니다. 비슷한 부분도 있고 다른부분도 있는 두 개발조직이 하나로 합쳐지면서 서로의 좋은 방법을 배우고 양보하고 이해하면서 더 나은 개발 문화를 만들어가고 있습니다. 만약 버즈빌이 어떤 한 방법만을 고집하고 있었다면 지금과 같이 멋진 팀을 만들어갈 수 없었을 것입니다. 핵심 가치를 이해하고 서로를 신뢰하기에 가능한 성과라고 생각합니다. 앞으로 또다른 문제를 만날 수도 있습니다. 하지만, 우리는 또다시 적응하고 더 좋은 방법을 찾을 겁니다. 늘 그래 왔듯이. --- ## [딥러닝 (Tensorflow) 을 이용한 추천시스템 개발](https://tech.buzzvil.com/blog/buzzvil-techblog-tensorflow-deeplearning) Date: 2017-02-22 | Category: Data & ML ### 1\.  글을 시작하기 전에 안녕하세요, 모바일 잠금화면 애드네트워크 [**버즈빌**](http://www.buzzvil.com)의 컨텐츠와 머신러닝 product manager 곽상훈 (Mike) 입니다. 버즈빌은 딥러닝을 이용하여 개인화된 컨텐츠를 자동 추천, 사용자 경험을 도우며 광고 플랫폼으로서의 기술적 우위를 보유하고 있습니다. 이 블로그에서는, 이러한 로직의 개발 과정과 결과를 소개하려고 합니다. 기초적인 neural network background 가 있다는 가정하에 코드는 최소화 하고 머신 러닝 모델의 high level design 위주로 기술하였으며, 자세한 수식/증명/예시/코드 등에 관해서는 도움이 될만한 참고 링크들을 첨부하였습니다. ### 2\. 배경 10 년전만 해도 흔히 쓰이던 인공지능 기술들은 보편적인 기계 학습에 쓰이기엔 부족함이 많았습니다. 그 원인 중 하나는 자동 feature selection 의 부재였습니다. 자연어 처리를 위한 feature 들 (e.g. 형태소 분석), image recognition 을 위한 feature 들 (e.g. edge detection) 등, specialized 된 목적을 위해 각 분야의 전문가들이 제작한 handmade feature 들에 의존을 많이 했기 때문입니다. 최근에 재조명을 받고 있는 neural network -- 혹은 deep learning -- 은 자동 feature learning 을 통하여 이 고민을 해결해 주어 분야에 관계 없이 보편적인 기계 학습을 하기에 조금 더 적합한 기술입니다. Black box 라서 모델을 직관적으로 이해하기 어렵고, training 시간도 오래 걸리고, parameter tuning 에 많은 노력이 소요되는 등의 단점들이 있지만, 전반적인 pattern recognition 에 있어서는 기가 막힌 효과를 보인다는 것은 이미 증명이 되었습니다 (참고: [http://karpathy.github.io/2015/05/21/rnn-effectiveness/](http://karpathy.github.io/2015/05/21/rnn-effectiveness/)). 처음 RNN 을 접하고 제 맥북으로 \[이문열의 삼국지\] 10권을 input 으로 위 링크에서 소개한 실험들과 비슷한 실험을 했는데 그럴싸한 결과가 나왔습니다 (따옴표, 물음표, 느낌표가 절묘합니다). > 그 말을 듣자 원소의 말에 조조는 원소에게 물었다. > "너는 어디 없이 이렇게 말했다. 이 말 아래로 돌피지 않으면 반드시 그 군사들을 죽이려 드는 것이 있다. 이번에는 원소와 함께 그들을 치고 달리 어찌 한가지는 것이냐!" > 그 말을 들고 조조가 다시 물었던 것이다. > "저희들은 모두 군량이 있으니 어떤 것이 오늘이오?" > 원소는 조조가 그렇게 대답하며 말했다. > "이 분은 무슨 말이냐?" > > "그 자리 같은 사졸들을 죽여 그 말을 듣고 있습니다. 이미 저것입니다"_ 그럼 거두절미하고 이 강력한 툴을 이용하여 버즈빌에서 만든 추천 로직을 소개 하겠습니다. ### 3. Application 버즈빌이 미국과 유럽에서 서비스 중인 [Slidejoy](https://www.getslidejoy.com/) 에서는 유저들을 위해 락스크린에 광고 외에도 다양한 컨텐츠를 제공하고 있습니다. 200 개 이상의 content provider 들로부터 매 시간 10,000 개가 넘는 기사를 저장, 정제하여 카테고리 별로 유저들에게 제공합니다 (참고: [https://techcrunch.com/2016/01/28/lockscreen-app-slidejoy-gets-a-newsy-new-feature/](https://techcrunch.com/2016/01/28/lockscreen-app-slidejoy-gets-a-newsy-new-feature/)). 초기 (neural network 적용 이전) 에는 컨텐츠들을 간단한 unigram tf-idf 방식으로 grouping 하여 가장 많이 mention 된 토픽 순서로 모든 유저들에게 일괄적으로 보여줬습니다.![](https://tech.buzzvil.com/blog/buzzvil-techblog-tensorflow-deeplearning/screen-shot-2017-02-22-at-10.38.40-am.png) _\[Image Source: 테크 크런치 2016년 1월 기사, [Lockscreen App Slidejoy Gets A Newsy New Feature](https://techcrunch.com/2016/01/28/lockscreen-app-slidejoy-gets-a-newsy-new-feature/)\]_ 하지만, 그 이후 보편적으로 popular 한 topic 외에도 개개인이 관심 있어할 만한 컨텐츠 제공의 필요성을 느껴 deep learning 을 이용하여 개인화 로직 개발을 시작했습니다. 모든 개인화 로직 개발은 최근에 빠른 adoption 을 보이고 있는 Google 의 Tensorflow 를 이용하였습니다. ### 4\. 모델 설계   **1) 데이타 수집:** Slidejoy 컨텐츠를 제공함으로써 수집하는 데이타는 크게 다음과 같습니다. * 유저 아이디, 성별, 나이 등의 **기본 유저 데이타** * 카테고리, provider 아이디, 제목, description 등의 **컨텐츠 데이타** * 컨텐츠 display 순서, 시간 및 클릭 여부 등 **유저 interaction 데이타** Tensorflow 의 training input 으로 사용하기 쉽게 날짜별로 위의 데이타를 tfrecord (참고: [https://www.tensorflow.org/api\_docs/python/python\_io/#tfrecords\_format\_details](https://www.tensorflow.org/api_docs/python/python_io/#tfrecords_format_details)) 파일에 저장했습니다. 초기 training 을 위해서 총 3달 분량의 데이타를 사용했습니다.   **2) 모델 components:** Tensorflow 에서는 먼저 데이타를 담을 tensor (간단하게 vector 로 생각하면 됩니다) 들을 이용하여 데이타의 흐름 (input 부터 back propagation 까지) 을 정의하는 **그래프**를 만듭니다. 그래프 정의 시에는 모든 tensor 관련 연산은 tensorflow 함수를 이용해야 합니다 (참고:[https://www.tensorflow.org/api\_docs/python/array\_ops/](https://www.tensorflow.org/api_docs/python/array_ops/)). 일반적으로 쓰이는 numpy 나 list 연산은 그래프에 적용이 안됩니다. 그래프 정의 후에 session 을 시작하여 그래프의 한 operation 에 실제 데이타를 흘려 (flow) 보내면 learning 이 진행됩니다. 그리하여 이름이 “Tensorflow” 인 것이죠. 머신 러닝을 처음 접하면 직관 적이지 않은 개발 방식이라 어색할 수 있지만, underlying 그래프만을 저장/로드/분석을 하기에 상당히 편리한 방식입니다.   * **Objective** 컨텐츠의 추천 로직은 기본적으로 ordering 을 유저의 취향에 맞게 하는 것이 목표 입니다. 유저의 취향은 여러 방식으로 정의할 수 있지만, 이 프로젝트에서는 유저의 컨텐츠 클릭 여부로 정했습니다. 유저/컨텐츠 정보를 input 으로 하여 neural network 가 최종적으로 유저가 클릭할 확률을 계산 하도록 했습니다. * **Input** 일별로 구분된 training data set 을 랜덤하게 섞어서 모델에 feed 할 수 있도록 Tensorflow 에서 제공하는 shuffle_batch 함수를 사용했습니다. 앞에서 언급했듯이 저는 input 파일들을 tfrecord 포맷으로 저장하는데요, binary 포맷이라서 일반 csv/text 파일보다 월등히 빠릅니다. Tensorflow 의 batching 은 queue/dequeue 방식을 사용합니다 (참고: [https://www.tensorflow.org/how\_tos/reading\_data/](https://www.tensorflow.org/how_tos/reading_data/)). 이를 사용하기 위해서는, ![](https://tech.buzzvil.com/blog/buzzvil-techblog-tensorflow-deeplearning/screen-shot-2017-02-22-at-10.42.12-am.png) _\[Image Source: [TensorFlow, 'Reading Data'](https://www.tensorflow.org/programmers_guide/reading_data)\]_ 1\.  tf.train.string\_input\_producer 에 필요 파일들을 filename queue 에 추가하고 ```python filename_queue = tf.train.string_input_producer(files) ``` 2\.  Input producer 에서 dequeue 된 파일들로부터 데이타 포맷 (csv, tfrecord 등) 에 맞는 reader 를 통해서 생성된 example 을 또 queue 에 추가합니다 ```python reader = tf.TFRecordReader() _, serialized_example = reader.read(filename_queue) ``` 3\.  최종적으로 shuffle_batch/batch 를 통해서 queue 에 있는 example 들을 dequeue 하여 실질적으로 데이타를 graph 에 feed 해 줄 수 있습니다. ```python features = tf.train.shuffle_batch( tensors=[serialized_example], batch_size=batch_size, capacity=capacity, min_after_dequeue=min_after_dequeue, ) ``` 4\.  Data feed 는 placeholder 에만 할 수 있습니다. Feed 를 받을 placeholder 들을 그래프 생성시에 define 해주면 됩니다. ```python # placeholder 들 생성 label = tf.placeholder(tf.int32, [None, ], name='y-input') ... # queue runner 시작 및 feature 추출 sess = tf.Session() coord = tf.train.Coordinator() threads = tf.train.start_queue_runners(sess=sess, coord=coord) features = sess.run(features) # Cost/loss 계산을 위해 batch output 을 placeholder 들에 feed feed_dict = { label = features[0].values, ... } # feature 를 이용해서 cost 계산 sess.run(cost, feed_dict=feed_dict) ``` 만약에 tfrecord 포맷으로 데이타를 write/read 할거라면 tensorflow 가 depend 하고 있는 python based protobuf library 를 C++ based protobuf 로 업데이트 할 것을 강력히 권장합니다. 무려 10 ~ 50배가 빨라집니다 (참고: [https://github.com/tensorflow/tensorflow/blob/master/tensorflow/g3doc/get\_started/os\_setup.md#protobuf-library-related-issues](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/g3doc/get_started/os_setup.md#protobuf-library-related-issues)) * **Embedding 처리** Sparse 한 feature (유저 아이디, 컨텐츠 카테고리 아이디, 유저 나이, 시간 등) 는 모두 embedding 처리합니다. Embedding 은 간단히 말해서 sparse 한 데이타를 dense 한 high dimensional vector space 에 프로젝트 해주는 lookup table/matrix 입니다. Embedding matrix 자체를 variable 로 설정하면 neural network 가 learning 을 하면서 weight 들과 함께 embedding matrix 도 업데이트가 되고, 결과적으로 비슷한 feature 들은 vector space 에서 grouping 되는 놀라운 현상이 발생합니다. Tensorflow 에서 embedding lookup 을 위한 함수들 ([https://www.tensorflow.org/api_docs/python/nn/embeddings](https://www.tensorflow.org/api_docs/python/nn/embeddings)) 이 존재하여 손쉽게 embedding 을 적용시킬 수 있습니다. ```python embeddings = tf.get_variable( 'embedding', initializer=tf.random_uniform([size, dimension], -1.0, 1.0) ) y = tf.nn.embedding_lookup(embeddings, feature) ``` 다만, 컨텐츠의 제목과 description 등의 단어들의 경우에는 variable 이 아니라 pre-trained 된 constant matrix (tf.constant) 를 사용합니다. 단어 embedding 의 경우 Wikipedia, Twitter 등에서 추출한 문서들을 이용하여 만들어진 많은 embedding matrix 들이 존재하고, 새로 embedding 을 training 해야 하는 수고를 덜 수가 있어서, pre-trained 된 embedding 을 사용하였습니다. 구체적으로, Stanford 에서 제공하는 Wikipedia + Gigaword embedding 을 사용하였습니다. Word embedding 의 자세한 methodoloy 는 [http://nlp.stanford.edu/projects/glove/](http://nlp.stanford.edu/projects/glove/) 에 소개 되어 있습니다. 참고 링크를 읽어 보시면 알겠지만, 같은 context 에서 자주 같이 등장하는 단어들은 vector space 에서 가깝게 위치하고, 심지어 간단한 vector 연산도 적용이 됩니다 (v\_king - v\_queen ~= v\_man - v\_woman). ![](https://tech.buzzvil.com/blog/buzzvil-techblog-tensorflow-deeplearning/man_woman_small.jpg) _\[Image Source: [Global Vectors for Word Representation](http://nlp.stanford.edu/projects/glove/)\]_ * **Hidden Layers:** 모델에는 크게 두가지 hidden layer 를 사용했습니다. **1\. RNN layer** 문장 feature (컨텐츠 제목 + description) 에는 RNN 을 적용 시켰습니다. RNN 적용 이전에 주로 image recognition 에 쓰이는 CNN 과 단방향 RNN 으로 실험을 해보았지만, 쌍방향 (bi-directional) RNN 의 효율이 가장 높았습니다 (하나의 layer 만을 사용했습니다). RNN cell 로는 기본 cell 의 vanishing/exploding gradient 문제를 보완한 LSTM (long short term memory) cell 을 사용했습니다 (참고: [http://colah.github.io/posts/2015-08-Understanding-LSTMs/](http://colah.github.io/posts/2015-08-Understanding-LSTMs/)) Tensorflow 에서 RNN 은 크게 두가지 버전이 있습니다: 기본 (static) RNN 과 dynamic RNN. Dynamic RNN 은 그래프를 session 을 execute 할 때 만들어서 초기 그래프 생성 시간이 기본 RNN 보다 월등히 빠릅니다. 다만 documentation ([https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/ops/rnn.py](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/ops/rnn.py)) 을 읽어 보시면 알겠지만, static RNN 은 output 을 계산 할 때 sequence length 에 맞춰서 모든 time step 을 계산 하지 않고 일찍 멈춥니다. 가령 컨텐츠 제목을 담은 embedded matrix 의 총 dimension 이 100 이라고 했을 때, 총 소요되는 rnn timestep 도 일반적으로 100 입니다. 그러나 sequence length 를 5로 지정하면 (컨텐츠의 제목이 짧아서) 5 번째 timestep 까지만 계산하고 early stop 을 합니다 (thus, saving number of computations). 이와 다르게, dynamic rnn 에서는 early stop 을 안하고 지정된 sequence length 이후의 output 은 0 으로 padding 을 해줍니다. 두 함수의 input specification 도 다르기 때문에 유의 하시기 바랍니다. 저는 초기 그래프 생성 시간이 그리 길지 않아서 그냥 static RNN 을 사용했습니다. ```python def lstm_cell(layer_size): return tf.nn.rnn_cell.LSTMCell( num_units=layer_size, forget_bias=1.0, state_is_tuple=True ) outputs, final_state_fw, final_state_bw = tf.nn.bidirectional_rnn( cell_fw=lstm_cell(layer_size), cell_bw=lstm_cell(layer_size), inputs=input, dtype=tf.float32, sequence_length=sequence_length ) ``` **2\. Feed forward layer** 위 RNN 에서의 output 과 embedding 된 input 들을 모두 concat 한다음에 multi-layer network 을 통하여 마지막 dimension 2 (click/no-click) 짜리 output 을 생성 하였습니다. 저는 Xavier weight initialization 을 사용했는데요, input 의 variance 를 유지시켜줘서 weight 때문에 input 이 소멸되거나 너무 증폭 되는 현상을 방지해줍니다 (참고: [http://jmlr.org/proceedings/papers/v9/glorot10a/glorot10a.pdf](http://jmlr.org/proceedings/papers/v9/glorot10a/glorot10a.pdf)). Layer 가 많은 network 에 아주 유용한 weight initialization 방식이죠. * **Cost Function** Neural network 의 최종 output dimension 을 2 로 하고, 확률 distribution 으로 바꾸기 위해서 output 의 softmax 를 계산을 합니다. Softmax 는 임의의 vector 값들을 확률로 바꾸기 위해서 가장 일반적으로 쓰이는 transform 방식이고, 식은 다음과 같습니다 (output vector _z_ 로부터 계산된 _c_ category 의 확률).  ![](https://tech.buzzvil.com/blog/buzzvil-techblog-tensorflow-deeplearning/screen-shot-2017-02-22-at-12.41.58-pm.png) 이 확률과 실제 클릭 여부 데이타 (label) 의 cross entropy 를 통하여 loss/cost 를 계산했습니다. Cross entropy 의 수식은 다음과 같습니다 (y 는 label, a 는 확률). ![](https://tech.buzzvil.com/blog/buzzvil-techblog-tensorflow-deeplearning/screen-shot-2017-02-22-at-12.42.02-pm.png) Mean squared error _(y - a)^2_ 대신에 cross entropy 를 cost function 으로 사용한 이유는 확률이 0 이나 1 에 근접할 때에 gradient 가 점점 줄어드는 (결국 learning 이 느려지는) 문제를 보완하기 위해서입니다. 그리하여 일반적으로 categorization 을 다룰 때 주로 cross entropy 를 씁니다 (참고: [http://neuralnetworksanddeeplearning.com/chap3.html](http://neuralnetworksanddeeplearning.com/chap3.html)). 이 전 과정 (softmax + cross entropy) 또한 Tensorflow 에서 softmax\_cross\_entropy\_with\_logits 라는 하나의 함수로 정의 되어 있어서 편합니다 (참고: [https://www.tensorflow.org/api_docs/python/nn/classification](https://www.tensorflow.org/api_docs/python/nn/classification)) **3) Evaluation 및 Monitoring** Training 데이타의 loss 값을 통하여 모델의 convergence 를 판단할 수 있지만, 모델의 실질적인 효과를 이해하기 위해서는 더 직관적인 metric 이 필요합니다. 일반적으로 데이타 categorization 의 효과를 판단할 때는 확률 (output 의 softmax 결과) 이 가장 높은 category 와 실제 label 을 비교하여 accuracy 를 계산합니다. 예를 들어, test 또는 evaluation data set 의 결과가 아래와 같다면, ![](https://tech.buzzvil.com/blog/buzzvil-techblog-tensorflow-deeplearning/screen-shot-2017-02-22-at-12.45.27-pm.png) Accuracy 는 66.6% 가 될 것입니다. 하지만 컨텐츠의 클릭 같은 경우에는 대부분 (90% 이상) 의 데이타가 0 (no click) 이기 때문에 이와 같은 accuracy metric 으로는 모델의 효과를 판단하기 어렵습니다. 이와 같은 데이타 bias 와 관계없이 클릭할 확률과 클릭 여부 데이타만을 가지고 모델의 효과를 판단하기 위해서 AUC (area under curve) 값을 사용했습니다. 자세한 AUC 의 정의는 [http://www.dataschool.io/roc-curves-and-auc-explained/](http://www.dataschool.io/roc-curves-and-auc-explained/) 를 참고하시면 됩니다. 간단히 소개하자면, * 다양한 threshold value 를 가지고 true positive rate 대비 false positive rate 를 그래프에 plot 한 후 그 그래프의 넓이를 구합니다 * 결과적으로, 계산된 클릭 확률 (p\_1, p\_2, p\_3, ... , p\_n. 0 <= p\_x <= 1) 들과 그에 따른 category (c\_1, c\_2, c\_3, ... , c\_n. c\_x = 1 or 0) 들이 있고 * 모든 (p\_x, p\_y) tuple 에 대해서 p\_x > p\_y 일 때 c\_x > c\_y 이면 AUC 값이 1 이 됩니다. * 반대로, 모든 (p\_x, p\_y) tuple 에 대해서 p\_x > p\_y 일 때 c\_x < c\_y 라면 AUC 값은 0 이 됩니다 * 일반적으로 확률 값이 랜덤이라면 AUC 는 ~0.5 이기 때문에, **0.5 이상이라면 계산된 확률 값이 의미가 있다는 것을 알 수 있습니다** 간편한 AUC 계산을 위해서 sklearn 에서 제공하는 함수를 사용했습니다 (참고: [http://scikit-learn.org/stable/modules/generated/sklearn.metrics.roc\_auc\_score.html](http://scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_auc_score.html))     **4) Tuning**   * **Overfit** 보통 같은 training data 를 반복해서 training 하다보면 training cost 는 계속 감소하지만 evaluation/test cost 는 줄지 않는 경우가 많습니다. Training data 에 모델이 overfit 되서 그런건데요, 이를 보완하기 위한 방안들이 몇가지 있습니다. **데이타:** 간단하며서도 가장 효과적인 방법입니다! Training 데이타량을 늘리는 겁니다. **L2 regularization:** Weight 가 너무 커지는 것을 방지합니다. High level 로 보자면, weight 가 커지면 input 에 따라서 output 의 swing 도 커지기 때문에 (예를 들어, 다양한 input 의 noise 에 model 이 fitting 될 수 있어서), 일반적인 input 을 위한 model 을 만들기 어려워집니다. ```python # weight 를 이용한 l2 regularization for weight in weights: cost += l2_reg_lambda * tf.nn.l2_loss(weight) ``` **Dropout:** 간단하며서도 가장 효과적인 방법입니다! Training 데이타량을 늘리는 겁니다. **![](https://tech.buzzvil.com/blog/buzzvil-techblog-tensorflow-deeplearning/screen-shot-2017-02-22-at-2.26.41-pm.png)** \[Image Source: A simple way to prevent neural networks from overfitting\] 위의 참고 그럼처럼 일정 확률로 node 를 없앤 채로 training 을 시킵니다. High level 로 보자면, 매번 랜덤한 다른 set 의 node 를 가지고 모델을 training 하기 때문에 여러개의 model 을 가지고 training 한 후 average 하는 효과를 가져옵니다. 결국 regularization 과 비슷하게 좀 더 일반적인 model 을 만들 수가 있습니다.   * **Tensorboard** 모델 설계 후 여러가지 방법으로 training 과정을 monitor 할 수 있지만, 가장 간편한 방식이 tensorflow 에서 제공하는 tensorboard 입니다. 매 iteration weight/bias 값의 변화 추이, accuracy 및 cost 의 변화 추이 등을 모니터 할 수 있습니다. 개인적으로 많은 도움이 된 부분은 graph visualization 입니다. 데이타의 dimension, 흐름, training 되고 있는 variable 등이 잘 정리 되어 있습니다 (참고: [https://www.tensorflow.org/versions/master/how\_tos/graph\_viz/](https://www.tensorflow.org/versions/master/how_tos/graph_viz/)) Tensorboard 를 이용하기 위해서는: **그래프 정의시 필요한 scalar 값마다 summary 기록 후 최종 merge** ```python tf.scalar_summary(‘cost’, cost) ... merged = tf.merge_all_summaries() ``` Session 생성 후 summary writer 정의 ```python Writer = tf.train.SummaryWriter([directory], sess.graph) ``` 매 iteration step 마다 summary 기록 추출 ```python summary, ... = sess.run([merged, ... ], feed_dict=...) writer.add_summary(summary, step) ``` * **Parameter optimization** Tuning 가능한 parameter 들이 많습니다: learning rate, batch size, dropout rate, l2 regularization rate 등등. 사실 이 parameter 들을 조정 하는 공식은 없습니다. 다양한 변화를 주면서 얼마나 빨리 converge 하는지, AUC 값은 어떤지, 등을 꾸준히 monitor 하면서 optimal 한 값들을 찾아 나가야 합니다: It is more art than science. 위의 모델 설계 과정을 거친 후 모델을 3달 분량의 데이타를 이용하여 총 100 epoch 정도 training 시켰습니다. Evaluation 데이타는 한 유저의 1주일 간의 컨텐츠 소비 데이타를 가지고 생성하였습니다. 유저마다 클릭 확률이 다르기 때문에 여러 유저가 섞인 데이타로 결과를 test 하지 않았습니다. 물론 evaluation data 는 training data 에 포함이 안 되었습니다. **Training 끝 무렵엔 evaluation data set 에서 ~0.7 의 AUC 값이 consistent 하게 나왔습니다.** 만족스러운 AUC 값이 나온 후 이 모델을 이용해서 실제 유저들에게 개인화된 컨텐츠를 제공하여 클릭률 증가를 살펴 보았습니다. ### 5\. 시스템 ![](https://tech.buzzvil.com/blog/buzzvil-techblog-tensorflow-deeplearning/screen-shot-2017-02-22-at-3.05.13-pm.png)Slidejoy 에 이 모델을 적용 시키기 위해 크게 두 component 가 있습니다: Daily training 과 prediction. * **Daily training** 은 지속적으로 진행됩니다. 다만 매일 가장 최근 60일 데이타만을 이용해서 training 합니다. 하루의 training 이 끝나면 모델 parameter 들을 p2 instance 로 옮겼습니다. 모든 training 데이타를 이용해서 다시 training 하는건 시간도 많이 소요 될 뿐 아니라, 유저의 성향이 바뀔 수도 있기 때문입니다. Training 을 위해서 AWS 의 g2.2xlarge instance 를 사용하였습니다. AWS 에서는 크게 두가지 GPU 서버를 제공하는데요 (g2, p2), 차이점이라면 p2 는 ~40% 정도 더 비싼 대신에 더 강력한 (3x GPU memory, 3x processing cores, 등등) GPU 를 사용한다는 것입니다. 이 프로젝트를 진행할 때 한 iteration 처리 속도는 p2.xlarge 가 g2.2xlarge 에 비해서 ~40% 가량 빨랐습니다. * **Prediction** 은 time sensitive 하기 때문에 조금 더 빠른 p2.xlarge instance 를 사용했습니다. 컨텐츠 수집이 끝나면 우선 AWS SQS 에 완료 메세지를 남겼습니다. Prediction instance 에서 이 메세지를 받으면 3,000 명의 테스트 유저들마다 매 시간 10,000 개 이상의 컨텐츠들의 확률을 계산하고 결과를 Redis 에 남겼습니다. ### 6\. 결과 모델의 효과를 파악하기 위해서 A/B 테스트를 진행했는데요, Slidejoy 컨텐츠의 active user 3,000 명을 추출하여 크게 3 그룹으로 나눴습니다. * 그룹 A: 랜덤한 컨텐츠가 보여집니다 * 그룹 B: Unigram TF-IDF 방식으로 기사들을 묶어서 가장 popular 한 토픽 순서대로 모든 유저들에게 일괄적으로 같은 컨텐츠가 보여집니다 * 그룹 C: Neural network 에서 계산된 확률이 높은 순서대로 유저별로 개인화된 컨텐츠가 보여집니다 2 주일동안 테스트를 해서 충분한양의 impression/click 을 확보한 후 결과를 살펴보니 **그룹 B 가 A 보다 클릭 비율이 22% 높았고, 그룹 C 가 B 보다 25% 높았습니다 (statistically significant)**. 간단한 모델이지만 실질적인 application 에서 의미있는 결과를 도출 할 수 있었습니다. ### 7\. 글을 마치며 이 프로젝트는 컨텐츠의 개인화를 위하여 시작을 했지만, 버즈빌에서는 neural network 기술을 광고 타게팅, 매출 증대, operation 자동화, 마케팅 등의 다양한 분야에 빠르게 적용시킬 예정입니다. 블로그 서두에 언급했듯이, neural network 의 장점은 각 분야에 specialized 된 feature 들을 고민할 필요가 없어서 하나의 generalized 모델을 여러 목적으로 사용하는 것이 가능합니다. 그래서 저희의 next step 으로는 **다양한 형태의 data 를 input 받아서 다양한 형태의 prediction 을 할 수 있는 generalized 된 모델과 시스템을 만드는 것입니다.** 필요에 따라서는 이 모델 위에 마치 레고처럼 특수한 layer 를 추가하거나 필요 없는 layer 는 생략 할 수 도 있겠지요. 위에 설명한 추천 로직은 앞에서도 언급했듯이 상당히 간단한? 모델입니다. 이와 비교해서 최근에 neural network 로 바꿔서 좋은 평가를 받고 있는 Google translate 의 모델을 보면 경외감이 듭니다 (참고: [https://arxiv.org/abs/1609.08144](https://arxiv.org/abs/1609.08144)) 그만큼 저희의 장기적인 목표를 위해서는 개선 할 수 있는 부분이 많습니다.![](https://tech.buzzvil.com/blog/buzzvil-techblog-tensorflow-deeplearning/screen-shot-2017-02-23-at-3.29.18-pm.png) _\[Image Source:[ Google's Neural Machine Translation System](https://arxiv.org/abs/1609.08144)\]_ **1.병렬 구조** 위 디자인에서 보면 알겠지만 (참고 문서에서도 설명이 되어있고), 단방향 보다 효과가 좋은 bi directional RNN 을 한 layer 만 사용한 이유는 model parallelism 을 극대화하기 위해서 입니다. 예를 들어, 한 단방향 RNN layer 의 첫번째 time slot 계산이 끝나면 바로 그 다음 RNN 의 첫번째 time slot 을 계산 할 수 있기 때문입니다 (쌍방향일 경우 모든 timeslot 이 계산 되어야 다음 layer 를 계산 할 수 있습니다). 결국 여러 gpu 를 사용해서 여러 layer RNN 을 병렬 구조로 계산을 할 수 가 있게 됩니다. Model parallelism 외에도 중요한 것이 data parallelism 입니다. 여러 개의 모델을 동시에 돌려서 하나의 shared model parameter 들을 업데이트 하도록 하는 방식입니다. 위에서도 언급했듯이 수많은 실험을 통해서만 parameter 들의 optimal 한 값과, 중요한 feature 들을 찾아 낼 수 있습니다. 그러기 위해서는 최대한 많은 실험을 빠르게 돌려야 하는데, 이러한 병렬 구조들이 가장 optimal 한 모델을 구현하는데 큰 도움이 될 것 입니다. **2\. Automated tuning** 실험 (모델에서 feature 를 빼고 metric 들을 monitor 하면서) 을 통해서 어떠한 feature 가 중요한지 또 중요하지 않은지를 판단해서 중요한 feature 들을 더 emphasize 하는 과정이 필요 합니다. 예를 들어 컨텐츠의 제목의 유무가 AUC 값의 증가에 큰 영향을 미친다면, 컨텐츠 제목 RNN 의 복잡도를 늘려서 모델이 제목의 context 를 더 잘 이해할 수 있도록 보완 할 수 있습니다. 또한, 앞서 언급했던 learning rate, regularization rate, dropout rate 등등의 다양한 parameter 들을 실험을 통해서 optimize 를 해야 하는데요. 이러한 과정의 많은 부분들을 자동화 하는 시스템을 설계 하는 것이 generalized model 을 만드는 데에 key 가 될 것입니다. **3\. Reinforcement Learning** Supervised learning 이외에도 neural network 를 이용한 reinforcement learning 모델이 많이 공개 되었습니다 (DQN, A3C 등등). 주로 state space 가 작으면서 well-defined 되어 있는 게임에 많이 적용이 되었는데, 광고/컨텐츠 allocation policy 에도 효과적으로 사용될 수 있을 것 같습니다. **4\. TFlearn (http://tflearn.org/)** 저는 이 프로젝트를 진행하면서 tensorflow 에서 제공하는 함수들을 사용했지만, 사실 더 간단한 library 가 있습니다. 바로 tflearn 인데요, 여러 반복적인 graph 생성 operation 들 (placeholder 생성, weight/bias 정의, regularization, dropout, 등등) 을 high level 함수들로 묶은 library 입니다. 모델 구현을 하면서 귀찮고 또 실수도 잦은 부분이 tensor 들의 dimension 지정하고, 필요에 따라서 reshape 하는 것인데, TFlearn 에서는 이러한 반복적이고 소모적인 operation 들을 깔끔하게 해결 해 줍니다. 예를 들어, 아래 두 줄의 코드로 weight, bias, activation, regularization, dropout 이 전부 포함된 feed forward layer 를 생성 할 수 있습니다. ```python dense1 = tflearn.fully_connected( input_layer, 64, activation='tanh', regularizer='L2', weight_decay=0.001 ) dropout1 = tflearn.dropout(dense1, 0.8) ``` 성능과 정확성등을 더 알아봐야 하지만, 실수를 줄이고, tensorflow 를 처음 접하거나 간단한 모델을 구현 하는 데에는 안성맞춤 인 것 같습니다. 앞으로, 딥러닝에 기반한 다양한 neural network 기술 연구가 어떻게 버즈빌의 서비스를 변화시킬 지 개인적으로도 기대가 큽니다. --- ## [Android MVP Pattern - What, Why and How?](https://tech.buzzvil.com/blog/android-mvp-pattern-what-why-and-how) Date: 2017-01-31 | Category: Frontend ### 1\.  글을 시작하기 전에 부끄럽지만 저는 버즈빌의 초보 개발자입니다. 버즈빌에서의 다양한 경험들이 저를 많이 변화시키고 있구요. 버즈빌의 뛰어난 개발자들 사이에서 일을 배워나가면서, 앞으로 향후에 저도 뛰어난 개발자가 되기 위해서는 반드시 공부해 두어야겠다고 생각한 몇 개의 영역이 있습니다. 그 중 하나가 '다양한 소프트웨어 디자인 패턴을 적절한 곳에 적용시키는 방법'이었습니다. 다행히 최근에는 많은 사람들의 경험을 통해 검증된 디자인 패턴이 많이 개발되고 있어, 다양한 패턴을 꾸준히 탐색해서 활용할 수 있는 안목만 키울 수 있다면 학습에 큰 어려움을 없을 것이라 생각하였습니다. 그동안 주로 기능적으로 시스템을 정상 동작하도록 만드는 것에만 집중했었지만,  여기서 더 나아가 가독성과 확장성이 좋고 협업하기 쉽게 만드는 구조를 짤 줄 알아야 장기적으로 더 좋은 프로덕트를 만들어 낼 수 있을 것이라는 어찌보면 막연한 생각도 배움에 대한 호기심을 자극하는데 한 몫 하였습니다. 그러던 중 실제 업무 진행 중에, 중 몇 가지 주요 패턴을 적용해 리팩토링을 할 기회를 갖게 되었습니다. 실무에서 직접 경험해보니 패턴의 유무가 만들어내는 결과의 차이는 생각보다 큰 역할을 하고 있었습니다. 미숙하지만, 이러한 맥락과 배경에서 저의 첫 버즈빌 기술 블로그를 시작해보고자 합니다. 이번 포스팅의 주요 내용은, 허니스크린 안드로이드 코드에 MVP패턴을 도입해 리팩토링 했던 경험을 바탕으로 안드로이드 어플리케이션에 MVP패턴을 적용하는 방법에 관한 것입니다. ### 2\. MVP 패턴에 대해서 ‘MVP 패턴'은 Model, View, Presenter의 앞글자를 따서 이름이 지어졌습니다. 이 패턴의 핵심 아이디어는 사용자 인터페이스(View)와 비즈니스 로직(Model)을 분리하고, 서로간에 상호작용을 다른 객체(Presenter)에 위임해 서로의 영향을 최소화하는 것에 있습니다. 각 파트의 자세한 설명을 살펴보면 아래와 같습니다. * **Model** : 내부적으로 쓰이는 데이터를 저장하고, 처리하는 역할을 합니다. 흔히 ‘비즈니스 로직’ 이라고 부르는 부분입니다. View, Presenter등 다른 어떤 요소에도 의존적이지 않은 독립적인 영역이죠.  * **View** : 사용자 인터페이스(User Interface-UI)라 불리는 영역입니다. 안드로이드에서는 Activity, Fragment가 대표적인 예입니다. Model에서 처리된 데이터를 Presenter를 통해 받아서 사용자에게 보여주며, 사용자 액션 및 Activity lifecycle 변화를 감지해서 Presenter에 보내는 역할을 합니다. Presenter를 이용해 데이터를 주고받기 때문에 Presenter에 의존적입니다. * **Presenter** : Model과 View사이의 매개체입니다. View에서 캐치한 유저 액션을 전달 받아서 Model의 로직을 호출하거나, Model의 로직으로부터 나온 결과를 전달 받아서 View에 보내 UI변경을 야기하는 등 둘 사이의 소통의 역할을 합니다. Model, View 모두에 의존적입니다. 'MVP패턴'을 이용해서 이와 같이 Model과 View간의 결합도를 낮추면, 새로운 기능을 추가하거나 변경할 필요가 있을 때 관련된 부분만 수정하면 되기 때문에 확장성이 좋아지며, 테스트 코드를 작성하기 편리해지기 때문에 더 안전한 코드 작업이 가능해집니다. [![MVP pattern](https://tech.buzzvil.com/blog/android-mvp-pattern-what-why-and-how/mvp-android.png)](https://code.tutsplus.com/tutorials/how-to-adopt-model-view-presenter-on-android--cms-26206) ### 3\. 안드로이드에 MVP가 적합한 이유 안드로이드에서 UI를 표현하는 컴포넌트들의 특징은 화면을 시각적으로 직접 그리는 역할 및 화면에 있는 UI 요소들에 대한 액션 처리를 항상 함께 담당한다는 것입니다. 이러한 프레임워크의 특징 때문에 기존에 웹 어플리케이션 등에서 많이 쓰이던 MVC(Model, View, Controller)패턴을 적용하기에는 화면을 그리는 View와 액션을 처리하는 Controller를 완전히 분리하기 어렵다는 한계가 있습니다. 이러한 이유때문에, MVP가 안드로이드에 더 적합하다는 논의가 이어져오고 있는거구요.  안드로이드를 개발한 구글 측에서도 [Android architecture blueprints](https://github.com/googlesamples/android-architecture) 라는 이름으로 MVP 패턴을 적용한 샘플 프로젝트를 공식적으로 운영하는 것으로 보아, 이 패턴은 안드로이드에 더 적절한 것으로 어느정도 검증되었다고 볼 수 있을 것 같습니다.  ### 4\. MVP패턴 적용 전 제가 실무를 통해 MVP패턴을 적용하여 리팩토링 한 부분은 허니스크린 유저 회원가입의 마지막 단계였습니다. 다시 말하자면, 유저에게 닉네임, 나이, 성별, 추천인 등을 입력받아 서버에 회원 가입 요청을 보내는, ‘프로필 입력' 단계라고 볼 수 있을 것 같습니다. 실제 코드를 바탕으로 다루기에는 지면이 충분하지 않기에 간략화된 버전을 이용해 MVP패턴을 적용하는 과정을 중심으로 기술하겠습니다. 기존에 ‘프로필 입력’ 단계를 구현한 Profile Activity의 코드를 간추리면 다음과 같습니다. ```java public class ProfileActivity extends Activity { EditText etNickname; Button btSignUp; String email; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_profile); email = getIntent().getStringExtra("email"); initView(); } private void initView() { etNickname = (EditText) findViewById(R.id.nickname); etNickname.setText(makeNickname()); btSignUp = (Button) findViewById(R.id.button_signup); btSignUp.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (checkNicknameLength()) { callSignUp(); } else { Toast.makeText(ProfileActivity.this, "Nickname is too short", Toast.LENGTH_SHORT).show(); } } }); } private String makeNickname() { int pos = email.indexOf("@"); return email.substring(0, pos); } private boolean checkNicknameLength() { if (etNickname.getText().toString().length() < 4) { return false; } else { return true; } } private void callSignUp() { Map params = new HashMap<>(); params.put("nickname", etNickname.getText().toString()); params.put("email", email); // Add additional parameters RequestInterface.requestPOST("/SIGNUP/", params, new ResponseListener() { @Override public void onSuccess(JSONObject json) { Intent intent = new Intent(ProfileActivity.this, MainActivity.class); Bundle params = new Bundle(); params.putString("user_info", json.getString("user_info")); intent.putExtras(params); startActivity(intent); } @Override public void onError(int code, String message) { Toast.makeText(ProfileActivity.this, message, Toast.LENGTH_SHORT).show(); } }); } } ``` 보시다시피 하나의 Profile Activity 클래스에서 모든 역할을 다 수행하고 있는데요. 이건 간략화한 예시이기 때문에 내용 파악이 쉬운 편이지만 여기에 다양한 뷰 요소가 추가되거나, DB접근, AsyncTask등 복잡한 백그라운드 로직이 추가된다면, 쉽게 내용을 파악하기 힘들 뿐만 아니라 모든 로직이 뒤섞여 있어서 기능을 추가하거나 변경하기 어렵고, Android framework에 종속성을 가지기 때문에 Unit test를 작성하기가 어려울 것입니다. ### 5\. 요구 사항 분석 위의 코드에 구현된 ‘프로필 입력’ 단계의 요구사항은 다음과 같이 정리할 수 있습니다.\[info_box title="Requirements - 1st"\] * 이전 액티비티에서 유저가 입력한 이메일을 전달받아 저장한다. * 닉네임 입력 칸에 이메일을 기반으로 만든 추천 닉네임(이메일의 @앞부분)을 보여준다. * 회원가입 버튼을 누르면 닉네임의 길이가 최소 길이 이상인지 확인하고 아닐 경우 에러 메세지를 보여주고 맞을 경우 다음 단계로 진행한다. * 이메일, 닉네임을 파라미터로 담아 회원가입 API를 호출한다. * 회원가입 API에서 실패 응답을 받으면 서버에서 받아온 에러 메세지를 유저에게 보여주고, 성공 응답을 받으면 회원 가입이 완료되면서 응답으로 받아온 완성된 유저 정보를 담아 허니스크린의 MainActivity로 이동한다. \[/info\_box\]위 요구사항을 세분화하여 특성에 따라 Data, UI 파트로 나누도록 하겠습니다.\[info\_box title="Requirements - 2nd"\] * Data: 이메일을 받아서 저장한다. UI: 액티비티 생성 시 이전 액티비티로부터 전달받은 Intent를 파싱해서 전달한다. * Data : 이메일을 기반으로 추천 닉네임을 만든다. UI: 닉네임 입력 칸에 추천 닉네임을 보여준다. * Data : 닉네임의 길이를 확인한다. UI: 회원가입 버튼이 클릭되는 이벤트를 감지하고 닉네임을 전달한다, 에러 메세지를 보여준다. * Data : 회원가입 API를 호출한다. UI: (없음) * Data : 회원가입 API의 응답을 받는다. UI: 에러 메세지를 보여준다, ProfileActivity에서 MainActivity로 화면을 전환시킨다. \[/info\_box\] ### 6\. 뼈대 구조 완성 Data파트의 요구사항을 구현하는 것이 바로 Model이기에 위의 Data파트의 요구사항 리스트를 통해 Model이 구현해야 할 메소드들의 프로토타입을 정의하도록 하겠습니다. 앞의 코드와 메소드 이름 등 겹치는 부분이 많으나 주목할만한 차이점은 Intent, EditText와 같은 View요소를 접근하는 부분을 제거하고 파라미터로 내용만 전달받도록 바꾼 것, 그리고, API는 비동기적인 응답을 받기 때문에 Listener를 파라미터로 전달해 Model을 이용하는 쪽에(즉, Presenter에) 응답을 전달할 수 있는 구조로 변경한 것입니다. ```java void setUserData(String email); String makeNickname(); boolean checkNicknameLength(String nickname); void callSignup(ApiListener listener); public interface ApiListener { void onSuccess(JSONObject json); void onFail(String message); } ``` UI파트의 요구사항을 ‘UI 변경’이라는 액티브 타입과 ‘이벤트 인식’이라는 패시브 타입으로 나누면 전자는 Presenter가 View를 호출하는 경우, 후자는 View가 Presenter를 호출하는 경우로 나눌 수 있습니다. 즉 전자에 해당되는 '추천 닉네임 보여주기’, ‘에러 메세지 보여주기’, ‘화면 전환하기’는 View에 구현되어야 하며, 후자에 해당되는 ‘액티비티 생성 이벤트 발생 시 인텐트 파싱’, ‘버튼 클릭 이벤트 감지’는 Presenter에 구현되어야 한다는 걸로 정리할 수 있습니다. 이렇게 정리한 것을 기반으로 서로 의존성을 갖는 View, Presenter간의 규약을 ‘Profile’이라는 Java interface에 정의하고, 최종 View, Presenter에서는 각각 Profile.View, Profile.Presenter interface를 구현해 보도록 하겠습니다. ```java public interface Profile { interface View { void showNickname(String nickname); void showErrorMessage(String message); void startMainActivity(JSONObject json); } interface Presenter { void initUserData(String email); void callSignup(String nickname); } } ``` ### 7\. 구현 이제 위에 정의된 프로토타입 및 인터페이스에 맞춰서 실제 클래스를 구현하도록 하겠습니다. 먼저 Model의 경우 Presenter, View에 의존성이 없기 때문에 위의 프로토타입 그대로를 구현하면 다음과 같이 완성됩니다. ```java public class ProfileModel { String email; public void setUserData(String email) { this.email = email; } public String makeNickname() { int pos = email.indexOf("@"); return email.substring(0, pos); } public boolean checkNicknameLength(String nickname) { if (nickname.length() < 4) { return false; } else { return true; } } public void callSignup(ApiListener listener) { Map params = new HashMap<>(); params.put("nickname", etNickname.getText().toString()); params.put("email", email); // Add additional parameters RequestInterface.requestPOST("/SIGNUP/", params, new ResponseListener() { @Override public void onSuccess(JSONObject json) { listener.onSuccess(json); } @Override public void onError(int code, String message) { listener.onFail(message); } }); } public interface ApiListener { void onSuccess(JSONObject json); void onFail(String message); } } ``` View, Presenter의 경우는 약간의 추가 작업이 필요합니다. View는 Presenter에 의존하기 때문에 Presenter 객체를 멤버 변수로 가지고 있으며 정해진 ‘이벤트 발생’ 시점에 해야 할 역할을 Presenter에게 위임하도록 구현해야 합니다. 또한, Presenter는 View와 Model에 종속적이기 때문에 둘 다를 멤버 변수로 가지고 있으며, 요구사항에 알맞게 View, Model의 메소드들을 호출해 UI파트와 data파트의 상호작용을 만들 수 있도록 구현해야 합니다. 이어서, ProfileActivity에서 Profile.View를 구현하면 다음과 같습니다. onCreate() 시점에 Presenter의 initUserData()를 호출하고, 버튼의 OnClickListener 내에서 callSignup()을 호출함으로써 이벤트 발생을 Presenter에게 알리고 있습니다. ```java public class ProfileActivity extends Activity implements Profile.View { EditText etNickname; Button btSignUp; Profile.Presenter presenter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_profile); presenter = new ProfilePresenter(this); presenter.initUserData(getIntent().getStringExtra("email")); initView(); } private void initView() { etNickname = (EditText) findViewById(R.id.nickname); btSignUp = (Button) findViewById(R.id.button_signup); btSignUp.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { presenter.callSignup(etNickname.getText().toString()); } }); } @Override public void showNickname(String nickname) { etNickname.setText(nickname); } @Override public void showErrorMessage(String message) { Toast.makeText(ProfileActivity.this, message, Toast.LENGTH_SHORT).show(); } @Override public void startMainActivity(JSONObject json) { Intent intent = new Intent(ProfileActivity.this, MainActivity.class); Bundle params = new Bundle(); params.putString("user_info", json.getString("user_info")); intent.putExtras(params); startActivity(intent); } } ``` 마지막으로, Profile.Presenter를 구현해서 ProfilePresenter 클래스를 완성시킵니다. 생성자 호출 방법은 여러 가지가 있으나 여기서는 고유한 lifecycle을 갖는 View가 생성되는 시점(onCreate())에 View를 파라미터로 전달하여 Presenter의 생성자를 호출하고 그 내부에서 ProfileModel 생성자를 호출하도록 구현하겠습니다. 나머지 메소드들은 요구사항에 정의된 대로 Model에서 데이터를 처리해서 View에 전달하는 역할을 합니다. ```java public class ProfilePresenter implements Profile.Presenter { Profile.View profileView; ProfileModel profileModel; public ProfilePresenter(Profile.View profileView) { this.profileView = profileView; this.profileModel = new ProfileModel(); } @Override public void initUserData(String email) { profileModel.setUserData(email); profileView.showNickname(profileModel.makeNickname()); } @Override public void callSignup(String nickname) { if (profileModel.checkNicknameLength(nickname)) { profileModel.callSignup(new ProfileModel.ApiListener() { @Override public void onSuccess(JSONObject json) { profileView.startMainActivity(json); } @Override public void onFail(String message) { profileView.showErrorMessage(message); } }); } else { profileView.showErrorMessage("Nickname is too short"); } } } ``` ### 8\. 글을 마치며 이로써 프로필 입력 단계에 MVP 패턴을 적용하여 리팩토링하는 전체 과정이 완료되었습니다. 실제 코드에 이를 적용한 이후 가장 크게 느꼈던 장점은 새로운 기능을 추가할 때 UI, data 파트를 나누어 적용하게 되어서 해야 할 일이 명확해졌고, 그 결과 쉽고 빠르게 적용이 가능했다는 것입니다. 이번에 다룬 MVP패턴은 기존에 정형화된 개발 패턴이 없었던 안드로이드에서 점차 입지를 넓혀가고 있으며, 이에 따라 다양한 적용 사례들을 쉽게 찾아볼 수 있긴 하지만, 이 블로그를 통하여 저는 제가 직접 사례들을 찾아가며 공부할때 느꼈던 막연함을 보완하고 조금 더 쉬운 방법으로 설명할 수 있게 코드에 적용을 마친 결과만이 아니라 요구사항 분석, 인터페이스 정의, 구현 등 ‘과정’에 대해 자세하게 기술하도록 노력하였습니다. 어떻게 보면 작은 성과이지만, 학습하고자 했던 영역 중 하나인 디자인 패턴 적용에 대해 처음부터 끝까지 스스로 학습하고 적용해, 허니스크린 개발에 조금이나마 효율성을 가져다 주었다는데 의의를 찾고 싶습니다. ### Reference - [https://codelabs.developers.google.com/codelabs/android-testing/index.html](https://codelabs.developers.google.com/codelabs/android-testing/index.html#0) - [https://github.com/googlesamples/android-architecture/tree/todo-mvp/](https://github.com/googlesamples/android-architecture/tree/todo-mvp/) - [https://code.tutsplus.com/tutorials/how-to-adopt-model-view-presenter-on-android--cms-26206](https://code.tutsplus.com/tutorials/how-to-adopt-model-view-presenter-on-android--cms-26206) --- ## [How to use Django rest framework](https://tech.buzzvil.com/blog/how-to-use-django-rest-framework-buzzvil) Date: 2016-12-26 | Category: Backend ### 1\. Django를 사용하게 된 배경과 적용 과정 버즈빌이 해당 프레임워크(Django)를 사용하게 된 지는 아마 한 1년 반 정도 전인 2015년 봄쯤이었던 것으로 기억합니다.  그 전에는 Django 함수를 기반으로 한 개발이 주로 되어 있었습니다. 그러다 새로운 프로젝트를 진행하게 되었고, 그때, 회사의 다른 개발자분들과 새로운 프로젝트를 진행할 스펙과 기술에 대해 정하던 중에, Front-end와 Back-end를 분리해야 할 필요가 있어서 알아보게 되었고,  Back-end에서는 Django rest framework를, Front-end에서는 Angularjs를 알게 되어 그때 Back-end 기술의 일환으로 Django Rest framework의 사용 결정했습니다. 먼저, 기존의 코드와 Django rest framework를 사용해서 어떤 식으로 단점을 보안 하였는지 말씀드리도록 하겠습니다. 첫번째로, 아래 보이는 코드가 기존 프로젝트에서 view를 return 할때 사용한 코드 입니다. 함수 기반의 view를 사용해서 작업을 했었습니다. ```python def test_view(self, request): return render_to_response('test', {}) ``` 이 view 방식의 단점은 front-end코드와 back-end코드가 맞물려 있다는 단점과, html 코드가 아닌 다른 코드를 리턴해야 할 필요가 있을 경우, 모든 코드의 수정이 필요하다는 것입니다. 아래의 코드는 json을 리턴하고 싶을 때 사용했던 코드 입니다. _(더 좋은 방법이 있을수도 있으나, 간단하게 parameter를 받아서 작업을 하였습니다.)_ ```python def test_view(self,request): # Return JSON type if request.GET.get('type') == 'json': return HttpResponse(data, content_type='application/json') return render_to_response('test', {}) ``` 이 또한 마찬가지로 http method를 분기 처리 할때 똑같이 발생하는 문제였습니다. ```python def test_view(self,request): # Return JSON type if request.method == 'POST': blahblah... if request.method == 'GET': return render_to_response('test', {}) ``` 이러한 문제가 발생함에 따라 처음에는 저기까지는 뭐 노가다로 하면 할수 있다고 생각이 들수도 있긴 합니다만,  Front-end가 점점 복잡 해짐에 따라서 **“ID로 필터를 걸어주세요.! 리턴되는 데이터가 너무 많으니 pagination처리해서 UI에 보여주고 싶어요!” **라는 요구 사항들이 들어오고, 많은 요구 사항에 대해 어떻게 유연하게 처리해 줄수 있지? 라고 생각을 하게 되었습니다. 오랜 고민 끝에, **"그래! 장고에서 pagination을 지원하니, 일단 그걸 쓰도록 해볼까?? filter는 어떻게하지?? 아 그냥 손수 parameter 받아서 구현하지 뭐 얼마나 많아 지겠어."** 라는 생각이 들어 구현 작업에 나섰습니다. 그리하여 나온 이 아래 코드가 filter와 장고에서 지원하는 pagination을 적용시킨 코드 입니다. 보시면 try…except 같은 코드가 pagination처리할때 보여서, 코드를 보기에도 좋아 보이지 않죠? ```python from django.core.paginator import Paginator def test_view(self,request): model_list = Model.objects.all() # Filter some_filter = request.GET.get('some_filter') if some_filter: model_list = model_list.filter(some=some_filter) ... # Pagination paginator = Paginator(model_list, 25) page = request.GET.get('page') try: models = paginator.page(page) except PageNotAnInteger: models = paginator.page(1) return render_to_response('test', {'models': model}) ``` 좋아.. 여기까지는 우리가 잘 버텼어!! 근데, 데이터 validation처리도 해야 하는데 어떻게 하지? 장고 Form을 이용하자!!! 음.. 그럼 사용할때 마다 Form이 필요한거겠네..라는 생각이 안 들 수가 없는데요. 그래서 고안한 방법이 아래의 코드입니다. ```python # test/forms.py from django.forms import ModelForm from test.models import Model class TestForm(ModelForm): class Meta: model = Model def clean(): cleaned_data = super(TestForm, self).clean() # add validation code return cleaned_data # test.views.py from django.core.paginator import Paginator from test.forms import TestForm def test_view(self,request): # Return JSON type if request.method == 'POST': form = TestForm(request.POST) if form.isvalid(): form.save() else: blahblah if request.method == 'GET': return render_to_response('test', {'models': model}) ``` 어찌어찌하여 여기까지는 잘 버텼다고 생각했지만 알고보니 아래와 같은 더 많은 요구사항들이 있었습니다. * 권한 별로 데이터의 제한도 두고 싶고 한데 이런건 어떻게 처리하지? * URL별로 권한은 설정 할수 있는데, 데이터의 대한 권한은 처리할수 있을까?? * 날짜 데이터를 내려줄때 해당 유저의 로컬 시간으로 보내줄수는 있을까? 위의 문제점들을 해결하기 위하여 이 작업을 또하긴 싫은데 어떻게하지 생각하다가 이 단점들을 보안할 수 있고, 나중에도 사용할수 있는 Django rest framework를 알게 되었고, 저희의 문제점을 해결해 줄 수 있을 거 같다는 생각이 들었습니다. ### 2\. Django Rest Framework에 대하여 Django rest framework 의 장점을 말씀드리자면 다음과 같습니다. * API 지원 * 다양한 authentication 지원 * class 기반의 구현 방식 * 유저 권한 별 데이터 제한 가능 * 다양한 return 타입 제공 e.g) json, csv, excel… * custom을 통해 확장 가능 위의 장점들에서도 알 수 있듯이 Django Rest Framework는 API레벨도 잘 지원하면서, Back-end와 Front-end를 분리할수 있고, Django와도 잘 맞는 프레임워크였고, 더불어, 새로운 프로젝트를 진행하는 시점에서 새로운 기술을 사용하는건 새로운 도전이라고 생각해서 사용하기로 결정하였습니다. 그래서 저희가 처음으로 도입한 방법은 문서에 있는 class 베이스의 generics package에 있는 것을 상속 받아서 구현하는 것이였습니다. 이게 처음에 짠 코드 입니다. 위에 비교해서 엄청 깔끔 해졌죠? ```python # test/urls.py urlpatterns = patterns( 'test.views', url(r'^/api/tests/(?P\d+)', TestDetailView.as_view()), url(r'^/api/tests', TestListView.as_view()), ) # test/views.py class TestListView(generics.ListCretateView): queryset = Model.objects.all() serializer_class = ModelSerializer class TestDetailView(generics.RetrieveUpdateDestroyAPIView): queryset = Model.objects.all() serializer_class = ModelSerializer ``` 이러한 방법으로 개발의 속도를 높이고 있던 와중에 점점 url이 많아짐에 따라 view도 많아지고, copy&paste 되는 코드도 많아 관리상의 이슈가 생기게 되었습니다. 이건 샘플로 url이 많아졌을 경우에 view를 이용해 컨트롤 할수 있는 방법입니다. ```python # test/urls.py urlpatterns = patterns( 'test.views', url(r'^/api/tests/(?P\d+)/pets/(?P\d+)', PetsDetailView.as_view()), url(r'^/api/tests/(?P\d+)/pets', PetsListView.as_view()), url(r'^/api/tests/(?P\d+)', TestDetailView.as_view()), url(r'^/api/tests', TestListView.as_view()), ) # test/views.py class TestListView(generics.ListCretateView): queryset = Model.objects.all() serializer_class = ModelSerializer class TestDetailView(generics.RetrieveUpdateDestroyAPIView): queryset = Model.objects.all() serializer_class = ModelSerializer class PetsListView(generics.ListCretateView): queryset = Pet.objects.all() serializer_class = PetSerializer class PetsDetailView(generics.RetrieveUpdateDestroyAPIView): queryset = Pet.objects.all() serializer_class = PetSerializer ``` 또한, url(N) === view(N) 개 생기는 관리상의 이슈와 확장성의 이슈를 해결할수 있는 좋은 방법이 없을까 고민하던 와중에, Django rest framework에 Viewset과 Router를 통해 view들의 집합을 만들수 있다는 것을 알게 되었고, 이것을 통해 위와 같은 관리상 이슈는 해결 할 수 있다는 것을 알았습니다. 이 아래 코드가 viewset과 router를 사용했을 때의 코드 입니다. 깔끔하게 정리 되었죠?? ```python # test/urls.py router = SimpleRouter() router.register(r'/pets', PetViewSet) router.register(r'/', TestViewSet) urlpatterns = patterns( url(r'^/api/tests', include(router.urls)) ) # test/viewsets.py class PetViewSet(viewsets.ModelViewSet): serializer_class = PetSerializer queryset = Pet.objects.all() class TestViewSet(viewsets.ModelViewSet): serializer_class = ModelSerializer queryset = Model.objects.all() ``` ### 3\. 추가적으로 해결해야할 문제들 이제 해결해야 할 문제는 페이징 처리와 filter에 관련된 내용인데요. 이 문제도 Django rest framework에서 제공하는 pagination\_class와 filter\_fields, search\_fields ordering\_fields를 쉽고 보기 좋게 해결 할 수 있었습니다. * pagination_class 는 페이징 처리시에 참조가 되는 class의 이름을 세팅하는 변수이고, 세팅이 되어 있을 경우에 리턴 되는 데이터를 페이징 처리해서 보여줍니다. 페이징 처리를 하였을 때랑 페이징 처리를 하지 않았을 경우, return 되는 데이터의 구조가 달라지게 됩니다. **(중요!!)** * pagination_class가 세팅 되어 있을 때 return되는 데이터의 구조 { count: 1000, results: \[…\] } * pagination_class가 세팅 되지 않았을 때 return되는 데이터의 구조 \[…\] * filter_backends 변수는 해당 class로 요청이 들어왔을 때 사용할 filter들을 정의할때 사용됩니다. 밑에 예제에서 보면 저는 3개의 필터(SearchFilter, OrderingFilter, DjangoFilterBackend)를 사용 했습니다. * SearchFilter * 사용법: url에 search parameter에 들어오는 값에 대한 filter설정을 할수 있습니다. e.g) www.superbong.com?search=daniel * search_fields에는 search에서 들어온 값이 필터될 fields를 설정합니다. * OrderingFilter * 사용법: url에  ordering에 들어오는 이름으로 ordering을 설정할수 있습니다. (* 앞에 -가 있을 경우 내림차순으로 ordering이 정렬됩니다.)  e.g) www.superbong.com?ordering=-id * ordering_fields에는 ordering으로 사용할 fields를 설정하게 됩니다. * DjangoFilterBackend * 이것은 url에서 fields이름으로 filter를 사용하고 싶을때, 사용하는 filter입니다. * url 뒤에 parameter로 column=값 을 설정하여 사용하면 됩니다. * filter_fields를 통해 column으로 들어올 fields를 설정할수 있습니다. * 그 외에 들어오는 column에 대해서는 무시 됩니다. 짜잔!! 이 아래에 있는 코드가 filter와 ordering을 썼을때의 최종 코드 입니다. ```python # test/utils.py class LargePagination(PageNumberPagination): page_size = 10 page_size_query_param = 'page_size' max_page_size = 10000 # test/viewsets.py class PetViewSet(viewsets.ModelViewSet): pagination_class = LargePagination serializer_class = PetSerializer queryset = Pet.objects.all() filter_backends = (filters.SearchFilter, filters.OrderingFilter, filters.DjangoFilterBackend, ) filter_fields = ('filter1', ) serach_fields = ('id', 'name', ) ordering_fields = ('id', 'created_at',) ordering = ('-id') ``` Django rest framework의 또 다른 장점이라고 할수 있는건, 권한 및 유저 별 데이터의 포맷을 변경 하기 용이 하다는 것입니다. 저희는 Serializer를 사용해서 포맷이나 데이터의 제한을 두고 있습니다. 아래와 같이 Serializer를 통해 client에 들어오는 데이터(to\_internal\_value)와 나가는 데이터(to_representation)의 변환을 쉽게 할수 있습니다. * **to\_internal\_value** * POST, PUT과 같이 데이터 변경이 있을 때 client에서 들어오는 데이터를 저장 전에 핸들링 할수 있는 함수입니다. * **to_representation** * GET, POST, PUT과 같이 데이터 변경이 있고 난 후에 client에 값을 변환해서 보여줄 경우 사용하는 함수 입니다. ```python # test/serializers.py class AllDataReturnSerializer(serializers.ModelSerializer): class Meta: model = Model fields = '__all__' def to_representation(self, val): return super(AllDataReturnSerializer, self).to_representation(val) def to_internal_value(self, val): return super(AllDataReturnSerializer, self).to_internal_value(val) class SomeDataReturnSerializer(serializers.ModelSerializer): class Meta: model = Model fields = ( 'some1', 'some2', 'some3', ) # test/viewsets.py class PetViewSet(viewsets.ModelViewSet): … def get_serializer_class(self): if return_all_data: return AllDataReturnSerlaizer elif return_some_data: return SomeDataReturnSerializer ``` ### 4\. 끝으로 Django rest framework를 쓰면서 느끼는 점은 완벽하지 않아도 규약이나 규칙을 통해 개발자의 개발 시간과 가독성을 올리는 데 한 몫을 하였습니다. 그와 별개로 인제 Django rest framework를 사용 한 후에 저희가 다음으로 신경 쓰는 부분은 테스트 인거 같습니다. 개발자가 많아지므로 인해서 관리해야 하는 코드도 많아지고, 변경을 했을 때의 생각치도 않은 곳에서 발생하는 오류가 점점 커지면서 소모되는 리소스의 양도 점점 많아지고 있습니다. 그래서 지금까지는 Back-end API레벨단의 테스트만 진행하고 있는데, 앞으로의 고민은 Front-end에 대한 코드가 점점 커지면서, 테스트에 대한 Needs가 필요한데, 이것에 대한 처리를 어떻게 해야 좋을지, 화면단의 테스트 코드까지 테스트코드로 진행해야 할지 고민해야 하는 큰 문제 인거 같습니다. 이에 앞으로도 많은 고민이 수반되어야 할 것 같습니다. --- ## [미니 게임 개발기](https://tech.buzzvil.com/blog/게임-개발기) Date: 2016-11-23 | Category: Frontend 버즈빌은 게임회사인가? "미니 게임 개발기" 라는 제목을 붙여놓으니 게임회사 같기도 하다. 오해하지 않도록 먼저, 버즈빌은 No.1 잠금화면 애드네트워크를 지향하는 애드 테크 회사라는것을 말해두고 싶다. 버즈빌 블로그에 올라온 다른 글들을 보더라도 대부분은 애드 테크 관련 글을 다루고 있다. 관련 기술을 기대하고 이 글을 보신 분들에게는 다소 의아할 수 있지만, 버즈빌에서 가끔씩은 하던 일에서 벗어나 이런 소소한 일탈이 허용된다는 것을 알리고 싶었다. ### 1\. 어느날 갑자기 "나는 왜 개발자가 되었는가?” 라는 질문에 곰곰이 생각했던 적이 있었다. 컴퓨터에 대한 관심이 개발자가 되도록 했고, 컴퓨터에 대한 관심은 어릴때부터 즐겨하던 게임들 때문이었다. 결국 게임 덕분에(?) 현재 개발자를 하고 있다. 대부분의 개발자들이 게임을 좋아하는 것으로 보아, 나 뿐만 아니라 게임을 좋아해서 개발자의 길로 들어서게 된 사람들이 꽤나 많을것 같다. 이러한 게임에 대한 관심때문에, 막연히 언젠간 게임을 만들어보고 싶다고 생각만 하고 있었다. 그러던 중 버즈빌에서 서비스하고 있는 허니스크린 안드로이드 앱의 UI를 개편할 일이 있었는데, 구현하고 보니 어디선가 본듯한 친숙한 모습을 하고 있다. 화면 하단의 슬라이더 뷰를 보고, 회사내의 다른 개발자가 알카노이드(벽돌깨기) 게임과 비슷하다고 말해주었고 이 말한마디에 한번 해볼까? 하고 미니 게임을 만들게 되었다. ![](https://tech.buzzvil.com/blog/%ea%b2%8c%ec%9e%84-%ea%b0%9c%eb%b0%9c%ea%b8%b0/minigame.jpg) ### 2\. 그래서 무엇을? 어찌보면 딴 짓이라고 생각될 수 있는 일이 아닌 일을 시작하게 되었다. 그러다보니 이 일에 시간을 많이 쓸 수는 없을 뿐더러, 회사내의 다른 리소스를 활용할 수도 없었다. 그래서 알카노이드의 벽돌 및 스테이지까지 모두 만들기는 무리였고, 우리 UI에 맞는 최대한 단순한 미니 게임을 만들기로 하였다. 그래서 다음과 같이 구현해야 할 리스트를 작성하였다. 처음으로 게임 비슷한 것을 만들려고 하니 아주아주 단순하게 만들기로 하였다. #### 구현해야 할 리스트 * 사람의 손으로 하단 받침대를 좌우로 컨트롤 할 수 있어야 한다. * 공은 화면 랜덤한 위치에서 생성되어서 시작된다. * 받침대와 벽을 부딪힐때마다 점수를 획득한다. * 게임이 진행됨에 따라 공을 빨라지게 하여 난이도를 높여주도록 한다. * 게임 종료시에 획득한 점수를 보여준다. * 스코어를 서버에 기록하여 랭킹을 보여준다. ### 3\. 클라이언트 개발 게임 개발을 시작하는데 있어서 게임 엔진을 고려해볼 수도 있겠지만, 우리 앱은 게임 앱이 아니다. 앱의 성격과 상관없는 기능을 넣는데 있어서 자칫 무거워질 수 있는 게임 엔진을 적용할 수는 없는 노릇이었다. 게임이란 것은 단순하게 생각하면, 사용자 인풋을 받아서 화면에 보이는 그래픽 요소들을 유저가 기대하는대로 변경해주면 되는 것이다. 만들려고 하는 미니게임도 마찬가지로 터치 이벤트를 사용자 인풋으로 하여 받침대와 공의 위치를 변경해주면 되는 것이다. 이러한 단순한 생각으로 클라이언트 구현을 시작하자. #### 공 움직이기 공은 한 방향으로 이동하다가 벽이나 받침대에 충돌하면 운동방향이 변경된다. 이게 전부다. 이를 위해서는 공과 벽과의 충돌 로직 및 공의 위치 변화를 구현하면 된다. 여기서 공은 단순 원이기 때문에 중심좌표와 반지름으로 표현될 수 있고, 벽과 공의 충돌은 공의 중심부터 벽과의 거리가 반지름과 같을 때로 판별할 수 있다. 그리고 공의 운동변화는 공의 중심좌표 이동이라고 할 수 있다. 게임 프레임 사이의 중심좌표의 이동은 다음과 같이 표현된다. ``` X' = X + Dx Y' = Y + Dy ((X, Y) : 현재 위치, (X', Y') : 다음 위치, (Dx, Dy) : 한 프레임에서의 위치 변화) ``` 충돌하기 전까지는 일정한 Dx와 Dy를 통해 이동하다가 충돌했을 때 각각의 값이 변한다. 충돌했을 때의 공의 운동방향 변화는 벽면과 수직 성분에 대해서만 이루어지기 때문에, 완전탄성 충돌을 가정한다면 속도의 방향만 바꿔준다. 따라서 좌우의 벽은 위 식에서 Dx만 부호를 변경해주고, 위아래의 벽은 위 식에서 Dy의 부호만 변경하여 준다. 이를 통해 오른쪽 벽 충돌 후 운동 변화를 구현한 로직은 다음과 같다. ``` if (rightPositionOfWall <= centerXOfBall + radius) { Dx *= -1 } centerXOfBall += Dx ``` #### 터치 이벤트를 수신하여 받침대 움직이기 안드로이드에서 터치 이벤트는 [액티비티](https://developer.android.com/reference/android/app/Activity.html)(안드로이드 앱내에서 하나의 화면에 대응)에서 발생하기 때문에 액티비티 내의 터치 이벤트를 수신하여 간단히 얻어낼 수 있다. 처음 손가락으로 화면을 눌렀을 때의 X좌표를 저장하고, 손가락 이동에 따른 X좌표의 변화만 안다면 받침대의 새로운 위치를 알아낼 수 있다. #### 공의 받침대 충돌 여기까지 구현한 것으로 기본적인 공의 움직임은 완성되었다. 손가락으로 받침대를 이동하고, 공은 의도한대로 벽과 받침대를 충돌하며 움직였다. 하지만 이 상태로는 공의 움직임이 너무 단조로웠다. 이 때문에 알카노이드 게임을 다시 플레이 해보았고, 받침대는 공의 움직임에 변화를 주는 방식이 벽과는 조금 달랐다. 벽과의 충돌은 일반적인 충돌원리가 적용이 되었지만, 공은 받침대의 어떤 위치와 충돌하는지에 따라 다른 움직임을 만들어냈다. 몇 번 플레이를 해보니 받침대의 중심점에서 오른쪽에서 충돌이 일어난 경우는 공이 왼쪽에서 날아오든 오른쪽에서 날아오든 상관없이 충돌 후 오른쪽으로 이동하였다. 반대로 받침대의 왼쪽에서 충돌이 일어난 경우는 무조건 공이 왼쪽으로 이동하였다. 그리고 받침대 끝에 맞을 수록 X성분으로의 이동이 더 커지는 방향으로 진행했다. 이를 통해 받침대 충돌시 일어나는 운동변화는 다음과 같이 수정하였다. ``` if collision_with_bottom_slider: Dx = (centerXOfBall - centerXOfSlider) / lengthOfSlider * D Dy = -sqrt(D*D - Dx*Dx) ``` (y좌표는 아래방향이 양의 방향이기 때문에, 받침대를 튕기고 위로 이동하기 위해 Dy는 음의 값을 갖는다, D=sqrt(Dx²+Dy²)) 이제 점점 공이 빨라지도록 하여 난이도를 높여 나가야 했다. 이를 위해 받침대에 공이 충돌할때마다 D값을 높여주면서 공의 빠르기를 높여나갔다. 점수는 공이 벽이나 받침대와 충돌할 때마다 1점씩 쌓이게 하였다. 특정 공 빠르기 이상이 될때 무조건 게임오버가 된다고 가정하면, 높은 점수를 획득하기 위해서는 받침대와의 충돌을 최소화 하면서 점수를 많이 획득해야 한다. 이렇게 플레이를 하기 위해서는 받침대의 중심과 가장 먼 위치로 충돌시켜서 다음 받침대 충돌까지 가장 많은 벽과의 충돌을 만들어 내야 한다. #### 게임 객체 그리기 안드로이드에서는 일반적인 뷰를 UI 쓰레드 내에서 그리게 된다. 사용자의 터치 이벤트를 포함한 대부분의 인풋도 UI 쓰레드 내에서 처리한다. 만약 일반적인 뷰를 통해 게임 객체들을 그리게 된다면, 그리고자 하는 뷰가 많아지면서 터치 이벤트 처리에 지연이 생기고 이 때문에 게임이 느려지거나 심지어는 ANR(안드로이드에서 응답이 없을 경우 에러)이 발생하기도 한다.따라서 공이나 받침대와 같이 게임 플레이시에 변화하는 객체들을 그려내기 위해서는 일반적인 뷰를 그리는 방식으로는 자연스러운 게임 움직임을 그려낼 수 없다. 이를 해결하기 위해서는 안드로이드에서 제공하는 [SurfaceView](https://developer.android.com/reference/android/view/SurfaceView.html)를 사용하면된다.SurfaceView를 사용하게 되면 화면 그리기를 UI 쓰레드가 아닌 다른 쓰레드에서 처리하도록 만들 수가 있다. 따라서 터치이벤트만 SurfaceView에 지속적으로 전달하고, 받침대 및 이에 따른 공의 움직임을 SurfaceView를 통해 그려내면 된다. ![](https://tech.buzzvil.com/blog/%ea%b2%8c%ec%9e%84-%ea%b0%9c%eb%b0%9c%ea%b8%b0/game_on_hs_ui.jpg) ### 4\. 서버 개발 원래는 클라이언트만 구현하고 적당히 마무리 하려고 하였다. 그리고 허니스크린의 UI가 변경되면서 UI에서 확장된 미니게임의 의미가 없어진것 같아 미니 게임을 제거하려고 하였다. 하지만 그동안의 게임 플레이 현황을 보니 매일 몇 만번의 플레이가 일어나고 있었고 허니스크린 네이버 연관검색어 상위권에 랭크되어 있었다. 제거한다면 많은 유저들의 악플에 시달릴 것 같아 계속 가져가기로 하고, 이참에 이벤트성으로 서버에서 랭킹 페이지까지 만들어 보기로 하였다. 서버에서 점수를 기록하여 1~30위까지의 점수 및 본인의 랭킹을 보여주기로 하였다. 구현 방법으로는 DB 사용, 로그 분석 등 다양한 방법을 생각할 수 있다. DB는 구현이 단순하지만 데이터 양이 많아질 수록 부담을 주게 될 테고, 로그 분석은 실시간 성이 떨어진다. 물론 이를 해결할 수 있는 다양한 방법이 있겠으나 들어가는 리소스를 생각해야만 했다. 최소한의 리소스로 구현 방법을 찾던중 다행히 우리가 사용하는 Redis에서 아주 쉽게 랭킹을 구현할 수 있는 기능을 제공하고 있었다. [Redis](http://www.redis.io/)는 인메모리 기반 데이터 베이스로 다양한 자료구조와 커맨드를 지원하였다. ZADD 커맨드를 사용하여 스코어를 기록하면 언제든지 ZRANGE 커맨드를 통해 높은 점수 순으로 원하는 순위까지의 데이터를 얻을 수 있다. 여기서 주의할 점은 ZADD는 한 유저에 대해 이미 스코어가 기록된 경우 기존 스코어를 업데이트 하고 새로운 랭킹이 계산된다.따라서 점수를 기록할 때 매번 ZADD를 호출한다면 유저의 최근 점수 기준으로 랭킹이 계산된다. 현재 점수 기준이 아닌 각 유저 별 최고기록으로 랭킹을 보여줘야 하기 때문에 ZSCORE를 통해 먼저 유저의 점수를 가져오고 더 높은 점수를 획득한 경우만 ZADD를 호출하도록 하였다. 열심히 ZADD로 점수를 기록하고 랭킹은 ZRANGE를 통해 가져오면 끝이다. Redis 덕분에 스코어 랭킹 시스템은 쉽게 구현이 되었다. ### 5\. 결과 위에서는 주요 로직들에 대해서 다루었지만, 실제로는 게임 어뷰징을 막기위한 안전장치들과 많은 예외상황 처리들도 포함되어 있기에 생각보다 많은 노력이 들어갔다. 랭킹페이지를 구현하고 3일간의 랭킹 이벤트를 진행하였는데, 이 기간동안 한국, 대만, 일본 합쳐서 60만 번의 점수가 기록되었고, 유저별 최고득점은 327점까지 나왔다. 아직은 게임이라고 하기엔 자연스럽지 못한 부분도 많고, 최적화 할 부분도 많지만 다행이 별 사고 없이 미니게임 이벤트가 종료되었다. 처음에는 이스터 에그로 시작되었지만 이벤트에도 활용되면서 하나의 기능이 되었기에 더 이상 이스터 에그라고 하기는 힘들것 같다.그렇다면 다음에는 어떤 이스터 에그를???.. --- ## [버즈빌 개발자 회고록, "우리 개발자 자니"](https://tech.buzzvil.com/blog/buzzvil_developer_lookback_essay) Date: 2016-08-01 | Category: Culture ##### "우리 개발자 자니? 서버가 다운됐네. 넝담 ㅎㅎ" 첫 입사날, 동료가 건내준 스티커이다. 무시무시하다. 실제로 일하는 동안 밤 낮 가리지 않고 일한 적이 많았다. 주말에도 회사 맥북을 켜서 이것저것 끄적이곤 했다. 그 원동력은 나중에 혹시라도 스타트업을 창업한다면 개발자로서 알아야 될 A부터 Z 까지 모든걸 배우고 싶다는 욕심이었다. 1년이 지난 지금, 이 순간에도 끊임없이 코드를 짜고 있을, 혹은 스타트업은 어떤 곳일지 기웃기웃 하고 있을 개발자들에게 나의 스타트업 1년 길고도 짧았던 이야기를 들려주고자 용기를 내어 글을 시작한다. 글을 읽고 "아!" 내지는 "오~" 라고 느낀다면 뿌듯할 것 같다. 이 포스트에서 넓게는 스타트업에서, 좁게는 버즈빌에서, 주니어 개발자로 일하면서 느꼈던/배웠던/깨달았던 소중한 경험들을 가감 없이 풀어보려한다. disclaimer! ----------- 스타트업의 문화는 정말 가지각색 천차만별이다. 스타트업 문화에 대한 보편적 담론은 애당초 존재할 수 없기에, 이 글은 버즈빌에서의 나의 경험을 바탕으로 한 지극히 주관적 생각임을 미리 밝혀둔다. ![](https://tech.buzzvil.com/blog/buzzvil_developer_lookback_essay/13918382_10208282853446765_2047325912_o.jpg) 아니 이게 무슨 짤방인가? 하고 있으리라 예상 된다. 버즈빌에서 겪은 경험들을 다양한 각도에서 서술하고자 무려 5개로 나눴다. 5개라니(..) 드르륵 내리고 있다면 한줄 요약을 미리 전한다. ##### "매우 재미있었다. 또 하고 싶다." 거두절미하고 이제부터 하나씩 서술해본다.   What my friends think I do? --------------------------- 군복무로 4학기를 연속으로 휴학하고, 모두가 이제 학교를 돌아갈 것이라고 생각했지만, 복학을 미루고 스타트업에서 일하기로 결정했을 때, "복학을 미루고 스타트업에서 일할거에요." 라는 말을 꺼내기도 무섭게 "맙소사(..)" 라는 반응부터 "오~" 라는 반응까지, 그리곤 호기심 섞인 질문들이 엄청나게 쏟아졌다. 1년이 지난 지금까지도 스타트업에서 일한다고 하면 굉장히 많은 질문 공세가 이어진다. 그 중 가장 빈번한 질문은 바로 이거다.   ##### "정말 수평적인가요? 그게 문화적, 업무적으로 어떤 의미인가요?"   한치의 고민도 없이 그렇다! 적어도 내가 경험한 버즈빌은 정말 그러하다. 50명에 달하는 적지 않은 수의 사람들이 함께 일하는 조직이었음에도 불구하고 모든 분들이 예외 없이 친구 같은 끈끈한 친밀감과 가족같은 편안함을 주는 분들이었다. 새 학기, 새 친구들을 한 명 씩 알아갈 때의 그 설렘을 회사라는 곳에서 다시 느끼게 될 줄은 상상이나 했을까. 더욱 놀라웠던 건, 친구처럼 지내면서도 일과 관련해서는 프로페셔널함을 잃지 않는 모습이었다. 친함에 기대는 ‘온정주의’가 아닌 그 정서적 친밀도를 바탕으로 높은 수준의 협업을 하고 결과를 만들어내는 ‘성과주의' 라고 설명할 수 있을 것 같다. 가끔은 카톡과 페이스북까지도 공과 사의 구분이 사라지면서 일상이 곧 업무였던건 함정! 하지만, 이 또한 즐겼던 건 자랑! 이렇게 서로 막역하게 지내면서도 업무적으로 전문성을 잃지 않았던 것은 멤버들의 주인 의식에 기반한다. 모두가 회사 일을 마치 내 일처럼 생각하고 있기 때문에, 내가 하는 일에 확실히 책임을 지고 부단히 노력한다. 내가 맡은 일에서 최상의 결과를 내는데 “원래 그런 것" 혹은 “그냥" 이라는 건 존재할 수 없다. 조금이라도 의심이 드는 일은 옳고 그름의 시시비비를 토론하고 최적점을 찾아간다. 엉뚱한 말을 하고 있을 수 없기 때문에 끊임없이 자기계발을 위해 노력했고, 서로 다른 의견들이 교환되며 최적 혹은 최선의 방향으로 결론이 도출되어 나가는 과정에서 많은 것을 배울 수 있었다. 여담이지만, 이 과정에서 본인의 횡설수설함에 고통 받았을 동료들에게 늦게나마 사과를 전한다. 잘하려던 노력으로 봐주길 (..)   개발적으로도 본인의 의지만 충분하다면 굉장히 다양한 영역에서 자유도 높은 일을 경험할 수 있다. 정말 산더미 같이 많은 일들이 산재되어 있는데(..), 이를 똑똑한 동료들과 함께 해결해 나갈 수 있다는 것은 축복이었다. 개발의 분야에서 좋은 팀워크는 1 더하기 1은 2 이상을 의미 한다고 믿는다. 버즈빌에서의 1년은 훌륭한 동료들과 협업하면서 2 이상을 경험했던 시기였다. 그 중에서도, AWS 인프라를 자유롭게 활용할 수 있던 경험은 특권 중 특권이다. AWS 가 제공하는 여러가지 서비스들을 다뤄보면서 최신 동향들을 파악할 수 있었고 장단점을 충분히 고려해 나가는 과정에서 해당 기술에 대한 이해를 높일 수 있었다.   버즈빌이 이렇게 수평적인 문화 / 업무 환경을 갖추게 된데는 외국인 동료들의 역할도 컸다. 글로벌 스타트업을 지향하는 스타트업답게, 버즈빌에는 실리콘 밸리 출신의 미국인 개발자부터, 프랑스인 수석 디자이너 외 아시아, 미주, 구주, 아프리카 출신의 멤버들이 있다. 한국말로 대화할 수 없기 때문에 이들과의 대화는 수평 그 자체일 수 밖에 없었다. 영어로 자유롭게 소통하는건 매우 고통스러운(..) 일이었지만, 글로벌 기업에서 일하게 된다면 꼭 거쳐야 할 통과의례를 미리 값싸게 치뤄본 것일지도. 그리고 그 과정에서 얻은건 단순히 자유롭게 소통하는 능력을 넘어서는 다양성을 존중하고 소통하는 방식 그리고 폭 넓은 시야, 그리고 더욱 명료한 논리성을 포함한다. 개발자들과의 소통에서 모두가 수평할 때, 남는것은 논리성이다.외국인 개발자들과 영어로 “핵심"에 대해 이야기하면서 그 어느때보다 빠르게 논리적인 사고에 익숙해졌다. 수평적인 문화는 단순히 편안한 근무환경과 좋은 사람들만으로는 다 설명할 수 없는 폭 넓은 학습의 기회였고, 회사와 개인의 성장을 촉진하는 기반이었다.   What my parents thinks I do? ---------------------------- 우리 부모님을 포함하여, 아마 많은 부모님들이 “스타트업" 이라는 단어 자체가 생소하지 않을까 싶다. 부모님이라고 적어놨지만 사실 스타트업이 생소한 모든 사람들을 생각하면 적는다. 어디나 그렇겠지만, 외부에서 보이는 장밋빛 모습 이면에는 스타트업만이 지닌 고유한 문제들도 많다. 다행스러운 것은, 이를 해결해나가는 과정에서 얻는 소득은 매우 값지다는 점이다. 스타트업에 조인하기 전, 창업은 "언젠가 한번 도전해 볼까?” 할만한 가벼운 주제였다. 지금 생각보해면 웃음이 나온다. 성공하는 스타트업을 만들기 위해서 얼마나 많은 노력과 실력과 운과 그리고 좋은 사람이 필요한지 몰랐던 때였으니까. 스타트업에서 일해보았지만, 아직도 경험이 많이 부족하다. 창업 경험도 없으면서 “스타트업에서 성공하기 위해 필요한 10가지” 와 같은 글을 적는 우를 범하고 싶지는 않다. 대신, 나중에 봐도 부끄럽지 않을 만큼 직접 경험한 개발자의 이야기 담백하게 적어보려 한다. 스타트업에서 좋은 개발자가 되려면 이 두가지 만큼은 갖춰야 하지 않을까한다.   #### 1\. 다산. 스타트업이 직면하는 정말 많은 문제들을 해결 하는 건 쉽지 않은 일이다. 개발에만 집중하다가도 틈틈이 들어오는 긴급 요청들, “이거 안돼요” 하는 동료들의 제보 등등 어느 하나 버릴 수 없는 일들이다. 이 때, 개발자의 생산성을 높이는 다양한 프레임워크들과 강력한 클라우드 인프라라는 두 친구가 있다면 스트레스로 파괴되는 뇌세포들을 조금이라도 줄일 수 있지 않을까. 새로운 툴을 빨리 습득하고 능숙하게 다루는 것은 분명 능력이다. 또한, 다산하는 프로그래머가 되기 위해선 오버엔지니어링을 줄이고 트레이드 오프들을 잘 정의할 줄 알아야 한다. 다산한다고 해서 아무도 안쓰는 코드를 짜는건 의미 없다. 디자인 단계에서 버릴건 버리고 챙길건 챙겨서 오래도록 쓰일 코드를 남기자.   #### 2\. 퀄리티. 다산하면서도 중요한건 코드의 품질이다. 자신이 짠 코드 한줄 한줄이 모두 유지보수 및 관리해야 할 코드라는 생각을 항상 해야한다. 버그가 없는 코드를 짜는 것은 당연히 필수다. 물론, 마음처럼 잘 되지 않는다. 암이 쉴 틈 없이 걸린다. 좀 더 꼼꼼한 초기 테스팅을 통해 나중에 힘들게 디버깅하는 일이 없도록 해야한다는 걸 뼈저리게 실감하는 순간의 연속이다. 버그 없는 코드를 짜는 능력은 스스로의 정신 건강과 동료들의 신뢰를 얻기 위해 반드시 습득해야 할 소양이다. 또한, 높은 수준의 코드를 짜기 위해선 펀더멘탈에 대한 이해도 반드시 필요하다. 특정 프레임워크를 사용할 때 내부적인 동작 방식을 이해하지 못하면 문제에 봉착했을 때 해결하기 어렵다. DB 인덱스 설계, Concurrency/Atomicity 이해, 여러 디자인 패턴 숙지 등등 기본적인 것들을 학교 다닐 때 놀지 말고 꼼꼼히 공부해 놓는 건 두말이 필요 없다.   적어놓고 보니 슈퍼개발자가 따로 없다. 비록 지금은 아닐지라도 부단히 자기 계발하며 슈퍼개발자를 지향하는게 스타트업 개발자의 모범이 아닐까 조심스레 생각한다. 갈 길이 멀다.   what my boss thinks I do? ------------------------- "우리 개발자 자니? 넝담 ㅎㅎ". 위 짤방을 다시 한번 보고오자. 여전히 매우 무시무시하다. 하지만, 1년 일하면서 깨닳은게 있다면 남의 눈치 혹은 보스를 만족시키기위한 보여주기식으로 일하다보면 지친다는 것이다. 스타트업에서 유쾌하게 일하려면 저마다의 원동력이 필요하다. 개인적으로 그 원동력은 나중에 스타트업을 창업하게 된다면 A부터 Z 까지 개발자로서 알아야 될 모든걸 배우고 싶다는 욕심이었다. 때론 배우려고 욕심부리다 오버엔지니어링해서 말아먹은 코드들도 많다. 사장님 죄송합니다 (..). 하지만, 외부의 압박이나 월급이 주는 의무감이 아닌 순수한 욕심이었기에 이해 해주실거라 믿는다. Boss 이야기가 나와서 하는 여담이지만 실제로 회사의 두 공동 CEO 분 한 분은 개발 PM 을 겸하셔서 업무간의 접점이 많았다. Boss 라고 느껴지기 보단 PM 역할을 하는 한 분의 동료라는 생각이 더 친숙했고, 그보다 점심먹고 커피 한 잔, 일 끝나고 맥주 한 잔 할 수 있는 친근한 분이라는 생각이 더 자주들었다. 그런 소프트한 리더쉽 아래에 좀 더 보은(?) 하고자 열심히 일 할 수 있었다.   what I think I do & what I actually did? ---------------------------------------- 스타트업의 다산해야 하는 개발 환경에서도 흥미로운 문제들을 많이 풀려고 노력했다. 너무 많이 회자되서 이젠 더 이상 핫하지 않은 머신러닝 기반 추천 시스템, AWS EMR을 이용한 로그 수집 및 분석, 광고 업계의 숙명과 같은 효율 개선 시도, MAB 등등, 일부는 시도에 그쳤고 일부는 성과도 있었다. 테크기반 스타트업이 지수적 성장을 거듭하기 위해선 언젠가 꼭 해결해야 되는 문제들이다. 이런 문제들을 해결하기 위한 간단한 접근 부터 복잡한 접근까지 여러 시도를 할 수 있었음에 감사한다. 그 과정에서 똑똑한 동료들에게 많이 배울 수 있었음 또한 마음의 빚으로 남아있다.   견고하게 성장하는 버즈빌이라는 로켓이 폭발적으로 더 성장함이 곧 개인의 성장이라 의심치 않았기 때문에 주인의식을 가지고 재미나게 일했다. 정신차려 보니 1년 2개월이라는 시간이 훌쩍 지나가 있었는데 그간의 경험에서 자부 할 수 있는게 한가지 있다. 그 누구보다도 스타트업에서의 일 그리고 버즈빌에서의 생활을 문화적으로/개발자로서 진심으로 즐겼다는 점이다. 좋아하고 배울점 많은 동료들을 매우 많이 사귀고 개발자로서의 스스로의 장단점을 알게 된 것도 큰 소득이다.   좋아하는걸 할 수 있고 그걸 잘하도록 노력할 수 있는 환경에서 최고의 동료들과 같이 일한다면 그것만큼 행복한 경험이 있을까. 나의 유쾌한 경험들을 공유함으로써 다른 이들의 도전에도 직간접적으로 도울 수 있다면 매우 뿌듯하겠다. 진부한 멘트를 끝으로 마무리할까 한다. [Buzzvil is hiring!](https://buzzvil.workable.com/) --- ## [Word2vec을 응용한 컨텐츠 클러스터링](https://tech.buzzvil.com/blog/word2vec_content_clustering) Date: 2016-06-16 | Category: Data & ML 버즈빌의 대표 프로덕트인 허니스크린은 사용자들에게 포인트를 적립할 수 있는 광고 뿐 만 아니라 다양한 컨텐츠를 제공합니다. SNS에서 핫한 글들, 뉴스 등을 볼 수 있기에 사용자들은 SNS, 포털 앱을 실행하지 않아도 잠금화면에서 손쉽게 컨텐츠를 소비할 수 있습니다. 이처럼 허니스크린에서는 잠금화면에서의 컨텐츠 소비를 통해 더 개선된 사용자 경험을 제공하기에 질 좋은 컨텐츠를 적절한 사용자들에게 제공해주는 것은 매우 중요한 일입니다. 버즈빌은 효율적인 컨텐츠 추천을 위해 머신러닝을 이용해 컨텐츠를 비슷한 것 끼리 분류하는 클러스터링 작업에 집중하고 있습니다. 클러스터링 작업은 다음과 같은 2가지 접근법으로 진행되고 있습니다. 1. 유사한 컨텐츠는 본문과 제목이 유사하다고 가정하여 컨텐츠 본문과 제목의 유사성을 측정을 바탕으로 한 클러스터링 2. 유저가 클릭한 컨텐츠들을 유사한 컨텐츠라 가정하여 유저 클릭 이력을 바탕으로 컨텐츠의 유사성 측정을 바탕으로 한 클러스터링 위와 같은 2가지 접근법으로 버즈빌에서는 다양한 머신러닝 기법을 이용해 문제를 해결하기 위해 노력하고 있습니다. 보다 효율적인 컨텐츠 클러스터링 방법을 만들기 위해 자연어처리(Natural Language Processing) 기법 중 Count-base method 인 Latent Dirichlet allocation(LDA), TF-IDF 등 다양한 방법을 시도하고 있습니다. 그 중에서도 가장 이용하기 쉽고, 좋은 결과가 나왔던 word2vec 방법에 대해 소개해드리고 버즈빌에서 이를 컨텐츠 클러스터링에 어떻게 활용하고 있는 지 소개해드리고자 합니다. word2vec -------- word2vec 은 2013년 Mikolov 등 구글 엔지니어들이 제안한 자연어처리 방식으로 단어를 vector space에 embedding 하고 벡터로 단어를 표현하는 것입니다. 이 방식을 통해 자연어 처리 분야에서 비약적인 정밀도 향상을 가능하게 하였습니다. word2vec의 알고리즘은 Neural Network에 같이 출연하는 단어들을 의미상 연관된 단어라고 가정하고 vector space 에서 두 단어 사이의 거리를 점차 줄이는 알고리즘입니다. 방법은 간단하지만 이 Neural Network를 학습시키는 데에는 많은 계산이 필요한데 word2vec은 최적화가 매우 잘 되어 있기에 빠른 시간 내에 학습을 시킬 수 있습니다. 구체적인 최적화 방법은 수학적으로 복잡하기에 여기서 언급하지 않고 궁금하신 분들은 아래 레퍼런스의 [논문](https://arxiv.org/pdf/1411.2738.pdf)을 참고해주시기 바랍니다. word2vec의 흥미로운 점은 단어 벡터들끼리의 연산이 의미있는 결과를 가져온다는 점입니다. word2vec으로 학습시킨 벡터들을 연산하면 다음과 같은 결과가 나옵니다. vector('Paris') - vector('France') + vector('Italy') = vector('Rome') vector('king') - vector('man') + vector('woman') = vector('queen') 이처럼 word2vec 은 단순한 단어의 벡터 표현이 아니라 단어의 추론, 복잡한 개념까지도 표현이 가능합니다. word2vec 모델의 구조는 CBOW(Continuous Bag of Words), Continuous Skip-Gram 2 가지 방식이 있습니다. 아래의 예시를 통해 2가지 방식의 차이점을 쉽게 알 수 있습니다. 문장 : When I find myself in times of trouble mother Mary comes to me CBOW Input : [When, I, find], [I, find, myself], [myself, in, times], ... Skip-Gram Input : [When, I, find], [When, I, myself], [When, find, myself], ... 위의 예시 처럼 CBOW 방식은 한 문장에서 연속적인 단어들을 입력값으로 집어 넣는 것으로 문맥을 통해 단어를 예측할 수 있는 모델입니다. Skip-Gram은 이와 반대로 한 문장 안에서 발생할 수 있는 단어 모음(Bag of words)를 모두 입력값으로 취하기에 단어를 통해 문맥을 예측할 수 있는 모델입니다. CBOW는 입력값이 Skip-Gram보다 적기 때문에 학습 시간이 더 짧고 잦은 빈도로 등장하는 단어에 대해 정확한 결과를 도출해낼 수 있습니다. Skip-Gram은 학습 시간이 더 길지만 작은 데이터셋에 대해 효율적이고 자주 등장하지 않는 단어에 대해 더 높은 정확도를 갖습니다. word2vec을 컨텐츠 클러스터링에 응용 ----------------------- 위의 내용을 요약하자면 word2vec은 한 문장 내에서 단어의 등장 빈도 데이터를 바탕으로 이를 단어를 벡터 스페이스에 embedding 하는 작업입니다. neural network를 통해 각 단어가 같이 등장한 빈도가 높으면 두 단어 벡터의 거리를 가깝게 하고, 빈도가 낮으면 두 닺어 벡터의 거리를 멀게함을 통해 단어벡터를 만드는 것입니다. 저는 이를 허니스크린 유저들의 컨텐츠 클릭 히스토리에 적용시키면 컨텐츠들을 효율적으로 클러스터링할 수 있을 것이라 생각했습니다. "비슷한 컨텐츠는 같이 클릭한 사람이 많은 컨텐츠이다"라고 가정을 한다면, word2vec을 통해 컨텐츠간 유사도를 정의할 수 있지 않을까 생각했습니다. 한 유저가 클릭한 컨텐츠 ID 이력을 1개의 문장이라고 생각하고, 컨텐츠 ID를 각각의 단어라고 생각한다면 위의 가설을 기반으로 word2vec을 통해 컨텐츠를 vector space에 embedding 해서 유사한 컨텐츠들 끼리 클러스터링 할 수 있지 않을까 생각했습니다. word2vec은 여러 개의 단어로 이루어진 문장(sentence)들을 입력하고 학습을 진행하는데 아래와 같이 캠페인 id들로 이루어진 문장을 학습시켰습니다. 유저 1 : {캠페인 ID | 유저 1이 클릭한 캠페인 ID} -> 문장 1 유저 2 : {캠페인 ID | 유저 2이 클릭한 캠페인 ID} -> 문장 2 ... 유저 380만명에 대해 위와 같은 380만개의 문장들을 만들었습니다. 그리고 유저가 클릭한 컨텐츠의 시간 순서에 따라 결과값이 영향을 받지 않게 하기 위해 컨텐츠 ID 순서를 임의로 섞어주었으며, 우연 오차를 방지하기 위해 임의로 섞은 10개의 문장들을 학습시켰습니다. 그리고 추가적으로 모델의 검증에 사용할 라벨데이터를 학습시켰습니다. ‘게임’, ‘웹툰’, ‘여행’, ‘연애’ 등의 태그를 몇몇 컨텐츠에 달았고 neural network에 다음과 같은 문장으로 학습시켰습니다. ['웹툰', 컨텐츠ID1, '웹툰', 컨텐츠ID2] ... 총 문장 (3800만 + 알파)개에 대해서 학습시켰고 word2vec 모델은 파이썬 gensim 라이브러리에 있는 모델을 이용했습니다. 학습시간은 2013 맥북 프로 13인치에서 약 2시간 30분이 소요되었습니다. 결과 분석 ----- ### 1\. 임의의 컨텐츠에 가까운 컨텐츠들 컨텐츠간 유사도는 컨텐츠 벡터간 cosine similarity로 정의했습니다. 두 벡터의 내적을 통해 두 벡터간 코사인 값을 계산하고 이를 유사도라고 정의했습니다. 즉 동일한 컨텐츠에 대해서는 1, 정 반대의 컨텐츠에 대해서는 -1 유사도를 갖습니다. 특정 컨텐츠는 학습된 모델에서 유사하다고 판단한 컨텐츠들과 실제로 유사했습니다. 가장 유사한 컨텐츠의 유사도가 0.95 이상인 경우 해당 컨텐츠와 유사도가 큰 컨텐츠들은 모두 같은 태그에 속했다고 볼 수 있었습니다. 아래는 실제 학습한 모델을 기반으로 얻어낸 결과로 한 컨텐츠와 가장 가까운 10개의 컨텐츠를 나타낸 것입니다. 아래의 컨텐츠는 가장 유사한 컨텐츠의 유사도가 0.95 이상인 컨텐츠입니다. 컨텐츠 - 역대급 미모 자랑하는 여자 아이돌 유사 컨텐츠 1) 현아를 실제로 보면 이런 느낌이다 2) 마리텔 양정원의 사적인 필라테스 3) 어제 마리텔 생방하고 남초싸이트 뒤집은 양정원 4) 컴백 트와이스 쇼케이스 사진 9장 5) 꼬치 여신이 나타났다며 SNS서 확산되고 있는 사진 6) 게임광고에 처음 등장해 색다른 매력 뽐내는 I.O.I. 7) 남취향저격 귀염심쿵 여돌짤모음 8) 태양의 후예 이후 가장 덕본 스타 5명? 9) 아버지 당선에 큰 영향을 끼친 그녀의 미모 10) 슈가맨서 승리 거둔 완전체 I.O.I.의 '엉덩이' ‘역대급 미모 자랑하는 여자 아이돌’ 컨텐츠와 관련된 컨텐츠들은 유사도가 0.95 이상으로 매우 높았으며 이들 또한 여자 연예인 관련 컨텐츠였습니다. 이러한 컨텐츠에 대해서는 유사 컨텐츠 정확도가 95% 이상이었습니다.. 아래의 컨텐츠는 가장 유사한 컨텐츠의 유사도가 0.95보다 작고 0.90 이상인 컨텐츠입니다. 컨텐츠 - 오마이뉴스_목사 꿈꾸던 신학생? 피해자에게도 꿈이 있었다 유사 컨텐츠 1) TV데일리_성폭행 논란 유상무 '코빅' 통편집 '병풍 굴욕' 2) 인사이트 아트&컬처_전쟁을 하지 말아야 하는 이유 3) Insight_학생수 3명 부족해 폐교 위기 인천봉화초등학교 4) Insight_강남역 살인 사건에 프로레슬러 김남훈이 남긴 글 5) Insight_유상무 성폭행 신고 여성 국선 변호사 선임 신청했다 6) HuffPost_이제 학교폭력 가해자에 예외는 없다 7) 스낵_강남역 살인 '묻지마 VS 여혐' 당신의 생각은? 8) 스낵_이세돌 프로기사회 돌연 탈퇴! 불합리한 관행 탓 9) Insight_유럽 시골마을 떠오르는 강원도 원주 풍경 이 컨텐츠들은 내용 간 큰 유사성은 없지만 전체적으로 뉴스라는 범주에 속한다고 볼 수 있었습니다. 다음은 제일 유사한 컨텐츠 유사도가 0.90 미만인 컨텐츠 입니다. 컨텐츠 - piki_장기자랑 시간 남학생들이 많이 불렀던 단골 곡 10 유사 컨텐츠 1) piki_동성애자들이 투쟁한 전대미문의 사건 2) piki_한때 전국을 장악했던 홈쇼핑 상품 BEST10 3) piki_1997년 전설의 H.O.T 모든 게 '최초'였던 그들 4) piki_자도자도 피곤하다면 배게 불면을 의심하라! 5) piki_평소 연락 없던 사람에게 받으면 언짢은 카톡 7 6) mango_내 위의 한계를 테스트 해보겠어! 전주 먹방여행 코스 7) piki_레전드 유행어를 남긴 추억의 TV광고 10편 8) piki_시급 2만 원을 준다는 기묘한 홀서빙 알바 9) mango_ 4월에 꼭 가봐야 할 맛집 리스트 30곳 제일 유사한 컨텐츠 유사도가 0.90 미만이지만 대체적으로 피키캐스트 글이라는 공통점이 있었습니다. 그러나 맛집, 여행 관련 컨텐츠가 추천되었기 때문에 유사도가 0.80 미만이면 같은 태그에 속한다고 볼 수 없었습니다. 이처럼 컨텐츠 벡터간 유사도가 감소함에 따라서 컨텐츠들이 같은 범주에 속한다고 판단하기가 어려움을 알 수 있었습니다. ### 2\. 태그 embedding 테스트 특정 캠페인에 태그를 달아서 학습시킨 데이터 기반으로 모델의 정확도를 테스트하였습니다. 다음은 웹툰과 미용 태그에 가장 가까운 컨텐츠들 목록입니다. 웹툰 태그 - 배틀코믹스_너의 목표는 이놈들보다 빨리 나를 처리하는 것이다 - 배틀코믹스_혹시 운동은 아니겠지요? - 배틀코믹스_그녀의 롤 실력만 믿고 가는거야! - 배틀코믹스_아빠는 너희 얼굴만 보면 다 견딜 수 있었단다 - 배틀코믹스_돈도 없는데 무슨 히어로야! - SNAC_충무공 이순신 장군은 왜 일본도를 들었을까? - 배틀코믹스_난 그런 흔해빠진 상대가 아니라고 - 뉴스1_안녕 멍뭉아 냐옹아 만나서 반가워 헤헷 :) - 배틀코믹스_럼블이 수영장에서 봉변당한 트리스타나를 구할수 있을까? - 배틀코믹스_제 안의 흑염룡을 깨워버렸어요!! 미용 태그 - piki_아이라인을 좀 더 예쁘게 그리기 위한 소소한 팁 7 - HuffPost_블랙헤드는 무엇이며 대체 어떻게 없애나? - Insight_나쁜 남자한테 끌리는 여성은 성적 판타지’ 있다 - piki_블랙헤드 제거를 위한 최상의 방법 찾기 - 대학내일_피부 뒤집어졌을 때가장 먼저 해야 할 일 8 - wikitree_씨스타 소유가 요즘 한다는 '다이어트' 비법 - HuffPost_생리로 당신의 건강을 확인하는 법 5가지 - 타임보드_일주일만에 출렁이는 팔뚝살 빼는법! 웹툰 태그의 정확도는 약 80퍼센트, 미용 태그의 정확도는 약 75퍼센트로 나왔습니다. 여기서 해당 태그와 관련이 없는 컨텐츠들에 대해 분석을 해보니 클릭 수가 매우 많거나 매우 적은 컨텐츠들이었습니다. 클릭수가 매우 많은 컨텐츠들은 학습 과정에서 모든 컨텐츠들과 가까워질 수 있기 때문에 이러한 오차가 발생했고, 클릭수가 매우 적은 컨텐츠들은 학습 데이터가 부족하기 때문에 오차가 발생했다고 볼 수 있습니다. ### 3\. CTR이 높은 컨텐츠와 가까운 컨텐츠 CTR(Click-through rate)이 높은 컨텐츠와 유사한 컨텐츠들은 다음과 같다. wikitree_씨스타 소유가 요즘 한다는 '다이어트' 비법 -> CTR : 28.14% StyleShare_봄이 기다려질 땐 포근한 비누향을 뿌려요 -> CTR : 18.6% wikitree_힙합계 '금수저 래퍼' 5인 -> CTR : 22.0% Nylon korea_올 봄 유행 립컬러부터 각질없이 레드립 바르는 TIP -> CTR : 26.4% 컨텐츠 노출 횟수 대비 클릭, 즉 CTR이 높은 컨텐츠와 유사한 컨텐츠들을 뽑았는데 이들은 모두 CTR이 높은 컨텐츠들이었습니다. 같은 범주에 속하는 컨텐츠라고 볼 수 없었지만 모두 CTR이 10% 이상이었습니다. 컨텐츠의 평균 CTR이 3-4%라는 걸 고려하면 이는 매우 높은 수치임을 알 수 있습니다. ### 4\. 컨텐츠 벡터끼리의 연산 word2vec에서 단어 벡터끼리의 연산이 의미있었던 것 처럼 컨텐츠 벡터끼리의 연산도 의미가 있을까 싶어서 진행해 보았습니다. 연산 결과는 다음과 같습니다. - 미용에 제일 가까운 컨텐츠 - 미용 + 웹툰 = 의미 찾기 어렵다. - 미용에 제일 가까운 컨텐츠 - 미용 + 웹툰 + 높은 CTR 컨텐츠 = 의미 찾기 어렵다. - 웹툰 + 높은 CTR 컨텐츠 + 웹툰 컨텐츠 = 높은 CTR의 웹툰 컨텐츠 첫번째 시도는 미용에 제일 가까운 컨텐츠에서 미용 태그를 빼고 웹툰 태그를 더하는 연산이었는데 웹툰과 제일 가까운 컨텐츠가 나올 것으로 예상했으나 전혀 다른 결과가 나와 의미를 찾기 어려웠습니다. 두번째 시도는 첫번째 시도와 유사하지만 높은 CTR을 갖는 컨텐츠를 찾는 연산인데 마찬가지로 예상과 전혀 다른 결과가 나와 의미를 찾기 어려웠습니다. 세번째 시도에서는 웹툰 컨텐츠 중 높은 CTR 컨텐츠를 찾는 연산인데 이를 통해 높은 CTR의 웹툰 컨텐츠를 찾을 수 있었습니다. 웹툰 태그의 컨텐츠들은 평균 4%의 CTR을 가졌으나 연산을 통해 얻은 컨텐츠들은 평균 8%의 높은 CTR을 가졌습니다. 현재 태그 데이터가 부족해 좋은 결과를 얻지 못했지만, 추후에 더 많은 태그 데이터를 추가하여 더 좋은 결과를 얻을 수 있을 것으로 기대됩니다.   Conclusion ---------- 별점 기록, 유저 평가 기록 없이 유저의 클릭 이력만 가지고 word2vec을 이용하여 높은 정확도의 컨텐츠 클러스터링 모델을 얻을 수 있었습니다. 이렇게 얻은 모델은 높은 정확도를 갖고 있었으며 추후에 사람이 지정한 태그 데이터를 추가하여 모델의 정확도를 발전시킬 수 있습니다. 또한 학습시간도 짧기에 이와 같은 방식으로 word2vec을 사용하여 다양한 클러스터링에 응용할 수 있을 것입니다. word2vec을 이용한 컨텐츠 클러스터링은 효율적인 컨텐츠 추천의 첫 단계입니다. 앞으로 클릭 이력이 없는 컨텐츠가 어떠한 벡터를 갖는 컨텐츠인지 예측하기 위해 TF-IDF, Paragraph2Vec 등 머신러닝을 이용해 효율적인 컨텐츠를 추천 시스템을 구축하고자 합니다. 앞으로도 기술적으로 진보하고 있는 버즈빌을 많이 주목해 주시고 다양한 소식으로 찾아뵙겠습니다.   References ---------- \[[http://arxiv.org/pdf/1301.3781.pdf](http://arxiv.org/pdf/1301.3781.pdf)\] \[[http://arxiv.org/pdf/1405.4053v2.pdf](http://arxiv.org/pdf/1405.4053v2.pdf)\] \[[https://code.google.com/archive/p/word2vec/](https://code.google.com/archive/p/word2vec/)\] \[[https://arxiv.org/pdf/1411.2738.pdf](https://arxiv.org/pdf/1411.2738.pdf)\] --- ## [허니스크린 포인트 시스템 마이그레이션을 위한 MySQL 성능 최적화](https://tech.buzzvil.com/blog/mysql_optimization_honeyscreen) Date: 2016-05-23 | Category: Backend [이전 블로그 포스트](http://tech.buzzvil.com/blog/aws-dynamodb/)에서 새롭게 변경한 DynamoDB 기반의 허니스크린의 포인트 시스템을 소개하였습니다. 가장 큰 변화는 따로 분리되어 있던 포인트 적립 히스토리 테이블과 유저의 총 포인트 테이블을 하나로 합쳐서 일관성에 문제가 없는 스키마 구조를 만든 것에 있습니다. 또 하나의 변화는 기존에 포인트 타입별로 따로 관리하였던 테이블을 하나로 합친 것이었습니다. 처음 허니스크린 포인트 테이블 스키마를 설계할 때 포인트를 타입별로 여러개의 테이블을 만들어 관리할 것이냐 하나의 테이블을 만들고 포인트 타입 컬럼을 추가하여 타입별 구분을 가능하게 할 것이냐의 고민을 하였습니다. 각각의 장단점은 아래와 같습니다. #### 여러개의 테이블 * 각 포인트 타입에 필요한 컬럼 및 인덱스 설정을 따로 할 수 있음 * 인덱스 추가 없이 타입에 종속된 쿼리를 빠르게 실행할 수 있음 * 특정 유저에 대해 시간별 포인트 적립 히스토리를 보기 위해서는 모든 테이블에 쿼리를 날려야해 성능저하 발생 * 포인트 타입이 많아질 수록 관리가 어려워짐 #### 하나의 테이블 * 타입에 상관없이 하나의 스키마를 사용해야 하므로 특정한 포인트 타입에 대해서 추가적인 컬럼이 필요할 경우 전체 테이블을 변경 해야함 * 포인트 타입마다 foreign key로 사용하는 컬럼이 있는데(광고 포인트는 광고 아이디, 유저 구매 포인트 차감은 주문 아이디) 이를 하나의 컬럼에서 관리할지 여러개의 컬럼을 둘지 고민해야함 * 포인트 타입이 추가되어도 테이블을 추가로 생성할 필요가 없음 * 포인트 총합 테이블이 따로 필요 없는 테이블 구조 설계 가능 처음 설계당시 저의 판단은 여러개의 테이블을 만들어서 사용하는 것이었습니다. 하지만 시간이 지남에 따라 하나의 테이블로 관리하는것이 더 좋았을 것 같다는 생각을 하게되었습니다. 가장 결정적인 원인은 포인트 테이블만 거의 10개에 달해 한 유저의 적립 히스토리를 가져오는데 성능적으로 문제가 생기는 것은 물론 관리에도 어려움이 있었다는 것이었습니다. 그리고 그 동안의 경험을 바탕으로 각 포인트 타입마다 필요한 요구사항들을 이미 알고 있기 때문에 제너럴한 포인트 테이블 설계가 가능한 상태였습니다. 그래서 포인트 히스토리를 하나의 테이블로 합치는 작업을 하기로 결정하였고 내친김에 DynamoDB로 이전하는 작업까지 병행하였습니다. 하나의 테이블로 관리되는 포인트 시스템에서는 마치 통장 내역을 기록하는 것 처럼 각 포인트를 쌓을 때 마다 쌓는 시점의 총 적립한 포인트를 같이 기록하기 때문에 특정 시점의 유저 포인트가 얼마였는지를 확인하는 것이 가능했습니다. 기존 시스템에서는 최종 시점의 총 포인트만을 별도의 테이블에 가지고 있기 때문에 특정 시점의 유저 포인트 상태를 확인하기 위해서는 그 시점까지 쌓은 포인트를 일일이 합산해야하는 문제점이 있었는데 이 문제가 자연스럽게 해결이 되었습니다. 포인트 시스템을 전환하면서 새로 쌓는 포인트에 대해서는 각 히스토리에 대한 총 포인트 데이터가 있었지만 이미 기존 시스템을 통해서 쌓은 포인트는 해당 정보가 없기에 이를 생성하기 위해서 몇가지 작업이 필요했습니다. 하지만 이미 그 동안 쌓아온 포인트가 수십억건에 달했기때문에 기존 히스토리에 대해 총 포인트를 업데이트 하는 작업이 쉽지는 않았습니다. 크게 아래와 같은 순서로 기존 포인트 히스토리에 대한 마이그레이션 작업을 진행했습니다. 1. 사용중인 AWS RDS 디비의 스냅샷을 이용해 마이그레이션용 인스턴스 생성 2. 통합 포인트 테이블 생성 3. 기존 타입별 포인트 테이블의 데이터를 통합 포인트 테이블로 복사 4. 각 유저의 첫 히스토리부터 시작하여 시간 순서대로 포인트 총합 컬럼 업데이트 5. S3으로 통합 포인트 테이블 백업 통합 포인트 테이블에서 각 유저의 히스토리별 총 포인트를 업데이트 하기 위해서는 유저별로 시간 순으로 정렬이 가능해야 했습니다. 이를 위해서 통합 포인트 테이블에는 유저 아이디와 적립 시간을 키로 가지는 복합키 인덱스가 필요했습니다. 하지만 인덱스를 먼저 생성하고 기존 테이블에서 데이터를 옮기면 insert성능이 저하되기 때문에 인덱스가 없는 상태로 데이터를 먼저 옮기고 그 후에 인덱스를 생성하는 방식으로 최적화를 하였습니다. 이어서 각 유저의 포인트 총 합을 업데이트하는 작업을 진행하였습니다. 하지만 이를 MySQL쿼리를 이용해서 구현하기가 쉽지 않았습니다. 다행히 [링크](http://dba.stackexchange.com/questions/56378/update-column-with-the-sum-of-column-in-previous-rows-in-mysql)에서 설명한 방법대로 order by를 이용한 update 쿼리와 variable의 조합을 이용하여 문제를 해결할 수 있었습니다. update 쿼리를 유저 아이디, 적립 시간을 기준으로 정렬을 하고 point\_sum variable에 포인트 누계를 저장해 놓은 뒤 다음 row를 업데이트 할 때 point\_sum variable을 참조해서 업데이트 하는 방식입니다. 다만 해당 링크에서는 전체 테이블을 하나의 히스토리로 보고 업데이트를 했지만 허니스크린에서는 각 유저별 히스토리를 구분하여 업데이트해야 했기에 조금 더 복잡한 쿼리를 사용해야 했습니다. last\_user\_id variable을 하나 더 선언하여 현재 업데이트할 row의 user\_id와 비교하여 다른 경우 point\_sum을 0으로 초기화해주는 작업이 필요합니다. 사용한 쿼리는 대략 아래와 같습니다. ```sql SET @last_point_sum := 0; SET @last_user_id := -1; UPDATE points SET point_sum = IF(@last_user_id = user_id, @last_point_sum := @last_point_sum + point_amount, @last_point_sum := 0), @last_user_id := user_id WHERE user_id BETWEEN 0 AND 999 ORDER BY user_id, created_at; ``` 작은 데이터 셋을 가지고 검증해보니 기대한대로 동작을 했지만 수십억건에 달하는 포인트 히스토리를 가지고 업데이트를 수행해보니 속도가 너무 느렸습니다. 이대로 가다가는 쿼리를 수행하는데 몇 주 이상이 걸릴 것으로 보여 포기하고 최적화할 방법을 찾아보기 시작했습니다. 가장 먼저 한 작업은 MySQL관련 파라미터를 수정하는 것이었습니다. 수정한 파라미터는 다음과 같습니다. ``` innodb_flush_log_at_trx_commit: 0 innodb_support_xa: 0 sync_binlog: 0 tx_isolation: READ-UNCOMMITTED ``` innodb\_flush\_log\_at\_trx\_commit는 장애시 commit이 완료된 트랜잭션이 유실되는 것을 방지하기 위해 필요한 파라미터인데 성능을 높이기 위해서는 0으로 설정하여 매 트랜잭션 commit마다 InnoDB log를 디스크로 sync하지 않도록 해줍니다. sync\_binlog를 0으로 세팅하면 binary log flush를 OS가 알아서 하게 하므로 성능을 높일 수 있습니다. [Binary log](http://dev.mysql.com/doc/refman/5.7/en/binary-log.html)는 리플리케이션과 특정한 데이터 복구 작업에서만 필요하기 때문에 아예 끄는 것이 더 좋은 것 같습니다. RDS 환경에서 binary log를 끄기 위해서는 [backup-retention-period를 0으로 세팅](https://forums.aws.amazon.com/thread.jspa?threadID=45258)하면 됩니다. innodb\_support\_xa는 InnoDB log와 binary log의 일관성을 보장하기 위해서 필요하다고 하는데 여기서는 필요 없기때문에 0으로 설정하였습니다. 하지만 이미 binary log를 끈 상태이기 때문에 큰 의미가 없을 것 같습니다. skip-innodb\_doublewrite 옵션을 이용하면 약간의 성능을 더 얻을 수 있지만 프로덕션 환경에서는 권장하지 않고 RDS 환경에서도 변경이 불가능하여 사용하지 못 했습니다. 이번 경우처럼 단순히 마이그레이션 용도인 경우에 가능하다면 skip-innodb\_doublewrite 옵션도 사용하면 좋을 것 같습니다. MySQL의 [Transaction Isolation Level](https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html) 기본 값은 REPEATABLE READ 입니다. 하지만 이를 보장하기 위해서 [Gap Lock](https://dev.mysql.com/doc/refman/5.7/en/innodb-record-level-locks.html)을 사용하는데 이를 피하려면 Transaction Isolation Level을 READ COMMITTED나 READ-UNCOMMITTED로 변경하면 됩니다. READ-UNCOMMITTED가 가장 오버헤드가 적다고 판단했기에 선택하였습니다.   다음으로 update쿼리의 문제점을 파악하기 위해 explain 쿼리를 이용해 수행 플랜을 확인해봤습니다. 의도한대로 유저 아이디/적립 시간 복합키를 이용하기는 했지만 Extra 컬럼에 "Using where; Using filesort" 와 같이 소팅을 별도로 하고 있는 것을 알 수 있었습니다. [스택오버플로우](http://stackoverflow.com/questions/6769941/do-mysql-update-queries-benefit-from-an-index)를 찾아보니 update구문은 where절에서만 인덱스를 사용한다는 답변이 있어 만들어놓은 복합 인덱스를 활용하지 못 하게 되었습니다. 그리고 또 다른 문제는 쿼리가 수행되는 동안 lock으로 인한 오버헤드가 있다는 것입니다. transaction의 lock 상태를 확인하기 위해서 아래의 커맨드를 이용해봤습니다. ```sql SELECT * FROM information_schema.INNODB_TRX ``` update 쿼리가 실행중인 동안 위의 쿼리를 실행해보면 trx\_rows\_locked 지속적으로 증가하는 것을 확인 할 수 있습니다. lock를 사용하고 있는지 확인하는 다른 방법으로 아래의 쿼리도 사용해봤습니다. ```sql SHOW ENGINE INNODB STATUS; ``` 쿼리 상태가 계속 변하기 때문에 각 state에서의 상태를 보기위해 쿼리가 진행되는 동안 두 번 상태를 확인해봤고 필요한 부분만 뽑아보면 다음과 같이 row lock을 사용하고 있는 것을 알 수 있습니다. ``` ---TRANSACTION 29583745568, ACTIVE 5 sec fetching rows mysql tables in use 1, locked 1 45467 lock struct(s), heap size 4863528, 5273956 row lock(s) MySQL thread id 424965, OS thread handle 0x2b64bcb9d700, query id 1579693 xxx.xxx.xxx.xxx user System lock ---TRANSACTION 29583745568, ACTIVE 18 sec starting index read mysql tables in use 1, locked 1 110753 lock struct(s), heap size 11744808, 8905501 row lock(s), undo log entries 447763 MySQL thread id 424965, OS thread handle 0x2b64bcb9d700, query id 1579693 xxx.xxx.xxx.xxx user updating ``` lock으로 인한 오버헤드를 줄이기 위해 기존 테이블에 업데이트 하는 대신 포인트 통합 테이블과 같은 스키마를 가지는 별도의 테이블을 만들고 기존테이블에서 읽어온 총 포인트를 업데이트한 결과를 새 테이블에 insert하는 방식으로 구현을 하였습니다. 이렇게한 이유는 [MySQL 레퍼런스 매뉴얼](http://dev.mysql.com/doc/refman/5.7/en/innodb-locks-set.html)에서 볼 수 있듯이 단순한 select 구문은 lock을 사용하지 않는 반면 update구문은 스캔한 모든 레코드에 대해서 exclusive next-key lock을 걸어야 하기 때문에 비효율적이라고 생각했기 때문입니다. 그리고 MySQL에서 auto-increment primary key를 가지는 테이블에 대한 insert가 매우 효율적으로 이루어진다고 알고 있습니다. 변경한 쿼리는 아래와 같습니다. ```sql SET autocommit=0; SET unique_checks=0; SET foreign_key_checks=0; SET @new_point_sum := 0; SET @last_user_id := -1; INSERT INTO points_final ( SELECT user_id, point_amount, date_at, point_type, coalesce(@new_point_sum := IF(@last_user_id = user_id, @new_point_sum + point_amount, 0)), coalesce(@last_user_id := user_id) FROM points WHERE user_id BETWEEN 0 AND 999 ORDER BY user_id, date_at ); COMMIT; SET autocommit=1; SET unique_checks=1; SET foreign_key_checks=1; ``` 여기서 coalesce를 사용한 이유는 MySQL에서 variable을 evaluation하는 순서가 보장되지 않는 문제를 회피하기 위해서입니다. [MySQL 레퍼런스 매뉴얼](http://dev.mysql.com/doc/refman/5.7/en/user-variables.html)을 살펴보면 "However, the order of evaluation for expressions involving user variables is undefined."와 같은 설명이 있습니다. 위의 최종 쿼리에서 살펴보면 @new\_point\_sum를 업데이트하는 구문이 실행되기 전에 @last\_user\_id를 업데이트하는 구문이 먼저 실행 될 가능성이 있다는 것입니다. 따라서 evaluation 순서를 보장할 수 있는 방법을 다음의 [링크](http://code.openark.org/blog/mysql/on-user-variables-evaluation-order)를 통해서 찾을 수 있었습니다. coalesce를 활용하는 방법인데 찜찜하지만 어쨌든 동작하는 것 같습니다. 그리고 autocommit을 0으로 세팅하여 쿼리가 트랜잭션 안에서 동작하게 하였는데 이렇게 하면 매 insert마다 commit을 하지 않고 트랙잭션이 종료되는 시점에 commit이 한번에 되기 때문에 더 효율 적입니다. 왠지 트랜잭션을 사용하면 무조건 느릴 것만 같은데 대량 insert 상황에서는 오히려 트랜잭션을 사용하는 것이 더 유리합니다. 그리고 이번 케이스에서는 별 의미가 없을 수 있지만 unique\_checks, foreign\_key\_checks 를 0으로 설정하여 필요없는 constraint체크는 하지 않도록 했습니다. "SELECT * FROM information\_schema.INNODB\_TRX" 쿼리를 이용해 확인해보니 trx\_rows_locked 값이 0으로 고정되어 있어 lock을 사용하지 않는 것을 확인 할 수 있었습니다. 마찬가지로 "SHOW ENGINE INNODB STATUS"를 이용해 확인해봐도 row lock이 걸리지 않는 것을 알 수 있었습니다. ``` ---TRANSACTION 29583746127, ACTIVE 2 sec fetching rows mysql tables in use 2, locked 1 2 lock struct(s), heap size 360, 0 row lock(s), undo log entries 203998 MySQL thread id 11, OS thread handle 0x2aea60329700, query id 390 xxx.xxx.xxx.xxx user Sending data ---TRANSACTION 29583746127, ACTIVE 15 sec inserting mysql tables in use 2, locked 1 2 lock struct(s), heap size 360, 0 row lock(s), undo log entries 1294454 MySQL thread id 11, OS thread handle 0x2aea60329700, query id 390 xxx.xxx.xxx.xxx user Sending data ``` insert로 쿼리를 변경하고 나니 update하는 것 보다 2~3배 이상의 성능 개선 효과가 있었습니다. 하지만 아직도 성능은 충분하지 않았습니다. 마이그레이션 쿼리가 돌아가는 동안 인스턴스의 상태를 확인해보니 CPU 점유율은 별로 높지 않은데 Disk IOPS와 throughput또한 최대로 활용하고 있지 못하고 있었습니다. 도대체 어디서 병목이 발생하는 것인지 알 수가 없는 상황이었습니다. 위의 쿼리를 동작을 말로 풀어보면 아래와 같습니다. * 유저 x의 전체 히스토리를 시간순으로 정렬된 상태로 가져옴. * 이때 user\_id/date\_at으로 걸려있는 인덱스를 통해 필요한 데이터 리스트를 조회하고 실제 데이터는 데이터 블록에서 다시 조회 * 조회된 데이터를 순차적으로 돌며 @new\_point\_sum 업데이트 * 업데이트한 포인트 히스토리를 새로운 포인트 테이블에 insert * 위의 작업을 모든 유저 x에 대해 수행 여기서 가장 문제가 될 만한 부분은 user\_id/date\_at 인덱스를 이용해 유저의 히스토리를 가져오는 부분이었습니다. 물론 인덱스를 이용하기 때문에 필요한 데이터 스캐닝은 빠르게 할 수 있지만 인덱스에 실려있는 user\_id/date\_at을 제외한 모든 다른 데이터는 데이터블록에서 한번 더 스캔해야 하는 문제점이 있습니다. 이 데이터 블록들은 디스크의 여기저기에 흩어져 있기 때문에 결과적으로는 최대 히스토리 갯수만큼 디스크 엑세스가 일어나야 한다는 것을 의미합니다. 따라서 속도가 매우 느릴 수 밖에 없습니다. 만약에 디스크가 HDD라면 디스크헤드가 여기저기 데이터를 찾느라 많은 시간을 소비할 수 밖에 없을 것이고 랜덤엑세스에 강한 SSD를 사용한다고 하더라도 문제는 여전히 있습니다. SSD는 블록 단위(512K)로 리드를 수행하기 때문에 한 row를 가져오더라도 한 블록 전체를 리드하게 되어 대역폭의 낭비가 심하게 됩니다. 전체 데이터가 인스턴스의 메모리크기보다 작다면 블록 단위로 읽어오더라도 메모리에 캐싱되어 있다가 나중에 재사용이 가능하지만 데이터 크기가 인스턴스의 메모리크기보다 크다면 계속해서 캐시 eviction이 발생하여 비효율적인 동작을 하게됩니다. 이를 해결하기 위해서 [블로그](http://gywn.net/2012/04/mysql-covering-index/)의 내용처럼 커버링 인덱스를 사용하기로 했습니다. 기존 user\_id/date\_at 인덱스에 테이블의 모든 컬럼을 추가하는 것입니다. 이렇게 하면 유저별 히스토리를 가져올 때 데이터 블록을 참조하지 않아도 되므로 성능이 비약적으로 향상되었습니다. 실제 몇 주 이상 걸릴 쿼리가 단 하루안에 완료가 되었습니다. 이제 남은 단 하나의 작업은 마이그레이션이 완료된 테이블을 S3 으로 옮기는 것입니다. AWS에서 제공하는 서비스인 Data Pipeline을 이용해 시도해보았으나 실패하였습니다. [링크](http://stackoverflow.com/questions/28057279/moving-files-5-gig-to-aws-s3-using-a-data-pipeline)에 나와있는대로 4G이상이 되는 테이블에 대해서는 이전 작업이 불가능하다는 설명이 있습니다. 게다가 컬럼을 구분하는 구분자로 콤마 밖에 지원하지 않아 데이터에 콤마가 포함되는 경우 이용이 불가능합니다. MySQL 데이터를 S3으로 이전하는 일은 일반적인 일이라 판단해서 Data Pipeline에서 잘 지원할 줄 알았지만 이렇게 제약이 있다는 것은 조금 아쉬운 부분이었습니다. 그래서 차선책으로 찾아본 툴은 mysqldump였습니다. 하지만 이 툴은 insert 쿼리 형태로만 백업이 가능하고 CSV형식으로 백업을 하려면 실제 데이터베이스의 디스크에 접근할 권한이 필요했는데 RDS에서는 디스크 접근 권한을 주지 않기에 이 또한 불가능하였습니다. xtrabackup또한 검토해보았으나 마찬가지로 디스크 접근 권한이 필요하여 사용이 불가능했습니다. 그래서 mysqldump대신 [mysql 커맨드라인 유틸리티를 직접 이용하는 방식](http://stackoverflow.com/questions/12040816/mysqldump-in-csv-format)을 참고하였습니다. 이제 백업한 데이터를 s3에 올리기만 하면 됩니다. 하지만 대용량의 백업 데이터를 디스크에 저장했다가 s3에 다시 올리는 것은 왠지 비효율적이라는 생각을 해서 백업한 데이터를 바로 s3에 올릴 수 있도록 쉘 스크립트를 작성하였습니다. pipe를 열어놓고 aws s3 커맨드의 인풋으로 설정하여 백그라운드 잡으로 실행한 뒤에 이 pipe에 mysql에서 읽어온 데이터를 그대로 밀어넣는 방식입니다. 쉘 스크립트에서 멀티프로세스 프로그래밍이 가능하다니 재미있는 것 같습니다. ```shell trap "rm -f pipe" EXIT rm -f pipe mkfifo pipe gzip pipe id=0 chunk=10000 while [ $id -le $last_pk ] do to=$(( $id + $chunk - 1 )) mysql -N -B -uusername db_name -ppassword -h mysql_address -e "SELECT * FROM points_final WHERE id between $id AND $to" >&3 id=$(( $id + $chunk )) done # Close pipe. exec 3>&- echo "All done!!" exit 0 ``` 결론 -- MySQL에 대한 이해가 아직은 많이 부족하지만 마이그레이션 작업을 위해 여러가지 시행착오를 거치면서 많은 것들을 배운 것 같습니다. 정리해보면 다음과 같습니다. * 대량의 update가 필요한 경우 때에 따라서 새로운 테이블을 만들어서 select/insert를 하는 것이 더 효율적일 수 있다. * 커버링 인덱스를 적극적으로 활용하자. 특히 마이그레이션을 위한 중간 테이블은 한 번 쓰고 버릴 것이므로 더더욱 커버링 인덱스를 활용하는 것이 유리하다. * 쿼리 수행 플랜을 확인하기 위해서 explain 쿼리를 활용하자. * explain쿼리로 문제를 찾을 수 없으면 show engine innodb status 쿼리를 활용하자. * 쉘 스크립트 만세! 감사합니다. Reference -- - [AWS DynamoDB at Buzzvil](http://tech.buzzvil.com/blog/aws-dynamodb/) - [Maximal write througput in MySQL](https://www.percona.com/blog/2010/02/28/maximal-write-througput-in-mysql/) - [SHOW INNODB STATUS walk through](https://www.percona.com/blog/2006/07/17/show-innodb-status-walk-through/) - [What is innodb\_support\_xa?](https://www.percona.com/blog/2011/03/02/what-is-innodb_support_xa/) - [트랜잭션이 중요한 비즈니스에서의 MySQL에 대한 고민들](http://www.mimul.com/pebble/default/2012/12/10/1355134898590.html) - [On user variables evaluation order](http://code.openark.org/blog/mysql/on-user-variables-evaluation-order) - [MySQL에서 커버링 인덱스로 쿼리 성능을 높여보자!!](http://gywn.net/2012/04/mysql-covering-index/) - [Update column with the sum of column in previous rows in MySQL](http://dba.stackexchange.com/questions/56378/update-column-with-the-sum-of-column-in-previous-rows-in-mysql) - [Can I trurn off binary logging?](https://forums.aws.amazon.com/thread.jspa?threadID=45258) - [Do mysql update queries benefit from an index?](http://stackoverflow.com/questions/6769941/do-mysql-update-queries-benefit-from-an-index) - [Moving files >5 gig to AWS S3 using a Data Pipeline](http://stackoverflow.com/questions/28057279/moving-files-5-gig-to-aws-s3-using-a-data-pipeline) - [Mysqldump in CSV format](http://stackoverflow.com/questions/12040816/mysqldump-in-csv-format) - [MySQL 5.7 Reference Manual](https://dev.mysql.com/doc/refman/5.7/en/) --- ## [AWS DynamoDB at Buzzvil](https://tech.buzzvil.com/blog/aws-dynamodb) Date: 2016-03-04 | Category: Backend 버즈빌의 대표 프로덕트는 잠금화면 리워드 앱인 허니스크린입니다. 유저는 잠금화면 상에서 혹은 허니스크린 오퍼월에서 광고에 참여하거나 혹은 단순히 잠금화면을 해제하는 것만으로도 포인트를 획득할 수 있으며, 이 포인트를 이용하여 다양한 상품을 구매할 수 있습니다. 허니스크린에서 포인트는 서비스의 핵심 요소인만큼, 유저가 획득한 혹은 사용한 포인트를 데이터베이스에 정확하게 기록하는 것은 매우 중요한 일입니다. 기존에는 MySQL RDS를 활용하여 포인트 시스템을 운영하였습니다. 그러나 서비스의 규모가 커짐에 따라 포인트 관련 요청이 기하급수적으로 증가하게 되어 DB에 상당한 부담을 주게 되었습니다. 그러한 부담은 DB에 여러 가지 문제를 야기할 수 있는 만큼, 대안을 찾던 중 AWS의 NoSQL 데이터베이스인 DynamoDB를 발견하게 되었습니다. DynamoDB의 큰 강점은 스케일링이 매우 쉽다는 것입니다. 따라서 기존DB의 부담을 줄이고, 보다 유연하게 포인트시스템을 운영하고자 버즈빌에서는 포인트시스템을 기존 관계형 데이터베이스에서 DynamoDB로 옮겨서 운영하고 있습니다. 이번 포스팅에서는 허니스크린의 포인트시스템에 맞게 어떻게 DynamoDB table 구조와 관련 function들을 만들었는지 다루고자 합니다. 기존 데이터베이스에서는 기본적으로 테이블을 두 종류로 나누어서 운영을 하고 있었습니다. 각 테이블의 단순화한 구조는 다음과 같습니다. #### amount table 유저의 포인트가 변동된 기록을 저장하는 테이블입니다. 즉 누가 언제 어떠한 방식으로 얼마나 포인트를 쌓았는지 기록하는 곳입니다. 예를 들어 앱 상에서 유저가 “적립 내역”을 조회하면 이 테이블에 요청을 보내게 됩니다. |||||| |--- |--- |--- |--- |--- | |id|user_id|account_type|amount|date| |…|…|…|…|…| |1012|12345|withdraw|1000|2016-02-28 12:00:01| |1013|39485|impression|10|2016-02-28 12:00:02| |1014|20938|action|200|2016-02-28 12:00:02| |…|…|…|…|…| #### sum table 한 유저가 현재 시점에서 보유한 포인트의 합을 저장하는 곳입니다. 예를 들어 앱의 메인 화면에서 현재 포인트의 총합을 보여줄 시 이 테이블에 요청을 보내게 됩니다. |||| |--- |--- |--- | |id|user_id|sum| |…|…|…| |256|12345|1200| |257|39485|560| |258|20938|3700| |…|…|…| 테이블이 두 종류인데 각 테이블의 데이터가 서로 일관성을 유지해야 하기 때문에 두 테이블에 대한 요청은 atomic하게 이루어져야 합니다. 즉 포인트 적립시 amount 테이블에 대한 insert와 sum 테이블에 대한 update가 하나의 transaction으로 묶여서 요청이 들어가게 됩니다. 이렇게 하면 요청이 중간에 실패하더라도 두 테이블 모두 transaction이 시작하기 전의 기존 상태로 돌아가게 됩니다. 따라서 예를 들어 1번 테이블에 적립 내역은 생겼는데 2번 테이블에서 sum이 변화하지 않거나, 2번 테이블에서 sum이 바뀌었는데 1번 테이블에 적립금 내역이 생기지 않는 등의 문제를 방지할 수 있습니다. 처음에는 기존 테이블들의 구조를 그대로 따라가기 위해 DynamoDB에도 다음과 같이 테이블을 만들고자 하였습니다. #### amount table ||| |--- |--- | |Table Name|amount| |Partition Key|user_id| |Sort Key|date| |Attributes|amount, account_type| #### sum table ||| |--- |--- | |Table Name|sum| |Partition Key|user_id| |Attributes|| 하지만 곧 테이블을 이렇게 생성할시 여러가지 문제점이 발생한다는 사실을 알 수 있었습니다. 가장 큰 문제점은 DynamoDB의 transaction은 atomicity를 보증하지 않는다는 점입니다. 즉 amount 테이블과 sum 테이블에 대한 요청이 하나의 transaction으로 묶일 수 없기 때문에 두 테이블 간의 일관성을 보증할 수 없습니다. 예를 들어 amount 테이블에 아이템을 생성하고 나서 sum 테이블의 아이템에 대한 update가 실패하면 적립 내역은 생겼는데 총 적립금은 바뀌지 않는 문제가 생깁니다. 여러 transaction을 atomic하게 묶을 수 없는 DynamoDB의 성격 때문에, 데이터의 일관성을 보증하기 위해서는 테이블을 하나로 합쳐야 한다는 사실이 분명해졌습니다. 따라서 두 테이블을 다음과 같이 합하기로 하였습니다. #### amount_sum table ||| |--- |--- | |Table Name|amount_sum| |Partition Key|user_id| |Sort Key|date| |Attributes|amount, account_type, sum| 두 테이블에 amount와 sum이 각각 나뉘어 있었던 것에 비해 이 테이블에서는 한 아이템이 해당 유저가 적립한 amount와 그 시점의 sum을 모두 갖고 있게 됩니다. 포인트 적립 요청이 들어왔을 시에는 다음과 같은 과정을 거치게 됩니다. 먼저 해당 user\_id의 가장 최신값을 읽어서 sum을 가져옵니다. 최신값 하나을 가져오기 위해서는 query에 Limit 옵션에 1을, ScanIndexForward에 false를 설정하면 됩니다. 그러면 query는 파티션 내에서 Sort Key의 크기를 기준으로 정렬된 아이템을 역순으로 한 개 가져오게 됩니다.  이렇게 최신값을 읽고 나서는 그 sum에 현재 적립하려는 amount을 더해 새로운 sum 값을 구한 뒤, 다른 정보들과 함께 새로운 아이템을 생성하게 됩니다. 즉 해당 유저의 가장 최신값 읽기, 새 아이템 생성 순입니다. 유저의 최신값을 읽는 것은 테이블을 변화시키지 않으므로, 테이블의 state를 바꾸는 transaction이 기존 구조에서는 (amount에 대한) insert와 (sum에 대한) update 두 가지였던 것이 여기서는 insert 한가지가 된 것입니다. 따라서 애초에 데이터를 변경하는 transaction이 한가지이므로 이 테이블에 대한 transaction은 atomic하게 이루어지게 됩니다. 그러나 여전히 몇가지 문제가 남아있습니다. 첫번째는 덮어쓰기 문제입니다. 테이블의 partition key와 sort key가 각각 user\_id와 date이기 때문에 한 유저에 대해 동시에 입력이 들어올 경우 한 입력이 다른 입력을 덮어쓰게 됩니다. 하지만 이 문제는 DynamoDB의 conditional write를 활용하면 간단하게 해결할 수 있습니다. 같은 user\_id와 date의 조합이 존재하지 않을 시에만 새 아이템을 생성하도록 condition을 설정하고, 만약 존재한다면 date를 1초 정도 더해가면서 user\_id와 date의 조합이 고유할 때까지 재시도를 하게 하면 date에 몇 초 정도 오차가 생길 수는 있지만 적어도 한 아이템을 다른 데이터가 덮어쓰는 일은 막을 수 있습니다. (conditional write 관련 참고: [http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html), [https://java.awsblog.com/post/Tx3RRJX73ZNOVL/Using-Improved-Conditional-Writes-in-DynamoDB](https://java.awsblog.com/post/Tx3RRJX73ZNOVL/Using-Improved-Conditional-Writes-in-DynamoDB)) 두번째는 테이블 내에서의 데이터 일관성 문제입니다. 예를 들어 한 유저에 대해 각각 100포인트를 적립하라는 요청 두 개가 동시에 들어왔다고 합시다. 그 유저의 최신 sum값은 1000이라고 가정합니다. 위에서 설명한 과정을 따른다면 먼저 두 요청은 거의 동시에 최신값을 읽어들입니다. 따라서 두 요청 모두 테이블로부터 읽은 유저의 최신 sum은 1000입니다. 두 요청 모두 100포인트를 더하려고 하므로 읽어들인 값에 100을 더해 sum이 1100인 새 아이템을 각각 생성합니다. 결과적으로 적립금이 1000포인트 있었던 유저가 100포인트 적립을 두 번 했는데도 불구하고 그 유저의 적립금 총합은 1200이 아닌 1100이 됩니다. 이는 각각의 요청이 순서대로 들어왔을 시에는 발생하지 않지만 거의 동시에 들어왔을시에는 이루어지는 문제로서, transaction의 isolation과 관련된 문제입니다. 이 문제는 다음과 같이 각 아이템에 대해 version값을 부여함으로써 해결할 수 있습니다. #### amount\_sum\_version table ||| |--- |--- | |Table Name|amount_sum_version| |Partition Key|user_id| |Sort Key|version| |Attributes|date, amount, account_type, sum| 이 테이블에서는 date 대신 version이 Sort key가 됩니다. version은 같은 역할을 할 수 있다면 어떤 형태도 상관없으나 여기서는 0부터 시작하는 Number type으로 하겠습니다. 이 테이블은 다음과 같이 위의 문제를 해결할 수 있습니다. 테이블 상에 최신값 sum이 1000인 유저에게 각각 100포인트를 더해주라는 요청이 동시에 2개 들어옵니다. 두 요청은 각각 테이블로부터 최신 sum값 1000과 그 아이템의 version값인 21을 읽어옵니다. 그리고나서 각각 sum에 100을 더하고 version에 1을 더해 sum이 1100이고 version이 22인 새 아이템을 거의 동시에 생성하려 합니다. 하지만 이 경우 둘 중 하나는 실패하게 됩니다. 위에서 제시한 것처럼 conditional write를 활용하고 있기 때문에 중복된 user\_id와 version 조합이 생성될 수 없기 때문입니다. 실패한 요청은 재시도를 하기위해 다시 테이블로부터 최신값을 읽어들이고, 이번에는  sum으로 1100을, version으로 22를 얻습니다. 그리고나서 그 요청은 sum이 1200이고 version이 23인 새 아이템을 생성하게 됩니다. 혹시 이 과정에서 또 실패하면 성공할 때까지(혹은 프로그래머가 정해놓은 횟수까지) 같은 과정을 재시도하게 됩니다. 이렇게 하면 위와 같은 isolation 문제도 해결할 수 있으며, 더 높은 버전은 낮은 버전보다 후에 생성되므로 한 user\_id 파티션 내에 시간순 정렬도 유지할 수 있습니다. 이렇게 하면 테이블에 새로운 아이템을 생성하는 것에 관련된 문제들은 모두 해결이 되었습니다. 다만 아직 테이블의 데이터를 조회하는 문제는 완전히 해결되지 않았습니다. 위와 같은 테이블 구조 상에서는 한 유저의 데이터는 시간순으로 볼 수 있지만, 전체 데이터를 시간순으로 보기 위해서는 유저 각각에 대해 query하거나 테이블 전체를 scan하는 방법밖에는 없기 때문입니다. 이는 유저가 매우 소수일때는 활용할 수 있는 방안이지만 유저가 몇 천, 몇 만 이상으로 늘어가면 매우 비효율적인 방법이 될 것입니다. 보다 효율적으로 전체 데이터를 시간순으로 보기 위해서는 다음과 같이 하나의 Attribute와 Global Secondary Index를 추가해야 합니다. #### amount\_sum\_version_scatter table ||| |--- |--- | |Table Name|amount_sum_version_scatter| |Partition Key|user_id| |Sort Key|version| |Attributes|date, amount, account_type, sum, scatter| #### scatter-date-index ||| |--- |--- | |Index Name|scatter-date-index| |Partition Key|scatter| |Sort Key|date| 간단하게 생각하면 모든 아이템에 같은 값을 준 뒤, 그 값을 Partition key로 하고 date를 Sort key로 하면 모든 값이 한 파티션 내에 시간순으로 정렬될 것입니다. 그러나 그렇게 하면 새로운 아이템이 생성시 항상 같은 파티션에 쓰이게 되는 hot partition 문제가 발생하여 쓰로틀링을 유발하게 됩니다. 이와 같은 문제를 방지하기 위해 새로운 아이템 생성시 scatter는 일정 범위 내의 랜덤한 값을 갖도록 합니다. 이렇게 하면 아이템들이 여러 파티션에 분배되어 쓰여지기 때문에 쓰로틀링이 발생할 가능성이 훨씬 줄어들게 됩니다. 이렇게 테이블을 세팅한 뒤에 데이터를 시간순으로 조회하기 위해서는 각 파티션을 돌며 Query를 시행하면 됩니다. 이 방법이 유저 각각에 대해 Query하는 것보다 효율적인 이유는 대부분의 경우 scatter 파티션의 수가 유저 수보다 훨씬 적고 (100 이하) scatter 파티션의 수는 철저히 프로그래머가 컨트롤할 수 있기 때문입니다. (참고: [https://blogs.aws.amazon.com/bigdata/post/Tx3KPZDXIBJEQ4B/Scaling-Writes-on-Amazon-DynamoDB-Tables-with-Global-Secondary-Indexes](https://blogs.aws.amazon.com/bigdata/post/Tx3KPZDXIBJEQ4B/Scaling-Writes-on-Amazon-DynamoDB-Tables-with-Global-Secondary-Indexes)) Conclusion ---------- 지금까지 버즈빌에서 포인트시스템을 위해 어떻게 DynamoDB를 활용하는지 소개했습니다. DynamoDB에 대한 기본적인 정보에 대해서는 이미 잘 정리된 자료들이 있기 때문에 포인트시스템을 구현할 시에 마주한 문제들과 그 해결방법을 위주로 설명했습니다. DynamoDB는 여러가지 분명한 강점이 있는 데이터베이스이지만 여러가지 약점 또한 안고 있는 것이 사실입니다. 하지만 AWS측의 지속적인 개선과 DynamoDB를 사용하는 여러 개발자들의 노력으로 강점은 살리면서 약점들을 극복해 나가고 있습니다(본문에 소개된 conditional write의 경우 2014년에 추가된 기능입니다). [AWS 공식 블로그](https://aws.amazon.com/blogs/aws/)를 보시면 새로 추가되고 업데이트되는 기능들을 실시간으로 체크하실 수 있습니다. 버즈빌에서는 DynamoDB 뿐만 아니라 [AWS의 다양한 최신 서비스를 활용하고 있습니다](http://tech.buzzvil.com/blog/buzzvil-aws/). 앞으로도 다양한 소식 알려드리겠습니다. --- ## [Weighted Random Shuffling Algorithms](https://tech.buzzvil.com/blog/weighted-random-shuffling) Date: 2016-02-10 | Category: Backend   버즈빌에서는 잘 알려진 허니스크린, 버즈스크린 이라는 프로덕트 외에도 광고의 서빙 및 최적화를 담당하는 버즈애드라는 프로덕트를 운영하고 있습니다. 근본적으로, 광고의 효율을 높이려면 효율을 사전에 계산 할 수 있는 "스코어링" 과정이 반드시 필요합니다. 예를들어, 허니스크린에 N개의 아이템(광고 및 컨텐츠)중에서 K (<= N) 개를 선택하여 내보내고 싶은 상황을 가정해 봅시다. 여기서 K개의 아이템을 정하는 기준은 다양 할 수 있습니다. 랜덤하게 K개를 무작위로 선택할수도 있고 CTR (Click Through Rate) 기준으로 정렬하였을 때 높은 K개를 선택할 수 있습니다. CTR 예측과 같은 정교한 스코어링 모델을 만드는 것은 쉽지않은 일입니다. 하지만 설령 높은 정확도를 가지는 이러한 모델이 있다하더라도 정렬과 같은 결정적(deterministic)인 방식은 여전히 문제점을 가지고 있습니다. 첫번째, CTR과 같은 스코어는 시간 및 조건에 따라 변화 할 수 있는 값입니다. 같은 광고나 컨텐츠를 중복해서 노출 한다면 해당 광고에 대한 유저의 피로도가 높아져 CTR은 떨어지게 됩니다. 반대로, 시간이 지남에 따라 관심사가 변하여 이전에는 흥미없던 컨텐츠를 소비하게 될 수도 있습니다. 둘째, [콜드 스타트 문제](https://en.wikipedia.org/wiki/Cold_start)를 겪게 됩니다. 잠금화면에 노출되는 광고 및 컨텐츠는 유저가 능동적으로 선택하여 볼 수 없습니다. 왜냐하면 유저는 미리 할당 된 광고 및 컨텐츠들만 소비하기 때문입니다. 따라서, 실제로는 유저가 더 좋아하는 잠재적 광고나 컨텐츠가 있을지라도 우리의 할당 최적화 모델은 편향 된 데이터를 바탕으로 다음 노출 아이템을 선택하게 됩니다. 스코어링 모델이 주어질 때 위 두가지 문제를 해결하는 좋은 방법중 하나가 바로 Weighted Random Shuffling 이라는 랜덤 알고리즘 입니다. Overview --------   Weighted Random Shuffling (줄여서 WRS) 은 이름에서 알 수 있는 것 처럼 $latex a\_1, a\_2, .., a\_n$ 의 양수의 정수 즉, 가중치들이 주어 졌을 때 이를 고려하여 섞는 방법을 말합니다. 가중치를 고려하는 샘플링이지만 한번 뽑힌 원소는 다시 고려하지 않는(sample without replacement) 샘플링을 WRS이라 부릅니다. 더 정확하게는, 가중치 배열 W =$latex w\_1, w\_2, .., w\_n$ 와 이미 샘플한 집합 S = $latex s\_1, s\_2, .., s\_n \\subset W$ , W와 S의 원소들의 가중치 합 $latex Sum\_W$와 $latex Sum\_S$ 가 주어질 때, 가중치 $latex a\_i \\in A-S$를 가진 원소가 배열의 가장 앞에 위치할 확률 $latex a\_i/(Sum\_W-Sum_S)$을 보장하는 샘플링입니다. 예를들어 A = \[1,2,3\] 의 가중치 배열이 주어질 때 각각의 원소는 각각의 순서에 아래의 확률을 가지고 위치하게 됩니다.   ![](https://tech.buzzvil.com/blog/weighted-random-shuffling/1.png) WRS을 통해 섞은 후 앞에서 부터 K개를 선택하게 되면 결정적 방식이 가진 단점들을 보완하여 아래와 같은 다양한 상황에 이용 될 수 있습니다. 1. 다양한 광고/컨텐츠 풀에서 주어진 조건에 맞는 K개 선택하기 2. A/B 테스팅의 확장 버전인 [Multi Armed Bandit](https://en.wikipedia.org/wiki/Multi-armed_bandit) 의 한 전략으로 사용하기 이러한 Weighted Random Shuffling 을 간단하게 구현한다면 $latex O(n^2)$의 시간복잡도를 가진 방법을 생각해 볼 수 있습니다. Naive Approach --------------   WRS 알고리즘은 주사위를 반복해서 던지는 과정으로 생각할 수 있습니다. 가중치 배열의 길이가 $latex N$일 때, 이 주사위는 $latex N$면을 가지고 있으며 각각의 눈이 나올 확률은 주어진 배열의 가중치들과 같습니다. 단, 주사위를 반복해서 던지면서 이미 나온 면들은 지워야 합니다. 즉, 주사위가 $latex N$면, $latex N-1$면, ..., 1면을 순서대로 가지게 되는거죠. 실제로 주사위를 던지는 과정은 $latex \[0, W)$ 구간의 랜덤한 숫자 $latex T$를 정한 후, 각각의 가중치를 순회하면서 $latex W\[i-1\] < T\[i\] <= W\[i\],\ where\ T\[i\] = T - sum\ of\ already\ chosen\ weights\ up\ to\ i$ 인 $latex i$번째 면을 반환하는 방식으로 구현할 수 있습니다. 의사코드로는 아래와 같습니다. func throw_dice (arr_weights) { W <- sum(arr_weights) T <- RANDOM(0, W) // sample uniformly from [0, W) cumsum <- 0 FOR i in 0..n-1, DO { IF T-cumsum <= arr_weigths[i], THEN RETURN arr_weights[i] cumsum += arr_weights[i] } } 이 알고리즘은 최악의 경우에$latex O(n^2)$의 수행시간을 가집니다. 큰 인풋 $latex N$의 경우에 이 알고리즘을 반복적으로 수행하기엔 부담스럽습니다. 수행 시간의 상한이$latex O(n^2)$ 보다 더 빠른 방법은 없을까요? Binary Search Tree ------------------ 위 방법을 개선하는 인사이트중 하나는 가중치 배열의 누적합(cumulative sum)을 구한 뒤 이진탐색을 통해 $latex cum\\\_w\[i-1\] < T\[i\] <= cum\\\_w\[i\]$ 을 만족하는 $latex i$를 찾는 방법입니다. 이는 전처리 과정에 $latex O(N)$, 탐색에 $latex O(logN)$ 만큼의 시간을 소비합니다. 해당 과정을 $latex N$번 반복한다면, 전처리 과정의 오버헤드 때문에 Naive 방법과 다르지 않는 $latex O(N^2)$의 수행시간이 걸리게 됩니다. 이를 개선하려면 이진 검색 트리 자료구조(https://en.wikipedia.org/wiki/Binary\_search\_tree)를 이용하는 방법이 있습니다. 이진 검색트리 자료구조를 사용하면 샘플링 후 누적합을 $latex O(logN)$ 만에 업데이트 할 수 있게 됩니다. 그럼 어떻게 이진 탐색 트리를 만들고 이를 이용하여 탐색 및 업데이트를 하는지 설명해보겠습니다. Tree Construction ----------------- 먼저 주어진 가중치 배열을 이용해 트리를 생성하기 위해서 새로운 클래스를 정의할 수도 있지만 주어진 배열을 그대로 이용할 수 있는 간단한 트릭이 있습니다. 인덱스를 0부터 카운트하지 않고 1부터 카운트하면, 모든 인덱스 $latex i$에 대해 왼쪽, 오른쪽 자식을 각각 $latex 2i, 2i+1$로 표현할 수 있습니다. 트리를 생성하기 위해서 먼저 편의상 첫번째 원소를 이진 검색 트리의 루트로 표현합니다. 그 후, $latex n$번째 원소부터 거꾸로 배열을 순회하면서 자신의 가중치를 부모의 가중치에 더해줍니다. 즉, 모든 원소 $latex i$에 대해 $latex (weight,\ total\\\_weight\\\_of\\\_subtree\\\_rooted\\\_at\\\_i)$ 를 계산합니다.   ![](https://tech.buzzvil.com/blog/weighted-random-shuffling/2.jpg) ------------------------------------------------------------- Search and Update -----------------   ![](https://tech.buzzvil.com/blog/weighted-random-shuffling/3.jpg) 위 방식으로 트리 $latex T$를 만들고 나면 검색과 업데이트는 간단해집니다. 각각의 샘플에 대해서 우리가 원하는 원소는 다음의 과정을 통해 결정됩니다. 1. 가중치 배열 $latex w\_1, w\_2, .., w_i$ 가 주어졌을 때, 먼저 $latex \[1, W)$ 구간의 랜덤한 유리수 $latex r$를 하나 샘플합니다. $latex W$는 모든 가중치의 합입니다. 2. 트리 $latex T$의 각각의 노드 $latex i$ 는 $latex (w\_i, tw\_i)$ 를 가지고 있습니다. $latex tw\_i$는 노드 $latex i$를 루트로하는 자식트리의 가중치 합을 나타냅니다. 이제, 루트에서 검색을 시작합니다. 지금까지 트리를 따라 검색을 하여 노드 $latex i$에 도달했을 때, 현재의 $latex r$값과 $latex w\_i$ 값을 비교하여 $latex r$이 작거나 같으면 해당 원소를 리턴합니다. 만약 위 조건이 참이 아니면, 왼쪽 자식 트리의 토탈 가중치합과 다시한번 비교합니다. 이때, 왼쪽 자식에 속한 모든 가중치의 합보다 $latex r$이 크다면 왼쪽 자식의 모든 노드는 고려대상이 아니기 때문에 오른쪽 자식 트리로 검색을 재게합니다. 1) 왼쪽자식으로 탐색 한다면 $latex r$ 을 $latex r = r - w\_i$ 로 업데이트하고 2)오른쪽 자식으로 탐색한다면 $latex r = r - w\_i - tw_{2i} $ 로 업데이트 합니다. 즉, 누적합을 계산하는 대신에 $latex r$을 줄여나가게 됩니다. 3. 우리가 원하는 원소 $latex i$를 찾았다면, 해당 원소의 가중치를 0으로 업데이트 합니다. 해당 원소의 가중치가 0이라면 2번 조건은 항상 거짓이기 때문에 해당 원소는 다시 뽑히지 않게 됩니다. 해당 원소를 찾은 뒤, 부모를 거쳐 트리의 루트까지 올라가면서 거쳐가는 원소의 $latex tw_i$ 을 업데이트합니다. 각각의 탐색과 업데이트는 $latex O(logN)$의 시간이 듭니다. 따라서 위 과정을 $latex N$번 반복하면 $latex O(NlogN)$의 시간이 들게 됩니다. Optimization Tricks ------------------- 실제 프로덕션 환경에서 위 알고리즘을 사용시 적용할 수 있는 몇가지 팁이 있습니다. 1. 위 방식으로 만들어진 BST는 가중치 배열의 원소가 홀수개라면 모든 노드의 자식이 0개 혹은 2개인 꽉찬 이진트리가 되고 (full binary tree), 짝수개라면 한개의 노드를 제외한 모든 노드의 자식이 0개 혹은 2개입니다. 실제 트리를 만드는 과정에서 원본 가중치 배열의 순서는 중요하지 않습니다. 가중치를 내림차순 정렬한 배열을 이용하여 BST를 만들면 가중치가 높은 원소들이 트리의 상위에 위치하게 되어 평균 검색 횟수를 줄일 수 있습니다. 2. WRS로 접근할 수 있는 문제들의 정의에 따라 다르겠지만, WRS을 적용해야 될 가중치 배열이 동적으로 빠르게 변하지 않는 경우에는 이미 만들어진 트리를 레디스, 맴캐시등의 캐시 구조에 저장한 뒤 반복해서 사용할 수 있습니다. 본문에 소개된 이진 탐색 트리를 사용하는 방법 외에도 전처리 과정의 오버헤드는 있지만 해당 과정을 캐싱하여 오버헤드를 줄이고 검색/업데이트를 상수시간에 수행할 수 있는 알고리즘도 존재합니다 ([alias method](https://en.wikipedia.org/wiki/Alias_method)). Wrapping it up... ----------------- 버즈빌에서는 빠르게 확장하는 환경속에서 효율적인 플랫폼이 되기위해 많은 중요한 문제들을 적절한 알고리즘과 자료구조를 사용하여 해결하고 있습니다. 앞으로도 모바일 광고시장의 선두자가 되기위해 기술적인 혁신을 거듭하는 버즈빌을 주목해주세요. --- ## [버즈빌 AWS 활용기](https://tech.buzzvil.com/blog/buzzvil-aws) Date: 2015-11-06 | Category: Backend 버즈빌에서는 잠금화면 리워드앱인 허니스크린 한국,일본,대만 버전과 애드네트워크인 버즈애드 그리고 잠금화면 SDK인 버즈스크린을 운영하고 있습니다. 그런데 이 모든 서비스를 현재 단 6명의 개발자가 운영하고 있어서 서버 운영 리소스를 줄이기 위해 가능한 많은 부분을 AWS에 의존하고 있습니다. AWS가 없었다면 지금의 인원으로 이 모든 서비스를 운영하는 것은 불가능했을 것입니다. 버즈빌 초기에는 EC2, ELB, RDS, S3을 활용했고 그 후 Auto Scaling, CloudFront, Lambda, DynamoDB, Route 53, Kinesis, SNS, VPC 등을 사용하기 시작했습니다. 이번 포스팅에서는 그동안 사용했던 서비스들에 대해 소개하며 유용한 팁, 그리고 제가 느낀 점들을 공유하겠습니다. ![](https://tech.buzzvil.com/blog/buzzvil-aws/buzzad-infrastructure.png) 버즈애드 AWS 구조 EC2 --- AWS의 대표 서비스입니다. EC2인스턴스를 생성하고 사용하는 것은 간단하지만 보안성을 향상시키려면 몇 가지 고려해야 할 사항들이 있습니다. 버즈빌에서 생성하는 모든 EC2 인스턴스에는 IAM role을 적용하고 있습니다. 해당 IAM role에 각종 AWS 퍼미션을 설정해 놓으면 소스코드에 access key/secret key를 넣어놓지 않아도 되기에 보안성을 높일 수 있습니다. 이미 다양한 언어에 대해서 [SDK](https://aws.amazon.com/ko/tools/)를 지원하고 있고 IAM role이 적용된 인스턴스에서 SDK를 사용하면 별도의 설정 없이 해당 role의 권한으로 AWS서비스를 사용할 수 있습니다. Security group 설정시 source를 anywhere로 설정하지 않습니다. 실제 접속이 필요한 IP와 Port만 열어둡니다. AWS 내부 인스턴스끼리 통신이 필요할 때는 IP대신에 특정 security group에 속한 인스턴스에 대해서 접속을 허용하는 것이 가능해 버즈빌에서는 이 기능을 활용하고 있습니다. 인스턴스 런칭 시 availability zone을 선택할 수 있는데 특정 availability zone의 장애 상황을 고려해서 시스템을 설계할 것이 아니라면 모든 인스턴스를 하나의 availability zone으로 몰아 넣는 것이 좋습니다. 같은 availability zone 안의 인스턴스 사이에서 발생하는 트래픽은 비용이 들지 않지만 다른 availability간에 발생하는 트래픽에는 비용이 청구되기 때문입니다. 작년에 burstable instance인 t2인스턴스가 출시되었습니다. 기본적인 소개는 [유저 가이드](http://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/t2-instances.html)에 잘 나와 있습니다. t2 인스턴스를 사용하면서 착각했던 부분은 t2.micro의 CPU 100% 상태의 성능이 t2.medium의 CPU 100% 상태의 성능보다 낮을 거라고 예상했던 것입니다. 사실 둘다 vCPU는 1개로 똑같기 때문에 credit이 남아 있는 한 똑같은 성능을 가집니다. 이 부분을 잘 이해하고 있으면 조금 더 t2인스턴스를 효율적으로 활용할 수 있습니다. 예를 들어 t1.micro 인스턴스로 충분히 burst 트래픽을 감당하는 것이 가능한데 단순히 응답 시간을 줄이기 위해서 t2.medium 인스턴스로 변경하는 판단을 하지 않을 수 있습니다. 마찬가지로 CloudWatch에서 CPU 사용량을 모니터링할 때도 주의해야 합니다. 만약 t2.micro인스턴스를 사용하고 있는데 CPU 점유율이 낮다고 방심하고 있으면 안됩니다. Baseline performance가 10%밖에 안되기 때문에 평균 CPU 점유율이 10%이상이 된다면 인스턴스 타입 변경을 고려해봐야 합니다. CloudWatch에서 남아있는 CPU credit과 사용한 CPU credit을 확인할 수 있으니 잘 체크해주어야 합니다. EC2 인스턴스를 많이 운영하다 보면 비용 절약을 위해 예약 인스턴스를 사용하는 경우가 있습니다. 가격을 보면 친절하게 온 디맨드 대비 절감액이 몇 %인지 나와 있습니다. 하지만 여기에 하나 더 변수가 있습니다. 바로 새 인스턴스 타입이 출시되거나 가격이 내려갈 수 있다는 것입니다. 예약 인스턴스는 한번 구입하면 타입을 바꿀 수가 없고(다만 같은 class내에서 변경은 가능합니다. 2대의 c3.large는 1대의 c3.xlarge로 변경 가능합니다.) 중간에 가격이 내려가더라도 이미 구입한 예약 인스턴스까지 가격 인하가 적용되지는 않습니다. 예전에 c3 인스턴스가 처음 나왔을 때는 기존 인스턴스를 c3으로 교체하는 것만으로도 체감 상 거의 30%정도의 비용 절감 효과를 누렸었습니다. 대략 1~2년에 한 번 꼴로 새 인스턴스 class가 출시되는 것으로 보이고 가격인하도 1~2년에 한 번씩은 하는 것 같습니다. 사실 저희도 지금 c4 instance를 예약 인스턴스로 구매하는 방안을 고려 중인데 아직은 조금 기다리고 있습니다. 실제로 2013년 11월에 c3 인스턴스가 출시됐고 2014년 11월에 c4 인스턴스가 출시됐습니다. 예약 인스턴스를 구매하기 전에 구매하려는 인스턴스가 출시된지 오래됐거나 가격 인하가 될 것 같은 느낌(?)이 오신다면 기다려보는 것도 좋은 방법입니다. ELB --- HTTPS가 필요한 경우 ELB에서 SSL termination이 가능합니다. ELB에서 HTTPS를 HTTP로 변환하여 내부 서버로 리퀘스트를 전달하기 때문에 웹서버의 부담을 줄일 수 있습니다. ELB에는 두 가지 종류가 있습니다. 하나는 외부 트래픽을 받아서 내부로 요청을 분산하는 External ELB이고 다른 하나는 AWS 인프라 내부에서만 사용 가능한 Internal ELB입니다. 참고로 Internal ELB는 Classic 환경에서는 사용할 수 없고 VPC에서만 사용할 수 있습니다. 내부 서버간의 통신에 트래픽 분산이 필요한 경우 Internal ELB를 적극 활용할 수 있습니다. ELB는 비용이 매우 저렴하고 안정적이며 거의 무한대로 확장 가능합니다. 마이크로서비스 아키텍처에서 Scaling이 필요한 서비스의 앞 단에 Internal ELB를 배치하여 활용하기에 좋습니다. 버즈빌에서는 광고 애드네트워크인 버즈애드 뿐만 아니라 프록시 서버/엘라스틱서치 서버와 통신하는 경우에 Internal ELB를 활용하고 있습니다. 엘라스틱서치는 자체적으로 로드 밸런싱을 지원하지만 ELB를 사용하는 것이 더 안전하다고 판단하였고 1-shard/multi-replica 설정으로 ELB를 활용하기 좋은 구조였기에 Internal ELB를 사용하였습니다. Auto Scaling ------------ 인스턴스 scale in/out을 자동화 해주는 서비스입니다. Auto Scaling을 사용하기 위해서 고려해야 할 점이 몇 가지 있습니다. 우선 인스턴스 개수가 고정적이지 않기 때문에 소스코드 배포시 배포할 인스턴스를 동적으로 찾아내는 방법이 필요합니다. 버즈빌에서는 fabric을 사용하고 있고 Auto Scaling 인스턴스에 태그를 붙여 해당 태그를 기준으로 인스턴스를 검색하여 배포를 하고 있습니다. 이와 더불어서 로그수집을 중앙에서 해야 합니다. Scale-in으로 인해 인스턴스가 종료될 때 아무 처리를 해주지 않으면 로그가 유실됩니다. 이를 막기 위해 로그를 보존하는 방법이 크게 세 가지가 있는데 첫 번째로는 logstash나 fluentd같은 log shipper를 이용해 실시간으로 로그를 중앙 서버로 이전하는 방법, 두 번째로 splunk나 loggly같은 유료 로그 수집 서비스를 이용하는 방법, 마지막으로 termination hook을 이용해 termination을 잠시 연장시키고 그 시간에 로그를 한 번에 옮기는 방법이 있습니다. 최근에는 CloudWatch에서도 [로그 수집 서비스](https://aws.amazon.com/cloudwatch/details/#log-monitoring)를 지원하는 것으로 알고 있습니다. 서비스 트래픽 패턴마다 다르겠지만 트래픽이 급격하게 증가하는 경우가 많다면 scale-out이 빨리 완료되도록 하는 것이 중요합니다. 그러기 위해서는 서버 설정을 위해 필요한 패키지들을 미리 설치하고 어느 정도 셋업이 완료된 AMI이미지를 준비하는 것이 scale-out시마다 발생하는 서버 셋업 시간을 줄일 수 있는 방법입니다. 그리고 CloudWatch detailed-monitoring을 켜놓아야 합니다. Auto Scaling은 CloudWatch에서 수집되는 데이터를 기반으로 scale in/out을 수행하는데 기본적으로 5분 간격으로 데이터를 수집하기 때문에 그만큼 갑작스런 트래픽 증가에 빠르게 반응할 수 없습니다. detailed-monitoring이 켜져있으면 1분 간격으로 데이터를 수집하기 때문에 좀 더 빠르게 트래픽 변화에 대응할 수 있습니다. Auto Scaling에서도 스팟 인스턴스를 사용하는 것이 가능합니다. 버즈빌에서는 Auto Scaling으로 대응할 수 없는 갑작스런 트래픽 증가나 다수의 인스턴스가 동시에 장애가 발생하는 등 예측할 수 없는 상황을 대비해 EC2 인스턴스의 CPU점유율을 60% ~ 70% 이하로 유지하고 있습니다. 이러한 예외적인 상황에 대비하기 위하여 항상 필요 이상의 인스턴스를 유지하는 것이 비효율적일 수 있습니다. 이를 해결하기 위해 하나의 서비스에 대해 Auto Scaling Group 두 개를 유지할 수도 있습니다. 하나는 온-디맨드 인스턴스 Auto Scaling Group으로 기존의 다이나믹 스케일링 설정 그대로 사용하는 것이고 스팟 인스턴스용 Auto Scaling Group을 고정 사이즈로 지정하여 여유분의 EC2 인스턴스를 운영하는 방법입니다. 이때 고정 사이즈의 크기는 갑자기 해당 스팟 인스턴스 전체가 사라지더라도 서비스에 장애가 나지 않을 정도로만 설정하는 것입니다. 한마디로 일반적인 Auto Scaling운영에 더해서 혹시 모를 상황을 대비하여 여유분의 EC2 인스턴스를 스팟 인스턴스로 활용하는 방법입니다. 스팟 인스턴스의 숫자를 조금 더 효율적으로 설정하려면 [Scheduled Scaling](http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/schedule_time.html)을 활용하는 것도 좋습니다. 하나의 서비스에 대해서 동시에 여러 개의 다이나믹 스케일링 그룹을 사용하면 예측할 수 없는 결과가 발생할 수 있으므로 추천하지 않습니다. RDS --- RDS MySQL을 Multi-AZ와 함께 사용하고 있습니다. 데이터베이스 백업부터 자동화된 failover까지 데이터베이스 운영/관리에 대한 지식이 부족한 저희에게 RDS는 큰 도움이 되었습니다. 직접 데이터베이스를 운영하다보면 당장 데이터가 늘어남에 따라 데이터를 백업하고 복구하는 것 조차 쉽지 않습니다. RDS에서는 이런 부분을 알아서 해주므로 관리에 들어가는 시간을 많이 절약할 수 있습니다. 버즈빌에서 운영 중인 서비스에서는 읽기 연산을 캐시로 해결하고 있고 대부분의 트래픽이 쓰기에서 발생하고 있어 read-replica는 사용하고 있지 않습니다. 하지만 데이터베이스 장애 시 다운 타임을 줄이기 위해 Multi-AZ를 사용하고 있습니다. Multi-AZ를 사용하게 되면 기존 대비 두 배의 비용이 들어가지만 서비스의 안전성을 위해 눈물을 머금고 사용하고 있습니다. 동작방식은 read-replica와 비슷한 것으로 알고 있지만 Multi-AZ로 생성된 replica 인스턴스에는 읽기 요청을 보낼 수 없고 오직 장애 상황에서 failover를 하는 데 사용하거나 인스턴스 재부팅 시에만 사용하게 됩니다. Mutli-AZ를 사용할 경우 인스턴스 scale-up이나 MySQL 버전 업그레이드와 같이 재부팅을 해야 하는 상황에서 발생하는 다운타임을 줄일 수 있습니다. 하지만 Multi-AZ를 사용한다고 해도 순식간에 재부팅이 되지는 않고 3분 ~ 5분 정도는 걸리는 것 같습니다. 한편 RDS를 사용하면서 도움을 많이 받는 부분 중 하나가 스냅샷을 통한 복구입니다. 예를 들어 스키마 변경 등의 큰 작업을 진행할 때 실제 데이터가 존재하는 상황에서 테스트를 해보고 싶을 경우 전날 스냅샷을 복구해서 테스트를 해보고 이를 실제 데이터베이스에 적용할 수 있습니다. 실제 데이터를 이용해 코드를 테스트해보고 싶은 경우에도 전날 스냅샷을 micro인스턴스로 복구해 사용할 수 있습니다. 최근 MySQL과 100% 호환이 되면서 성능은 다섯 배까지 좋다는 Aurora를 사용하려고 검토를 해봤으나 안타깝게도 Aurora가 micro 인스턴스를 지원하지 않아 포기하였습니다. 스냅샷을 통해 손쉽게 운영 데이터를 다른 인스턴스에 복구 할 수 있다는 것, micro인스턴스를 이용해 적은 비용으로 여러 가지 테스트를 해 볼 수 있는 것은 RDS의 큰 장점입니다. RDS 런칭 시 주의하셔야 할 부분 중 하나는 처음에 Allocated-storage사이즈를 필요 이상으로 크게 잡으면 안된다는 것입니다. 운영 중에 크기를 늘리는 것은 가능하지만 줄이는 것은 불가능합니다. 저희는 처음에 이를 모르고 1TB를 잡고 서비스를 런칭하였는데 스토리지 비용으로만 한달에 약 10만원 이상이 나갔습니다. 개발 시에는 10GB정도로 런칭시에는 50GB정도로 시작하여 점차 늘려나가는 것을 추천합니다. DynamoDB -------- DynamoDB는 AWS에서 제공하는 NoSQL서비스입니다. NoSQL이기에 트랜잭션이 지원되지 않거나 인덱스를 추가하는 데 제약이 있는 등 여러 가지 불편한 점들이 있지만 운영하는 서비스의 특성에 맞게 잘 활용한다면 쉽게 성능 확장이 가능한 데이터베이스로 사용할 수 있습니다. 기본적으로 읽기에 비해 쓰기가 5배 비쌉니다. 게다가 Strongly Consistent Read가 아닌 Eventually Consistent Read를 활용하면 읽기 비용이 반으로 줄게되어 결과적으로 쓰기가 10배 비싸게 됩니다. 여기에 필요에 따라 secondary index를 몇 개 더 추가한다면 쓰기 비용은 또 몇 배 올라가게 됩니다. 따라서 DynamoDB를 write intensive한 목적으로 사용한다면 비용이 많이 들 수 있음을 우선 인지해야 합니다. 이를 조금이라도 해결하기 위해 [쓰기 요청을 비동기로 처리하여 write peak를 줄이는 방법](http://readme.skplanet.com/?p=7570) 또는 [동적으로 capacity를 조절](http://blog.recopick.com/68)할 수 있습니다. 하지만 관리 리소스를 줄이기 위해서 DynamoDB를 사용하는데 비용 절약을 위해 또 다시 관리 리소스를 들이는 것이라 선뜻 적용하기에는 어려운 부분이 있습니다. Eventually Consistent Write나 Capacity Auto Scaling과 같은 기능이 AWS에서 직접 지원된다면 참 좋을 것 같습니다. DynamoDB는 비용 계산하는 것이 상당히 까다롭습니다. 예를들어 Read/Write시 item size에 따라 capacity 소비량이 달라지기 때문에 item size가 어떻게 계산되는지 알아야 합니다. 이때 [Item size를 구하는 공식](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.html#ItemSizeCalculations) 또한 그리 간단하지 않습니다. 심지어 attribute name의 길이까지 item size에 크게 영향을 미칠 수 있기 때문에 read/write operation에서 capacity가 1이상 들어간다면 attribute name을 짧게 하는 것을 고려해야 할 수도 있습니다. 이 외에도 write operation에서 secondary index를 얼마나 사용하느냐, secondary index를 이용한 read 패턴이 어떻게 다른지에 따라 비용이 달라지는 등 고려해야 할 사항이 수도 없이 많습니다. 때문에 실제로 DynamoDB를 도입할 계획이 있다면 [공식 문서](http://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/Introduction.html)를 처음부터 끝까지 정독하는 것을 권장합니다. 문서를 읽다보면 '아 정말 모든것이 돈이구나'라고 느끼게 됩니다. 관리비용은 0에 가깝지만 이를 제대로 쓰기 위해서는 초기 학습비용이 만만치 않습니다. 대신 잘만 활용한다면 유용한 서비스임에는 틀림 없는 것 같습니다. ElasticCache ------------ AWS에서 제공하는 서비스를 사용하는 것이 기본 정책이지만 버즈빌에서는 예외적으로 ElasticCache는 사용하지 않고 있습니다. Memcached의 경우 패키지 관리자를 이용하면 커맨드 한 번에 설치가 가능하고 설정 또한 memory limit과 허용할 IP설정 정도를 지정하는 것 외에는 특별히 건드릴 것이 없습니다. 게다가 Memcached가 충분히 안정적으로 동작하는 것이 검증되었기에 직접 운영을 한다고 해도 관리 비용이 크게 들지 않습니다. EC2위에서 직접 운영하는 것 대비 ElasticCache의 비용이 약 25%이상 비쌉니다. ElasticCache에서는 Memcached 외에도 Redis를 지원합니다. 버즈빌에서는 Redis를 cache로 쓰는 것에서 더 나아가서 광고의 Click/Impression/Conversion 데이터까지 관리하는 persistence로 사용할 정도로 적극적으로 활용하고 있습니다. 하지만 ElasticCache의 Redis는 persistence보다는 cache용으로 사용하는 것에 초점이 맞춰져 있는듯한 인상이 강합니다. 인스턴스 장애시 데이터 유실을 최소화할 수 있는 AOF옵션을 켤 수 있지만 ElasticCache는 EBS가 아닌 local instance store를 사용하기 때문에 인스턴스 재부팅시 데이터가 소실 됩니다. AOF대신 Multi-AZ를 사용할 것을 권장하지만 어쨌든 데이터가 디스크에 저장되어 있지 않고 메모리에 있다는 사실이 찜찜했습니다. 그래서 ElasticCache를 활용하는 대신 관리비용이 들더라도 Redis를 직접 설치해서 운영하고 있습니다. S3 & CloudFront --------------- S3에 올려진 이미지 등의 컨텐츠들은 CloudFront를 통해서 서빙되고 있습니다. 요금표([S3](https://aws.amazon.com/ko/s3/pricing/), [CloudFront](https://aws.amazon.com/ko/cloudfront/pricing/))에서 볼 수 있듯이 S3과 CloudFront간의 비용 차이가 크지 않습니다. 도쿄 리전 기준으로 S3과 CloudFront에서 외부로 발생하는 트래픽 비용은 동일하며 요청 횟수당 비용이 CloudFront가 2배 정도 되는데 파일 사이즈에 따라 다르겠지만 트래픽 비용 대비 작은 부분을 차지합니다. 오리진에서 엣지 로케이션으로 전송하는 데이터 비용은 무료입니다. 따라서 가능하면 CloudFront를 활용하는 것이 좋습니다. 게다가 CloudFront는 10TB이상의 트래픽이 발생하면 연간 계약을 통해 할인을 받을 수 있습니다. 게다가 S3 오리진이 도쿄에 있는데 미국/유럽에서 트래픽이 많이 발생하는 경우 오히려 CloudFront를 이용하는 것이 더 쌀 수도 있습니다. 미국/유럽에서 발생하는 트래픽 요금이 상대적으로 싸기 때문입니다. CloudFront는 오리진으로 S3이 아닌 ELB 지정하는 것이 가능합니다. 만약 웹서버의 응답이 캐싱 가능하다면 CloudFront를 활용하는 것도 가능합니다. 버즈빌에서는 컨텐츠와 일부 광고 이미지를 자동으로 생성해주는 [이미지서버](http://tech.buzzvil.com/blog/scaling-phantomjs-ghost-town/)가 있는데 CloudFront를 활용하여 한 번 생성한 이미지는 캐싱을 하여 사용하고 있습니다. 하지만 CloudFront 사용 시 file invalidation이 다소 불편합니다. Invalidation이 완료되기 까지 시간이 걸리고 추가 비용도 발생합니다. 따라서 파일을 수정할 필요가 있는 경우 이름을 변경하여 invalidation이 필요 없도록 하는 것이 좋습니다. Route 53 -------- ELB 또는 CloudFront distributions를 지정할 때 CNAME대신 Alias를 활용하고 있습니다. Alias쿼리는 무료이기 때문에 조금이나마 비용을 절약할 수 있습니다. Alias쿼리는 CNAME과 비슷하게 보이지만 실제 동작은 A 레코드를 리턴하는 방식이기 때문에 CNAME처럼 두 번의 DNS 쿼리가 필요한 경우가 없습니다. 따라서 DNS쿼리 성능에 있어서도 CNAME보다는 아주 조금이지만 더 좋다고 볼 수 있습니다. Alias를 사용하면 안되는 특별한 이유가 있지 않다면 Alias를 활용하는 것을 추천합니다. Lambda ------ [AWS 예제](http://docs.aws.amazon.com/lambda/latest/dg/walkthrough-s3-events-adminuser.html)에 잘 나와있듯이 자동화된 이미지 리사이징을 위해 사용하고 있습니다. S3에 원본 이미지가 올라오는 이벤트를 받아 다양한 크기의 이미지를 재생성하여 업로드합니다. 버즈빌에서는 이미지 전송으로 인해 발생하는 트래픽을 줄이기 위해 WEBP포맷을 적극적으로 활용하고 있습니다. 하지만 성능이 낮은 클라이언트에서는 WEBP 렌더링 시간이 많이 걸리므로 JPEG포맷을 사용합니다. 이를 위해서 이미지는 항상 JPEG와 WEBP 두 개의 포맷으로 업로드해야 합니다. 현재는 웹서버 로직에서 WEBP변환을 하고 있지만 추후에는 이 부분도 Lambda를 이용해 처리할 계획입니다. 버즈빌에서 주로 사용하는 언어는 Python인데 현재 Node.js, Java에 더해서 최근에는 Python을 지원하기 시작해 조금 더 편하게 Lambda를 사용할 수 있게 되었습니다. Kinesis ------- 웹 서버의 로그 수집을 위해 Kinesis를 활용하고 있습니다. 각 서버에서는 fluentd가 Kinesis로 실시간으로 로그를 전송하고 별도의 인스턴스에서 Kinesis로부터 로그를 가져와 저장합니다. 실제로 Kinesis를 사용해보니 한 가지 불편한 점이 있었습니다. Kinesis 스트림은 내부적으로 여러 개의 샤드로 구성되어 있고 처리량을 늘리기 위해서는 샤드를 늘려야 하는데 GUI 콘솔에서는 샤드 개수를 수정하는 것이 불가능합니다. 오직 API를 이용해서만 가능하며 이 또한 key rebalancing을 직접 고려해서 코드를 작성해야 합니다. 뿐만 아니라 스트림으로부터 데이터를 가져오는 consumer 쪽에서도 샤드 개수 변화에 따른 고려를 해줘야 합니다. 처음에는 Kinesis의 스트림을 하나의 커다란 큐로 보고 사용만 하면 되는 줄 알았지만 스트림 내부의 샤드를 고려해야 해 생각보다 관리에 있어서 불편한 점이 있습니다. Consumer쪽 로직은 아마존에서 제공하는 [Kinesis Client Library](https://github.com/awslabs/amazon-kinesis-client)를 적극 활용하는 것을 추천합니다. Application 로직을 제외한 모든 부분을 KCL이 잘 추상화하여 관리해주기 때문에 개발자 입장에서 편하게 개발 할 수 있습니다. Kinesis 사용기와 관련된 자세한 부분은 추후 블로그 포스팅을 통해 더 상세하게 다뤄보겠습니다. 계정관리 ---- 기본적으로 root account는 사용하지 않고 IAM 유저를 생성해서 사용하고 있습니다. 가끔씩 AWS계정을 해킹당해 엄청난 비용이 청구됐다는 글을 볼 수 있습니다. 이를 방지하기 위해서는 MFA설정을 하는 것이 좋습니다. MFA 설정을 하면 OTP를 이용해 한 번 더 인증을 진행하므로 보안성을 크게 향상시킬 수 있습니다. 그런데 root account에 MFA를 설정했다가 핸드폰이 고장나거나 분실되는 경우 이를 복구하는 것이 상당히 귀찮을 수 있습니다. 이를 막기 위한 한 가지 팁은 QR코드를 이용해서 디바이스를 등록 할 때 다른 기기(예를 들면 가지고 있는 아이패드)도 같이 등록하는 것입니다. 이렇게 하면 동시에 여러 개의 기기를 등록할 수 있어 핸드폰 분실 등 유사시에 대비를 할 수 있습니다. 또는 QR코드를 캡쳐해서 프린트한 뒤 따로 보관해도 됩니다. 하지만 캡쳐한 QR코드 파일을 컴퓨터에 보관하는 것은 위험하므로 꼭 삭제해야 합니다. 마치며 --- 지금까지 버즈빌에서 사용하고 있는 AWS 서비스에 대해 소개했습니다. 기본적인 사용법에 관해서는 이미 많은 자료들이 있기 때문에 실제로 버즈빌에서 시스템을 운영하며 알게된 팁을 위주로 설명했습니다. 이 글을 정리하면서 느낀 점은 AWS 역시 계속 발전하고 있다는 것입니다. 사실 일 년에도 몇 개씩 새로운 서비스가 출시되고 있는 것을 보면 저걸 다 어떻게 유지, 보수를 하는건지 저희가 걱정이 될 정도입니다. 하지만 이는 그만큼 기술 혁신을 위해 아마존에서도 많은 노력을 하고 있다는 뜻입니다. 그렇기에 다른 클라우드 사업자들이 AWS를 따라잡기가 쉽지는 않을 것 같습니다. 또한 AWS에서는 여러 종류의 블로그를 운영하고 있습니다. 그 중에서도 [AWS 공식 블로그](https://aws.amazon.com/ko/blogs/aws/)에서는 각종 서비스 업데이트 소식, 새로운 서비스 런칭 소식, 가격 인하 소식 및 여러가지 팁 등을 소개하고 있어 많은 도움이 됩니다. 이것으로 버즈빌에서 AWS를 어떻게 활용하고 있는지에 대한 소개를 마치겠습니다. 앞으로도 AWS에서 유용한 서비스들이 많이 나왔으면 좋겠습니다. 다음 포스팅에서는 이러한 인프라 위에서 동작하고 있는 버즈애드 서버의 아키텍처에 대해서 자세하게 다뤄보겠습니다. --- ## [This Is Not a Traditional Hackathon Story](https://tech.buzzvil.com/blog/not-traditional-internal-hackathon-story) Date: 2014-07-08 | Category: Culture Two weeks ago, the entire Buzzvil team went to Yeoju for our annual workshop and our first internal hackathon. But let me just say upfront, this is not a traditional hackathon story. Nor is this about a traditional company workshop. At Buzzvil, we like to break out of the box, so we came up with a fusion of the two: a 3-day hackathon + workshop that became a one-of-a-kind experience for all of us. [![](https://farm4.staticflickr.com/3909/14539124064_daa83dbe9b_h.jpg)](https://www.flickr.com/photos/tkazec/14539124064/in/set-72157644236839716/) What Is a Hackathon? And What Is a Workshop? -------------------------------------------- By now, most of us are familiar with [what hackathons are](http://en.wikipedia.org/wiki/Hackathon "Wikipedia | Hackathon") and [how they can benefit companies and communities](http://www.fastcompany.com/3029885/why-you-should-probably-host-a-hackathon "Fast Company | WHY YOU SHOULD PROBABLY HOST A HACKATHON"). Typically, hackathons involve engineers getting together to build and often demo a new product or feature in an intensive 24~48-hour time span. While many large-scale hackathons are open to the public, more and more companies are seeing [the benefits of internal hackathons](http://www.gamasutra.com/blogs/AndrewPedersen/20140311/212797/Hacking_the_Game_Industry_Part_I_Three_Reasons_Why_Your_Company_Should_Run_Internal_Hackathons.php "Gama Sutra | Three Reasons Why Your Company Should Run Internal Hackathons") and are making them a regular part of their company culture, such as at [Yelp](http://officialblog.yelp.com/2014/01/what-the-heck-is-hackathon.html), [Indeed](http://engineering.indeed.com/blog/2014/02/5-tips-best-hackathon-ever/ "Indeed | 5 Tips For a Best. Hackathon. Ever."), and [SumAll](http://blog.sumall.com/journal/company-needs-host-hackathons.html "Why Your Company Needs to Host Hackathons"), to name a few. [![](https://farm4.staticflickr.com/3886/14623554503_5e050b761d_h.jpg)](https://www.flickr.com/photos/tkazec/14623554503/in/set-72157644236839716/) Then, you have your traditional Korean company "workshop." Just as a bit of background, many Korean companies have workshops once or twice a year for their entire team. All the employees stay overnight at an out-of-town pension or resort where they have team building activities, employee awards, and games. They're similar to the idea of company offsites or retreats, but much more casual. Last year, the Buzzvil team had our workshop in Busan to align with attending the [G-Star Conference](http://www.gstar.or.kr/eng/ "G-STAR "), but this year, we wanted to do something new. Our leadership team decided that an internal hackathon would be a great learning experience for all of us, so we threw a planning team together and set the wheels in motion. Buzzvil's First Internal Hackathon + Workshop --------------------------------------------- ### Goals We originally had three different goals for our internal hackathon: * **Team building**: Get our team to work with others that they may not regularly work with. * **Creative output**: Give everyone the opportunity to get away from their desks and daily tasks to squeeze out their creative juices. * **Product development**: Create working products or services that can be integrated into our existing [Honeyscreen](http://en.honeyscreen.com) app. While all three goals are important, we decided that for a team our size and for our first internal hackathon, the team building and creativity aspects are higher priorities than building a working product or feature. We designed our internal hackathon to be non-technical – more of an "ideathon" with a concept and business plan pitch. ### Theme As this was everyone's first hackathon, we thought it'd be helpful to set guidelines for the topic, so we decided to have a theme, one that aligns perfectly with [our company mission](http://www.buzzvil.com/#mission "Buzzvil | Mission"): "First Screen." We intentionally chose "first screen" rather than "lock screen" to allow room for interpretation, as the "first screen" can include devices other than smartphones. ### Prizes We figured that if we wanted everyone to get excited for the hackathon, we'd better make it worth their time and effort! So our planning team (with the support of our CEOs, John and Young) went all out for the grand prize: the first-place team wins a company-paid trip to a resort in Southeast Asia, plus additional vacation days for the trip. To further encourage team building, we also announced the MVP awards. Each team would be nominating one person from their team that demonstrated the greatest leadership, passion, and teamwork during the hackathon, and all six MVPs would receive a pair of movie tickets each. ### Judging Criteria With such an appealing grand prize on the line, we wanted to make sure that the judging criteria was as transparent and fair as possible. Given the time constraints and available resources, teams were not required to have a working demo for their product. Instead, everyone was given the breakdown of exactly what the scoring sheet would look like, so that they could plan their presentations accordingly: #### 60% of the score is calculated from peer scores: **25% - Real World Application** * _Does this product identify a real need, demand, or problem?_ * _Does this product solve the identified need, demand, or problem?_ **25% - Concreteness** * _Does the team identify clear customer segment(s)?_ * _Does the team present the Unique Value Proposition(s) clearly and effectively?_ * _Does the team effectively use channels to acquire new users and customers?_ * _Does the team present a strategy for customer relationships and retention?_ * _Overall, does the team thoroughly and persuasively present the product/service?_ **10% - Uniqueness** * _Is this idea a creative or new solution to a need or problem?_ * _Does the product innovate on AT LEAST ONE of the following? (a) features (b) design (c) user interactions (d) technical solutions_ #### 40% of the score is calculated from our two CEOs' scores: **20% - Possibility and Feasibility** * _Is it possible to create a Minimum Viable Product (MVP) within 3 months?_ * _Is it a stand alone product that can be built with our own resources? Or does it require other contributing partners to complete the product?_ **20% - Teamwork** * _Did each team member participate?_ * _Does the team clearly demonstrate each member's roles and responsibilities (R&R)?_ ### Idea Pitching and Team Selection We wanted to make sure that everyone on our team could participate in our first internal hackathon, so all 24 of us (not including our two CEOs) were asked to submit a short, written description of their hackathon idea in advance. Then, each Buzzvillian had to pitch his or her idea within one minute to the rest of the group on the first day of the hackathon. Presenting in front of a group can be intimidating, even if it's your teammates that you work with on a daily basis, but this was just another opportunity for us to push ourselves beyond our comfort zones, and everyone stepped up to the challenge spectacularly. [![](https://farm4.staticflickr.com/3858/14599803941_0ad0a87a46_h.jpg)](https://www.flickr.com/photos/tkazec/14599803941/in/set-72157644236839716/) [![](https://farm4.staticflickr.com/3899/14601093914_9264946ffb_h.jpg)](https://www.flickr.com/photos/tkazec/14601093914/in/set-72157644236839716/) After all the pitches, everyone then voted for their top two ideas (excluding their own). To submit and tally up everyone's votes, we used Google Forms, which was a great tool to track and streamline the voting process. The top six ideas then moved forward, with the original presenter as the team leader and three others who voted for the idea as teammates. It took a little bit of juggling to finalize the teams, but we eventually worked out the team arrangements. Then, everyone was let loose to start hacking! [![](https://farm6.staticflickr.com/5492/14599804231_ad6833a16c_h.jpg)](https://www.flickr.com/photos/tkazec/14599804231/in/set-72157644236839716/) [![](https://farm6.staticflickr.com/5531/14623138723_0bb9b8d47d_h.jpg)](https://www.flickr.com/photos/tkazec/14623138723/in/set-72157644236839716/) [![](https://farm4.staticflickr.com/3867/14601094274_ef45c1ad5b_h.jpg)](https://www.flickr.com/photos/tkazec/14601094274/in/set-72157644236839716/) [![](https://farm3.staticflickr.com/2907/14416428140_971d4e789f_h.jpg)](https://www.flickr.com/photos/tkazec/14416428140/in/set-72157644236839716/) ### Schedule Some of our team members were flying back in from out of the country, so we had to be flexible with our schedule for those that were arriving late. In the end, the hackathon portion lasted just under 24 hours. Not a lot of time, admittedly, but having a tighter deadline forced us to be that much more productive! #### Day 1 * **1:00 - 3:00 PM:** Travel to workshop location * **3:00 - 6:00 PM:** Workshop games and activities * **6:00 - 7:30 PM:** Dinner * **7:30 - 8:00 PM:** Hackathon Kick-off * **8:00 - 8:30 PM:** Idea Pitches * **8:30 - 9:00 PM:** Team selection and announcements * **9:00 - 11:00 PM:** Start hacking! * **11:00 PM:** Quiet Time - teams can continue working, but keep the volume down #### Day 2 * **9:00 - 10:00 AM:** Breakfast * **9:00 AM:** Teams continue hacking * **12:30 - 1:30 PM:** Lunch * **1:30 - 4:30 PM:** Teams continue hacking * **4:30 PM:** Hand in final presentations * **4:30 - 6:30 PM:** Team presentations * **6:30 - 7:00 PM:** Scoring period * **7:00 - 9:00 PM:** Dinner * **8:00 PM:** Announce winning team and MVPs * **9:00 - 11:00 PM:** Team bonding and games #### Day 3 * **10:00 - 11:00 AM:** Pack up and check out * **11:00 AM:** Team photo * **11:30 AM:** Lunch * **1:00 PM:** Travel back to Seoul ### Final Presentations All the final presentations were handed in by 4:30 p.m. and teams were chosen to present in random order. Each team had 10 minutes to thoroughly present their final product concept and cover all the aspects of the judging criteria to receive the highest score. Then, a 2-minute Q&A session followed for each team, giving our CEOs and all the other teams a chance to ask for clarification and grill the presenters. [![](https://farm6.staticflickr.com/5592/14623138823_6f4384806f_h.jpg)](https://www.flickr.com/photos/tkazec/14623138823/in/set-72157644236839716/) ### Results After all the peer scores were submitted with Google Forms and added up with the CEO scores, we had a clear winner. All six teams did very well and the scores of the first and last teams only differed by about 13 points, but the grand prize went to Team 1, who came up with a lock screen product that regulates smartphone usage in school classrooms. Congrats to Ohsu, Jinwoo, Kyunghwa, and Sungmi! Team 6, the runner-up team, presented a product that enables users to have an interactive "pet" on the first screen of your device, and they received honorable mention as well as a complimentary team dinner. Other team ideas included an emergency call app, a location-based social dating app, a short-form messaging app, and a weather + photo app, all with the element of bringing useful functions right to the first screen of the smartphone. All of our teams did an amazing job at bringing Buzzvil's mission to life! ### Rest & Relaxation After all the hard work and effort that everyone put in to the hackathon, the rest of our workshop was dedicated to food, fun and games! [![](https://farm6.staticflickr.com/5237/14599803521_c8d31eefab_h.jpg)](https://www.flickr.com/photos/tkazec/14599803521/in/set-72157644236839716/) [![](https://farm3.staticflickr.com/2927/14416428400_0d2028df55_h.jpg)](https://www.flickr.com/photos/tkazec/14416428400/in/set-72157644236839716/) [![](https://farm3.staticflickr.com/2915/14599804571_4dc8c20f4a_h.jpg)](https://www.flickr.com/photos/tkazec/14599804571/in/set-72157644236839716/) [![](https://farm4.staticflickr.com/3896/14580054396_7b03db7ea7_h.jpg)](https://www.flickr.com/photos/tkazec/14580054396/in/set-72157644236839716/) After everyone had their fill of BBQ for dinner, we were split into three teams and had a little friendly competition in games like charades, thigh wrestling (it's a real game in Korea!), and word guessing games. The eight people on the winning team each received a half-day off as a prize. Many thanks to Gibeom for organizing the games! [![](https://farm4.staticflickr.com/3915/14580054246_26aab55b56_h.jpg)](https://www.flickr.com/photos/tkazec/14580054246/in/set-72157644236839716/) [![](https://farm6.staticflickr.com/5553/14416489499_59cfc0f80a_h.jpg)](https://www.flickr.com/photos/tkazec/14416489499/in/set-72157644236839716/) [![](https://farm3.staticflickr.com/2911/14416664247_7e75009cb7_h.jpg)](https://www.flickr.com/photos/tkazec/14416664247/in/set-72157644236839716/) ### Learnings and Feedback We were all incredibly impressed by how hard everyone worked and the quality of the ideas the teams were able to produce in such a short period of time. There were a few logistic hiccups here and there, and we learned the hard way that a solid wifi connection is a must-have, but even though not everything went perfectly, our first internal hackathon + workshop was a tremendous success with far greater results than we expected. Our hackathon added a truly interactive team component to our workshop weekend, and this unique combination naturally brought everyone together and helped us to get better connected with each other. [![](https://farm6.staticflickr.com/5510/14416428960_d045f147ae_h.jpg)](https://www.flickr.com/photos/tkazec/14416428960/in/set-72157644236839716/) [![](https://farm4.staticflickr.com/3916/14580057566_31dfc3448a_h.jpg)](https://www.flickr.com/photos/tkazec/14580057566/in/set-72157644236839716/) As always, getting feedback is critical at Buzzvil, and we made sure to give everyone a chance to respond with their thoughts and suggestions. Based on our feedback survey, over 90% of our team were satisfied with the hackathon + workshop overall, and more than 80% said they'd like another Buzzvil hackathon. Sounds like we need to get ready for round 2! And yes, those are our team t-shirts. --- ## [Scaling PhantomJS With Ghost Town](https://tech.buzzvil.com/blog/scaling-phantomjs-ghost-town) Date: 2014-05-29 | Category: Frontend My first project at Buzzvil was to develop a system to reliably render images at scale with PhantomJS. The result of my efforts was [Ghost Town: simple queued & clustered PhantomJS processing](https://www.npmjs.org/package/ghost-town) in a tiny Node.js module. Problem ------- PhantomJS is not an easy library to work with. Crashing and freezing is common. High memory usage and slow startup time is expected, and scaling must be done manually. Ultimately, [PhantomJS](http://phantomjs.org) and [phantomjs-node](https://www.npmjs.org/package/phantom) are neglected projects, so these issues can be expected to remain indefinitely. At Buzzvil, we needed the ability to reliably render a wide variety of images, with low latency and therefore high concurrency. No existing project effectively managed this, so I researched and designed a module that could both gracefully recover from crashes and scale automatically. Solution -------- Enter Ghost Town. It does the heavy lifting of launching and managing PhantomJS and queuing and processing tasks, and then it gets out of your way to let you do the rest. Each item is stored in the master’s queue until a worker is ready, and then assigned for processing. If the processing times out or the assigned worker fails, Ghost Town will requeue it. Reliability guaranteed! To prevent memory leaks, Ghost Town creates separate PhantomJS processes for each worker, and periodically relaunches them based on their number of pages created. Implementation -------------- Initializing Ghost Town is easy. (Several configuration options are available for tweaking the runtime and efficiency settings; [see the documentation for details](https://www.npmjs.org/package/phantom#readme).) With no configuration: var town = require("ghost-town")(); Next, implement the master and worker as when using the normal cluster module. Usually a simple `if (town.isMaster)` check will suffice. if (town.isMaster) { // master code // queue items here } else { // worker code // process items here } The main master method is `town.queue(data, next)`, which queues an item with `data`, and calls `next(err, data)` when the item has been processed by the worker. You’re free to implement the master queuer however you like, probably using some sort of messaging system. At Buzzvil, we use [Thrift](http://thrift.apache.org). Our master starts a server, and queues render requests as it receives them: thrift.createServer(Renderer, { render: function (html, width, height, next) { town.queue({ html: html, width: width, height: height }, function (err, data) { next(err, !err && new Buffer(data, "base64")); }); } }).listen(1337); The only worker hook is the `town!queue(page, data, next)` event. Ghost Town automatically manages everything, so `page` will always be a brand new PhantomJS page. All you need to do is configure the page, process the `data` passed from the master, and call `next(err, data)` to pass it back. At Buzzvil, we render HTML documents. Our worker configures the page (size, headers, content) and renders it to an image: town.on("queue", function (page, data, next) { // setup code // sequentially configure the page here // page.set("viewportSize", ...) // page.set("customHeaders", ...) // page.set("onLoadFinished", ...) // page.set("content", ...) page.renderBase64("jpeg", function (data) { next(null, data); }); }); Conclusion ---------- Ghost Town has been successfully running at scale in production for over a month now, ultimately a significant improvement over our previous manual concurrency solution. Further improvements can be expected with the release of Node.js v0.12, and we hope the community benefits from our [release of Ghost Town](https://github.com/buzzvil/ghost-town) under the MIT license. Happy coding! ## Seminars --- ## [Google File System](https://tech.buzzvil.com/seminar/tech-weekly-gfs) Date: 2022-09-29 | Author: Raf Kim 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2022년 9월 30일 Buzzvil Tech Weekly에서는 Google File System에 대해 Data Engineer Raf가 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Metastable Failure in Distributed Systems](https://tech.buzzvil.com/seminar/tech-weekly-metastable-failure) Date: 2022-08-01 | Author: Raf Kim 버즈빌 개발팀에서는 매주 금요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2022년 8월 2일 Buzzvi일 Tech Weekly에서는 분산시스템에서 일어날 수 있는 회복 불가능 상태인 Metastable Failure State 대해 Data Engineer Raf가 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Minerva: Airbnb's Metric Platform](https://tech.buzzvil.com/seminar/tech-weekly-minerva) Date: 2022-06-02 | Author: Raf Kim 버즈빌 개발팀에서는 매주 금요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2022년 6월 3일 Buzzvi일 Tech Weekly에서는 Airbnb의 Metric Platform인 Minerva에 대해 Data Engineer Raf가 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Shape Up Method](https://tech.buzzvil.com/seminar/tech-weekly-shape-up) Date: 2022-02-24 | Author: Joel Lim 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2022년 2월 셋째주 Buzzvil Tech Weekly에서는 **Shape Up Method** 라는 제목으로 작은 조직에서 효과적으로 일하는 방법에 대해서 Server Developer 겸 Project Manager Joel이 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Buzzvil Billing Data Pipeline](https://tech.buzzvil.com/seminar/tech-weekly-billing-data-pipeline) Date: 2022-02-04 | Author: Will Choi 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2022년 2월 첫째주 Buzzvil Tech Weekly에서는 **Buzzvil Billing Data Pipeline** 이라는 제목으로 버즈빌 정산 데이터가 만들어지는 과정에 대해서 Server Developer Will이 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Journey of Dash's release cycle](https://tech.buzzvil.com/seminar/tech-weekly-journey-of-dash-release-cycle) Date: 2022-01-28 | Author: Luke Hwang 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2022년 1월 마지막 주 Buzzvil Tech Weekly에서는 **Journey of Dash's release cycle** 이라는 제목으로 대시라는 버즈빌의 어드민 프론트엔드의 릴리즈 사이클 변천사에 대해서 Frontend Engineer Luke가 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Exactly-Once in Apache Flink](https://tech.buzzvil.com/seminar/tech-weekly-exactly-one-in-flink) Date: 2022-01-05 | Author: Raf Kim 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2022년 1월 첫째 주 Buzzvil Tech Weekly에서는 **Excatly-Once in Apache Flink** 라는 제목으로 Flink에 Exactly-Once 를 어떻게 보장하고 있는지에 대해서 Data Engineer Raf가 발표해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다. *버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [MMORPG Server](https://tech.buzzvil.com/seminar/tech-weekly-mmorpg-server) Date: 2021-12-01 | Author: Alan Kim 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 12월 첫째 주 Buzzvil Tech Weekly에서는 **MMORPG Server** 라는 제목으로 MMORPG 게임 서버 개발 경험에 대해서 Client Team Leader Alan이 발표해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Container](https://tech.buzzvil.com/seminar/tech-weekly-container) Date: 2021-11-24 | Author: Bale Do 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 11월 넷째 주 Buzzvil Tech Weekly에서는 **Container** 라는 제목으로 Container 기술에 대해서 Server Developer Bale이 발표해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Building & Deploying CF](https://tech.buzzvil.com/seminar/tech-weekly-building-and-deploy-cf) Date: 2021-11-17 | Author: Peter Kim 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 11월 셋째 주 Buzzvil Tech Weekly에서는 **Building & Deploying CF** 라는 제목으로 버즈빌 광고 추천 시스템을 개발하며 데이터 프로세싱을 최적화한 경험에 대해서 Machine Learning Engineer Peter가 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Web Framework.](https://tech.buzzvil.com/seminar/tech-weekly-web-framework) Date: 2021-10-13 | Author: Justin Jeong 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 10월 둘째 주 Buzzvil Tech Weekly에서는 **Web Framework** 라는 제목으로 Web Framework에 대해서 Front-end Developer Justin이 발표해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [블랙박스 테스팅 기법](https://tech.buzzvil.com/seminar/tech-weekly-black-box-testing) Date: 2021-09-08 | Author: Erica 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 9월 둘째 주 Buzzvil Tech Weekly에서는 **블랙박스 테스팅 기법** 라는 제목으로 블랙박스 테스팅이 무엇인지? 또 어떻게 수행할 수 있는지를 QA Manager Erica가 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Starting Graph-Database!](https://tech.buzzvil.com/seminar/tech-weekly-starting-graph-database) Date: 2021-09-01 | Author: Asher 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 9월 첫째 주 Buzzvil Tech Weekly에서는 **Starting Graph-Database** 라는 제목으로 Graph-Database의 개념과 유스케이스를 Server Developer Asher가 발표해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [What the Agile!](https://tech.buzzvil.com/seminar/tech-weekly-what-the-agile) Date: 2021-08-25 | Author: Amy 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 8월 마지막 주 Buzzvil Tech Weekly에서는 **What the Agile!** 이라는 제목으로 Agile조직에서의 QA에 대한 내용을 QA Manager Amy가 발표해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Data Wrangling-Dangling](https://tech.buzzvil.com/seminar/tech-weekly-data-wrangling-dangling) Date: 2021-08-18 | Author: Venzino 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 8월 셋째 주 Buzzvil Tech Weekly에서는 **Data Wrangling-Dangling** 이라는 제목으로 Python 라이브러리를 활용하여 데이터를 다루는 흥미로운 방법과 경험에 대해서 Data Engineer Venzino가 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Who is QA manager?](https://tech.buzzvil.com/seminar/tech-weekly-who-is-qa-manager) Date: 2021-08-11 | Author: Jasmin 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 8월 둘째 주 Buzzvil Tech Weekly에서는 **Who is QA manager?** 라는 제목으로 QA manager의 업무와 버즈빌의 QA AS-IS와 TO-BE 에 대해서 QA Manager Jasmin이 발표해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [How to make Web Based Collaborate Code Editor](https://tech.buzzvil.com/seminar/tech-weekly-how-to-make-web-based-collaborate-code-editor) Date: 2021-07-28 | Author: Ethan 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 8월 첫째 주 Buzzvil Tech Weekly에서는 **How to make Web Based Collaborate Code Editor** 라는 제목으로 웹기반의 협업 코드 에디터 개발 경험을 Client Developer Ethan이 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Introduction to Data Quality](https://tech.buzzvil.com/seminar/tech-weekly-introduction-to-data-quality) Date: 2021-07-28 | Author: Andy Kim 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 7월 마지막 주 Buzzvil Tech Weekly에서는 **Introduction to Data Quality** 라는 제목으로 Data Quality에 대한 설명과 버즈빌의 현황과 방향에 대해서 Data Engineer Andy가 발표해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Hey, you can do it.](https://tech.buzzvil.com/seminar/tech-weekly-you-can-do-it) Date: 2021-07-14 | Author: JD Lim 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 7월 둘째 주 Buzzvil Tech Weekly에서는 **Hey, you can do it!** 이라는 제목으로 클라이언트 엔지니어링에 대해서 Client Developer JD가 발표해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [파이썬으로 데스크탑 어플리케이션 만들기](https://tech.buzzvil.com/seminar/tech-weekly-building-a-desktop-app-with-python) Date: 2021-07-07 | Author: Zune Seo 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 7월 첫째 주 Buzzvil Tech Weekly에서는 **파이썬으로 데스크탑 어플리케이션 만들기** 라는 제목으로 PyQt 를 활용하여 파이썬으로 데스크톱 어플리케이션을 개발하였던 경험에 대해서 CTO Zune이 발표해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Golang Generic Proposal](https://tech.buzzvil.com/seminar/tech-weekly-generic-proposal) Date: 2021-06-30 | Author: Joel Lim 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 6월 마지막 주 Buzzvil Tech Weekly에서는 **Golang Generic Proposal** 이라는 제목으로 Go에 새롭게 추가될것으로 기대되는 Generic 타입에 대해서 Server Developer Joel이 발표해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Baby Machine Learning](https://tech.buzzvil.com/seminar/tech-weekly-baby-machine-learning) Date: 2021-06-23 | Author: Hes Song 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 6월 넷째 주 Buzzvil Tech Weekly에서는 **Baby Machine Learning** 이라는 제목으로 회귀부터 신경망 이론까지의 머신러닝에 대한 전반적인 사항을 Machine Learning Engineer Hes가 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [SaaS License 도전기](https://tech.buzzvil.com/seminar/tech-weekly-saas-license) Date: 2021-06-16 | Author: Noah Lee 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 6월 셋째 주 Buzzvil Tech Weekly에서는 **SaaS License 도전기** 라는 제목으로 버즈빌에서도 현재 사용중인 Heytaco Anywhere 라는 서비스를 개발하며 SaaS License 모델을 구현하였던 경험을 Devops Engineer Noah가 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [How to apply the NLP model to advertising](https://tech.buzzvil.com/seminar/tech-weekly-how-to-apply-the-nlp-model-to-advertising) Date: 2021-06-09 | Author: Bronn Cha 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 6월 둘째 주 Buzzvil Tech Weekly에서는 **How to apply the NLP model to advertising** 이라는 제목으로 NLP 모델을 현재 어떻게 버즈빌 광고에 적용하고 있는지에 대한 발표를 Machine Learning Engineer Bronn이 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Building a Cryptocurrency Trading System](https://tech.buzzvil.com/seminar/tech-weekly-building-a-cryptocurrency-trading-system) Date: 2021-06-02 | Author: Will Choi 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 6월 첫째 주 Buzzvil Tech Weekly에서는 **Building a Cryptocurrency Trading System** 이라는 제목으로 암호화폐 트레이딩 시스템 구축 경험에 대한 발표를 Server Developer Will이 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Async Cosmic Python](https://tech.buzzvil.com/seminar/tech-weekly-async-cosmic-python) Date: 2021-05-26 | Author: Isac Yoo 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 5월 마지막 주 Buzzvil Tech Weekly에서는 **Async Cosmic Python** 이라는 제목으로 버즈빌에서 Python 프로젝트의 DDD 구현에 사용하는 Cosmic Python의 몇 가지 문제와 해결방법에 대한 제안을 Server Developer Isac이 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Draw a page with SPA](https://tech.buzzvil.com/seminar/tech-weekly-draw-a-page-with-spa) Date: 2021-05-12 | Author: May Min 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 5월 둘째 주 Buzzvil Tech Weekly에서는 **Draw a page with SPA** 라는 제목으로 Single Page Application에 대한 발표를 Front-end Developer May가 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/) --- ## [Improving Adserver](https://tech.buzzvil.com/seminar/tech-weekly-improving-adserver) Date: 2021-04-21 | Author: Claud Choi 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 4월 넷째 주 Buzzvil Tech Weekly에서는 **Improving Adserver** 라는 제목으로 버즈빌의 핵심 Backend 서버인 Adserver의 CI 성능 개선 경험에 대한 발표를 Server Developer Claud가 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/) --- ## [http.Client in go](https://tech.buzzvil.com/seminar/tech-weekly-http-client-in-go) Date: 2021-04-14 | Author: Raf Kim 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 4월 둘째 주 Buzzvil Tech Weekly에서는 **http.Client in go** 라는 제목으로 go 라이브러리인 http.Client를 파헤쳐본 경험에 대한 발표를 Data Engineer Raf가 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Architecture Chronicle](https://tech.buzzvil.com/seminar/tech-weekly-architecture-chronicle) Date: 2021-03-24 | Author: Kyle Jang 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 3월 넷째 주 Buzzvil Tech Weekly에서는 **Architecture Chronicle** 이라는 제목으로 소프트웨어 아키텍처의 전반적인 연대기에 대한 발표를 Server Developer Kyle이 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Android QA](https://tech.buzzvil.com/seminar/tech-weekly-android-qa) Date: 2021-03-17 | Author: Ethan Yoo 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 3월 셋째 주 Buzzvil Tech Weekly에서는 **Android QA**라는 제목으로 Android를 위한 QA 프로세스에 대한 논의를 Client Developer Ethan이 진행해주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Getting Started with SLO](https://tech.buzzvil.com/seminar/tech-weekly-getting-started-with-slo) Date: 2021-03-10 | Author: Liam Hwang 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 3월 둘째 주 Buzzvil Tech Weekly에서는 **Getting Started with SLO**라는 제목으로 Service Level Objective에 대한 소개와 실제로 어떻게 이를 회사 문화에 녹여낼 수 있는지 대한 논의를 DevOps Engineer Liam이 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Progressive Web App](https://tech.buzzvil.com/seminar/tech-weekly-progressive-web-app) Date: 2021-03-03 | Author: Nathan Yang 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 3월 첫째 주 Buzzvil Tech Weekly에서는 **Progressive Web App** 이라는 제목으로 웹사이트를 모바일 앱 처럼 만들기 위한 좋은 방법 대한 발표를 Server Developer Nathan이 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [How to build a self-driving car](https://tech.buzzvil.com/seminar/tech-weekly-how-to-build-a-self-driving-car) Date: 2021-02-24 | Author: Peter Kim 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 2월 넷째 주 Buzzvil Tech Weekly에서는 **How to build a self-driving car** 라는 제목으로 자율 주행 자동차에 대한 흥미로운 발표와 시뮬레이션을 Machine Learning Engineer Peter가 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Just Build It!](https://tech.buzzvil.com/seminar/tech-weekly-just-build-it) Date: 2021-01-27 | Author: Benjamin Baldivia 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2021년 1월 마지막 주 Buzzvil Tech Weekly에서는 **Just Build It** 이라는 제목으로 신속하게 프로젝트를 빌드하기 위한 방법에 대한 발표를 Front-end Developer Benjamin이 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Manipulating Financial Data in Python](https://tech.buzzvil.com/seminar/tech-weekly-manipulating-financial-data-in-python) Date: 2020-11-18 | Author: Harry Park 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2020년 11월 셋째 주 Buzzvil Tech Weekly에서는 **Manipulating Financial Data in Python** 이라는 제목으로 파이썬을 이용하여 주식 데이터를 처리하기에 대한 발표를 Client Developer Harry가 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Cloud Events](https://tech.buzzvil.com/seminar/tech-weekly-cloud-events) Date: 2020-11-11 | Author: Claud Choi 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2020년 11월 둘째 주 Buzzvil Tech Weekly에서는 **Cloud Events** 라는 제목으로 CNCF에 포함되어 있는 CloudEvents 라는 프로젝트에 대한 발표를 Server Developer Claud가 진행해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [How to Secure My Files](https://tech.buzzvil.com/seminar/tech-weekly-how-to-secure-my-files) Date: 2020-08-05 | Author: Brice Bang 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2020년 8월 첫째 주 Buzzvil Tech Weekly에서는 **How to Secure My Files**라는 제목으로 파일을 안전하게 하는 방법에 대한 논의가 진행되었습니다. 비밀번호를 안전하게 관리하는 방법, 파일을 안전하게 삭제하는 방법에 대해 알아보고, 마지막으로 디스크를 암호화하여 삭제하지 않아도 안전하게 관리하는 방법에 대해 살펴보았습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
--- ## [RabbitMQ Overview](https://tech.buzzvil.com/seminar/tech-weekly-rabbitmq) Date: 2020-01-21 | Author: Raf Kim 버즈빌 개발팀에서는 매주 금요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2020년 1월 22일 Buzzvi의 Tech Weekly에서는 RabbitMQ에 대해 Backend Engineer Raf가 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Solid State Drive](https://tech.buzzvil.com/seminar/tech-weekly-solid-state-drive) Date: 2019-09-17 | Author: Raf Kim 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2019년 9월 18일 Buzzvil Tech Weekly에서는 SSD의 내부 동작에 대해 Backend Engineer Raf가 공유해 주셨습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
*버즈빌에서 함께 일할 출중한 개발자를 찾습니다.(전문연구요원포함)([바로가기](https://buzzvil.career.greetinghr.com/)) --- ## [Buzzvil Client Modularization](https://tech.buzzvil.com/seminar/buzzvil-client-modularization) Date: 2019-05-15 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다. 이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2019년 5월 셋째 주 Buzzvil Tech Weekly에서는 **Buzzvil Client Modularization**의 주제로 논의가 진행되었습니다. Client Modularization에 대한 Summary를 하고 더 나아가 버즈빌에서 모듈화를 수행하기 위해 고려해야 하는 주제들을 함께 살펴보았습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.
--- ## [XDD in Action & Go Testing](https://tech.buzzvil.com/seminar/tech-weekly-xdd-in-action-go-testing) Date: 2019-02-27 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Go & Clean Architecture in Action](https://tech.buzzvil.com/seminar/tech-weekly-go-clean-architecture-in-action) Date: 2019-02-27 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [DDD & Clean Architecture in Action](https://tech.buzzvil.com/seminar/tech-weekly-ddd-clean-architecture-in-action) Date: 2019-02-27 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Evolution of HTTP](https://tech.buzzvil.com/seminar/tech-weekly-evolution-of-http) Date: 2019-02-20 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [RESTful API](https://tech.buzzvil.com/seminar/tech-weekly-restful-api) Date: 2019-01-30 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Attack N Defence](https://tech.buzzvil.com/seminar/tech-weekly-attack-n-defence) Date: 2019-01-23 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Introduction to Airflow - A dataflow engine](https://tech.buzzvil.com/seminar/tech-weekly-introduction-to-airflow-a-dataflow-engine) Date: 2019-01-16 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Storybook X Styleguidist](https://tech.buzzvil.com/seminar/tech-weekly-storybook-x-styleguidist) Date: 2019-01-02 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Kotlin Coroutine](https://tech.buzzvil.com/seminar/tech-weekly-kotlin-coroutine) Date: 2018-12-19 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Domain Driven Design](https://tech.buzzvil.com/seminar/tech-weekly-domain-driven-design-2) Date: 2018-12-12 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Block Chain](https://tech.buzzvil.com/seminar/tech-weekly-block-chain) Date: 2018-12-06 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [LDA Topic Modeling](https://tech.buzzvil.com/seminar/tech-weekly-lda-topic-modeling) Date: 2018-11-28 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Site Reliability Engineering](https://tech.buzzvil.com/seminar/tech-weekly-site-reliability-engineering) Date: 2018-11-21 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Normal Flow](https://tech.buzzvil.com/seminar/tech-weekly-normal-flow) Date: 2018-11-14 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Protocol and Value
 Oriented Programming](https://tech.buzzvil.com/seminar/tech-weekly-protocol-and-value%e2%80%a8-oriented-programming) Date: 2018-11-07 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Breaking Chains](https://tech.buzzvil.com/seminar/tech-weekly-breaking-chains) Date: 2018-10-31 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [CAP and PACELC : the basic theorem of distributed database system](https://tech.buzzvil.com/seminar/tech-weekly-cap-and-pacelc-the-basic-theorem-of-distributed-database-system) Date: 2018-10-24 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Dive in IAC (Infrastructure as Code)](https://tech.buzzvil.com/seminar/tech-weekly-dive-in-iac-infrastructure-as-code) Date: 2018-10-17 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Living in Cloud-Native World](https://tech.buzzvil.com/seminar/tech-weekly-living-in-cloud-native-world) Date: 2018-10-10 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Uncle Bob Special](https://tech.buzzvil.com/seminar/tech-weekly-uncle-bob-special) Date: 2018-09-19 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Hello TLS 1.3](https://tech.buzzvil.com/seminar/tech-weekly-hello-tls-1-3) Date: 2018-09-12 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Design patterns](https://tech.buzzvil.com/seminar/tech-weekly-design-patterns) Date: 2018-08-29 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다.
--- ## [Dagger2](https://tech.buzzvil.com/seminar/tech-weekly-dagger2) Date: 2018-08-22 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. Dependency Injection tool인 Dagger2에 대해서 알아보기에 앞서서 기본적인 Dependency Injection의 개념에 대해 알아보고 Dagger2를 활용할 수 있는지를 살펴보았습니다. 이어서 Dagger2를 사용한 데모를 진행하며 Dagger2를 사용하는 것이 좋은지에 대해서도 논의해볼 수 있었습니다. 보다 자세한 내용은 아래의 슬라이드를 통해 확인하실 수 있습니다.   *버즈빌에서는 함께할 개발자를 채용 중입니다. 지금 바로 확인 해보세요! (전문연구요원 포함)
--- ## [에자일 방법론](https://tech.buzzvil.com/seminar/tech-weekly-agile-methodology) Date: 2018-08-08 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 기존에 전통적으로 사용되던 개발방법론들로 부터 애자일 방법론까지의 흐름을 살펴보고 버즈빌에서 적용하고 있는 개발방법론에 대해 되짚어 보는 시간이었습니다. 뿐만아니라 앞으로의 발전시켜나갈 버즈빌의 개발문화에서 우리가 지향해 나가야하는 가치들을 돌아보고 공유하였습니다. 아래의 슬라이드를 통해 상세한 내용을 확인해보세요!   *버즈빌에서는 함께할 개발자를 모시고 있습니다. (전문연구요원 포함)
--- ## [Typescript](https://tech.buzzvil.com/seminar/tech-weekly-typescript) Date: 2018-08-01 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. Typescript 대한 기초부터 간단한 활용 방법과 문법에 대해 알아보았습니다. 또한 Typescript가 가진 장점들과 한계에 대해 논의하며 버즈빌에서는 Typescript를 어떻게 활용해 나갈 수 있을지에 대한 의견을 나누는 시간을 가졌습니다. 아래의 슬라이드를 통해 그 내용들을 살펴보실 수 있습니다.   *버즈빌에서는 함께할 개발자를 채용하고 있습니다. (전문연구요원 포함)
--- ## [Android App Repackaging](https://tech.buzzvil.com/seminar/tech-weekly-android-app-repackaging) Date: 2018-07-25 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 안드로이드에서 사용되는 APK파일의 구조와 빌드 과정에 대해 알아보고 이를 repackaging하는 방법을 통해 가해질 수 있는 악의적인 공격들에 대해 논의해보았습니다. 이를 통해 우리가 가진 취약점을 돌아보고 더욱 안전한 앱을 만들기 위한 방법에 대한 논의도 이어졌습니다. 보다 자세한 사항은 아래의 슬라이드를 통해 확인하실 수 있습니다.  
--- ## [Ad-client](https://tech.buzzvil.com/seminar/tech-weekly-ad-client) Date: 2018-07-18 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 서비스를 지속해 나가면서 새로운 기능들을 개발하다보면 서비스의 크기가 처음에 생각했던 것보다 커지는 경우가 발생합니다. 이렇게 되면 특정 부분의 간단한 수정하기 위한 작업도 전체코드를 위한 작업환경의 세팅을 해야하고 배포과정도 복잡해지는 등의 문제가 발생하는데요. 버즈빌에서도 비슷한 문제가 있었고 이를 해결하기 위해서 특정 기능을 전체 서비스에서 분리하는 시도를 하고 있습니다. Ad-Client는 그 시도 중에 하나라고 할 수 있겠습니다. 자세한 내용은 아래 슬라이드를 참조하세요!   *버즈빌에서 함께할 개발자를 채용 중입니다. (전문연구요원 포함)
--- ## [MySQL Online Schema](https://tech.buzzvil.com/seminar/tech-weekly-mysql-online-schema) Date: 2018-07-13 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 이번 Buzzvil Tech Weekly에서는 **'**MySQL Online Schema**'**의 주제로 논의가 진행되었습니다. 다양한 기능들이 추가되면서 과거에 만들었던 DB의 Schema를 수정해야하는 상황들이 종종 발생하게 되는데요. 이런 상황에서 중단 없이 Online으로 DB의 Schema를 바꿀 수 있는 방법들에 대해 알아보고 각각의 장단점을 비교해보는 시간을 가졌습니다. 이를 바탕으로 몇 개의 Table에 대한 Migration도 실제로 진행할 예정입니다. 아래의 슬라이드를 통해 자세한 내용을 확인해 보실 수 있습니다.
--- ## [Kubernetes in Action on AWS](https://tech.buzzvil.com/seminar/tech-weekly-kubernetes-in-action-on-aws) Date: 2018-07-04 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 이번 Buzzvil Tech Weekly에서는 **'Kubernetes in Action on AWS'**의 주제로 논의가 진행되었습니다. Container Orchestration Tool 인 Kubernetes의 구조와 장점에 대해 알아보고 각자의 Kubernetes 를 활용할 수 있는 환경을 세팅해보고 간단한 데모도 함께 진행하였습니다. 지금은 Kubernetes를 전체 시스템에 활용하고 있지는 않지만 점차 그 활용 범위를 늘려 나갈 계획입니다. 아래의 슬라이드를 통해 자세한 사항을 만나보실 수 있습니다.  
--- ## [Django MySQL Optimization](https://tech.buzzvil.com/seminar/tech-weekly-django-mysql-optimization-2) Date: 2018-06-27 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 이번 Buzzvil Tech Weekly에서는 **'Django MySQL Optimization'**의 주제로 논의가 진행되었습니다. MySQL 최적화를 위해서 DB에서 어떤 쿼리들이 불리고 있는지를 체크하는 프로파일링 방법에서부터 불필요한 쿼리들을 찾아내서 실제로 최적화를 진행한 사례까지 함께 살펴 보았습니다. 이 시간을 통해서 앞으로 개발을 진행하면서 DB에 최적화된 코드를 작성하기 위해서는 어떤 방식으로 고민해야할지를 함께 고민해 보는 시간이었습니다. 자세한 내용은 아래의 슬라이드를 참고해 주세요.
--- ## [Upgrading Buzzad to Python 3](https://tech.buzzvil.com/seminar/tech-weekly-upgrading-buzzad-to-python-3) Date: 2018-06-20 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 이번 Buzzvil Tech Weekly에서는 **'**Upgrading to Buzzad to Python 3**'**의 주제로 논의가 진행되었습니다. python2와 python3의 대표적인 차이점들과 업그레이드를 도와주는 툴들을 함께 살펴보고 python2를 python3로 업그레이드 하는 과정에서 생길 수 있는 문제들과 업그레이드 전에 미리 준비해야하는 점에는 어떤 것들이 있을지를 논의하였습니다. 이를 바탕으로 현재 python2로 되어 있는 BuzzAD 서버를 python3로 업그레이드 해 나갈 예정입니다. 아래 슬라이드를 통해 보다 상세한 내용을 만나보실 수 있습니다.
--- ## [MySQL Replication](https://tech.buzzvil.com/seminar/tech-weekly-mysql-replication) Date: 2018-05-23 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2018년 5월 네째 주 Buzzvil Tech Weekly에서는 **'**MySQL Replication**'**의 주제로 논의가 진행되었습니다. DB운영 효율화를 위해 MySQL DB를 migrate하는 과정에서 꼭 필요한 DB Replication에 대해서 살펴보고 실제로 이를 버즈빌에서 어떻게 적용했는지 함께 살펴보았습니다. 아래의 슬라이드를 통해 자세한 내용을 확인해 보세요!
--- ## [Android IPC Mechanisms](https://tech.buzzvil.com/seminar/tech-weekly-android-ipc-mechanisms-2) Date: 2018-05-16 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2018년 5월 셋째 주 Buzzvil Tech Weekly에서는 **'Android IPC Mechanisms - And how we handled the migration issue'**의 주제로 논의가 진행되었습니다. 안드로이드 OS의 구조와 IPC(Inter-Process Communication)에 대해 상세히 살펴보고 구글 이슈를 대응하는 과정에서 이를 어떤식으로 활용했는지를 함께 리뷰하는 시간을 가졌습니다. 상세한 내용은 아래 슬라이드를 통해 확인하실 수 있습니다.
--- ## [Kotlin for Android 101](https://tech.buzzvil.com/seminar/tech-weekly-kotlin-for-android-101) Date: 2018-05-09 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2018년 5월 둘째 주 Buzzvil Tech Weekly에서는 **'Kotlin for Android 101'**의 주제로 논의가 진행되었습니다. 기본적으로 Kotlin이 Java와 어떻게 다른지와 어떤 장점이 있는지를 살펴보고 앞으로 클라이언트 단에서 Kotlin을 활용하기 위해서는 어떤 부분을 주의해야 할지도 함께 고민해 보았습니다. 자세한 사항은 아래의 슬라이드를 통해 살펴보실 수 있습니다.
--- ## [JSON WEB TOKENS(JWT)](https://tech.buzzvil.com/seminar/tech-weekly-json-web-tokensjwt) Date: 2018-04-25 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2018년 4월 네째 주 Buzzvil Tech Weekly에서는 **'JSON WEB TOKENS(JWT)'**의 주제로 논의가 진행되었습니다. JWT란 무엇인지를 간단하게 알아보고 여러 개의 Microservice로 운영되는 서비스를 디자인 함에 있어서 Authentication 의 방법으로 JWT를 어떻게 활용할 수 있을지에 대해 논의하였습니다. 아래의 슬라이드를 통해 자세한 사항을 확인해주세요.
--- ## [Clean Architecture](https://tech.buzzvil.com/seminar/clean-architecture) Date: 2018-04-18 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2018년 4월 셋째 주 Buzzvil Tech Weekly에서는 **'**Clean Architecture**'**의 주제로 논의가 진행되었습니다. Clean Code에 대한 Summary를 하고 더 나아가 Clean Architecture를 만들기 위해서 고려해야 하는 원칙들 중에서 가장 대표적인 SOLID Principle을 각각의 예시와 함께 살펴보았습니다. 아래의 슬라이드를 통해 보다 자세한 사항을 확인하실 수 있습니다.!
--- ## [Redis - Persistence, Availability & Security](https://tech.buzzvil.com/seminar/tech-weekly-redis-persistence-availability-security) Date: 2018-04-11 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2018년 4월 둘째 주 Buzzvil Tech Weekly에서는 **'**Redis - Persistence, Availability & Security**'**의 주제로 논의가 진행되었습니다. Redis DB를 Persistence / Availability / Security 세 가지 관점에서 돌아보고 앞으로 Redis DB를 활용할때 주의할 점들에 대해 함께 논의해보는 시간을 가졌습니다. 보다 상세한 내용은 아래의 슬라이드를 통해 확인해 보세요 !
--- ## [MySQL Transactions and Locks](https://tech.buzzvil.com/seminar/tech-weekly-mysql-transactions-and-locks-2) Date: 2018-04-04 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2018년 4월 첫 주 Buzzvil Tech Weekly에서는 **Andy**이 발제해주신 **'**MySQL Transactions and Locks**'**의 주제로 논의가 진행되었습니다. DB의 기본원칙인 ACID와 이를 보장하기 위한 MySQL의 transaction에 대해 살펴보았습니다. 또한 traffic이 많은 상황에서는 어떤 종류의 transaction을 사용하는 것이 효과적일지도 함께 고민해 보았습니다. 자세한 내용은 아래의 슬라이드를 확인해 주세요.
--- ## [TCP/IP 101](https://tech.buzzvil.com/seminar/tech-weekly-tcp-ip-101) Date: 2018-03-14 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2018년 3월 두번째 주 Buzzvil Tech Weekly에서는 **Brice**이 발제해주신 **'TCP/IP 101'**의 주제로 논의가 진행되었습니다.  네트워크 통신의 기본이 되는 TCP/IP에 대한 기본적인 부분을 함께 돌아보고 네트워크 단에서 생길 수 있는 문제들과 이를 해결하는 방안들에는 어떤 방안들이 있는지를 간단하게 알아보는 시간을 가졌습니다. 보다 자세한 사항은 아래 슬라이드를 참고해주세요 !
--- ## [MySQL Index](https://tech.buzzvil.com/seminar/tech-weekly-mysql-index) Date: 2018-03-07 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2018년 3월 첫 주 Buzzvil Tech Weekly에서는 **Yohan**이 발제해주신 **'My SQL Index'** 라는 주제로 논의가 진행되었습니다. 버즈빌에서 사용하고 있는 여러 종류의 DB중에 가장 많이 사용되는 MySQL를 활용함에 있어 기본이 되는 Index에 대해 돌아보고 개선 방안에 대해 논의해보는 시간을 가졌습니다. 자세한 발표 내용은 아래 슬라이드를 참고해주세요 !
--- ## [Logs, AWS Athena, AWS Glue](https://tech.buzzvil.com/seminar/tech-weekly-logs-aws-athena-glue) Date: 2018-02-28 버즈빌 개발팀에서는 매주 수요일, 모든 팀의 개발자들이 모여 본인이 관심있는 분야에 대한 노하우나 담당하고 있는 업무와 관련된 새로운 기술에 대한 논의를 진행하는 시간을 가지고 있습니다.  이 시간을 통해 내가 알고 있는 것들에 대해 더 확실히 알고 모르는 것들에 대해 배우며 이를 바탕으로 보다 효율적이고 창의적으로 업무를 진행하기 위해 노력하고 있습니다. 2018년 2월 마지막 주 Buzzvil Tech Weekly에서는 **Andy**가 발제해 주신 **'Logs, AWS Athena, AWS Glue'** 라는 주제로 논의가 진행되었습니다. 버즈빌에서 만들어내는 수많은 데이터를 어떻게 다루고 있고 이를 더 효율적으로 다루기 위해서는 어떤 방법들을 활용해 볼 수 있을지 고민해보는 시간이었습니다. 보다 자세한 사항은 아래의 슬라이드를 참고해 주세요 !
## Handbook --- ## [안드로이드 모듈 구성](https://tech.buzzvil.com/handbook/android-module) Date: unknown 이 문서는 안드로이드 모듈을 구성할 때 고려할 사항들을 설명한다. 다음 내용들은 추천 사항이며, 프로젝트나 모듈에 따라 다른 구성을 가질 수 있다. ## 가이드라인 - 신규로 작성되는 모듈은 가능한 `Kotlin` 으로 구성한다. - 의존성 주입 패턴(Dependency injection pattern)을 적극적으로 활용한다. - 신규 모듈은 버즈빌 안드로이드 단일 리포지터리(Monorepo)인 [buzz-android](https://github.com/Buzzvil/buzz-android) 에 생성한다. ## 모듈 생성 단위 기능으로 의미가 있고 여러 모듈에서 사용될만한 기능이라면 신규 모듈을 생성하는 것을 권장한다. 신규 모듈 생성 시 모듈 디자인과 리뷰 과정을 선행해야 하며 이를 통해 디자인 문서를 생성한다. 이후 작성한 디자인 문서를 바탕으로 적절한 레이어에 모듈을 생성하고, Readme, 기본 라이브러리, 버전, 배포, Proguard 설정을 작성한다. ### 앱 모듈 일반적인 application / sdk 를 구성하는 모듈 계층이다. 시스템의 최상위에서 각 모듈의 디펜던시와 조립을 담당하며 비지니스 로직은 포함하지 않는다. 앱 모듈은 피쳐 모듈과 라이브러리 모듈의 접근 권한을 갖고 있으며, 시스템 전체 의존성 주입을 관리한다. 앱 모듈은 `/app` 폴더 안에 생성한다. ### 피쳐 모듈 유저가 사용하는 단위 기능을 제공하는 모듈 계층이다. 기능별로 비지니스 로직을 포함하고 있으며 직접 구현 혹은 라이브러리 모듈을 사용하여 구현을 완성한다. 피쳐 모듈은 다른 피쳐 모듈과 라이브러리 모듈을 참조 할 수 있다. 일반적인 경우 동일 레이어 모듈 간에는 참조를 권하지 않으며, 참조를 해야 하는 경우 순환 참조가 일어나지 않도록 주의해야 한다. 피쳐 모듈은 `/feature` 폴더 안에 생성한다. ### 라이브러리 모듈 여러 피쳐 모듈에서 재사용하는 단위 로직을 분리하여 라이브러리 모듈로 구성한다. 피쳐 모듈과 라이브러리 모듈의 구분은 정확히 규칙을 정의하기에 모호한 경우가 있지만, 일반적으로 유저 관점에서의 기능인지를 주요 구분 팩터로 여긴다. 라이브러리 모듈은 다른 라이브러리 모듈을 참조 할 수 있다. 일반적인 경우 동일 레이어 모듈 간에는 참조를 권하지 않으며, 참조를 해야 하는 경우 순환 참조가 일어나지 않도록 주의해야 한다. 피쳐 모듈은 `/lib` 폴더 안에 생성한다. ## 모듈 구성 ### `build.gradle` 가이드라인 디펜던시 버전 관련 값은 `rootProject.ext`에 정의된 변수를 사용한다. 이를 통해 전체 모듈이 같은 디펜던시를 가지고 있게 유지할 수 있으며, 의존성 지옥(Dependency hell)으로 빠지는 것을 방지할 수 있다. 디펜던시 추가 시 `implementation` 과 `api` 등 의존성 설정의 차이를 분명히 파악하고 적합한 키워드를 사용한다. 내부 디펜던시를 숨김으로서 빌드 성능을 최적화 할 수 있고, 모듈을 사용할 때 복잡성을 낮출 수 있다. 자세한 내용은 [Dependency configurations](https://developer.android.com/studio/build/dependencies#dependency_configurations) 문서를 참조한다. ## 기본 라이브러리 구성 ### RxJava / RxKotlin ```gradle implementation "io.reactivex.rxjava2:rxkotlin:$rootProject.ext.rxKotlinVersion" ``` [RxJava](https://github.com/ReactiveX/RxJava)는 옵져버 패턴(Observer Pattern)과 이터레이터 패턴(Iterator Pattern), 함수형 프로그래밍(Functional Programming)의 아이디어들로 구성된 라이브러리다. 이를 활용해서 비동기적인 동작을 손쉽게 구성할 수 있다. RxJava를 활용해서 콜백 지옥(Callback hell)을 벗어날 수도 있다. [Single](http://reactivex.io/documentation/single.html)을 활용해서 네트워크 요청 같은 비동기(Asynchronous) 동작의 데이터 스트림(Data Stream)을 제어할 수도 있다. 자세한 활용법은 [ReactiveX](http://reactivex.io/)를 참고한다. ### Dagger 2 ```gradle implementation "com.google.dagger:dagger:$rootProject.ext.daggerVersion" implementation "com.google.dagger:dagger-android-support:$rootProject.ext.daggerVersion" annotationProcessor "com.google.dagger:dagger-compiler:$rootProject.ext.daggerVersion" annotationProcessor "com.google.dagger:dagger-android-processor:$rootProject.ext.daggerVersion" ``` [Dagger](https://google.github.io/dagger/)는 자바와 안드로이드를 위한 의존성 주입(Dependency injection) 라이브러리다. Dagger를 활용해서 의존성 주입 패턴을 쉽게 구성하고 테스트할 수 있다. [의존성 주입](https://en.wikipedia.org/wiki/Dependency_injection)과 [의존성 역전 원칙](https://en.wikipedia.org/wiki/Dependency_inversion_principle)에 대해서는 각각 링크된 문서들을 참고한다. #### 모듈 내 의존성 주입 모듈 내부에서 사용하는 의존성의 경우 Dagger의 [@Component](https://dagger.dev/api/latest/dagger/Component.html), [@Subcomponent](https://dagger.dev/api/latest/dagger/Subcomponent.html), [@Module](https://google.github.io/dagger/api/latest/dagger/Module.html) 선택 이용하여 구성한다. ##### Feature Layer 일반적으로 피쳐 모듈의 경우 @Component 혹은 @Subcomponent를 사용하여 별도로 의존성을 관리할 수 있도록 한다. ```kotlin @Component(modules = [LibraryModule::class]) interface FeatureComponent { fun inject(view: View) } ``` ```kotlin @Subcomponent(modules = [LibraryModule::class]) interface FeatureSubcomponent { fun inject(view: View) } ``` ##### Library Layer 라이브러리 모듈은 별도의 Scope 관리가 필요한 경우가 아니라면 @Module을 제공하는 것으로 구현을 마무리한다. ```kotlin @Module object LibraryModule { @Provides fun provideSome() = Some() } ``` --- ## [멀티 모듈 SDK 버전 관리](https://tech.buzzvil.com/handbook/android-multimodule-sdk) Date: unknown ## 들어가며 앱이 모듈화되는 경우에는 각 모듈을 로컬 라이브러리로 참조하여 별도로 모듈의 버전 관리 없이 앱의 버전만 관리하여 APK 파일을 배포가 가능하다. 하지만 SDK의 경우 각각의 모듈에 대해서 버전을 모두 명시 후에 배포까지 다 해줘야 하는 이슈가 있다. 이 문서에서는 멀티 모듈 앱과 멀티 모듈 SDK의 차이에 대해서 간단히 설명하고 멀티 모듈 SDK에서는 왜 버전을 각각 관리해줘야 하는지 설명한다. ## 멀티 모듈 앱 ### 구조도 멀티 모듈 앱은 다음과 같은 구조로 되어 있다. 위와 같이 앱의 경우 빌드되는 결과물인 APK 파일 안에 앱에 필요한 모든 코드가 다 들어있다. 즉, 별도로 각 모듈을 배포할 필요가 없고 APK 파일 안에 모든 모듈 데이터가 포함된다. ### 참조 모듈과 버전 앱의 경우 APK에 모든 필요한 정보가 병합되어 빌드되기 때문에, 멀티 모듈로 구성되더라도 **앱 자체에 대해서만 버전 관리**를 하면 모듈 간의 충돌이 발생하지 않는다. ## 멀티 모듈 SDK ### 구조도 멀티 모듈 SDK는 다음과 같은 구조로 되어 있다. ![Multi module SDK structure](https://tech.buzzvil.com/handbook/aar-structure.png) 이처럼 SDK의 경우 빌드 결과물인 AAR 파일 내부에 자기 자신의 코드만 갖고 있고, 참조하는 모듈의 코드를 직접 갖고 있지 않다. 대신에 AAR은 [POM 파일](http://maven.apache.org/pom.html)을 포함하고 있으며 해당 파일 안에 참조하는 모듈의 이름, 버전 등을 명시한다. 이렇게 빌드된 AAR 파일은 다음에 앱을 빌드할 때 [그래들의 의존성 정책](https://docs.gradle.org/current/userguide/dependency_resolution.html)에 따라 최종적으로 어떤 모듈에 어떤 버전을 쓸 것인지 결정된다. 따라서 SDK의 경우 멀티 모듈로 구성 된다면 참조하고 있는 **각 모듈을 모두 버전에 맞게 배포를 해줘야** 앱의 빌드 시점에 해당 모듈을 찾아서 빌드가 가능하다. ### 참조 모듈과 버전 그래들 멀티 프로젝트를 구성하다 보면 모듈을 참조하는 방법은 보통 다음 2가지를 사용한다. ([참고 문서](https://developer.android.com/studio/build/dependencies)) #### 원격 바이너리 참조 ```gradle implementation 'com.buzzvil:my-module:1.2.3' // 저장소에서 com.buzzvil 그룹의 my-module 모듈 중 1.2.3 버전에 해당하는 바이너리를 찾아서 연동 ``` 원격 바이너리 참조의 경우 참조하는 모듈의 바이너리 파일을 설정한 저장소에서 찾아서 연동한다. 이때 해당 모듈의 그룹, 이름, 버전 정보를 정확히 기입을 해서 참조를 하므로 스크립트 상으로 어떤 모듈을 참조하고 있는지 명확히 알 수 있다. 단, 해당 모듈이 저장소에 배포된 상태여야 빌드가 가능하다. #### 로컬 라이브러리 모듈 참조 ```gradle implementation project(':my-module') // 프로젝트에서 my-module 모듈을 찾아서 연동 ``` 로컬 라이브러리 모듈 참조의 경우 참조하는 모듈을 프로젝트 로컬 라이브러리 모듈에서 찾아서 연동한다. 이때 해당 모듈의 세부 정보를 기입하지는 않는다. 그렇다면 이때 SDK 파일을 빌드하면 해당 모듈의 이름과 버전 정보는 어떻게 기록되는가? ![POM builder process](https://tech.buzzvil.com/handbook/pom-build-process.png) 로컬 라이브러리 참조 모듈이 포함된 SDK 파일을 빌드할 경우에는, 빌드 툴이 빌드 시점에 해당 모듈 프로젝트에서 직접 모듈에 대한 정보를 추출해서 POM 파일을 생성해준다. 따라서 평소에는 별도의 버전 정보를 참조하고 있지 않고, **SDK를 빌드하는 시점에 해당 모듈 프로젝트에 설정된 버전 정보를 바탕으로 AAR 파일을 생성한다.** ### 개발 주의 사항 멀티 모듈로 구성된 SDK를 배포할 때 주의해야 할 사항을 설명한다. #### 첫째, 배포 시점에 모든 참조 모듈을 함께 배포해야 한다. SDK의 결과물인 AAR 파일에는 참조하는 모듈의 코드가 포함되지 않는다. 따라서 SDK 배포 시에는 참조하고 있는 모든 모듈이 배포되어야 한다. #### 둘째, 배포 시점에 모든 모듈의 버전이 고유해야 한다. POM 파일에 참조하고 있는 모듈이 어떤 것인지 정해지는 것은 배포할 AAR 파일을 빌드하는 시점이다. 일반적으로는 [빈트레이](https://bintray.com/) 업로드를 할 때 SDK가 빌드되며 버전 정보가 정해진다. 즉, 이 시점에는 모든 참조 모듈들의 버전이 정확히 관리되어야 한다. 만약 1.0.0으로 미리 배포된 후 코드 수정이 있었지만 아직 모듈의 버전 정보가 1.0.0으로 되어 있다면 AAR 파일에는 해당 모듈이 1.0.0으로 기록될 것이고, SDK 사용자는 의도하지 않은 버전의 모듈을 참조하게 될 것이다. 각각의 모듈의 버전을 어떻게 관리할 것인지는 [모노리포 개발 가이드](/handbook/monorepo-workflow)를 참조한다. #### 셋째, 다른 SDK 간에 모듈 충돌이 일어날 수 있음을 인지한다. 만약 여러 SDK가 동일한 모듈을 참조하고 있고 해당 SDK들이 하나의 앱에서 빌드되고 있다면, 모듈간 충돌에 대해 고민해봐야 한다. ![Multi module using same module](https://tech.buzzvil.com/handbook/multi-sdk-using-same-module.png) 위와 같이 각각의 SDK가 같은 모듈의 서로 다른 버전을 참조하고 있으면 최종적으로 앱이 빌드되는 시점에 모듈의 어떤 버전을 사용할지 결정이 된다. 이 경우 한쪽의 SDK에서 다른 버전의 모듈을 사용하게 되기 때문에 충돌이 일어날 수 있다. 이를 잘 해결하기 위해서 기본적으로 개발자는 모듈의 하위호환에 대해서 인지하고 개발을 진행해야 하고, 연동자는 [그래들의 버전 해석 정책](https://docs.gradle.org/current/userguide/dependency_resolution.html)을 이용해서 어떤 버전을 사용할지 제어해야 한다. --- ## [안드로이드 테스트](https://tech.buzzvil.com/handbook/android-test) Date: unknown 이 문서는 안드로이드 테스트 방법과 가이드라인을 제공한다. 이는 안드로이드 테스트 추천 라이브러리와 활용 방법에 대한 내용을 포함한다. 단 아래 내용은 추천 사항일 뿐이며, 각 프로젝트에 따라서 언제든 자유롭게 다른 방법을 적용하거나 제안할 수 있다. 다만 이 경우에도 프로젝트 내에서는 통일성을 갖는 것을 장려한다. ## 테스트 프레임워크 및 라이브러리 안드로이드 테스트를 위해 아래 라이브러리들을 활용한다. 추가 라이브러리 활용은 언제든지 가능하나, 스태틱(static) 함수 모킹 등 테스트 가이드라인을 벗어나는 기능을 활용하기 위해 더 강력한 라이브러리(e.g PowerMock)를 사용하는 것은 지양한다. ### JUnit 4 [https://junit.org/junit4/](https://junit.org/junit4/) 안드로이드 테스트를 위한 프레임워크이다. [JUnit 5](https://junit.org/junit5/)가 출시되어 있지만 안드로이드 환경에서 사용하려면 [추가적인 작업](https://www.lordcodes.com/posts/testing-on-android-using-junit-5)이 필요하여 아직 기본적으로 제공되는 [JUnit 4](https://junit.org/junit4/)를 사용하고 있다. ### Mockito 2 [http://site.mockito.org/](http://site.mockito.org/) 모킹을 위한 라이브러리이다. 오브젝트를 모킹하여 테스트 오브젝트가 의존하고 있는 오브젝트를 모킹하여 테스트 환경을 구성할 수 있다. 스태틱 함수나 파이널 클래스(final class) 등은 모킹할 수 없다. 이를 가능하게 하는 파워목(PowerMock) 라이브러리가 있지만, 일반적으로 스태틱 함수의 모킹이 반드시 필요한 경우 코드 구성이 잘못되어 있거나 테스트 방법이 잘못되었을 가능성이 높기에 파워목 사용을 지양한다. ### Robolectric [http://robolectric.org/](http://robolectric.org/) 안드로이드 프레임워크의 동작을 시뮬레이션하여 안드로이드 액티비티나 뷰 등의 동작이 필요한 테스트를 안드로이드 에뮬레이터나 실제 디바이스 없이 수행 가능하게 도와준다. 유닛 테스트 시 안드로이드 프레임워크를 활용해야만 하는 경우 유용하다. Robolectric 4 이 출시되었지만 아래 문서는 Robolectric 3 기준으로 작성되어 있다. ## 유닛 테스트 유닛 테스트 기본 가이드라인은 [테스트 기본 원칙](/handbook/test-principles#%EC%9C%A0%EB%8B%9B-%ED%85%8C%EC%8A%A4%ED%8A%B8) 문서를 참고한다. ### 실행 방법 #### `gradle` 콘솔 `gradlew` 를 사용하여 테스트한다. 이 때 테스트 메소드를 지정함으로써 원하는 테스트만 수행할 수 있다. ```bash # 특정 테스트 실행 ./gradlew test --tests com.buzzvil.locker.SomeTest.someSpecificFeature # 특정 패키지 테스트 실행 ./gradlew test --tests all.in.specific.package* ``` #### 안드로이드 스튜디오 실행하고자 하는 테스트 함수 혹은 클래스에 커서를 둔 뒤 `컨트롤 + 시프트 + D` 키 조합을 사용해 테스트를 수행할 수 있다. 또한 테스트 메소드 좌측에 보이는 아이콘을 눌러서 테스트를 수행할 수 있다. ![icon-test](https://tech.buzzvil.com/img/icon-test.png#icon-small) ### 테스트 코드 구성 #### 네이밍 가이드라인 네이밍은 [Javatests style rules](https://source.android.com/setup/contribute/code-style#javatests-style-rules) 를 따른다. 테스트 대상 메소드 이름 앞에 test 를 붙여서 카멜 표기법으로 표현한다. 테스트 상황에 대한 표현은 언더바(`_`)를 사용해 구분한 뒤 카멜 표기법을 활용해 표현한다. ```java @Test public void testIsCampaignFilteredOut_noFilter() { ... } ``` #### 임포트(import) 가이드라인 테스팅 메소드는 스태틱 임포트(static import)를 사용해 임포트한다. ```java // 임포트 이후 `Mockito.mock` 대신 `mock` 을 바로 사용한다. import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; ``` #### 디펜던시 모킹(mocking) 가이드라인 디펜던시 인젝션(Depedency Injection)을 사용해서 디펜던시를 모킹한다. ```java class CampaignFilter { CampaignFilter() { this(PreferenceHelper.getString(PrefKey.FILTERED_AD, "").split(DELIMITER), PreferenceHelper.getString(PrefKey.FILTERED_CONTENT, "").split(DELIMITER)); } CampaignFilter(final String[] filteredAds, final String[] filteredContent) { this.filteredAds = filteredAds; this.filteredContent = filteredContent; } // ... } ``` ```java class CampaignFilterTest { @Test public void testIsCampaignFilteredOut_noFilter() { // 필터가 없는 경우를 테스트한다. final CampaignFilter campaignFilter = new CampaignFilter(new String[]{}, new String[]{}); ... } } ``` #### 안드로이드 프레임워크 모킹 ##### 액티비티 생애 주기(Activity lifecycle) [로보렉트릭(Robolectric)](http://robolectric.org/activity-lifecycle/)을 사용해서 액티비티 생애 주기를 테스트한다. ```java @RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class) public class VideoOverlayActivityTest { @Test public void testOnCreate() { final VideoOverlayActivity activity = Robolectric.buildActivity( VideoOverlayActivity.class, getDefaultIntent()).create().get(); assertThat(activity.isFinishing(), is(false)); } } ``` #### 쉐도잉(Shadowing) 스태틱 함수를 모킹하는 것은 가능한 지양한다. 싱글톤의 활용 역시 클래스 안에서 직접 생성해서 사용하기 보다는 디펜던시 인젝션을 사용해서 디펜던시를 가져야 한다. 다만 기존의 코드로 인해 불가피할 경우 로보레트릭의 쉐도우를 사용하여 구현한다. ```java @Implements(SomeSingleton.class) public class ShadowSomeSingleton { static SomeSingleton someSingleton; @Implementation public static SomeSingleton getInstance() { if (someSingleton == null) { someSingleton = mock(SomeSingleton.class); } return someSingleton; } } ``` ```java @RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, shadows = ShadowSomeSingleton.class) public class SomeActivityTest { // ... } ``` ## References - [twitter-kit-android/CONTRIBUTING.md at master · twitter/twitter-kit-android · GitHub](https://github.com/twitter/twitter-kit-android/blob/master/CONTRIBUTING.md) - [AOSP Java Code Style for Contributors | Android Open Source Project](https://source.android.com/source/code-style.html) - [GitHub - stripe/stripe-android: Stripe Android SDK](https://github.com/stripe/stripe-android) --- ## [API 정의](https://tech.buzzvil.com/handbook/api-definition) Date: unknown 이 문서는 버즈빌의 API(애플리케이션 프로그래밍 인터페이스, Application Programming Interface)를 정의하고 사용하는 방법을 기술한다. 인터페이스는 [Protocol Buffers](https://developers.google.com/protocol-buffers/)를 통해 정의한다. ## 가이드라인 ### API 우선 방식 (API-First Approach) 서비스 구성 시 API 우선 방식을 사용한다. API 우선 방식에서는 서버 혹은 클라이언트 서비스를 개발하기에 앞서 API 를 먼저 구성한다. 이를 통해 각 팀은 병렬적으로 일을 진행할 수 있고, 개발 속도가 빨라지며, API를 더 안정적으로 제공할 수 있다. 자세한 내용은 [Understanding the API-First Approach to Building Products](https://swagger.io/resources/articles/adopting-an-api-first-approach/) 문서를 참고한다. ## API 정의 ### API 레포지토리 [buzzapis](https://github.com/Buzzvil/buzzapis) 레포지토리에서 API를 정의한다. 해당 레포지토리에는 버즈빌 내부 서비스의 API 정의가 모두 포함되어 있다. 프로젝트 구성 및 사용 방법은 프로젝트에 포함되어 있는 [README](https://github.com/Buzzvil/buzzapis) 파일을 참고한다. ### API 정의 방법 [Protocol Buffers](https://developers.google.com/protocol-buffers/)를 통해 API를 정의한다. 자세한 문법은 [Protocol Buffers](https://developers.google.com/protocol-buffers/)를 참고한다. > **Note.** 버즈빌에서는 [proto3](https://developers.google.com/protocol-buffers/docs/proto3) > 문법을 사용한다. #### API 네이밍 컨벤션 API 네이밍 구성 시 [Google Cloud API 네이밍 컨벤션](https://cloud.google.com/apis/design/naming_convention) 을 참고한다. ##### 유의 사항 - 패키지 이름은 버젼명으로 끝난다. e.g `package buzzvil.library.v1;` - 메소드 이름은 `VerbNoun` 형태를 따른다. #### API 정의 예시 ```protobuf syntax = "proto3"; package buzzvil.library.v1; service Library { rpc GetBook(GetBookRequest) returns (Book) {} } message GetBookRequest { string id = 1; } message Book { string title = 1; string author = 2; } ``` ### HTTP/JSON API 지원 Protocol Buffers를 통해 gRPC 외에도 HTTP/JSON API를 지원할 수 있다. [Googleapis](https://github.com/googleapis/googleapis)를 활용하여 [gRPC Gateway](https://github.com/grpc-ecosystem/grpc-gateway) 혹은 [Envoy](https://github.com/envoyproxy/envoy) 같은 게이트웨이에서 HTTP 요청을 gRPC 요청으로 변환 가능하다. 자세한 내용은 [Transcoding HTTP/JSON to gRPC](https://cloud.google.com/endpoints/docs/grpc/transcoding) 문서를 참고한다. #### HTTP API 정의 `google/api/annotations.proto`를 통해 HTTP API를 정의할 수 있다. `rpc` 정의에 다음 예시와 같은 `option`을 정의한다. 자세한 문법 및 사용 방법은 [Transcoding HTTP/JSON to gRPC](https://cloud.google.com/endpoints/docs/grpc/transcoding) 문서와 [google/api/http.proto](https://github.com/googleapis/googleapis/blob/master/google/api/http.proto#L45) 파일을 참고한다. ```protobuf syntax = "proto3"; import "google/api/annotations.proto"; package buzzvil.library.v1; service Library { rpc GetBook(GetBookRequest) returns (Book) { option (google.api.http) = { get: "/v1/books/{id}" }; } } ``` ## API 배포 정의된 API 는 [Protocol Buffers](https://developers.google.com/protocol-buffers/)와 [Swagger](https://swagger.io/) 등을 통해 각 언어의 라이브러리로 빌드된다. 빌드된 라이브러리는 각 언어별 패키지 관리 시스템(Package management system)으로 배포된다. ### 패키지 정보 각 API 의 패키지 정보는 해당 API 폴더의 `package.json` 파일로 관리된다. `name` 과 `version` 필드는 필수 사항이며, 그 외 추가 정보들이 담길 수 있다. `version` 정보는 최초 생성 시점 외에는 직접 수정하지 않는다. 해당 정보는 `master` 브랜치 병합 시점에 풀 리퀘스트(Pull Request) 정보를 바탕으로 자동으로 설정된다. #### `package.json` 예시 ```json { "name": "book", "version": "0.1.0", "description": "Book service provides library data." } ``` ### 패키지 관리 시스템 #### Go Go API 라이브러리는 [github.com](http://github.com)을 패키지 관리 시스템으로 사용한다. 빌드 시점에 [buzzapis](https://github.com/Buzzvil/buzzapis) 레포지토리 내부에 빌드된 Go API 파일이 첨부된다. 자세한 사용법은 [Go 프로젝트](/handbook/go-project) 문서를 참고한다. #### Python Python API 라이브러리는 버즈빌 내부에서 사용하는 PyPI를 통해 관리한다. 자세한 사용법은 [Python 프로젝트](/handbook/python-project) 문서를 참고한다. #### Java Java API 라이브러리는 [Bintray](https://bintray.com/)를 통해 관리한다. Java 라이브러리는 gRPC 와 HTTP 라이브러리로 각각 빌드되어 배포된다. 자세한 사용법은 [Java 프로젝트](/handbook/java-project) 문서를 참고한다. --- ## [버즈빌 아키텍처](https://tech.buzzvil.com/handbook/buzzvil-architecture) Date: unknown 이 웹사이트는 버즈빌 소프트웨어 아키텍처 전반에 대한 내용과 개발을 위해 필요한 문서들을 모아두는 공간이다. ## 들어가며 광고 생태계는 많은 이해관계자로 인해 복잡한 구성을 가지고 있다. 애드테크 기업들은 이러한 광고 업계의 많은 부분을 자동화하고 최적화 함으로써 더욱 효과적이고 효율적인 광고 생태계를 만들어가고 있다. 허나 비즈니스 도메인의 복잡성으로 인해 시스템 역시 태생적인 복잡성을 지니고 있다. 기술이 발전함에 따라서 애드테크 분야 안에서도 더욱 전문성을 지닌 분야들이 늘어나고 있고, 이에 따라 시스템의 복잡성 또한 증가하고 있다. 버즈빌은 글로벌 애드테크 기업으로서 복잡한 광고 기술 스택을 효율적이고 유연하게 구성하고자 지속적인 노력을 하고 있다. 버즈빌 시스템은 마이크로서비스 아키텍처, 멀티 모듈, 모노리포 등을 통해 시스템과 서비스가 비즈니스의 변화에 유연하게 대응할 수 있다. 또한 클린 아키텍처, 도메인 주도 설계 등을 통해 서비스 혹은 모듈 사이의 의존성을 줄이고 관련성이 깊은 것들의 응집성을 높임으로써 각 분야의 기술력을 높이는 동시에 기능적으로 다양한 시도를 할 수 있는 구조를 가지고 있다. ### 버즈빌 개발 익히기 아래 항목은 버즈빌 아키텍처에서 서비스를 개발, 테스트, 배포 및 운영하기 위해 필요한 기본적인 안내 문서이다. 개발을 시작하기에 앞서 순서대로 읽는 것을 추천한다. 1. [API 정의](/handbook/api-definition) 2. [프로젝트 생성](/handbook/new-project) 3. [소프트웨어 아키텍처](/handbook/repository) 4. [개발 가이드](/handbook/go-project) 5. [테스팅 가이드](/handbook/test-principles) 6. [서비스 배포](/handbook/deployment) 7. TODO: [서비스 모니터링 및 문제 해결]() --- ## [기여하기](https://tech.buzzvil.com/handbook/contributing) Date: unknown ## 기여하기 ## 문서 작성 원칙 --- ## [서비스 배포](https://tech.buzzvil.com/handbook/deployment) Date: unknown 본 문서에서는 버즈빌에서 백엔드 서비스를 배포하기 위해 준비해야하는 내용들에 대한 정보를 제공한다. ## 가이드라인 버즈빌에서 쿠버네티스에 서비스를 배포하는 과정은 아래와 같다. ![CI CD 파이프라인 개요](https://tech.buzzvil.com/handbook/ci_cd_process.svg) 프로젝트에 따라 상이할 수 있지만 일반적인 백엔드 워크로드 배포 파이프라인을 꾸리기 위해 아래와 같은 준비가 필요하다. * Docker 이미지 생성 * Helm chart 작성 * Spinnaker pipeline 구성 ## Docker 이미지 생성 `Dockerfile` 작성 및 도커 컨테이너 이미지 빌드에 대한 전반적인 내용은 [도커](/handbook/docker) 문서를 참고한다. ## 헬름 차트(Helm Chart) 작성 각 서비스의 배포는 쿠버네티스 패키지 매니저인 헬름(Helm)을 이용해 동일한 구성을 여러 환경에 손쉽게 배포할 수 있도록 한다. 자세한 내용은 [헬름](/handbook/helm) 문서를 참고한다. ## Spinnaker pipeline 구성 TBD --- ## [도커(Docker)](https://tech.buzzvil.com/handbook/docker) Date: unknown ## 도커(Docker) 이 문서는 도커에 대한 간단한 설명과 각 프로젝트 팀에서 컨테이너 오케스트레이션 환경에 최적화된 이미지를 만드는 방법에 대한 가이드라인을 제공한다. ### 도커란 컨테이너 기술은 마이크로서비스 아키텍쳐와 밀접한 관계가 있다. 컨테이너 이미지는 일반적으로 하나의 프로세스를 격리된 환경에서 실행하는 것으로, 동일한 컨테이너를 여러대 구동함으로써 프로세스의 수를 손쉽게 횡적으로 확장가능하다는 장점이 있다. 도커는 컨테이너 기반 기술 중 가장 널리 채택되어 활용되고 있는 구현체 중 하나로 이하에서 다루는 컨테이너는 도커 컨테이너를 의미한다. 컨테이너 모델에서는 컨테이너 이미지를 하나의 프로세스 개념으로 보기 때문에, Dockerfile의 `ENTRYPOINT`에서 실행하는 프로세스가 컨테이너의 수명 주기를 결정한다. 쿠버네티스같은 컨테이너 관리(orchestration) 환경에서는 프로세스의 종료 코드나 livenessProbe, readinessProbe 등의 각종 애플리케이션 상태 체크 방법을 활용해 애플리케이션의 정상 종료 여부를 판단하고 재시작을 하거나 종료처리 등을 해준다. 컨테이너 오케스트레이션과 관련된 내용은 별도 문서로 다룰 예정이다. ### 도커의 기반 기술 https://stackoverflow.com/questions/16047306/how-is-docker-different-from-a-virtual-machine ### 이미지 레이어 도커 이미지를 빌드하면 Dockerfile의 매 커맨드마다 실행된 결과(delta)가 레이어로 추가되며, 매 레이어에는 고유한 식별자가 부여된다. 이 레이어는 동일한 문맥(context) 내에서는 동일한 결과를 만들어내기 때문에 캐싱이 용이하다. 이 이유로 인해 변경이 잦은 커맨드는 가능한 뒤에 실행하는 것이 좋다. ### Dockerfile Dockerfile은 도커 컨테이너 이미지를 생성하는 가장 대표적이고 보편적인 방법이다. 자세한 내용은 [Dockerfile 레퍼런스](https://docs.docker.com/engine/reference/builder/)를 참조하도록 한다. ### .dockerignore `docker build` 커맨드를 실행할 때 `.dockerignore`에 명시된 패턴의 파일들을 도커 데몬에 문맥으로 전달하지 않도록 하는데 사용된다. `.gitignore` 파일과 비슷하게 동작한다. 예를 들어 환경마다 그 결과가 달라지는 `node_modules` 디렉토리나 실제 애플리케이션에서 사용되지 않는 코드(배포용 스크립트나 설정파일 등)나 리소스 등은 가능하면 제외하는 것을 추천한다. 심지어는 `Dockerfile`이나 `.git` 디렉토리 등도 제외하는 경우도 꽤 있다. `vendor` 디렉토리에 디펜던시를 모두 포함하는 go 프로젝트는 굳이 제외하지 않는 것이 좋다. ### 네트워킹 TBD ### 볼륨 TBD ## 배포용 도커이미지 만들기 ### 프로세스 앞서 언급한 것처럼 도커 컨테이너는 프로세스에 대응된다고 생각하면 되는데, `ENTRYPOINT`로 컨테이너의 수명주기를 결정하는 프로세스를 정의한다. 하나의 컨테이너 안에서 여러개의 프로세스를 띄우고 supervisor 등을 이용해 다중 프로세스를 운영하는 것도 가능하지만, 일반적인 케이스는 아니며 컨테이너 오케스트레이션의 이점을 누리기 힘들어지는 문제가 있다. 다만 실행 환경에서 얼마든지 `ENTRYTPOINT`는 재정의할 수 있다(e.g. 쿠버네티스의 command). ### 환경변수 TBD ### 이미지 사이즈 최적화 도커 이미지 크기가 커지면 이미지 빌드 후 레지스트리에 푸시할 때나, 배포 환경에서 이미지를 가져올 때 시간이 오래 소요되며, 레지스트리 저장 공간에 따른 비용 및 데이터 전송 트래픽에 대한 비용이 증가한다. 따라서 배포에 사용되는 이미지는 가능한 크기를 줄이는 것을 권장한다. #### alpine, slim과 같은 가벼운 기본(base) 이미지 사용하기 알파인 리눅스는 초경량 리눅스 배포판으로, 이미지 사이즈가 5MB 내외다. 알파인을 기반으로 한 파이썬 이미지는 29MB 내외로, 데비안을 기반으로 한 기본 파이썬 이미지가 356MB인 것과 비교하면 10배 이상 차이가 난다. ### 레이어 캐싱 활용도를 높이는 방법 ```docker COPY SOME_FILE . RUN chown app:app SOME_FILE ``` 위와 같이 파일을 추가하고 다음 레이어에서 권한을 변경하면 추가된 파일 크기만큼의 레이어가 생기고, 권한을 변경한 스텝에서 또 파일 크기만큼의 레이어가 추가된다. ```docker COPY --chown app:app SOME_FILE ``` 대신 위와같이 `COPY` 커맨드에 권한 설정을 한번에 하는 것과 같은 최적화를 할 수 있다. 여러 라인의 `RUN` 스텝이 있다면 가능하면 합쳐서 하나의 레이어로 만드는 것이 좋다. `docker history IMAGE` 커맨드를 이용해 빌드한 이미지의 레이어별 사이즈를 체크할 수 있다. ### 멀티스테이지 빌드를 활용한 캐싱 [멀티스테이지 빌드](https://docs.docker.com/develop/develop-images/multistage-build/)를 활용하면 빌드에만 사용되는 라이브러리는 최종 이미지에 포함하지 않고 빌드 결과만 활용할 수 있다. gradle을 포함한 빌드용 이미지에서 jar 파일을 빌드한 후 jdk 컨테이너에 복사해서 사용하는 등의 예시가 있다. ### 언어별 Dockerfile 작성 요령 #### golang go는 빌드된 바이너리만 있으면 소스코드나 의존성이 있는 패키지가 없이 단독으로 배포가 가능하다. 때문에 `golang` 이미지를 기반으로 해 바이너리를 빌드한 후, 추가적인 레이어를 생성하지 않는(no-op) [scratch](https://hub.docker.com/_/scratch) 이미지에 바이너리만 복사해 배포할 수 있다. 때문에 빌드 시스템이나 배포환경에서는 바이너리 크기에 준하는 가벼운 이미지를 만들고 사용할 수 있다. ```docker FROM golang:1.1-alpine as builder RUN apk update && apk add --no-cache ca-certificates && update-ca-certificates RUN adduser -D -g '' app WORKDIR $GOPATH/src/path/to/package COPY . . RUN go build -o /go/bin/binary_name FROM scratch COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /etc/passwd /etc/passwd COPY --from=builder /go/bin/binary_name /go/bin/binary_name USER app ENTRYPOINT ["/go/bin/binary_name"] ``` #### python 파이썬도 알파인 이미지를 쓰는 것을 권장한다. 다만 현재 데비안 기반 컨테이너에서 이미 운영 중인 코드베이스는 알파인 패키지와의 호환성 문제로 slim 이미지를 사용하고 있다. `pip install`에 소요되는 시간과 그 결과로 추가되는 레이어의 크기가 큰 편으로, 패키지 설치하는 부분을 가능한 캐싱이 가능하도록 코드를 제외한 requirements 파일만 먼저 추가해 패키지를 설치한 이후 나머지 코드를 추가하는 방식으로 이미지를 빌드하는 것을 권장한다. ```docker FROM python:3.7.2-alpine RUN apk --no-cache add build-base libffi-dev openssl-dev ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 PYTHONUNBUFFERED=1 WORKDIR /app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . ./ ``` #### nodejs ```docker FROM node:10.12-alpine WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . EXPOSE 8080 CMD ["npm", "start"] ``` #### 정적 웹사이트 정적 웹사이트는 배포환경에서는 빌드에 필요한 nodejs 환경이나 node_modules이 전혀 필요가 없다. 멀티스테이지 빌드를 이용해 웹사이트를 빌드한 후 결과물만 alpine nginx 베이스 이미지(9MB)에 추가해 정적 컨텐츠를 서빙할 수 있다. ```docker FROM node:8-alpine AS builder WORKDIR /app COPY package*.json /app/ RUN npm install COPY . /app RUN npm run build FROM nginx:1.15-alpine COPY --from=builder /app/build /usr/share/nginx/html ``` https://docs.docker.com/develop/develop-images/multistage-build/ ## 컨테이너 보안 관련 고려사항 ### 사용자(user) 권한 축소하기 일반적으로 리눅스에서 서비스(nginx, postgres 등)를 구동할 때 서비스 구동에 필수적인 권한만 부여된 사용자를 생성하는 것과 마찬가지로, 구동되는 애플리케이션이 필요 이상의 권한을 획득할 수 없도록 내부에서 서비스를 위한 유저를 생성하는 것을 권장한다. ### 가능한 경량한 기본 이미지 사용하기 각종 툴이나 라이브러리가 설치돼 있는 이미지는 컨테이너에서 실행권한을 획득했을 때 할 수 있는 행동의 선택지가 높아진다. 애플리케이션 구동에 필수적인 라이브러리만 포함한다. 나중에 디버깅을 위해 curl이나 기능이 풍부한 bash, 각종 네트워크 툴, 심지어는 vim 등을 설치하고 싶을 수 있지만, 애플리케이션 구동에 필수적인 라이브러리가 아닌 이상 제외하는 것이 좋다. 운영계에서는 프로파일러나 APM, 로그 수집 시스템, 분산 트레이싱 등을 최대한 활용해야 한다. ### 퍼블릭 이미지 사용에 유의 Docker hub 등의 퍼블릭 영역에 있는 이미지들은 각 프로젝트에서 공식적으로 관리되는 이미지 이외의 이미지는 운영계에서 사용하는 것을 삼가야 한다. 아직 base image들은 별도로 관리하지 않고 있으나, 향후 기본 이미지들도 직접 관리하는 것을 고려 중이다. ### GCR 취약점 스캔 GCR에 푸시된 데비안, 우분투, 알파인 베이스 이미지에 대한 자동적 취약점 스캔이 이뤄지고 주로 취약점이 있는 시스템 패키지에 대한 알려진 이슈를 자동으로 검증해 준다. 애플리케이션에서 사용하는 패키지나 라이브러리 등에 대한 취약점 스캔은 GitHub에서 제공하는 [security alert](https://github.blog/2017-11-16-introducing-security-alerts-on-github/) 서비스를 이용할 수 있다. ### 구글 클라우드 빌드 구글의 클라우드 빌드(Cloud Build)는 컨테이너 이미지 빌드를 수행할 수 있는 CI 중 간편하고 빠르며, 저장소에 대한 보안 스캔 등의 부가기능들이 제공되어 지속적으로 컨테이너 이미지를 빌드하는데 사용하고 있다. 기본적으로 저장소에 Dockerfile만 포함돼 있다면 간단한 트리거 설정만으로도 코드 저장소와 연동하여 커밋이나 태그 푸시 등을 트리거로 해 컨테이너 이미지를 빌드하고 레지스트리(GCR)에 업로드할 수 있다. 빌드 속도를 높이기 위해서 시스템 패키지나 pip, node module과 같은 패키지들을 설치하는 과정을 별도의 스테이지로 분리해 새로운 이미지를 빌드할 때 `--cache-from` 플래그를 활용하여 이전 빌드에서 빌드한 베이스 이미지를 캐시로 활용할 수 있다. 해당 기능을 이용하기 위해서는 빌드 스텝을 YAML 파일(cloudbuild.yaml)로 정의할 수 있다. ([참고](https://cloud.google.com/cloud-build/docs/build-config)) ```yaml steps: - name: 'gcr.io/cloud-builders/docker' entrypoint: 'bash' args: - '-c' - | docker pull asia.gcr.io/$PROJECT_ID/${_SVC_BASENAME}/base || true - name: 'gcr.io/cloud-builders/docker' entrypoint: 'bash' args: - '-c' - | docker build \ --cache-from asia.gcr.io/$PROJECT_ID/${_SVC_BASENAME}/base \ --tag asia.gcr.io/$PROJECT_ID/${_SVC_BASENAME}/base \ -f Dockerfile.prod \ --target base \ . docker build \ --cache-from asia.gcr.io/$PROJECT_ID/${_SVC_BASENAME}/base \ --tag asia.gcr.io/$PROJECT_ID/${_SVC_BASENAME}:$TAG_NAME-$SHORT_SHA \ -f Dockerfile.prod \ . images: - 'asia.gcr.io/$PROJECT_ID/${_SVC_BASENAME}/base' - 'asia.gcr.io/$PROJECT_ID/${_SVC_BASENAME}:$TAG_NAME-$SHORT_SHA' timeout: '1200s' ``` ### 도커 이미지 태깅 도커 이미지를 빌드할 때 별도로 태깅을 하지 않으면 기본적으로 `latest`라는 태그가 부여된다. 때문에 `latest`라는 태그를 사용하면 항상 최신의 이미지를 사용할 것으로 생각하기 쉽지만, 동일한 이미지명을 다른 내용을 담고있는 컨테이너 이미지 태그에 재사용할 경우 컨테이너 오케스트레이션이나 배포 관리가 어려워지는 측면이 있다. 때문에 특정 릴리즈 상태를 나타내는 정적인 불변의 태그명(커밋 sha, 브랜치명, 태그명 등)을 매 릴리즈마다 부여하는 것을 추천한다. ## 도커 레지스트리 ### 퍼블릭/프라이빗 레지스트리 ### ECR, GCR ### 이미지 정리 TBD ## 도커를 이용한 로컬 개발환경 ### docker-compose TBD ### skaffold TBD --- ## [Go 프로젝트](https://tech.buzzvil.com/handbook/go-project) Date: unknown ## 프로젝트 시작하기 우선 [프로젝트 시작하기](/handbook/new-project) 문서를 따라 새로운 프로젝트를 생성한다. ### 디펜던시 불러오기 [dep](https://github.com/golang/dep)을 사용해서 필요한 디펜던시를 초기화한다. ```bash dep init ``` 이후 디펜던시 변경이 있을 경우 `dep ensure`를 통해서 디펜던시를 업데이트 할 수 있다. ## 프로젝트 레이아웃(Project Layout) Go 프로젝트 레이아웃은 기본적으로 [golang-standards/project-layout](https://github.com/golang-standards/project-layout) 레포지토리의 정의를 참고한다. | 디렉토리 | 설명 | | - | - | | `/cmd` | 프로젝트의 주요 어플리케이션을 포함한다. `/cmd` 안의 디렉토리 명은 실행할 파일 이름과 동일하게 구성한다. 서버를 실행시키거나 데이터베이스 연결을 맺는 등의 역할을 수행한다. | | `/internal` | 프로젝트 외부에 공개되지 않는 라이브러리 코드는 이 곳에 위치한다. 서비스 프로젝트의 경우 대부분의 코드가 이 안에 위치한다. | | `/internal/app` | 실제 어플리케이션 코드를 포함한다. 프레임워크를 포함하는 프레젠테이션 영역은 보통 이 디렉토리 안에 포함된다. | | `/internal/pkg` | 앱 사이에 공유되는 라이브러리 코드를 포함한다. 도메인 영역과 데이터 영역은 보통 이 디렉토리 안에 포함된다. | | `/pkg` | 외부로 공개하는 라이브러리 코드를 포함한다. | | `/vendor` | 프로젝트가 사용하는 외부 라이브러리를 포함한다. 일반적으로 `dep` 을 사용해 관리된다. | ## API 구현 [API 정의](/handbook/api-definition) 문서를 통해 정의된 API의 기능을 구현한다. ### 라이브러리 적용 API 정의를 통해 빌드된 Go 라이브러리는 [buzzapis](https://github.com/Buzzvil/buzzapis) 레포지토리의 go 폴더 안에 생성된다. [Dep](https://github.com/golang/dep) 등의 의존성 도구를 통해서 필요한 API 라이브러리를 가져올 수 있다. 1. API 라이브러리를 임포트(import)한다. ```go // e.g main.go import "github.com/Buzzvil/buzzapis/go/library" ``` 2. `Dep`을 통해 라이브러리를 가져온다. ```bash $ dep ensure ``` ### 인터페이스 구현 라이브러리에 정의된 API를 구현함으로써 클라이언트에게 서비스를 제공할 수 있다. ```go package librarysrv import ( pb "github.com/Buzzvil/buzzapis/go/library" ) type server struct { } // New creates library service server. func New(db *sql.DB) pb.LibraryServiceServer { return &server{} } func (s *server) GetBook(c context.Context, r *pb.GetBookRequest) (*pb.Book, error) { // TODO: Find the book from database. return &pb.Book{Title: "Title here.", Author: "Author here."}, nil } ``` ## 서버 구성 ### 서버 생성 및 실행 gRPC를 통해 서버를 생성하고 실행할 수 있다. 이때 `main.go` 에서는 연결을 맺거나 서버를 실행하는 역할에만 집중하고 실제 구현은 내부 패키지에게 위임한다. 자세한 사용법은 [Starting the go server](https://grpc.io/docs/tutorials/basic/go.html#starting-the-server) 문서를 참고한다. ```go // cmd/librarysvc/main.go package librarysvc import ( "log" "net" "os" pb "github.com/Buzzvil/buzzapis/go/library" "github.com/Buzzvil/librarysvc/internal/app/librarysrv" "google.golang.org/grpc" ) func main() { lis, err := net.Listen("tcp", fmt.Sprintf(":%s", os.Getenv("PORT"))) if err != nil { log.Fatalf("Failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterLibraryServiceServer(s, librarysrv.New()) if err := s.Serve(lis); err != nil { log.Fatalf("Failed to serve: %v", err) } } ``` ### 서버 로깅 [logrus](https://github.com/sirupsen/logrus) 등의 라이브러리를 통해 로깅 관련 옵션을 설정할 수 있다. 자세한 사용법은 [logrus](https://github.com/sirupsen/logrus) 레포지토리를 확인한다. ```go // cmd/librarysvc/main.go package librarysvc import ( "github.com/grpc-ecosystem/go-grpc-middleware" "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" "github.com/grpc-ecosystem/go-grpc-middleware/tags" "github.com/sirupsen/logrus" ) func newGrpcServer() *grpc.Server { logrusEntry := logrus.NewEntry(logrus.StandardLogger()) opts := []grpc_logrus.Option{} grpc_logrus.ReplaceGrpcLogger(logrusEntry) return grpc.NewServer( grpc_middleware.WithUnaryServerChain( grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)), grpc_logrus.UnaryServerInterceptor(logrusEntry, opts...), ), grpc_middleware.WithStreamServerChain( grpc_ctxtags.StreamServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)), grpc_logrus.StreamServerInterceptor(logrusEntry, opts...), ), ) } ``` --- ## [Go 테스트](https://tech.buzzvil.com/handbook/go-test) Date: unknown 이 문서는 Go 테스트 방법과 가이드라인을 제공한다. 이는 Go 테스트 추천 라이브러리와 활용 방법에 대한 내용을 포함한다. 단 아래 내용은 추천 사항일 뿐이며, 각 프로젝트에 따라서 언제든 자유롭게 다른 방법을 적용하거나 제안할 수 있다. 다만 이 경우에도 프로젝트 내에서는 통일성을 갖는 것을 장려한다. ## 테스트 라이브러리 ### Testify [github.com/stretchr/testify](https://github.com/stretchr/testify) - TestSuite 을 사용하기 위해 - 모킹을 활용하기 위해 ### Faker [github.com/bxcodec/faker](github.com/bxcodec/faker) 테스트에 사용하는 객체를 정의할 때 모든 값을 일정하게 대입하거나 값을 넣지 않으면 다양한 케이스를 처리할 수 없으므로 일부 객체의 멤버값들이 임의의 값을 가질수 있도록 할 수 있다. ### Go SQLMock [gopkg.in/DATA-DOG/go-sqlmock.v2](https://gopkg.in/DATA-DOG/go-sqlmock.v2) - 데이터베이스에 대한 의존성 없이 실제 쿼리를 돌리지 않고 쿼리 그 자체에 대해 검증 할 수 있다. ## 가이드라인 ### 테스트 파일 및 패키지 이름 - 파일 이름은 테스트 대상이 되는 go 파일의 이름과 맞춘다. - 테스트 대상 패키지에 _test 를 붙여서 테스트를 작성한다. (Blackbox 테스트) - 패키지 내부 구현이 복잡할 경우 대상 패키지와 같은 이름을 사용할 수 있다. (Whitebox 테스트) | | 대상 | 테스트 | | ---- | ---- | ---- | | 파일 | controller.go | controller_test.go | | 패키지 | package articlesvc | package articlsvc_test (package articlesvc) | | 함수 | GetArticles() | Test_GetArticles() | ### 모킹 - 참조된 객체는 모킹하여 테스트한다. ## 유닛 테스트 작성 ### 테스트 스위트(TestSuite) 정의 테스트 스위트(TestSuite)를 활용해서 각 유닛 테스트 초기화 및 테스트 정리를 수행한다. `SetupTest` 함수를 통해 테스트 대상과 기타 테스트마다 초기화 되어야 하는 변수들의 초기화를 수행할 수 있고, `TearDownTest` 함수를 통해 테스트 이후 테스트로 변경된 사항을 정리할 수 있다. 자세한 내용은 [Suite Package](https://github.com/stretchr/testify#suite-package) 문서를 참고한다. ```go type ControllerTestSuite struct { suite.Suite controller articlesvc.Controller engine *core.Engine usecase *mocks.Usecase } func (ts *ControllerTestSuite) SetupTest() { ts.engine = echo.New() ts.usecase = new(mocks.Usecase) ts.controller = articlesvc.NewController(ts.engine, ts.usecase) } // Write test definition with TestSuite. func TestControllerSuite(t *testing.T) { suite.Run(t, new(ControllerTestSuite)) } ``` ### 모킹 객체를 준비하기 모킹 객체는 mock.Mock 을 활용해서 작성한다. 아래의 예시는 유스케이스 목을 작성하는 예제이다. 구현은 해당 함수를 부르는 쪽에서 응답을 정하도록 작성한다. ```go var _ article.Usecase = &Usecase{} type Usecase struct { mock.Mock } func (u *Usecase) GetBy(lang string) ([]*article.Article, error) { ret := u.Called(lang) return ret.Get(0).([]*article.Article), ret.Error(1) } func (u *Usecase) Save(a *article.Article) error { ret := u.Called(a) return ret.Error(0) } func (u *Usecase) Delete(a *article.Article) error { ret := u.Called(a) return ret.Error(0) } ``` ### 테스트 케이스를 작성하기 테스트 대상(Class)의 모든 공개(Public) 함수들을 테스트 한다. 아래는 위에서 준비한 모킹 객체를 활용해서 컨트롤러 테스트케이스를 작성한 예제이다. 유스케이스의 함수들을 모킹할 때 어떤 함수(GetBy)의 파라미터로 어떤 값(en) 을 전달되어야 하는지를 작성하고 이에 대한 응답도 지정해준다. ```go func (ts *ControllerTestSuite) TestController_GetArticles() { // Given var mockArticles []*article.Article err := faker.FakeData(&mockArticles) ts.NoError(err) ts.usecase.On("GetBy", "en").Return(mockArticles, nil).Once() // 유스케이스 함수 모킹 req := (&network.Request{ Method: http.MethodGet, Url: "/articles", Params: &url.Values{ "lang": []string{"en"}, }, }).Build() ts.NoError(err) ctx, rec := ts.buildContextAndRecorder(req.GetHttpRequest()) // When err = ts.controller.GetArticles(ctx) // Then ts.NoError(err) ts.Equal(http.StatusOK, rec.Code) ts.usecase.AssertExpectations(ts.T()) } ``` ### 테스트 정의하기 테스트 스위트를 활용해서 테스트할 대상의 함수들에 대해 테스트 작성을 완료 했다면 해당 테스트 스위트를 실행할 테스트 코드를 작성한다. 테스트 코드는 Test 로 시작해야 하며 파라미터로 \*testing.T 를 전달한다. ```go func TestControllerSuite(t *testing.T) { suite.Run(t, new(ControllerTestSuite)) } ``` ## 테스트 코드 예제 ### 프로젝트 레이아웃
의존성 흐름: 컨트롤러 -> Article 도메인의 유스케이스 -> 엔티티 / 리포지토리 -> 데이터소스의 DbArticleGormSrouce ## 테스트 대상 & 예제 ### 컨트롤러 - api/articlesvc/controller.go - 의존성: Article 도메인의 유스케이스 (Get / Save / Delete)
[go-rest-sample] controller_test.go

```go package articlesvc_test // Step 3. Write test cases for each public method for the testing target. // In this case, articles controller is the testing target and it has 3 functions. (Get / Post / Delete) func (ts *ControllerTestSuite) TestController_GetArticles() { // Given var mockArticles []*article.Article err := faker.FakeData(&mockArticles) ts.NoError(err) ts.usecase.On("GetBy", "en").Return(mockArticles, nil).Once() req := (&network.Request{ Method: http.MethodGet, Url: "/articles", Params: &url.Values{ "lang": []string{"en"}, }, }).Build() ts.NoError(err) ctx, rec := ts.buildContextAndRecorder(req.GetHttpRequest()) // When err = ts.controller.GetArticles(ctx) // Then ts.NoError(err) ts.Equal(http.StatusOK, rec.Code) ts.usecase.AssertExpectations(ts.T()) } func (ts *ControllerTestSuite) TestController_PostArticles() { var mockArticle article.Article err := faker.FakeData(&mockArticle) mockArticle.ID = 0 // 새로운 Article ts.NoError(err) ts.usecase.On("Save", mock.Anything).Return(nil).Once() req := (&network.Request{ Method: http.MethodPost, Url: "/articles", Params: &url.Values{ "title": []string{mockArticle.Title}, "content": []string{mockArticle.Content}, "lang": []string{mockArticle.Lang}, }, }).Build() ctx, rec := ts.buildContextAndRecorder(req.GetHttpRequest()) err = ts.controller.PostArticles(ctx) ts.NoError(err) ts.Equal(http.StatusOK, rec.Code) ts.usecase.AssertExpectations(ts.T()) } func (ts *ControllerTestSuite) TestController_DeleteArticles() { var mockArticle article.Article err := faker.FakeData(&mockArticle) ts.NoError(err) ts.usecase.On("Delete", mock.Anything).Return(nil).Once() req := (&network.Request{ Method: http.MethodDelete, Url: "/articles", Params: &url.Values{ "id": []string{strconv.FormatInt(mockArticle.ID, 10)}, }, }).Build() ctx, rec := ts.buildContextAndRecorder(req.GetHttpRequest()) err = ts.controller.DeleteArticles(ctx) ts.NoError(err) ts.Equal(http.StatusOK, rec.Code) ts.usecase.AssertExpectations(ts.T()) } func (ts *ControllerTestSuite) buildContextAndRecorder(httpRequest *http.Request) (ctx core.Context, rec *httptest.ResponseRecorder) { rec = httptest.NewRecorder() ctx = ts.engine.NewContext(httpRequest, rec) return } // Step 2. Write test definition with TestSuite. func TestControllerSuite(t *testing.T) { suite.Run(t, new(ControllerTestSuite)) } // Step 1. Define a test suite. // Testing target and some pre-defined members should be initialized. // In this case, controller is testing target and usecase is defined for mocking. type ControllerTestSuite struct { suite.Suite controller articlesvc.Controller engine *core.Engine usecase *mocks.Usecase } func (ts *ControllerTestSuite) SetupTest() { ts.engine = echo.New() ts.usecase = new(mocks.Usecase) ts.controller = articlesvc.NewController(ts.engine, ts.usecase) } ```

### 유스케이스 - article/usecase.go - 의존성: 도메인 내의 entity.go, repository.go
[go-rest-sample] usecase_test.go

```go package article_test func (ts *UsecaseTestSuite) TestArticleUsecase_GetBy() { var mockArticles []*article.Article lang := "en" err := faker.FakeData(&mockArticles) ts.NoError(err) for _, mn := range mockArticles { mn.Lang = lang } ts.repo.On("Find", `lang = ?`, []interface{}{lang}).Return(mockArticles, nil).Once() results, err := ts.usecase.GetBy("en") ts.NoError(err) ts.Equal(mockArticles, results) ts.repo.AssertExpectations(ts.T()) } func (ts *UsecaseTestSuite) TestArticleUsecase_Save() { var mockArticle article.Article err := faker.FakeData(&mockArticle) ts.NoError(err) mockArticle.ID = 0 ts.repo.On("Save", &mockArticle).Return(nil).Once() err = ts.usecase.Save(&mockArticle) ts.NoError(err) ts.repo.AssertExpectations(ts.T()) } func (ts *UsecaseTestSuite) TestArticleUsecase_Delete() { var mockArticle article.Article err := faker.FakeData(&mockArticle) ts.NoError(err) ts.repo.On("Delete", &mockArticle).Return(nil).Once() err = ts.usecase.Delete(&mockArticle) ts.NoError(err) ts.repo.AssertExpectations(ts.T()) } func TestControllerSuite(t *testing.T) { suite.Run(t, new(UsecaseTestSuite)) } var ( _ suite.SetupTestSuite = &UsecaseTestSuite{} ) type UsecaseTestSuite struct { suite.Suite repo *mocks.Repository usecase article.Usecase } func (ts *UsecaseTestSuite) SetupTest() { ts.repo = new(mocks.Repository) ts.usecase = article.NewUsecase(ts.repo) } ```

[installedappsvc] usecase_test.go

```go package installedapp func TestGetInstalledApps(t *testing.T) { uid := int64(10001) apps := []InstalledApp{InstalledApp{UserID: uid, PkgName: "com.test.package.1"}} mockRepo := new(mockRepo) mockRepo.On("GetByUserID", uid).Return(apps, nil).Once() u := NewUsecase(mockRepo) pkgs, err := u.GetInstalledApps(uid) mockRepo.AssertExpectations(t) assert.Nil(t, err) assert.Equal(t, 1, len(pkgs)) assert.Equal(t, "com.test.package.1", pkgs[0].PkgName) } func TestSaveInstalledApps(t *testing.T) { uid := int64(10001) pkgName := "com.test.package.1" pkgNames := []string{pkgName} apps := []InstalledApp{InstalledApp{UserID: uid, PkgName: pkgName}} mockRepo := new(mockRepo) mockRepo.On("Create", apps).Return(nil).Once() mockRepo.On("DeleteByUserIDExcept", uid, pkgNames).Return(nil).Once() u := NewUsecase(mockRepo) u.SaveInstalledApps(uid, pkgNames) mockRepo.AssertExpectations(t) } ```

### 리포지토리 - article/repo/repo.go - 의존성: datasource/dbarticle/source.go
[go-rest-sample] repo_test.go

```go //https://github.com/jirfag/go-queryset/blob/master/queryset/queryset_test.go func (ts *RepoTestSuite) TestArticleRepository_Find() { var mockArticles []*dbarticle.Article lang := "en" err := faker.FakeData(&mockArticles) ts.NoError(err) for _, mn := range mockArticles { mn.Lang = lang } ts.dbSource.On("Find", `lang = ?`, []interface{}{[]interface{}{"en"}}).Return(mockArticles, nil).Once() var results []*article.Article results, err = ts.repo.Find("lang = ?", "en") ts.NoError(err) ts.Equal(len(mockArticles), len(results)) ts.dbSource.AssertExpectations(ts.T()) } func (ts *RepoTestSuite) TestArticleRepository_Save() { var mockDBArticle dbarticle.Article err := faker.FakeData(&mockDBArticle) ts.NoError(err) mockDBArticle.ID = 0 mockDBArticle.DeletedAt = nil mockDBArticle.CreatedAt = time.Time{} mockDBArticle.UpdatedAt = time.Time{} ts.dbSource.On("Save", &mockDBArticle).Return(nil).Once() mockArticle := repo.DBArticleToArticle(mockDBArticle) err = ts.repo.Save(&mockArticle) ts.NoError(err) ts.dbSource.AssertExpectations(ts.T()) } func (ts *RepoTestSuite) TestArticleRepository_Delete() { mockDBArticle := dbarticle.Article{ ID: 1, } mockDBArticle.DeletedAt = nil ts.dbSource.On("Delete", &mockDBArticle).Return(nil).Once() mockArticle := repo.DBArticleToArticle(mockDBArticle) err := ts.repo.Delete(&mockArticle) ts.NoError(err) ts.dbSource.AssertExpectations(ts.T()) } func TestRepoSuite(t *testing.T) { suite.Run(t, new(RepoTestSuite)) } type RepoTestSuite struct { suite.Suite dbSource *dbarticle.MockDBSource repo article.Repository } func (ts *RepoTestSuite) SetupTest() { ts.dbSource = &dbarticle.MockDBSource{} ts.repo = repo.New(ts.dbSource) } ```

[installedappsvc] repo_test.go

```go package iarepo type repoTestSuite struct { suite.Suite db *sql.DB } func TestRepoTestSuite(t *testing.T) { suite.Run(t, new(repoTestSuite)) } func (s *repoTestSuite) SetupSuite() { db, err := sql.Open("postgres", "") require.Nil(s.T(), err) s.db = db } func (s *repoTestSuite) SetupTest() { _, err := s.db.Exec("DELETE FROM installed_apps") require.Nil(s.T(), err) } func (s *repoTestSuite) TearDownSuite() { s.db.Close() } func (s *repoTestSuite) TestGetByUserID() { require := require.New(s.T()) assert := assert.New(s.T()) uid := int64(10001) pkgName := "com.test.package.1" _, err := s.db.Exec("INSERT INTO installed_apps (user_id, package_name) VALUES ($1, $2)", uid, pkgName) require.Nil(err) r := New(s.db) as, err := r.GetByUserID(uid) require.Nil(err) require.Equal(1, len(as)) assert.Equal(pkgName, as[0].PkgName) } func (s *repoTestSuite) TestCreate() { require := require.New(s.T()) assert := assert.New(s.T()) uid := int64(10001) as := []ia.InstalledApp{ ia.InstalledApp{UserID: uid, PkgName: "com.test.package.1"}, ia.InstalledApp{UserID: uid, PkgName: "com.test.package.2"}, } r := New(s.db) err := r.Create(as) require.Nil(err) rows, err := s.db.Query("SELECT user_id, package_name, uninstalled_at FROM installed_apps") require.Nil(err) require.True(rows.Next()) var ra ia.InstalledApp var ts *time.Time rows.Scan(&ra.UserID, &ra.PkgName, &ts) a := as[0] assert.Equal(a.UserID, ra.UserID) assert.Equal(a.PkgName, ra.PkgName) assert.Nil(ts) } func (s *repoTestSuite) TestCreateWhenUninstalledPackageExists() { require := require.New(s.T()) assert := assert.New(s.T()) uid := int64(10001) pkgName := "com.test.package.1" _, err := s.db.Exec("INSERT INTO installed_apps (user_id, package_name, uninstalled_at) VALUES ($1, $2, $3)", uid, pkgName, time.Now()) require.Nil(err) as := []ia.InstalledApp{ia.InstalledApp{UserID: uid, PkgName: pkgName}} r := New(s.db) err = r.Create(as) require.Nil(err) rows, err := s.db.Query("SELECT uninstalled_at FROM installed_apps") require.Nil(err) require.True(rows.Next()) var ts *time.Time rows.Scan(&ts) assert.Nil(ts) } func (s *repoTestSuite) TestDeleteByUserIDExcept() { require := require.New(s.T()) assert := assert.New(s.T()) uid := int64(10001) pkgName := "com.test.package.1" _, err := s.db.Exec("INSERT INTO installed_apps (user_id, package_name) VALUES ($1, $2)", uid, pkgName) require.Nil(err) r := New(s.db) err = r.DeleteByUserIDExcept(uid, []string{}) require.Nil(err) rows, err := s.db.Query("SELECT uninstalled_at FROM installed_apps") require.Nil(err) require.True(rows.Next()) var ts *time.Time rows.Scan(&ts) assert.NotNil(ts) } func (s *repoTestSuite) TestDeleteByUserIDExceptWhenFilterMatches() { require := require.New(s.T()) assert := assert.New(s.T()) uid := int64(10001) pkgName := "com.test.package.1" _, err := s.db.Exec("INSERT INTO installed_apps (user_id, package_name) VALUES ($1, $2)", uid, pkgName) require.Nil(err) r := New(s.db) err = r.DeleteByUserIDExcept(uid, []string{pkgName}) require.Nil(err) rows, err := s.db.Query("SELECT uninstalled_at FROM installed_apps") require.Nil(err) require.True(rows.Next()) var ts *time.Time rows.Scan(&ts) assert.Nil(ts) } ```

### 데이터소스 - datasource/dbarticle/gorm_source.go - 의존성: [gorm](http://doc.gorm.io/)
[go-rest-sample] gorm_source_test.go

```go package dbarticle_test //https://github.com/jirfag/go-queryset/blob/master/queryset/queryset_test.go func (ts *RepoTestSuite) TestGormArticleRepository_Find() { articles := getTestArticles(2) req := "SELECT * FROM `articles` WHERE `articles`.`deleted_at` IS NULL" ts.mock.ExpectQuery(fixedFullRe(req)). WillReturnRows(getRowsForArticles(articles)) var results []*dbarticle.Article results, err := ts.dbSource.Find("") ts.NoError(err) ts.Equal(articles, results) } func (ts *RepoTestSuite) TestGormArticleRepository_Save() { n := getTestArticles(1)[0] query := "INSERT INTO `articles` (`title`,`content`,`lang`,`author_id`,`created_at`,`updated_at`,`deleted_at`) VALUES (?,?,?,?,?,?,?)" args := []driver.Value{n.Title, sqlmock.AnyArg(), n.Lang, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), nil} ts.mock.ExpectExec(fixedFullRe(query)). WithArgs(args...). WillReturnResult(sqlmock.NewResult(1, 1)) err := ts.dbSource.Save(n) ts.NoError(err) } func (ts *RepoTestSuite) TestGormArticleRepository_Delete() { n := getTestArticles(1)[0] n.ID = 1 query := "UPDATE `articles` SET `deleted_at`=? WHERE `articles`.`deleted_at` IS NULL AND `articles`.`id` = ?" args := []driver.Value{sqlmock.AnyArg(), n.ID} ts.mock.ExpectExec(fixedFullRe(query)). WithArgs(args...). WillReturnResult(sqlmock.NewResult(1, 1)) err := ts.dbSource.Delete(n) ts.NoError(err) } func getTestArticles(num int) []*dbarticle.Article { articles := make([]*dbarticle.Article, 0) for i := 0; i < num; i++ { n := &dbarticle.Article{ Title: fmt.Sprintf("Title - %d", i), Lang: "en", } articles = append(articles, n) } return articles } func getRowsForArticles(articles []*dbarticle.Article) *sqlmock.Rows { var fieldNames = []string{"id", "title", "content", "lang"} rows := sqlmock.NewRows(fieldNames) for _, n := range articles { rows = rows.AddRow(n.ID, n.Title, n.Content, n.Lang) } return rows } func fixedFullRe(s string) string { return fmt.Sprintf("^%s$", regexp.QuoteMeta(s)) } func TestRepoSuite(t *testing.T) { suite.Run(t, new(RepoTestSuite)) } type RepoTestSuite struct { suite.Suite mock sqlmock.Sqlmock db *gorm.DB dbSource dbarticle.DBSource } func (ts *RepoTestSuite) SetupTest() { db, mock, err := sqlmock.New() ts.NoError(err) ts.mock = mock ts.db, err = gorm.Open("mysql", db) ts.NoError(err) ts.db.LogMode(true) ts.db = ts.db.Set("gorm:update_column", true) ts.dbSource = dbarticle.NewSource(ts.db) } func (ts *RepoTestSuite) AfterTest(suiteName, testName string) { _ = ts.db.Close() } ```

--- ## [gRPC](https://tech.buzzvil.com/handbook/grpc) Date: unknown 이 문서는 gRPC에 대한 간단한 설명과 사용 방법 및 가이드라인을 제공한다. gRPC를 사용하기 위한 protobuf에 관한 내용도 포함한다. ## gRPC의 정의 ![gPRC Diagram](https://tech.buzzvil.com/handbook/grpc.svg) gRPC는 Google 에서 만든 RPC(Remote Procedure Call) 프로토콜이다. 네트워크 요청을 소프트웨어 내부에 있는 함수를 호출하듯이 사용하게 도와주는 프로토콜이다. gRPC 정의에 관한 자세한 내용은 [What is gRPC?](https://grpc.io/docs/guides/index.html) 문서를 참고한다. ### gRPC 이점 #### 성능 네트워크 요청으로 보내기 위해 Protobuf를 직렬화(Serialization) 및 역직렬화(Deserialization)하는 작업은 JSON 형태의 직렬화/역직렬화 보다 빠르다. 또한 gRPC의 네트워크 속도가 HTTP POST/GET 속도보다 빠르다. 특히 POST 요청 시 많은 차이를 보인다. 자세한 내용은 [Mobile gRPC Benchmarks ](https://github.com/david-cao/gRPCBenchmarks) 문서를 참고한다. #### API-우선 방식 Protobuf를 통해 기능을 개발하기 전에 API를 먼저 정의할 수 있다. API가 먼저 정의될 경우 개발팀이 병렬적으로 일을 진행할 수 있고, 개발 속도가 빨라지며, API를 좀 더 안정적으로 제공할 수 있다. 자세한 내용은 [Understanding the API-First Approach to Building Products](https://swagger.io/resources/articles/adopting-an-api-first-approach/) 문서를 참고한다. #### REST API 지원 Protobuf로 정의된 API는 [envoyproxy](https://www.envoyproxy.io/)나 [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) 같은 gateway 를 통해 REST API로 제공 가능하다. gRPC로 정의된 API를 OpenAPI 프로토콜로 변환하여 REST API를 사용하는 클라이언트에도 API-우선 방식을 적용할 수 있다. ## gRPC 사용 방법 ### 데이터 구조 및 서비스 정의 gRPC는 데이터 구조 및 서비스를 정의하기 위해 기본적으로 [protocol buffers](https://developers.google.com/protocol-buffers/)를 사용한다. 자세한 문법 및 사용 방법은 [API 정의 방법](/handbook/api-definition#api-%EC%A0%95%EC%9D%98-%EB%B0%A9%EB%B2%95) 문서를 참고한다. ### gRPC 코드 생성 정의한 `.proto` 파일을 사용해 각 프로그래밍 언어별 코드를 생성할 수 있다. 언어별로 코드를 생성하는 방법을 제공된다. 각 언어별 코드 생성 방법은 [gRPC Quick Start](https://grpc.io/docs/quickstart/) 문서에서 확인할 수 있다. ```bash # Command example for generating code protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld ``` 버즈빌에서는 [buzzapis](https://github.com/Buzzvil/buzzapis) 레포지토리를 통해 API 라이브러리를 빌드한다. 자세한 방법은 [API 정의](/handbook/api-definition) 문서를 참고한다. ### gRPC 구현 `.proto` 파일을 통해 생성된 코드를 사용해 실제 기능을 구현한다. 일반적으로 생성된 코드는 서버 인터페이스를 제공하며, 해당 인터페이스의 기능을 구현함으로써 클라이언트에게 서버의 기능을 제공할 수 있다. 각 언어별 구현 방법은 [gRPC Quick Start](https://grpc.io/docs/quickstart/)를 참고한다. #### Go 프로젝트 예시 [authsvc.proto](https://github.com/Buzzvil/authsvc/blob/master/api/authsvc.proto) 파일을 통해 [authsvc.pb.go](https://github.com/Buzzvil/authsvc/blob/master/api/authsvc.pb.go) 코드를 생성한다. [server.go](https://github.com/Buzzvil/authsvc/blob/master/internal/app/authsrv/server.go) 에서 생성된 서비스의 인터페이스를 구현함으로써 클라이언트에게 해당 서비스를 제공할 수 있다. ```protobuf // authsvc.proto service AuthService { rpc GetAuth(GetAuthRequest) returns (Auth); } ``` ```go // server.go func (s *server) GetAuth(context.Context, *pb.GetAuthRequest) (*pb.Auth, error) { // ... } ``` #### Python 프로젝트 예시 [weathersvc.proto](https://github.com/Buzzvil/weathersvc/blob/master/api/weathersvc.proto) 파일을 통해 [weathersvc_pb2.py](https://github.com/Buzzvil/weathersvc/blob/master/api/weathersvc_pb2.py)와 [weathersvc_pb2_grpc.py](https://github.com/Buzzvil/weathersvc/blob/master/api/weathersvc_pb2_grpc.py) 코드를 생성한다. [server.py](https://github.com/Buzzvil/weathersvc/blob/master/weathersvc/weathersrv/server.py) 에서 생성된 서비스의 인터페이스를 구현함으로써 클라이언트에게 해당 서비스를 제공할 수 있다. ```protobuf // weatersvc.proto service WeatherService { rpc GetWeather (Location) returns (Weather) {} } ``` ```py # server.py class WeatherServer(weathersvc_pb2_grpc.WeatherServiceServicer): def GetWeather(self, request, context): # ... ``` ### gRPC 서버 실행 생성된 코드를 활용해 gRPC 서버를 생성 및 실행한다. 자세한 내용은 [gRPC Tutorials](https://grpc.io/docs/tutorials/) 문서에서 확인할 수 있다. 언어별 사용 법은 아래 문서를 확인한다. - [Go Project](/handbook/go-project) - [Python Project](/handbook/python-project) ### gRPC 클라이언트 요청 TBD #### OpenAPI 코드 생성 Protobuf 파일을 사용해서 OpenAPI(Swagger) 코드를 생성할 수 있다. 또한 이렇게 생성된 OpenAPI 코드를 통해 gRPC로 구현된 REST API 를 사용하는 클라이언트의 코드를 생성할 수 있다. OpenAPI 생성 방법은 [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway#usage) 의 `Usage 7.(Optional) Generate swagger definitions` 부분을 참고한다. 이때 `import "google/api/annotations.proto";` 디펜던시의 경우 [gRPC Transcoding](https://cloud.google.com/endpoints/docs/grpc/transcoding) 문서의 `Ensure HTTP rules are deployed` 부분을 참고하여 빌드할 수도 있다. ```bash protoc -I. \ -I./googleapis \ --swagger_out=logtostderr=true:. \ path/to/your_service.proto ``` --- ## [헬름(Helm)](https://tech.buzzvil.com/handbook/helm) Date: unknown 본 문서는 쿠버네티스 패키지 매니저인 헬름(Helm)에 대한 정보와 헬름 차트(Chart) 작성법에 대한 가이드라인을 제공한다. ## 헬름(Helm)이란 [Helm](https://helm.sh/)은 쿠버네티스 애플리케이션을 패키지 형태로 손쉽게 관리할 수 있도록 도와주는 도구다. 디플로이먼트(Deployment), 스테이트풀셋(Statefulset)과 같은 컴퓨팅 자원, 서비스(Service)나 인그레스(Ingress)와 같은 디스커버리/로드밸런싱 자원에 대한 정의를 템플릿 기반의 YAML로 작성하여 배포할 수 있다. 템플릿 엔진을 활용함으로써 여러 배포환경에 대한 반복적인 API 오브젝트 정의를 하지 않고 별도의 YAML 파일에 배포환경별로 달라지는 내용만 따로 명시하여 덮어쓸 수 있다. 릴리즈에 대한 버전 관리 또한 지원하여 문제가 발생할 시 CLI를 이용해 간단히 롤백 할 수 있다. 다만 버즈빌에서는 Helm의 템플릿 엔진만을 이용하고, 실제 릴리즈 관리는 Spinnaker라는 CD(Continuous Delivery) 플랫폼에서 이뤄진다. Spinnaker는 Helm을 이용해 템플릿을 렌더링하고 직접 쿠버네티스 클러스터에 반영하게 된다. ### 헬름 구성 요소 * Client: Helm CLI. * Tiller: 쿠버네티스 클러스터쪽에 설치되는 컴포넌트. CLI와 통신하여 클러스터에 실제로 반영하는 역할을 담당. 참고: 향후 릴리즈될 Helm 3 버전에서는 서버 컴포넌트인 Tiller에 권한이 과하게 부여되는 문제 때문에 클라이언트 전용 방식으로 바뀔 예정이다. ### 헬름 차트(Chart) Helm 차트라는 패키징 포맷을 사용한다. 차트는 쿠버네티스 리소스의 묶음을 템플릿 형태로 관리하며, 하나의 차트가 여러 개의 하위 차트(subchart)를 포함하는 방식으로 복잡한 워크로드를 정의하는 것도 가능하다. 필요한 경우 쿠버네티스 기본 리소스 뿐 아니라 Istio와 같은 서비스 메쉬에서 필요한 `VirtualService`, `Gateway`, 또는 Cert Manager에서 관리할 `Certificate` 리소스와 같은 CRD(Custom Resource Definition)도 포함할 수 있다. 버즈빌 내부 서비스에 대한 차트 정의는 [charts](https://github.com/Buzzvil/charts)에서 모노 리포 형태로 관리하고 있다. 이 모노 리포는 [Helm 공식 차트 리포지토리](https://github.com/helm/charts)와 비슷한 방식으로 구성된다. 차트 변경사항은 master 브랜치에 머지되면 CI를 통해 사내 차트 레지스트리에 업로드된다. 사내 차트 리포지토리의 구성 및 사용법은 [README](https://github.com/Buzzvil/charts/blob/master/README.md)를 참조한다. #### 차트 만들기 버즈빌 내부 서비스에 대한 차트는 개별 프로젝트가 아닌 [차트 모노 리포](https://github.com/Buzzvil/charts)에서 관리한다. 모노 리포를 사용하면 차트에 대한 lint, test, 릴리즈 등에 대한 CI를 프로젝트마다 별도로 세팅하지 않고 한 곳에서 처리할 수 있는 장점이 있다. 차트 작성은 버즈빌 헬름 차트 모노리포에서 CLI로 시작한다. ```bash $ helm create PROJECT_NAME > Creating PROJECT_NAME ``` 작성된 차트는 일반적으로 아래와 같은 구조로 이루어진다. ```bash buzzscreen ├── Chart.yaml ├── templates │   ├── _helpers.tpl │   ├── configmap.yaml │   ├── deployment.yaml │   ├── fluentd-configmap.yaml │   ├── fluentd-file-configmap.yaml │   ├── hpa.yaml │   ├── pdb.yaml │   ├── secret.yaml │   ├── service.yaml │   └── virtualservice.yaml └── values.yaml ``` * 차트 이름을 프로젝트 이름과 가능한 한 동일하게 생성하고, `Chart.yaml`에 적절한 description을 입력한다. * `helm create`으로 기본 생성되는 파일 중 `NOTES.txt`와 `tests` 디렉토리는 사용하지 않으므로 제거한다. * helper를 사용하여 여러 템플릿에 반복적으로 사용되는 `labels`, `matchLabels` 같은 표현의 반복을 줄인다([authsvc example](https://github.com/Buzzvil/charts/blob/master/charts/authsvc/templates/_helpers.tpl#L9-L25) 참고). * 배포하려는 서비스가 게이트웨이를 통해 직접 접근되는 경우 `VirtualService`를 정의한다. Istio의 `Gateway`를 사용하지 못하는 일부 케이스의 경우 `Ingress`를 정의한다. 클러스터 내부에서만 사용되는 경우 `Service`만 정의한다. * 가능하면 각 차트 내에 README 파일을 추가하여 value에 대한 설명, 배포 시 유의사항 등에 대한 설명을 추가한다. 공식 헬름 stable 차트 리포의 차트를 참고한다(i.e. [mysql example](https://github.com/helm/charts/tree/master/stable/mysql)). * 차트를 작성할 때 VS Code의 [쿠버네티스 공식 익스텐션](https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools)을 사용하면 신텍스 하이라이팅, 린트, 자동완성, 템플릿 출력 테스트 등을 해볼 수 있다. * 운영 중인 서비스의 안정성을 위해 PDB(Pod Disruption Budget), HPA(Horizontal Pod Autoscaler)도 차트에 함께 정의한다. 이외에도 공식 리포지토리의 [The Chart Best Practices Guide](https://github.com/helm/helm/tree/master/docs/chart_best_practices)를 참고하는 것을 권장한다. ### 배포 환경별 헬름(Helm) value 파일 작성 각 차트는 릴리즈 시에 value를 바인딩함으로써 각 차트마다 `values.yaml`에 기본값으로 정의된 value들을 덮어쓸 수 있다. 헬름 CLI를 이용하는 경우 덮어쓸 value가 정의된 YAML 파일을 지정하거나, `--set` 플래그를 이용해 개별적인 값들을 덮어쓸 수 있다. 버즈빌에서는 헬름 차트 배포를 스피네커(Spinnaker)를 이용해서 진행하는데, 이때는 덮어쓸 values를 배포환경별로 [별도의 모노리포](https://github.com/Buzzvil/helm-state)를 통해 관리하고, 배포 시점에 스피네커가 해당하는 환경의 설정 파일을 읽어와 헬름 템플릿을 렌더링한다. 이를 통해 [앱과 설정을 분리하여 배포](https://12factor.net/config)하는 방식이 가능하다. #### 커스텀 메트릭으로 HPA 만들기(Autoscaling v2) TBD --- ## [Java 프로젝트](https://tech.buzzvil.com/handbook/java-project) Date: unknown TBD --- ## [쿠버네티스 운영](https://tech.buzzvil.com/handbook/k8s-operation) Date: unknown ## 쿠버네티스 운영 이 문서는 쿠버네티스와 클러스터에 배포된 워크로드들의 상태를 확인하고 문제가 있을 시 트러블슈팅하는 데 도움이 될만한 정보를 기술하고 있다. ### kubectl `kubectl`은 쿠버네티스 클러스터에 각종 커맨드를 실행하기 위한 커맨드라인 인터페이스로, 쿠버네티스 클러스터에 배포된 워크로드들의 상태를 조회하고 운영하기 위해 가장 기본적으로 필요한 툴이다. 기본적인 사용법은 [공식문서](https://kubernetes.io/docs/reference/kubectl/overview/)를 참고하면 되고, [사내 EKS 클러스터 접속을 위한 kubectl 설정 가이드](https://buzzvil.atlassian.net/wiki/spaces/DO/pages/394395738/Kubectl+Not+ready)를 참고해 EKS 클러스터에 대한 세팅을 할 수 있다. `kubectl`은 [go 클라이언트](https://github.com/kubernetes/client-go)에 기반하고 있는데, 해당 라이브러리를 이용하면 손쉽게 커스텀한 클라이언트 프로그램을 작성할 수 있다. #### oh-my-zsh plugin `kubectl` 사용이 익숙해졌다면 oh-my-zsh의 [kubectl 플러그인](https://github.com/robbyrussell/oh-my-zsh/tree/master/plugins/kubectl)을 이용해 커맨드 에일리어스 및 자동완성을 이용할 수 있다. 특히 레플리카셋, 파드 이름 등은 해당 자원을 관리하는 디플로이먼트, 잡 등의 이름에 기반하여 랜덤하게 부여되므로 자동완성 기능이 있으면 매우 편리하게 운영과 관련된 작업을 할 수 있다. #### krew(kubectl 플러그인 매니저) [`krew`](https://github.com/kubernetes-sigs/krew/)는 kubectl 플러그인을 위한 패키지 매니저다. 파드(Pod) 재시작 플러그인처럼 `kubectl`에서 기본적으로 지원하지 않는 확장 기능이 다수 제공되므로 확인해볼 것을 권장한다. #### kubectx [`kubectx`](https://github.com/ahmetb/kubectx) + `kubens` 는 간편하게 클러스터나 네임스페이스 컨텍스트를 전환할 수 있도록 도와주는 유틸리티이다. 여러 클러스터를 관리하거나 네임스페이스를 오가며 작업할 때 유용하다. 현재 버즈빌에서는 서비스별로 네임스페이스를 할당하고, 개별 사용자의 접근제어도 네임스페이스 단위로 이뤄지기 때문에 `kubens postbacksvc` 와 같은 커맨드로 본인이 작업할 네임스페이스로 컨텍스트를 전환하는 것이 권장된다. 여러 컨텍스트를 옮겨다니며 작업하는 경우 리소스를 잘못 변경하는 경우가 생길 수 있다. [kube-ps1](https://github.com/jonmosco/kube-ps1/)을 이용하면 커맨드 프롬프트에 현재 context(cluster, namespace) 정보를 출력되기 때문에 실수의 여지가 줄어든다. ![kube-ps1](https://tech.buzzvil.com/handbook/kube-ps1.png) ### 파드 상태 조회하기 파드(Pod)는 쿠버네티스에서 하나 또는 그 이상의 연관된 컨테이너가 배포되는 기본적인 단위이다. 연관된 컨테이너라 함은 메인이 되는 컨테이너(주로 애플리케이션 컨테이너)를 보조해 메트릭을 외부 저장장치로 전송하거나, nginx와 같은 리버스 프록시가 함께 묶여 스케쥴링이 되는 사이드카 패턴을 의미한다. 횡적인 확장을 위해 컨테이너가 추가적으로 필요한 경우 파드에 컨테이너를 추가적으로 정의하는 것이 아니라 파드의 개수를 늘리는 방향으로 확장한다. 애플리케이션 상태를 확인하기 위해 가장 먼저 하게 되는 일은 주로 파드의 상태를 확인하는 것이다. 파드가 재시작되고 있지 않는지, 노드에 잘 스케쥴링 되었는지, 리소스(CPU 및 메모리 자원)는 어느정도 할당되었고 현재 사용하고 있는지 등을 확인할 수 있다. 리소스 할당은 특히나 중요한데, 리소스의 요청량(requests) 및 한도(limits)에 의해 파드가 어떻게 스케쥴링 될지, 과도한 리소스가 요구될 시 해당 컨테이너에 CPU를 더 스케쥴링할지, OOM Kill을 할 지 등이 결정되기 때문이다. #### kubectl 이용 파드의 상태를 조회하는 가장 기본적인 방법은 `kubectl`을 이용하는 방법이다. ```bash # 파드 상태 확인 # READY: 파드에 정의된 컨테이너 중 몇 개의 컨테이너가 준비되었는지 확인할 수 있다. # STATUS: 모든 컨테이너가 잘 동작하고 있으면 Running. 다른 상태에 대한 설명은 https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase 참고 # RESTARTS: 파드가 총 몇 회 재시작되었는지를 나타낸다. livenessProbe 실패 시, 혹은 메인 커멘드가 종료될 시 쿠버네티스는 파드를 재시작한다. $ kubectl get pods > NAME READY STATUS RESTARTS AGE buzzscreen-api-dev-7cc4c6b6b9-4rnq8 3/3 Running 2 7d8h buzzscreen-api-dev-7cc4c6b6b9-wh8bk 3/3 Running 1 7d20h buzzscreen-api-prod-68f4494fdd-6tz8j 3/3 Running 1 46m buzzscreen-api-prod-68f4494fdd-czpvt 0/3 Init:0/1 0 2s # 파드 리소스 사용량 확인 # CPU: 1 == 1000m == 1코어 $ kubectl top pods > NAME CPU(cores) MEMORY(bytes) buzzscreen-api-dev-7cc4c6b6b9-4rnq8 4m 119Mi buzzscreen-api-dev-7cc4c6b6b9-wh8bk 4m 123Mi buzzscreen-api-prod-68f4494fdd-6tz8j 3m 123Mi buzzscreen-api-prod-68f4494fdd-8vrxg 68m 110Mi ``` #### 스피네커 이용하기 커맨드라인 인터페이스 없이도 스피네커 UI의 Infrastructure - Clusters 메뉴에서 배포된 디플로이먼트에 속한 파드의 상태 확인 및 간단한 로그 조회 등을 할 수 있다. ![스피네커 UI에서 파드 상태 확인](https://tech.buzzvil.com/handbook/spinnaker_pod_status.png) #### Newrelic Infrastructure Newrelic Infrastructure의 쿠버네티스 연동을 통해 클러스터의 노드나 파드 등에 대한 상태를 GUI로 손쉽게 확인할 수 있다. 노드의 스토리지, 개별 파드의 리소스 사용량 및 잦은 재시작 등에 대한 현황을 확인하고 얼럿을 설정할 수 있다. ![Newrelic Infrastructure - Kubernetes](https://tech.buzzvil.com/handbook/newrelic_infrastructure_kubernetes.png) ### Log 조회 컨테이너 워크로드는 로그스트림을 `STDOUT`, `STDERR`로 출력하는 것을 기본으로 한다. 개별 컨테이너에서 로그 저장이나 관리를 담당하지 않고 클러스터 레벨에서 한번에 하기 위함이다. 이렇게 출력한 로그를 조회하는 방법은 다음과 같다. #### kubectl logs 가장 기본적인 방법으로, `kubectl logs` 커맨드를 사용할 수 있다. 이 커맨드는 개별 파드에 대한 조회가 가능하며 `-f, --follow` 옵션을 이용해 테일을 할 수 있다. ```bash # POD_NAME에 해당하는 파드를 출력. 선택적으로 CONTAINER_NAME에 해당하는 컨테이너의 로그만 출력 가능. $ kubectl logs -f POD_NAME -c CONTAINER_NAME # 특정 label에 매칭되는 여러 파드의 로그를 출력. 하나의 파드에 여러 컨테이너가 존재할 경우 컨테이너 옵션이 필수이다. $ kubectl logs -l LABEL=value -c CONTAINER_NAME ``` #### stern `kubectl`은 조회할 파드를 식별해야 하기에 빠르게 여러 파드의 로그를 조회하기에 불편하다. [`stern`](https://github.com/wercker/stern)을 이용하면 파드 이름을 쿼리하여 매칭되는 여러 파드의 로그를 실시간으로 테일을 할 수 있으며, `--since TIME` 옵션을 이용해 특정 시점부터의 로그를 손쉽게 조회하는 것이 가능하다. ```bash # buzzscreen-prod이 들어간 모든 파드의 1분 이내의 로그 조회 $ stern buzzscreen --since 1m ``` 파드의 이름을 쿼리 기반으로 검색할 수 있기 때문에, `stern`과 `grep`을 잘 활용하면 `kubectl logs`보다 훨씬 편리하게 로그를 확인할 수 있다. bash나 zsh [자동완성도 제공](https://github.com/wercker/stern#completion)하니 활용하는 것을 권장한다. #### kibana `kubectl`이나 `stern`은 파드가 삭제되면 이전의 로그를 조회할 수 없기에 클러스터 레벨에서 STDOUT으로 출력된 로그를 별도의 ElasticSearch로 저장하고 있다. 로그 조회는 kibana를 이용해 할 수 있고, 각종 쿼리를 이용해 조회하는 범위를 좁혀나갈 수 있어 유용하다. ### Rollback TBD ### Grafana TBD --- ## [멀티리포 vs 모노리포](https://tech.buzzvil.com/handbook/multirepo-vs-monorepo) Date: unknown ## 들어가며 본 문서는 소스 형상 관리 시스템상에서 소스 리포지토리를 관리하는 방법인 `멀티리포`와 `모노리포`를 비교하고 분석하여, 개발 조직이 어떤 방식을 선택해야 하는지 정보를 제공한다. ## 멀티리포, 모노리포 선택의 필요성 멀티리포, 모노리포에 선택에 대해 고민하기에 앞서, 왜 우리가 이들에 관심을 가져야 하는지 알아야 한다. 그 이유는 프로젝트의 최초 생성과 변화 과정을 들여다보면 알 수 있다. ### 초기 프로젝트 구조 프로젝트 초기에는 모노리딕 시스템으로 서비스를 구현하게 된다. 프로젝트의 크기가 크지 않고, 개발자의 수도 적기 때문에 모든 것을 한곳에서 처리하는 것이 효율적이다. 이 경우 시스템 자체가 쪼개어지지 않고 하나이기 때문에 리포지토리 역시 하나로 관리하게 된다. ### 프로젝트 거대화에 따른 마이크로 서비스 구성 프로젝트가 거대화되면서 모노리딕 시스템은 문제를 야기한다. 대표적으로 높은 결합도와 낮은 응집력을 예로 들 수 있다. 이를 해결하기 위해서 개발 조직은 시스템의 각 부분을 도메인 별로 분리해서 마이크로 서비스로 구성하기 시작한다. 이때 개발 조직은 쪼개진 각 서비스를 하나의 리포지토리에서 관리할지, 각자 다른 리포지토리에서 관리할지 고민하게 된다. ## 멀티리포 vs 모노리포 리포지토리를 관리하는 방법은 시스템의 각 모듈을 개별 리포지토리에서 관리할 것인지, 하나의 리포지토리에서 관리할 것인지에 따라서 달라진다. 이때 나눠서 관리하는 것을 `멀티리포`, 하나로 관리하는 것을 `모노리포`라 정의한다. ### 멀티리포 시스템의 서비스별로 리포지토리를 각자 만들어서 관리한다.
서비스 간의 연동이 소스 단위로 이루어지지 않는다.
각 서비스가 별도의 폴더로 구성된다. #### 장점 - 강한 오너쉽 확보
- 리포지토리 별로 오너를 지정 - 마스터의 코드가 깨질 여지가 적음 - 코드 베이스가 아예 나뉘어 있음 - 서로 간의 작업 충돌로 마스터 코드가 깨질 가능성이 적음 - 형상 관리, CI 속도가 빠름 - 리포지토리의 크기가 작기 때문에, 리파지토리 훅을 기반으로 동작하는 도구들의 속도가 빨라짐 #### 단점 - 코드 재사용이 쉽지 않으므로, 중복 코드 가능성이 높아짐 - 다른 리포지토리의 코드를 사용하기 위해서 해야 할 작업이 좀 있음 - 하나의 피쳐 개발을 위해 여러 리포지토리에 머지를 해야 함 - 코드 리뷰가 나누어짐 - 버전 연동이 깨질 위험이 있음 - 하나의 브레이킹 체인지가 다른 리포지토리로 즉시 전파되지 않음 - 디펜던시 헬 - 프로젝트가 거대화됨에 따라 의존 그래프가 매우 복잡해지게 됨 - 서로의 코드에 변화가 생길 때 이를 대처하기 쉽지 않음 ### 모노리포 시스템의 각 서비스를 모두 하나의 리포지토리에서 일괄 관리한다.
서비스 간의 연동이 소스 단위로 이루어진다.
최상위 폴더부터 트리 구조로 서비스 폴더가 구성된다. #### 장점 - 지속적인 소스의 무결성 보장 - 리포지토리는 항상 모든 서비스가 연동된 올바른 상태를 유지함 - 통합된 버전 관리 - 모든 서비스가 연동된 상태에서 손쉽게 하나의 버전으로 관리 가능 - 코드의 공유와 재사용이 용이 - 소스 단위의 연동이 이루어진 상태 - 의존성 관리가 쉬움 - 전체 서비스의 의존 관계가 한 리포지토리에서 확인 및 설정 가능 - 원자 단위 변화 - 변화가 여러 스텝이 아니라 한 리포지토리에서 한 스텝으로 이루어짐 - 여러 프로젝트팀 간의 협업이 쉬움 - 하나의 리포지토리에서 함께 작업하며, 여러 서비스에 손쉽게 접근 가능 - 유연한 팀 바운더리 설정과 코드 오너쉽을 가져갈 수 있음 - 하나의 리포지토리, 하나의 서비스에 제한된 코드 오너쉽을 유지하지 않아도 됨 - 통합 CI 및 테스트 - 모든 소스가 연동된 상태. CI 및 테스트 구성이 손쉬움 - 전체 코드가 트리 구조로 명확히 보임 - 한 번의 코드 리뷰에 모든 변화가 요약 #### 단점 - 무분별한 의존성 연결 가능 - 의존성 연결이 쉽기 때문에 오히려 과도한 의존 관계가 나타날 수 있음 - 형상 관리 및 CI 속도 저하 - 리포지토리의 크기가 크기 때문에, 리파지토리 훅을 기반으로 동작하는 도구들의 속도가 느려짐 ## 주요 특징 비교 ### 코드 오너쉽 코드 오너쉽의 경우 모노리포, 멀티리포의 측면보다는 마이크로 서비스의 구축 여부가 더 중요하다. 각 서비스가 도메인에 맞게 잘 분리가 되어 있다면, 서비스별로 오너쉽을 부여할 수 있을 것이다.

모노리딕 시스템에서는 아직 서비스의 분리가 미비하고, 하나의 서비스 안에서 여러 부분으로 오너쉽을 나눠 가져야만 한다. 이 때문에 코드의 오너쉽이 모호하게 분리된다. 이를 해결하기 위한 가장 좋은 방법은 마이크로 서비스로 시스템을 나누는 것이고, 그것이 힘들다면 도구의 도움을 받아서 폴더별로 코드의 오너를 지정하는 방법이 있다. ### 브레이킹 체인지 전파 멀티리포의 경우 각 리포지토리 별로 소스를 관리하기 때문에 브레이킹 체인지가 일어났을 경우 이를 의존하는 모듈에 자동으로 변화가 전파되지 않는다. 반대로, 모노리포의 경우 하나의 소스에서 작업을 진행하기 때문에 브레이킹 체인지가 발생했을 경우, 즉시 에러로 검출이 가능하다. ### 코드 리뷰 멀티리포의 경우 여러 리포지토리에 변경이 필요한 피쳐 작업이 있을 때, 코드 리뷰 또한 여러 리포지토리로 퍼트려야 한다. 모노리포의 경우 하나의 리포지토리에서 하나의 작업으로 처리가 되므로 코드 리뷰 효과가 명확하게 진행이 된다. ### 의존성 관리 모노리포의 경우 모든 소스가 한 리포지토리에서 관리되므로, 의존성 관리에 유리하다. 모든 의존 관계를 한 리포지토리에서 확인 가능하며, 관련 설정 파일도 한곳에서 모아서 관리가 가능하다. 반대로 멀티리포는 각 리포지토리가 자신의 의존관계만을 알고 있기 때문에 전체적인 의존관계를 파악하기 쉽지 않다. ### 버전 관리 멀티리포와 모노리포의 장단점이 존재한다. 멀티리포의 경우 서비스별로 형상 관리가 유지되기 때문에, 버전 관리를 서비스 단위로 독립적으로 하는 데 유리하다. 하지만 이는 위에서 살펴보았듯이 각 서비스 간의 버전 충돌 문제를 야기한다. 모노리포의 경우 각 서비스의 통합 버전 관리가 기본 베이스가 된다. 모든 소스가 연동된 상태에서 버전 관리가 이루어지기 때문에 서비스별로 버전 전략을 다르게 가져가며 배포에 어려움이 있다. 이러한 세밀한 버전 관리의 경우 브랜치 전략을 고도화하고, 도구의 도움을 받아 해결해야 한다. ### 도구 속도 멀티리포가 더 유리하다. 멀티리포는 각 작은 리포지토리별로 각자 도구들이 동작하게 되며, 작업의 단위가 작아지기 때문에 속도가 더 빠르다. 하지만, 이는 리포지토리 내에서도 특정 부분에 대해서만 형상 관리, CI 등을 제공하는 툴을 사용해서 해결할 수 있다. ## 결론 모노리포, 멀티리포는 간단히 생각하면 리포지토리를 어떻게 나눌 것이냐에 대한 방법론이다. 하지만 깊게 들여다보면 각각의 장단점이 존재하고 있고 주요 특징별로 고려해야 할 부분이 상당수 존재한다.

그렇다면 개발 조직은 위 방법 중 어떠한 것을 선택해야 하는가? 그 선택의 기준은 현재 혹은 미래에 계획 중인 시스템 구조가 무엇인지, 그리고 그에 맞는 것이 무엇인지 고민하는 것이다. 만약, 구조가 여러 모듈로 쪼개져 있고 의존관계가 복잡하지 않은 경우 멀티리포가 관리에 더 편할 수도 있다. 하지만, 시스템의 규모 및 개발 조직이 거대화되고 있다면, 그로 인해 의존성 그래프가 급격히 복잡해질 것으로 예측된다면, 모노리포가 관리에 더 유용할 수 있다.

## 참고 자료 ### 기업별 모노리포 사례 - [Google - Why Google Stores Billions of Lines of Code in a Single Repository(Video)](https://youtu.be/W71BTkUbdqE) - [Google - Why Google Stores Billions of Lines of Code in a Single Repository](https://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext) - [Facebook - Scaling Mercurial at Facebook](https://code.fb.com/core-data/scaling-mercurial-at-facebook) - [Uber - Uber Technology Day: Monorepo to Multirepo and Back Again(Video)](https://youtu.be/lV8-1S28ycM) - [Uber - The Journey To Android Monorepo: The History Of Uber Engineering’s Android Codebase Organization](https://eng.uber.com/android-monorepo/) - [Airbnb - From Monorail to Monorepo: Airbnb’s journey into Microservices](https://youtu.be/sakGeE4xVZs) - [Spotify - Segment's transition back to a monorepo(Video)](https://open.spotify.com/episode/5SjMEcmi2oTsBpcz3eJH0C) - [Netflix - Towards true continuous integration: distributed repositories and dependencies](https://medium.com/netflix-techblog/towards-true-continuous-integration-distributed-repositories-and-dependencies-2a2e3108c051) ### 기타 - [Monorepos in the Wild](https://medium.com/@maoberlehner/monorepos-in-the-wild-33c6eb246cb9) - [Awesome Monorepo](https://github.com/korfuri/awesome-monorepo) - [Advantages of monorepos](https://danluu.com/monorepo/) - [Report: “We went Monorepo.”](https://softwareengineeringdaily.com/2018/11/27/report-we-went-monorepo/) - [Monorepos: Please don't!](https://medium.com/@mattklein123/monorepos-please-dont-e9a279be011b) - [Monorepos: Please do!](https://medium.com/@adamhjk/monorepo-please-do-3657e08a4b70) --- ## [프로젝트 시작하기](https://tech.buzzvil.com/handbook/new-project) Date: unknown 이 문서는 새로운 서비스 혹은 모듈 프로젝트를 시작하는 방법을 소개한다. ## 준비물 프로젝트를 시작하기 위해서 아래 항목들이 필요하다. - [Cookiecutter](https://cookiecutter.readthedocs.io) - [Docker](https://www.docker.com/) ## 사전 지식 ### 클린 아키텍처 로버트 C. 마틴(Robert C. Martin a.k.a Uncle Bob)가 제안한 소프트웨어 아키텍처이다. 소프트웨어의 각 부분이 담당하는 레이어를 정의하고 레이어 사이의 의존성 규칙을 정의함으로써 레이어간의 의존성을 최소화 할 수 있다. 예를 들어 표현(Presentation)의 변경이 비지니스 로직의 변경 없이 적용될 수 있으며, 그 반대 또한 가능하다. 레이어간 의존성이 적으므로 데이터 저장소의 변경이 쉽고, 또한 도메인 로직 변경이 프레임워크의 영향을 받지 않는 장점도 있다. 좀 더 자세한 내용은 [The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 문서를 참고한다. ### 기능 기반 패키지(Package by Feature) 패키지는 함께 사용되거나 함께 변경되는 코드를 같은 곳에 두는(높은 응집성, Highly-cohesive) 동시에 서로 관련이 없거나 독립적으로 존재하는 코드를 분리(낮은 결합도, Loosely-coupled)하는데 큰 역할을 한다. 언어별로 패키지(package) 관련 문법이나 사용 방법은 다르지만 목적이나 원칙은 거의 동일하다. 패키지 방식에는 대표적으로 레이어 기반 패키지(Package by Layer)와 기능 기반 패키지(Package by Feature) 방식이 있다. 레이어 기반 패키지 방식은 상대적으로 개념을 이해하기 쉽다는 장점이 있으나 높은 응집성과 낮은 결합도를 이끌어내기 어렵다. 하나의 기능 변경을 위해 여러 패키지를 수정해야하며, 이를 위해 많은 이동을 해야한다. 반면 기능 기반 패키지는 패키지의 목적을 달성하기 위한 장점을 충분히 가지고 있기에 많은 부분에서 사용되고 있다. --- ## [프로덕션 체크리스트](https://tech.buzzvil.com/handbook/production-checklist) Date: unknown 이 문서는 새롭게 작성한 서비스를 배포하기에 앞서 확인해야 할 사항들을 기술하고 있다. 쿠버네티스 환경에 신규 서비스를 배포하기에 앞서 작성한 서비스가 아래 사항들을 준수하는지 확인하는 것을 권장한다. ## 도커 이미지 프로덕션 환경에 배포되는 도커 이미지는 단순히 `docker-compose up`으로 개발환경을 구동할 수 있는 상태를 만드는 것 이상의 준비가 필요하다. [도커](/handbook/docker) 문서를 참고하여 프로덕션 환경에 적합한 경량하고 안전한 도커 파일을 작성한다. ## 로그 모든 로그는 `STDOUT` 또는 `STDERR`으로 출력한다. 컨테이너화된 서비스는 서버나 메인 커맨드를 구동하는 단일 관심사에만 신경을 쓰고, 로그 파일이나 디렉토리에 대한 관리, logrotate 등의 사항은 컨테이너 오케스트레이션(쿠버네티스)으로 위임한다. 메트릭을 외부 스토리지로 전송하는 등의 요구사항이 있는 경우에도 fluentd 등을 사이드카 패턴으로 배포할 수 있지만 파드의 lifecycle 복잡도를 높이므로 반드시 필요한 경우를 제외하고는 권장하지 않는다. 향후 `STDOUT`으로 출력한 메트릭 로그를 S3에 아카이빙 한 이후 Airflow에서 메트릭을 따로 필터링해서 프로세싱하는 작업을 추가하는 방향을 고려할 수 있다. ## 환경변수 외부 서비스 인증 정보나 데이터베이스 접속 정보와 같은 애플리케이션의 중요한 설정은 환경변수로 컨트롤 할 수 있어야 한다. 이는 [12 요소 앱](https://12factor.net/ko/)에서 설명하는 [설정](https://12factor.net/config)에 해당하는 것으로, 쿠버네티스와 같은 컨테이너 오케스트레이션 환경에서는 빌드 아티팩트(컨테이너 이미지)와 설정을 분리하는 것이 쉬워지고, 시간이 지남에 따라 추가되는 배포환경(CI나 e2e 테스트 환경)을 코드 변경 없이 설정할 수 있다. ## 디펜던시 시스템 패키지나 언어별 의존성 패키지는 버전을 명시해서 고정시키는 것이 좋다. 버전이 모호하게 지정되어 있을 경우(예를 들어 메이져 버전까지만 명시된 경우), 시간이 지나 동일한 소스코드로 컨테이너 이미지를 빌드한 경우 이전에 빌드된 것과 다른 의존성 그래프를 가진 채로 빌드된다. `package-lock.json`과 같은 락파일을 버전 컨트롤 시스템을 이용해 관리하는 것이 좋다. 관리가 소홀해진 프로젝트는 의존성 패키지 버전이 뒤쳐지기 쉽다. [dependabot](https://dependabot.com/)같은 도구 이용해 지속적으로 패키지 버전을 리뷰하고 업데이트 할 수 있도록 한다. ## README 작성 프로젝트의 README 파일에는 해당 애플리케이션에 대한 기본적인 사용법을 기술하여야 한다. 구동에 필요한 요구사항, 서버 시작, 마이그레이션 방법 등이 있다. ## 정상적인 종료(Graceful shutdown) 롤링 업데이트나 노드 드레인 등의 이벤트가 발생할 시 기존 파드는 종료(Terminating) 상태에 돌입한다. 운영 중인 파드가 종료되는 것은 쿠버네티스 환경에서는 일상적이고 흔하게 발생하는 일이다. 다만 개별 애플리케이션은 파드 종료를 정상적으로 핸들링할 의무를 지니게 된다. 애플리케이션은 `SIGTERM` 시그널을 받게 되면 더 이상 추가적인 요청을 받지 않고 기존에 처리 중인 요청을 마저 처리하고 프로세스를 종료하도록 하는 루틴을 작성해야 한다. * [golang](https://medium.com/over-engineering/graceful-shutdown-with-go-http-servers-and-kubernetes-rolling-updates-6697e7db17cf) 파드의 수명주기와 관련된 구체적인 내용은 구글 클라우드의 블로그의 [Kubernetes best practices: terminating with grace](https://cloud.google.com/blog/products/gcp/kubernetes-best-practices-terminating-with-grace)를 참고. ## Readiness Probe, Liveness Probe 각 애플리케이션은 정상 상태를 확인할 수 있는 방법을 제공해야 한다. 쿠버네티스는 정상 상태 확인에 대한 두 가지 방식을 제공한다. * readiness: applicaiton이 요청을 처리할 준비가 됐는지 체크한다. readiness probe가 성공적으로 응답하면 요청을 처리할 수 있다는 의미이다. * liveness: 애플리케이션이 문제가 있어서 재시작이 필요한 상태를 의미한다. liveness probe가 실패 응답을 내보내고 지정된 임계점을 넘으면 오케스트레이터는 파드를 재시작한다. 정상 상태 확인은 http 엔드포인트 뿐 아니라 커맨드 실행으로도 할 수 있다. grpc 서버의 경우 [grpc-health-probe](https://github.com/grpc-ecosystem/grpc-health-probe) 프로토콜을 구현하여 `grpc-health-probe` 커맨드를 실행하는 방식으로 정상 상태 확인을 할 수 있다. ## 모니터링 이 외에도 에러 트래킹, APM(Applicaiton Performance Management) 등에 대한 설정이 필요하다. TBD --- ## [프로젝트 템플릿](https://tech.buzzvil.com/handbook/project-template) Date: unknown 이 문서에서는 새로운 프로젝트를 생성하는 방법을 소개한다. 버즈빌에서는 [쿠키커터](https://cookiecutter.readthedocs.io)를 사용해서 신규 프로젝트를 생성한다. ## 언어별 프로젝트 템플릿 각 언어별 프로젝트 템플릿은 다음 링크에서 확인할 수 있다: - [Go 프로젝트 템플릿](https://github.com/Buzzvil/cookiecutter-go) - [Python 프로젝트 템플릿](https://github.com/Buzzvil/cookiecutter-python) ## 신규 프로젝트 생성 쿠키커터를 이용해 신규 프로젝트를 생성한다. ### Go 프로젝트 생성 예시 ```bash cookiecutter git@github.com:Buzzvil/cookiecutter-go.git ``` 이후 프롬프트에 나오는 각 항목에 프로젝트 정보를 기입한다. ```bash model [UserProfile]: Article package_name [article]: article service_name [articlesvc]: articlesvc ``` ## 프로젝트 구조 프로젝트 템플릿은 클린 아키텍처와 도메인 주도 설계, 그리고 기능 기반 패키지를 바탕으로 구성되어 있다. 언어 혹은 프레임워크별 프로젝트 구성은 [개발 가이드 문서](/handbook/go-project)에서 확인할 수 있다. --- ## [Python 프로젝트](https://tech.buzzvil.com/handbook/python-project) Date: unknown ## API 구현 [API 정의](/handbook/api-definition) 문서를 통해 정의된 API를 구현한다. ### 라이브러리 적용 API 정의를 통해 빌드된 Python 라이브러리는 버즈빌 내부적으로 사용하는 프라이빗 [PyPI](https://pypi.org/) 패키지 관리 시스템으로 배포된다. `pip` 커맨드를 사용해서 배포된 라이브러리를 가져올 수 있다. ```bash $ pip install --extra-index-url $BUZZVIL_PYPI_HOST PACKAGE && pip freeze > requirements.txt Collecting PACKAGE ... ``` ### 인터페이스 구현 라이브러리에 정의된 API를 구현함으로써 클라이언트에게 서비스를 제공할 수 있다. ```python # librarysvc/librarysrv/server.py import logging import grpc from library import library_pb2, library_pb2_grpc class LibraryServer(library_pb2_grpc.LibraryServiceServicer): def GetBook(self, request, context): # TODO: Find the book from database. return library_pb2.Book( title="Title here.", author="Author here." ) ``` ## 서버 구성 ### 서버 생성 및 실행 gRPC를 통해 서버를 생성하고 실행할 수 있다. 이때 `app.py` 에서는 연결을 맺거나 서버를 실행하는 역할에만 집중하고 실제 구현은 내부 패키지에게 위임한다. 자세한 사용법은 [Starting the python server](https://grpc.io/docs/tutorials/basic/python.html#starting-the-server) 문서를 참고한다. ```python # app.py from concurrent import futures import os import time import logging import grpc from librarysvc.librarysrv.server import LibraryServer from library import library_pb2_grpc _ONE_DAY_IN_SECONDS = 60 * 60 * 24 def serve(): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) library_pb2_grpc.add_LibraryServiceServicer_to_server(LibraryServer(), server) server.add_insecure_port('[::]:{}'.format(os.environ['PORT'])) server.start() try: while True: time.sleep(_ONE_DAY_IN_SECONDS) except KeyboardInterrupt: server.stop(0) if __name__ == '__main__': logging.basicConfig() serve() ``` ### 서버 로깅 TBD ## 프로젝트 구성 ### 폴더 구조 TBD --- ## [파이썬 테스트](https://tech.buzzvil.com/handbook/python-test) Date: unknown 이 문서는 파이썬 테스트 방법과 가이드라인을 제공한다. 이는 파이썬 테스트 추천 라이브러리와 활용 방법에 대한 내용을 포함한다. 해당 문서 내용은 추천 사항이며, 각 프로젝트에 따라 다른 방법을 적용하거나 제안할 수 있다. 다만 이 경우에도 프로젝트 내에서는 통일성을 갖는 것을 장려한다. ## 유닛 테스트 이 문서에서는 python 이 제공하는 기본 라이브러리인 [unittest](https://docs.python.org/3/library/unittest.html)를 사용해서 유닛 테스트를 구성한다. 더 효율적으로 테스트를 구성하기 위해 [pytest](https://docs.pytest.org)를 활용할 수 있다. 유닛 테스트 기본 가이드라인은 [테스트 기본 원칙](/handbook/test-principles#%EC%9C%A0%EB%8B%9B-%ED%85%8C%EC%8A%A4%ED%8A%B8) 문서를 참고한다. ### 테스트 레이아웃 프로젝트 최상위 폴더에 `tests` 폴더를 추가한 뒤 해당 폴더에 테스트 파일을 추가한다. 이 레이아웃을 통해 애플리케이션 코드와 테스트 코드를 분리할 수 있다. 한편 테스트 코드를 애플리케이션 코드의 일부로 배치하는 레이아웃도 가능하다. 프로젝트에 따라 각자 테스트 레이아웃을 선택하되 [Choosing a test layout / import rules](https://docs.pytest.org/en/latest/goodpractices.html#choosing-a-test-layout-import-rules) 문서를 참고하여 판단한다. ### 테스트 구성 `unittest.TestCase` 를 사용하여 테스트 클래스를 구성한다. 해당 클래스의 단정 메소드(assert methods)를 활용해서 검증하고자 하는 상태를 확인한다. ```python import unittest from unittest.mock import Mock from actual.package import some_method class TestClass(unittest.TestCase): def test_some_method(self): target = some_method() self.assertIsNotNone(target) self.assertEqual(target.value, 42) ``` ### 테스트 전/후처리 `unittest.TestCase` 의 `setUp` 를 통해서 테스트에 필요한 전 처리를 수행할 수 있다. 테스트 대상 객체 초기화나 테스트마다 재사용되는 디펜던시 객체 초기화 등이 이에 포함된다. ```python import unittest from unittest.mock import Mock from actual.package.repo import Repo class TestRepo(unittest.TestCase): def setUp(self): self.target = Target() def test_target_method(self): return_value = self.target.do_something() self.assertTrue(return_value) ``` 또한 `unittest.TestCase` 의 `tearDown` 메소드를 통해 테스트 도중 발생한 변경 사항을 초기화 할 수 있다. 예를 들어 불가피하게 데이터베이스 사용이 필요한 테스트의 경우 `tearDown` 메소드에서 생성된 데이터를 초기화할 수 있다. ### 디펜던시 모킹 테스트 대상이 가지고 있는 디펜던시는 모킹하여 사용한다. 디펜던시 객체의 구현에 따라 테스트 결과가 달라지지 않게 주의한다. #### repo.py ```python class Repo: def __init__(self, cache_data_source: DataSource, remote_data_source: DataSource): self.cache_data_source = cache_data_source self.remote_data_source = remote_data_source # ... ``` #### test_repo.py ```python import unittest from unittest.mock import Mock from actual.package.repo import Repo class TestRepo(unittest.TestCase): def test_some_method(self): cache_data_source = Mock() remote_data_source = Mock() repo = Repo(cache_data_source, remote_data_source) # ... ``` ### 테스트 픽스쳐(Fixture) 테스트에 필요한 리소스를 미리 구성하여 사용할 수 있다. TBD --- ## [레포지토리](https://tech.buzzvil.com/handbook/repository) Date: unknown ## 레포지토리 패턴(Repository Pattern) 레포지토리는 도메인 주도 설계(Domain Driven Design, DDD)의 일부 개념으로 소개되었다. 레포지토리는 데이터를 저장하고 불러오는 로직을 담당하는 객체이다. 도메인 레이어는 레포지토리를 통해서 데이터베이스에 저장된 데이터와 별개로 단순하게 추상화된 객체를 반환 받을 수 있다. 이 덕분에 레포지토리를 사용하는 측에서는 데이터베이스 연결 상태나 SQL 구문 등에 대한 걱정 없이 아이템을 추가, 수정, 삭제하는 작업을 할 수 있다. ### 팩토리(Factory)와의 차이점 팩토리는 메모리 내에 객체 생성을 담당한다. 반면 레포지토리는 기존 객체의 재구성에 가깝다. 팩토리에서 생성 로직을 통해 생성된 객체가 레포지토리를 통해 저장되고, 이후 필요한 시점에 재구성 된다고 볼 수 있다. ### 레포지토리 선언과 정의 레포지토리 인터페이스는 도메인 계층(Domain Layer)에 선언하고 레포지토리 구현은 데이터 계층(Data Layer)에 정의한다. 이를 통해 도메인 계층의 도메인 로직 테스트를 쉽게 수행할 수 있고, 각 계층 내의 변화가 다른 계층에 미치는 영향을 줄일 수 있다. ## 레포지토리 구성 및 사용 이 부분에서는 [Design the infrastructure persistence layer](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design) 문서에서 사용한 `Order Aggregate`를 예시로 사용한다. ![Order Aggregate](https://tech.buzzvil.com/handbook/repository-aggregate-database-table-relationships.png) ### 프로젝트 레이아웃(Project Layout) 프로젝트 레이아웃는 다음과 같이 구성한다. 다만 해당 내용은 가이드를 위한 예시일 뿐이며, 기능 중심 패키징(Package by Layer)와 클린 아키텍처(Clean Architecture) 의존성 규칙(Dependency Rule)을 유지하는 선에서 자유로이 변경할 수 있다. ```bash . └── order ├── entity.go ├── repo │   └── repo.go # Repository Implementation ├── repo.go # Repository Interface ├── usecase.go └── valueobject.go ``` ### 레포지토리 역할 데이터베이스는 레포지토리를 통해서만 업데이트 한다. 어그리게이트 루트(Aggregate Root) 별로 한 개의 레포지토리를 가지며, 레포지토리의 각 동작은 [단위 작업(Unit of Work)](https://martinfowler.com/eaaCatalog/unitOfWork.html) 기준으로 구성한다. 이 때 일반적으로 단위 작업은 하나의 트랜잭션(Transaction)을 기준으로 생각할 수 있다. 엔티티, 밸류 오브젝트의 변경 사항은 도메인 레이어에서 수행되며, 레포지토리를 포함하는 데이터 레이어에서는 해당 변경 사항을 적용하는 것에 집중한다. `order/entity.go` 안에 정의된 `Order` 엔티티는 어그리게이트 루트로서 존재하며, `Order`를 위한 레포지토리는 한 개만 존재한다. `Order`의 자식 엔티티인 `OrderItem`이나 밸류 오브젝트(Value Object)인 `Address`의 경우, 데이터베이스 테이블이 다를지라도 `Order` 레포지토리를 통해서 접근한다. ### 다중 데이터 소스(Multiple Data Source) 하나의 어그리게이트 루트를 사용하기 위해 여러 데이터 소스를 사용해야 할 경우가 존재한다. 레포지토리는 모델 주도 설계의 일부이며, 데이터베이스의 성격에 따라서 정의되지 않는다. 따라서 데이터베이스가 다중이라고 하더라도 그 때문에 레포지토리를 여러 개 정의하지는 않는다. 하나의 레포지토리에서 다중 데이터 소스를 사용하기 위해서 데이터 계층에 데이터 소스(Data Source)를 정의하여 사용할 수 있다. 레포지토리는 여러 데이터 소스로부터 받은 데이터를 조합하여 원하는 객체를 생성해야 한다. 예를 들어 `Order` 객체를 재생성 하기 위해 레포지토리 내부에서 MySQL 데이터와 Redis 데이터를 조합하는 로직이 존재할 수 있다. ### 객체 관계 매핑(Object Relational Mapping, ORM) 도구 관계형 데이터베이스를 사용할 때 객체 관계 매핑 도구를 사용할 수 있다. ORM 사용 시 테이블 매핑을 객체에 설정하는데, 종종 이 객체를 여러 모델에서 공통으로 접근해야 하기도 한다. 이 때 해당 매핑에 관련한 코드는 패키지 외부에서 독립적인 패키지에 정의하고, 레포지토리에서 해당 패키지를 사용하는 방식으로 구현 가능하다. ```bash . ├── rdb │   └── schema.go └── order ├── repo │   └── repo.go ... ``` `rdb/schema.go` 파일 안에 ORM 관련 객체를 정의한다. `order/repo/repo.go` 파일 안에서 ORM 과 `rdb` 패키지를 사용해서 데이터베이스에 접근한 뒤 해당 데이터를 활용해서 엔티티를 재구성 할 수 있다. #### schema.go ```go type Profile struct { ID int64 `gorm:"primary_key"` // ... } ``` #### repo.go ```go var p rdb.Profile if err := r.db.First(&p).Error; err != nil { return nil, err } ``` ## 참고 자료 - [Microsoft Infrastructure Persistence Layer Design](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design) - [Repository Pattern](https://deviq.com/repository-pattern/) - [Tactical Domain Driven Design](https://vaadin.com/learn/tutorials/ddd/tactical_domain_driven_design) - [도메인 주도 설계](http://book.interpark.com/product/BookDisplay.do?_method=detail&sc.prdNo=208559760&gclid=EAIaIQobChMIxcS1oMeq5gIVT3ZgCh01sgc8EAQYASABEgK1CvD_BwE) --- ## [테스트 기본 원칙](https://tech.buzzvil.com/handbook/test-principles) Date: unknown 잘 구성된 테스트는 더 빠르고 안정적으로 제품을 만들 수 있게 도와준다. 이 문서는 테스트 구성을 위한 기본 원칙과 가이드라인을 제공한다. ## 일곱 테스팅 원칙(Seven Testing Principles) 각 항목에 대한 자세한 내용은 [Seven Testing Principles](https://www.utest.com/articles/seven-testing-principles) 문서를 참고한다. - 테스팅은 결함의 존재를 보여주는 것이다. - 완벽한 테스트는 불가능하다. - 테스트 구성은 가능한 빠르게 시작한다. - 결함은 군집되어 있다. - 살충제 역설(Pesticide Paradox): 비슷한 테스트가 반복되면 새로운 결함을 발견할 수 없다. - 테스팅은 문맥에 의존적이다. - 오류 부재의 궤변: 사용되지 않는 시스템이나 사용자의 기대에 부응하지 않는 기능의 결함을 찾고 수정하는 것은 의미가 없다. ## F.I.R.S.T 원칙 유닛 테스트를 구성하기 위해서 F.I.R.S.T 원칙을 따른다. 각 항목에 대한 자세한 내용은 [F.I.R.S.T Principles](https://howtodoinjava.com/best-practices/first-principles-for-good-tests/) 문서를 참고한다. - Fast: 유닛 테스트는 빨라야 한다. - Isolated: 다른 테스트에 종속적인 테스트는 절대로 작성하지 않는다. - Repeatable: 테스트는 실행할 때마다 같은 결과를 만들어야 한다. - Self-validating: 테스트는 스스로 결과물이 옳은지 그른지 판단할 수 있어야 한다. - Timely: 유닛 테스트는 프로덕션 코드가 테스트를 성공하기 직전에 구성되어야 한다. 테스트 드리븐 개발(TDD) 방법론에 적합한 원칙이지만 실제로 적용되지 않는 경우도 있다. ## 유닛 테스트 유닛 테스트는 아래 기본 가이드라인 항목에 따라 작성한다. 그 외 자세한 유닛 테스트 가이드라인은 [JUnit Best Practices Guide](https://howtodoinjava.com/best-practices/unit-testing-best-practices-junit-reference-guide/) 문서를 통해 숙지한다. - 퍼블릭 메소드를 테스트한다. - 테스트 결과에 영향을 미치는 디펜던시 객체는 모킹한다. - 디스크 관련 디펜던시는 가능한 사용을 피한다. - 네트워크 관련 디펜던시는 사용하지 않는다. --- ## [모노리포 개발 가이드](https://tech.buzzvil.com/handbook/workingflow-in-monorepo) Date: unknown 본 문서는 클라이언트의 모노리포에서 작업을 함에 앞서 알아야 하는 규칙과 작업 흐름을 설명한다. ## 프로젝트 구성 모노리포란 하나의 리포지토리에서 여러 프로젝트를 함께 관리하는 것을 의미한다. 따라서 하나의 리포에는 많은 모듈이 포함되어 있으며 이들은 서로 [로컬 라이브러리 모듈](https://developer.android.com/studio/build/dependencies)로 참조를 하고 있다. 이에 대해 더 자세히 알고 싶으면 [Multirepo vs Monorepo](/handbook/multirepo-vs-monorepo) 문서를 참조한다. ### 레이어 구성 ![](https://tech.buzzvil.com/handbook/monorepo-dir-hierarchy.png) - 앱 레이어 - 일반적인 application / sdk 를 구성하는 모듈 계층입니다. Buzzscreen SDK, BuzzAd SDK 등이 포함됩니다. - 피쳐 레이어 - 유저 입장에서의 단위 기능을 제공하는 모듈 계층입니다. 앱 모듈에서 필요한 기능을 구현하기 위해 여러 피쳐 모듈을 조합할 수 있습니다. 비디오 기능 모듈, 만보기 기능 모듈 등이 포함됩니다. - 라이브러리 레이어 - 위 모듈에서 공통으로 사용되는 로직을 단위 기능으로 제공하는 모듈 계층입니다. auth, logger 등이 포함됩니다. ### 로컬 라이브러리 모듈 로컬 라이브러리 모듈이란 로컬 프로젝트 내에서 참조하여 사용하는 모듈을 의미한다. 모노리포 구조의 클라이언트에서는 여러 로컬 라이브러리 모듈들을 참조하여 사용하는데 다음과 같이 사용한다. ```gradle implementation project(":path") ``` 로컬 라이브러리 모듈의 경우 별도의 버전을 지칭하지 않고 현재의 코드를 직접 참조하여 개발을 수행한다. 그렇기 때문에 개발자는 로컬 라이브러리 모듈들에 대해서 버전 관리를 하지 않아도 된다는 착각을 할 수 있다. 하지만 SDK 개발을 하면서 이는 옳지 않은 말이며, 개발자는 각 로컬 라이브러리 모듈에 대해서 버전 관리 및 배포를 수행해줘야 한다. 그 이유는 앱과 SDK의 빌드 결과물이 다르기 때문인데 자세한 내용은 [문서](/handbook/android-multi-module-sdk)를 참조한다. 결론적으로 버즈빌의 SDK 개발을 위해서는 각 로컬 라이브러리 모듈에 대해서 버전 관리 및 배포를 수행해야 한다. ## 브랜치 모델 브랜치 모델이란 Git과 같은 소스 제어 도구에서 브랜치를 어떻게 관리하는지에 대하여 정의한 모델이다. 많이 알려진 모델로는 Git Flow, Trunk-Based Development 등이 있다. 클라이언트 모노리포에서는 항상 배포 가능한 올바른 상태의 소스를 유지하기 위해서 Trunk-Based Development 브랜칭 모델을 사용한다. ### Trunk-Based Development ![TBD Overview](https://trunkbaseddevelopment.com/trunk1c.png) Trunk-Based Development란 소스 제어 도구에서 브랜치를 어떻게 관리할지에 대한 전략이다. 개발자들은 마스터에서 협업하고, 긴 작업 단위의 브랜치 생성을 피한다. 그렇게 함으로서 협업 간 충돌을 최소화하고 빌드가 실패되는 상태를 막는다. 이 모델의 가장 주요한 특징은 트렁크라 불리는 **마스터 브랜치를 항상 배포 가능한 올바른 상태로 유지**하는 것이다. 이를 보장하기 위한 주요 규칙은 다음과 같다. #### 개발 규칙 - 개발은 직접 푸시 혹은 브랜치 푸시로 진행한다. - 직접 푸시 : 마스터에서 작은 단위의 개발을 진행 후 바로 푸시한다. - 브랜치 푸시 : 마스터에서 파생된 피쳐 브랜치를 생성한 후 PR을 통해 마스터로 푸시한다. - 버즈빌에서는 개발 중 직접 푸시를 사용하지 않고, **브랜치 푸시를 사용한다.** - 피쳐 브랜치는 가능한 [작은 작업 단위](https://trunkbaseddevelopment.com/short-lived-feature-branches/)를 가져야 한다. - 장기간 지속하여야 하는 피쳐 개발의 경우 [피쳐 플래그](https://trunkbaseddevelopment.com/feature-flags/)와 [인터페이스](https://trunkbaseddevelopment.com/branch-by-abstraction/)를 사용해 작업 단위를 나눈다. #### 배포 규칙 - 배포는 직접 배포 혹은 브랜치 배포로 진행한다. - 직접 배포 : 마스터에서 바로 배포를 수행한다. - 브랜치 배포 : 마스터에서 필요 시점에 [배포 브랜치](https://trunkbaseddevelopment.com/branch-for-release/)를 생성 후 배포를 수행한다. - 버즈빌에서는 필요에 따라 직접 배포와 브랜치 배포를 혼용하고 있다. 자세한 내용은 아래 버즈빌 배포를 참조한다. - 배포 브랜치에서 작업한 결과는 마스터로 머지하지 않는다. - 배포 브랜치에서 추가 작업이 필요할 경우 마스터에서 작업을 수행 후 해당 커밋을 체리픽하여 가져온다. 더 자세히 알아보고 싶다면 [Trunk-Based Development](https://trunkbaseddevelopment.com/) 문서를 참조한다. ## 버전 관리 ### 유의적 버전 버즈빌 클라이언트에서는 통일된 버전 정책을 갖기 위해 유의적 버전을 사용한다. 유의적 버전이란 제품에 버전을 어떻게 부여하고 관리해야 하는지에 대한 규칙이다. 이를 활용해 각 모듈 혹은 제품마다 일정한 규칙의 버전 정책을 확립하고, 협업에 있어 혼란이 생길 수 있는 부분을 제거한다. 유의적 버전은 다음과 같은 주요 원칙을 갖는다. - 버전은 크게 Major, Minor, Patch로 이루어진 3가지 버전을 갖는다. - Major : 하위 호환이 불가능하도록 API가 수정되었을 때 올린다. - Minor : 하위 호환을 지원하면서 기능을 추가했을 때 올린다. - Patch : 하위 호환을 지원하면서 버그를 수정했을 때 올린다. - Major.Minor.Patch 버전 이후에 pre-release 버전을 확장해서 사용할 수 있다. - 예시 : 1.0.1-rc.1 or 1.2.3-alpha.2 ... 더 자세히 알아보고 싶다면 [유의적 버전](https://semver.org/) 문서를 참조한다. ### 버즈빌 적용 사례 유의적 버전은 [Major, Minor, Patch]에 해당하는 핵심 버전 이후에 프리 릴리즈 코드 및 빌드 코드 등을 자유롭게 확장하여 사용할 수 있다. 버즈빌에서는 유의적 버전의 [규칙](https://semver.org/#semantic-versioning-specification-semver)을 따르는 모든 버전을 허용하지만, 주로 다음과 같은 규칙을 이용해서 제품의 버전을 관리하고 있다. ![buzzvil version sample](https://tech.buzzvil.com/handbook/buzzvil-version-sample.png) - [Major].[Minor].[Patch]-[Pre Release Code].[Pre Release Version] - Major : 메이져 버전 - Minor : 마이너 버전 - Patch : 패치 버전 - 확장 버전 - Pre Release Code : 프리 릴리즈 코드 - 현재는 rc만 사용. alpha, beta 등 다른 키워드 역시 필요하다면 논의 후 사용 가능 - Pre Release Version : 프리 릴리즈 코드 버전 이때 프리 릴리즈 코드를 활용한 정보 기록은 금지한다. 예를 들어 A/B 테스트를 위해 배포할 버전이라는 의미로 1.0.0-abtest.rc.1과 같은 버전을 주입하는 것을 금지한다. 이는 프리 릴리즈 코드의 목적에 맞지 않고 버전의 우선순위 비교 규칙상 사전식으로 버전 비교를 수행하기 때문에 버전 순서에 혼란을 줄 수 있다. 만약 이처럼 특정 버전의 시점에 의미를 기록하고 싶다면 Git의 tag 기능을 활용하는 것으로 한다. ### 버전 변경 시점 버전을 어떤 규칙으로 구성할 것인지도 중요하지만, 언제 버전을 올려야 하는지도 버전 관리의 중요한 한 부분이다. 버즈빌은 브랜치 모델로 Trunk-Based Development를 사용하고 있고, 그에 해당하는 버전 관리 규칙을 따르고 있다. 하지만 이 룰을 모든 모듈에 엄격히 적용할 경우 관리의 복잡성이 과도하게 늘어나기 때문에, 모듈의 상황에 따라 예외 규칙을 허용하여 버전 변경을 수행한다. 이 섹션에서는 Trunk-Based Development에서는 어느 시점에 버전을 올려야 하고, 버즈빌에서는 어떻게 규칙을 수정하여 버전을 올려야 하는지 정의한다. #### Trunk-Based Development ![git version validation](https://tech.buzzvil.com/handbook/client_git_version_validation.png) Trunk-Based Development에서는 모든 상태가 배포가 가능한 상태임을 보장해야 한다. 그러기 위해서는 모든 마스터의 커밋이 올바른 버전 상태를 가져야 함을 의미한다. 때문에 개발자는 마스터로 향하는 모든 푸시 및 PR에서 변화가 있는 모듈의 버전을 유의적 버전 규칙에 맞게 올려줘야 한다. 하지만 이처럼 모든 커밋에서 버전을 올려주는 것은 개발 과정에서 개발자가 신경 써야 하는 부분이 늘어나게 되는 문제가 있고, CI를 통해 자동화를 구축하여도 절차의 복잡도는 올라갈 수 밖에 없다. #### 버즈빌 정책 버즈빌에서는 Trunk-Based Development의 규칙을 간소화하기 위해서 커밋 시점마다 버전을 올리지 않고 `배포되는 시점`에 모듈의 버전을 올려준다. 이때 이야기하는 `배포되는 시점`은 `제품에 배포 태그를 다는 시점`을 의미한다. 단, 주의할 점은 모듈을 배포할 때 자기 자신의 버전을 올려줄 뿐만 아니라, 자신이 참조하는 로컬 라이브러리 모듈 중에서 변화가 있는 모듈 역시 버전을 올려줘야 한다는 점이다. 이러한 작업은 모듈이 많아질수록 사람이 수작업으로 하기 힘든 부분이다. 때문에 버즈빌에서는 그래들 플러그인을 개발하여 배포를 자동으로 수행한다. ### 핫픽스 대응 브랜치 모델과 유의적 버전 규칙을 잘 따른다면, 배포가 일어난 이후 모듈이 핫픽스가 이루어져도 기능 변화는 없어야 하고 패치 버전만 올라가야 한다. 하지만 실제 SDK 사업을 운영하다 보면 핫픽스를 통해서 기능 변화를 주어야만 하는 경우를 마주하게 된다. 그렇게 되면 기존의 개발 규칙을 어기게 되는 것이고 CI를 통해 올바른 버전 상태임을 검증할 수 없게 된다. 위와 같은 문제를 해결하기 위해 다음과 같은 규칙을 정의한다. #### 첫째, 핫픽스는 CI에서 버전 검증을 해주지 않는다. 다양한 변수가 있을 수 있기 때문에 작업자의 판단과 강도 높은 테스트를 통해서 안정성을 검증한다. #### 둘째, 핫픽스에서 로컬 라이브러리 모듈의 수정을 피한다. 핫픽스에서 모듈의 수정이 일어나면 기존의 모듈과 다른 새로운 버전의 코드가 생성되는 것이다. 이 때 배포 모듈의 경우 기본적으로 새로운 버전으로 배포가 되지만 로컬 라이브러리 모듈의 경우 별도로 새로운 버전으로 세팅하여 배포를 해야 한다. 따라서 모듈의 수정이 필요할 경우 가능한 마스터 브랜치에서 작업 및 배포 후 리모트 참조를 하는 방법으로 코드의 수정을 피한다. #### 셋째, 핫픽스에서 수정하는 모듈은 핫픽스 버전으로 배포한다. 만약 핫픽스에서 꼭 로컬 라이브러리 모듈의 수정이 일어나서 다시 배포해야 할 경우에는, 마스터에서 사용하던 버전과의 충돌을 방지하기 위하여 프리 릴리즈 코드를 hotfix로 해서 배포한다. (예시 : 1.0.1-hotfix.1) ## 배포 규칙 모노리포에서 어떤 규칙을 갖고 배포를 할지 정의한다. ### 브랜치 모델과 배포 클라이언트의 모노리포에서는 브랜치 모델로 Trunk-Based Development를 사용하고 있다. 때문에 배포하는 방법은 두 가지 경우로 나뉠 수 있다. - 배포 브랜치 - 마이너 버전을 기준으로 배포 브랜치를 생성 - 이후 유지보수 작업은 마스터에서 진행 후 배포 브랜치로 체리픽 후 패치 버전 증가 - 직접 배포 - 마스터에서 원하는 커밋에서 바로 배포 ### 버즈빌 배포 현재 버즈빌에서는 특별한 경우가 아니면 배포 브랜치를 만들지 않고 마스터에서 태깅한 이후 바로 배포를 수행하고 있다. 이렇게 할 경우 다음 문제를 고려해야 한다. ![SDK version issue when released from master](https://tech.buzzvil.com/handbook/sdk-release-diff.png) 위와 같이 마스터에서 배포를 수행하는 도중에 기능 변화가 적용된 커밋이 있다면, SDK의 마이너 버전이 증가한 것이기 때문에 패치 버전의 증가만으로는 배포를 수행할 수 없다. 하지만 이를 해결하기 위해 위와 같이 모든 경우에 배포 브랜치를 생성하여 체리픽으로 관리를 하게 되면, 배포 시스템이 너무 복잡해지기 때문에 버즈빌에서는 다음과 같은 규칙으로 배포를 진행한다. ![Buzzvil release policy](https://tech.buzzvil.com/handbook/buzzvil-release-policy.png) - 기본적으로 배포는 마스터에서 태깅 후 직접 배포한다. - 기능 변화가 중간에 있었다 하더라도 작업자의 판단에 따라 마이너 버전을 올리지 않고 배포한다. - 작업자가 판단할 때 포함되면 안 되는 변화가 있다면 이전 배포 태그에서 브랜치를 생성하여 배포한다. ## FAQ - Q) 버전 관리를 안드로이드의 `versionName` 을 기준으로 수행하는데 `versionCode` 로 하면 안 되는가? - A) CI에서 버전의 증가를 정확히 검증하기 힘들기 때문에 힘들다. versionCode는 정수 기반으로 1씩 증가하는 룰을 갖고 있는데 핫픽스 브랜치에서 새로 배포를 해야 할 경우 versionCode가 별도로 증가 할 수 있다. 이 경우 마스터의 헤드에서 versionCode가 3이었을 경우 다음 커밋의 versionCode는 4일 수도 아닐 수도 있다. 따라서 버전에 대한 검증을 정확히 하기 위해서 정수기반의 versionCode가 아닌 시맨틱 버전이 적용된 versionName을 기준으로 버전 관리를 수행한다. ## Reference - https://trunkbaseddevelopment.com/ - https://semver.org/