멈재

스터디 현황판을 github actions으로 관리해보자 (a.k.a README 자동 업데이트 시키기) 본문

git

스터디 현황판을 github actions으로 관리해보자 (a.k.a README 자동 업데이트 시키기)

멈재 2023. 4. 19. 18:28
728x90

우연히 코딩테스트 문제 푼 목록을 레포지토리 화면(README)에 보여주는 포스팅 보게 되었다.
 
[Git] github actions로 README.md 자동생성하기

 

[Git] github actions로 README.md 자동생성하기

최근에 LeetCode를 풀고 있는데, 내가 풀어 둔 문제 목록을 레포지토리 첫 화면에 예쁘게 보여주면 좋겠다는 생각이 들었다. LeetHub처럼 LeetCode의 내 실제 제출 목록을 분석해서 깃헙에 자동으로 올

holika.tistory.com

 
이 아이디어를 빌려 평일 오전마다 하는 모각공 스터디에 살짝 응용해서 적용하면 좋겠다는 생각에 요 내용을 시작하게 되었다.
 
github actions가 실행되어 동작되면 다음과 같은 형태로 스터디 현황판을 업데이트하게 된다.

 
전체 코드는 깃허브를 참고해주세요.

 

GitHub - we-can-do-better/2023-mogakgong

Contribute to we-can-do-better/2023-mogakgong development by creating an account on GitHub.

github.com

 


 

Github Actions

Github Actions는 어떤 이벤트(event)가 발생했을 때 자동으로 특정 작업이 일어나게 하거나 주기적으로 어떤 작업을 반복해서 실행시킬 수 있다.
가령 저장 공간인 레포지토리(repository)에 Pull Request가 생성되면 코드 리뷰도 할 수 있고, 빌드(build)해서 배포(deploy)의 작업을 자동화할 수 있다. 심지어 특정 시간이 되면 통계 데이터도 낼 수 있다.
이렇게 지속적으로 수행해야 하는 반복 작업들을 Github Actions이 대신해주게 된다.
 
 
내가 구현한 방식의 경우에는 특정 브랜치(main)에 PUSH 이벤트가 발생하는 경우 해당 작업(jobs)이 실행되도록 설정해두었다.
 
 

Application

처음에는 Github Actions가 동작했을 때 해야 하는 일을 작은 단위로 나누었다.

  1. 스터디 멤버 정보를 읽어온다.
  2. 가져온 스터디 멤버들의 폴더에서 오늘과 일치하는 파일이 존재하는지 확인한다.
  3. 일치한다면 README 파일에 반영한다.

 
 
각각 알아보자면,

1. 스터디 멤버 정보를 읽어온다.

처음에는 README 파일에서 아래와 같이 "스터디 멤버만" 읽어오는 로직을 구현했었다.

while (scanner.hasNextLine()) {
    index++;
    String nextLine = scanner.nextLine();

    if(nextLine.equals(STUDY_MEMBER_HEADER)) {
        String curLine = scanner.nextLine();
        String[] headers = curLine.replace(" ", "").split("\\|");
        return new StudyBoard(headers, index);
    }
}

 
그런데 구현하다 보니 스터디 현황판을 읽어오는 작업도 필요하다는 걸 알게 돼서 차라리 한 번의 읽기 작업으로 모든 라인 수를 읽어오고, 애플리케이션에서 정제하는 것이 리소스를 더 줄일 수 있다는 생각에 다음의 방식으로 변경시켰다.

// ReaderUtils#getReadmeFile
public static List<String> getReadmeFile() {
    File file = new File(README_PATH);
    List<String> lines = toListByReadme(file);

    return lines;
}

// ReaderUtils#toListByReadme
private static List<String> toListByReadme(File file) {
    try {
        List<String> lines = new ArrayList<>();

        Scanner scanner = new Scanner(file);
        while (scanner.hasNextLine()) {
            lines.add(scanner.nextLine());
        }
        return lines;
    } catch (FileNotFoundException e) {
        throw new RuntimeException("README 파일이 존재하지 않습니다.", e);
    }
}

 
 

2. 가져온 스터디 멤버들의 폴더에서 오늘과 일치하는 파일이 존재하는지 확인한다.

이 과정은 다음과 같이 더 잘게 쪼갤 수 있었다.

  1. README 파일 마지막 줄인 가장 최근 날짜의 행을 읽어온다.
  2. 읽어온 행(가장 최근 날짜)이 오늘 날짜와 일치하는지 확인한다. 일치하지 않는다면 오늘 날짜의 행을 추가한다.
  3. 스터디 멤버 폴더를 루프(loop) 돌면서 오늘 날짜와 일치하는 파일을 찾아낸다.

 
슈도 코드로 적는 건 생각보다 간단했지만 구현에는 생각보다 오랜 시간이 걸렸다.

private static final Pattern pattern = Pattern.compile("\\d{1,2}월\\s*\\d{1,2}일");

// MemberBoard#updateBoard
public void updateBoard(List<String> lines) {
    String lastLine = getLastLine(lines);        // 1.
    validateLatestDate(lastLine);                // 2.

    for (int i = 0; i < members.size(); i++) {
        String[] files = ReaderUtils.getFileListInMemberDirectory(members.get(i)); // 멤버 폴더안 파일 목록

        for (String file : files) { 
            Matcher matcher = pattern.matcher(file);

            // 날짜가 일치할 경우 쓰기 작업
            if (DateUtils.isMatchDate(matcher)) {  // 3.
                refreshBoard(i, ReaderUtils.getReadmeFile());
            }
        }
    }
}

 
무엇보다 날짜 비교를 하는 로직이 어려웠다.

  • Matcher
  • LocalDate 포맷
  • 입력값에 따라 LocalDate 인스턴스 생성
// DateUtils.isMatchDate
public static boolean isMatchDate(Matcher matcher) {
    if (matcher.find()) {
        String[] groups = matcher.group().split("월|일");
        LocalDate savedDate = makeLocalDateWithFile(groups[0], groups[1]);
        String convertSavedDate = convertLocalDateToDateFormat(savedDate);

        if(convertSavedDate.equals(FORMAT_TODAY_DATE)) {
            return true;
        }
    }
    return false;
}

기능적으로 해결하지 못한 건 Pattern.matcher로 파일명이 [x월 x일] 형태가 아닌 [0419]는 Matcher로 만들어내지 못했다.
아직까지도 보완할 아이디어가 떠오르지 않은 상태이다.. 😢
 
 

3. 일치한다면 README 파일에 반영한다.

MemberBoard#refreshBoard는 MemberBoard#updateBoard의 구현 과정 중 일부이다.

// MemberBoard#refreshBoard
private void refreshBoard(int member, List<String> lines) {
    String[] separateBoardStatus = getLastLine(lines).split(VERTICAL_SLASH);

    for (int i = 0; i < separateBoardStatus.length; i++) {
        if(sameIndex(member, i)) separateBoardStatus[i + 2] = "■";
    }

    String result = Arrays
            .stream(separateBoardStatus)
            .collect(Collectors.joining("|"));

    lines.set(lines.size() - 1, result + "|");

    ReaderUtils.applyMemberBoard(lines);
    //System.out.println("result = " + getLastLine(lines));
}

// ReaderUtils#applyMemberBoard
public static void applyMemberBoard(List<String> lines) {
        try {
            Path path = Paths.get(README_PATH);
            Files.write(path, lines, StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        System.out.println("==> Success overwritten README");
    }

이 코드는 개선할 점이 남아있다.
 
MemberBoard#refreshBoard는 스터디 멤버 폴더를 돌면서 오늘 날짜인 파일이 있는지 확인을 한다. 만약 오늘 날짜와 일치하는 파일이 있다면 쓰기 작업(ReaderUtils#applyMemberBoard)을 실행하게 된다.
이 말은 스터디 멤버 N명 모두가 정상적인 참여를 했다면 쓰기 작업이  N번 일어나는 것을 의미한다.
 
따라서 이때 직접적인 쓰기로 변경하는 것이 아닌, 리스트에서 마지막 라인을 꺼내온 다음 문자열을 대체하도록 해서 쓰기 작업을 줄이는 것이 더 좋은 방법이라고 볼 수 있다.

 


 

단순 호기심으로 시작했기 때문에 촘촘하게 다루지 못한 부분들이 여전히 남아있다.

 

- 검증 처리

- 예외 처리

- I/O 최소화

- 메서드 재활용 등

 

향후 보완이 완료되면 퇴고를 해보려 한다.
 
 
 
 
참고