학습 목적
토이 프로젝트로 Pixiv 사이트의 기능을 만들어 보고 싶어서 이미지 업로드와 리사이즈를 통한 썸네일화에 대해 배워보려고 한다.
이미지 리사이징
하는 이유?
- 페이지 로딩 속도 개선: 큰 이미지 파일은 로딩 시간이 길어질 수 있습니다. 이미지를 리사이즈하면 파일 크기가 줄어들어 웹 페이지의 로딩 속도가 빨라집니다. 빠른 로딩 시간은 사용자 경험을 개선하고, 검색 엔진 최적화(SEO)에도 긍정적인 영향을 미칩니다.
- 대역폭 절약: 리사이즈된 이미지는 파일 크기가 작아져 서버와 사용자 양측에서 사용되는 데이터 대역폭을 절약할 수 있습니다. 이는 특히 모바일 데이터 요금이 중요한 상황이나 대역폭이 제한된 환경에서 유용합니다.
- 적절한 해상도유지: 디바이스별로 최적의 해상도를 유지하기 위해 이미지를 리사이즈할 수 있습니다. 예를 들어, 모바일 기기에서는 작은 이미지가 더 적합하고, 고해상도 모니터에서는 더 큰 이미지가 필요할 수 있습니다.
- 디자인 및 레이아웃 일관성 유지: 웹사이트의 디자인과 레이아웃은 일관성이 있어야 합니다. 이미지를 적절한 크기로 리사이즈하면 페이지의 다른 요소와 조화롭게 배치할 수 있습니다.
- 서버 저장 공간 절약: 큰 이미지 파일은 서버 저장 공간을 많이 차지합니다. 이미지를 리사이즈하여 파일 크기를 줄이면 서버 저장 공간을 절약할 수 있습니다.
- 성능 최적화: 많은 수의 큰 이미지가 있는 경우, 서버의 성능에 영향을 미칠 수 있습니다. 이미지 파일 크기를 줄이면 서버의 부하를 줄일 수 있습니다
SpringBoot를 이용한 이미지 리사이징
Java를 이용한 이미지 리사이징은 다음 라이브러리를 이용하여 가능합니다.
- java.awt.Graphics2D
- Image.getScaledInstance()
- Imgscalr
- Thunbnailator
- Marvin
각 라이브러리에 대한 간단한 설명
java.awt.Graphics2D
- 설명: Graphics2D는 Java의 기본 그래픽 라이브러리인 AWT(Abstract Window Toolkit)의 클래스입니다. 이미지 리사이징을 수행할 때는 이 클래스를 사용하여 고급 그래픽 조작(예: 회전, 변환, 리사이즈)을 할 수 있습니다.
- 사용 방법: Graphics2D 객체를 생성하고, drawImage 메서드를 사용하여 이미지를 원하는 크기로 그려 리사이즈합니다. 이 방법은 사용자가 품질을 직접 관리할 수 있는 장점이 있지만, 구현이 다소 복잡할 수 있습니다.
Image.getScaleInstance()
- 설명: getScaledInstance()는 Java의 Image 클래스의 메서드로, 간단하게 이미지를 리사이즈할 수 있도록 해줍니다.
- 사용 방법: 이 메서드는 이미지의 너비와 높이를 지정하여 이미지 객체의 크기를 조정한 새로운 이미지를 생성합니다. 추가로, 리사이즈 방법(빠름, 부드러움, 고품질 등)을 지정할 수 있는 플래그를 사용할 수 있습니다. 그러나 이 메서드는 성능과 품질이 다소 제한적일 수 있습니다.
Imgscalr
- 설명: Imgscalr는 Java에서 이미지 리사이징을 쉽게 할 수 있게 해주는 경량 라이브러리입니다. 이 라이브러리는 고성능과 간단한 API를 제공하여, 여러 가지 리사이징 알고리즘을 지원합니다.
- 사용 방법: 사용자가 한두 줄의 코드만으로도 이미지 리사이징을 수행할 수 있어 매우 직관적입니다. 예를 들어, Scalr.resize(image, Method.QUALITY, Mode.AUTOMATIC, targetWidth, targetHeight)와 같은 방법으로 사용할 수 있습니다. 이 라이브러리는 빠르고, 대부분의 일반적인 리사이징 작업에 적합 합니다.
Thumbnailator
- 설명: Thumbnailator는 Java에서 이미지 리사이징을 쉽게 수행하기 위해 설계된 또 다른 경량 라이브러리입니다. Thumbnailator는 고성능과 간단한 API를 제공하며, 다양한 리사이징 옵션을 지원합니다.
- 사용 방법: Thumbnailator는 예제와 같이 매우 간단한 사용법을 제공합니다( Thumbnails.of(image).size(width, height).toFile(outputFile) ) 다양한 입력과 출력 형식을 지원하며, 사용자 정의 이미지 필터 및 품질 설정도 가능합니다.
Marvin
- 설명: Marvin은 이미지 프로세싱을 위한 더 넓은 범위의 기능을 제공하는 Java 프레임워크입니다. 이미지 리사이징뿐만 아니라, 필터링, 변형, 컬러 조정 등 고급 이미지 처리 기능을 지원합니다.
- 사용 방법: Marvin은 MarvinImage 객체를 사용하여 이미지 조작을 수행합니다. 이미지 리사이징의 경우, MarvinPlugin을 통해 다양한 리사이징 알고리즘을 적용할 수 있습니다. 고급 이미지 처리가 필요한 경우 유용하지만, 상대적으로 다른 라이브러리보다 학습 곡선이 있을 수 있습니다.
AWS Lambda Image Resize
AWS Lambda?
AWS Lambda는 아마존 웹 서비스(AWS)에서 제공하는 서버리스 컴퓨팅 서비스입니다. 서버리스 컴퓨팅이란 서버를 직접 관리하지 않고 코드를 실행할 수 있는 컴퓨팅 모델을 말합니다. AWS Lambda를 사용하면 서버를 프로비저닝하거나 관리할 필요 없이 코드를 업로드하고, 특정 이벤트가 발생할 때 해당 코드를 자동으로 실행할 수 있습니다.
AWS Lambda의 주요 특징
- 서버 관리 불필요: 사용자는 인프라를 관리할 필요 없이 코드 실행에만 집중할 수 있습니다. AWS가 자동으로 인프라를 관리하고, 필요에 따라 확장합니다.
- 자동 확장: Lambda 함수는 자동으로 확장되어 동시에 여러 요청을 처리할 수 있습니다. 즉, 트래픽 변화에 따라 필요한 만큼만 리소스를 사용하며, 자동으로 처리 용량을 조절합니다.
- 이벤트 기반 실행: Lambda 함수는 다양한 이벤트 소스에 의해 트리거됩니다. 예를 들어, S3 버킷에 파일이 업로드되거나, DynamoDB 테이블에 데이터가 추가될 때, 또는 API Gateway를 통한 HTTP 요청 시 함수가 실행될 수 있습니다.
- 요금 모델: AWS Lambda는 사용한 만큼만 비용을 지불하는 방식으로, 코드가 실행된 시간과 사용된 리소스에 따라 과금됩니다. 사용자가 서버를 미리 프로비저닝하지 않기 때문에 비용 효율적입니다.
- 다양한 언어 지원: AWS Lambda는 Node.js, Python, Java, C#, Go, Ruby 등 다양한 프로그래밍 언어를 지원합니다. 이를 통해 개발자는 익숙한 언어로 서버리스 애플리케이션을 개발할 수 있습니다.
AWS Lambda는 마이크로서비스 아키텍처, 실시간 파일 처리, 데이터 변환, API 백엔드 구축 등 다양한 용도로 활용될 수 있는 강력한 서버리스 컴퓨팅 서비스입니다.
Lambda를 사용해야 하는 경우
- 파일 처리: 업로드 후 Amazon Simple Storage Service(S3)를 사용하여 Lambda 데이터 처리를 실시간으로 트리거 합니다.
- 스트림 처리: Lambda 및 Amazon Kinesis를 사용하여 애플리케이션 작업 추적, 거래 주문 처리, 클릭스트림 분석, 데이터 정리, 로그 필터링, 인덱싱, 소셜 미디어 분석, 사물 인터넷(IoT) 디바이스 데이터 텔레메트리 및 계측을 위한 실시간 스트리밍 데이터를 처리합니다.
- 웹 애플리케이션: Lambda를 다른 AWS 서비스와 결합하여 여러 데이터 센터에서 고가용성 구성으로 자동으로 스케일 업/ 스케일 다운되고 실행되는 강력한 웹 애플리케이션을 빌드합니다.
- IoT 백엔드: Lambda를 사용하여 서버리스 백엔드를 구축함으로써 웹, 모바일, IoT 및 서드 파티 API 요청을 처리합니다.
- 모바일 백엔드: Lambda 및 Amazon API Gateway를 사용하여 백엔드를 구축함으로써 API 요청을 인증하고 처리합니다. AWS Amplify를 사용하여 iOS, Android, 웹 및 React Native 프론트엔드와 손쉽게 통합합니다.
주요 기능
- 환경 변수: 환경 변수를 사용하여 코드를 업데이트하지 않고 함수의 동작을 조정합니다.
- 버전: 예를 들어 안정적인 프로덕션 버전의 사용자에게 영향을 주지 않고 베타 테스트에 새 함수를 사용할 수 있도록 함수 배포를 관리합니다.
- 컨테이너 이미지: 기존 컨테이너 도구를 재사용하거나 기계 학습과 같은 상당한 종속 구성 요소에 의존하는 더 큰 워크로드를 배포할 수 있도록 AWS에서 제공하는 기본 이미지 또는 대체 기본 이미지를 사용하여 Lambda 함수에 대한 컨테이너 이미지를 생성합니다.
- 계층: 라이브러리와 기타 종속 구성 요소를 패키징하여 배포 아카이브의 크기를 줄이고 코드를 더 빠르게 배포할 수 있도록 합니다.
- Lambda 확장: 모니터링, 관측성, 보안 및 거버넌스를 위한 도구로 Lambda 함수를 보강합니다.
- 함수 URL: Lambda 함수에 전용 HTTP(S) 엔드포인트를 추가합니다.
- 응답 스트리밍: Node.js 함수에서 클라이언트 응답 페이로드를 다시 스트리밍하여 첫 번째 바이트까지 시간(TTFB) 성능을 개선하거나 더 큰 페이로드를 반환하도록 Lambda 함수 URL을 구성합니다.
- 동시성 및 크기 조정 컨트롤: 프로덕션 애플리케이션의 크기 조정 및 응답성에 대해 세밀한 제어를 적용합니다.
- 코드 서명: 승인도니 개발자만 변경되지 않은 신뢰할 수 있는 코드를 Lambda 함수에 게시하는지 확인합니다.
- 프라이빗 네트워킹: 데이터베이스, 캐시 인스턴스, 내부 서비스 등의 리소스에 대해 프라이빗 네트워크를 생성합니다.
- 파일 시스템 액세스: 함수 코드가 높은 동시성으로 안전하고 공유 리소스에 액세스하고 수정할 수 있게 Amazon Elastic File Systme(Amazon EFS)을 로컬 디렉터리에 탑재하도록 함수를 구성합니다.
- Lambda SnapStart for Java: 일반적으로 함수 코드를 변경하지 않고 추가 비용 없이 Java 런타임의 시작 성능을 최대 10배 향상시킵니다.
AWS Lambda 사용 S3 이미지 리사이징 방법
1. S3 버킷 생성
한 버킷에서 경로를 나눠서 사용하는 것은 재귀 호출이 될 수 있기 때문에 버킷을 나누는 것을 AWS에서 권장하고 있다.
버킷 사용 예시
- bucket1: Lambda를 생성할 때 소스 코드가 10MB 이상일 경우 S3에 올려서 사용하는데 그때 사용
- bucket2: 이미지를 저장할 버킷
- bucket3: bucket2에 저장된 이미지를 리사이징 해 저장할 버킷
2. IAM 정책 & 역할 생성
정책 생성
직접 JSON을 입력한다.
- bucket1에 대한 권한(s3:GetObject), bucket3에 대한 권한(s3:PutObject, s3:PutObjectAcl)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:PutLogEvents",
"logs:CreateLogGroup",
"logs:CreateLogStream"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::jikgong-image/*"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl" // S3 Access Denied 에러 해결
],
"Resource": "arn:aws:s3:::jikgong-resize-bucket/*"
}
]
}
- S3 Access Denied 에러 발생 시 주석 부분 코드 필요
역할 생성
만든 정책을 사용할 역할을 생성한다.
4. Lambda 함수에 사용할 함수 생성
origin S3 버킷에 이미지가 업로드 되었을 때 자동으로 resized 버킷에 리사이징된 이미지가 저장되도록 함수를 생성(Node.js의 경우 AWS CLI 또는 Lambda 콘솔을 사용해 생성 가능)
build.gradle 의존성 추가
implementation group: 'com.amazonaws', name: 'aws-lambda-java-core', version: '1.2.1'
implementation group: 'com.amazonaws', name: 'aws-lambda-java-events', version: '3.7.0'
implementation group: 'com.amazonaws', name: 'aws-java-sdk', version: '1.11.969'
lambda 함수 작성
AWS 제공 예제 코드를 프로젝트에 맞게 수정해서 사용하면 된다.
예시 코드
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.S3EventNotificationRecord;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.S3Event;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
public class ResizeHandler implements RequestHandler<S3Event, String> {
private static final float MAX_HEIGHT = 60;
private final String JPG_TYPE = (String) "jpg";
private final String JPG_MIME = (String) "image/jpeg";
private final String JPEG_TYPE = (String) "jpeg";
private final String JPEG_MIME = (String) "image/jpeg";
private final String PNG_TYPE = (String) "png";
private final String PNG_MIME = (String) "image/png";
private final String GIF_TYPE = (String) "gif";
private final String GIF_MIME = (String) "image/gif";
public String handleRequest(S3Event s3event, Context context) {
LambdaLogger logger = context.getLogger();
try {
S3EventNotificationRecord record = s3event.getRecords().get(0);
String srcBucket = record.getS3().getBucket().getName(); // 원본 버킷 이름
String key = record.getS3().getObject().getUrlDecodedKey(); // 객체의 키 (파일 경로 및 이름)
String dstBucket = "jikgong-resize-bucket"; // 수정된 저장될 버킷 이름
// 파일 확장자 추출
Matcher matcher = Pattern.compile(".*\\.([^\\.]*)").matcher(key);
if (!matcher.matches()) {
logger.log("Unable to infer image type for key " + key);
return "";
}
String imageType = matcher.group(1);
// 지원하지 않는 이미지 형식인 경우 로그를 남기고 리턴
if (!(JPG_TYPE.equals(imageType)) && !(JPEG_TYPE.equals(imageType)) && !(PNG_TYPE.equals(imageType)) && !(GIF_TYPE.equals(imageType))) {
logger.log("Skipping non-image " + key);
return "";
}
// S3에서 이미지 가져오기
AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient();
S3Object s3Object = s3Client.getObject(new GetObjectRequest(srcBucket, key));
InputStream objectData = s3Object.getObjectContent();
// 이미지 리사이징 처리
BufferedImage srcImage = ImageIO.read(objectData);
int srcHeight = srcImage.getHeight();
int srcWidth = srcImage.getWidth();
int width = (int) (srcWidth * (MAX_HEIGHT / srcHeight)); // 비율에 맞춰 너비 계산
int height = (int) MAX_HEIGHT;
// 새 이미지 버퍼 생성 및 그래픽 설정
BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = resizedImage.createGraphics();
g.setPaint(Color.white);
g.fillRect(0, 0, width, height);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(srcImage, 0, 0, width, height, null);
g.dispose();
// 바이트 배열로 변환
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(resizedImage, imageType, os);
InputStream is = new ByteArrayInputStream(os.toByteArray());
ObjectMetadata meta = new ObjectMetadata();
meta.setContentLength(os.size()); // 메타데이터 설정
meta.setContentType(getMimeType(imageType)); // MIME 타입 설정
// 리사이즈된 이미지를 S3에 저장
logger.log("Writing to: " + dstBucket + "/" + key);
try {
s3Client.putObject(new PutObjectRequest(dstBucket, key, is, meta).withCannedAcl(CannedAccessControlList.PublicRead));
} catch (AmazonServiceException e) {
logger.log(e.getErrorMessage());
System.exit(1);
}
logger.log("Successfully resized " + srcBucket + "/" + key + " and uploaded to " + dstBucket + "/" + key);
return "Ok";
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// MIME 타입을 반환하는 보조 메소드
private String getMimeType(String imageType) {
switch(imageType) {
case JPG_TYPE:
case JPEG_TYPE:
return JPG_MIME;
case PNG_TYPE:
return PNG_MIME;
case GIF_TYPE:
return GIF_MIME;
default:
return "";
}
}
}
build.gradle에 다음 코드를 추가하고 gradle build 진행
task buildZip(type: Zip) {
from compileJava
from processResources
into('lib') {
from configurations.runtimeClasspath
}
}
명령어를 통해 zip 생성
./gradlew buildZip
- 생성한 zip 파일을 S3에 업로드(파일 사이즈가 크면 S3를 URL을 통해 AWS Lambda에 적용)
5. Lambda 함수 생성
함수 이름을 입력하고, 사용할 언어(작성한 함수 언어)를 선택 후 기존 실행 역할 변경에서, 생성한 역할을 선택
코드 소스를 업로드(zip 파일을 S3에 업로드 한 경우 URL을 입력)
런타임 설정을 편집한다.
- 사용 언어, 패키지, 클래스, 메서드 명을 입력
트리거를 추가한다.
- Lambda에서 작업한 로그들은 모니터링 → Cloud Watch Logs에서 확인할 수 있음
6. S3에 이미지를 업로드해 AWS Lambda에서 리사이징을 수행하는지 테스트
7. 프로젝트에서 리사이징된 이미지 활용
썸네일 이미지에 대해서만 resize를 진행하도록 Prefix 조건을 두어 thumbnail만 처리하도록 수정
저장할 때 prefix 설정
// unique 이름 생성
String storeImageName = createStoreImageName(extension);
// 썸네일 이미지라면 Prefix를 등록해 AWS Lambda가 실행되도록 세팅
String prefix = isFirst ? "thumbnail_" : "";
storeImageName = "jobPost/" + prefix + storeImageName;
thumbnail url 조회
// 버킷에서 이미지 조회
public String getImgPath(String fileName) {
if (amazonS3Client.doesObjectExist(bucket, fileName)) {
return amazonS3Client.getUrl(bucket, fileName).toString();
} else {
throw new CustomException(ErrorCode.S3_NOT_FOUND_FILE_NAME);
}
}
// resize버킷에서 이미지 조회
public String getThumbnailImgPath(String fileName) {
if (amazonS3Client.doesObjectExist(resize_bucket, fileName)) {
return amazonS3Client.getUrl(resize_bucket, fileName).toString();
} else {
// resize_bucket에 파일이 없을 경우, 메인 bucket에서 검색
if (amazonS3Client.doesObjectExist(bucket, fileName)) {
log.warn("resize_bucket 에서 조회를 시도했지만, bucket에만 존재");
return amazonS3Client.getUrl(bucket, fileName).toString();
} else {
// 두 버킷 모두에 파일이 없을 경우 예외 발생
throw new CustomException(ErrorCode.S3_NOT_FOUND_FILE_NAME);
}
}
}
Lambda 사용 참조
https://dgjinsu.tistory.com/59
https://velog.io/@kmss6905/Lambda-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95
https://oliveyoung.tech/blog/2023-05-19/aws-lambda-resize/
'항해 99 > Spring' 카테고리의 다른 글
프로젝트 코드 리팩토링 (0) | 2024.06.04 |
---|---|
CI/CD 2 (0) | 2024.05.13 |
CI / CD (0) | 2024.05.11 |
ORM (0) | 2024.05.03 |
Web Game 코드 설계 정리 (0) | 2024.04.23 |