✍️ 개선 배경
미간지 프로젝트를 리팩토링하면서
"무엇을 더 개선할 수 있을까?" 고민하던 중, 글쓰기 기능의 속도가 너무 느리다는 걸 발견했다.
문제는 바로 이미지 리사이즈 처리.
🐢 문제 상황
- 글쓰기 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 전략
'스프링부트' 카테고리의 다른 글
[스프링 부트] keycloak 사용 SSO(OIDC) 인증 서버 간단하게 구축해보기 (1) | 2024.11.25 |
---|---|
스프링 부트 Redis 분산 락 활용 동시성 제어 (1) | 2024.07.04 |
[스프링] 프로젝트 N+1 해결하기 (fetch join (0) | 2023.09.01 |
[Spring boot] 스프링 부트 에러페이지 커스터마이징하기 (0) | 2022.04.06 |