Play! Framework 2.2.0 x Amazon S3 파일 업로드

S3는 서비스에서 사용할 수 있도록 RESTful API를 제공하는데, API의 wrapper로 Java Library가 있어 Java 코드에서 손쉽게 사용할 수 있다.
Play에서 aws-java-sdk의 디펜던시를 추가하면 이용이 가능하다.

우선 build.sbt 파일에 디펜던시를 추가한다. 현재 최신 버전이 1.6.4 이므로 아래와 같이 파일을 수정한다.

libraryDependencies ++= Seq(
 "org.mybatis" % "mybatis" % "3.1.1",
 "mysql" % "mysql-connector-java" % "5.1.26",
 "net.sf.flexjson" % "flexjson" % "3.1",
 "com.google.inject" % "guice" % "4.0-beta",
 "com.google.inject.extensions" % "guice-multibindings" % "4.0-beta",
 "org.mybatis" % "mybatis-guice" % "3.5",
 "com.amazonaws" % "aws-java-sdk" % "1.6.4",
 javaCore,
 javaJdbc,
 cache
)

평소와 같이 디펜던시를 추가하면 update를 통해 디펜던시를 내려받고 IDE에 맞게 프로젝트를 새로 고침하는 커맨드로 환경을 바꿔준다.

play 2.2.0 built with Scala 2.10.2 (running Java 1.7.0_25), http://www.playframework.com

> Type "help play" or "license" for more information.
> Type "exit" or use Ctrl+D to leave this console.

[dottegi_server] $ update
[info] Updating {file:/Users/xxxxxxxx/Project/workspace/dottegi/dottegi_server/}dottegi_server...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] downloading http://repo1.maven.org/maven2/com/amazonaws/aws-java-sdk/1.6.4/aws-java-sdk-1.6.4.jar ...
[info]  [SUCCESSFUL ] com.amazonaws#aws-java-sdk;1.6.4!aws-java-sdk.jar (11033ms)
[info] Done updating.
[success] Total time: 28 s, completed Nov 4, 2013 3:06:44 PM
[dottegi_server] $ eclipse
[info] About to create Eclipse project files for your project(s).
[info] Compiling 2 Scala sources and 29 Java sources to /Users/xxxxxxxx/Project/workspace/dottegi/dottegi_server/target/scala-2.10/classes...
[info] Compiling 2 Java sources to /Users/xxxxxxxx/Project/workspace/dottegi/dottegi_server/target/scala-2.10/classes...
[info] Successfully created Eclipse project files for project(s):
[info] dottegi_server
[dottegi_server] $ 

이제 프로젝트에서 sdk를 사용할 수 있게 되었다. 하지만 서버가 시작할 때 자동으로 실행되는 플러그인을 구현해야 제대로 사용할 수 있다. 그러나 아직 Amazon에서 play를 위해 정식으로 제공하는 S3 플러그인이 없으므로 하나 만들어야 한다.
소스폴더 밑에 plugins 라는 폴더를 하나 생성한 뒤 그 안에 S3Plugin.java라는 클래스를 하나 만들자.

package com.unionpool.dottegi.api.plugins;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;

import play.Application;

public class S3Plugin {

 public static final String AWS_S3_BUCKET = "aws.s3.bucket";
 public static final String AWS_ACCESS_KEY = "aws.access.key";
 public static final String AWS_SECRET_KEY = "aws.secret.key";
 private final Application application;

 public static AmazonS3 amazonS3;

 public static String s3Bucket;

 public S3Plugin(Application application) {
  this.application = application;
 }

 public void onStart() {
  String accessKey = application.configuration()
    .getString(AWS_ACCESS_KEY);
  String secretKey = application.configuration()
    .getString(AWS_SECRET_KEY);
  s3Bucket = application.configuration().getString(AWS_S3_BUCKET);

  if ((accessKey != null) && (secretKey != null)) {
   AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey,
     secretKey);
   amazonS3 = new AmazonS3Client(awsCredentials);
   amazonS3.createBucket(s3Bucket);
   play.Logger.info("Using S3 Bucket: " + s3Bucket);
  }
 }

 public boolean enabled() {
  return (application.configuration().keys().contains(AWS_ACCESS_KEY)
    && application.configuration().keys().contains(AWS_SECRET_KEY) && application
    .configuration().keys().contains(AWS_S3_BUCKET));
 }
}

플러그인 파일을 만든 뒤에는 이 파일의 존재를 프레임워크에 알려줘야 한다. conf 폴더 안에 play.plugins 라는 파일을 생성하고 아래의 내용을 넣어준다.

1500:com.unionpool.dottegi.api.plugins.S3Plugin

뒤쪽은 당연히 플러그인 파일의 위치이고 1500이라는 숫자는 기본 play 플러그인들이 모두 시작한 뒤에 시작하라는 의미이다.

아직 설정이 끝나지 않았다. S3 플러그인은 실행하기 위해 세 가지 정보가 필요한데, Access key, Secret key, bucket이다. 플러그인 파일에 변수로 선언된 것들은 application.conf 파일에 설정된 내용을 읽어오고 application.conf 파일에는 아래와 같이 설정되어 있어야 한다. 현재 버전 컨트롤 시스템으로 github를 쓰고 있는데 민감한 정보를 소스에 담아 공개적인 장소에 올리기도 그렇고 해서 아래와 같은 형태로 변수를 선언한다.

# Amazon S3 Plugin variable
aws.access.key=${?AWS_ACCESS_KEY}
aws.secret.key=${?AWS_SECRET_KEY}
aws.s3.bucket=${?AWS_S3_BUCKET_NAME}

실제 내용은 해당 서버의 환경변수에 선언해 주면 된다. 아래의 ACCESS_KEY 등은 당연히 가짜.

export AWS_ACCESS_KEY="AJFIEJDJWOGJEKDKSLEI"
export AWS_SECRET_KEY="329FJDSLQKDJjdje9EJFskdl3DKFJEIWODJ2KD3K"
export AWS_S3_BUCKET="mybucket"

혹시나 저 ACCESS KEY나 SECRET KEY가 어디 있는지 모르는 사람을 위해.


대쉬보드에서 우측 상단의 로그인한 유저의 이름을 클릭하면 나오는 컨텍스트 메뉴에서 My Account로 들어가서 왼쪽 메뉴의 '보안 자격 증명' 영어로 아마 Secret Credential인가 그런거... 하여간 들어가면 우측에 아래의 화면이 중간쯤에 있다.


거기에 있는 Access Key ID와 Secret Access Key, 이건 Show 링크를 클릭하면 아래와 같이 모달 팝업을 띄워 알려준다.


이제 다 알았으니까 그 정보로 환경 변수를 설정해 주면 된다.

이전에 참고했던 방법은 JPA로 하고 따로 레이어 구분이 없이 모델에서 모든 작업이 다 이루어지는 것이어서 현재 구조에 맞추기 위해 service와 dao를 새로 만들었다. model은 기존의 profile image의 meta 정보만을 가지고 있는 model과 파일자체에 대한 model 두 개를 새로 만들어 줬다. file의 id(UUID)와 이름을 생성하여 S3에 업로드한 후 해당 경로를 가져와 다시 meta를 DB에 추가할 것이다.

file model은 아래와 같다.

package com.unionpool.dottegi.api.models.user;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.UUID;

import com.unionpool.dottegi.api.plugins.S3Plugin;

public class UserProfileImageFile {

 private String id;
 private String bucket;
 private String name;
 private String fileUrl;
 private File file;
 
 public UserProfileImageFile() throws MalformedURLException {
  
  this.id = UUID.randomUUID().toString();
  this.bucket = S3Plugin.s3Bucket;
 }
 
 public URL getUrl() throws MalformedURLException {
  
  return new URL(S3Plugin.s3URL + this.bucket + "/" + getActualFileName());
 }
 
 public String getActualFileName() {
  return id + "_" + name;
 }
 
 public String getName() {
  return name;
 }
 
 public void setName(String name) {
  this.name = name;
 }
 
 public File getFile() {
  return file;
 }
 
 public void setFile(File file) {
  this.file = file;
 }
 
}

서비스에서는 아까 만들었던 S3Plugin이 있는지만 파악한다. 없는 경우 예외를 발생시키고 있다면 그대로 진행.

 @Override
 public String uploadProfileImage(UserProfileImageFile userProfileImageFile) throws MalformedURLException {
  
  if(S3Plugin.amazonS3 == null) {

   play.Logger.error("Could not save");
   
   throw new RuntimeException("Could Not Save Profile Image");
   
  } else {
   
   return userDao.uploadProfileImage(userProfileImageFile);
  }
 }

dao에서는 실제 객체를 S3에 업로드하게 된다. 거창하게 업로드하고 경로를 받아오는 것 같지만 실제로는 업로드될 경로를 미리 다 정해준 뒤에 그 경로를 이용하는 것이다. 실제 업로드한 뒤에 경로를 가져올 수 있는 방법도 있을 것 같은데...

 @Override
 public String uploadProfileImage(UserProfileImageFile userProfileImageFile) throws MalformedURLException {
  
  PutObjectRequest putObjReq = 
    new PutObjectRequest(S3Plugin.s3Bucket, userProfileImageFile.getActualFileName(), userProfileImageFile.getFile());
  putObjReq.withCannedAcl(CannedAccessControlList.PublicRead);
  S3Plugin.amazonS3.putObject(putObjReq);
  
  return userProfileImageFile.getUrl().toString();
 }

그리고 컨트롤러를 보면. 하다 만 것 같은 모양... 그래도 파일 업로드에 필요한 것들은 있다.

 public Result uploadUserProfile() {
  
  MultipartFormData body = request().body().asMultipartFormData();
  FilePart uploadFilePart = body.getFile("profileImage");
  Map temp = body.asFormUrlEncoded();
  String tempStr = body.toString();
  
  if(uploadFilePart != null) {
   try {
    UserProfileImageFile file = new UserProfileImageFile();
    file.setName(uploadFilePart.getFilename());
    file.setFile(uploadFilePart.getFile());
    
    String url = userService.uploadProfileImage(file);
    
    return ok(url);
    
   } catch (MalformedURLException e) {
    play.Logger.error(e.toString());
    return badRequest();
   }
   
  } else {
   return badRequest();
  }
  
 }

파일 업로드를 테스트하기 위해서 전에 사용하던 restclient가 아닌 cocoa restclient를 이용. 전의 것도 file upload 테스트가 되는 것 같긴 한데 뭔가 복잡.
어쨌든, cocoa restclient는 쉽다.

먼저 Request Hader에 Content-Type을 multipart/form-data로 추가해 주고


Files 탭을 누르고 들어가서 오른쪽에 있는 + 버튼을 눌러주면 파일을 추가할 수 있다. 추가한 후에 File Key를 실제 사용하는 key 이름으로 바꾸는 것을 잊지 말자.


어플리케이션 돌리고 테스트 해 보면.


성공.

댓글 없음:

댓글 쓰기