기존 코드
Controller
@PostMapping
public ResponseEntity<?> uploadImage(@RequestParam("file") MultipartFile file) {
try {
// 1. DB에 파일 메타데이터 저장 및 응답
ImageResponse imageResponse = uploadService.saveImageMetadata(file);
// 2. 비동기적으로 MinIO에 파일 저장
uploadService.uploadImage(file, imageResponse);
return ResponseEntity.ok(imageResponse.getOriginalFileUUID());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("Upload failed: " + e.getMessage());
}
}
Service
@Slf4j
@Service
@RequiredArgsConstructor
public class UploadService {
private final MinioClient minioClient;
private final DataService dataService;
private final KafkaTemplate<String, String> kafkaTemplate;
@Value("${minio.bucket}")
private String bucketName;
@Value("${cdn-server.url}")
private String cdnBaseUrl;
//이미지 데이터 db 저장
@Transactional
public ImageResponse saveImageMetadata(MultipartFile file) throws Exception {
// 업로드 파일명을 불러옴
String originalName = file.getOriginalFilename();
// 파일 이름이 존재하지 않는 경우
if(originalName == null) throw new IllegalArgumentException("잘못된 파일입니다.");
// test_image.jpg > [0] -> test_image, [1] -> jpg
String[] fileInfos = FileUtil.splitFileName(file.getOriginalFilename());
// ImageExtension Enum 에 설정한 확장자가 아닌 경우는 업로드 하지 않음
ImageExtension extension = ImageExtension.findByKey(fileInfos[1])
.orElseThrow(() -> new Exception("지원하지 않는 확장자 입니다."));
int imageWidth = 0;
int imageHeight = 0;
try {
BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
imageWidth = bufferedImage.getWidth();
imageHeight = bufferedImage.getHeight();
} catch (IOException e) {
log.error("IOException 발생: 이미지 입력 스트림을 읽을 수 없습니다.", e);
}
ImageRequest imageRequest = ImageRequest.create(
originalName,
extension.getKey(),
cdnBaseUrl,
imageWidth,
imageHeight
);
return dataService.uploadImage(imageRequest);
}
//이미지 업로드
@Async
public void uploadImage(MultipartFile file, ImageResponse image) {
try {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(image.getStoredFileName())
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
} catch (IOException e){
log.error("IOException 발생: 이미지 입력 스트림을 읽을 수 없습니다.", e);
}
catch (Exception e) {
log.error("Exception 발생: ", e);
}
kafkaTemplate.send("image-upload-topic", image.getStoredFileName());
}
}
JMeter 테스트 결과 초당 처리량 133.6개
문제점 : 이미지 메타데이터 정보를 저장 한 후에 클라이언트로 응답함. 이미지 정보(원본 이미지 이름, 너비, 높이 등..)를 가져오는 데 시간이 소요됨. 특히 용량이 커질수록 시간이 오래걸리는 데, 이미지 크기에 상관없이 일정한 응답을 주고 싶음. => db저장도 비동기로 처리하자
수정 코드
Controller
@PostMapping
public ResponseEntity<?> uploadImage(@RequestParam("file") MultipartFile file) {
try {
CompletableFuture<ImageResponse> imageResponseFuture = uploadService.saveImageMetadata(file);
imageResponseFuture.thenAccept(imageResponse ->
uploadService.uploadImage(file, imageResponse)
);
// 즉시 응답 반환
return ResponseEntity.ok("이미지 업로드가 시작되었습니다.");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("Upload failed: " + e.getMessage());
}
}
Service
public CompletableFuture<ImageResponse> saveImageMetadata(MultipartFile file) {
return CompletableFuture.supplyAsync(() -> {
try {
// 업로드 파일명을 불러옴
String originalName = file.getOriginalFilename();
// 파일 이름이 존재하지 않는 경우
if (originalName == null) throw new IllegalArgumentException("잘못된 파일입니다.");
// 파일 정보 분리
String[] fileInfos = FileUtil.splitFileName(originalName);
// 확장자 체크
ImageExtension extension = ImageExtension.findByKey(fileInfos[1])
.orElseThrow(() -> new Exception("지원하지 않는 확장자 입니다."));
// 이미지 크기 확인
BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
int imageWidth = bufferedImage.getWidth();
int imageHeight = bufferedImage.getHeight();
ImageRequest imageRequest = ImageRequest.create(
originalName,
extension.getKey(),
cdnBaseUrl,
imageWidth,
imageHeight
);
return dataService.uploadImage(imageRequest);
} catch (Exception e) {
log.error("이미지 메타데이터 저장 중 오류 발생: ", e);
throw new RuntimeException(e);
}
});
}
// 이미지 업로드
public void uploadImage(MultipartFile file, ImageResponse image) {
CompletableFuture.runAsync(() -> {
try {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(image.getStoredFileName())
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
kafkaTemplate.send("image-upload-topic", image.getStoredFileName());
} catch (Exception e) {
log.error("이미지 업로드 중 오류 발생: ", e);
}
});
}
CompletableFuture는 Java에서 비동기 프로그래밍을 간편하게 할 수 있도록 도와주는 도구이다.
JMeter 테스트 결과 초당 처리량 537.6개
같은 시간에 처리할 수 있는 양이 4배는 많아졌다.