daadaadaah / review-study-app

0 stars 0 forks source link

알림 기능 개선점 2-2. Stack을 활용하여 알림 전송 주기 변경(매 로그 데이터 저장시 -> 모아서 배치)으로 알림 기능 개선하여 DX 향상 #78

Open daadaadaah opened 3 weeks ago

daadaadaah commented 3 weeks ago

배경 : 기존 방식(매 로그 저장시마다 전송) 문제 파악

개선 과정

[개요]
1. 개선책(Stack을 활용한 배치 전송) 
2. 추가 보완점 개선 
(1) 단점 보완 1 : 유효성 검사와 사전 전송 방식으로 메시지 및 파일 제한 초과 문제 해결
(2) 단점 보완 2 : 추가적인 서비스 클래스 도입으로 로직 단순화

1. 개선책(Stack을 활용한 배치 전송)

2) 각 자료구조의 특징

3) Stack이 적합한 이유

(2) 배치 전송으로 인한 단점

2. 추가 보완점 개선

(1) 단점 보완 1 : 유효성 검사와 사전 전송 방식으로 메시지 및 파일 제한 초과 문제 해결

1) 문제

2) 해결책

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 클래스의 책임
책임 1. Job, Step, Task 로그 Google Sheets에 저장
책임 2. 로그 저장 실패 처리
책임 3. 로그 저장 결과 Discord로 알림
# 개선 전 LogDirectSaveGoogleSheetsService 클래스의 책임
책임 1. Job, Step, Task 로그 Google Sheets에 저장
책임 2. 로그 저장 실패 처리
책임 3. 로그 저장 결과 메시지 Stack에 저장
책임 4. 배치 전송을 위한 유효성 검사
책임 5. 로그 저장 결과 Discord로 알림

2) 개선 방안 : 스프링 이벤트(Spring Event) 활용 vs 추가적인 서비스 클래스 도입

스프링 이벤트 활용 방법 추가적인 서비스 클래스 도입
장점 1. 비동기 처리 용이: 스프링의 비동기 지원을 활용하여 이벤트를 비동기적으로 처리할 수 있습니다.
2. 유연한 확장성: 새로운 기능이나 서비스가 필요할 때, 기존 코드를 수정하지 않고 새로운 이벤트를 추가하거나 기존 이벤트에 대한 리스너를 추가함으로써 유연하게 확장할 수 있습니다.
3. 관심사 분리로 유지보수성 향상
1. 간단한 방법으로 책임 분리 가능: 각 서비스가 특정 책임을 가지며, 책임이 명확하게 분리됩니다.
2. 코드 이해 용이: 서비스 클래스가 명확한 역할을 갖고 있어, 코드의 이해도가 높아지고 디버깅이 상대적으로 쉬워집니다.
3. 복잡한 이벤트 시스템 학습 없이 사용 가능: 기존 객체 지향 설계 원칙을 쉽게 적용할 수 있습니다.
단점 1. 복잡성 증가: 이벤트 발생 및 구독, 처리 과정이 복잡해질 수 있으며, 이벤트 흐름을 추적하고 관리하기 어려울 수 있습니다.
2. 디버깅 어려움: 이벤트의 발생 및 처리 흐름을 추적하고 이해하는 데 어려움이 있을 수 있습니다.
3. 학습 곡선: 스프링 이벤트 시스템에 대한 이해와 설정에 일정한 학습 곡선이 있을 수 있습니다.
1. 확장성 문제: 새로운 기능 추가 시 기존 서비스 클래스를 수정하거나 새로운 클래스를 추가해야 하며, 이로 인해 유지보수가 어려울 수 있습니다.
2. 비동기 처리 어려움: 비동기 처리를 직접 구현해야 하며, 이는 코드 복잡성을 증가시킬 수 있습니다.
3. 이벤트 기반의 유연성 부족: 스프링 이벤트에 비해 기능 확장에 있어 유연성이 떨어질 수 있습니다.

3) 최종 개선책 선택

4) 최종 구현

성과

  1. 알림 전송 주기를 배치 단위로 변경한 결과, 알림 횟수가 크게 감소하여 디스코드 채널의 소음이 줄어들었습니다.
  2. 유효성 검사와 사전 전송 방식으로 배치 전송의 단점을 극복함으로써 전송 신뢰성이 향상되었습니다.
  3. 추가적인 클래스 추가로 로그 저장 기능로그 저장 결과 저장 및 알림 전송 기능(-> 체크 필요)을 분리함으로써 코드의 복잡성을 줄이고, 관리하기 쉽게 유지보수성이 향상되었습니다.
daadaadaah commented 3 weeks ago
  1. 전송 주기 변경 : https://github.com/daadaadaah/review-study-app/pull/59
  2. 퍼사트 패턴 도입 : https://github.com/daadaadaah/review-study-app/pull/62
daadaadaah commented 3 weeks ago
기존 코드

@Service
public class LogDirectSaveGoogleSheetsService implements LogService {

    private final LogGoogleSheetsRepository logGoogleSheetsRepository;

    private final NotificationService notificationService;

    private final LogHelper logHelper;

    private final UnSavedLogFileFactory unSavedLogFileFactory;

    private final Stack<LogSaveResult> logSaveResultStack = new Stack<>();

    @Autowired
    public LogDirectSaveGoogleSheetsService(
        LogGoogleSheetsRepository logGoogleSheetsRepository,
        NotificationService notificationService,
        LogHelper logHelper,
        UnSavedLogFileFactory unSavedLogFileFactory
    ) {
        this.logGoogleSheetsRepository = logGoogleSheetsRepository;
        this.notificationService = notificationService;
        this.logHelper = logHelper;
        this.unSavedLogFileFactory = unSavedLogFileFactory;
    }

    /**
     * saveJobLog는 Job 관련된 로그를 저장하는 메서드이다.
     *
     * < @Async("logSaveTaskExecutor") 추가한 이유 >
     * - 비즈니스 로직인 Github 작업과 로그 작업을 디커플링 시키기 위해
     * -
     *
     *
     */
    // TODO : 디스코드로! Job, Step, Task 모두 보내면, 디스코드 시끄러울 것 같은데, 이거 어떻게 할지 고민해보기
    @Async("logSaveHandlerExecutor") // TODO : 로그 저장 실패시, 비동기 예외 처리 어떻게 할 것인지 + 트랜잭션 처리
    public void saveJobLog(SaveJobLogDto saveJobLogDto) {

        BatchProcessType batchProcessType  = BatchProcessType.JOB;

        UUID jobId = saveJobLogDto.jobId();

        long jobDetailLogId = saveJobLogDto.endTime();

        JobDetailLog jobDetailLog = JobDetailLog.of(
            jobDetailLogId,
            logHelper.getEnvironment(),
            saveJobLogDto,
            logHelper.getCreatedAt(saveJobLogDto.startTime())
        );

        ExecutionTimeLog executionTimeLog = ExecutionTimeLog.of(
            jobId,
            null,
            logHelper.getEnvironment(),
            batchProcessType,
            saveJobLogDto.methodName(),
            saveJobLogDto.status(),
            saveJobLogDto.statusReason(),
            jobDetailLogId,
            saveJobLogDto.endTime() - saveJobLogDto.startTime(),
            logHelper.getCreatedAt(saveJobLogDto.startTime())
        );

        try {
            logGoogleSheetsRepository.saveJobLogsWithTx(jobDetailLog, executionTimeLog);

            logSaveResultStack.add(new LogSaveResult(
                LogSaveResultType.SUCCESS,
                createLogsSaveSuccessMessage(batchProcessType, jobId),
                new ArrayList<>()
            ));

        } catch (Exception exception) {

            List<UnSavedLogFile> unSavedLogFiles = createUnSavedJobLogsFiles(jobDetailLog, executionTimeLog);

            String jobDetailLogFileName = unSavedLogFiles.get(0).fileName();

            String jobExecutionTimeLogFileName = unSavedLogFiles.get(1).fileName();

            logSaveResultStack.add(new LogSaveResult(
                LogSaveResultType.FAILURE,
                createLogsSaveFailureMessage(
                    batchProcessType,
                    exception,
                    jobDetailLogFileName,
                    jobExecutionTimeLogFileName
                ),
                unSavedLogFiles
            ));
        }

        // TODO : 배치
        notifyBatchProcessTotalResult();
    }

    private void notifyBatchProcessTotalResult() {
        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);
        }
    }

    private List<UnSavedLogFile> createUnSavedJobLogsFiles(JobDetailLog jobDetailLog, ExecutionTimeLog executionTimeLog) {
        List<UnSavedLogFile> unSavedLogFiles = new ArrayList<>();

        String githubApiDetailLogFileName = unSavedLogFileFactory.createFileNameWithExtension(jobDetailLog.getClass().getSimpleName() +"_"+ jobDetailLog.id());

        unSavedLogFiles.add(new UnSavedLogFile(githubApiDetailLogFileName, jobDetailLog));

        String githubApiExecutionTimeLogFileName = unSavedLogFileFactory.createFileNameWithExtension(executionTimeLog.getClass().getSimpleName() +"_"+ executionTimeLog.id());

        unSavedLogFiles.add(new UnSavedLogFile(githubApiExecutionTimeLogFileName, executionTimeLog));

        return unSavedLogFiles;
    }

    @Async("logSaveHandlerExecutor")
    public void saveStepLog(SaveStepLogDto saveStepLogDto) {
        BatchProcessType batchProcessType  = BatchProcessType.STEP;

        UUID stepId = saveStepLogDto.stepId();

        long stepDetailLogId = saveStepLogDto.endTime();

        StepDetailLog stepDetailLog = StepDetailLog.of(
            stepDetailLogId,
            logHelper.getEnvironment(),
            saveStepLogDto,
            logHelper.getCreatedAt(saveStepLogDto.endTime())
        );

        long timeTaken = saveStepLogDto.endTime() - saveStepLogDto.startTime();

        ExecutionTimeLog executionTimeLog = ExecutionTimeLog.of(
            saveStepLogDto.stepId(),
            saveStepLogDto.jobId(),
            logHelper.getEnvironment(),
            batchProcessType,
            saveStepLogDto.methodName(),
            saveStepLogDto.status(),
            saveStepLogDto.statusReason(),
            stepDetailLogId,
            timeTaken,
            logHelper.getCreatedAt(saveStepLogDto.endTime())
        );

        try {

            logGoogleSheetsRepository.saveStepLogsWithTx(stepDetailLog, executionTimeLog);

            logSaveResultStack.add(new LogSaveResult(
                LogSaveResultType.SUCCESS,
                createLogsSaveSuccessMessage(batchProcessType, stepId),
                new ArrayList<>()
            ));

        } catch (Exception exception) {

            List<UnSavedLogFile> unSavedLogFiles = createUnSavedStepLogsFiles(stepDetailLog, executionTimeLog);

            String stepDetailLogFileName = unSavedLogFiles.get(0).fileName();

            String stepExecutionTimeLogFileName = unSavedLogFiles.get(1).fileName();

            logSaveResultStack.add(new LogSaveResult(
                LogSaveResultType.FAILURE,
                createLogsSaveFailureMessage(
                    batchProcessType,
                    exception,
                    stepDetailLogFileName,
                    stepExecutionTimeLogFileName
                ),
                unSavedLogFiles
            ));
        }
    }

    private List<UnSavedLogFile> createUnSavedStepLogsFiles(StepDetailLog stepDetailLog, ExecutionTimeLog executionTimeLog) {
        List<UnSavedLogFile> unSavedLogFiles = new ArrayList<>();

        String githubApiDetailLogFileName = unSavedLogFileFactory.createFileNameWithExtension(stepDetailLog.getClass().getSimpleName() +"_"+ stepDetailLog.id());

        unSavedLogFiles.add(new UnSavedLogFile(githubApiDetailLogFileName, stepDetailLog));

        String githubApiExecutionTimeLogFileName = unSavedLogFileFactory.createFileNameWithExtension(executionTimeLog.getClass().getSimpleName() +"_"+ executionTimeLog.id());

        unSavedLogFiles.add(new UnSavedLogFile(githubApiExecutionTimeLogFileName, executionTimeLog));

        return unSavedLogFiles;
    }

    @Async("logSaveHandlerExecutor")
    public void saveTaskLog(SaveTaskLogDto saveTaskLogDto) {
        BatchProcessType batchProcessType  = BatchProcessType.TASK;

        UUID taskId = saveTaskLogDto.taskId();

        long taskDetailLogId = saveTaskLogDto.endTime();

        long timeTaken = saveTaskLogDto.endTime() - saveTaskLogDto.startTime();

        GithubApiLog githubApiLog = new GithubApiLog(
            taskDetailLogId,
            logHelper.getEnvironment(),
            saveTaskLogDto.batchProcessName(),
            saveTaskLogDto.httpMethod(),
            saveTaskLogDto.url(),
            saveTaskLogDto.requestHeaders(),
            saveTaskLogDto.requestBody(),
            saveTaskLogDto.responseStatusCode(),
            saveTaskLogDto.responseHeaders(),
            saveTaskLogDto.responseBody(),
            timeTaken,
            logHelper.getCreatedAt(saveTaskLogDto.endTime())
        );

        ExecutionTimeLog executionTimeLog = ExecutionTimeLog.of(
            taskId,
            saveTaskLogDto.stepId(),
            logHelper.getEnvironment(),
            batchProcessType,
            saveTaskLogDto.batchProcessName(),
            saveTaskLogDto.status(),
            saveTaskLogDto.statusReason(),
            taskDetailLogId,
            timeTaken,
            logHelper.getCreatedAt(saveTaskLogDto.endTime())
        );

        try {

            logGoogleSheetsRepository.saveGithubApiLogsWithTx(githubApiLog, executionTimeLog);

            logSaveResultStack.add(new LogSaveResult(
                LogSaveResultType.SUCCESS,
                createLogsSaveSuccessMessage(batchProcessType, taskId),
                new ArrayList<>()
            ));

        } catch (Exception exception) {

            List<UnSavedLogFile> unSavedLogFiles = createUnSavedGithubApiLogFiles(githubApiLog, executionTimeLog);

            String githubApiDetailLogFileName = unSavedLogFiles.get(0).fileName();

            String githubApiExecutionTimeLogFileName = unSavedLogFiles.get(1).fileName();

            logSaveResultStack.add(new LogSaveResult(
                LogSaveResultType.FAILURE,
                createLogsSaveFailureMessage(
                    batchProcessType,
                    exception,
                    githubApiDetailLogFileName,
                    githubApiExecutionTimeLogFileName
                ),
                unSavedLogFiles
            ));
        }
    }

    private List<UnSavedLogFile> createUnSavedGithubApiLogFiles(GithubApiLog githubApiLog, ExecutionTimeLog executionTimeLog) {
        List<UnSavedLogFile> unSavedLogFiles = new ArrayList<>();

        String githubApiDetailLogFileName = unSavedLogFileFactory.createFileNameWithExtension(githubApiLog.getClass().getSimpleName() +"_"+ githubApiLog.id());

        unSavedLogFiles.add(new UnSavedLogFile(githubApiDetailLogFileName, githubApiLog));

        String githubApiExecutionTimeLogFileName = unSavedLogFileFactory.createFileNameWithExtension(executionTimeLog.getClass().getSimpleName() +"_"+ executionTimeLog.id());

        unSavedLogFiles.add(new UnSavedLogFile(githubApiExecutionTimeLogFileName, executionTimeLog));

        return unSavedLogFiles;
    }
}
변경 후 코드

@Service
public class LogDirectSaveGoogleSheetsService implements LogService {

    private final LogGoogleSheetsRepository logGoogleSheetsRepository;

    private final LogHelper logHelper;

    private final LogSaveDiscordNotificationFacade logSaveDiscordNotificationFacade;

    @Autowired
    public LogDirectSaveGoogleSheetsService(
        LogGoogleSheetsRepository logGoogleSheetsRepository,
        LogHelper logHelper,
        LogSaveDiscordNotificationFacade logSaveDiscordNotificationFacade
    ) {
        this.logGoogleSheetsRepository = logGoogleSheetsRepository;
        this.logHelper = logHelper;
        this.logSaveDiscordNotificationFacade = logSaveDiscordNotificationFacade;
    }

    /**
     * saveJobLog는 Job 관련된 로그를 저장하는 메서드이다.
     *
     * < @Async("logSaveTaskExecutor") 추가한 이유 >
     * - 비즈니스 로직인 Github 작업과 로그 작업을 디커플링 시키기 위해
     * -
     *
     *
     */
    // TODO : 디스코드로! Job, Step, Task 모두 보내면, 디스코드 시끄러울 것 같은데, 이거 어떻게 할지 고민해보기
    @Async("logSaveHandlerExecutor") // TODO : 로그 저장 실패시, 비동기 예외 처리 어떻게 할 것인지 + 트랜잭션 처리
    public void saveJobLog(SaveJobLogDto saveJobLogDto) {

        BatchProcessType batchProcessType  = BatchProcessType.JOB;

        UUID jobId = saveJobLogDto.jobId();

        long jobDetailLogId = saveJobLogDto.endTime();

        JobDetailLog jobDetailLog = JobDetailLog.of(
            jobDetailLogId,
            logHelper.getEnvironment(),
            saveJobLogDto,
            logHelper.getCreatedAt(saveJobLogDto.startTime())
        );

        ExecutionTimeLog executionTimeLog = ExecutionTimeLog.of(
            jobId,
            null,
            logHelper.getEnvironment(),
            batchProcessType,
            saveJobLogDto.methodName(),
            saveJobLogDto.status(),
            saveJobLogDto.statusReason(),
            jobDetailLogId,
            saveJobLogDto.endTime() - saveJobLogDto.startTime(),
            logHelper.getCreatedAt(saveJobLogDto.startTime())
        );

        try {
            logGoogleSheetsRepository.saveJobLogsWithTx(jobDetailLog, executionTimeLog);

            logSaveDiscordNotificationFacade.stackBatchProcessLogSaveSuccessResult(batchProcessType, jobId);

        } catch (Exception exception) {

            logSaveDiscordNotificationFacade.stackJobLogSaveFailureResult(exception, jobDetailLog, executionTimeLog);
        }

        logSaveDiscordNotificationFacade.sendBatchProcessResultsNotification();
    }

    @Async("logSaveHandlerExecutor")
    public void saveStepLog(SaveStepLogDto saveStepLogDto) {
        BatchProcessType batchProcessType  = BatchProcessType.STEP;

        UUID stepId = saveStepLogDto.stepId();

        long stepDetailLogId = saveStepLogDto.endTime();

        StepDetailLog stepDetailLog = StepDetailLog.of(
            stepDetailLogId,
            logHelper.getEnvironment(),
            saveStepLogDto,
            logHelper.getCreatedAt(saveStepLogDto.endTime())
        );

        long timeTaken = saveStepLogDto.endTime() - saveStepLogDto.startTime();

        ExecutionTimeLog executionTimeLog = ExecutionTimeLog.of(
            saveStepLogDto.stepId(),
            saveStepLogDto.jobId(),
            logHelper.getEnvironment(),
            batchProcessType,
            saveStepLogDto.methodName(),
            saveStepLogDto.status(),
            saveStepLogDto.statusReason(),
            stepDetailLogId,
            timeTaken,
            logHelper.getCreatedAt(saveStepLogDto.endTime())
        );

        try {

            logGoogleSheetsRepository.saveStepLogsWithTx(stepDetailLog, executionTimeLog);

            logSaveDiscordNotificationFacade.stackBatchProcessLogSaveSuccessResult(batchProcessType, stepId);

        } catch (Exception exception) {

            logSaveDiscordNotificationFacade.stackStepLogSaveFailureResult(exception, stepDetailLog, executionTimeLog);
        }
    }

    @Async("logSaveHandlerExecutor")
    public void saveTaskLog(SaveTaskLogDto saveTaskLogDto) {
        BatchProcessType batchProcessType  = BatchProcessType.TASK;

        UUID taskId = saveTaskLogDto.taskId();

        long taskDetailLogId = saveTaskLogDto.endTime();

        long timeTaken = saveTaskLogDto.endTime() - saveTaskLogDto.startTime();

        GithubApiLog githubApiLog = new GithubApiLog(
            taskDetailLogId,
            logHelper.getEnvironment(),
            saveTaskLogDto.batchProcessName(),
            saveTaskLogDto.httpMethod(),
            saveTaskLogDto.url(),
            saveTaskLogDto.requestHeaders(),
            saveTaskLogDto.requestBody(),
            saveTaskLogDto.responseStatusCode(),
            saveTaskLogDto.responseHeaders(),
            saveTaskLogDto.responseBody(),
            timeTaken,
            logHelper.getCreatedAt(saveTaskLogDto.endTime())
        );

        ExecutionTimeLog executionTimeLog = ExecutionTimeLog.of(
            taskId,
            saveTaskLogDto.stepId(),
            logHelper.getEnvironment(),
            batchProcessType,
            saveTaskLogDto.batchProcessName(),
            saveTaskLogDto.status(),
            saveTaskLogDto.statusReason(),
            taskDetailLogId,
            timeTaken,
            logHelper.getCreatedAt(saveTaskLogDto.endTime())
        );

        try {

            logGoogleSheetsRepository.saveGithubApiLogsWithTx(githubApiLog, executionTimeLog);

            logSaveDiscordNotificationFacade.stackBatchProcessLogSaveSuccessResult(batchProcessType, taskId);

        } catch (Exception exception) {

            logSaveDiscordNotificationFacade.stackTaskLogSaveFailureResult(exception, githubApiLog, executionTimeLog);
        }
    }
}
@Service
public class LogSaveDiscordNotificationFacade {

    private NotificationService notificationService;

    private final UnSavedLogFileFactory unSavedLogFileFactory;

    private final Stack<LogSaveResult> logSaveResultStack = new Stack<>();

    @Autowired
    public LogSaveDiscordNotificationFacade(
        NotificationService notificationService,
        UnSavedLogFileFactory unSavedLogFileFactory
    ) {
        this.notificationService = notificationService;
        this.unSavedLogFileFactory = unSavedLogFileFactory;
    }

    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);
        }
    }

    public void stackBatchProcessLogSaveSuccessResult(
        BatchProcessType batchProcessType,
        UUID batchProcessId
    ) {
        logSaveResultStack.add(new LogSaveResult(
            createLogsSaveSuccessMessage(batchProcessType, batchProcessId),
            new ArrayList<>()
        ));
    }

    public void stackJobLogSaveFailureResult(
        Exception exception,
        JobDetailLog jobDetailLog,
        ExecutionTimeLog executionTimeLog
    ) {
        List<UnSavedLogFile> unSavedLogFiles = new ArrayList<>();

        String jobDetailLogFileName = unSavedLogFileFactory.createFileNameWithExtension(
            jobDetailLog.getClass().getSimpleName() + "_" + jobDetailLog.id());

        unSavedLogFiles.add(new UnSavedLogFile(jobDetailLogFileName, jobDetailLog));

        String jobExecutionTimeLogFileName = unSavedLogFileFactory.createFileNameWithExtension(
            executionTimeLog.getClass().getSimpleName() + "_" + executionTimeLog.id());

        unSavedLogFiles.add(new UnSavedLogFile(jobExecutionTimeLogFileName, executionTimeLog));

        logSaveResultStack.add(new LogSaveResult(
            createLogsSaveFailureMessage(
                BatchProcessType.JOB,
                exception,
                jobDetailLogFileName,
                jobExecutionTimeLogFileName
            ),
            unSavedLogFiles
        ));
    }

    public void stackStepLogSaveFailureResult(
        Exception exception,
        StepDetailLog stepDetailLog,
        ExecutionTimeLog executionTimeLog
    ) {

        List<UnSavedLogFile> unSavedLogFiles = new ArrayList<>();

        String jobDetailLogFileName = unSavedLogFileFactory.createFileNameWithExtension(
            stepDetailLog.getClass().getSimpleName() + "_" + stepDetailLog.id());

        unSavedLogFiles.add(new UnSavedLogFile(jobDetailLogFileName, stepDetailLog));

        String jobExecutionTimeLogFileName = unSavedLogFileFactory.createFileNameWithExtension(
            executionTimeLog.getClass().getSimpleName() + "_" + executionTimeLog.id());

        unSavedLogFiles.add(new UnSavedLogFile(jobExecutionTimeLogFileName, executionTimeLog));

        logSaveResultStack.add(new LogSaveResult(
            createLogsSaveFailureMessage(
                BatchProcessType.STEP,
                exception,
                jobDetailLogFileName,
                jobExecutionTimeLogFileName
            ),
            unSavedLogFiles
        ));
    }

    public void stackTaskLogSaveFailureResult(
        Exception exception,
        GithubApiLog githubApiLog,
        ExecutionTimeLog executionTimeLog
    ) {

        List<UnSavedLogFile> unSavedLogFiles = new ArrayList<>();

        String jobDetailLogFileName = unSavedLogFileFactory.createFileNameWithExtension(
            githubApiLog.getClass().getSimpleName() + "_" + githubApiLog.id());

        unSavedLogFiles.add(new UnSavedLogFile(jobDetailLogFileName, githubApiLog));

        String jobExecutionTimeLogFileName = unSavedLogFileFactory.createFileNameWithExtension(
            executionTimeLog.getClass().getSimpleName() + "_" + executionTimeLog.id());

        unSavedLogFiles.add(new UnSavedLogFile(jobExecutionTimeLogFileName, executionTimeLog));

        logSaveResultStack.add(new LogSaveResult(
            createLogsSaveFailureMessage(
                BatchProcessType.TASK,
                exception,
                jobDetailLogFileName,
                jobExecutionTimeLogFileName
            ),
            unSavedLogFiles
        ));
    }
}