Refactor: 이미지 벌크 업로드

This commit is contained in:
김용수 2024-10-08 15:01:55 +09:00
parent 6a32b071ff
commit 402620130b
4 changed files with 58 additions and 11 deletions

View File

@ -56,12 +56,10 @@ public class ImageController {
@Operation(summary = "이미지에 대한 Presigned ", description = "이미지에 대한 Presigned 주소를 받아옵니다.") @Operation(summary = "이미지에 대한 Presigned ", description = "이미지에 대한 Presigned 주소를 받아옵니다.")
@SwaggerApiError({ErrorCode.BAD_REQUEST, ErrorCode.NOT_AUTHOR, ErrorCode.SERVER_ERROR}) @SwaggerApiError({ErrorCode.BAD_REQUEST, ErrorCode.NOT_AUTHOR, ErrorCode.SERVER_ERROR})
public List<ImagePresignedUrlResponse> uploadFolderByPresignedImage( public List<ImagePresignedUrlResponse> uploadFolderByPresignedImage(
@CurrentUser final Integer memberId,
@RequestBody final List<ImageMetaRequest> imageMetaList, @RequestBody final List<ImageMetaRequest> imageMetaList,
@PathVariable("project_id") final Integer projectId, @PathVariable("project_id") final Integer projectId,
@PathVariable("folder_id") final Integer folderId) { @PathVariable("folder_id") final Integer folderId) {
log.debug("requestImageList {}", imageMetaList); return imageService.uploadFolderByPresignedImage(imageMetaList, projectId, folderId);
return imageService.uploadFolderByPresignedImage(memberId, imageMetaList, projectId, folderId);
} }
@GetMapping("/folders/{folder_id}/images/{image_id}") @GetMapping("/folders/{folder_id}/images/{image_id}")
@ -81,7 +79,6 @@ public class ImageController {
@Operation(summary = "이미지 폴더 이동", description = "이미지가 위치한 폴더를 변경합니다.") @Operation(summary = "이미지 폴더 이동", description = "이미지가 위치한 폴더를 변경합니다.")
@SwaggerApiError({ErrorCode.BAD_REQUEST, ErrorCode.NOT_AUTHOR, ErrorCode.SERVER_ERROR, ErrorCode.PARTICIPANT_EDITOR_UNAUTHORIZED}) @SwaggerApiError({ErrorCode.BAD_REQUEST, ErrorCode.NOT_AUTHOR, ErrorCode.SERVER_ERROR, ErrorCode.PARTICIPANT_EDITOR_UNAUTHORIZED})
public void moveFolderImage( public void moveFolderImage(
@CurrentUser final Integer memberId,
@PathVariable("folder_id") final Integer folderId, @PathVariable("folder_id") final Integer folderId,
@PathVariable("project_id") final Integer projectId, @PathVariable("project_id") final Integer projectId,
@PathVariable("image_id") final Long imageId, @PathVariable("image_id") final Long imageId,
@ -98,7 +95,6 @@ public class ImageController {
@PathVariable("folder_id") final Integer folderId, @PathVariable("folder_id") final Integer folderId,
@PathVariable("project_id") final Integer projectId, @PathVariable("project_id") final Integer projectId,
@PathVariable("image_id") final Long imageId) { @PathVariable("image_id") final Long imageId) {
log.debug("project: {} , folder: {}, 삭제하려는 이미지: {}, 현재 로그인 중인 사용자 : {}", projectId, folderId, imageId);
imageService.deleteImage(projectId, folderId, imageId); imageService.deleteImage(projectId, folderId, imageId);
} }

View File

@ -0,0 +1,41 @@
package com.worlabel.domain.image.repository;
import com.worlabel.domain.image.entity.Image;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.sql.PreparedStatement;
import java.sql.Timestamp;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class ImageBulkRepository {
private final JdbcTemplate jdbcTemplate;
@Transactional
public void saveAll(List<Image> imageList) {
String sql = "INSERT INTO project_image (image_extension,folder_id,image_key,status,image_title)" +
" values (?, ?, ?, ?, ?)";
int batchSize = 1000; // 배치 크기 설정
for (int i = 0; i < imageList.size(); i += batchSize) {
List<Image> batchList = imageList.subList(i, Math.min(i + batchSize, imageList.size()));
jdbcTemplate.batchUpdate(sql,
batchList,
batchList.size(),
(PreparedStatement ps, Image image) -> {
ps.setString(1, image.getExtension()); // image_extension (String)
ps.setInt(2, image.getFolder().getId()); // folder_id (Integer)
ps.setString(3, image.getImageKey()); // image_key (String)
ps.setString(4, image.getStatus().name()); // status (String or Enum)
ps.setString(5, image.getTitle()); // image_title (String)
});
}
}
}

View File

@ -2,6 +2,7 @@ package com.worlabel.domain.image.service;
import com.worlabel.domain.folder.entity.Folder; import com.worlabel.domain.folder.entity.Folder;
import com.worlabel.domain.image.entity.Image; import com.worlabel.domain.image.entity.Image;
import com.worlabel.domain.image.repository.ImageBulkRepository;
import com.worlabel.domain.image.repository.ImageRepository; import com.worlabel.domain.image.repository.ImageRepository;
import com.worlabel.global.exception.CustomException; import com.worlabel.global.exception.CustomException;
import com.worlabel.global.exception.ErrorCode; import com.worlabel.global.exception.ErrorCode;
@ -25,6 +26,7 @@ public class ImageAsyncService {
private final S3UploadService s3UploadService; private final S3UploadService s3UploadService;
private final ImageRepository imageRepository; private final ImageRepository imageRepository;
private final ImageBulkRepository imageBulkRepository;
private final ThreadPoolTaskExecutor imageUploadExecutor; private final ThreadPoolTaskExecutor imageUploadExecutor;
@Async("imageUploadExecutor") @Async("imageUploadExecutor")
@ -72,4 +74,10 @@ public class ImageAsyncService {
private String getExtension(String fileName) { private String getExtension(String fileName) {
return fileName.substring(fileName.lastIndexOf(".") + 1); return fileName.substring(fileName.lastIndexOf(".") + 1);
} }
@Transactional
@Async("imageUploadExecutor")
public void saveImages(final List<Image> imageList) {
imageBulkRepository.saveAll(imageList);
}
} }

View File

@ -7,6 +7,7 @@ import com.worlabel.domain.folder.repository.FolderRepository;
import com.worlabel.domain.image.entity.Image; import com.worlabel.domain.image.entity.Image;
import com.worlabel.domain.image.entity.LabelStatus; import com.worlabel.domain.image.entity.LabelStatus;
import com.worlabel.domain.image.entity.dto.*; import com.worlabel.domain.image.entity.dto.*;
import com.worlabel.domain.image.repository.ImageBulkRepository;
import com.worlabel.domain.image.repository.ImageRepository; import com.worlabel.domain.image.repository.ImageRepository;
import com.worlabel.domain.participant.entity.PrivilegeType; import com.worlabel.domain.participant.entity.PrivilegeType;
import com.worlabel.domain.project.entity.Project; import com.worlabel.domain.project.entity.Project;
@ -22,6 +23,7 @@ import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.compress.utils.IOUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -321,13 +323,12 @@ public class ImageService {
@Transactional @Transactional
@CheckPrivilege(PrivilegeType.EDITOR) @CheckPrivilege(PrivilegeType.EDITOR)
public List<ImagePresignedUrlResponse> uploadFolderByPresignedImage(final Integer memberId, public List<ImagePresignedUrlResponse> uploadFolderByPresignedImage(final List<ImageMetaRequest> imageMetaList,
final List<ImageMetaRequest> imageMetaList,
final Integer projectId, final Integer projectId,
final Integer folderId) { final Integer folderId) {
Folder folder = getOrCreateFolder(folderId, projectId); Folder folder = getOrCreateFolder(folderId, projectId);
List<ImagePresignedUrlResponse> presignedUrls = new ArrayList<>(); List<ImagePresignedUrlResponse> presignedUrls = new ArrayList<>();
List<Image> imageList = new ArrayList<>();
for (ImageMetaRequest meta : imageMetaList) { for (ImageMetaRequest meta : imageMetaList) {
// UUID 생성 이미지 Key 지정 // UUID 생성 이미지 Key 지정
String key = UUID.randomUUID().toString().substring(0,13); String key = UUID.randomUUID().toString().substring(0,13);
@ -336,21 +337,22 @@ public class ImageService {
// Presigned URL 생성 // Presigned URL 생성
String presignedUrl = s3UploadService.generatePresignedUrl(key, extension); String presignedUrl = s3UploadService.generatePresignedUrl(key, extension);
log.debug("presignedUrl {}", presignedUrl);
// DB에 이미지 메타데이터 저장 // DB에 이미지 메타데이터 저장
Image image = Image.of(fileName, s3UploadService.addBucketPrefix(key), extension, folder); Image image = Image.of(fileName, s3UploadService.addBucketPrefix(key), extension, folder);
imageRepository.save(image); imageList.add(image);
// Presigned URL과 함께 응답 데이터 생성 // Presigned URL과 함께 응답 데이터 생성
ImagePresignedUrlResponse response = ImagePresignedUrlResponse.of(meta.getId(), presignedUrl); ImagePresignedUrlResponse response = ImagePresignedUrlResponse.of(meta.getId(), presignedUrl);
presignedUrls.add(response); presignedUrls.add(response);
} }
imageAsyncService.saveImages(imageList);
log.debug("이미지 개수 {}",presignedUrls.size());
return presignedUrls; return presignedUrls;
} }
// 이미지 가져오면서 프로젝트 소속 여부를 확인
// 이미지 가져오면서 프로젝트 소속 여부를 확인
private Image getImageByIdAndFolderIdAndFolderProjectId(final Integer folderId, final Long imageId, final Integer projectId) { private Image getImageByIdAndFolderIdAndFolderProjectId(final Integer folderId, final Long imageId, final Integer projectId) {
return imageRepository.findByIdAndFolderIdAndFolderProjectId(imageId, folderId, projectId) return imageRepository.findByIdAndFolderIdAndFolderProjectId(imageId, folderId, projectId)
.orElseThrow(() -> new CustomException(ErrorCode.DATA_NOT_FOUND)); .orElseThrow(() -> new CustomException(ErrorCode.DATA_NOT_FOUND));