-
Notifications
You must be signed in to change notification settings - Fork 0
사례 3. Stack을 활용하여 알림 전송 주기 변경(매 로그 데이터 저장시 -> 모아서 배치)으로 알림 기능 개선하여 DX 향상 #78
Copy link
Copy link
Open
Description
배경 : 기존 방식(매 로그 저장시마다 전송) 문제 파악
-
다음 그림처럼 배치 프로세스별(Job, Step, Task) 로그 데이터가 저장될 때마다 디스코드로 알림을 전송하는 방식이었습니다.

-
그런데, 이 알림이
주간 이슈 배치 Close 작업의 경우, (최대 스터디 인원인 10명인 상황 가정)1분도 안되는 시간에 22번의 알림이 전송되면서 디스코드 채널이 과도하게 시끄러워지는 문제가 발생했습니다. 따라서, 이를 개선시키고 싶었습니다
개선 과정
[개요]
1. 개선책(Stack을 활용한 배치 전송)
2. 추가 보완점 개선
(1) 단점 보완 1 : 유효성 검사와 사전 전송 방식으로 메시지 및 파일 제한 초과 문제 해결
(2) 단점 보완 2 : 추가적인 서비스 클래스 도입으로 로직 단순화1. 개선책(Stack을 활용한 배치 전송)
- Stack을 사용하여 모든 로그 저장이 완료될 때까지 로그 저장 결과 알림 메시지를 모아두었다가, 마지막 로그 저장 시점에 한 번에 전송 방식으로 변경했습니다.
(1) Stack 선택한 이유
1) 로그 데이터 저장 알림 메시지의 특징
- 로그 데이터는 작업(Job) → 단계(Step) → 개별 작업(Task) 순서로 호출되지만, 실제 완료 순서는 반대입니다. Task 로그가 먼저 저장되고, 그다음 Step, 마지막으로 Job 로그가 저장됩니다.
- 로그 데이터는 작업의 전체 흐름을 파악하는 데 사용되므로, 사용자 인터페이스(UI)에서 작업이 호출된 순서대로 메시지를 보여주는 것이 더 직관적입니다.
2) 각 자료구조의 특징
- Queue (FIFO): 먼저 저장된 로그가 먼저 꺼내집니다. 따라서, 저장된 순서대로 작업을 처리할 때 유리합니다.
- Stack (LIFO): 나중에 저장된 로그부터 먼저 꺼내집니다. 따라서, 저장된 순서와 반대로 작업을 처리할 때 유리합니다.
3) Stack이 적합한 이유
- 호출 순서대로 알림 메시지를 전송하는 것이 작업 흐름을 파악하기에 더 직관적입니다.
- 따라서, Queue의 경우, 역순이라는 사실을 인지하고 읽어야 하기 때문에 불편합니다.
- 반면, Stack을 사용하면 선입후출(LIFO) 방식으로 작업 순서대로 알림을 관리할 수 있어, UI적으로 더 나은 사용자 경험을 제공합니다.
- 따라서, Stack이 적합하다고 생각했습니다.
(2) 배치 전송으로 인한 단점
- 이로 인해 알림 횟수가 감소되어 디스코드 채널의 산만함을 해결한 장점을 얻었지만, 기존 방식와 비교했을 때, 다음과 같은 단점도 존재했습니다.
단점 1. 중간에 서버가 중단되었을 때, Stack에 저장된 알림 메시지가 소실될 우려가 있다.
단점 2. 모아서 보내는 방식이므로, 기존 방식에 비해 메시지 또는 첨부 파일 제한 초과 문제 발생 가능성이 높다.
단점 3. 기존 방식에 비해 로직이 상대적으로 복잡질 수 있다. (예 : Stack 저장 로직, 유효성 검사 로직, 디스코드 보내기 로직 등)- 단점 1의 경우, 크게 문제될 것이 없다고 생각했습니다. 왜냐하면, 로그 저장 알림 메시지은 필수 기능이 아니라 부가 긴으으로, 좋은 기능이기 때문에, 알림 메시지가 서버 중단으로 중간에 소실 되더라도, 문제가 되지 않는다고 생각했기 때문입니다.
- 다만, 단점 2와 단점3은 보완할 필요가 있다고 생각하여, 다음과 같은 방법으로 보완했습니다.
2. 추가 보완점 개선
(1) 단점 보완 1 : 유효성 검사와 사전 전송 방식으로 메시지 및 파일 제한 초과 문제 해결
1) 문제
- 디스코드에서 한 번에 보낼 수 있는 메시지의 최대 글자 수는 2000자이고, 파일은 총 10개라는 제한이 있습니다. 로그 저장 결과를 알림으로 전송할 때, 이 제한을 초과하는 경우 메시지가 잘리거나 파일이 누락되는 문제가 발생할 수 있습니다.
2) 해결책
- 알림 메시지와 파일을 Stack에서 꺼내 생성할 때, 유효성 검사를 통해 한도 초과가 예상되는 경우에는 기존 메시지와 파일을 먼저 전송한 후, 새로운 메시지를 만들어 전송하도록 코드를 개선했습니다.
3) 구현 코드
public void sendBatchProcessResultsNotification() {
List<UnSavedLogFile> logFiles = new ArrayList<>(); // 전송할 로그 파일 목록
StringBuilder currentMessage = new StringBuilder(""); // 전송할 메시지 내용
while (!logSaveResultStack.isEmpty()) {
LogSaveResult logSaveResult = logSaveResultStack.pop();
String newMessage = logSaveResult.message();
// 메시지 또는 파일이 최대 허용 범위를 초과하는지 확인
if (
currentMessage.length() + newMessage.length() > MAX_DISCORD_MESSAGE_LENGTH
|| logFiles.size() + logSaveResult.unSavedLogFiles().size() > MAX_DISCORD_FILE_COUNT
) {
// 현재 메시지와 파일을 디스코드에 전송
notificationService.sendMessageWithFiles(currentMessage.toString(), logFiles);
// 메시지 및 파일 목록 초기화
currentMessage = new StringBuilder(newMessage);
logFiles = new ArrayList<>(logSaveResult.unSavedLogFiles());
} else {
// 현재 메시지에 새 메시지를 추가
currentMessage.append("\n").append(newMessage);
// 현재 파일 목록에 새 파일들을 추가
logFiles.addAll(logSaveResult.unSavedLogFiles());
}
}
// 남은 메시지와 파일들을 최종 전송
if (currentMessage.length() > 0) {
notificationService.sendMessageWithFiles(currentMessage.toString(), logFiles);
}
}4) 결과
- 그 결과, 디스코드의 메시지 및 파일 전송 제한을 효과적으로 관리하여, 로그 저장 결과를 안정적으로 전송할 수 있게 되었습니다.
(2) 단점 보완 2 : 추가적인 서비스 클래스 도입으로 로직 단순화
1) 배경
LogDirectSaveGoogleSheetsService클래스의 경우, 개선 작업 전에는 Google Sheets에 데이터를 저장한 후, Discord로 알림을 전송하는 기능만 수행했었습니다.
# 개선 전 LogDirectSaveGoogleSheetsService 클래스의 책임
책임 1. Job, Step, Task 로그 Google Sheets에 저장
책임 2. 로그 저장 실패 처리
책임 3. 로그 저장 결과 Discord로 알림- 그런데, 개선 작업을 해보니,
LogDirectSaveGoogleSheetsService클래스가 다음과 같이 책임이 과중되어서, 코드가 복잡해지는 문제가 있었습니다.
# 개선 전 LogDirectSaveGoogleSheetsService 클래스의 책임
책임 1. Job, Step, Task 로그 Google Sheets에 저장
책임 2. 로그 저장 실패 처리
책임 3. 로그 저장 결과 메시지 Stack에 저장
책임 4. 배치 전송을 위한 유효성 검사
책임 5. 로그 저장 결과 Discord로 알림- 이외에도 부수적인 책임(예 : 저장 실패 로그 파일 생성 등)도 함께 존재해서, 책임 분리를 통해 코드를 단순화시키고 싶었습니다.
2) 개선 방안 : 스프링 이벤트(Spring Event) 활용 vs 추가적인 서비스 클래스 도입
- 책임 분리 방법으로 크게 2가지를 생각했고, 각각의 장단점은 다음과 같습니다.
| 스프링 이벤트 활용 방법 | 추가적인 서비스 클래스 도입 | |
|---|---|---|
| 장점 | 1. 비동기 처리 용이: 스프링의 비동기 지원을 활용하여 이벤트를 비동기적으로 처리할 수 있습니다. 2. 유연한 확장성: 새로운 기능이나 서비스가 필요할 때, 기존 코드를 수정하지 않고 새로운 이벤트를 추가하거나 기존 이벤트에 대한 리스너를 추가함으로써 유연하게 확장할 수 있습니다. 3. 관심사 분리로 유지보수성 향상 |
1. 간단한 방법으로 책임 분리 가능: 각 서비스가 특정 책임을 가지며, 책임이 명확하게 분리됩니다. 2. 코드 이해 용이: 서비스 클래스가 명확한 역할을 갖고 있어, 코드의 이해도가 높아지고 디버깅이 상대적으로 쉬워집니다. 3. 복잡한 이벤트 시스템 학습 없이 사용 가능: 기존 객체 지향 설계 원칙을 쉽게 적용할 수 있습니다. |
| 단점 | 1. 복잡성 증가: 이벤트 발생 및 구독, 처리 과정이 복잡해질 수 있으며, 이벤트 흐름을 추적하고 관리하기 어려울 수 있습니다. 2. 디버깅 어려움: 이벤트의 발생 및 처리 흐름을 추적하고 이해하는 데 어려움이 있을 수 있습니다. 3. 학습 곡선: 스프링 이벤트 시스템에 대한 이해와 설정에 일정한 학습 곡선이 있을 수 있습니다. |
1. 확장성 문제: 새로운 기능 추가 시 기존 서비스 클래스를 수정하거나 새로운 클래스를 추가해야 하며, 이로 인해 유지보수가 어려울 수 있습니다. 2. 비동기 처리 어려움: 비동기 처리를 직접 구현해야 하며, 이는 코드 복잡성을 증가시킬 수 있습니다. 3. 이벤트 기반의 유연성 부족: 스프링 이벤트에 비해 기능 확장에 있어 유연성이 떨어질 수 있습니다. |
3) 최종 개선책 선택
-
최종적으로
LogSaveNotificationService라는 추가적인 서비스 클래스를 도입하기로 결정했습니다. 이유는 다음과 같습니다.- 복잡한 코드 단순화: 스프링 이벤트는 책임 분리에 효과적이지만, 코드의 양이 많아지고 복잡도가 증가하여 오히려 코드 관리와 디버깅이 어려워질 수 있었습니다. 이벤트와 리스너의 수가 많아지면, 시스템이 비대해지고 유지보수가 복잡해지는 경향이 있었습니다.
- 비동기 처리 필요 없음: 비동기 처리가 필수적이지 않은 상황에서는 스프링 이벤트의 비동기 처리 기능이 오히려 부담이 될 수 있었습니다. 동기 작업으로도 충분히 책임을 분리하고 관리할 수 있었기에, 비동기 처리가 필요한 경우가 아니면 스프링 이벤트를 사용하는 것이 적합하지 않았습니다.
- 추가적인 서비스 클래스의 장점:
LogSaveNotificationService와 같은 추가적인 서비스 클래스를 도입함으로써, 특정 기능에 대한 책임을 명확히 하고, 코드의 가독성과 유지보수성을 높일 수 있었습니다. 또한, 클래스 간의 의존성을 명확히 하여, 시스템의 복잡도를 줄이고, 디버깅을 더 용이하게 할 수 있었습니다.
-
결론: 스프링 이벤트는 이벤트 기반 비동기 처리가 필요한 상황에서는 유용하지만, 현재의 요구 사항과 상황에서는 동기 작업으로 충분히 책임을 분리할 수 있는 만큼, 코드의 복잡도를 줄이고 관리하기 쉬운 구조를 위해 추가적인 서비스 클래스를 도입하는 것이 더 적합하다고 판단하였습니다.
4) 최종 구현
성과
- 알림 전송 주기를 배치 단위로 변경한 결과, 알림 횟수가 크게 감소하여 디스코드 채널의 소음이 줄어들었습니다.
- 유효성 검사와 사전 전송 방식으로 배치 전송의 단점을 극복함으로써 전송 신뢰성이 향상되었습니다.
- 추가적인 클래스 추가로
로그 저장 기능과로그 저장 결과 저장 및 알림 전송 기능(-> 체크 필요)을 분리함으로써 코드의 복잡성을 줄이고, 관리하기 쉽게 유지보수성이 향상되었습니다.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels