목차

appengine + springboot + kotlin + jpa + cloudsql 연동하기

회사에서 새로 시작하는 API 서버 개발에 아래와 같은 환경을 적용하기로 했다.

각각 놓고보면 다들 유명하고 널리 쓰이는 기술들이지만 얘들을 한꺼번에 적용한 가이드라인이 없어서 내가 직접 가이드를 작성해볼까 한다. A to Z 형식의 가이드는 아니고 셋팅 중 삽질을 크게 했던 부분 위주로 작성해보도록 하겠다.

build.gradle 작성

의존성 관리를 위한 build.gradle 파일이다. 아래의 코드 내용은 spring initializer를 통해서 생성된 프로젝트를 바탕으로 이것저것 수정한 버전이다.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112

buildscript {
	ext {
		kotlinVersion = '1.3.0'
		springBootVersion = '2.0.2.RELEASE'
	}
	repositories {
		jcenter()
		mavenCentral()
	}
	dependencies {
		classpath 'com.google.cloud.tools:appengine-gradle-plugin:1.+'    // latest App Engine Gradle tasks
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
		classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
		classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion"
		classpath 'com.google.gms:google-services:4.1.0'
	}
}

plugins {
	id "org.jetbrains.kotlin.plugin.spring" version '1.3.0'
	id "org.jetbrains.kotlin.plugin.noarg" version '1.3.0'
	id "org.jetbrains.kotlin.plugin.allopen" version '1.3.0'
}

apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'org.springframework.boot'
apply plugin: 'org.jetbrains.kotlin.plugin.spring'
apply plugin: 'kotlin-jpa'
apply plugin: 'kotlin-noarg'

apply plugin: 'kotlin-kapt'
apply plugin: 'idea'

apply plugin: "kotlin-allopen"

apply plugin: 'war'                               // standard Web Archive plugin
apply plugin: 'com.google.cloud.tools.appengine'  // App Engine tasks

noArg {
	annotation("javax.persistence.Entity")
}

allOpen {
	annotation("javax.persistence.Entity")
}

group = 'com.companyname'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
compileKotlin {
	kotlinOptions {
		freeCompilerArgs = ["-Xjsr305=strict"]
		jvmTarget = "1.8"
	}
}
compileTestKotlin {
	kotlinOptions {
		freeCompilerArgs = ["-Xjsr305=strict"]
		jvmTarget = "1.8"
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation('com.google.appengine:appengine:+')
	implementation('com.google.appengine:appengine-api-1.0-sdk:+')
	implementation('com.google.appengine.tools:appengine-gcs-client:0.8')

	// kotlin
	compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
	compile "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"

	// spring boot
	compile "org.springframework.boot:spring-boot-starter-web:${springBootVersion}"
	compile "org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}"
	compile "org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}"

	// jackson
	compile "com.fasterxml.jackson.datatype:jackson-datatype-jdk8"
	compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"
	compile "com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:2.+"
	compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.+"

	// db: hibernate
	compile "org.hibernate:hibernate-core:5.2.7.Final"
	compile "org.hibernate:hibernate-entitymanager:5.2.7.Final"
	compile "org.hibernate:hibernate-java8:5.2.7.Final"
	compile group: 'org.qlrm', name: 'qlrm', version: '2.0.2'

	//db : mysql
	implementation('mysql:mysql-connector-java:5.+')
	implementation('com.google.cloud.sql:mysql-socket-factory-connector-j-8:1.0.11')
	implementation('com.google.cloud.sql:mysql-socket-factory:1.0.11')
	
	compile 'javax.xml.bind:jaxb-api:2.3.0'

	testImplementation('org.springframework.boot:spring-boot-starter-test:2.0.2.RELEASE')
	testImplementation 'com.google.appengine:appengine-testing:1.+'
	testImplementation 'com.google.appengine:appengine-api-stubs:1.+'
	testImplementation 'com.google.appengine:appengine-tools-sdk:1.+'
}

appengine {  // App Engine tasks configuration
    deploy {   // deploy configuration

    }
}

appengine-web.xml 파일 작성

보통 스프링의 각종 프로퍼티들은 resources/application.properties에 작성하는 경우가 많다(요즘에는 yml 파일로 많이들 작성한다고 카더라). 하지만 우리는 App Engine 환경에서 서버를 실행할 것이기 때문에 webapp/WEB-INF/appengine-web.xml에 각종 프로퍼티들을 선언해 줄 것이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
    <version>1</version>
    <threadsafe>true</threadsafe>
    <runtime>java8</runtime>
    <env>standard</env>
    <use-google-connector-j>true</use-google-connector-j>

    <system-properties>
        <property name="spring.datasource.continue-on-error" value="true"/>
        <property name="spring.datasource.initialization-mode" value="always"/>

        <property name="spring.jackson.serialization.fail-on-empty-beans" value="false"/>
        <property name="spring.jpa.hibernate.ddl-auto" value="update"/>
        <property name="spring.jpa.properties.hibernate.hbm2ddl.auto" value="update"/>
        <property name="spring.jpa.properties.hibernate.dialect" value="org.hibernate.dialect.MySQL57Dialect"/>
        <property name="spring.jpa.properties.hibernate.id.new_generator_mappings" value="true"/>
        <property name="spring.jpa.properties.hibernate.format_sql" value="true"/>

        <property name="spring.datasource.url" value="jdbc:mysql://google/<DATABASE_NAME>?cloudSqlInstance=<INSTANCE_CONNECTION_NAME>&socketFactory=com.google.cloud.sql.mysql.SocketFactory&user=<MYSQL_USER_NAME>&password=<MYSQL_USER_PASSWORD>&useSSL=false"/>
    </system-properties>
</appengine-web-app>

위 내용들을 application.properties에 형식을 맞춰서 작성해도 동작이 되긴 하는듯 하다. 하지만 구글의 앱엔진 가이드에는 appengine-web.xml에 작성하는걸 기준으로 설명하고 있으니 일단은 위와 같이 작성해두도록 하겠다.

위 내용에서 내가 특히 헤맸던 설정이 있다.

1
<property name="spring.jpa.properties.hibernate.dialect" value="org.hibernate.dialect.MySQL57Dialect"/>

이 옵션인데 현재 내가 최초에 적용했던 값은 org.hibernate.dialect.MySQL5Dialect였다(7이 빠졌다). 하이버네이트를 MySQL5 버전에 연결하기 위한 옵션값인데 로컬서버에 설치된 MYSQL5.7에 연결했을때는 아무 문제도 없었다. 근데 앱엔진에 배포하고 테스트를 해보니 하이버네이트가 DB에 연결을 못하고 있었다. 그래서 열심히 구글링을 해본 결과 저 dialect 값이 문제였다. 이유는 모르겠다. 아마 앱엔진에서 CloudSQL에 접근할때 사용되는 프록시와 연관이 있을지도..? 아무튼 MySQL57Dialect라고 정확히 명시를 해줘야 앱엔진 환경에서도 DB에 문제없이 연결이 된다.

main class 작성하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

@SpringBootApplication
class Application : SpringBootServletInitializer(){
    @Bean
    fun hibernate5Module(): Module { //for jpa lazy loading
        return Hibernate5Module()
    }
}

fun main(args: Array<String>) {
    SpringApplication.run(Application::class.java, *args)
}

여기까지 작성한다면 API 개발을 위한 기본적인 셋팅이 완료된 것이다. 이제 @Controller, @Service 등등을 작성하면서 API를 완성해나가면 된다.

참고 : Kotlin에서 JPA 사용할 때 주의할 점