티스토리 뷰
현재 운용되고 있는 시스템의 대부분은 자체 비즈니스 솔루션을 위해 내재된 API 를 주로 사용하곤 하지만, 상대적으로 많은 양의 데이터를 처리하거나 데이터로 적재하기 위해서 대기업이나 다른 기업에서 제공해주는 오픈 소스 API 또는 외부 라이브러리를 자주 사용하곤 합니다. 기본적으로 안정적이게 운영되는 서비스들은 DDoS 공격으로부터 서비스를 보호하거나, API Resource의 사용성의 공평성을 제공해주기 위해 API Rate Limit을 두어 API Endpoint에 사용량을 제한하곤 합니다.
보통 내부에서 사용하는 프로그램들에는 기본적으로 보안 로직이 적용되어 있어 인증/인가가 된 사용자들만 접근을 가능하게끔 하는 것이 일반적이지만, 메신저 API 를 호출하는 경우는 이에 맞지 않는 것 같습니다.
서비스의 가용성(API Level, Network Level, Container Level, CPU Level)을 유지하기 위해 방어 로직을 작성하는 것이 좋고 제가 생각한 해결 방안은 2가지가 있습니다.
- 클라이언트의 요청이 오게 된 경우 알림 발송이 작동되기 때문에 선택 사항 1번에서 설명한 것과 같이 메시징 큐 시스템을 적용한 후, 메시지 구독에 스케줄링을 거는 방법
- 서버 내에 메시지를 발송하는 Thread 에 대기 시간을 걸어 다시 재전송을 하는 방법 (선호하지 않음)
1. Schedule 을 적용하는 방법
Spring Framework 를 사용한다 가정하였을 때 기본적으로 제공해주는 의존성에 포함이 되어 있어 따로 의존성 추가를 하지 않아도 가능합니다. Spring 3.1 이상부터 지원
실행하고자 하는 서비스의 main 함수에 @EnableShceduling 의 Annotation을 적용합니다.
import org.springframework.scheduling.annotation.Scheduled;
@SpringBootApplication
@EnabledScheduling
public class ServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceApplication.class, args);
}
}
그리고 구독하고자 하는 클래스에 스케줄링을 적용합니다. 이를 알기 위해서는 Cron 표현식을 알아야 하는데 해당 내용은 제 블로그에 정리해두었습니다. https://abbo.tistory.com/26
가장 먼저 Scheduler 클래스를 구현해보도록 하겠습니다.
AlertServiceScheduler.java
@Component
@RequiredArgsConstructor
public class AlertServiceScheduler {
private final SlackService slackService;
private final AlertMessageQueueRepository alertMessageQueueRepository; // 메시지 큐를 구독하여 데이터를 저장하는 리포지토리
/**
* Cron 표현식에 설정한 대로 1분마다 스케줄이 수행됩니다.
*/
@Scheduled(cron = "0 0/1 * * * *")
public void subscribeMessagingQueue() {
Optional<AlertMessageQueue> optional = alertMessageQueueRepository.findOldestAndNotSend();
if(optional.isPresent()) {
AlertDTO.Request request = optional.get().toRequest();
slackService.to(request).send();
}
}
/**
* 구독한 큐에 담긴 메시지를 데이터로 저장합니다.
* 이벤트 리스너를 통해 new SimpleMessage<AlertMessageQueueRequestDTO() 가
* 생성되는 경우 해당 로직이 호출되는 방법으로 변경하였습니다.
*
* 또, MessageQueue 의 비동기적인 상황을 고려하여 @Async 를 추가하였습니다.
*
* @param message
*/
@Async
@RabbitListener(queues = "queueName")
@EventListener
public void subscribeEvent(@Payload SimpleMessage<AlertMessageQueueRequestDTO> message) {
alertMessageQueueRepository.save(meesage.getData().toEntity());
}
}
아래의 클래스는 메시지를 전달받아 Object로 변환하는 클래스입니다.
SimpleMessage.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SimpleMessage<T> {
private T data;
private String action;
private String subject;
}
다음으로 이벤트 리스너로 전달 받은 메시지를 DTO화 하는 클래스입니다.
AlertMessageQueueRequestDTO.java
@AllArgsConstructor
public class AlertMessageQueueRequestDTO {
private List<String> target = new ArrayList<>();
private ServerSeverity severity;
private String mesaage;
/**
* DTO를 Entity 모델로 변환합니다.
* @return
*/
public AlertMessageQueue toEntity() {
return new AlertMessageQueue(this.target, this.severity, this.mesaage);
}
}
다음 클래스는 MessageQueue로 담겨있는 메시지들을 데이터화 시키는 작업입니다.
데이터베이스에 담기 위해 @Entity 로 선언하였고, target 에 대한 정보는 string으로 저장하여 나중에 DTO 변환시 List로 변환이 가능합니다.
AlertMessageQueue.java
@Entity
@Data
@Table(name = "alert_message_queues")
public class AlertMessageQueue {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "alert_message_queue_id")
private Long alertMessageQueueId;
@Column(name = "message", columnDefinition = "TEXT")
private String target;
@Enumerate(EnumType.STRING)
private ServerSeverity severity;
private String message;
private boolean notSend;
public AlertMessageQueue(List<String> target, ServerSeverity severity, String message) {
this.target = target.toString();
this.severity = severity;
this.message = message;
this.notSend = true;
}
public AlertDTO.Request toRequest() {
AlertDTO.Request request = new AlertDTO.Request();
request.setTarget(Arrays.asList(this.target.split(",")));
request.setSeverity(this.severity);
request.setMessage(this.message);
return request;
}
}
마지막으로 AlertMessageQueueRepository 을 추가하고 SlackServiceImpl 에서 메시지 발송이 실패했을 시 해당 테이블에 적재하는 것을 추가하여 누락된 건이 없도록 합니다.
AlertMessageQueueRepository.java
@Repository
public interface AlertMessageQueueRepository extends JpaRepository<AlertMessageQueue, Long> {
@Query(value = "SELECT * FROM alert_message_queues WHERE not_send = true "
+ "ORDER BY alert_message_queue_id ASC LIMIT 1", nativeQuery = true)
Optional<AlertMessageQueue> findOldestAndNotSend();
}
SlackServiceImpl.java
import java.util.ArrayList;
public class SlackService {
private final AlertMessageQueueRepository alertMessageQueueRepository;
public AlertDTO.FailResponse send() {
final List<String> failTargets = new ArrayList<>();
final List<UserGroup> userGroups = this.request.hasAll() ?
userGroupRepository.findAll()
: userGroupRepository.findByGroupnameIn(request.toUserGroups());
final List<User> users = request.hasAll() ?
userRepository.findAll() : userRepository.findByNicknameIn(request.toUsers());
Set<User> receivedUsers = new HashSet<>(); // 보낸 사용자를 체크하기 위한 HashSet
Set<String> slackChannelTargets = users.stream().map(User::getMemberId)
.collect(Collectors.toSet()); // Slack 알림 발송을 위한 HastSet
for (UserGroup userGroup : userGroups) {
slackChannelTargets.removeAll(
userGroup.getUsers().stream().map(User::getMemberId).collect(Collectors.toList()));
receivedUsers.addAll(userGroup.getUsers());
slackChannelTargets.add(userGroup.getGroupname());
}
this.userCount = receivedUsers.size();
try {
this.send = true;
for (String channel : slackChannelTargets) {
final ChatPostMessageResponse response = slack.methods(
slackProperties.getToken())
.chatPostMessage(
req -> req.channel(channel).text(this.request.createMessage()));
if (!response.isOk()) {
log.error(response.getError());
// 실패한 경우 미발송된 대상들을 위해 다시 리스트에 추가합니다.
failTargets.add(channel);
this.send = false;
}
}
// 요청에 대한 응답이 실패가 1건이라도 있는 경우 실패로 간주할 수 있기 때문에
// 이 때 AlertMessageQueue 에 넣어주어 스케줄링이 돌 때마다 재요청을 하도록 합니다.
if (this.send == false || !failTargets.isEmpty()) {
alertMessageQueueRepository.save(
new AlertMessageQuery(failTargets, this.request.getSeverity(),
this.request.getMessage())
);
// 전체 발송이 되지 않았다는 응답 결과를 보내주어도 좋습니다.
return AlertDTO.FailResponse.of(failTargets, users, userGroups);
}
} catch (Exception exception) {
log.error(ExceptionUtils.getStackTrace(exception));
throw SlackSendException.create(exception);
}
return this;
}
}
'Study' 카테고리의 다른 글
| 스티브 잡스가 지켰던 '회의 - 3가지 원칙' (0) | 2022.04.23 |
|---|---|
| 물건 정리를 잘하는 사람과 일 잘하는 사람의 상관관계 (0) | 2022.04.22 |
| 정규표현식 공부하기 좋은 날입니다 (0) | 2022.04.22 |
| 코로나 자가키트 사용 방법과 확진 그리고 열흘 후.. (0) | 2022.03.05 |
| 코로나 격리 해제 후 생활지원금 신청 후기 (0) | 2022.03.03 |
| 코로나 5일차 증상과 대처 방법 (1) | 2022.02.27 |