Skip to content

Commit

Permalink
파일 처리 로직 비동기 처리 (#815)
Browse files Browse the repository at this point in the history
* feat: 비동기 작업 중 발생하는 예외를 처리하는 핸들러 작성

* chore: 비동기 처리를 위한 설정 클래스 작성

* feat: 파일 관련 로직 비동기 처리

* chore: 스레드 풀 크기를 기본 설정으로 변경 및 스레드 이름 prefix 변경

* style: 사용하지 않는 import 문 제거

* refactor: 비동기 작업을 수행하는 TalkPickFileHandler를 추가하여 파일 관련 로직을 TalkPickService에서 분리

* chore: ThreadPoolTaskExecutor의 corePoolSize를 2로 설정

* chore: corePoolSize를 1로 변경

* feat: 비동기 작업에서 발생한 예외의 스택 트레이스 출력하는 코드 추가

* refactor: TalkPickService가 FileRepository를 직접 의존하지 않도록 코드 구조 개선

* refactor: 스택 트레이스를 log.error에 포함

* chore: threadPoolTaskExecutor의 corePoolSize를 default 값으로 설정

* test: TalkPickService에 TalkPickFileHandler mocking
  • Loading branch information
Hanjaemo authored Dec 22, 2024
1 parent 7268ed4 commit bae479d
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ public interface FileRepository extends JpaRepository<File, Long>, FileRepositor
void deleteByResourceIdAndFileType(Long tempTalkPickId, FileType fileType);

void deleteByResourceIdInAndFileType(List<Long> ids, FileType fileType);

boolean existsByResourceIdAndFileType(Long talkPickId, FileType fileType);
}
27 changes: 27 additions & 0 deletions src/main/java/balancetalk/global/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package balancetalk.global.config;

import balancetalk.global.exception.CustomAsyncUncaughtExceptionHandler;
import java.util.concurrent.Executor;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("Async task - ");
executor.initialize();
return executor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new CustomAsyncUncaughtExceptionHandler();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package balancetalk.global.exception;

import java.lang.reflect.Method;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;

@Slf4j
public class CustomAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {

@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.error("exception message - {} {}", ex.getMessage(), ex.getStackTrace());
log.error("method name - {}", method.getName());
for (Object param : params) {
log.error("param value - {}", param);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package balancetalk.talkpick.application;

import static balancetalk.file.domain.FileType.TALK_PICK;
import static balancetalk.global.exception.ErrorCode.NOT_FOUND_TALK_PICK;
import static balancetalk.talkpick.dto.TalkPickDto.CreateTalkPickRequest;
import static balancetalk.talkpick.dto.TalkPickDto.TalkPickDetailResponse;
import static balancetalk.talkpick.dto.TalkPickDto.TalkPickResponse;
import static balancetalk.talkpick.dto.TalkPickDto.UpdateTalkPickRequest;

import balancetalk.file.domain.File;
import balancetalk.file.domain.FileHandler;
import balancetalk.file.domain.repository.FileRepository;
import balancetalk.global.exception.BalanceTalkException;
import balancetalk.member.domain.Member;
import balancetalk.member.domain.MemberRepository;
import balancetalk.member.dto.ApiMember;
import balancetalk.member.dto.GuestOrApiMember;
import balancetalk.talkpick.domain.TalkPick;
import balancetalk.talkpick.domain.TalkPickFileHandler;
import balancetalk.talkpick.domain.repository.TalkPickRepository;
import balancetalk.vote.domain.TalkPickVote;
import java.util.List;
Expand All @@ -32,33 +29,27 @@ public class TalkPickService {

private final MemberRepository memberRepository;
private final TalkPickRepository talkPickRepository;
private final FileRepository fileRepository;
private final FileHandler fileHandler;
private final TalkPickFileHandler talkPickFileHandler;

@Transactional
public Long createTalkPick(CreateTalkPickRequest request, ApiMember apiMember) {
Member member = apiMember.toMember(memberRepository);
TalkPick savedTalkPick = talkPickRepository.save(request.toEntity(member));
Long savedTalkPickId = savedTalkPick.getId();
if (request.containsFileIds()) {
relocateFiles(request.getFileIds(), savedTalkPickId);
talkPickFileHandler.handleFilesOnTalkPickCreate(request.getFileIds(), savedTalkPickId);
}
return savedTalkPickId;
}

private void relocateFiles(List<Long> fileIds, Long talkPickId) {
List<File> files = fileRepository.findAllById(fileIds);
fileHandler.relocateFiles(files, talkPickId, TALK_PICK);
}

@Transactional
public TalkPickDetailResponse findById(Long talkPickId, GuestOrApiMember guestOrApiMember) {
TalkPick talkPick = talkPickRepository.findById(talkPickId)
.orElseThrow(() -> new BalanceTalkException(NOT_FOUND_TALK_PICK));
talkPick.increaseViews();

List<String> imgUrls = fileRepository.findImgUrlsByResourceIdAndFileType(talkPickId, TALK_PICK);
List<Long> fileIds = fileRepository.findIdsByResourceIdAndFileType(talkPickId, TALK_PICK);
List<String> imgUrls = talkPickFileHandler.findImgUrlsBy(talkPickId);
List<Long> fileIds = talkPickFileHandler.findFileIdsBy(talkPickId);

if (guestOrApiMember.isGuest()) {
return TalkPickDetailResponse.from(talkPick, imgUrls, fileIds, false, null);
Expand Down Expand Up @@ -88,44 +79,15 @@ public void updateTalkPick(Long talkPickId, UpdateTalkPickRequest request, ApiMe
Member member = apiMember.toMember(memberRepository);
TalkPick talkPick = member.getTalkPickById(talkPickId);
talkPick.update(request.toEntity(member));

if (request.notContainsAnyFileIds()) {
return;
}

List<Long> deletedFileIds = deleteRequestedFiles(request);

if (request.containsNewFileIds()) {
List<Long> newFileIds = request.getNewFileIds();
newFileIds.removeIf((deletedFileIds::contains));
relocateFiles(newFileIds, talkPickId);
}
}

private List<Long> deleteRequestedFiles(UpdateTalkPickRequest request) {
if (request.containsDeleteFileIds()) {
List<Long> deleteFileIds = request.getDeleteFileIds();
List<File> files = fileRepository.findAllById(deleteFileIds);
fileHandler.deleteFiles(files);
return deleteFileIds;
} else {
return List.of();
}
talkPickFileHandler
.handleFilesOnTalkPickUpdate(request.getNewFileIds(), request.getDeleteFileIds(), talkPickId);
}

@Transactional
public void deleteTalkPick(Long talkPickId, ApiMember apiMember) {
Member member = apiMember.toMember(memberRepository);
TalkPick talkPick = member.getTalkPickById(talkPickId);
talkPickRepository.delete(talkPick);
deleteAllAssociatedFiles(talkPickId);
}

private void deleteAllAssociatedFiles(Long talkPickId) {
List<File> files = fileRepository.findAllByResourceIdAndFileType(talkPickId, TALK_PICK);
if (files.isEmpty()) {
return;
}
fileHandler.deleteFiles(files);
talkPickFileHandler.handleFilesOnTalkPickDelete(talkPickId);
}
}
74 changes: 74 additions & 0 deletions src/main/java/balancetalk/talkpick/domain/TalkPickFileHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package balancetalk.talkpick.domain;

import static balancetalk.file.domain.FileType.TALK_PICK;

import balancetalk.file.domain.File;
import balancetalk.file.domain.FileHandler;
import balancetalk.file.domain.repository.FileRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@RequiredArgsConstructor
public class TalkPickFileHandler {

private final FileRepository fileRepository;
private final FileHandler fileHandler;

@Async
@Retryable(backoff = @Backoff(delay = 1000))
@Transactional
public void handleFilesOnTalkPickCreate(List<Long> fileIds, Long talkPickId) {
relocateFiles(fileIds, talkPickId);
}

private void relocateFiles(List<Long> fileIds, Long talkPickId) {
List<File> files = fileRepository.findAllById(fileIds);
fileHandler.relocateFiles(files, talkPickId, TALK_PICK);
}

public List<String> findImgUrlsBy(Long talkPickId) {
return fileRepository.findImgUrlsByResourceIdAndFileType(talkPickId, TALK_PICK);
}

public List<Long> findFileIdsBy(Long talkPickId) {
return fileRepository.findIdsByResourceIdAndFileType(talkPickId, TALK_PICK);
}

@Async
@Retryable(backoff = @Backoff(delay = 1000))
@Transactional
public void handleFilesOnTalkPickUpdate(List<Long> newFileIds, List<Long> deleteFileIds, Long talkPickId) {
deleteFiles(deleteFileIds);
newFileIds.removeIf((deleteFileIds::contains));
relocateFiles(newFileIds, talkPickId);
}

private void deleteFiles(List<Long> deleteFileIds) {
if (deleteFileIds.isEmpty()) {
return;
}
List<File> files = fileRepository.findAllById(deleteFileIds);
fileHandler.deleteFiles(files);
}

@Async
@Retryable(backoff = @Backoff(delay = 1000))
@Transactional
public void handleFilesOnTalkPickDelete(Long talkPickId) {
if (notExistsFilesBy(talkPickId)) {
return;
}
List<File> files = fileRepository.findAllByResourceIdAndFileType(talkPickId, TALK_PICK);
fileHandler.deleteFiles(files);
}

private boolean notExistsFilesBy(Long talkPickId) {
return !fileRepository.existsByResourceIdAndFileType(talkPickId, TALK_PICK);
}
}
2 changes: 1 addition & 1 deletion src/main/resources/config
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
package balancetalk.talkpick.application;

import balancetalk.file.domain.repository.FileRepository;
import static balancetalk.vote.domain.VoteOption.A;
import static balancetalk.vote.domain.VoteOption.B;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import balancetalk.member.domain.Member;
import balancetalk.member.dto.GuestOrApiMember;
import balancetalk.talkpick.domain.Summary;
import balancetalk.talkpick.domain.TalkPick;
import balancetalk.talkpick.domain.TalkPickFileHandler;
import balancetalk.talkpick.domain.repository.TalkPickRepository;
import balancetalk.talkpick.dto.TalkPickDto.TalkPickDetailResponse;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand All @@ -15,15 +27,6 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import static balancetalk.vote.domain.VoteOption.A;
import static balancetalk.vote.domain.VoteOption.B;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class TalkPickServiceTest {

Expand All @@ -34,7 +37,7 @@ class TalkPickServiceTest {
TalkPickRepository talkPickRepository;

@Mock
FileRepository fileRepository;
TalkPickFileHandler talkPickFileHandler;

TalkPick talkPick;
GuestOrApiMember guestOrApiMember;
Expand Down Expand Up @@ -66,6 +69,8 @@ void findById_Success_ThenIncreaseViews() {
// given
when(talkPickRepository.findById(1L)).thenReturn(Optional.ofNullable(talkPick));
when(guestOrApiMember.isGuest()).thenReturn(true);
when(talkPickFileHandler.findImgUrlsBy(1L)).thenReturn(any());
when(talkPickFileHandler.findFileIdsBy(1L)).thenReturn(any());

// when
talkPickService.findById(1L, guestOrApiMember);
Expand All @@ -80,7 +85,8 @@ void findById_Success_ThenMyBookmarkIsFalse_ByGuest() {
// given
when(talkPickRepository.findById(1L)).thenReturn(Optional.ofNullable(talkPick));
when(guestOrApiMember.isGuest()).thenReturn(true);
when(fileRepository.findImgUrlsByResourceIdAndFileType(any(), any())).thenReturn(List.of());
when(talkPickFileHandler.findImgUrlsBy(anyLong())).thenReturn(List.of());
when(talkPickFileHandler.findFileIdsBy(anyLong())).thenReturn(List.of());

// when
TalkPickDetailResponse result = talkPickService.findById(1L, guestOrApiMember);
Expand All @@ -95,6 +101,8 @@ void findById_Success_ThenVoteOptionIsNull_ByGuest() {
// given
when(talkPickRepository.findById(1L)).thenReturn(Optional.ofNullable(talkPick));
when(guestOrApiMember.isGuest()).thenReturn(true);
when(talkPickFileHandler.findImgUrlsBy(anyLong())).thenReturn(List.of());
when(talkPickFileHandler.findFileIdsBy(anyLong())).thenReturn(List.of());

// when
TalkPickDetailResponse result = talkPickService.findById(1L, guestOrApiMember);
Expand Down

0 comments on commit bae479d

Please sign in to comment.