본문으로 바로가기

프로젝트의 전체 소스 코드는 이곳에서 확인하실 수 있습니다.

 

f-lab-edu/shoe-auction

개인 간 신발 거래 서비스. Contribute to f-lab-edu/shoe-auction development by creating an account on GitHub.

github.com


현재 진행하고 있는 shoe-auction 프로젝트는 브랜드 정보와 상품 정보를 등록할때 해당하는 이미지 파일도 함께 업로드를 진행해야 한다. 이번 포스팅에서는 AWS S3에 이미지 파일을 저장하는 방법AWS Lambda를 이용한 이미지 파일 용량 리사이징 과정을 소개하려고 한다.

AWS S3에 이미지 파일 업로드하기

1. S3 버킷 생성

먼저 여기를 클릭해서 AWS에 로그인 한 후 S3 버킷을 생성해보자.

버킷 만들기 클릭

적절한 버킷 이름을 입력하고 AWS 리전은 서울로 설정해주자.

모든 퍼블릭 액세스 차단을 해제하고, 아래 경고 메시지를 체크해주자. 모든퍼블릭 액세스 차단을 해제하지 않으면 S3에 이미지를 등록할때

Access Denied (Service: Amazon S3; Status Code: 403; Error Code: AccessDenied; Request ID :

~

와 같은 오류가 발생한다. 따라서 생성시에는 액세스 차단을 해제하고 추후 버캣 정책을 설정해야 한다.

생성된 버킷으로 이동한 후 권한 탭으로 이동하자.

{
  "Version": "2012-10-17",
  "Id": "Policy1577077078140",
  "Statement": [
    {
      "Sid": "Stmt1577076944244",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::{버킷명}/*"
    }
  ]
}

위 내용을 그대로 입력하고 버킷명만 자신의 버킷 이름으로 변경해주자.

다음으로 생성한 S3에 접근하기 위해 IAM 사용자를 등록해보자.

사용자를 클릭하고 왼쪽 상단에 사용자 추가 버튼을 클릭

적절한 사용자 이름 입력 후 프로그래밍 방식 엑세스에 체크한 후 다음 버튼 클릭

S3 사용 권한을 부여하기 위해 AmazonS3FullAccess를 검색해서 체크한 후 다음 버튼 클릭

생성된 accessKey와 secretKey를 잊어버리지 않게 적어두자. 참고로 secretKey는 절대로 외부에 노출해서는 안된다.

여기까지 완료하면 이미지 파일을 S3에 등록하기 위한 작업이 모두 완료되었다.

build.gradle 설정 추가
dependencies {
    implementation group: 'com.amazonaws', name: 'aws-java-sdk-s3', version: '1.11.964'
    runtimeOnly group: 'org.springframework.cloud', name: 'spring-cloud-dependencies', version: 'Hoxton.SR10', ext: 'pom'
    implementation group: 'org.apache.tika', name: 'tika-parsers', version: '1.25'
}
application.yml

S3에 이미지 파일을 업로드하는 컨트롤러는 다음과 같다.

bransService.saveBrand에서는 등록할 브랜드 정보와 MultipartFile을 전달받아서 DB와 S3에 등록하는 작업이 이루어진다. 비즈니스 로직을 모두 다루기에는 포스팅이 너무 길어질 것 같아서 해당 메서드를 실행했을 때 일어나는 일들을 간략하게 설명 하자면 다음과 같다.

  1. 브랜드 정보를 담은 DTO와 이미지 파일을 MultipartFile 형태로 요청이 들어온다.
  2. MultipartFile은 S3로 전송할 수 없는 타입이기 때문에 File로 변환하는 과정을 거친다.
  3. 변환하는 과정에서 적절한 파일명으로 변환하는 과정을 거친다.
  4. File을 S3에 put(전송)한다.
  5. DB에 브랜드 정보와 이미지 경로를 저장한다.

Postman을 이용해 이미지 파일을 전송하면 다음과 같이 S3 버킷에 적절한 파일명으로 변경된 이미지 파일이 업로드 된 것을 확인할 수 있다.


AWS Lambda를 이용한 이미지 리사이징

S3에 이미지 파일을 업로드하는 작업은 생각보다 간단하게 구현할 수 있었다.

하지만 shoe-auction 프로젝트에서는 아래와 같이 동일한 이미지가 크기만 다르게 여러 용도로 쓰이게 된다.

(상품의 썸네일, 미리보기 등등)

🧐그렇다면 왜 리사이징 작업을 해야할까?

리사이징 이라는 개념을 알기 전에는 S3에 등록한 원본 이미지파일을 상황에 맞게 CSS로 가로/세로 길이를 줄이면 된다고 생각했다.

하지만 이미지의 크기가 크면 클수록 당연히 용량도 커지게 되고, 썸네일이나 미리보기와 같이 작은 이미지 파일을 사용할때 용량이 큰 이미지 파일을 크기만 줄여서 사용한다면 비효율적인 서버 운영이 되는 것이다.

따라서 작은 이미지 파일을 사용할때는 이미지의 크기와 함께 이미지 파일의 용량을 줄임으로써 네트워크 통신을 하는 과정에서 더 작은 용량의 이미지 파일을 가져오는것이 더 효율적인 서버 운영이 될 것이다.

🔎AWS Lambda ?

"AWS Lambda는 서버를 provisioning 하거나 관리하지 않고도 코드를 실행 할 수 있게 해주는 컴퓨팅 서비스이다."

AWS Lambda는 대표적인 서버리스 아키텍처로 개발자가 서버에 대한 고민을 하지 않고 애플리케이션을 개발할 수 있도록 도와준다. 여기서 서버리스라는 말은 실제로 서버가 없다는 뜻이 아니고 내가 요청받고 처리하는 서버가 없다는 것을 의미한다. 쉽게 말해서 AWS 서버가 내 서버 대신 일을 처리해준다는 뜻이다.

 

따라서 이미지 업로드시 용량 리사이징 작업을 AWS Lambda에서 처리하게 되므로 개발자는 서버에 대한 걱정 없이 비즈니스 로직 개발에만 집중할 수 있게 된다.

 

또한 실제로 사용한 시간에 대해서만(100ms 단위) 비용을 지불하기 때문에 비용적인 측면에서도 효율이 좋다.

적용 과정

먼저 기존 S3 버킷 외에 리사이징된 이미지를 저장할 버킷을 아래와 같이 생성한다.

이후 원본 이미지를 저장할 버킷과 리사이징된 이미지를 저장할 버킷의 접근 권한을 포함하는 정책을 생성해야 한다. 정책 페이지로 접속해서 정책 생성 버튼을 클린한다.

정책 생성에서 JSON에 아래 코드를 입력한다 정책 이름은 임의로 AWSLambdaS3Policy 설정하자.

 

{
    "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:::shoeauction-brands-origin-temp/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::shoeauction-brands-origin-resized/*"
        }
    ]
}  

다음으로 AWS 리소스에 액세스 할 수 있는 권한을 제공하는 실행 역할을 생성해야 한다.

역할 페이지에 접속해서 역할 만들기 버튼을 클릭한다.

신뢰할 수 있는 객체로 Lambda를 선택하고 다음 버튼을 클릭한다.

이전에 만든 정책을 연결해야 하기 때문에 AWSLambdaS3Policy 를 검색 후 선택한다.

역할 이름은 임의로 lambda-s3-role로 입력하자.

함수 생성

이제 우리의 origin S3 버킷에 이미지가 업로드 되었을때 자동으로 resized 버킷에 리사이징된 이미지가 저장되도록 함수를 만들어야 한다. 여러가지 언어를 지원하지만 본 프로젝트에서는 Java11로 진행하였다.

Node.js로 진행할 경우 콘솔의 코드 편집기로 바로 코드 작성이 가능하지만 java11은 해당 기능을 지원하지 않기 때문에 인텔리제이 또는 이클립스를 이용해 함수(메소드)를 작성 후 zip파일을 빌드하는 과정을 거쳐야 한다.

  1. 프로젝트 생성
    적절한 IDE를 이용해 리사이징 메서드를 작성할 프로젝트를 생성한다.
  2. 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'

 

  3. 메서드 작성

package lambda;

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 BrandImageResizeHandler 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();
            // Object key may have spaces or unicode non-ASCII characters.
            String key = record.getS3().getObject().getUrlDecodedKey();
            String dstBucket = srcBucket.replace("temp", "resized");
            // Infer the image type.
            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 "";
            }
            // Download the image from S3 into a stream
            AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient();
            S3Object s3Object = s3Client.getObject(new GetObjectRequest(
                srcBucket, key));
            InputStream objectData = s3Object.getObjectContent();
            // Read the source image
            BufferedImage srcImage = ImageIO.read(objectData);
            int srcHeight = srcImage.getHeight();
            int srcWidth = srcImage.getWidth();
            // Infer the scaling factor to avoid stretching the image
            // unnaturally
            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();
            // Fill with white before applying semi-transparent (alpha) images
            g.setPaint(Color.white);
            g.fillRect(0, 0, width, height);
            // Simple bilinear resize
            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g.drawImage(srcImage, 0, 0, width, height, null);
            g.dispose();
            // Re-encode image to target format
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            ImageIO.write(resizedImage, imageType, os);
            InputStream is = new ByteArrayInputStream(os.toByteArray());
            // Set Content-Length and Content-Type
            ObjectMetadata meta = new ObjectMetadata();
            meta.setContentLength(os.size());
            if (JPG_TYPE.equals(imageType)) {
                meta.setContentType(JPG_MIME);
            }
            if (JPEG_TYPE.equals(imageType)) {
                meta.setContentType(JPEG_MIME);
            }
            if (PNG_TYPE.equals(imageType)) {
                meta.setContentType(PNG_MIME);
            }
            if (GIF_TYPE.equals(imageType)) {
                meta.setContentType(GIF_MIME);
            }
            // Uploading to S3 destination bucket
            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);
        }
    }
}

해당 메서드는 현재 진행중인 프로젝트에 맞게 작성된 코드로, AWS에서 제공하는 예제코드를 조금만 수정한 코드다.

샘플 Amazon S3 함수 코드

 

  4. build.gradle에 다음과 같은 코드를 추가하고 `gradle build`를 진행하자.

task buildZip(type: Zip) {
    from compileJava
    from processResources
    into('lib') {
        from configurations.runtimeClasspath
    }
}

build.dependsOn buildZip

   빌드가 완료되면 아래와 같은 경로에 zip 파일이 생성된다.

 

 

람다 생성

Lambda 함수 생성 버튼을 클릭하고 기존 역할 사용에 체크하고 위에서 생성한 역할을 선택한다.

생성한 Lambda에 build한 코드 소스를 추가해야하는데 10MB가 초과하는 경우 바로 업로드할 수 없다. 따라서 임의의 S3버킷을 하나 생성한 후 좀 전에 build한 .zip파일을 업로드 후 업로드한 .zip 파일의 객체 URL을 복사 하자. 

다시 생성한 Lambda로 돌아가서 코드 소스에 바로 위에서 복사한 객체URL을 입력 후 , 런타임 설정의 핸들러 값을 [패키지명.클래스명::메서드명] 으로 설정해야 한다.

마지막으로, 함수 개요에서 트리거 추가를 클릭 후 S3를 선택한다. 버킷은 원본 이미지가 저장되는 버킷을 선택하고, 이벤트 유형을 모든 객체 생성 이벤트로 선택한다.

결과

이제 이미지를 전송하면 원본 이미지가 저장되는 S3 버킷에는 원본 이미지가 저장되고, 리사이징 S3버킷에도 아래와 같이 리사이징이 진행된 이미지가 저장된다.

 

 

Reference


docs.aws.amazon.com/ko_kr/lambda/latest/dg/with-s3-example.html

 

자습서: Amazon S3과 함께 AWS Lambda 사용 - AWS Lambda

자습서: Amazon S3과 함께 AWS Lambda 사용 버킷에 업로드된 각 이미지 파일에 대해 썸네일을 만들어 보겠습니다. 객체 생성 시 Amazon S3가 호출할 수 있는 Lambda 함수(CreateThumbnail)를 생성할 수 있습니다.

docs.aws.amazon.com