ImgBB API를 이용하여 이미지 업로드 기능을 만들어 보았다.
굳이 ImgBB를 선택한 이유는 무료라는 점과 간단하다는 점.
물론 추가적인 용량이나 다른 기능이 필요하다면 결제를 해야 하지만, 결제를 해도 저렴하다.
3년 이용 시 한 달에 3.99달러이며, 3년이면 143.64달러이다.
그래서 해당 서비스를 이용하기로 했다.
작동방식 확인
우선 API를 이용해 보자.
설명서를 살펴보면 이미지 업로드 기능은 GET방식과 POST방식 두 가지를 지원한다고 되어있다.
하지만, GET방식의 경우 url길이에 제한이 있으므로 POST방식을 추천한다고 되어있다.
파라미터값으로 4가지를 받는다.
- key(필수) : 사용자를 증명하는 secret key이다.
- image(필수) : 이미지 업로드이니 이미지는 당연한 것.
- name(옵션) : 업로드될 때, 사진의 이름을 정해줄 수 있다.
- expiration(옵션) : 해당 이미지의 자동삭제시간을 지정할 수 있다. 미지정시 직접 삭제할 때까지 삭제되지 않는다.
요청 예시)
curl --location --request POST "https://api.imgbb.com/1/upload?expiration=600&key=YOUR_CLIENT_API_KEY"
--form "image=R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
응답 예시)
{
"data": {
"id": "2ndCYJK",
"title": "c1f64245afb2",
"url_viewer": "https://ibb.co/2ndCYJK",
"url": "https://i.ibb.co/w04Prt6/c1f64245afb2.gif",
"display_url": "https://i.ibb.co/98W13PY/c1f64245afb2.gif",
"width":"1",
"height":"1",
"size": "42",
"time": "1552042565",
"expiration":"0",
"image": {
"filename": "c1f64245afb2.gif",
"name": "c1f64245afb2",
"mime": "image/gif",
"extension": "gif",
"url": "https://i.ibb.co/w04Prt6/c1f64245afb2.gif",
},
"thumb": {
"filename": "c1f64245afb2.gif",
"name": "c1f64245afb2",
"mime": "image/gif",
"extension": "gif",
"url": "https://i.ibb.co/2ndCYJK/c1f64245afb2.gif",
},
"medium": {
"filename": "c1f64245afb2.gif",
"name": "c1f64245afb2",
"mime": "image/gif",
"extension": "gif",
"url": "https://i.ibb.co/98W13PY/c1f64245afb2.gif",
},
"delete_url": "https://ibb.co/2ndCYJK/670a7e48ddcb85ac340c717a41047e5c"
},
"success": true,
"status": 200
}
포스트맨을 이용하여 직접 보내보자
url들을 눌러보면 정상적으로 사진이 잘 보인다.
자동으로 이미지를 저용량, 중간, 원본으로 리사이징도 해주니 상당히 편리하다.
클라이언트에서 바로 보내야 하나? 서버를 거쳐서 보내야 하나?
클라이언트에서 이미지 서버로 바로 전송할 경우 서버 부하는 감소하지만 시크릿키가 노출될 위험이 있다.
하지만 서버를 거쳐서 갈 경우 시크릿키가 노출될 위험은 줄어들지만, 서버 부하가 증가한다.
프론트엔드 개발자분과 협의를 해본 결과, 서버를 거쳐서 가는 게 보안상 좋을 것 같다고 한다.
방향이 정해졌으니, 코딩하러 가보자.
ImageUploadAPIAdapter
내가 필요한 기능은 이미지 업로드이다.
지금은 ImgBB의 업로드 API를 이용하지만 추후에 다른 더 좋은 api나 아니면 직접 이미지 서버를 구축을 하게 될 수도 있기 때문에, interface를 이용하여 추상화시켜 주었다.
public interface ImageUploadAPIAdapter {
ImageResponseDto uploadImage(MultipartFile file);
}
@Getter
@NoArgsConstructor
public class ImageResponseDto {
private String image;
private String thumb;
}
Controller
@RestController
@RequiredArgsConstructor
public class ImageController {
private final ImageUploadAPIAdapter imageUploadAPI;
... 생략
@PostMapping("/images")
public ResponseEntity postImage(@RequestParam MultipartFile file) {
ImageResponseDto imageResponseDto = imageUploadAPI.uploadImage(file);
return ResponseEntity.status(HttpStatus.CREATED).body(imageResponseDto);
}
}
월드컵을 생성할 때, 이미지를 함께 전송하도록 하는 것이 아닌, 이미지 업로드를 따로 만들어주었다.
월드컵과 이미지 간의 결합도를 낮추어 주어, 유지보수 간에 좀 더 용이하게 되며,
월드컵 후보 이미지 수정, 후보만 따로 등록 등 다른 다양한 기능들에서 활용할 수 있다.
또한, 현재는 계획에 없으나 추후에 다른 추가적인 이미지가 필요한 다양한 곳에서 활용할 수 있다.
ImgBB 응답 데이터를 담을 객체
@Getter
@NoArgsConstructor
public class ImgBBInfo {
private ImgBBData data;
private boolean success;
private int status;
}
@Getter
@NoArgsConstructor
public class ImgBBData {
private String id;
private String title;
private String urlViewer;
private String url;
private String displayUrl;
private int width;
private int height;
private int size;
private long time;
private int expiration;
private ImgBBImage image;
private ImgBBImage thumb;
private ImgBBImage medium;
private String deleteUrl;
}
@Getter
@NoArgsConstructor
public class ImgBBImage {
private String filename;
private String name;
private String mime;
private String extension;
private String url;
}
아까 포스트맨으로 요청을 보내본 결괏값들을 담을 객체들을 만들어 주었다.
사실 필요한 필드 변수만 선언해 주어도 되긴 하지만, 추후에 개발진행을 하다가 필요하지 않으면 없애기로 했다.
ImgBBImageUploadAPI
@Component
@RequiredArgsConstructor
public class ImgBBImageUploadAPI implements ImageUploadAPIAdapter {
private final String apiUrl = "https://api.imgbb.com/1/upload";
private final WebClient webClient;
@Value("${imgbb.secret}")
private String secret;
@Override
public ImageResponseDto uploadImage(MultipartFile file) {
// 요청 바디 설정
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("key", secret);
try {
body.add("image", new NamedByteArrayResource(file.getBytes(), file.getName()));
} catch (IOException e) {
throw new BusinessLogicException(CommonExceptionCode.BAD_REQUEST);
}
// POST 요청 보내서 ImgBBData 객체에 받아오기
ImgBBData imgBBData = webClient.post()
.uri(apiUrl)
.contentType(MediaType.MULTIPART_FORM_DATA)
.bodyValue(body)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(ImgBBInfo.class)
.blockOptional()
.map(ImgBBInfo::getData)
.orElseThrow(() -> new BusinessLogicException(CommonExceptionCode.SERVICE_UNAVAILABLE));
// 필요한 정보만 파싱하여 ImageResponseDto 에 담아 반환
return ImageResponseDto
.builder()
.image(imgBBData.getImage().getUrl())
.thumb(imgBBData.getThumb().getUrl())
.build();
}
private static class NamedByteArrayResource extends ByteArrayResource {
private final String filename;
public NamedByteArrayResource(byte[] byteArray, String filename) {
super(byteArray);
this.filename = filename;
}
@Override
public String getFilename() {
return filename;
}
}
}
MultipartFile을 받아서 ByteArrayResource로 변환한 뒤, webClient를 이용하여 이미지 서버로 전송을 한다.
그리고 response를 앞에서 미리 만든 객체에 담아 받아온다.
그리고 필요한 값만 파싱 하여 리턴해준다.
이외에도 이미지와 관련된 파일만 받도록 하는 로직을 추가해야 한다.
우선은 여기까지만 해놓고 실행해 보자.
의도한 대로 잘 작동한다.
다만, 문제가 있다. 서버를 거쳐서 이미지를 전송하게 되니 상당히 속도차이가 굉장히 심각하다.
이미지서버로 바로 전송했을 경우에는 1초가량 걸렸는데, 지금은 그 6배이다....
네트워크 대역폭이나, 서버에서 이미지를 압축한다던가, 지연시간에 따라 다를 수 있지만, 어떤 방법을 사용하더라도 바로 전송하는 것보다는 빨라질 것이라고 생각은 하지 않는다.
거기다 서버의 성능을 생각한다면 더더욱...
결론
프론트엔드 개발자분과 다시 협의를 해본 결과, 그냥 클라이언트에서 바로 전송하기로 했다.
다시 생각해 보니, api키의 경우 노출되더라도 업로드만 가능하기 때문에 크게 지장은 없을 것 같기도..
무엇보다, 속도가 너무 심각하게 느리기 때문에...
'개발일지 > 돌픽' 카테고리의 다른 글
Spring boot 쿼리파라미터 Dto 사용하기 (@ModelAttribute) (2) | 2023.05.23 |
---|---|
spring-boot Cache-Control 설정으로 부하 줄이기 (0) | 2023.05.19 |
Spring-boot 파일 업로드 - 공식문서 따라하기 ! (0) | 2023.05.12 |
네이버 무료 검색 API 이용해보기 (0) | 2023.05.01 |
Google Custom Search API 사용해보기 (0) | 2023.04.30 |