code_review_thumbnail.webp

누구나 그럴싸한 리뷰 계획을 가지고 있다. PR이 쌓이기 전까지는.

안녕하세요, 헤렌 개발파트 웹프론트셀에서 Web Front-end 개발을 담당하고 있는 테디라고 합니다 🧸

제가 1년 넘게 몸담고 있는 헤렌은 빠른 속도로 프로덕트를 개발해 왔습니다. 점점 개발 조직이 Scale-up 되면서 저와 함께 공비서 서비스를 개발하는 웹프론트셀 구성원 규모도 제가 합류했던 시점과 비교했을 때 2배 늘어났어요. 구성원이 늘어나면서 다양한 기능을 동시에 병렬적으로 개발하기도, 규모가 큰 기능을 도메인별로 작게 쪼개어 여러 구성원이 함께 개발하는 날들이 많아지게 되었습니다.

헤렌 웹프론트셀은 main, develop 브랜치를 포함해 feature branch 에 변경 사항을 적용할 때 동료 구성원의 코드 리뷰가 완료되어 Approve를 받아야 하는 규칙을 운영하고 있는데요. 구성원이 적을 때에는 서로 대략 어떤 일감을 진행하고 있는지 알고 있었기 때문에 코드 리뷰에 시간이 별로 소요되지 않았습니다.

하지만, 구성원이 많아지면서 PR이 쌓이는 속도가 점점 빨라지기 시작했습니다. 동시에 여러 기능을 개발하다 보니 동료 구성원이 개발하고 있는 기능의 요구 사항이나 비즈니스적인 성격을 PR을 리뷰하면서 처음 접하는 케이스가 많아졌어요. 코드는 결국 현실 세계의 문제를 프로그래밍 언어로 풀어낸 결과물인데, 현실 세계의 문제가 무엇인지 알지 못한 채 PR을 마주하게 되니 자연스럽게 리뷰어들이 리뷰하는 데 걸리는 시간과 에너지도 따라서 증가하게 되었습니다 🥲

pr_slack_1_blur.JPEG

pr_slack_2_blur.JPEG

매일매일 쌓여가는 PR과 함께 몰려 오는 불안감

쌓여 가는 PR들과 PR Approve가 있어야 부모 Branch에 코드 반영이 가능한 Git Branch 전략은 웹프론트셀에게 있어 선택의 갈림길에 서도록 만들었습니다. 그저 쭉 훑어 봤을 때 문제가 없어 보이면 동료를 믿고 Approve를 제출하는 그저 통과 의례 같은 코드 리뷰를 할 지, 구성원 모두가 함께 비즈니스 요구 사항을 기반으로 코드 위에서 신이 나게 토론하고 피드백하며 프로덕트의 품질을 보장하는 코드 리뷰를 하는 대신 막대한 에너지와 시간을 투자할지 말이죠.

모두 개발과 프로덕트에 진심인 만큼, 두 번째 방법을 따르면서 동시에 코드 리뷰가 전체적인 프로덕트 개발에 병목이 되지 않도록, 아래의 사진처럼 코드 리뷰만 기다리다가 아까운 시간을 낭비하지 않을 방안을 찾기 시작합니다 🔥

이번 코드 리뷰 개선 프로세스를 진행하면서, 뱅크샐러드 기술 블로그우아한형제들 기술 블로그에서 많은 아이디어를 얻을 수 있었습니다. 이 자리를 빌려 감사하다는 말씀 드립니다 🙇‍♂️

Untitled

Step #1 작게, 밀도 있게, 빠르게, 코드 리뷰 프로세스의 변화

PR 리뷰에 점점 많은 시간과 에너지가 필요하게 되고, 이 때문에 병목이 발생하게 된 원인은 앞서 언급한 사례를 포함해 다음과 같이 추려볼 수 있습니다.

  1. PR이 어떠한 요구사항을 해결하기 위해 어떠한 변경사항을 담고 있는 것인지 Reviewer가 이해하고 리뷰하는 과정에 시간과 에너지가 많이 소모된다.
  2. 일부 PR의 경우, 변경 사항의 사이즈가 커서 리뷰하는 데 있어 확인해야 하는 범위가 넓고 부담으로 다가온다.
  3. Review를 언제까지 완료해야 한다는 데드라인이 없다 보니, 기능 개발이 바쁠 경우 자연스럽게 후순위로 밀리게 된다.

방대한 양의 변경된 코드는 세상을 아름답게 바라보도록 만들어 줍니다

방대한 양의 변경된 코드는 세상을 아름답게 바라보도록 만들어 줍니다

이러한 3가지 요인을 해소하는 것을 코드 리뷰 프로세스 개선에 있어 핵심적인 가치로 판단했습니다. 개선 과정에 있어 가장 중요한 포인트는, Reviewer에게 있어 코드 리뷰는 메인 업무가 아니라는 점입니다. 앞으로 나올 변화들은 모두 이러한 포인트를 기반으로 **“Reviewer가 적은 노력으로도 밀도 있고 빠르게 코드 리뷰를 할 수 있도록 도와주는 것“**에 초점을 두고 있습니다.

PR Template 개선

Assginee는 Reviewer가 PR을 리뷰하는 데 있어 적은 노력만으로도 빠르게 나와 같은 문제 상황에 빠져들어 현실 세계의 문제를 바라보고, 내가 제시한 해답에 대해 함께 고민해 줄 수 있도록 도와주어야 합니다. 따라서 단순히 내가 PR에서 코드적으로 어떠한 변화를 일으켰는지만 적는 것은 Reviewer에게 최선의 피드백을 끌어내기 힘든 결과를 낳을 확률이 높습니다.

내가 개발한 기능과 요구사항을 처음 접하더라도, 내가 어떠한 문제를 해결하기 위해 이러한 코드를 작성하게 되었는지 Assignee의 시간과 리소스를 투자해 최대한 자세하게 설명해 주는 것이 좋아요. Reviewer가 파악하고 있는 정보는 Assignee가 코드 구현을 위해 문제를 바라보고 탐구하며 얻은 정보보다 훨씬 적기 때문입니다. 따라서 구현된 코드에 대한 전체적인 문맥을 Reviewer가 적은 노력으로도 충분히 비슷한 수준으로 파악할 수 있도록 자세하게 설명해 주어야 합니다.

개선된 PR Template은 기술적인 변경점 뿐만 아니라, 어떤 요구사항을 해결하기 위해 이러한 변화가 발생하게 되었고 Assginee가 개발하면서 애매하다고 느꼈거나 Reviewer 들이 집중해서 리뷰 해 주면 좋을 것 같은 점들을 적을 수 있도록 구성했습니다. 이를 통해 PR을 Open 한 후 Slack이나 대면 커뮤니케이션 등을 통해 추가로 설명하는 일을 최소화하고, 웹프론트셀 구성원 모두가 일관된 양식으로 PR 본문을 작성할 수 있도록 유도했습니다.

## 🔍️ 이 PR을 통해 해결하려는 문제가 무엇인가요?

>어떤 기능을 구현한건지, 이슈 대응이라면 어떤 이슈인지 PR이 열리게 된 계기와 목적을 Reviewer 들이 쉽게 이해할 수 있도록 적어 주세요
>일감 백로그 링크나 다이어그램, 피그마를 첨부해도 좋아요

* 

## ✨ 이 PR에서 핵심적으로 변경된 사항은 무엇일까요?
> 문제를 해결하면서 주요하게 변경된 사항들을 적어 주세요
*

## 🔖 핵심 변경 사항 외에 추가적으로 변경된 부분이 있나요?
> 없으면 "없음" 이라고 기재해 주세요
*

## 🙏 Reviewer 분들이 이런 부분을 신경써서 봐 주시면 좋겠어요
> 개발 과정에서 다른 분들의 의견은 어떠한지 궁금했거나 크로스 체크가 필요하다고 느껴진 코드가 있다면 남겨주세요
*

## 🩺 이 PR에서 테스트 혹은 검증이 필요한 부분이 있을까요?
> 테스트가 필요한 항목이나 테스트 코드가 추가되었다면 함께 적어주세요
*

### 📌 PR 진행 시 이러한 점들을 참고해 주세요
* Reviewer 분들은 코드 리뷰 시 좋은 코드의 방향을 제시하되, 코드 수정을 강제하지 말아 주세요.
* Reviewer 분들은 좋은 코드를 발견한 경우, 칭찬과 격려를 아끼지 말아 주세요.
* Review는 특수한 케이스가 아니면 Reviewer로 지정된 시점 기준으로 3일 이내에 진행해 주세요.
* Comment 작성 시 Prefix로 P1, P2, P3 를 적어 주시면 Assignee가 보다 명확하게 Comment에 대해 대응할 수 있어요
    * P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
    * P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
    * P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)
#

---
## 📝 Assignee를 위한 CheckList
- [ ] To-Do Item

이렇게 개선된 PR Template을 활용해 웹프론트셀 구성원들은 함께 기능을 개발하는 이해 관계자가 아니어도, 어떤 요구 사항을 해결하기 위해 어떠한 변화가 있었고 어떤 부분을 신경써서 봐야 할 지에 대해 적은 노력만 기울여도 한 눈에 파악할 수 있게 되었습니다.

pr_conversation_blur.jpg

Pn 룰 정의

Pn 룰은 상단의 PR Template에 적혀 있는 것처럼, Reviewer가 피드백을 남길 때 Assignee에게 얼마나 해당 피드백에 대해 강조하고 싶은 지 표현하기 위한 규칙입니다.

아래의 사진은 Pn 룰을 활용해 동작 상 오류는 없지만, 통일성 있는 코드 스타일을 유지하고 재사용성을 증가시킬 수 있을 것 같은 아이디어에 대해 텍스트 기반으로 이루어지고 있는 커뮤니케이션입니다.

pr_conversation_blur_2.JPEG

PR Label 세팅 (D-n 룰)

PR이 어떤 상태인지 List 페이지부터 한눈에 알아볼 수 있도록 Label도 다양하게 세팅했습니다. 곧 등장할 “PR 리뷰 중인 공그리” Bot의 도움으로 D-n Label은 매일 자동으로 세팅되었고, StatusType Label은 해당 PR의 Assignee가 프로젝트 구성원들에게 해당 PR에 대해 간략하게 설명할 수 있는 도구로 활용할 수 있도록 자율적으로 세팅하도록 구성했습니다.

Untitled

Step #2 개선된 코드 리뷰 프로세스에 기술 한 스푼 투하

코드 리뷰 프로세스와 관련해 다 함께 리뷰를 집중적으로 진행하는 시간부터 효율적인 텍스트 기반 비동기 커뮤니케이션을 도와줄 다양한 수단을 마련했습니다. 하지만, 이런 일련의 규칙과 약속만으로는 해결할 수 없는 문제가 있습니다. 바로, 반복적인 수작업 입니다.

D-n 룰은 Assignee가 셀 구성원들에게 있어 리뷰를 특정 기한까지는 완료해 달라고 어필할 수 있는 수단입니다. 하지만, 매일 아침마다 Github Pull Request 페이지에 진입해 하루 줄어든 기한의 D-n Label로 변경하는 건 또 하나의 병목이 발생하는 지점입니다. 코드 리뷰가 업무의 집중도에 악영향을 미치게 되는 거죠.

개발자라는 직업의 장점은 일상의 문제를 기술로써 해결할 수 있다는 겁니다. 이러한 반복적인 수작업은 기술로써 해결하기 가장 좋은 타깃이고, Github과 같은 많은 개발자가 애용하는 플랫폼은 분명 연동할 수 있는 API를 제공할 거라고 판단했습니다. 예상대로 요구사항에 들어맞는 라이브러리를 발견하게 되고, 구현하려는 순간 아이디어 하나가 스쳐 지나갔어요 ⚡️

<aside> 🤔 라이브러리를 활용해서 Label을 자동으로 변경하는 김에, 매일 아침마다 Slack 코드 리뷰 채널에 리뷰가 필요한 PR들을 메시지로 리마인드 해 주면 리뷰가 보다 빨리 끝나지 않을까?

</aside>

평소에 Slack Bot을 개발해 보고 싶었던 저는 바로 Pull Request Reminder Bot 개발로 목표를 새롭게 설정하고 바로 실행으로 옮깁니다.

우선, Python으로 Github REST API를 손쉽게 활용할 수 있는 PyGithub 라이브러리를 설치합니다. PyGithub 라이브러리를 활용해 우리는 원하는 Repository의 다양한 데이터를 얻을 수 있습니다. 해당 Repo에 열려 있는 Open 상태의 PR 리스트, Repo에 세팅되어 있는 label 들이 해당하죠.

GitHub - PyGithub/PyGithub: Typed interactions with the GitHub API v3

PyGithub 라이브러리에서 제공하는 Github Class에 Access Token을 넘겨 instance를 생성합니다. 해당 instance를 활용해 우리는 우리가 원하는 Repository에 접근해 우리가 필요로 하는 데이터들을 가져옵니다. label의 경우, 다음과 같이 간단하게 Github Class가 제공하는 API를 활용하면 됩니다.

g = Github(PRIVATE_ACCESS_TOKEN)
repo = g.get_repo(TARGET_GITHUB_REPO)
labels = repo.get_labels()

PR List 데이터를 얻기 위해선 일련의 과정이 필요합니다. 우리가 Pull Request Reminder Bot을 통해 알고 싶은 정보는 “Open 상태의” 리뷰를 필요로 하는 PR들이기 때문입니다. get_pulls API는 arguments 없이 쓰면 모든 상태의 PR List를 불러오기 때문에, 필터링 조건을 명시해서 넘겨 줍니다.

def _get_total_pull_requests():
    count = 0
    pull_requests_list = []

    # 현재 열려있는 PR 목록들을 가져온다.
    for pull in repo.get_pulls(
        state="open",
        sort="updated",
    ):
        pull_requests_list.append(pull)
        count += 1
    return count, pull_requests_list

_get_total_pull_request 메소드의 첫 번째 return 값인 count의 value가 0보다 클 경우, 두 번째 return 값인 pulls list를 for 문을 통해 순회하면서 1) D-n 라벨을 decreased 된 값으로 업데이트하고 2) Slack의 PR 리뷰 중인 공그리에게 전송할 메시지를 구성하는 로직을 수행하게 됩니다.

count, pulls = _get_total_pull_requests()
if count > 0:
    for pull in pulls:
        pr_link = _make_pr_link_with_no(pull.number)

        dday_label = list(filter(lambda x: x.name.startswith('D-') or x.name == "OverDue", pull.labels))

        before_label = dday_label[0].name if len(dday_label) > 0 else ''
        after_label = _get_decreased_label(before_label)

        _set_changed_label(pull, before_label, after_label)
        pr_msg_to_slack += _get_pr_msg_to_slack(pr_link, after_label, pull.title)

이런 로직들을 거쳐 PR 리뷰 중인 공그리 Bot은 매주 평일 아침 9시마다 Target RepositoryOpen 상태로 남아 있는 PR이 있는지, 있다면 PR 리뷰 마감까지 얼마나 남았는지 코드 리뷰 채널을 통해 구성원들에게 알려주고 있습니다 🔥

✅ 리뷰를 기다리고 있는 PR이 존재할 때

34216C8E-E0F2-46D4-9DCB-000BE99408C5.JPEG

❌ 리뷰를 기다리고 있는 PR이 남아있지 않을 때

Untitled

Untitled

PR 리뷰 중인 공그리는 가끔 이런 사소한 감동을 선사해 주었답니다

Review on #코드_리뷰_문화를_리뷰해_봐요

코드 리뷰 문화를 리뷰하고 개선한 지 어느덧 5~6개월이 지났습니다. 산더미같이 쌓인 PR에 Reviewer는 기겁하고 Assignee는 Slack으로 코드 리뷰를 리마인드 하며 하염없이 기다리고, PR이 Open 된 Github이 아닌 Slack, 대면 커뮤니케이션 등 소통 채널이 일원화되지 않던 과거의 모습들은 최근에 와서 많이 사라졌다고 생각됩니다.

6개월에 가까운 시간 동안 서로 다른 프로젝트를 진행하면서 끊임없이 변화된 코드 리뷰 프로세스로 코드 리뷰를 진행하면서 익숙해진 결과, 이제는 서로 다른 기능을 개발하고 있더라도 빠르게 요구 사항에 대해 파악해 에너지를 적게 소모하고 PR이 작고 명확한 단위로 자주 열리게 되어 빠르게 해결되고 있는 편이에요.

지금 이 순간에도 코드 리뷰와 관련해 개선해 보면 좋을 부분들에 대해 끊임없이 토론하고 변화에 대해 실험해 보고 있고, 이러한 순간들이 모여 전체적으로 더 나은 개발 문화와 프로덕트를 만들어 나갈 것이라고 기대합니다:D


참고

공통시스템개발팀 코드 리뷰 문화 개선 이야기 | 우아한형제들 기술블로그

코드 리뷰 in 뱅크샐러드 개발 문화 | 뱅크샐러드


저희와 함께 하고 싶은 분들은 헤렌의 채용 담당자 에게 커피챗을 요청해 보세요! 헤렌은 현재 다양한 개발 직군을 적극적으로 채용하고 있습니다 🚀

헤렌 채용 │ HERREN CAREERS

https://hits.seeyoufarm.com/api/count/incr/badge.svg?pvs=4&url=https://www.notion.so/0909/PR-Reminder-Bot-c7f5e7bd5f1d4bf889f269fa7482a894&count_bg=#79C83D&title_bg=#555555&icon=&icon_color=#E7E7E7&title=hits&edge_flat=false