개요
Spring Boot와 TypeScript 기반으로 웹 애플리케이션을 제작하고 있는데 유저 프로필 이미지를 받아 저장하는
로직이 필요했다.
이를 위해서 프론트에서 보낸 이미지를 MultiPartFile로 받아서 S3에 저장하는 방식으로 구현하기로 했다.
S3 생성이나 키 생성과 같은 AWS 관련 할 일은 생략하겠다.
Spring Boot S3 연결
연결을 위해서는 AWS의 access키랑 secret키를 등록해야 한다. 오타에 조심하자.. }안써서 1시간 고생했다.
cloud:
aws:
s3:
bucket: ${bucket-name}
credentials:
access-key: ${access-key}
secret-key: ${secret-key}
region:
static: ${region}
auto: false
stack:
auto: false
위 yml 파일 설정으로 AWS의 S3 정보를 Spring Boot에 등록한다.
프론트에서 이미지 정보 받기
@RequestPart로 이미지를 MultipartFile 타입으로 받는다.
이미지 받아올 Controller
@PatchMapping("/update")
public ResponseEntity<String> memberUpdateByChannel(@RequestPart("updateMemberRequest") String updateRequest,
@RequestPart("userImage") MultipartFile userImage,
@RequestParam Long memberId,
@RequestParam Long channelId) throws IOException {
MemberUpdateRequestDto updateRequestDto = objectMapper.readValue(updateRequest, MemberUpdateRequestDto.class);
log.info("userName={}", updateRequestDto.getUserNickname());
log.info("userImage={}", userImage);
memberChannelService.updateMember(userImage, updateRequestDto, memberId, channelId);
return ResponseEntity.ok("Ok");
}
이미지 사용할 Service
@Transactional
public void updateMember(MultipartFile userImage, MemberUpdateRequestDto updateRequestDto,
Long memberId, Long channelId) throws IOException {
// long memberId = Long.parseLong(CookieUtil.getCookieValue("memberId", request));
Member member = findMember(memberId);
String image = null;
if(userImage != null){
image = memberS3UploadService.saveFile(userImage);
}
member.updateChannelCreate(updateRequestDto, image, channelId);
}
이미지 S3 업로드 메소드
@RequiredArgsConstructor
public class MemberS3UploadService {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
public String saveFile(MultipartFile multipartFile) throws IOException {
String originalFilename = multipartFile.getOriginalFilename();
if (containsKorean(originalFilename)) {
originalFilename =
URLEncoder.encode(originalFilename, StandardCharsets.UTF_8).replace("+", "%20");
}
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(multipartFile.getSize());
metadata.setContentType(multipartFile.getContentType());
amazonS3.putObject(bucket, originalFilename, multipartFile.getInputStream(), metadata);
return amazonS3.getUrl(bucket, originalFilename).toString();
}
public void deleteImage(String originalFilename) {
try {
URL url = new URL(originalFilename);
String path = url.getPath().substring(1);
String imagePath = URLDecoder.decode(path, StandardCharsets.UTF_8);
amazonS3.deleteObject(bucket, imagePath);
} catch (Exception e) {
throw new RuntimeException("파일 삭제 실패");
}
}
private boolean containsKorean(String text) {
return text.matches(".*[가-힣]+.*");
}
}
MultipartFile 타입의 이미지에서 Content를 꺼내서 amazonS3에 넣어준다.
삭제 방식은 originalFileName(DB에 있는 url)을 받아서 이를 S3에서 찾아서 삭제한다.
이미지 이름이 한글일 때 인코딩과 디코딩을 통해서 저장, 추출했다.
문제점
S3에 이미지를 저장할 때 이미지 이름이 영어나 숫자면 상관없지만 한글일 경우 S3에서 자체 인코딩하여 저장한다.
이 때문에 받은 S3에 저장된 값의 url이 이미지 원래 이름과 다른 문제가 발생한다.
S3에 저장된 이미지의 key는 한글로 매핑되지만 url은 인코딩된 문자열로 저장되어 db에 저장된 url로는 찾을 수 없다.
해결 1
이미지의 이름을 유저와 관련된 특정한 문자열로 저장한다.
예를 들어 유저의 프로필 사진이라면 유저 id 또는 닉네임에 profileImage라는 특정한 prefix 태그를 붙여서 저장한다.
-> 이 방식은 prefix관련하여 설정할 때 모든 개발자들이 파악해야하기 때문에 일단 보류하고 추후에 회의 후 결정하기로 했다.
해결 2
이미지의 이름이 한글이라면 일단 저장하기 전에 인코딩하고 이를 S3에 넘겨서 저장한다.
이후 S3에서 꺼낼 때 이를 디코딩하여 originalFilename과 비교한다.
-> 일단 이 방식으로 테스팅하고 추후에 회의를 통해 1번으로 바꿀 것 같다.
Postman 테스팅
Postman Body에 raw가 아닌 form-data 형식으로 값을 넣어서 RequestPart를 알려주고 이를 Spring에서
@RequestFile 형식으로 받아서 사용한다. 나머지 값들은 마찬가지로 RequestPart 형식의 String으로 받아서
위 코드처럼 ObjectMapping을 통해서 Json 타입으로 변경 후 원래 raw 타입으로 받은 것처럼 사용한다.
결론
AWS를 이용하여 Spring Boot와 연결하는 것이 처음이라 여러 삽질을 많이했다..
특히 키 값들을 git에 올리거나 공개해버리는 바람에 권한 제한도 걸려 사용자도 다시 생성하고 했다.
앞으로 사용할 일이 많기도 하고 이번 프로젝트에서도 게시글이나 다른 파일을 저장할 때도 S3를 사용할 예정이라
다른 사용 방식이나 방법을 찾는다면 글을 써서 기록하고 필요할 때마다 봐야겠다....
어렵고만...
참조
1. https://gaeggu.tistory.com/33
2. https://techblog.woowahan.com/11392/
3. https://chb2005.tistory.com/200
'Dev > Spring' 카테고리의 다른 글
Springboot Argument Resolver 사용하기 (5) | 2024.09.01 |
---|---|
Controller Test하기 feat WebMvcTest (3) | 2024.06.02 |
@Builder 사용 시 List 초기화 NullPointException (2) | 2024.04.21 |
Spring Cloud Eureka Swagger 연결하기 (4) | 2024.04.15 |
JWT 리프레시 토큰 Cookie에 저장하기 (4) | 2024.03.28 |