스프링부트

[Spring Boot] 이미지 업로드 비동기 처리로 글쓰기 속도 30초 → 1초 개선하기

컴공코딩러 2024. 6. 19. 15:52

 

 


✍️ 개선 배경

미간지 프로젝트를 리팩토링하면서
"무엇을 더 개선할 수 있을까?" 고민하던 중, 글쓰기 기능의 속도가 너무 느리다는 걸 발견했다.

문제는 바로 이미지 리사이즈 처리.


🐢 문제 상황

  • 글쓰기 API 호출 시, 이미지 리사이즈 처리가 끝날 때까지 기다려야 했다.
  • 테스트 결과, 요청 완료까지 최대 30초 이상 걸리는 경우도 있었다.

 (Postman으로 측정한 기존 응답 시간 – 약 30.31초)


✅ 결론부터 말하면

리사이즈 함수만 비동기 처리했더니…

  • 📈 30초 → 1.3초로 성능 개선

💡 기능 흐름은 유지되면서 UX는 훨씬 빨라짐

📷 (Postman으로 측정한 개선 후 응답 시간 – 약 1.372초)


🔎 기존 구조

String detailImageUrl = gcsService.uploadDetailImage(userPost.getImageFile());
String thumbnailImageUrl = gcsService.uploadThumbnailImage(userPost.getImageFile());
  • 이 두 함수가 이미지 리사이즈 후 GCS에 업로드까지 진행
  • 350x467 사이즈로 리사이즈해서 저장
  • 용량 줄이기 + 썸네일 별도 제공 (로딩 속도 개선용)
BufferedImage outputImage = new BufferedImage(width, height, inputImage.getType());
Graphics2D g2d = outputImage.createGraphics();
g2d.drawImage(inputImage, 0, 0, width, height, null);
 

📎 Graphics2D 기반으로 이미지 리사이즈
📎 GCS에 저장 후 URL 리턴


💥 근데 너무 느렸다

  • GCS 업로드가 가끔씩 10초 이상 걸림
  • 글쓰기 API 전체가 그 시간 동안 블로킹
  • 함수 내부를 아무리 바꿔도 큰 개선이 없었다

🔗 관련 이슈: 이미지 resize 속도 개선 시도 #86


💡 그래서 생각한 해결책

1. 서버 분리? ❌ 비용 문제

  • 이미지 리사이징만 따로 처리하는 서버를 둘까 했지만,
  • 단순 작업에 서버 하나 더 두는 건 비효율적

2. 비동기 처리? ✅ 적합!

  • 글쓰기는 빠르게 완료
  • 이미지 리사이징은 백그라운드에서 처리
  • 이미지가 아직 준비되지 않았다면 “업로드 중입니다” 이미지 표시
    📷 (업로드 중 이미지 예시)

🔧 개선된 코드 구조

🎯 글쓰기 API (비동기 적용)

@PostMapping("/write")
public ResponseEntity<?> writePost(UserPostRequestDto userPost, HttpServletRequest request) {
    ...

    // 초기에는 '업로드 중' 이미지 URL 설정
    String uploadingImageUrl = bucketUrl + "uploading.png";

    UserPost post = UserPost.builder()
        .detailImageUrl(uploadingImageUrl)
        .thumbnailImageUrl(uploadingImageUrl)
        ...
        .build();

    userPostService.writePost(post);

    // 비동기로 이미지 리사이징 + GCS 업로드
    Future<String> detailFuture = imageService.resizeAndUploadDetailImage(userPost.getImageFile());
    Future<String> thumbFuture = imageService.resizeAndUploadThumbnailImage(userPost.getImageFile());

    CompletableFuture.runAsync(() -> {
        try {
            String detailUrl = detailFuture.get();
            String thumbUrl = thumbFuture.get();
            post.setDetailImageUrl(detailUrl);
            post.setThumbnailImageUrl(thumbUrl);
            userPostService.writePost(post); // DB에 다시 저장
        } catch (Exception e) {
            e.printStackTrace();
        }
    });

    return apiResponse.success("성공", HttpStatus.ACCEPTED);
}

⚙️ 비동기 처리 방식: Future 선택 이유

  • Runnable: 리턴값 ❌, 예외처리 ❌ → 탈락
  • Callable: 리턴값 ✅
  • Future: 결과 받기 + 예외처리 ✅ → 사용
Future<String> resizeAndUpload(...);
future.get(); // 결과 대기 + 예외 처리 가능

📸 결과 확인

📷 (초기 게시글 화면 – "업로드 중입니다" 이미지 노출)
📷 (리사이징 완료 후 – 실제 이미지 URL로 자동 업데이트)


📈 성능 개선 전후 비교

구분 처리 방식 평균 소요 시간
개선 전 동기 리사이즈 + 업로드 약 30초
개선 후 비동기 처리 + 백그라운드 리사이즈 약 1초

🧠 배운 점

  • 단순한 비동기 처리만으로도 큰 UX 개선 가능
  • Runnable, Callable, Future의 차이를 명확히 이해하게 됨
  • "업로드 중입니다" 이미지처럼 임시 리소스 제공도 중요한 UX 전략