java-school logo

구글 앱 엔진 시작하기

개발 환경

자바와 메이븐은 필수다.
여기선 IDE로 이클립스를 선택했으나 다른 IDE를 선택해도 상관없다.

1. 자바 설치

자바 8 최신 버전을 설치한다.
자바 9로 구글 앱 엔진 프로젝트를 개발할 수 없다.
자바 8 설치는 JDK 설치를 참조한다.

2. 메이븐 설치

메이븐 최신 버전을 설치한다.
메이븐 설치는 메이븐 설치를 참조한다.

3. 이클립스 설치

www.eclipse.org에서 이클립스 인스톨러 압축 파일을 내려받는다.
압축을 풀고 생성된 디렉터리에서 eclipse-inst로 시작하는 이클립스 인스톨러를 실행한다.
목록에서 Java EE Developers를 선택해 설치한다.

구글 앱 엔진 방명록(guestbook) 아키타입 생성

자신이 지정한 워크스페이스에서 다음 명령을 실행한다.

mvn archetype:generate -Dappengine-version=1.9.57 -Dfilter=com.google.appengine.archetype:

C:\ Command Prompt
C:\Lab>mvn archetype:generate -Dappengine-version=1.9.57 -Dfilter=com.google.appengine.archetype:
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] >>> maven-archetype-plugin:2.4:generate (default-cli) > generate-sources @ standalone-pom >>>
[INFO] 
[INFO] <<< maven-archetype-plugin:2.4:generate (default-cli) < generate-sources @ standalone-pom <<<
[INFO] 
[INFO] --- maven-archetype-plugin:2.4:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Interactive mode
[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
1: remote -> com.google.appengine.archetypes:appengine-flexible-archetype (A basic Java application with Google App Engine Flexible.)
2: remote -> com.google.appengine.archetypes:appengine-skeleton-archetype (A skeleton application with Google App Engine)
3: remote -> com.google.appengine.archetypes:appengine-standard-archetype (A basic Java application with Google App Engine Standard)
4: remote -> com.google.appengine.archetypes:endpoints-skeleton-archetype (A skeleton project using Cloud Endpoints Frameworks with Google App Engine Standard)
5: remote -> com.google.appengine.archetypes:guestbook-archetype (A guestbook application with Google App Engine)
6: remote -> com.google.appengine.archetypes:hello-endpoints-archetype (A simple starter application using Cloud Endpoints Frameworks with Google App Engine Standard)
7: remote -> com.google.appengine.archetypes:skeleton-archetype (-)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): :  5 
Choose com.google.appengine.archetypes:guestbook-archetype version: 
1: 1.7.4
2: 1.7.4.1
3: 1.7.5
4: 1.7.6
5: 1.7.7
6: 1.8.4
7: 2.0.0-1.9.10
8: 3.0.0-1.9.20
9: 3.0.1-1.9.21
10: 3.0.1-1.9.25
11: 3.0.2-1.9.38
12: 3.1.0-1.9.42
13: 4.0.0
14: 4.0.1
Choose a number: 14:  12   
Define value for property 'groupId': : net.java_school.guestbook
Define value for property 'artifactId': : guestbook
Define value for property 'version':  1.0-SNAPSHOT: :  1 
Define value for property 'package':  net.java_school.guestbook: : ↵
[INFO] Using property: appengine-version = 1.9.57
[INFO] Using property: application-id = your-app-id
Confirm properties configuration:
groupId: net.java_school.guestbook
artifactId: guestbook
version: 1.0-SNAPSHOT
package: net.java_school.guestbook
appengine-version: 1.9.57
application-id: your-app-id
 Y: : ↵
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: guestbook-archetype:3.1.0-1.9.42
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: net.java_school.guestbook
[INFO] Parameter: artifactId, Value: guestbook
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: net.java_school.guestbook
[INFO] Parameter: packageInPathFormat, Value: net/java_school/guestbook
[INFO] Parameter: package, Value: net.java_school.guestbook
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: groupId, Value: net.java_school.guestbook
[INFO] Parameter: appengine-version, Value: 1.9.57
[INFO] Parameter: application-id, Value: your-app-id
[INFO] Parameter: artifactId, Value: guestbook
[INFO] project created from Archetype in dir: C:\Lab\guestbook
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 42.996 s
[INFO] Finished at: 2017-06-05T19:53:41+09:00
[INFO] Final Memory: 15M/180M
[INFO] ------------------------------------------------------------------------

빌드가 완료되면 artifactId 값과 같은 이름의 서브 폴더가 생긴다.
artifactId 값을 guestbook으로 주었으므로 guestbook 서브 폴더가 생긴다.
guestbook 폴더로 이동하여 다음 명령을 실행한다.

C:\ Command Prompt

C:\Lab>cd guestbook
C:\Lab\guestbook>mvn clean install

로컬 테스트

로컬 테스트를 위한 명령어는 mvn appengine:devserver 이다.
실행하면 다음 에러를 만나게 된다.

C:\ Command Prompt
C:\Lab\guestbook>mvn appengine:devserver
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.455 s
[INFO] Finished at: 2017-06-05T20:02:59+09:00
[INFO] Final Memory: 7M/106M
[INFO] ------------------------------------------------------------------------
[ERROR] Could not find goal 'devserver' in plugin com.google.cloud.tools:appengine-maven-plugin:1.0.0 among available goals deploy, help, run, stage, start, stop -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoNotFoundException

에러를 피하기 위한 다음 작업을 진행한다.
pom.xml 파일을 열고 아래 강조된 부분을 삭제한다.

<properties>
    <app.id>your-app-id</app.id>
    <app.version>1</app.version>
    <appengine.version>1.9.57</appengine.version><!-- 삭제한다 -->
    <appengine.maven.plugin.version>1.0.0</appengine.maven.plugin.version><!-- 삭제한다 -->

${appengine.version}을 모두 1.9.57로 수정한다.

appengine-maven-plugin 플러그인을 아래를 참조해 수정한다.

수정 전
<plugin>
    <groupId>com.google.cloud.tools</groupId>
    <artifactId>appengine-maven-plugin</artifactId>
    <version>${appengine.maven.plugin.version}</version>
    <configuration>
    </configuration>
</plugin>
수정 후
<plugin>
    <groupId>com.google.appengine</groupId>
    <artifactId>appengine-maven-plugin</artifactId>
    <version>1.9.57</version>
    <configuration>
    </configuration>
</plugin>

마지막으로 maven.compiler.source와 maven.compiler.target 값을 모두 1.8로 수정한다.
아래는 수정을 마무리한 pom.xml 파일이다.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
		xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <packaging>war</packaging>
    <version>1</version>

    <groupId>net.java_school.guestbook</groupId>
    <artifactId>guestbook</artifactId>

    <properties>
        <app.id>your-app-id</app.id>
        <app.version>1</app.version>

        <objectify.version>5.1.13</objectify.version>
        <guava.version>18.0</guava.version>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
        <archiveClasses>true</archiveClasses>
    </properties>

    <prerequisites>
        <maven>3.5.0</maven><!--3.3.9도 테스트 통과 -->
    </prerequisites>

    <dependencies>
        <!-- Compile/runtime dependencies -->
        <dependency>
            <groupId>com.google.appengine</groupId>
            <artifactId>appengine-api-1.0-sdk</artifactId>
            <version>1.9.57</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

<!-- [START Objectify_Dependencies] -->
        <dependency>
          <groupId>com.google.guava</groupId>
          <artifactId>guava</artifactId>
          <version>${guava.version}</version>
        </dependency>
        <dependency>
          <groupId>com.googlecode.objectify</groupId>
          <artifactId>objectify</artifactId>
          <version>${objectify.version}</version>
        </dependency>
<!-- [END Objectify_Dependencies] -->

        <!-- Test Dependencies -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-all</artifactId>
            <version>2.0.2-beta</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.google.appengine</groupId>
            <artifactId>appengine-testing</artifactId>
            <version>1.9.57</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.google.appengine</groupId>
            <artifactId>appengine-api-stubs</artifactId>
            <version>1.9.57</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <!-- for hot reload of the web application-->
        <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/classes</outputDirectory>
        <plugins>
          <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>versions-maven-plugin</artifactId>
            <version>2.3</version>
            <executions>
              <execution>
                <phase>compile</phase>
                <goals>
                  <goal>display-dependency-updates</goal>
                  <goal>display-plugin-updates</goal>
                </goals>
              </execution>
            </executions>
          </plugin>

          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>2.6</version> <!-- required for Eclipse Mars -->
            <configuration>
              <archiveClasses>true</archiveClasses>
              <webResources>
                <!-- in order to interpolate version from pom into appengine-web.xml -->
                <resource>
                  <directory>${basedir}/src/main/webapp/WEB-INF</directory>
                  <filtering>true</filtering>
                  <targetPath>WEB-INF</targetPath>
                </resource>
              </webResources>
            </configuration>
          </plugin>
          <plugin>
            <groupId>com.google.appengine</groupId>
            <artifactId>appengine-maven-plugin</artifactId>
            <version>1.9.57</version>
            <configuration>
            </configuration>
          </plugin>
        </plugins>
    </build>
</project>

다음은 프로젝트가 자바 8 환경을 사용할 수 있게 하는 설정이다.
appengine-web.xml에 다음을 추가한다.

<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
    <application>${app.id}</application>
    <version>${app.version}</version>
    <threadsafe>true</threadsafe>
    <runtime>java8</runtime>

    <system-properties>
        <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
    </system-properties>
</appengine-web-app>

mvn appengine:devserver 실행한다.
Dev App Server is now running이 보이면, 웹 브라우저로 http://localhost:8080을 방문하여 테스트한다.
테스트가 끝나면 Ctrl + C로 종료한다.

서비스 신청

https://cloud.google.com에서 비자 또는 마스터 신용카드나 체크카드로 서비스를 신청할 수 있다.
구글 계정에 로그인해야 서비스 신청을 할 수 있다.

서비스 신청 후 구글은 1년간 사용할 수 있는 300달러 바우처를 제공한다.
1년간 300달러에 해당하는 서비스를 체험하는 것은 무료이다.
300달러를 소진하거나 1년이 지나면 구글은 서비스를 계속 사용할지를 묻는다.
서비스를 계속 사용하겠다는 확답을 주면, 구글은 서비스 비용을 0으로 초기화하고 이후 발생한 서비스 비용은 신청시 등록한 카드로 자동 결재한다.
사용자는 바우처를 사용하는 동안 자신에 맞는 서비스를 정하고 예산을 수립할 수 있도록 노력해야 한다.

프로젝트 생성

구글 앱 엔진의 자바 환경은 플렉시블 환경(자바 8)과 표준 환경(자바 7)으로 나뉜다.
이 글은 표준 환경을 기준으로 작성했다.
표준 환경이라도 자바 8을 사용할 수 있다.

신청을 완료하고 다시 https://cloud.google.com을 방문하면 GO TO CONSOLE 버튼이 보일 것이다.
이 버튼을 클릭하면 웹 콘솔 화면으로 이동하게 된다.

웹 콘솔 화면에서 처음으로 해야 할 일은 프로젝트를 생성하는 것이다.
왼쪽 상단의 Google Cloud Platform 옆에서 프로젝트를 생성할 수 있는 메뉴를 찾아 실행한다.
새 프로젝트 이름, 아이디 프로젝트 이름을 입력하면 프로젝트 아이디는 프로젝트 이름-숫자 형태로 만들어진다.
프로젝트 이름을 독특하게 짓는다면 프로젝트 이름과 프로젝트 아이디가 같을 수 있다.
기억해야 할 것은 프로젝트 아이디이다.

서버 테스트

pom.xml을 열고 <app.id>your-app-id</app.id>에서 your-app-id를 자신의 프로젝트 아이디로 수정한다.

<properties>
    <app.id>프로젝트 아이디</app.id>
    <app.version>1</app.version>

명령 프롬프트에서 다음 명령을 실행한다.

C:\ Command Prompt
C:\Lab\guestbook>mvn appengine:update

다음과 같은 에러가 발생할 수 있다.

404 Not Found
This application does not exist(project_id=u'')...

위 에러는 다음 작업을 통해 바로 잡을 수 있다.
구글 클라우드 웹 콘솔에 방문한 후, 왼쪽 메뉴 항목에서 App Engine를 선택한다.
아래 그림을 참조하여 프로그래밍 언어와 지역을 선택하는 것까지 진행한다.

웹 엔진 시작하기 웹 엔진 언어 선택 웹 엔진 지역 선택 웹 엔진 초기화

지역을 선택한 다음의 과정은 대화형 가이드에 따라 앱을 배포하는 과정이다.
방명록 예제를 이미 작성한 우리는 이 과정을 생략한다.

mvn appengine:update를 다시 실행한다.
처음으로 앱을 올리는 경우, 웹 브라우저 실행되고 웹 브라우저의 내용을 명령 프롬프트에 복사하는 절차가 있다.
Copy this code

빌드가 성공하면 https://프로젝트 아이디.appspot.com을 방문한다.
로컬 테스트와 달리 다음과 같은 서버 에러를 만날 것이다.
First Server Error 하지만 몇 분 후 다시 접속하면 서버 에러가 사라지고 방명록 화면을 볼 수 있다.
First Remote Test

처음에 보았던 에러의 원인은 데이터스토어 인덱스가 생성되기 전에 방문했기 때문이다.
데이터스토어 인덱스는 생성되는 데 몇 분 정도 소요된다.
만약 인덱스가 생성되지 않아 500 Server Error가 계속된다면(그럴 경우는 거의 없지만), WEB-INF/ 디렉터리에 datastore-indexes.xml 파일을 생성한 후, mvn appengine:update_indexes 명령으로 인덱스를 생성할 수 있다.
인덱스가 없어서 발생하는 서버 에러의 경우, 구글 클라우드는 로그 메시지를 통해 자세한 내용을 알려준다.
log message
방명록에 필요한 인덱스를 로그 메시지가 권고한 내용대로 datastore-indexes.xml 파일에 복사한다.

datastore-indexes.xml
<datastore-indexes autoGenerate="true">
	<datastore-index kind="Greeting" ancestor="true" source="manual">
		<property name="date" direction="desc" />
	</datastore-index>
</datastore-indexes>

datastore-indexex.xml 파일을 사용해 인덱스를 생성하려면 다음 명령을 실행한다.

mvn appengine:update_indexes

데이터스토어 인덱스는 구글 클라우드 콘솔에서 확인할 수 있다.
datatore indexes

Git 사용하기

구글 클라우드는 개인 저장소를 제공한다.
개인 저장소를 사용하기 위해선 다음 과정이 필요하다.
(순서대로 해야한다)

1. Git 설치

https://git-scm.com에서 최신 바이너리 파일을 다운로드하고 압축을 푼다.
압축을 풀고 생성된 디렉터리를 적당한 곳으로 옮긴 후 Git의 bin 디렉터리의 경로를 PATH 환경변수에 추가한다.

2. Cloud SDK 설치

https://cloud.google.com/sdk/downloads에서 다운로드하고 압축을 푼다.
압축을 풀고 생성된 디렉터리를 적당한 곳으로 옮긴다.

리눅스와 맥 OS X에서 환경 변수 PATH에 Cloud SDK의 bin 경로를 추가하려면 다음을 실행한다.

./google-cloud-sdk/install.sh

윈도에서 환경 변수 PATH에 Cloud SDK의 bin 경로를 추가하려면 다음을 실행한다.

.\google-cloud-sdk\install.bat

3. Git 저장소 생성

gcloud source repos create 개인 저장소 이름 명령으로 구글 클라우드에 개인 저장소를 생성한다.

gcloud source repos create firstGaePjt

firstGaePjt 저장소를 구글 클라우드에 생성했다.

gcloud source repos clone 로컬 저장소 이름 명령으로 로컬 저장소를 생성한다.

gcloud source repos clone firstGaePjt

firstGaePjt 로컬 저장소를 개발 시스템에 생성했다.
원격 저장소에 아무런 내용이 없으므로 warning: 리모트 HEAD가 없는 레퍼런스를 참고하므로, 체크아웃할 수 없습니다.라는 메시지가 보인다.
명령을 실행한 디렉터리에 firstGaePjt라는 디렉터리가 생성된다. 이 곳이 소스가 위치할 디렉터리다.
C:\Lab\guestbook의 모든 파일을 firstGaePjt 디렉터리에 복사한다.

이후는 git 사용법과 같다.

git add . -A
git commit -m "First Commit"
git push origin master
참고