안녕하세요. B2C랩유닛 APPS실 커머스개발팀 백엔드파트 손수정입니다.

이 글은 웹서비스팀 개발문화 만들기 를 먼저 읽는걸 권장합니다. 😊

저희 팀은 AWS CodeCommit 을 이용하여 형상관리를 하고 있습니다. 그리고 CodeCommit 의 Pull Request(이하 pr) 를 이용하여 코드 리뷰를 진행하고 있습니다.

PR 생성, 업데이트, 병합, 닫힘, 코멘트 생성의 이벤트에 대한 알림을 슬랙을 통해 받고 있었습니다. (위에 링크한 글의 Slack Bot + Lambda + CloudWatch Event을 통한 알림을 사용합니다.) 팀원이 많아지고, PR 문화가 활성화되면서 기존에는 느끼지 못했던 불편함을 느끼기 시작했습니다. 리뷰를 받기 위해 대기하는 시간이 늘어나고, 리뷰를 위해 들여야 하는 시간도 늘어나면서 팀원들의 피로감이 증가하고 있었습니다. 그래서, 코드 리뷰를 더 효율적으로 하기 위해 작은 것부터 편리하게 만든 기록을 공유합니다.

athena

AWS CodeCommit 은 다른 형상 관리 툴보다 편의적인 기능 제공에 다소 부족함이 있습니다.

  • 외부 툴 integration 이 쉽지 않다
  • 파일의 소스 길이가 너무 길면 볼 수 없다
  • 메서드 간 이동이 되지 않는다
  • 파일별 변경 점을 찾아다니며 봐야 한다

좋은 툴을 사용하는 것도 방법이지만, 현재 사용하고 있는 툴을 이관하는 것은 많은 시간이 필요한 작업입니다. 당장 팀원들의 효율을 높이기 위해 AWS CodeCommit 을 이용할 때의 불편한 점들을 해결하기로 했습니다.


현재의 문제점은?

  1. pr 을 생성하고, 생성 알림은 오지만 해당 알림의 스레드로 리뷰를 직접 요청해야 한다.
athena

2. pr에 코멘트가 생성되면 코멘트 내용을 보기 위해서는 직접 console 에 들어가서 확인해야 한다.

athena

3. 리뷰어가 현재 리뷰 상태인지 알 수 없다. (코멘트를 달거나 승인을 하기 전까지)


문제점 해결하기

  1. pr을 생성하고 스레드로 직접 멘션을 걸던 작업을 자동화하면 어떨까? 라고 생각했습니다.
athena

pr이 생성되면, 스레드로 작성자에게 리뷰를 요청하도록 했습니다. 요청하기 버튼을 클릭하면 리뷰어를 지정할 수 있는 모달이 생성됩니다.

athena

슬랙 app 의 modal 기능을 사용하여, 리뷰어를 지정할 수 있게 하였습니다.

blocks = {
	"title": {
	    "type": "plain_text",
	    "text": "리뷰어를 지정해주세요"
	},
	"submit": {
	    "type": "plain_text",
	    "text": "요청하기"
	},
	"blocks": [
	    {
	        "type": "input",
	        "element": {
	            "type": "users_select",
	            "action_id": "user-1",
	            "placeholder": {
	                "type": "plain_text",
	                "text": "Select a user"
	            },
	        },
	        "label": {
	            "type": "plain_text",
	            "text": "첫번째 리뷰어를 지정해주세요"
	        }
	    },
	    {
	        "type": "input",
	        "element": {
	            "type": "users_select",
	            "action_id": "user-2",
	            "placeholder": {
	                "type": "plain_text",
	                "text": "Select a user"
	            },
	        },
	        "label": {
	            "type": "plain_text",
	            "text": "두번째 리뷰어를 지정해주세요"
	        }
	    },
	],
	"type": "modal"
}

슬랙의 modal 은 message 에 사용하는 modal 과 비슷한 block입니다. (슬랙의 block kit 생성 기능으로 생성 가능합니다.) type 만 modal 로 지정해주면 됩니다. 슬랙의 views_open 기능을 이용해서 모달을 띄울 수 있습니다.

call_result = slack_client.views_open(
    trigger_id=trigger_id, # 슬랙 action 에서 전달받은 trigger_id
    view=json.dumps(blocks) # 위에서 생성한 block
)

이때의 trigger_id 는 위에서 리뷰 요청하기 버튼 클릭 시 슬랙에서 전달받은 trigger_id 입니다. (button 액션 시 받은 body 값에 있습니다.) 슬랙의 modal 은 trigger_id 가 유효하지 않으면 정상 작동하지 않습니다.
저희 팀에서는 한사람에게 리뷰가 치우치는 것을 방지하기 위해 스크립트를 통해 랜덤으로 리뷰어를 지정하고 있었습니다. 또한 팀원들이 하는 작업이 스쿼드 별로 다르기 때문에, 필수 리뷰어가 달라야 했습니다. 형평성 있는 분배와 작업 별 기본 리뷰어를 디폴트로 지정하는 일, 두 가지 기능을 추가하기로 했습니다!

athena

지정된 리뷰어를 리뷰어 지정 모달에서 초깃값으로 설정하기 위해서는 initial_user 옵션을 사용하면 됩니다.

blocks = {
    "title": {
        "type": "plain_text",
        "text": "리뷰어를 지정해주세요"
    },
    "submit": {
        "type": "plain_text",
        "text": "요청하기"
    },
    "blocks": [
        {
            "type": "input",
            "element": {
                "type": "users_select",
                "action_id": "user-1",
                "placeholder": {
                    "type": "plain_text",
                    "text": "Select a user"
                },
            },
            "label": {
                "type": "plain_text",
                "text": "첫번째 리뷰어를 지정해주세요"
            }
        },
        {
            "type": "input",
            "element": {
                "type": "users_select",
                "action_id": "user-2",
                "placeholder": {
                    "type": "plain_text",
                    "text": "Select a user"
                },
            },
            "label": {
                "type": "plain_text",
                "text": "두번째 리뷰어를 지정해주세요"
            }
        },
    ],
    "type": "modal"
}

위처럼 blocks 를 설정하면 초깃값 지정 없이 모달이 생성됩니다.

blocks["blocks"][0]["element"]["initial_user"] = "user" # 슬랙의 USER ID

위처럼 각 element 에 initial_user 를 지정해주면, 아래처럼 user select box 에 초깃값을 설정할 수 있습니다.

athena

pr 을 생성하면, 리뷰어 지정 스크립트를 실행하고, 리뷰어를 지정해서 알림을 보냅니다. 요청하기 버튼을 클릭하면 지정된 리뷰어가 보이고, 물론 리뷰어를 변경할 수 있습니다.

2. pr 에 작성된 코멘트 내용을 보이게 하자.

{
	'version': '0', 
	'id': '4**1', 
	'detail-type': 'CodeCommit Comment on Pull Request', 
	'source': 'aws.codecommit',
	'account': '*', 
	'time': '2022-05-04T08:39:47Z', 
	'region': 'ap-northeast-2', 
	'resources': ['arn:aws:codecommit:ap-northeast-2:*:brandi-api'], 
	'detail': {
	  'afterCommitId': 'b*e', 
	  'beforeCommitId': 'f*2', 
	  'callerUserArn': 'arn:aws:iam::*:user/sohnsj', 
	  'commentId': 'e*a', 
	  'event': 'commentOnPullRequestCreated', 
	  'inReplyTo': 'e*7', 
	  'notificationBody': 'A pull request event occurred in the following AWS CodeCommit repository: brandi-api. The user: arn:aws:iam::*:user/s** made a comment or replied to a comment. The comment was made on the following Pull Request: 4213. For more information, go to the AWS CodeCommit console, 
	  'pullRequestId': '4213', 
	  'repositoryId': '9**', 
	  'repositoryName': 'brandi-api'
	}
}

CloudWatch 로 부터 받는 event 정보인데, 한계가 있어 코멘트 내용까지는 받지 못했었습니다. 하지만, pullRequestId 와 commentId 는 알 수 있으니, CodeCommit 에 요청을 보내 코멘트 내용을 가져올 수 있었습니다.

# event 는 cloudwatch 로 부터 전달받은 event
client = boto3.client('codecommit')

event_detail_info = event['detail']
comment_response = client.get_comment(commentId=event_detail_info['commentId'])

if comment_response.get('comment'):
    comment_content = comment_response['comment']['content']

aws codecommit 에서 get_comment 메서드를 이용하여서 comment 내용을 가져올 수 있습니다!

athena

코멘트 내용이 보이니, pr 작성자가 아닌 사람도 코멘트 내용을 볼 수 있었고, 좋은 피드백은 모두 공감하여 적용할 수 있었습니다.

athena

이렇게, PR 작성자와 코멘트 작성자 외에 모든 팀원이 코멘트를 확인하고, 의견을 공유할 수 있게 됐습니다. 🎉

cloudwatch event 에서 이 코멘트가 다른 코멘트의 reply 코멘트인지 여부도 확인할 수 있었습니다. inReplyTo 키로 기존 코멘트의 id를 전달해주고 있어, 그 id 로 기존 코멘트 내용도 조회하도록 했습니다.

if event_detail_info.get('inReplyTo'):
  is_reply = True
  comment_response = client.get_comment(commentId=event_detail_info['inReplyTo'])
  if comment_response.get('comment'):
      reply_content = comment_response['comment']['content']
athena

코멘트의 답글을 다는 경우도, 원래 코멘트의 내용도 가져와 기존 코멘트 내용과 코멘트 답글 내용도 함께 볼 수 있게 됐습니다.


3. 리뷰어가 더 이상 코멘트를 남길 것이 없는지, 리뷰를 시작했는지 알지 못하고 계속 기다리기만 하는 시간을 줄이기 위해, 리뷰 시작/리뷰 종료에 대한 이벤트를 받기로 했습니다.

athena

리뷰 요청하기를 누르고, 리뷰어를 지정하면, 리뷰어들을 태그하고 각각 리뷰어들이 리뷰를 시작했을 때 누를 수 있는 버튼을 만들어 주었습니다.

athena

이제, 리뷰이는 리뷰어가 리뷰를 시작했는지, 리뷰를 완료 했는지 알림을 받아볼 수 있게 됐습니다!


추가 기능 넣기

불편함을 해결하고 나니, 원하는 기능들이 생겼습니다. (상상 속에 있는 걸 현실로 옮기기로 했습니다.)

  1. 피드백을 반영하고 난 후, 소스가 업데이트되면 지정되었던 리뷰어에게 재 알림을 주면 좋겠다.
  2. 리뷰어로 할당된 pr들을 모아서 보고 싶다.
athena


소스 업데이트 시 리뷰어에게 재 알림 보내기

기존에는 pr 생성과 리뷰어 지정이 함께 연동되지 않았습니다. (소스 업데이트 시 pr 요청자가 지정되었던 리뷰어를 기억하고 태그를 해야 했었습니다.)

pr 에 지정된 리뷰어를 유지하는 방법을 찾아보다가, pr 의 승인 규칙을 적용하기로 했습니다. pr 의 필수 승인자로 리뷰어를 지정하면, 소스를 업데이트하여도 승인 규칙을 조회해서 기존의 리뷰어를 찾을 수 있을 것 같았습니다.

리뷰어 지정하고 리뷰를 요청하면, 슬랙으로부터 view_submission type의 액션을 받게 됩니다.

{
   "type":"view_submission",
   "team":{
      "id":"T*",
      "domain":"b*"
   },
   "user":{
      "id":"U*",
      "username":"p*",
      "name":"p*",
      "team_id":"T*"
   },
   "api_app_id":"A*",
   "token":"1*",
   "trigger_id":"3*",
   "view":{
      "id":"V*",
      "team_id":"T*",
      "type":"modal",
      "blocks":[...],
      "private_metadata":"",
      "callback_id":"",
      "state":{
         "values":{
            "JqX":{
               "user-1":{
                  "type":"users_select",
                  "selected_user":"U*"
               }
            },
            "x8eSC":{
               "user-2":{
                  "type":"users_select",
                  "selected_user":"U*"
               }
            },
            "C1Bo":{
               "emergency-selct":{
                  "type":"static_select",
                  "selected_option":{
                     "text":{
                        "type":"plain_text",
                        "text":":x:",
                        "emoji":True
                     },
                     "value":"e*"
                  }
               }
            }
         }
      },
      "hash":"1*",
      "title":{...},
      "clear_on_close":False,
      "notify_on_close":False,
      "close":None,
      "submit":{
         "type":"plain_text",
         "text":"\*0",
         "emoji":True
      },
      "previous_view_id":None,
      "root_view_id":"V*",
      "app_id":"A*",
      "external_id":"",
      "app_installed_team_id":"T*",
      "bot_id":"B*"
   },
   "response_urls":[],
   "is_enterprise_install":False,
   "enterprise":None
}

view['state']['values'] 에서 선택된 값들을 받을 수 있습니다. values 객체 안의 키값은 계속 변경되는 값입니다!

answer_blocks = payload['view']['state']['values']
answer_keys = answer_blocks.keys()

first_reviewer = None
second_reviewer = None
emergency_answer = False

for key in answer_keys:
    answer_keys = answer_blocks[key]
    if answer_keys.get('user-1'):
        first_reviewer = answer_keys['user-1']['selected_user']
    elif answer_keys.get('user-2'):
        second_reviewer = answer_keys['user-2']['selected_user']

그래서 loop 를 돌려, 지정한키 (user-1, user-2) 를 찾아 리뷰어를 찾아오도록 했습니다.

이때 지정한 키는, 위에서 blocks 를 생성할 때 각 element 에 지정한 action_id 입니다.

이제 codecommit 의 create_pull_request_approval_rule 를 이용하여, 선택된 리뷰어가 필수 승인자로 지정되게 합니다. codecommit 의 pull_request_approval_rule 은 필수 승인자 수와, 필수 승인자 (Approval pool) 을 지정할 수 있습니다.

aws_client = boto3.client('codecommit')

code_commit_approver = f'arn:aws:iam::{domain_id}:user/'
first_user = USER_NAME.get(first_reviewer).get('aws_name') # aws 의 username
second_user = USER_NAME.get(second_reviewer).get('aws_name') # aws 의 username

rule_content = {
    'Version': '2018-11-08',
    'Statements': [
        {
            'Type': 'Approvers',
            'NumberOfApprovalsNeeded': 2, # 승인자 수
            'ApprovalPoolMembers': [] # 필수 승인자
        }
    ]
}

rule_content['Statements'][0]['ApprovalPoolMembers'].append(f'{code_commit_approver}{first_user}')
rule_content['Statements'][0]['ApprovalPoolMembers'].append(f'{code_commit_approver}{second_user}')

result = aws_client.create_pull_request_approval_rule(
    pullRequestId=pull_request_id,
    approvalRuleName=f'{pull_request_id}_commerce_dev_approval_rule', # 승인 룰 이름
    approvalRuleContent=json.dumps(rule_content)
)
athena

pr이 생성되고, 리뷰어를 지정하면, approval rules 를 생성하여서, 필수 승인자로 리뷰어를 지정해주었습니다.

이제 소스를 업데이트하면, 해당 pr 에 지정된 승인 룰을 조회해서, 기존에 지정된 리뷰어를 조회할 수 있습니다.

approver = {
    "first_approver": None,
    "second_approver": None
}

aws_client = boto3.client('codecommit')
pull_request = aws_client.get_pull_request(pullRequestId=pullRequestId)

approve_rule = pull_request['pullRequest']['approvalRules']
approve_rule_name = f'{pullRequestId}_commerce_dev_approval_rule' # 승인 룰 생성시 지정했던 승인 룰 이름

for rule in approve_rule:
    if rule.get('approvalRuleName') and rule['approvalRuleName'] == approve_rule_name:
        approve_rule_content = json.loads(rule['approvalRuleContent'])
        rule_statements = approve_rule_content['Statements'][0]['ApprovalPoolMembers']
        approver['first_approver'] = rule_statements[0].split('/')[1]
        approver['second_approver'] = rule_statements[1].split('/')[1]

codecommit 에서 get_pull_request 메서드를 통해 지정된 승인 룰을 조회하고, 필수 승인자로 지정된 유저에게 자동으로 재리뷰를 요청하도록 했습니다.

athena


리뷰어로 할당된 pr 모아보기

현재 오픈된 pr 들의 승인 규칙을 모두 조회하여, 내가 필수 승인자로 지정되어 있지만 승인하지 않은 pr 을 모아볼 수 있는 기능을 만들기로 했습니다.

해당 기능은 슬랙의 slash command 를 이용하기로 했습니다.

pull_request_list = []

for repo in repo_list:
  # 오픈 되어 있는 pr 들 조회
  open_pull_requests = aws_client.list_pull_requests(
       repositoryName=repo,
       pullRequestStatus='OPEN'
  )
	
  for request in open_pull_requests['pullRequestIds']:
      # pr 정보 조회
      request_info = aws_client.get_pull_request(pullRequestId=request)
	
      pull_requests = request_info.get('pullRequest')
	
      if pull_requests['approvalRules']:
          is_need = False
          for rule in pull_requests['approvalRules']:
              # 승인 룰 조회
              if rule['approvalRuleName'] == f'{request}_commerce_dev_approval_rule':
                  approval_content = json.loads(rule['approvalRuleContent'])
                  rule_statements = approval_content['Statements'][0]['ApprovalPoolMembers']
	
                  # 리뷰어로 지정되어 있는지 체크
                  if user_id in [rule_statements[0].split('/')[1], rule_statements[1].split('/')[1]]:
                      # 현재 pr의 승인상태 체크
                      pull_request_approval_state = aws_client.get_pull_request_approval_states(
                          pullRequestId=request, revisionId=pull_requests.get('revisionId')
                      )
                      approvals = pull_request_approval_state['approvals']
                      is_need = True

                      # 내가 pr 승인했는지 체크
                      for approval in approvals:
                          if approval['userArn'].split('/')[1] == {user_id} and approval['approvalState'] == 'APPROVE':
                              is_need = False
          if is_need:
              pull_request_list.append({"pull_request": pull_requests, "repository": repo})

command 가 호출되면, 현재 open 상태인 pr 들을 조회하여 승인이 필요한 pr 들을 배열에 저장했습니다.

저장된 pr을 dm 으로 보내기로 했습니다.

slack 봇으로 dm 을 보내기 위해서는, channel id 를 알아야 합니다.

response = slack_client.conversations_open(users=user_id, pervent_creation=False)

if response['ok']:
    channel_id = response['channel']['id']

slack api 의 conversations_open 메서드를 이용해서 슬랙봇과 dm 하는 채널의 id 를 얻을 수 있습니다. (prevent_creation 옵션은 dm 채널이 열려있지 않을 시 dm 채널을 만들도록 하는 옵션입니다. default 값은 True 입니다.)

이렇게 받아온 channel id 로 리뷰가 필요한 pr 리스트를 dm 으로 보내줍니다.

athena

이제 저희팀은 /리뷰타임 을 입력하면, 리뷰가 필요한 pr 들을 모아서 볼 수 있습니다!


슬랙에서 여러 개의 버튼 액션과 상호 작용하기

현재 사용하는 버튼은 4종류입니다.

  • 리뷰 요청하기
  • 리뷰 시작하기
  • 리뷰 끝
  • 재리뷰 요청하기

슬랙 api 의 Interactivity & Shortcuts 설정에서는 Interactivity 의 request url 을 한 개만 설정할 수 있습니다. (버튼이 여러 종류여도, 하나의 url 로만 요청을 받을 수 있습니다.)

그래서, 여러 개의 버튼으로 사용할 수 있게, button 의 action-id 를 고유하게 설정하여, 각각 다른 기능을 하게 해주었습니다.

# 슬랙에서 요청을 보낼때 body 에 내용을 담아 보냄
body = parse_qs(event['body'])

if body.get('payload'):
    payload = json.loads(body['payload'][0])

    if payload['type'] == 'view_submission': # 리뷰어 지정 모달 제출 시 
        return send_reviewer_request(payload, slack_client)

    if payload['type'] == 'block_actions': # 버튼 클릭 시 
        if payload['actions'][0]['action_id'] == 'review-request-action': # 리뷰 요청하기
            call_result = slack_client.send_review_request_view(payload['trigger_id'], payload['actions'][0]['value'], payload['user']['id'])
            if not call_result:
                return {'statusCode': 500}
            return call_result
        elif payload['actions'][0]['action_id'] == 'rereview-request-action':  # 재리뷰 요청하기
            call_result = slack_client.send_rereview_reqeust(payload['actions'][0]['value'], payload['user']['id'])
            if not call_result:
                return {'statusCode': 500}
            return {'statusCode': 200}
        elif payload['actions'][0]['action_id'] in ['first-reviewer-start', 'second-reviewer-start']: # 리뷰 시작 버튼
            call_result = slack_client.send_reviewer_start(payload['actions'][0]['value'], payload['channel']['id'], payload['user']['id'])
            if not call_result:
                return {'statusCode': 500}
            return {'statusCode': 200}
        elif payload['actions'][0]['action_id'] == 'reviewer-end': # 리뷰 끝 버튼
            call_result = slack_client.send_reviewer_end(payload['actions'][0]['value'], payload['channel']['id'], payload['user']['id'])
            if not call_result:
                return {'statusCode': 500}
            return {'statusCode': 200}


마무리

사실 브랜디는 형상 관리 툴 이관을 계획하고 있습니다. 서론에서 이야기 한 것처럼, CodeCommit을 통한 PR은 불편한 점이 많기 때문입니다. 그렇지만 형상 관리 툴 이관 전에 최대한 불편함을 개선함으로써, 저희 팀은 CodeCommit 안에서 조금 더 효율적으로 코드 리뷰를 진행할 수 있게 되었습니다.

기존에 리뷰를 위해 오래 대기하던 pr 들이 줄어들었고, 팀원 모두가 코멘트를 참고하고 공감할 수 있게 되었습니다.

다음 글은 Github 으로 이관 후에도 편리한 코드 리뷰 문화를 유지하게 된 기록을 공유하게 될 것 같습니다.

커머스개발팀은 비효율적인 것을 싫어하고, 더 나은 개발문화를 위해 꾸준히 연구하고 노력합니다.
함께 더 나은 개발 문화를 만들어가실 분들을 기다립니다.


참고글


손수정 | APPS실 커머스개발팀
브랜디, 오직 예쁜 옷만