개발 중 로컬 서버에서 발생한 에러는 바로 탐지가 가능하지만,
배포 서버에서 실행 중일 때 발생한 에러는 바로 탐지가 어렵다.
그래서 에러 발생 시 메세지를 띄워주면 좋겠다고
누구나 한 번 쯤은 생각해보았을 것이다.
Slack API 를 활용하여 알림을 보낸다면
이러한 알림 서비스를 구현하고 활용할 수 있다.
다만 이 파트에서는 에러 자동 탐지 알림 대신
사용자 편의를 의한 단순 메세지 발송에 대해서만 적었다.
500 서버 에러 탐지 알림은 아래 다른 게시글에 올려두었다.
https://gengminy.tistory.com/53
⚙️ Dependency
implementation 'com.slack.api:slack-api-client:1.27.2'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
- Slack API 사용을 위한 slack api client 추가
- StringUtils 를 사용하기 위한 apache 모듈 추가 (이건 반드시 필요한 것은 아님)
🚀 Incoming Webhooks URL 발급받기
토큰을 가져와 Slack API Bean 을 만들어 등록 및 사용하는 방식도 있지만,
이 글에서는 Incoming Webhooks 라는 서드파티 앱을 추가한 후
전용 URL 을 발급받아 메세지를 전송하는 방식을 택했다.
🔍 Incoming Webhooks URL 발급받는 방법
1. 채널 세부정보 보기
Slack 워크스페이스 생성 -> 채널 오른쪽 클릭 -> 채널 세부정보 보기
2. 앱 추가 들어가기
통합 -> 앱 -> 앱 추가
3. Incoming Webhooks 앱 추가
Incoming WebHooks 검색 -> 앱 디렉터리에서 -> 설치
Slack에 추가 클릭
메세지를 받기 원하는 채널 선택 -> 수신 웹후크 통합 앱 추가 클릭
다음과 같이 웹후크 URL 이 발급된다!
이것을 슬랙 URL 로 등록하고, 메세지를 보내면 된다.
🔍 Incoming Webhooks URL 작동 테스트 하기
웹훅 URL 발급받은 창에서 아래로 내리면
다음과 같이 예시 요청을 보낼 수 있도록 샘플 코드가 주어진다.
curl -X POST --data-urlencode "payload={\"channel\": \"#webhook\", \"username\": \"webhookbot\", \"text\": \"이 항목은 #개의 webhook에 포스트되며 webhookbot이라는 봇에서 제공됩니다.\", \"icon_emoji\": \":ghost:\"}" https://hooks.slack.com/services/{고유 문자열}
위에 있는 요청을 보냈을 때 응답으로 ok 가 온다면
송수신이 정상적으로 이루어짐을 확인할 수 있다.
슬랙 채널에서도 알림이 성공적으로 오는 모습
🚀 Slack URL 등록 비즈니스 로직
이제 채널 URL 발급받는 방법을 알았으니
채널을 등록하고 메세지를 보내보도록 하자.
📝 HostController
@RestController
@RequestMapping("/v1/hosts")
@RequiredArgsConstructor
public class HostController {
private final UpdateHostSlackUrlUseCase updateHostSlackUrlUseCase;
//... 생략
@PatchMapping("/{hostId}/slack")
public HostDetailResponse patchHostSlackUrlById(
@PathVariable Long hostId,
@RequestBody @Valid UpdateHostSlackRequest updateHostSlackRequest) {
return updateHostSlackUrlUseCase.execute(hostId, updateHostSlackRequest);
}
}
특정 호스트의 아이디를 통해 호스트를 가져온다
📝 UpdateHostSlackRequest
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UpdateHostSlackRequest {
@NotBlank(message = "올바른 슬랙 URL 을 입력해주세요")
@URL(message = "올바른 슬랙 URL 을 입력해주세요")
private String slackUrl;
}
호스트 슬랙 업데이트를 위한 요청 DTO
단순하게 String 만 하나 가져오고
DTO 내부에서 Validation 한다.
📝 UpdateHostSlackUrlUseCase
@UseCase
@RequiredArgsConstructor
public class UpdateHostSlackUrlUseCase {
private final HostService hostService;
private final HostAdaptor hostAdaptor;
private final HostMapper hostMapper;
private final SlackMessageProvider slackMessageProvider;
@Transactional
@HostRolesAllowed(role = MANAGER, findHostFrom = HOST_ID)
public HostDetailResponse execute(Long hostId, UpdateHostSlackRequest updateHostSlackRequest) {
final Host host = hostAdaptor.findById(hostId);
final String slackUrl = updateHostSlackRequest.getSlackUrl();
hostService.validateDuplicatedSlackUrl(host, slackUrl);
try {
slackMessageProvider.register(slackUrl);
return hostMapper.toHostDetailResponse(hostService.updateHostSlackUrl(host, slackUrl));
} catch (UnknownHostException e) {
throw InvalidSlackUrlException.EXCEPTION;
}
}
}
두둥 프로젝트에서는 Service 끼리의 참조에서 생기는
순환 참조와 복잡도 증가 등을 막기 위해
UseCase 라는 레이어를 하나 더 도입했다.
즉 UseCase 에서 종합적인 비즈니스 로직이 실행된다.
SlackUrl 의 중복 주입을 검증하고
레포지토리에서 Host 를 가져와 주입하는 로직이다.
try catch 로 감싼 이유는
이후 내부에서 반환되는 UnknownHostException 을 깔끔하게 처리하기 위함이다.
반환값은 Setter 호출 및 Response DTO 생성이니
무시해도 된다.
📝 SlackMessageProvider
@Service
@RequiredArgsConstructor
@Slf4j
public class SlackMessageProvider {
@Value("${slack.webhook.username}")
private String username;
@Value("${slack.webhook.icon-url}")
private String iconUrl;
/** 이벤트 핸들러 자체에서 비동기로 실행하기 때문에 @Async 어노테이션 지움 */
public void sendMessage(String url, String text) {
// 슬랙 url 이 null 일경우 안보냄.
if (Objects.isNull(url)) return;
try {
doSend(url, text);
} catch (Exception ignored) {
}
}
/** 호스트가 존재하는 지 확인하기 위해 동기로 처리 */
public void register(String url) throws UnknownHostException {
final String text = "두둥 슬랙 알림이 성공적으로 등록되었습니다!";
doSend(url, text);
}
private void doSend(String url, String text) throws UnknownHostException {
final Slack slack = Slack.getInstance();
final Payload payload =
Payload.builder().text(text).username(username).iconUrl(iconUrl).build();
try {
String responseBody = slack.send(url, payload).getBody();
if (!StringUtils.equals(responseBody, "ok")) {
throw new UnknownHostException("올바른 슬랙 URL이 아닙니다.");
}
} catch (UnknownHostException error) {
// 호스트가 존재하지 않을 경우 abort
throw error;
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
}
이제 슬랙 API 를 본격적으로 사용하는 부분이다.
doSend 라는 private method 에서는
해당 URL 에 텍스트를 보내는 로직을 수행한다.
아까 전에 해당 URL 에 Payload 를 담아 post 요청을 보내면
응답 값으로 ok 가 반환되는 것을 확인했다.
요청에 실패하면 슬랙 API 자체에서 UnknownErrorException 가 던져지는데,
이를 응용하여 "ok" 가 반환되면 통과시키고
그렇지 않으면 UnknownErrorException 을 임의로 던져주도록 변형했다.
IOException 은 슬랙 메세지 전송 실패에 대해 처리하기 위해 추가했다.
Slack 인스턴스를 가져와서 Payload 에 원하는 값을 넣고
slack.send() 메소드를 호출하면 메세지 전송이 된다.
다른 슬랙 알림 같은 경우는 @Async 를 통해 비동기로 처리했으나,
등록 로직 같은 경우는 응답이 제대로 오는지 안오는지 먼저 확인해야 하기 때문에
동기로 처리하였다.
🚀 Slack 비동기 메세지 전송 비즈니스 로직
이벤트를 간편하게 구현하기 위해 커스텀 객체들이 많으니 감안하고 봐주시길
이벤트 발행과 처리에 대해서 이 글에서 상세하게 다루지는 않겠다.
📝 Host
@Getter
public class Host extends BaseTimeEntity {
//...생략
public void setSlackUrl(String slackUrl) {
if (StringUtils.equals(this.slackUrl, slackUrl)) {
throw DuplicateSlackUrlException.EXCEPTION;
}
Events.raise(HostRegisterSlackEvent.of(this));
this.slackUrl = slackUrl;
}
//...생략
}
Events 오브젝트는 ApplicationEventPublisher 를 감싸서 임의로 구현한 것이다.
publishEvent 의 의미라고 생각하면 되겠다.
Host 내부에서 setter 를 호출할 때 호스트 등록 이벤트를 발생시켰다.
📝 HostRegisterSlackEvent
@Getter
@Builder
@ToString
public class HostRegisterSlackEvent extends DomainEvent {
private final Long hostId;
private final String hostName;
public static HostRegisterSlackEvent of(Host host) {
return HostRegisterSlackEvent.builder()
.hostId(host.getId())
.hostName(host.toHostProfileVo().getName())
.build();
}
}
Events 에서 DomainEvent 라는 객체로 통합 처리하기 위해 이를 상속받는다.
발행시킬 이벤트는 POJO 로 구성한다.
📝 DomainEvent
@Getter
public class DomainEvent {
private final LocalDateTime publishAt;
public DomainEvent() {
this.publishAt = LocalDateTime.now();
}
}
DomainEvent 객체는 그냥 이렇게 생겼다.
📝 HostRegisterSlackEventHandler
@Component
@RequiredArgsConstructor
@Slf4j
public class HostRegisterSlackEventHandler {
private final HostAdaptor hostAdaptor;
private final SlackMessageProvider slackMessageProvider;
@Async
@TransactionalEventListener(
classes = HostRegisterSlackEvent.class,
phase = TransactionPhase.AFTER_COMMIT)
public void handle(HostRegisterSlackEvent hostRegisterSlackEvent) {
final Host host = hostAdaptor.findById(hostRegisterSlackEvent.getHostId());
final String message = HostSlackAlarm.slackRegistrationOf(host);
slackMessageProvider.sendMessage(host.getSlackUrl(), message);
}
}
발행시킨 이벤트를 리스닝하고 처리해주는 핸들러를 구현한다.
@Async 를 통해 비동기로 처리하며,
@TransactionalEventListener 의 phase 를 AFTER_COMMIT 으로 설정해
트랜잭션이 종료되어 성공적으로 Host 엔티티에 값이 들어간 이후에
이 이벤트가 실행되도록 했다.
그래서 아래의 메세지가 성공적으로 나오게 된다.
이를 활용하여 각종 호스트 유저 가입, 티켓 판매 알림 등등
알림을 구현하여 유저에게 편의성을 제공하였다.
'🚀 프로젝트 > 🥁 두둥' 카테고리의 다른 글
[DuDoong] 두둥 프로젝트 관련 글 정리 (0) | 2023.03.15 |
---|---|
[Spring] 스프링 500 에러 발생 시 Slack 알림 전송하기 (슬랙 봇) (0) | 2023.03.15 |
[Spring] 스프링 날짜 타입 JSON 변환 및 포맷팅하기 - @JsonFormat, @JacksonAnnotationsInside (0) | 2023.02.27 |
[Spring] 스프링 Custom Enum Deserializer 구현으로 JSON Enum null 로 파싱하기 (0) | 2023.02.21 |
[Spring] 스프링 Enum Validator Reflection 으로 개선 및 구현하기 (0) | 2023.02.21 |