메시지 기능 좀 더 넣어보기

Kim Dong Kyun·2024년 8월 6일
0

메시지에 기능이 좀 더 필요하다고 생각해서, 나중에 다시하지 말고 그냥 지금 해놓기로.

메시지는 어떻게 생성되고, 어떻게 사용되는지 정리해보자. 예시를 들면 아래와 같다.

"안녕하세요 여러분! {author}님이 새로운 PR을 제출했어요: {title}. 리뷰는 {assignee}님께 할당되었습니다."   
+ "여기서 확인할 수 있어요: [PR 링크]({link}) 꼼꼼하게 리뷰하고 피드백 부탁드려요. 감사합니다!";  

여기서 {author}, {title} 등등은 유저가 등록할 템플릿에서 필수로 있어줘야 하는 것들이다. 실제 PR이 올라오면 위 메시지는

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

이런 식으로 변신하게 될 것이다. 변신된 메시지는 메신저(discord...) 로 전달된다.

  1. 사용자는 메시지 템플릿을 등록한다. 이 템플릿은 필수 메시지에 해당하는 값들 (작성자, pr타이틀)이 어디에 있을지 정해주는 역할을 한다.
  • 이 템플릿은 MessageTemplate 라는 새 객체로 작성한다.
  • MessageTemplate 데이터는 DB에서, 회원별로(혹은 레포지토리별로) 관리한다.
  1. 등록한 템플릿에서 실제 PR 정보가 들어가야 할 곳에(예를 들어 {prTitle} 같은곳) 실제 PR의 정보를 넣어줘야 한다.

지금 일단 두 개의 동작이 나왔다. 한번 테스트를 작성해보자.

@Test  
@DisplayName("필수 placeHolder 가 없을 시 예외가 발생한다.")  
void createValidationTest() {  
    String userInput = "TEST";  
  
    assertThatThrownBy(() -> new MessageTemplate(userInput))  
            .isInstanceOf(ReviewMessageException.MissingPlaceholderException.class)  
            .hasMessage("필수 메시지 입력값이 누락되었습니다. 누락된 값 : AUTHOR_NAME");  
}  
  
@Test  
@DisplayName("ReviewMessage 객체 생성에 성공한다.")  
void createTest() {  
    String template = "안녕하세요 여러분! {author}님이 새로운 PR을 제출했어요: {title}. 리뷰는 {assignee}님께 할당되었습니다."  
            + "여기서 확인할 수 있어요: [PR 링크]({link}) 꼼꼼하게 리뷰하고 피드백 부탁드려요. 감사합니다!";  
  
    MessageTemplate messageTemplate = new MessageTemplate(template);  
    assertThat(messageTemplate).isNotNull();  
}

...

@Entity  
@NoArgsConstructor  
@Getter  
public class MessageTemplate {  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long id;  
  
    private String message;  
  
    public MessageTemplate(String message) {  
        validate(message);  
        this.message = message;  
    }  
  
    private void validate(String message) {  
        Arrays.stream(TemplatePlaceholder.values())  
                .filter(placeholder -> !message.contains(placeholder.getPlaceholder()))  
                .findFirst()  
                .ifPresent(placeholder -> {  
                    throw new ReviewMessageException.MissingPlaceholderException(placeholder.name());  
                });  
    }  
}

코드는 위와 같은 형태로 작성 해 보았다. 우리는 템플릿에 들어갈 {prTItle} 같은 녀석들을 TemplatePlaceholder 라는 enum 으로 관리하므로, 객체의 생성자에서 이를 통해 validate 할 수 있다.

그렇다면 이 템플릿에서, 필요한 부분을 실제 PR 정보로 바꾸기 위해서는 어떻게 해야할까?

  1. 컴포지션을 사용해서 VO와 같은 형태로 DefaultMessage 를 가지기
  2. 그냥 DefaultMessage 를 따로 분리해놓고 외부 서비스에서 주입받기.

나는 2번을 선택하고 싶다. 왜냐면 DefaultMessage 는 반드시 깃헙 PR의 정보가 필요하다. 그리고 깃헙 PR 정보는 외부 API를 통해서 불러와야 한다.

MessageTemplate 라는 깜찍이 도메인이 커져봐야 얼마나 커지겠냐고 생각하긴 하지만, 난 JPA 엔티티를 도메인과 비슷하게 사용하기를 원하고 있는 상태이므로 외부 API를 통해서 정보를 가져와야만 영속화 할 수 있는 형태로 구현하고 싶지는 않다.

더불어 MessageTemplate 에서 VO로 관리하게 된다면, DefaultMessage 의 정보들은 DB에 없으므로 반드시 외부에서 값을 주입받아야 한다. 의미없다고 생각했다.

따라서 DefaultMessage 는 Jpa Entity 가 아닌 나만의 깜찍한 도메인으로 남기고, 이 도메인은 외부 API 에 의해서만 생성되는 것으로 남겨두자. 이렇게 함으로써 도메인은 도메인만, 인프라쪽은 인프라만 테스트하기도 용이 할 것이다.


다시 돌아와서, 오케이! 그러면 메시지의 조합을 해서 마지막 결과를 내놓는 쪽은 어디어야 하는가? 나는 DefaultMessage, MessageTemplate 이 아닌.Service여야고 본다. (조회관련 작업은 서비스에게 주고싶다)

간단히 작성하고 테스트해보자

@Service  
@RequiredArgsConstructor  
@Transactional  
public class MessageService {  
  
    private final MessageTemplateRepository messageTemplateRepository;  
  
    // GithubPrResponse 를 주는 쪽, 그리고 assignee 를 주는 쪽을 호출하기로 한다. 혹은 호출당하기로 한다.  
    public String getDefaultMessage(String githubRepositoryId, GithubPRResponse githubPRResponse, String assignee){  
        DefaultMessage defaultMessage = githubPRResponse.toDefaultMessage(assignee);  
        MessageTemplate messageTemplate = messageTemplateRepository.findByGithubRepositoryId(githubRepositoryId).orElseThrow(  
                () -> new MessageTemplateException.MessageTemplateNotFoundException(githubRepositoryId)  
        );  
  
        String template = messageTemplate.getTemplate();  
        return defaultMessage.mergeMessage(template);  
    }  
}

...

@SpringBootTest  
class MessageServiceTest {  
  
    @MockBean  
    private MessageTemplateRepository messageTemplateRepository;  
  
    @Autowired  
    private MessageService messageService;  
  
    @BeforeEach  
    public void setUp() {  
        MockitoAnnotations.openMocks(this);  
    }  
  
    @Test  
    void testGetDefaultMessage() {  
        // arrange  
        String githubRepositoryId = "githubRepositoryId";  
        String assignee = "assignee";  
        String template = "{author} {title} {assignee} {link}";  
        GithubPRResponse githubPRResponse = new GithubPRResponse("prTitle", "prLink", "prAuthor");  
        MessageTemplate messageTemplate = new MessageTemplate(template, githubRepositoryId);  
        when(messageTemplateRepository.findByGithubRepositoryId(githubRepositoryId)).thenReturn(Optional.of(messageTemplate));  
  
        // act  
        String defaultMessage = messageService.getDefaultMessage(githubRepositoryId, githubPRResponse, assignee);  
  
        // assert  
        String expected = "prAuthor prTitle assignee prLink";  
        assertThat(defaultMessage).isEqualTo(expected);  
    }  
}

간단한 흐름은 위와 같다.

코드 전문은
요기에서

0개의 댓글