본문 바로가기
개발툴

내가 자동화 스크립트를 만드는 이유?

by 탁구치는 개발자 2024. 12. 3.

 

저는 업무 할 때 반복적인 일이 발생하면 무조건 자동화 스크립트를 만들어서 활용합니다.
여러 자동화 스크립트를 만들어 가면 확실히 중요한 업무에 더 많은 에너지와 집중력을 쏟아 부을 수 있습니다.

궁금한게 있었습니다.

"나는 왜 반복적인 작업들을 귀찮아 하고, 자동화를 원하는 걸까?"

 

이 질문에 대해 고민하다가, 제 성향이 부지런함과 효율을 중시하기 때문이라는 결론에 도달했습니다.

자동화 스크립트를 만들기 위해서는 문제를 해결하기 위한 방안을 깊이 생각해야 하고, 이를 구현하기 위해 노력을 들여 프로그램을 만들어야 하기 때문입니다.
하지만 '남이 만든 그릇에 내 인생을 담지 마라' 책을 읽고 난 후 내가 자동화 스크립트를 계속 만들려고 하는 이유를 깨닫게 되었습니다.

책에 아래 문장이 있습니다.

p.17
"게으름, 조바심, 자만심은 프로그래머의 3대 미덕이다."
어째서 게으름이 미덕일 수 있는 걸까? 그것은 게으른 사람일수록 일을 하기 싫어서 어떻게 하면 더 빨리, 효율적으로 일을 끝낼지 깊이 고민하기 때문이다. 열심히 일하는 사람은 다소 귀찮은 일이 있어도 체력과 근성으로 어떻게든 버텨내지만 게으른 사람은 모든 일을 성가시게 느껴 더 효율적인 방법을 떠올리려 한다.



그렇습니다.
전 게으른 사람입니다.
게으르기 때문에 반복적인 작업들을 자동화 스크립트에게 위임 하는 걸 좋아합니다.
게으름이라는 단어가 주는 부정적인 인식이 깨지는 순간이였습니다.
게으름이 무조건 나쁘기 보다는 특정 환경과 상황속에서는 장점이 될 수 있다는 깨달음을 얻었습니다.

그럼 실제 어떤 작업들을 자동화 하고 있는지 말씀드려 보겠습니다.

  • 출근해서 시작해야 하는 여러 프로그램(아웃룩, 인텔리J, 인트라넷, 기타 사이트 등)을 자동으로 실행합니다.
  • Jira 티켓을 만들 때 기본적으로 입력해야 하는 항목에 대해서도 자동으로 입력되도록 파이썬을 이용하여 처리합니다.
  • 회사의 공지 사항도 팀즈 메신저로 알림 받을 수 있게 자동화 합니다.
  • Git, Kubernetes, Docker 명령어들에 대한 자동화 스크립트를 사용합니다. 명령어를 가끔식 잊어먹기도 하고, 반복적인 타이핑도 귀찮았습니다.
  • 특정 디렉토리를 백업하는 스크립트
  • ACL 체크를 도와주는 스크립트
  • 회사에서 정의한 Git 커밋 메세지를 생성해 주는 스크립트
  • Ubuntu 초기 셋팅에 필요한 스크립트
  • Maven 설정을 Gradle 설정으로 변환시켜 주는 스크립트
  • Git 사이트에서 특정 키워드에 해당하는 소스를 찾아주는 스크립트
  • 모니터링 시스템의 수치를 스크래핑 하여 팀즈로 전달해 주는 스크립트
  • 기타 등등...

무수히 많은 스크립트가 제 대신 일을 해주고 있습니다.


이 중에서 Git 스크립트에 대한 코드를 예시로 보여드리겠습니다.

# -*- coding: utf-8 -*-

import datetime
import json
import random
import re
import string
import subprocess
import sys

import requests
from bs4 import BeautifulSoup
from tabulate import tabulate

from common.common_service import CommonService


class GitCommand(CommonService):

    limit_row_num = 10

    common_headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36'
    }

    def __init__(self):
        super().__init__()

    def command_list(self):
        commands = []
        commands.append("help")
        commands.append("auto_merge")
        commands.append("commit")
        commands.append("merge")
        commands.append("auto_pull")
        commands.append("list_stash")
        commands.append("save_stash")
        commands.append("pop_stash")
        commands.append("apply_stash")
        commands.append("drop_stash")
        commands.append("reset")
        commands.append("rebase")
        commands.append("rebase_interactive")
        commands.append("rebase_change_commit_date")
        commands.append("rebase_continue")
        commands.append("print_commit_history")
        commands.append("make_commit_history_for_test")

        self._print_table(["num", "command"], commands)
        return commands

    # ==============================================
    # valid 체크
    # ==============================================
    def check_current_branch(self):
        current_branch = self.current_branch()
        if current_branch in ("develop", "master"):
            print("does not support to do anything in this branch ==> %s" % current_branch)
            sys.exit()

    # ==============================================
    # common
    # ==============================================
    def help(self):
        print("save_stash : stash 에 저장하기")
        print("apply_stash : stash 에서 꺼내오기 (stash 삭제 하지 않음)")
        print("pop_stash : stash 에서 꺼내오기 (stash 삭제 함)")
        print("=============== rebase 관련 ==================")
        print("pick : 해당 커밋을 수정하지 않고 사용")
        print("reword : 커밋 메시지만 수정")
        print("edit : 커밋 정보 수정 가능 (메시지, author, date)")
        print("squash : 해당 커밋을 이전 커밋과 합치기")
        print("위의 옵션을 적용한 후 저장하면 이후 edit 모드 화면이 출력되고 그곳에서 commit message를 수정해야 한다.")
        print("==============================================")

    def process(self, cmd):
        print("# %s" % cmd)
        output = subprocess.check_output(cmd, shell=True)
        return output.decode("utf-8")
    
    def call(self, cmd):
        print("# %s" % cmd)
        subprocess.call(cmd, shell=True)

    def current_branch(self):
        output = self.process("git rev-parse --abbrev-ref HEAD")
        return output.rstrip()

    def last_commit(self):
        return self.process("git rev-list -1 HEAD")

    def list_branch(self):
        branch_list = self.process("git branch").splitlines()
        new_branch_list = []
        for i, branch in enumerate(branch_list):
            b = re.sub("(\\s|\\*)", "", branch)
            new_branch_list.append(b)

        self._print_table(["num", "branch"], new_branch_list)
        return new_branch_list

    def git_commit_log(self, max_count):
        commit_logs = self.process("git log --oneline --max-count=%d" % max_count).splitlines()
        commit_log_list = []
        for i, log in enumerate(commit_logs):
            commit_log = log.split(" ", 1)[1]
            commit_log_list.append(commit_log)

        self._print_table(["num", "log"], commit_log_list)
        return commit_log_list

    # ==============================================
    # features
    # ==============================================
    def auto_merge(self):
        git.check_current_branch()
        c_branch = self.current_branch()
        if c_branch.find("feature") > -1:
            self.call("git checkout develop")
            self.call("git pull origin develop")
            self.call("git merge %s --no-edit" % c_branch)
            self.call("git push origin develop")
            self.call("git checkout %s" % c_branch)
        elif c_branch.find("hotfix") > -1:
            self.call("git checkout develop")
            self.call("git pull origin develop")
            self.call("git merge %s" % c_branch)
            self.call("git push origin develop")
            self.call("git checkout master")
            self.call("git pull origin master")
            self.call("git merge %s" % c_branch)
            self.call("git push origin master")
            self.call("git checkout %s" % c_branch)

    def commit(self):
        commit_msg = input("please type commit message : ")
        self.call("git add *")
        self.call("git commit -m \"%s\"" % commit_msg)

    def merge(self):
        new_branch_list = self.list_branch()
        from_branch_number = input("please choose a branch to merge (가져올브랜치) : ")
        if self._only_number(from_branch_number):
            from_branch = new_branch_list[int(from_branch_number)]
            new_branch_list = self.list_branch()
            to_branch_number = input("please choose a branch to be merged (머지되는브랜치) : ")
            if self._only_number(to_branch_number):
                to_branch = new_branch_list[int(to_branch_number)]
                self.call("git checkout %s" % to_branch)
                self.call("git pull origin %s" % to_branch)
                self.call("git merge %s" % from_branch)

                answer = input("Would you like to push this branch? (y or n) : ")
                if answer == 'y':
                    self.call("git push origin %s" % to_branch)
                self.call("git checkout %s" % from_branch)

    def rebase(self):
        self.help()
        branch_list = self.list_branch()
        from_branch_number = input("어떤 브랜치의 commit을 이동시킬 것 입니까? (from branch) : ")
        from_branch = branch_list[int(from_branch_number)]
        to_branch_number = input("%s의 commit들을 어느 브랜치의 끝으로 이동시킬 것입니까? : " % from_branch)
        to_branch = branch_list[int(to_branch_number)]
        # 대상이 되는 브랜치로 이동하여 pull 받는다. (최신 소스 반영)
        self.call("git checkout %s" % to_branch)
        self.call("git pull origin %s" % to_branch)
        # rebase 진행
        self.call("git checkout %s" % from_branch)
        self.call("git rebase %s" % to_branch)
        answer = input("병합하시겠습니까? (y or n)")
        if answer == 'y':
            self.call("git checkout %s" % to_branch)
            self.call("git merge %s" % from_branch)

    def rebase_interactive(self):
        self.help()
        self.git_commit_log(20)
        commit_log_num = input("어느 commit 까지 수정하실 건가요? : ")
        self.call("git rebase -i HEAD~%d" % (int(commit_log_num) + 1))

    def rebase_change_commit_date(self):
        now = datetime.datetime.now().strftime("%b %d %H:%M:%S %Y")
        self.call('git commit --amend --no-edit --date "%s +0900"' % now)

    def rebase_continue(self):
        self.call("git rebase --continue")

    def auto_pull(self):
        self.call("git checkout develop")
        self.call("git pull origin develop")
        self.call("git checkout master")
        self.call("git pull origin master")

    def list_stash(self):
        stash_list = self.process("git stash list").splitlines()
        for i, stash in enumerate(stash_list):
            print(i, stash)
        return stash_list

    def save_stash(self):
        now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        stash_name = input("please type stash name : ")
        self.call("git stash save -u %s %s" % (now, stash_name))

    def apply_stash(self):
        self.list_stash()
        number = input("please choose number : ")
        if self._only_number(number):
            self.call("git stash apply %s" % number)

    def pop_stash(self):
        self.list_stash()
        number = input("please choose number : ")
        if self._only_number(number):
            self.call("git stash pop %s" % number)

    def drop_stash(self):
        self.list_stash()
        number = input("please choose number : ")
        if self._only_number(number):
            self.call("git stash drop %s" % number)

    def reset(self):
        # git reset --hard  a3bbb3c
        line_number = input("how many lines? : ")
        commit_list = self.process("git log -%s --pretty=oneline" % line_number).splitlines()
        for i, commit in enumerate(commit_list):
            print(i, commit)

    def make_commit_history_for_test(self):
        current_branch = self.current_branch()
        print("current branch : %s" % current_branch)
        file_name = input("test file : ")
        for _ in range(10):
            with open(file_name, "a") as file:
                msg = ''.join(random.choice(string.ascii_lowercase) for c in range(5))
                file.write(msg)
            file.close()
            self.call("git add *")
            self.call("git commit -m '%s_%s_%s_%d'" % (current_branch, msg, file_name, _))

    def login(self, git_login_url):
        company_user_name = self._select_value("nano_id")
        company_user_pw = self._select_value("nano_pw")
        session = requests.Session()
        session.get(git_login_url, headers=self.common_headers)

        data = {'j_username': company_user_name, 'j_password': company_user_pw, '_atl_remember_me': 'on', 'submit': 'Log in'}
        session.post(git_login_url, headers=self.common_headers, data=data)
        return session

    def print_commit_history(self):
        config = json.loads(self._select_value("commit_history_env"))
        url = config.get("url")
        services = config.get("services")

        self._print_table(["project", "repository"], services)
        print("if you want to see whole projects, please press enter keyboard.")
        number = input("please choose a project : ")

        # 모두
        if not number:
            for service in services:
                self.project_history(service, url)
        # 단 건
        else:
            service = services[int(number)]
            self.project_history(service, url)

    def project_history(self, service, url):
        project = service.get("project")
        repository = service.get("repository")

        git_url = url.get("git_url")
        git_login_url = url.get("git_login_url")
        git_project_url = url.get("git_project_url") % (project, repository)
        session = self.login(git_login_url)

        res = session.get(git_project_url, headers=self.common_headers)
        soup = BeautifulSoup(res.text, 'html.parser')
        elements = soup.select("#commits-table > tbody > tr")

        data_list = []
        for i, element in enumerate(elements):
            if i == self.limit_row_num:
                break
            try:
                author = element.select_one(".author > div").attrs['title']
                commit_link = element.select_one(".commitid").attrs['href']
                commit_msg = element.select_one(".message-subject").text
                date_text = element.select_one("time").text

                author = author[0:3]
                commit_msg = commit_msg.split("\n")[0]
                commit_link = git_url + commit_link

                data_list.append([author, commit_link, date_text, commit_msg])
            except Exception as ex:
                continue

        print(tabulate(data_list, headers=["name", "link", "date", "message"], tablefmt="rst"))
        print("\n\n")

    def execute(self):
        command_list = self.command_list()
        number = input("please choose command : ")
        if self._only_number(number):
            command = command_list[int(number)]
            git_command = GitCommand()
            self._reflection(git_command, command)


# ==============================================
# main
# ==============================================
if __name__ == "__main__":
    git = GitCommand()
    git.execute()


여러 가지 기능들이 제공되고 있고, 대표적으로 가장 많이 사용하고 있는 merge 기능에 대해서 소개해 보겠습니다.

develop 브랜치에서 작업을 한 후 master 브랜치에 머지 한 후 push 의 과정을 담아보겠습니다.

 

(1) develop 브랜치에서 코드 작성 후 commit 을 합니다.

 

(2) 터미널에서 git_command.py 명령어를 수행합니다.

3번을 선택합니다.

(3) 머지 시 가져올 브랜치를 선정합니다.

가져올 브랜치 0번을 선택합니다. (develop 브랜치)

(4) 머지 시 대상 브랜치를 선정합니다.

대상 브랜치 1번을 선택합니다. (master 브랜치)

 

(5) 머지가 됩니다.

원격지에 push 할지 물어봅니다. y 를 입력하고 엔터를 칩니다.

 

(6) push가 되었습니다.


이 과정이 몇 번의 타이핑 만으로 끝났습니다.

분명 누군가는 "타이핑 해봤자 얼마 안되는데 그냥 타이핑 하자!" 라고 생각하실지 모릅니다.
하지만 전 게으른 사람 입니다. ㅠㅠ
그런 사소한 반복 조차도 전 불편함을 느낍니다.

지금까지 제가 자동화 스크립트를 만들어서 업무에 활용하는 방법에 대해서 이야기 해보았습니다.
사실 업무가 아닌 삶의 여러 방면에서 저의 게으름을 잘 이용하여 자동화 하는 걸 즐기고 있습니다.
이처럼 다양한 자동화 스크립트들이 저에게 더 중요한 선택과 결정 그리고 집중력을 선사해 주고 있다고 생각합니다.