가장 간단한것부터 시작하기

Kim Dong Kyun·2024년 8월 5일
0
post-thumbnail

인프콘에서 재민님, 토비님, 영호님의 세션을 듣고 난 뒤 많은 인사이트를 받았다. 그 중에서도 재미있던것 재민님과 토비님이 공통적으로 말씀하신(맥락적으로 동일한), 리팩토링 가능한 구조로 어플리케이션을 작성 해 나가기(설계를 잘하는 것=설계를 하지 않는 것)에 감명을 받았는데, 개인 사이드 프로젝트로 구상했던 깃허브 리뷰 자동할당 어플리케이션을 이 방식을 사용해서 구현 해 보고 싶어졌다.

깃허브 리뷰 자동할당 시스템은 이 글로부터 시작되었는데, 목적은 아래와 같다.

  1. 팀 프로젝트 진행 시 코드리뷰를 가능한 다양한 사람들에게 할당하고 싶다.
  2. 프론트, 백엔드 모두 사용 할 수 있으며, 별다른 코드적인 조정 없이 한 번 등록해두면 계속 알림이 오면 좋겠다.
  3. 리뷰를 할당 한 뒤 팀이 사용하는 메신저(Discord, Slack...)에 할당된 사람이 멘션되면 좋겠다. (리뷰를 미루지 못하게!)

최소기능부터 시작하기

최소기능을 어떻게 잡아갈까 생각하다가, 외부 의존성(Github API...)이 배제된 상태로 동작만 테스트 가능한 형태의 어플리케이션을, 테스트를 적극적으로 작성해나가면서 구현 해 보고 싶었다.

내가 생각한 도메인은 시작 단계에서는 1. 회원 , 2. 레포지토리(깃허브) 3. 메신저(슬랙...) 4. 회원이 보내길 원하는 메시지 네개이다. 이 중 외부 의존성이 가장 적고 테스트하기 좋은 것을 고르라면 아무래도 메시지일 것이다. 메시지부터 시작해보자.


데이터를 변경하는 로직은 객체지향적으로

데이터를 변경하는 로직은 도메인 스스로가 가지는, 객체지향적인 방식으로 구현하길 원한다. 따라서 도메인에 대한 테스트부터 시작하기로 했다.

간단한 메시지로 객체 생성 테스트를 해보았다.

class MessageTest {  
    @Test  
    void createMessage() {  
        String message = "테스트 메시지";  
        ReviewMessage reviewMessage = new ReviewMessage(message);  
        assertThat(reviewMessage).isNotNull();  
    }  
}

public class ReviewMessage {  
  
    private String message;  
  
    public ReviewMessage(String message) {  
        this.message = message;  
    }  
}

객체 생성은 당연하게도 잘 된다. 사용자가 전하려고 하는 메시지는 테스트 메시지가 아닐 것이지만.

메시지는 무슨 일을 할까? 사실 별 거 없다. 유저가 pr 생성 후 리뷰 요청 메시지를 보낼 때 담기기 원하는 정보는 몇 개 없을것이다. 한 번 추려보자

  1. pr 제목
  2. pr 링크(URL)
  3. pr 작성자
  4. 리뷰 할당자

이 정도 아닐까?
이 정보 중 시스템에서 필수로 넣어주고 싶은 부분을 정하자면 pr 제목, Pr 링크, 리뷰 할당자 정도일 것이다. 나머지는 옵션으로 넣는 걸로.

슬랙 등 메시지에서 해당 정보들을 N개의 알림으로 날려버리면 사용자는 이걸 스팸으로 인식하게 될 것이다. 그럼 리뷰를 안하겠지. 그러니 하나의 메시지에 필수 정보 와, 사용자가 넣길 원하는 선택 정보 를 분리해서 넣어보도록 하자. (선택정보는 추후에 추가하자.)

먼저, 필수 정보를 넣어본다. 필수 하면 생각나는건 생성자니, 생성자를 조금 고쳐보자.

public ReviewMessage(String prTitle,  
                     String prLink,  
                     String prAuthor,  
                     String reviewAssignee) {  
    this.message = message;  
}
...

이거 좀 짜친다. String 으로 된 녀석이 순서없이 들어가 있는것도 마음에 들지 않고, 선택 정보와 혼동될수도 있을 것 같다. 빌더를 사용하는 것도 고려 할 수 있지만, 저 정보들을 모두 가지고 있는 객체를 한번 선언 해 봐도 괜찮을 것 같다. 아니다 싶으면 빼지 뭐.

@Test  
@DisplayName("사용자가 리뷰를 요청 할 때 사용할 메시지 객체를 생성 할 수 있다.")  
void createReviewMessage() {  
    String message = "테스트 메시지";  
    ReviewMessage reviewMessage = new ReviewMessage(message);  
    assertThat(reviewMessage).isNotNull();  
}  
  
@Test  
@DisplayName("사용자가 리뷰를 요청 할 때 사용하는 필수 메시지를 가진 객체를 생성 할 수 있다.")  
void createDefaultMessage() {  
    String prTitle = "pr 제목";  
    String prLink = "pr 링크";  
    String prAuthor = "pr 작성자";  
    String reviewAssignee = "리뷰 할당자";  
  
    DefaultMessage defaultMessage = new DefaultMessage(prTitle, prLink, prAuthor, reviewAssignee);  
    assertThat(defaultMessage).isNotNull();  
}

당연하게도, 테스트가 잘 돈다.

아...여기서 고민이 등장한다. 이거 설계 아냐? 싶다. 근데 뭐, 테스트 하기 편하게 먼저 작성하는거니, 불안함은 잠시 넣어두도록 한다. 테스트는 아주작은 스텝으로 가져가고 싶다.

그러면 여기서 하나 더 고민이 생긴다. ReviewMessage 가 저 필드들을 다 알 필요가 없다. 그래서 우리는 분리했지. 그리고 어차피 메시지를 하나로 만들어야 하니까, DefaultMessage 가 하나의 메시지를 만들어서 ReviewMessage 에게 전달하도록 해보자.

먼저 메시지를 합쳐(merge)야 한다.

@Test  
@DisplayName("필수 메시지를 조합해서 하나의 메시지로 리턴 할 수 있다.")  
void createUnitedMessage() {  
    String prTitle = "pr 제목";  
    String prLink = "pr 링크";  
    String prAuthor = "pr 작성자";  
    String reviewAssignee = "리뷰 할당자";  
  
    DefaultMessage defaultMessage = new DefaultMessage(prTitle, prLink, prAuthor, reviewAssignee);  
    String mergedMessage = defaultMessage.mergeMessage();  
    assertSoftly(  
            softly -> {  
                softly.assertThat(mergedMessage).contains(prTitle);  
                softly.assertThat(mergedMessage).contains(prLink);  
                softly.assertThat(mergedMessage).contains(prAuthor);  
                softly.assertThat(mergedMessage).contains(reviewAssignee);  
            }  
    );  
}

public String mergeMessage() {  
    return String.format("타이틀 : %s%n", prTitle) +  
            String.format("작성자 : %s%n", prAuthor) +  
            String.format("할당자 : %s%n", reviewAssignee) +  
            String.format("링크 : %s%n", prLink);  
}
  • 메시지의 형식이나 줄바꿈은 계속 변할 수 있으니, 메시지를 정확히 가지고 있는지만 테스트한다.

오케이. 그러면 일단 필수 정보는 완성되었고, 형태는 이럴 것이다.

타이틀 : pr 제목
작성자 : pr 작성자
할당자 : 리뷰 할당자
링크 : pr 링크

낫배드 하긴 한데, 좀 삭막하다. 유저가 추가로 커스텀 할 수 있게 해볼까?

커스텀하는 메시지는 어떤 형식이어야 할까? 내가 유저라면 중요 정보를 사이 사이에 넣은 하나의 자연스러운 메시지를 원할 것 같다. 즉 이런 느낌을 원할 것 같다.

안녕하세요 여러분! **Author Name**님이 새로운 PR을 제출했어요: **PR Title Example**. 리뷰는 **Assignee Name**님께 할당되었습니다. 🔗 여기서 확인할 수 있어요: [PR 링크](http://example.com) 꼼꼼하게 리뷰하고 피드백 부탁드려요. 감사합니다!

오케이. 이런 느낌으로 메시지를 빌드하려면 어떤 작업이 필요할까?

  1. 사용자가 메시지를 등록 할 때, {prTitle} 과 같은 정보들을 메시지 사이사이에 정해진 양식으로 추가한다.
  2. 통짜 메시지에 저장하고 있는 정보들을 리턴해준다.

조회 관련해서는 현재는 고려하지 않고 싶다. 그러니까 새 테스트

  1. 사용자가 등록한 메시지가 인펏으로 제공된다. 이 메시지는 ReviewMessage 라는 객체로 맵핑된다.
  2. 필수 데이터는 사용자가 등록하는 것이 아니므로, 임의의 필수 메시지를 reviewMessage 와 조합해서 하나의 예쁜 메시지로 만든다.

오케이. 해보자

@Test  
@DisplayName("필수 메시지를 조합해서 하나의 메시지로 리턴 할 수 있다.")  
void createUnitedMessage() {  
    String prTitle = "pr 제목";  
    String prLink = "pr 링크";  
    String prAuthor = "pr 작성자";  
    String reviewAssignee = "리뷰 할당자";  
  
    DefaultMessage defaultMessage = new DefaultMessage(prTitle, prLink, prAuthor, reviewAssignee);  
  
    String template = "안녕하세요 여러분! {author}님이 새로운 PR을 제출했어요: {title}. 리뷰는 {assignee}님께 할당되었습니다."   
+ "여기서 확인할 수 있어요: [PR 링크]({link}) 꼼꼼하게 리뷰하고 피드백 부탁드려요. 감사합니다!";  
    String mergedMessage = defaultMessage.mergeMessage(template);  
      
    String expected = "안녕하세요 여러분! pr 작성자님이 새로운 PR을 제출했어요: pr 제목. 리뷰는 리뷰 할당자님께 할당되었습니다." +  
            "여기서 확인할 수 있어요: [PR 링크](pr 링크) 꼼꼼하게 리뷰하고 피드백 부탁드려요. 감사합니다!";  
    assertSoftly(  
            softly -> {  
                softly.assertThat(mergedMessage).contains(prTitle);  
                softly.assertThat(mergedMessage).contains(prLink);  
                softly.assertThat(mergedMessage).contains(prAuthor);  
                softly.assertThat(mergedMessage).contains(reviewAssignee);  
                softly.assertThat(mergedMessage).isEqualTo(expected);  
            }  
    );

테스트 코드는 위와 같은. 형태로 변경하기로 했다. 구현이 테스트코드에 드러나는 것은 좋은 일이 아니니, 리터럴이 조금 거슬리지만 . 이형태로 바꾸어보자.

그리고, 구현부를 변경한다.

@Getter  
public enum TemplatePlaceholder {  
    AUTHOR_NAME("{author}"),  
    PR_TITLE("{title}"),  
    ASSIGNEE_NAME("{assignee}"),  
    PR_LINK("{link}");  
  
    private final String placeholder;  
  
    TemplatePlaceholder(String placeholder) {  
        this.placeholder = placeholder;  
    }  
}
public class DefaultMessage {  
  
    private String prTitle;  
    private String prLink;  
    private String prAuthor;  
    private String reviewAssignee;  
  
    public DefaultMessage(String prTitle, String prLink, String prAuthor, String reviewAssignee) {  
        this.prTitle = prTitle;  
        this.prLink = prLink;  
        this.prAuthor = prAuthor;  
        this.reviewAssignee = reviewAssignee;  
    }  
  
    public String mergeMessage(String template) {  
        return template  
                .replace(TemplatePlaceholder.PR_TITLE.getPlaceholder(), prTitle)  
                .replace(TemplatePlaceholder.AUTHOR_NAME.getPlaceholder(), prAuthor)  
                .replace(TemplatePlaceholder.ASSIGNEE_NAME.getPlaceholder(), reviewAssignee)  
                .replace(TemplatePlaceholder.PR_LINK.getPlaceholder(), prLink);  
    }  
}

사용자가 입력하는 메시지에 {author} 과 같이 상수로 관리하는 메시지를 깃허브에서 가져올 실제 title, author... 등과 바꿔 줄 것이다. 그러므로 이 부분은 열거형으로 관리한다.

이렇게 바꾸니, ReviewMessage 가 어떤 일을 하는지 모호해진다. 아무 일도 안 하고. 있으니, 리뷰메시지는 없애버리자. 필요하다면 나중에 추가 할 것이다.

0개의 댓글