카테고리 없음

CompletableFuture 비동기 처리로 응답 속도 향상 시키기

dev_ajrqkq 2024. 10. 12. 20:02

기존 코드

 

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배는 많아졌다.