| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
- java map
- 컴파일
- 코딩부트캠프후기
- javaJVM
- sqld자격증합격
- 항해15기
- java참조자료형
- java list 저장
- 항해99후기
- java list 출력
- java최솟값구하기
- java기본자료형
- 인터프린터언어
- 노베이스부트캠프
- 격파르타장점
- java map 출력
- java 자료구조 활용
- 격파르타합격후기
- 비전공자sqld
- java알고리즘문제풀이
- java map 저장
- java set 출력
- 프로그래머스제일작은수
- java set 저장
- java알고리즘
- javaJRE
- 프로그래머스
- 격파르타비전공자
- 작은수제거하기
- 격파르타후기
- Today
- Total
코딩과 결혼합니다
[Game_Crew] 트러블슈팅 : S3 + 이미지 리사이징 적용하기 본문
이미지 업로드 기능 도입
유저프로필과 게시물에 사진을 올릴 수 있게 함으로 사용자의 참여도를 높이고, 커뮤니케이션을 강화한다.
S3 사용 이유
높은 내구성과 가용성으로 데이터 손실의 위험을 최소화하고 필요한 만큼의 저장공간을 제공하여 효율적이고 합리적인 비용을 지불하며 사용할 수 있다. 그리고 다양한 보안 기능을 제공하여 데이터를 안전하게 저장할 수 있다.
이미지 리사이징 - BufferedImage를 사용한 이유
Spring에서 기본 제공하는 클래스인 BufferedImage로 이미지 리사이징을 하였다. BufferedImage는 이미지 데이터를 메모리에 저장하는 데 사용되는 클래스이나, 이미지 처리를 위해 사용될 수 있다. 예를 들어 이미지의 크기를 변경하거나, 다른 이미지의 형식으로 변환하거나 이미지를 조작하는 등의 작업을 수행할 수 있다.
1. 이미지 데이터를 메모리에 직접 저장하므로, 디스크 I/O를 줄여 애플리케이션 성능을 향상할 수 있다.
2. 이미지를 조작하는 등의 다양한 작업을 수행하는 메서드를 제공하여 편리하게 사용 가능하다.
3. 이미지의 품질을 유지하면서 크기를 줄이는 고품질 리사이징 알고리즘을 제공한다.
4. Java 표준 라이브러리의 일부로서, 다른 라이브러리나 플랫폼에 의존하지 않고 이미지 처리 작업을 수행한다.
이미지 리사이징으로 저장 공간을 절약, 데이터 전송 시간 감소에 따른 사용자 경험 개선, 그리고 메모리의 사용량을 줄여 애플리케이션의 성능을 향상하는데 기여한다.
🔸application.properties
# AWS Account Credentials (AWS)
cloud.aws.credentials.accessKey=
cloud.aws.credentials.secretKey=
# AWS S3 bucket Info (S3 bucket)
cloud.aws.s3.bucket=
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto=false
🔸S3 Config
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder
.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
}
}
@Value로 application.properties에 입력한 특정한 값을 주입받는다.
AmazonS3 ClientBuilder로 Amazon S3 서비스에 접근하기 위한 클라이언트를 생성한다. 기본 설정, 리전 설정, 자격 증명 설정을 한 후 build() 메서드를 통해 AmazonS3Client 인스턴스를 생성한다.
🔸UserController
@PutMapping("/image")
public MessageResponseDto updateUserImage(@AuthenticationPrincipal UserDetailsImpl userDetails,
@RequestPart("userImg") MultipartFile userImg) throws IOException {
Long userId = userDetails.getUser().getUserId();
userS3Service.updateUserImage(userId, userImg);
return new MessageResponseDto(Message.USER_IMG_UPDATE_SUCCESSFUL, HttpStatus.OK);
}
게임 크루 프로젝트에서는 회원가입을 할 때에 userImg가 기본으로 null 값으로 설정이 되고, 이후에 필요에 따라 마이페이지에서 이미지 등록/ 수정을 할 수 있다. 때문에 PUT 메서드를 사용하였다.
❓@RequestPart는 하나의 HTTP 요청 안에 여러 '부분'이나 '파트'를 포함하는 요청인데 하나의 파트만 보내도 되나?
@RequestPart를 사용하려면 반드시 여러 파트를 보내야 하는 것은 아니다. 단일 파트만을 처리하는 경우에도 사용할 수 있다. 다만, 여러 파트를 함께 보내는 경우에 이 어노테이션을 사용하면 각각의 파트를 효율적으로 처리할 수 있다는 장점이 있는 것이다.
여기서 이 어노테이션의 사용은 클라이언트가 보낸 이미지 파일 데이터를 효율적으로 받아오기 위함이다. 만약 @RequestParam등 다른 어노테이션을 사용하면, 이미지 파일 데이터를 제대로 받아오지 못하거나 처리하기 어려워질 수 있다.
@RequestPart 어노테이션은 주로 'multipart/form-data' 형식의 HTTP 요청을 처리할 때 사용된다. 예를 들어, HTML 폼에서 파일 업로드와 같이 여러 데이터를 한 번에 전송하는 경우가 이에 해당한다. 이런 경우, 각각의 입력 필드나 파일 등은 하나의 '파트'로 간주되며, 이러한 각각의 파트를 개별적으로 처리할 수 있다.
🔸UserS3Service
@Service
@RequiredArgsConstructor
public class UserS3Service {
private final AmazonS3Client amazonS3Client;
private final UserRepository userRepository;
@Value("${cloud.aws.s3.bucket}")
private String bucketName;
@Transactional
public void updateUserImage(Long userId, MultipartFile userImg) throws IOException {
User user = userRepository.findById(userId)
.orElseThrow(()-> new CustomException(ErrorMessage.NON_EXISTENT_USER, HttpStatus.BAD_REQUEST));
long size = userImg.getSize();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(userImg.getContentType());
objectMetadata.setContentLength(size);
objectMetadata.setContentDisposition("inline");
String prefix = UUID.randomUUID().toString();
String fileName = prefix + "_" + userImg.getOriginalFilename();
String bucketFilePath = "user_image/" + fileName;
// 이전에 이미지가 존재하면 삭제
if (user.getUserImg() != null) {
String oldFileName = user.getUserImg().substring(user.getUserImg().lastIndexOf("/") + 1);
amazonS3Client.deleteObject(bucketName, "user_image/" + oldFileName);
}
try {
amazonS3Client.putObject(new PutObjectRequest(bucketName, bucketFilePath, userImg.getInputStream(), objectMetadata));
} catch (AmazonS3Exception e) {
throw new RuntimeException("S3 업로드 실패", e);
}
// S3에서 이미지 URL 가져오기
String imageUrl = amazonS3Client.getUrl(bucketName, bucketFilePath).toString();
user.updateUserImg(imageUrl);
userRepository.save(user);
}
}
😎 기존의 유저 이미지를 Amazon S3에서 삭제하고, 데이터베이스에서는 새 이미지의 URL로 갱신
- 저장 공간을 절약하고, 이로 인한 비용을 줄일 수 있다.
- 현재 사용 중인 파일만을 유지하게 되며 데이터 관리의 효율성을 높인다.
🤔 .withCannedAcl(CannedAccessControlList.PublicRead) ?
amazonS3Client.putObject(
new PutObjectRequest(bucketName, bucketFilePath,
new ByteArrayInputStream(resizedImageBytes),
objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead)
);
. withCannedAcl(CannedAccessControlList.PublicRead)는 해당 S3 오브젝트에 대한 접근 제어 목록(ALC)을 설정하는 부분이다. 여기서는 누구나 해당 오브젝트를 읽을 수 있도록 한다.
2023-11-11T11:34:29.034+09:00 ERROR 21928 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: com.amazonaws.services.s3.model.AmazonS3Exception: The bucket does not allow ACLs (Service: Amazon S3; Status Code: 400; Error Code: AccessControlListNotSupported; Request ID: H7K6YBE407DNPZJX; S3 Extended Request ID:
실행시켰더니 에러가 났다. S3 버킷이 ACL를 허용하지 않는다는 내용으로 S3 버킷의 설정 문제 때문에 일어난 것으로 보였다. 이를 해결하는 방법으로는 ACL 설정을 변경하거나, 코드에서 ACL 설정을 제거하는 것인데 나는 ACL 설정을 제거하는 것을 택하였다. [ .withCannedAcl(CannedAccessControlList.PublicRead) 부분 삭제 ]
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>F9125QVH3BJ8PZBR</RequestId>
<HostId>n0teJ/CAc2yyqRCmU89hv+VRTFP34rEsQwbKdDcJdU48mfmBkRVFDEPJPwk5gPfEUTP1RYCXbws=</HostId>
</Error>
url이 잘 수정되었고 사진이 브라우저에 잘 나오는지 확인해보려 하였는데 이런 에러 메세지가 나왔다.
해당 에러 메세지는 주로 Amazon S3버킷의 퍼블릭 엑세스가 제한되어 있거나, IAM 사용자의 권한이 충분하지 않을 때 발생한다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::gamecrew-cicd-bucket/*"
]
}
]
}
버킷정책을 추가해 주어 해결하였다.
Amazon S3 ➡️ 버킷 목록에서 해당 버킷 선택 ➡️ 권한 탭 클릭 ➡️ 버킷 정책 (버킷에 대한 퍼블릭 액세스를 허용하는 정책을 설정해야 한다.)
🤔 .withCannedAcl(CannedAccessControlList.PublicRead) 코드와 S3 버킷 정책 뭔 차이야?
- 적용 범위 : 전자는 특정 객체에 대한 엑세스 권한을 설정하는 데 사용. S3 버킷 정책은 버킷 전체에 적용된다.
- 권한 관리 : S3 버킷 정책은 버킷 전체에 대한 권한을 관리하므로, 모든 객체가 동일한 권한을 가지게 된다.
- 따라서 세밀한 권한 관리가 필요하면 전자, 모든 객체에 동일한 권한을 부여하려면 후자를 선택한다.
🔸PostS3Service
이미지 리사이징이 추가되었다. 그리고 여러 개의 사진을 받아올 수 있다.
String formatName = getFormatName(photo); // 원본 이미지의 확장자를 반환
// 이미지가 500px보다 크면 리사이즈
if (originalImage.getWidth() > targetWidth) {
BufferedImage resizedImage = resizeImage(originalImage, targetWidth);
// 리사이즈된 이미지를 S3에 업로드
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(resizedImage, formatName, outputStream);
byte[] resizedImageBytes = outputStream.toByteArray();
objectMetadata.setContentLength(resizedImageBytes.length);
amazonS3Client.putObject(
new PutObjectRequest(bucketName, bucketFilePath, new ByteArrayInputStream(resizedImageBytes), objectMetadata)
);
} else {
// 여기서 inputStream 을 그대로 사용하기 보다 byte array 로 변환하여 사용
byte[] fileBytes = photo.getBytes();
objectMetadata.setContentLength(fileBytes.length);
amazonS3Client.putObject(
new PutObjectRequest(bucketName, bucketFilePath, new ByteArrayInputStream(fileBytes), objectMetadata)
);
}
targetWidth를 500px로 정하고 그보다 크면 리사이징, 같거나 작으면 inputStream을 byte array로 변환하여 그대로 올린다.
private String getFormatName(MultipartFile photo) {
String originalFileName = photo.getOriginalFilename();
int lastDotIndex = originalFileName.lastIndexOf(".");
// 확장자가 없거나 파일 이름이 '.'으로 시작하는 경우를 처리
if (lastDotIndex == -1 || lastDotIndex == 0) {
return null; // 또는 기본 포맷을 반환
}
return originalFileName.substring(lastDotIndex + 1);
}
모든 이미지 확장자를 리사이징 해주기 위해 originalFilename에서 확장자를 반환해 주는 메서드를 작성하여 적용하였다.
🤔 byte array로 변환하는 이유는?
이미지를 Amazon S3에 안전하게 업로드하기 위해서이다. 이렇게 하면 데이터 손실 없이 이미지를 처리하고 전송할 수 있다.
public BufferedImage resizeImage(BufferedImage originalImage, int targetWidth) {
double aspectRatio = (double) originalImage.getHeight() / originalImage.getWidth();
int targetHeight= (int) (targetWidth * aspectRatio);
BufferedImage scaledImge= new BufferedImage(targetWidth,targetHeight ,BufferedImage.TYPE_INT_RGB);
Graphics2D g2d= scaledImge.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.drawImage(originalImage ,0 ,0 ,targetWidth,targetHeight,null );
g2d.dispose();
return scaledImge;
}
😎 원본 이미지의 비율을 유지하며 크기 변경하기
- 원본 이미지의 가로세로 비율 계산
- 새로운 빈 이미지 생성 - 이미지의 크기는 목표 너비와 목표 높이로 설정
- Graphics2D 객체(g2d)를 생성 - scaledImage의 그래픽 컨텍스트를 나타낸다.
- g2d에 렌더링 힌트를 설정 - 이미지 크기를 변경할 때 보간법을 사용하여 이미지의 품질을 향상한다.
- g2d를 사용하여 원본 이미지를 새로운 크기로 그린다.
문제
S3에 사진을 업로드하는 코드가 중복된다. 그리고 postImg만 리사이징을 하고 있다.
공통적인 부분을 다루는 클래스를 새로 작성하여 기능을 적용하는 것이 유지보수나 가독성 면에서 좋을 것 같다.
https://coding-s2-chaewon.tistory.com/209
'코딩과 매일매일♥ > Game_Crew' 카테고리의 다른 글
| [Game_Crew] 리팩토링 : S3 + 이미지 리사이징 적용하기 (0) | 2023.11.12 |
|---|---|
| [Game_Crew] WebSocket으로 1대1 채팅기능 구현하기(2) (0) | 2023.11.12 |
| [Game_Crew] WebSocket으로 1대1 채팅기능 구현하기(1) - 적용x (0) | 2023.11.01 |
| [Game_Crew] 리팩토링 + 트러블슈팅 : 이메일 인증 코드 리팩토링 (0) | 2023.10.31 |
| [Game_Crew] 트러블슈팅 : 이메일 인증 완성 (0) | 2023.10.27 |