회사에서는 자체 프레임워크로 손쉽게 S3에 이미지를 업로드할 수 있고 업로드가 성공 또는 실패했을 때 반환되는 JSON 객체 결과를 그냥 써오기만 했다.
그냥 쓰는 것과 어느정도의 이해를 하고 쓰는 것은 차이가 크기 때문에 정리를 해보고자 한다.
나는 이미지 등의 파일을 선택했을 때 즉시 S3 Bucket으로 이미지가 업로드되도록 해보려 한다.
html은 input 한 줄이면 된다.
<input type="file" id="file">
application.properties에 aws 정보를 설정한다.
aws.s3.accesskey=[본인의 accesskey]
aws.s3.secretkey=[본인의 secretkey]
aws.s3.bucket=[s3 bucket name]
accesskey, secretkey가 노출되어 타인이 악용한다면 과금될 수 있다. 절대 노출되지 않도록 주의하자.
만약 git을 사용하고 있다면 반드시 .gitignore 처리를 하고 commit, push 할 것!
자, 파일을 선택했을 때 S3 Bucket에 이미지가 업로드되어야 하니 자바스크립트에서 해당 이벤트를 만들어보겠다.
빠르게 진행하기 위해 jQuery를 사용했다.
<input type="file" id="file">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script>
$(function() {
$('#file').on('change', function() {
var formData = new FormData();
var userImg = $('#file');
var originalFileName = userImg[0].files[0].name;
formData.append('userImg', userImg[0].files[0], originalFileName);
$.ajax({
type: 'post',
url: '/apis/upload-img',
data: formData,
dataType: 'json',
processData: false, // 파일 전송할 때는 false(query string을 만들지 않기 위함)
contentType: false, // default -> "application/x-www-form-urlencoded; charset=UTF-8", false -> "multipart/form-data"
error: function (xhr, status, error) {
alert('파일 업로드 실패');
},
success: function (json) {
alert('파일 업로드 성공');
}
});
});
});
</script>
'change' 이벤트(파일 선택을 했을 때 실행됨)가 발생했을 때 로직을 구현했다.
1. formData 객체를 생성하고 <input type="file" id="file>에 저장된 정보를 제이쿼리 선택자($)를 통해 userImg 변수에 넣는다.
2. 이후 formData.append 메서드를 통해 업로드한 파일 정보를 객체에 추가한다.
3. ajax 부분을 설명하자면 /apis/upload-img url로 post 요청을 보낼 것이며 파일을 전송하기 때문에 "multipart/form-data" 형식의 contentType을 지정했다.
이렇게 되면 s3에 파일을 업로드할 준비는 끝난 상태이다.
이제 Controller, Service 부분을 살펴보자.
package com.example.demo.controller;
@Slf4j
@Controller
public class MainController {
@Autowired
private S3Service s3Service;
@RequestMapping(value = "/", method = RequestMethod.GET)
public String getMainPage() {
return "index";
}
@RequestMapping(value = "/apis/upload-img", method = RequestMethod.POST)
@ResponseBody
public String uploadImg(@RequestParam("userImg") MultipartFile file) {
String imgUrl = s3Service.uploadImg(file);
if (imgUrl == null) {
return null;
}
return imgUrl;
}
}
첫 번째 RequestMapping은 메인(input 있는 곳)으로 가기 위함이다.
두 번째 RequestMapping은 S3에 이미지 업로드하는 것을 처리하기 위함이다.
RequestParam에서 userImg로 받은 이유는 위의 script 파일에서 formData에 append 메서드를 사용하여 데이터를 추가할 때 key를 userImg로 받았기 때문이다.
S3Service를 따로 만들고 의존성 주입을 했으며 s3Service.uploadImg 메서드를 호출했다.
해당 메서드는 이미지 Url을 리턴하기 때문에 정상적으로 이미지 Url을 리턴한다면 ajax에서 호출했을 때 success에 걸릴 것이며 그렇지 않다면 error에 걸릴 것이다.
package com.example.demo.core.amazon.service;
@Service
public class S3Service {
@Value("${aws.s3.accesskey}")
private String accessKey;
@Value("${aws.s3.secretkey}")
private String secretKey;
@Value("${aws.s3.bucket}")
private String bucket;
private AmazonS3 s3Client;
@PostConstruct
public void setS3Client() {
AWSCredentials credentials = new BasicAWSCredentials(this.accessKey, this.secretKey);
s3Client = AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(Regions.AP_NORTHEAST_2)
.build();
}
public String uploadImg(MultipartFile file) throws IOException {
String fileName = file.getOriginalFilename();
s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null)
.withCannedAcl(CannedAccessControlList.PublicRead));
return s3Client.getUrl(bucket, fileName).toString();
}
}
Service에서는 application.properties에 저장한 AWS 정보들을 @Value 어노테이션을 통해 가져올 수 있다.
@PostConstruct 어노테이션을 사용한 이유는 s3Client 변수를 설정할 때 만약 기본 생성자를 사용한다면 생성자에서 s3Client를 생성할 시점에서는 accessKey, secretKey가 null이기 때문에 credentials이 제대로 만들어지지 않기 때문이다.
따라서 secretkey, accesskey가 만들어진 이후에 credentials가 생성되게 하기 위해 @PostConstuct를 사용했다.
uploadImg 함수는 MultipartFile 객체를 받아 fileName을 뽑아놓고 생성한 s3Client에 putObject 메서드를 이용하며 PutOjbjectRequest에다 버킷위치, 파일명, 업로드 파일을 파라미터에 넣어 객체생성한다.
그런데 이렇게 되버리면 S3에 파일은 정상적으로 업로드가 되지만 '파일 업로드 실패'가 떠버린다.
$.ajax({
type: 'post',
url: '/apis/upload-img',
data: formData,
dataType: 'json',
processData: false, // 파일 전송할 때는 false(query string을 만들지 않기 위함)
contentType: false, // default -> "application/x-www-form-urlencoded; charset=UTF-8", false -> "multipart/form-data"
error: function (xhr, status, error) {
alert('파일 업로드 실패');
},
success: function (json) {
alert('파일 업로드 성공');
}
});
왜냐하면 dataType을 json으로 지정했는데 나는 imgUrl String을 리턴했으니 타입이 일치하지 않아서이다.
이를 해결하기 위해 어떤 것을 사용해야 할지 고민을 하다 ResponseEntity<T> 클래스를 알게 되었다.
ResponseEntity<T>
스프링에서 제공을 해주며 Http Response를 JSON형식으로 반환시켜준다.
얘를 가지고 위의 '파일 업로드 실패'가 뜨는 것을 해결해보겠다.
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponseMessage {
private String status; // HttpStatus
private String message; // Http Default Message
private String errorMessage; // Error Message to USER
private String errorCode; // Error Code
}
ApiResponseMessage 클래스를 생성한다.
필드의 개수는 자유이다.
위의 에러가 나는 곳의 원인은 Controller의 리턴 부분이므로 해당 부분을 ResponseEntity<>를 적용하여 해결해보자.
@RequestMapping(value = "/apis/upload-img", method = RequestMethod.POST)
@ResponseBody
public ResponseEntity<ApiResponseMessage> uploadImg(@RequestParam("userImg") MultipartFile file) throws IOException {
String imgUrl = s3Service.uploadImg(file);
if (imgUrl == null) {
ApiResponseMessage message = new ApiResponseMessage("Fail", "Upload Fail", "", "");
return new ResponseEntity<ApiResponseMessage>(message, HttpStatus.BAD_REQUEST);
}
ApiResponseMessage message = new ApiResponseMessage("Success", imgUrl, "", "");
return new ResponseEntity<ApiResponseMessage>(message, HttpStatus.OK);
}
위처럼 imgUrl을 정상적으로 받아왔을 때와 받아오지 못했을 때로 나뉘어 Response 처리를 해주면 된다.
JSON을 반환하기 때문에 S3에 업로드가 성공된다면 '파일 업로드 성공'이라는 메세지도 확인할 수 있다.
'Programming > Spring Framework' 카테고리의 다른 글
[스프링부트] Tiles, JSP 사용을 위한 Dependency 설정 및 JSP 수정사항 자동 반영하기 (0) | 2021.11.06 |
---|---|
[스프링] HandlerMethodArgumentResolver에 대해 알아보자 (0) | 2021.06.09 |
[스프링부트] fetch api post 전송 방법 (0) | 2021.05.13 |
[스프링부트] @JsonNaming, @JsonProperty는 언제 사용할까? (1) | 2021.05.12 |
[스프링] IoC Container에 Bean 객체가 등록되는 방법 두 가지를 알아보자 (0) | 2021.01.09 |