Discord Webhook로 메시지를 보내는 Spring Boot Web App

코코블루·2021년 8월 18일
1

시작하며

개인적으로 Twitch 방송을 자주봅니다. 작업할 때는 잔잔한 라디오 방송, 심심할 때는 역동적인 게임 방송을 봅니다. 그러다보니 자연스럽게 본방 사수하는 방송들이 생기게되었습니다. "본방사수!"를 외치며 처음부터 끝까지 보고자하지만, Twitch에서는 데스크톱 환경에서는 알림을 보내주지 않습니다. 그래서 생각해본 방법이 있습니다.

"Twitch API를 써서 Discord Webhook으로 방송 알림을 보내보면 어떨까!?"하는 생각이었습니다. 인터넷을 보아하니 IFTTT를 이용하면 쉽게 바로 설정이 가능하다고 하더라고요. 하지만, Webhook의 개념도 이해하며 예제를 만들어보고 싶었습니다.

어떻게 진행되나요?


사실, Application은 완성되어 있고, 실제로 개인적으로 사용 중입니다. 하지만, "하는 것은 쉬워도 설명은 어렵다."라고 했던가요. 어떤 순서로 설명을 드려야할까 하는 생각이 들어서 많이 막혔었는데, 순차적으로 진행해보고자합니다. 즉, 이 글은 시리즈로 진행됩니다.

이 시리즈는 아래와 같은 순서로 진행될 예정입니다!

1. Discord Webhook로 메시지를 보내는 Spring Boot Web App 소개 (지금 글)

Github Tag: Ver 1.0
Form을 통해 제출된 데이터를 기반으로 메시지를 Webhook으로 보내는 코드를 작성합니다. 즉, 이번 글의 경우에는 꼭 위와 같은 목표가 아니라도 단순히 Discord의 Webhook 기능을 통해 메시지를 보내는 것을 해보고 싶으신 분들도 사용이 가능합니다!

2. Twitch API 문서 읽어보기

Twitch에서 건네주는 데이터를 Web App에서 처리할 수 있게 하기 전에 Twitch API에서 어떤 데이터를 받을 수 있고, 어떤 식으로 데이터가 오는지 확인해보겠습니다.

3. Twitch의 요청을 받기 위한 코드 작성

Github Tag: Ver 1.0.1
Twitch에서 건네주는 데이터를 Web App에서 처리할 수 있도록 클래스를 작성해보겠습니다.

4. 데이터를 처리할 DB 구축 및 코드 작성

Github Tag: Ver 1.0.2
알림을 받기 원하는 Twitch User와 정보를 전송할 때 사용할 데이터를 저장하는 데이터베이스를 구축하겠습니다. DB의 경우에는 RDBMS를 사용할지, 아니면 NOSQL을 사용할지 아직 정확히 정하지 못했습니다. 기존에는 mariaDB를 사용했는데, 아마도 데이터 특성상 NOSQL이 더 편할 것 같아 이 방향으로 좀 기울긴 했습니다만, 확실치 않습니다. ㅠㅠ
이마저도 "별도 DB 서버 없이 사용할 수 있도록 Json 파일에 넣어둘까?"라는 생각이 들어서 조금 더 시간이 지난 후에 정하겠습니다.

5. 주기적으로 데이터를 갱신하는 코드 작성

Github Tag: Ver 1.0.3
Twitch에서 알림을 받기 위해서는 주기적으로 Twitch 측에 "제가 이 데이터를 원합니다!"라고 알려야합니다. 이를 Spring의 Cron을 이용하여 주기적으로 데이터를 요청하는 코드를 작성하겠습니다.

6. Twitch 데이터와 결합하여 Webhook로 실제 데이터 전송

Github Tag: Ver 2.0
Twitch가 Web App에 전달해주는 데이터를 갖고 Discord Webhook로 실제 데이터를 전송해보는 코드를 작성하고 테스트 해보겠습니다.

이번 글의 구성도


이번에 소개하는 Web App의 흐름은 위와 같습니다. 상당히 단순합니다.



위와 같은 Form에 Webhook URL 등의 정보를 넣고 Send!를 누르면 서버에서 보냅니다.

Source

소스에서 특별하게 보셔야할 부분이 있다면, DTO Package에 있는 'WebhookRequest.java'와 'DiscordWebhookMessage.java'입니다.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class WebhookRequest {
    // HTML Form에 맞게 Mapping
    private String webhookUrl;
    private String sender;
    private String senderProfile;
    private String message;
    
    // HTML Form에 맞게 Mapping 된 데이터를 Discord Webhook에 맞게 변환하는 메소드
    public DiscordWebhookMessage toDiscordWebhookMessage() {
        log.info("Called toDiscordWebhookMessage");

        DiscordWebhookMessage discordWebhookMessage = new DiscordWebhookMessage();
        discordWebhookMessage.setUsername(getSender());
        discordWebhookMessage.setAvatarUrl(getSenderProfile());
        discordWebhookMessage.setContent(getMessage());

        return discordWebhookMessage;
    }
}

위는 View에서 받은 데이터를 Mapping 받는 DTO입니다. 여기서 'toDiscordWebhookMessage()'라는 메소드가 있는데, 이 메소드가 Discord Webhook에서 요구하는 내용을 만족한 DTO로 변환하는 메소드입니다.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Repository // 없어도 상관 없음. 다음 commit에서 삭제 예정
public class DiscordWebhookMessage {
    // Json으로 변환될 때의 Key 값 정의
    @JsonProperty("username")
    private String username;
    @JsonProperty("avatar_url")
    private String avatarUrl;
    @JsonProperty("content")
    private String content;
}

위 DTO가 실제로 Discord 서버에 전송될 Data 입니다. @JsonProperty 어노테이션을 통해 Json으로 변환할 때 Key 값을 정의합니다. 만일, 이를 정의하지 않는다면 'avatar_url'이 카멜케이스가 적용된 채 전송되어 에러를 일으킬 것입니다.

@Service
public class DiscordMessageServiceImpl implements DiscordMessageService {
    @Override
    public boolean sendDiscordWebHook(WebhookRequest webHookRequest) {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();

        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
        HttpEntity<DiscordWebhookMessage> entity = new HttpEntity<>(webHookRequest.toDiscordWebhookMessage(), headers);

        RestTemplate rt = new RestTemplate();

        try {
            ResponseEntity<String> response = rt.exchange(
                    webHookRequest.getWebhookUrl(), //{요청할 서버 주소}
                    HttpMethod.POST, //{요청할 방식}
                    entity, // {요청할 때 보낼 데이터}
                    String.class);

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }
}

전송의 경우 DiscordMessageService가 수행합니다. Spring Framework에서 지원하는 RestTemplate를 이용하여 POST request를 Discord 서버에 보냅니다.

테스트해보자!

이렇게 만들어진 Application을 테스트하는 것은 상당히 쉽습니다. 첫번째로, Discord에서 Webhook URL을 받아보겠습니다.

위 순서로 진행하시면 되는데, Webhook의 이름의 경우, 관리를 위한 이름일 뿐이기에 이 Webhook이 어떤 용도로 쓰이는지 기재하시면 나중에 관리하시는데 편리할겁니다.

Form에서 채워 넣어야할 내용은 위 사진을 참고하시면 됩니다.
'Sender Profile URL'의 경우에는 http로 시작하는 이미지의 링크를 제공해야하는데요. Multipart를 이용해서 이미지를 업로드해서 처리하는 방법이 있겠습니다. 다음에 기회가 되면 이미지를 업로드한 뒤, 링크를 첨부하는 코드를 작성해보겠습니다. 단, 이마저도 localhost 환경에서는 테스트가 불가능하기 때문에 Azure나 AWS 등의 클라우드 서비스와 연동하는 방법도 생각해볼 수 있겠습니다.

이 내용은 상당히 간단한 편이라서, "본격적인 코딩이다!"라고 하기에는 좀 어렵습니다. 그래도 지금 진행하고 있는 글을 목표하는 것이 아닌 Discord Webhook를 이용한 또 다른 Java App을 생각하고 계신다면, 이 글이 도움이 되실 것 같습니다.

질문 사항은 Github Issue나 공개 댓글로 작성 부탁드립니다. 감사합니다!

Github에서 소스 보기

profile
Have A Happy Coding Time!

2개의 댓글

comment-user-thumbnail
2022년 6월 5일

안녕하세요. 저도 비슷한 봇을 개발하려고 찾아보다가 궁금한 점이 있어 댓글을 남깁니다.
현재 트위치 API 문서에 webhooks guide 파트는 통째로 사라졌는지 보이지가 않습니다. 지금도 개발하신 봇이 여전히 잘 작동되고 있을까요?
그리고 글에 첨부하신 깃헙 레포지토리 링크는 삭제됐는지 확인이 안 되네요. 트위치 API 문서 중 Get Streams가 공개되어 있긴 한데, 역시 웹훅을 쓰는 것이 서버의 리소스를 절약하는 측면에서 효율적인 방법일 것 같아 어떻게 해결하셨는지 궁금한 마음입니다.
마지막으로 예전에 제가 python으로 봇을 만든 적이 있습니다만, python에서 asyncio.sleep를 사용하여 컴퓨터의 부담을 줄이기 위해 반복되는 요청을 일시적으로 멈추는데, 시간 차이가 조금은 생기더군요. 여기서는 'Spring의 Cron을 이용하여 주기적으로 데이터를 요청'한다고 하셨는데, 라이브 알림이 발생할 때 시간 차이가 조금씩 발생하지는 않나요?
답변에 미리 감사 인사드립니다. :)

1개의 답글