Bigtable, Blobstore 및 Google Storage를 사용하는 GAE 스토리지

[출처] http://www.dbguide.net/knowledge.db?cmd=view&boardUid=165874&boardConfigUid=20&boardStep=&categoryUid=574

GAE용 세 가지 데이터 스토리지 옵션에 관해 배운다.

이러한 저장소는 바이트를 덤프할 장소로 항상 사용되었기 때문에 디스크 드라이브와 그 위에 있는 파일 시스템을 당연한 것으로 생각하기 쉽다. 파일을 작성할 경우에는 파일의 위치, 사용 권한 및 공간 요구사항만 고려하면 된다. java.io.File을 생성하고 작업에 착수하면 되며, java.io.File은 사용자가 데스크탑 컴퓨터나 웹 서버 또는 모바일 디바이스 중 어느 위치에 있거나 동일하게 작동한다. 그러나 GAE로 작업하기 시작하면 이러한 투명성이나 투명성의 부족이 바로 분명해진다. GAE에서는 사용할 수 있는 파일 시스템이 없기 때문에 파일을 디스크에 쓸 수 없다. 이 클래스는 GAE SDK에서 블랙리스트에 올라 있기 때문에 사실상, java.io.FileInputStream을 선언하는 것만큼 컴파일 오류가 throw 명령을 통해 처리된다.

다행히도 사용 기간에는 옵션이 있으며 GAE는 특히 강력한 몇 가지 스토리지 옵션을 제공한다. 처음부터 확장성을 염두에 두고 설계되었기 때문에 GAE는 두 가지 키 값 저장소를 제공하며, Datastore(즉, Bigtable)는 일반적으로 데이터베이스에 삽입되는 일반 데이터를 저장하는 반면에 Blobstore는 대용량 2진 BLOB을 저장한다. 이 두 가지 저장소는 일정 시간 액세스 특성이 있으며 과거에 다루었을 수도 있는 파일 시스템과는 완전히 다르다.

이러한 두 가지 저장소 외에도 새로운 저장소가 있는데, 그것이 바로 Google Storage for Developers이다. 이 저장소는 기존의 파일 시스템과는 완전히 다른 Amazon S3와 비슷하게 작동한다. 이 기사에서는 각각의 GAE 스토리지 옵션을 차례로 구현하는 예제 애플리케이션을 빌드하게 된다. 독자는 Bigtable, Blobstore 및 Google Storage for Developers를 사용하는 것과 관련된 실천적 경험을 얻게 될 뿐만 아니라 각 구현의 장단점을 이해하게 될 것이다.

사전 설정: 예제 애플리케이션

GAE 스토리지 시스템을 탐구하기 전에 예제 애플리케이션에 필요한 세 가지 클래스를 작성해야 한다.

  • 사진을 표시하는 bean 클래스. Photo에는 title 및 caption과 같은 필드뿐만 아니라 2진 이미지 데이터를 저장하는 데 필요한 몇 가지 다른 필드가 포함되어 있다.
  • Photo가 GAE 데이터 저장소, Bigtable에 지속되게 하는 DAO 클래스. DAO에는 Photo를 삽입하는 메소드 하나와 ID로 Photo를 다시 끄집어내는 또 다른 메소드가 포함되어 있다. 이 클래스에서는 지속성을 위해 오픈 소스 라이브러리인 Objectify-Appengine을 사용한다.
  • Template Method 패턴을 사용하여 세 가지 단계의 워크플로우를 캡슐화하는 servlet 클래스. 여기에서는 워크플로우를 사용하여 각 GAE 스토리지 옵션을 탐구한다.

애플리케이션 워크플로우

이 기사에서는 동일한 절차에 따라 각 GAE 데이터 스토리지 옵션에 관해 배우게 되며, 이 과정에서 독자는 해당 기술에 집중할 수 있는 기회를 얻게 될 뿐만 아니라 각 스토리지 방법의 장단점을 비교할 수 있게 된다. 애플리케이션 워크플로우는 다음과 같이 매번 동일하다.

  1. 양식 표시 및 업로드
  2. 스토리지에 이미지를 업로드하고 데이터 저장소에 레코드 저장
  3. 이미지 제공

그림 1은 애플리케이션 워크플로우의 다이어그램이다.


그림 1. 각 스토리지 옵션을 설명하는 데 사용된 세 가지 단계의 워크플로우
GAE 데이터 스토리지 예제 애플리케이션 다이어그램

이 예제 애플리케이션을 이용하면 2진 이미지를 작성하여 제공하는 모든 GAE 프로젝트의 핵심 태스크를 실습할 수 있다는 부가적인 혜택을 누릴 수 있다. 이제 이러한 클래스를 작성해 보자.

간단한 GAE 애플리케이션

Eclipse를 설치하지 않았으면 Eclipse를 다운로드한 후, Eclipse용 Google 플러그인을 설치하고 GWT를 사용하지 않는 Google Web Application 프로젝트를 새로 작성한다. 프로젝트 파일을 구조화하는 방법에 관한 안내는 이 기사에 포함되어 있는 샘플 코드를 참조한다. Google 웹 애플리케이션 설정이 완료되면 목록 1과 같이 이 애플리케이션의 첫 번째 클래스인 Photo를 추가한다. (게터와 세터를 생략했다는 점에 유의한다.)


목록 1. Photo
import javax.persistence.Id; public class Photo {     @Id    private Long id;    private String title;    private String caption;    private String contentType;    private byte[] photoData;    private String photoPath;     public Photo() {    }     public Photo(String title, String caption) {        this.title = title;        this.caption = caption;    }     // getters and setters omitted}

@Id 어노테이션은 기본 키가 되는 필드를 지정하며 Objectify로 작업할 경?? 각 레코드는 엔티티라고도 하며 기본 키를 필요로 한다. 이미지가 업로드되면 이 이미지를 바이트 배열인 photoData에 직접 저장하도록 선택할 수 있다. 이 이미지는 Photo의 나머지 필드와 함께 Blob 특성으로 데이터 저장소에 작성된다. 다시 말해서 이 이미지는 bean 바로 옆에 저장되고 페치된다. 그대신 이미지가 Blobstore나 Google Storage에 업로드되면 바이트가 해당 시스템에 저장되고 photoPath는 바이트의 경로를 가리킨다. 어느 경우에든 photoData 또는 photoPath가 사용된다. 그림 2에는 각 저장소의 기능이 명확하게 표시되어 있다.


그림 2. photoData와 photoPath의 작동 방식
photoData와 photoPath 간의 차이점이 표시되어 있는 다이어그램

다음에는 bean의 지속성을 처리하게 된다.

오브젝트 기반 지속성

앞에서 언급한 바와 같이 Objectify를 사용하여 Photo bean을 위한 DAO를 작성할 것이다. JDO와 JPA가 더 인기 있고 널리 사용되는 지속성 API이지만, 이러한 API는 학습 커브가 가파르다. 앞에서와 달리 하위 레벨 GAE 데이터 저장소 API를 사용하기로 선택할 수도 있지만, 이런 경우에는 bean을 마샬링하여 데이터 저장소 엔티티에 저장해야 하는 성가신 작업이 수반된다. Objectify는 Java 리플렉션을 통해 이러한 작업을 처리한다. (Objectify-Appengine을 포함하여 GAE 지속성 대안에 관해 자세히 배우려면 참고자료를 참조한다.)

먼저, 목록 2에서와 같이 PhotoDao 클래스를 작성하고 코딩한다.


목록 2. PhotoDao
import com.googlecode.objectify.*;import com.googlecode.objectify.helper.DAOBase; public class PhotoDao extends DAOBase {     static {        ObjectifyService.register(Photo.class);    }     public Photo save(Photo photo) {        ofy().put(photo);        return photo;    }        public Photo findById(Long id) {        Key<Photo> key = new Key<Photo>(Photo.class, id);        return ofy().get(key);    }}

PhotoDaoObjectify 인스턴스를 느리게 로드하는 편리한 클래스인 DAOBase를 확장한다. Objectify는 API로 향하는 기본 인터페이스로, ofy 메소드를 통해 노출된다. 그러나 ofy를 사용하려면 목록 2에 있는 Photo와 같은 정적 초기화 프로그램에 지속적 클래스를 등록해야 한다.

DAO에는 Photo를 삽입하고 찾는 데 필요한 두 가지 메소드가 포함되어 있다. 각 메소드에서 Objectify에 대한 작업은 해시 테이블에 대한 작업만큼이나 간단하다. findById에 있는 Key를 사용하여 Photo를 페치한다는 사실을 알았을 수도 있지만, 이 기사에서는 Keyid 필드를 둘러싸는 랩퍼라고 생각한다.

이제 지속성을 관리할 PhotoDaoPhoto bean이 작성되었다. 다음에는 애플리케이션 워크플로우를 더 구체화한다.

Template Method 패턴을 통한 애플리케이션 워크플로우

Mad Lib을 해 본 경험이 있으면 Template Method 패턴이 이해가 될 것이다. 각 Mad Lib은 독자가 채우게 되는 다량의 빈 지점으로 구성된 이야기를 나타낸다. 독자의 입력, 즉 빈 지점이 어떻게 채워지느냐에 따라 이야기는 급격하게 바뀐다. 마찬가지로 Template Method 패턴을 사용하는 클래스에는 일련의 단계가 포함되어 있으며 이 중 일부는 빈 채로 남아 있다.

이 기사에서는 Template Method 패턴을 사용하여 예제 애플리케이션의 워크플로우를 실행하는 서블릿을 빌드할 것이다. 먼저, 추상 서블릿을 스텁아웃한 후, 이름을 AbstractUploadServlet으로 지정한다. 목록 3에 있는 코드를 참조로 사용할 수 있다.


목록 3. AbstractUploadServlet
import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.http.*; @SuppressWarnings("serial")public abstract class AbstractUploadServlet extends HttpServlet { }

다음에는 목록 4에 있는 세 가지 추상 메소드를 추가한다. 각 메소드는 워크플로우의 단계를 나타낸다.


목록 4. 세 가지 추상 메소드
protected abstract void showForm(HttpServletRequest req,     HttpServletResponse resp) throws ServletException, IOException; protected abstract void handleSubmit(HttpServletRequest req,     HttpServletResponse resp) throws ServletException, IOException; protected abstract void showRecord(long id, HttpServletRequest req,     HttpServletResponse resp) throws ServletException, IOException;

이제, Template Method 패턴을 사용하고 있다는 점을 감안하여 목록 4에 있는 메소드와 목록 5에 있는 코드를 각각 공백과 공백을 짜맞추는 이야기라고 생각한다.


목록 5. 드러나는 워크플로우
 @Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp)    throws ServletException, IOException {    String action = req.getParameter("action");    if ("display".equals(action)) {        // don't know why GAE appends underscores to the query string        long id = Long.parseLong(req.getParameter("id").replace("_", ""));        showRecord(id, req, resp);    } else {        showForm(req, resp);    }} @Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp)     throws ServletException, IOException {    handleSubmit(req, resp);}

서블릿을 생각나게 하는 것

오랫동안 이전의 일반 서블릿으로 작업해 온 경우에는 doGetdoPostHTTP GETPOST를 처리하는 데 필요한 표준 메소드였다. GET을 사용하여 웹 자원을 페치하고 POST를 사용하여 데이터를 전송하는 것이 일반적인 관습이었다. 이러한 방식으로 doGet 구현은 업로드 양식이나 스토리지의 사진을 표시하고 doPost는 업로드 양식을 제출한다. 이러한 작업은 AbstractUploadServlet을 확장하여 각 작동을 정의하는 클래스가 담당한다. 그림 3에 있는 다이어그램에는 발생하는 이벤트의 시퀀스가 표시되어 있다. 정확히 어떠한 일이 벌어지고 있는지 분명히 알려면 몇 분 정도 시간이 필요하다.


그림 3. 시퀀스 다이어그램의 워크플로우
예제 애플리케이션 워크플로우의 시퀀스 다이어그램

세 가지 클래스가 빌드되면 예제 애플리케이션을 시작할 준비가 된다. 이제, Bigtable을 시작으로 각 GAE 스토리지 옵션이 애플리케이션 워크플로우와 어떻게 상호 작용하는지 집중적으로 살펴보도록 하자.

GAE 스토리지 옵션 #1: Bigtable

Google의 GAE 문서에는 Bigtable이 공유되고, 정렬된 배열로 기술되어 있지만, Bigtable을 방대한 서버 전체에 청크된 거대한 해시 테이블로 여기는 것이 더 이해하기 쉽다고 생각한다. 관계형 데이터베이스와 마찬가지로 Bigtable에는 데이터 유형이 있다. 사실상, Bigtable과 관계형 데이터베이스는 blob 유형을 사용하여 2진 데이터를 저장한다.

blob 유형을 나중에 살펴보게 될, GAE의 다른 키 값 저장소인 Blobstore와 혼동하지 말아야 한다.

blob은 다른 필드와 함께 로드하여 즉시 사용할 수 있으므로 Bigtable의 blob에 대한 작업이 가장 편하다. 한 가지 중요한 제한사항은 blob은 크기가 1MB 이하여야 한다는 점이지만, 이러한 제한사항은 미래에는 완화될 것이다. 현재는 크기가 1MB 미만인 사진을 찍는 디지털 카메라를 찾기가 어려우므로 Bigtable을 사용하는 것이 이미지와 관련된 유스 케이스의 단점을 돌릴 수 있는 방법이다. 현재 1MB 규칙을 따르고 있거나 이미지보다 더 작은 무엇인가를 저장하고 있는 경우에도 세 가지 GAE 스토리지 대안 중 Bigtable이 좋은 선택이 될 수 있으며 작업하기도 가장 수월하다.

Bigtable에 데이터를 업로드할 수 있으려면 업로드 양식을 작성해야 한다. 그런 다음에는 Bigtable에 맞게 사용자 정의된 세 가지 추상 메소드로 구성된 서블릿 구현을 처리할 것이다. 1MB 한계는 사람들이 위반하기 쉬우므로 마지막에는 오류 처리 기능을 구현하게 된다.

업로드 양식 작성

그림 4에는 Bigtable용 업로드 양식이 표시되어 있다.


그림 4. Bigtable용 업로드 양식
디지털 이미지용 업로드 양식의 스크린샷

이 양식을 작성하려면 datastore.jsp 파일로 시작하여 목록 6에 있는 코드 블록을 삽입한다.


목록 6. 사용자 정의 업로드 양식
<html>    <head>        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />    </head>    <body>		        <form method="POST" enctype="multipart/form-data">            <table>                <tr>	                    <td>Title</td>                    <td><input type="text" name="title" /></td>                </tr>                <tr>	                    <td>Caption</td>                    <td><input type="text" name="caption" /></td>                </tr>                <tr>	                    <td>Upload</td>                    <td><input type="file" name="file" /></td>                </tr>                <tr>                    <td colspan="2"><input type="submit" /></td>                </tr>				            </table>        </form>    </body>	</html>

이 양식에서는 method 속성과 enclosure 유형을 각각 POST와 multipart/form-data로 설정해야 한다. action 속성을 지정하지 않았으므로 이 양식은 자신에게 제출된다. POST를 수행하면 결국 AbstractUploadServletdoPosthandleSubmit을 호출한다.

양식을 작성했으므로 이 양식을 지원하는 서블릿을 살펴보도록 하자.

Bigtable에 업로드하고 업로드한 이미지를 다시 꺼내기

여기에서는 세 가지 메소드를 차례로 구현한다. 하나는 방금 작성한 양식을 표시하고 또 다른 하나는 업로드를 처리한다. 마지막 메소드는 업로드한 이미지를 다시 사용자에게 표시하는 역할을 하므로 이러한 과정이 어떻게 완료되는지 확인할 수 있다.

이 서블릿은 Apache Commons FileUpload 라이브러리를 사용한다. 이 라이브러리와 그 종속 항목을 다운로드하여 프로젝트에 삽입한다. 이 작업이 완료되면 목록 7에 있는 스텁을 생각해 본다.


목록 7. DatastoreUploadServlet
import info.johnwheeler.gaestorage.core.*;import java.io.*;import javax.servlet.ServletException;import javax.servlet.http.*;import org.apache.commons.fileupload.*;import org.apache.commons.fileupload.servlet.ServletFileUpload;import org.apache.commons.fileupload.util.Streams; @SuppressWarnings("serial")public class DatastoreUploadServlet extends AbstractUploadServlet {    private PhotoDao dao = new PhotoDao();}

여기에서는 아직 흥미로운 내용이 없다. 필요한 클래스를 가져오고 나중에 사용할 PhotoDao를 구성한다. 추상 모델을 구현할 때까지는 DatastoreUploadServlet은 컴파일하지 않을 것이다. 목록 8에 있는 showForm을 시작으로 각각 단계별로 살펴보도록 하자.


목록 8. showForm
@Overrideprotected void showForm(HttpServletRequest req, HttpServletResponse resp)     throws ServletException, IOException {    req.getRequestDispatcher("datastore.jsp").forward(req, resp);        }

아는 바와 같이 showForm은 업로드 양식을 전달할 뿐이다. 목록 9에 있는 handleSubmit이 더 관련이 있다.


목록 9. handleSubmit
@Overrideprotected void handleSubmit(HttpServletRequest req,    HttpServletResponse resp) throws ServletException, IOException {    ServletFileUpload upload = new ServletFileUpload();     try {        FileItemIterator it = upload.getItemIterator(req);         Photo photo = new Photo();         while (it.hasNext()) {            FileItemStream item = it.next();            String fieldName = item.getFieldName();            InputStream fieldValue = item.openStream();             if ("title".equals(fieldName)) {                photo.setTitle(Streams.asString(fieldValue));                continue;            }             if ("caption".equals(fieldName)) {                photo.setCaption(Streams.asString(fieldValue));                continue;            }             if ("file".equals(fieldName)) {                photo.setContentType(item.getContentType());                ByteArrayOutputStream out = new ByteArrayOutputStream();                Streams.copy(fieldValue, out, true);                photo.setPhotoData(out.toByteArray());                continue;            }        }         dao.save(photo);        resp.sendRedirect("datastore?action=display&id=" + photo.getId());                } catch (FileUploadException e) {        throw new ServletException(e);    }        }

코드가 길지만 이 코드가 하는 기능은 단순하다. handleSubmit 메소드는 업로드 양식의 요청 본문을 스트리밍하고, 각 양식 값을 FileItemStream으로 추출한다. 반면에 Photo는 한 번에 한 조각씩 설정한다. 각 필드를 조사하여 무엇이 유용한지 확인하는 것은 그다지 좋지 않지만, 이렇게 하는 것이 스트리밍 데이터와 스트리밍 API의 처리 방식이다.

코드로 다시 돌아가서 file 필드를 살펴보면 ByteArrayOutputStream은 업로드된 바이트를 photoData에 조금씩 보내는 데 도움을 준다. 마지막으로 PhotoDao를 사용하여 Photo를 저장하고 경로 재지정을 전송한다. 그러면 최종 추상 클래스인 showRecord(목록 10)를 시작할 수 있다.


목록 10. showRecord
@Overrideprotected void showRecord(long id, HttpServletRequest req,     HttpServletResponse resp) throws ServletException, IOException {    Photo photo = dao.findById(id);            resp.setContentType(photo.getContentType());            resp.getOutputStream().write(photo.getPhotoData());    resp.flushBuffer();                    }

showRecordPhoto를 검색하고, photoData 바이트 배열을 직접 HTTP 응답에 쓰기 전에 content-type 헤더를 설정한다. flushBuffer는 남아 있는 컨텐츠를 강제로 브라우저로 보낸다.

마지막으로 1MB보다 큰 이미지를 업로드하는 경우, 이를 오류로 처리하는 일부 코드를 추가해야 한다.

오류 메시지 표시

앞에서 언급한 바와 같이 Bigtable에는 1MB 한계가 있으며, 이는 이미지와 관련된 대부분의 유스 케이스에서 해결되지 않고 있는 문제점이다. 기껏해야 사용자에게 이미지의 크기를 조정한 후, 다시 시도하라고 요청할 수 있을 뿐이다. 데모를 하기 위한 것이므로 목록 11에 있는 코드는 GAE 예외가 throw 명령으로 처리될 때 단순히 예외 메시지를 표시한다. (이는 서블릿에 특정된 표준 오류 처리 방식이지 GAE에 특정된 것은 아니라는 점에 유의한다.)


목록 11. 오류가 발생함
import java.io.*;import javax.servlet.ServletException;import javax.servlet.http.*; @SuppressWarnings("serial")public class ErrorServlet extends HttpServlet {    @Override    protected void service(HttpServletRequest req, HttpServletResponse res)         throws ServletException, IOException {        String message = (String)               req.getAttribute("javax.servlet.error.message");                PrintWriter out = res.getWriter();        out.write("<html>");        out.write("<body>");        out.write("<h1>An error has occurred</h1>");                        out.write("<br />" + message);                out.write("</body>");        out.write("</html>");    }}

이 기사에서 작성하게 될 다른 서블릿과 ErrorServlet을 web.xml에 등록하는 것을 잊지 말아야 한다. 목록 12에 있는 코드는 ErrorServlet을 가리키는 오류 페이지를 등록한다.


목록 12. 오류 페이지 등록
<servlet>    <servlet-name>errorServlet</servlet-name>	      <servlet-class>        info.johnwheeler.gaestorage.servlet.ErrorServlet    </servlet-class></servlet> <servlet-mapping>    <servlet-name>errorServlet</servlet-name>    <url-pattern>/error</url-pattern></servlet-mapping> <error-page>    <error-code>500</error-code>    <location>/error</location></error-page>

이것으로 GAE 데이터 저장소라고도 하는 Bigtable에 대한 간단한 소개를 마무리한다. Bigtable은 GAE 스토리지 옵션 중 가장 직관적이지만, 파일 크기가 1MB로 제한되는 단점이 있어서 썸네일보다 더 큰 것(그러한 것이 있는 경우) 에 대해서는 Bigtable을 사용하고 싶지 않을 것이다. 다음에는 최대 2GB 크기의 파일을 저장하고 제공할 수 있는 또 다른 키 값 스토리지 옵션인 Blobstore를 살펴본다.

GAE 스토리지 옵션 #2: Blobstore

Bigtable에 비해 Blobstore는 크기면에서 장점을 지니지만, 문제점이 없는 것은 아니다. 다시 말해서 Blobstore를 사용하는 경우에는 일회성 업로드 URL을 사용해야 하기 때문에 여기저기에 웹 서비스를 구축하기가 어렵다. 예제는 다음과 같다.

/_ah/upload/aglub19hcHBfaWRyGwsSFV9fQmxvYlVwbG9hZFNlc3Npb25fXxh9DA

웹 서비스 클라이언트는 해당 URL에 POST하기 전에 이 URL을 요청해야 하며 이로 인해 네트워크에서 추가적인 호출이 필요하다. 이는 대부분의 애플리케이션에 심각한 영향을 주지는 않지만 그렇다고 아무런 문제가 없는 것은 아니다. CPU 사용 시간에 요금이 청구되는 GAE에서 클라이언트가 실행 중인 경우에는 이점이 문제가 될 수 있다. URLFetch를 통해 일회성 URL에 업로드를 전달하는 서블릿을 빌드하면 이러한 문제점을 피할 수 있을 것으로 생각한다면 다시 생각해 보아야 한다. URLFetch는 전송할 수 있는 한계가 1MB이기 때문에 그렇게 하려면 Bigtable을 사용하는 편이 낫다. 그림 5에 있는 그래픽에는 판단에 도움을 줄 수 있는, 한 갈래의 웹 서비스 호출과 두 갈래의 웹 서비스 호출의 차이점이 표시되어 있다.


그림 5. 한 갈래의 웹 서비스 호출과 두 갈래의 웹 서비스 호출의 차이점
웹 서비스 클라이언트와 Blobstore(두 갈래로 된) 사이 및 웹 서비스 클라이언트와 Bigtable(한 갈래로 된) 사이에서 이동하는 이미지 파일이 표시되어 있는 그래픽

Blobstore에는 장단점이 있으며 다음 섹션에서는 이점을 자세하게 확인한다. 다시 한 번 더 양식을 빌드하고 업로드하며 AbstractUploadServlet으로 제공되는 세 가지 추상 메소드를 구현하지만, 이번에는 Blobstore에 맞게 코드를 조정한다.

Blobstore용 업로드 양식

업로드 양식을 Blobstore에 맞게 고칠 부분은 별로 없으므로 datastore.jsp를 blobstore.jsp 파일로 복사한 후, 이 파일에 목록 13 코드의 굵은체로 표시된 행을 추가한다.


목록 13. blobstore.jsp
<% String uploadUrl = (String) request.getAttribute("uploadUrl"); %><html>    <head>        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />    </head>    <body>		        <form method="POST" action="<%= uploadUrl %>"            enctype="multipart/form-data">		<!-- labels and fields omitted -->        </form>    </body>	</html>

일회성 업로드 URL은 다음에 코딩할 서블릿에서 생성된다. 여기에서 업로드 URL이 해당 요청으로부터 구문 분석되어 양식의 action 속성에 배치된다. 업로드할 Blobstore 서블릿에 대한 제어는 없다. 그렇다면 기타 양식 값은 어떻게 얻게 될까? 그 해답은 Blobstore API의 콜백 메커니즘에 있다. 일회성 URL이 생성되면 콜백 URL을 Blobstore API에 전달한다. 업로드 후에는 Blobstore가 콜백을 호출하여 원래의 요청을 업로드된 모든 blob과 함께 전달한다. 다음에 AbstractUploadServlet을 구현하면 이러한 작동 상태에 있는 모든 것을 확인할 수 있다.

Blobstore에 업로드하기

먼저, 목록 14를 참조로 사용하여 AbstractUploadServlet을 확장하는 BlobstoreUploadServlet 클래스를 스텁아웃한다.


목록 14. BlobstoreUploadServlet
import info.johnwheeler.gaestorage.core.*;import java.io.IOException;import java.util.Map;import javax.servlet.ServletException;import javax.servlet.http.*;import com.google.appengine.api.blobstore.*; @SuppressWarnings("serial")public class BlobstoreUploadServlet extends AbstractUploadServlet {    private BlobstoreService blobService =         BlobstoreServiceFactory.getBlobstoreService();    private PhotoDao dao = new PhotoDao();}

초기 클래스 정의는 DatastoreUploadServlet에서 했던 것과 비슷하지만, BlobstoreService 변수가 새로 추가되었다. 이것이 목록 15의 showForm에 있는 일회성 URL을 생성한다.


목록 15. showForm for blobstore
@Overrideprotected void showForm(HttpServletRequest req, HttpServletResponse resp)     throws ServletException, IOException {    String uploadUrl = blobService.createUploadUrl("/blobstore");    req.setAttribute("uploadUrl", uploadUrl);    req.getRequestDispatcher("blobstore.jsp").forward(req, resp);}

목록 15에 있는 코드는 업로드 URL을 작성하여 이 URL을 해당 요청에 설정한다. 그 다음에는 이 코드가 목록 13에서 작성된 양식에 전달되며, 여기서 업로드 URL이 예상된다. 콜백 URL은 web.xml에 정의된 대로 이 서블릿의 컨텍스트에 설정된다. 이와 같이 Blobstore가 다시 POST되면 목록 16에 표시되어 있는 handleSubmit에 도달한다.


목록 16. Blobstore용 handleSubmit
@Overrideprotected void handleSubmit(HttpServletRequest req,     HttpServletResponse resp) throws ServletException, IOException {    Map<String, BlobKey> blobs = blobService.getUploadedBlobs(req);    BlobKey blobKey = blobs.get(blobs.keySet().iterator().next());     String photoPath = blobKey.getKeyString();    String title = req.getParameter("title");    String caption = req.getParameter("caption");        Photo photo = new Photo(title, caption);    photo.setPhotoPath(photoPath);    dao.save(photo);     resp.sendRedirect("blobstore?action=display&id=" + photo.getId());}

getUploadedBlobsBlobKeysMap을 리턴한다. 업로드 양식은 단일 업로드를 지원하므로 예상되는 유일한 BlobKey를 가져와서 이것의 문자열 표현을 photoPath 변수에 채운다. 그 후에는 나머지 필드가 구문 분석되어 변수에 저장되고 새로운 Photo 인스턴스에 설정된다. 이 인스턴스는 목록 17에 있는 showRecord로 경로가 재지정되기 전에 데이터 저장소에 저장된다.


목록 17. Blobstore용 showRecord
@Overrideprotected void showRecord(long id, HttpServletRequest req,     HttpServletResponse resp) throws ServletException, IOException {    Photo photo = dao.findById(id);    String photoPath = photo.getPhotoPath();     blobService.serve(new BlobKey(photoPath), resp);}

showRecord에서는 방금 handleSubmit에 저장된 Photo가 Blobstore에서 다시 로드된다. 어떤 것이든 업로드된 것의 실제 바이트는 Bigtable에 있었기 때문에 bean에 저장되지 않는다. 그 대신 BlobKeyphotoPath와 함께 다시 빌드되어 브라우저에 이미지를 제공하는 데 사용된다.

Blobstore는 양식 기반 업로드에 대한 작업을 짜증나게 하지만, 웹 서비스 기반 업로드는 얘기가 다르다. 다음에는 정확히 상반되는 난제, 즉 양식 기반 업로드는 해킹이 다소 필요한 반면에 서비스 기반 업로드는 쉽다는 점을 충족시켜 주는 Google Storage for Developers를 확인하게 된다.

GAE 스토리지 옵션 #3: Google Storage

Google Storage for Developers는 세 가지 GAE 스토리지 옵션 중에서 가장 기능이 강력하며 특별한 몇 가지 사항을 분명하게 하면 사용하기도 쉽다. Google Storage는 Amazon S3와 공통점이 많으며 사실상, 이 두 가지는 동일한 프로토콜과 RESTful 인터페이스를 사용하며 따라서 라이브러리가 JetS3t와 같은 S3 및 Google Storage와 함께 작동하도록 되어 있다. 불행히도 이 기사를 쓰는 현재는 이 라이브러리가 스레드 복제와 같은 허용되지 않은 조작을 수행하기 때문에 Google App Engine에서 정확하게 작동하지 않는다. 따라서 잠깐 동안 RESTful 인터페이스와 함께 작동하도록 내버려 두고 이렇게 하지 않았으면 이러한 API가 수행하게 되었을 힘든 작업 중 일부를 수행한다.

Google Storage는 ACL(Access Control Lists)을 통해 강력한 액세스 제어를 지원하므로 노력을 들일 만한 가치가 있다. ACL을 사용하면 오브젝트에 읽기 전용 및 읽기-쓰기 액세스 권한을 부여할 수 있으므로 Facebook과 Flickr에 있는 사진과 마찬가지로 사진을 쉽게 공용화하거나 사설화할 수 있다. 이 기사에서는 ACL을 다루지 않으므로 업로드하게 되는 모든 것에는 공용 읽기 전용 액세스 권한이 부여된다. ACL을 자세히 배우려면 Google Storage 온라인 문서(참고자료)를 참조한다.

Google Storage 정보

2010년 5월에 프리뷰 에디션으로 릴리스된 Google Storage for Developers는 현재, 미국에 있는 개발자 중 제한된 인원만 사용할 수 있으며 프리뷰 에디션에 대한 대기 목록이 있다. 아직은 초기 단계에 있는 Google Storage에는 구현상에 몇 가지 문제점이 있으며, 이 섹션에서는 이러한 문제점을 극복하는 방법을 살펴본다. Google Storage와 GAE 간에 명백한 통합 경로가 없다는 점은 코드를 추가로 작성해야 한다는 점을 의미하지만, 액세스 제어를 필요로 하는 경우와 같은 일부 유스 케이스의 경우는 노력을 들일 만한 가치가 있다. 조만간 통합 라이브러리를 살펴볼 수 있기를 바란다.

Blobstore와 달리 Google Storage는 기본적으로 웹 서비스와 브라우저 클라이언트 형태로 모두 사용할 수 있다. 데이터는 RESTful PUT이나 POST를 통해 전송된다. 첫 번째 옵션은 요청이 구조화되는 방식과 헤더가 작성되는 방식을 제어할 수 있는 웹 서비스 클라이언트용이다. 여기에서 탐구하게 될 두 번째 옵션은 브라우저 기반 업로드용이다. 업로드 양식을 처리하려면 Javascript 핵이 필요한데, 알다시피 여기에는 몇 가지 복잡한 문제점이 존재한다.

Google Storage 업로드 양식 해킹

Blobstore와 달리, Google Storage는 URL이 POST된 후에도 콜백 URL로 전달하지 않는다. 그 대신 지정한 URL로 경로를 재지정한다. 양식 값은 경로 재지정을 거쳐 실행되지 않기 때문에 이렇게 하면 문제가 발생한다. 이러한 문제점을 회피하려면 같은 웹 페이지에 두 가지 양식을 작성하여, 하나에는 제목과 캡션 텍스트 필드를 삽입하고 다른 하나에는 파일 업로드 필드와 Google Storage 필수 매개변수를 포함시켜야 한다. 그런 다음에는 Ajax를 사용하여 첫 번째 양식을 제출한다. Ajax 콜백이 호출되면 두 번째 업로드 양식을 제출한다.

이 양식은 마지막 두 가지 양식보다 더 복잡하므로 단계별로 구성한다. 먼저, 아직 빌드되지 않은 전달 서블릿(목록 18)에 의해 설정되는 몇 가지 값을 추출한다.


목록 18. 양식 값 추출
<% String uploadUrl = (String) request.getAttribute("uploadUrl");String key = (String) request.getAttribute("key");String successActionRedirect = (String)     request.getAttribute("successActionRedirect");String accessId = (String) request.getAttribute("GoogleAccessId");String policy = (String) request.getAttribute("policy");String signature = (String) request.getAttribute("signature");String acl = (String) request.getAttribute("acl");%>

uploadUrl은 Google Storage의 REST 엔드포인트를 유지한다. API는 아래와 같은 두 가지를 제공한다. 어느 것이나 사용할 수 있지만, 기울임꼴로 되어 있는 컴포넌트를 해당 값으로 바꿔야 한다.

  • bucket.commondatastorage.googleapis.com/object
  • commondatastorage.googleapis.com/bucket/object

나머지 변수는 다음과 같은 Google Storage 매개변수이다.

  • key: Google Storage에 업로드된 데이터의 이름
  • success_action_redirect: 업로드가 완료되면 경로가 재지정되는 위치
  • GoogleAccessId: Google에서 지정하는 API 키
  • policy: Base-64로 인코딩된 JSON 문자열(데이터 업로드 방식 제한)
  • signature: 해시 알고리즘으로 사인되어 Base-64로 인코딩된 정책. 인증에 사용된다.
  • acl: 액세스 제어 목록 스펙

두 가지 양식과 제출 단추

목록 19에 있는 첫 번째 양식에는 Title과 Caption 필드가 있다. 둘러싸는 <html><body> 태그는 생략되었다.


목록 19. 첫 번째 업로드 양식
<form id="fieldsForm" method="POST">    <table>        <tr>	            <td>Title</td>            <td><input type="text" name="title" /></td>        </tr>        <tr>	            <td>Caption</td>            <td>                <input type="hidden" name="key" value="<%= key %>" />	                <input type="text" name="caption" />            </td>        </tr>			    </table>		</form>

이 양식은 자신에게 POST한다는 점을 제외하면 그다지 언급할 만한 사항이 없다. 목록 20에 있는 양식을 살펴보도록 하자. 이 양식은 숨겨진 입력 필드를 여섯 개 포함하고 있어서 훨씬 더 크다.


목록 20. 숨겨진 필드가 있는 두 번째 양식
 <form id="uploadForm" method="POST" action="<%= uploadUrl %>"     enctype="multipart/form-data">    <table>        <tr>            <td>Upload</td>            <td>                <input type="hidden" name="key" value="<%= key %>" />                <input type="hidden" name="GoogleAccessId"                     value="<%= accessId %>" />                <input type="hidden" name="policy"                     value="<%= policy %>" />                <input type="hidden" name="acl" value="<%= acl %>" />                <input type="hidden" id="success_action_redirect"                     name="success_action_redirect"                     value="<%= successActionRedirect %>" />                <input type="hidden" name="signature"                    value="<%= signature %>" />                <input type="file" name="file" />            </td>        </tr>        <tr>            <td colspan="2">                <input type="button" value="Submit" id="button"/>            </td>        </tr>    </table></form>

JSP Scriptlet(목록 18)에서 추출된 값은 숨겨진 필드에 배치된다. 파일 입력은 맨 아래에 있다. 제출 단추는 목록 21과 같이 Javascript를 사용하여 작동 가능하게 할 때까지는 아무런 작동도 하지 않는 일반 단추이다.


목록 21. 업로드 양식 제출
<script type="text/_javascript" src="https://Ajax.googleapis.com/Ajax/libs/jquery/1.4.3/jquery.min.js"></script><script type="text/_javascript">    $( document).ready(function() {			        $('#button').click(function() {            var formData = $('#fieldsForm').serialize();            var callback = function(photoId) {                var redir = $('#success_action_redirect').val() +                    photoId;                $('#success_action_redirect').val(redir)                $('#uploadForm').submit();             };			             $.post("gstorage", formData, callback);         });     });</script>

목록 21에 있는 Javascript는 JQuery로 작성된다. 라이브러리를 사용하지 않았지만, 코드를 이해하? 가져온다. 그 후에는 단추를 클릭하면 Ajax를 통해 첫 번째 양식이 제출되도록 클릭 리스너가 단추에 설치된다. 그러고 나면, 여기서 간단히 빌드하게 될 서블릿의 handleSubmit 메소드에 도달하며, 여기서는 Photo가 구성되어 데이터 저장소에 저장된다. 마지막으로 업로드 양식이 제출되기 전에 새 Photo ID가 콜백에 리턴되고 success_action_redirect에 있는 URL에 추가된다. 재지정된 경로에서 다시 돌아오면 Photo를 검색하여 그 이미지를 표시할 수 있다. 그림 6에는 전체 이벤트 시퀀스가 표시되어 있다.


그림 6. Javascript 호출 경로가 표시되어 있는 시퀀스 다이어그램
Javascript 호출 경로가 표시되어 있는 시퀀스 다이어그램

양식을 처리하려면 정책 문서를 작성하고 사인할 유틸리티 클래스가 필요하다. 그 후에는 AbstractUploadServlet을 서브클래스로 분류할 수 있다.

정책 문서 작성 및 사인

정책 문서는 업로드를 제한한다. 예를 들면, 업로드할 수 있는 용량이나 허용되는 파일 유형을 지정할 수 있으며 파일 이름을 제한할 수도 있다. 공용 버킷은 정책 문서를 필요로 하지 않지만, Google Storage와 같은 사설 버킷에는 정책 문서가 있어야 한다. 작동하게 하려면 목록 22에 있는 코드를 기반으로 하는 GSUtils 유틸리티 클래스를 스텁아웃한다.


목록 22. GSUtils
import java.io.UnsupportedEncodingException;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;import java.text.DateFormat;import java.text.SimpleDateFormat;import java.util.Calendar;import java.util.GregorianCalendar;import java.util.TimeZone; import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec; import com.google.appengine.repackaged.com.google.common.util.Base64; private class GSUtils {}

일반적으로 유틸리티 클래스가 정적 메소드로만 구성된다는 점을 감안하면 기본 생성자를 사설화하여 인스턴스화를 막는 것이 좋다. 이 클래스를 스텁아웃하면 정책 문서를 작성하는 데 관심을 돌릴 수 있다.

정책 문서는 JSON 형식으로 되어 있지만, 이 JSON은 어떤 멋진 라이브러리에 의존하지 않아도 될 정도로 단순하다. 그 대신, 간단한 StringBuilder를 사용하여 수동으로 JSON 형식을 처리할 수 있다. 우선, ISO8601 날짜를 구성하고 이 날짜에 따라 정책 문서가 만료되도록 설정해야 한다. 정책 문서가 만료되면 업로드가 진행되지 않는다. 그 다음에는 앞에서 언급한 제한조건(정책 문서의 conditions)을 삽입해야 한다. 마지막으로 이 문서는 Base-64로 인코딩된 후, 호출자에게 리턴된다.

목록 23에 있는 메소드를 GSUtils에 추가한다.


목록 23. 정책 문서 작성
public static String createPolicyDocument(String acl) {    GregorianCalendar gc = new GregorianCalendar();    gc.add(Calendar.MINUTE, 20);     DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");    df.setTimeZone(TimeZone.getTimeZone("GMT"));    String expiration = df.format(gc.getTime());     StringBuilder buf = new StringBuilder();    buf.append("{\"expiration\": \"");    buf.append(expiration);    buf.append("\"");    buf.append(",\"conditions\": [");    buf.append(",{\"acl\": \"");    buf.append(acl);    buf.append("\"}");            buf.append("[\"starts-with\", \"$key\", \"\"]");    buf.append(",[\"starts-with\", \"$success_action_redirect\", \"\"]");    buf.append("]}");     return Base64.encode(buf.toString().replaceAll("\n", "").getBytes());}

미래의 20분으로 설정된 GregorianCalendar를 사용하여 만기 날짜를 구성한다. 이 코드는 복잡하므로, JSONLint(참고자료 참조)와 같은 도구를 통해 이 코드를 콘솔에 인쇄하고 복사하고 실행하면 도움이 된다. 다음에는 정책 문서에 acl을 전달하여 acl을 하드코딩하지 않아도 되도록 한다. 어느 것이든 그 변수는 acl과 같은 메소드 인수로 전달되어야 한다. 마지막으로 이 문서는 Base-64로 인코딩된 후, 호출자에게 리턴된다. 정책 문서에서 허용되는 것에 관한 자세한 정보는 Google Storage 문서를 참조한다.

Google의 SDC(Secure Data Connector)

이 기사에서는 Google의 SDC(Secure Data Connector)를 다루지 않지만, Google Storage를 사용할 경우에는 이 도?스템이 방화벽 뒤에 있는 경우에도 자신의 시스템에 있는 데이터를 쉽게 액세스할 수 있다.

Google Storage에서의 인증

정책 문서는 두 가지 기능을 한다. 정책을 시행하는 기능 외에 정책 문서는 업로드를 인증하기 위해 생성하는 시그너처의 기초가 된다. Google Storage에 등록하면 등록자와 Google만 알 수 있는 비밀 키가 주어진다. 등록자가 등록자의 위치에서 비밀 키를 사용하여 이 문서에 사인하면 Google도 같은 키를 사용하여 이 문서에 사인한다. 시그너처가 일치하면 업로드가 허용된다. 그림 7에는 이러한 과정이 잘 묘사되어 있다.


그림 7. Google Storage에서 업로드를 인증하는 과정
GAE 인증 과정이 표시되어 있는 다이어그램

여기에서는 시그너처를 생성하기 위해 GSUtils를 스텁아웃하는 과정에서 가져온 javax.cryptojava.security 패키지를 사용한다. 목록 24에는 메소드가 표시되어 있다.


목록 24. 정책 문서 사인
public static String signPolicyDocument(String policyDocument,    String secret) {    try {        Mac mac = Mac.getInstance("HmacSHA1");        byte[] secretBytes = secret.getBytes("UTF8");        SecretKeySpec signingKey =             new SecretKeySpec(secretBytes, "HmacSHA1");        mac.init(signingKey);        byte[] signedSecretBytes =             mac.doFinal(policyDocument.getBytes("UTF8"));        String signature = Base64.encode(signedSecretBytes);        return signature;    } catch (InvalidKeyException e) {        throw new RuntimeException(e);    } catch (NoSuchAlgorithmException e) {        throw new RuntimeException(e);    } catch (UnsupportedEncodingException e) {        throw new RuntimeException(e);    }}

Java 코드의 보안 해싱과 관련해서는 이 기사에서는 다루고 싶지 않은 장황한 이야기가 일부 있다. 목록 24에 표시되어 있듯이 문제는 해싱이 적절하게 수행되는 방식과 해시가 리턴되기 전에 Base-64로 인코딩되어야 한다는 점에 있다.

이러한 선수조건이 처리되면 익숙한 영역, 즉 파일을 Google Storage에 업로드하고 Google Storage에서 파일을 검색할 세 가지 추상 메소드를 구현하는 작업으로 돌아가게 된다.

Google Storage에 업로드하기

먼저, 목록 25에 있는 코드를 기반으로 하는 GStorageUploadServlet 클래스를 스텁아웃한다.


목록 25. GStorageUploadServlet
import info.johnwheeler.gaestorage.core.GSUtils;import info.johnwheeler.gaestorage.core.Photo;import info.johnwheeler.gaestorage.core.PhotoDao; import java.io.IOException;import java.io.PrintWriter;import java.util.UUID; import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse; @SuppressWarnings("serial")public class GStorageUploadServlet extends AbstractUploadServlet {    private PhotoDao dao = new PhotoDao();}

목록 26에 있는 showForm 메소드는 업로드 양식을 통해 Google Storage에 전달하는 데 필요한 매개변수를 설정한다.


목록 26. Google Storage용 showForm
@Overrideprotected void showForm(HttpServletRequest req, HttpServletResponse resp)     throws ServletException, IOException {    String acl = "public-read";    String secret = getServletConfig().getInitParameter("secret");    String accessKey = getServletConfig().getInitParameter("accessKey");    String endpoint = getServletConfig().getInitParameter("endpoint");    String successActionRedirect = getBaseUrl(req) +         "gstorage?action=display&id=";    String key = UUID.randomUUID().toString();    String policy = GSUtils.createPolicyDocument(acl);    String signature = GSUtils.signPolicyDocument(policy, secret);     req.setAttribute("uploadUrl", endpoint);    req.setAttribute("acl", acl);    req.setAttribute("GoogleAccessId", accessKey);    req.setAttribute("key", key);    req.setAttribute("policy", policy);    req.setAttribute("signature", signature);    req.setAttribute("successActionRedirect", successActionRedirect);     req.getRequestDispatcher("gstorage.jsp").forward(req, resp);}

acl은 public-read로 설정되므로 모든 사람이 업로드된 모든 것을 볼 수 있게 된다. 다음 세 가지 변수, secret, accessKeyendpoint는 Google Storage에 접근하여 인증하는 데 사용된다. 이러한 변수는 web.xml에 선언되어 있는 init-params로부터 가져오므로 세부사항은 샘플 코드를 참조한다. showRecord에 있는 URL로 전달하는 Blobstore와 달리 Google Storage는 경로 재지정을 실행한다는 점을 상기한다. 경로 재지정 URL은 successActionRedirect에 저장된다. successActionRedirect는 목록 27에 있는 헬퍼 메소드에 의존하여 경로 재지정 URL을 생성한다.


목록 27. getBaseUrl()
private static String getBaseUrl(HttpServletRequest req) {    String base = req.getScheme() + "://" + req.getServerName() + ":" +         req.getServerPort() + "/";    return base;}

헬퍼 메소드는 수신 요청을 폴링하여 제어권이 showForm으로 넘어가기 전에 기본 URL을 생성한다. 돌아오게 되면 고유성이 보장되는 문자열인 UUID(Universally Unique Identifier)를 사용하여 키를 작성한다. 다음에는 앞에서 빌드한 유틸리티 클래스를 사용하여 정책과 시그너처를 생성한다. 마지막으로 JSP에 전달하기 전에 JSP에 맞게 요청 속성을 설정한다.

목록 28에는 handleSubmit이 표시되어 있다.


목록 28. Google Storage용 handleSubmit
@Overrideprotected void handleSubmit(HttpServletRequest req, HttpServletResponse     resp) throws ServletException, IOException {    String endpoint = getServletConfig().getInitParameter("endpoint");    String title = req.getParameter("title");    String caption = req.getParameter("caption");    String key = req.getParameter("key");     Photo photo = new Photo(title, caption);    photo.setPhotoPath(endpoint + key);    dao.save(photo);     PrintWriter out = resp.getWriter();    out.println(Long.toString(photo.getId()));    out.close();}

첫 번째 양식이 제출되면 Ajax POST에 의해 handleSubmit에 놓이게 된다. 여기에서는 업로드가 처리되지 않고, Ajax 콜백에서 별도로 처리된다. handleSubmit은 첫 번째 양식을 구문 분석하여 Photo를 생성한 후, 이것을 데이터 저장소에 저장한다. 그 후에는 Photo의 ID를 응답 본문에 쓰는 과정을 통해 이 ID가 Ajax 콜백으로 리턴된다.

콜백 과정에서 업로드 양식이 Google Storage 엔드포인트에 제출된다. Google Storage가 업로드를 처리하면 경로 재지정을 실행하여 목록 29에 있는 showRecord로 돌아가도록 설정된다.


목록 29. Google Storage용 showRecord
@Overrideprotected void showRecord(long id, HttpServletRequest req,     HttpServletResponse resp) throws ServletException, IOException {    Photo photo = dao.findById(id);    String photoPath = photo.getPhotoPath();    resp.sendRedirect(photoPath);}

showRecordPhoto를 검색하여 photoPath로 경로를 재지정한다. photoPath는 Google의 서버에서 호스트되는 이미지를 가리킨다.

결론

이 기사에서 세 가지의 Google 중심적 스토리지 옵션을 살펴보고 이러한 옵션의 장단점을 평가했다. Bigtable은 작업하기 쉬웠지만 파일의 크기가 1MB로 제한되었다. Blobstore의 blob은 최대 크기가 2GB이지만, 웹 서비스에서 처리하기에는 일회성 URL이 문제가 되었다. 마지막 Google Storage for Developers는 가장 강력한 옵션이었다. 사용한 스토리지에 대해서만 비용을 지불하면 되고 하나의 파일에 저장할 수 있는 데이터 양에 대한 제한도 없다. 그러나 Google Storage는 그 라이브러리가 현재 GAE를 지원하지 않기 때문에 다루기에는 가장 복잡한 솔루션이라고 할 수 있다. 또한, 현존하는 데이터 저장소 중에서 브라우저 기반 업로드에 대한 지원이 가장 단순하지 않다.

GAE가 Java 개발자들에게 더 인기 있는 개발 플랫폼이 되면서 Google의 다양한 스토리지 옵션을 이해하는 것이 중요해졌다. 이 기사에서는 Bigtable, Blobstore 및 Google Storage for Developers에 대한 간단한 구현 예제를 살펴보았다. 한 가지 스토리지 옵션을 계속해서 고수하건 아니면 다양한 유스 케이스에 맞게 각 옵션을 사용하건 현재는 GAE에 다량의 데이터를 저장하는 데 필요한 도구가 있어야 한다.

출처 : 한국IBM

제공 : DB포탈사이트 DBguide.net

+ Recent posts