Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Play 2.1에 Slick 연동하기

최근 scala 학습을 위해서 개인 프로젝트에 scala를 사용하면서 play를 사용하기로 했다. play는 1.0때 사용하고 2.0은 거의 안써봤는데 scala에서는 웹프레임워크의 선택권도 많지 않고 Play 2.0이 Typesafe 스택이기도 해서 선택했다. Play 2.0에 대한 생각도 여러가지로 들긴 하는데 이건 좀 더 써보고 하기로 하고 Play는 데이터접근 계층에 Anorm을 사용하는데 Typesafe 스택이기도 하면서 요즘 좀더 대세인 Slick을 사용하기로 했다. Slick에 대한 자세한 얘기는 내용이 많아 다음으로 미루고 Play 2.1에서 Slick을 연동하는 방법을 을 살펴보자.

Play Framework와 Slick 연동

Slick을 통합하면서 원하는 봐는 간단했다. 데이터접근 계층에 Slick을 사용하고 데이터접근 계층만 별도로 테스트케이스를 작성할 수 있어야 한다. 그리고 어플리케이션에서는 사용하는 RDBMS를 사용하고 테스트케이스에서는 메모리디비등으로 교체해서 테스트할 수 있어야 한다. 당연한 요구사항이지만 문서도 많지 않고 관련글이 많지 않아서 이 설정만 하는데도 꽤 어려웠다. 검색해보면 Play2에 Slick을 통합하는 몇가지 플러그인이 있기는 한데 Play 2.0이 자세히 파악안된 상태라서 이 문제를 모듈로 해결하는게 맞는 접근인지 약간 애매했고 어느 모듈이 잘 유지가 되는지 잘 몰랐기에 직접 구현하는 방법을 선택했다. 나중에 홍덕기님이 알려주셔서 보니 play-slick 모듈이 괜찮아 보이고 개발자가 Typesafe직원이므로 유지도 잘 될듯 하다.

검색을 해보면 Slick과 관련한 글이 많지 않은데(Slick은 정말 많지 않으므로 ScalaQuery로 검색해 보는 것도 도움이 된다.) 아주 기본적인 사용법을 설명할 글 정도밖에 없다. 하지만 어플리케이션을 작성한다고 생각하면 당연히 모델클래스가 다수 존재하게 되므로 한 파일에서 상단에서 디비연결을 하고 하단에서 Slick을 사용하는 방식으로는 코드를 작성하지 않는다. 당연히 디비를 관리하는 클래스나 객체가 별도로 있고 여기서 커넥션을 받아서 각 모델클래스가 사용하는 것이 자연스럽고 테스트코드를 작성할 때는 이 디비 관리 클래스의 커넥션 정보를 바꿔서 테스트할 수 있어야 하는데 대다수의 예제는 그냥 하나의 파일에서 디비설정하고 Slick 사용법을 알려주는 정도가 전부였다.

그나마 찾은 글이 Play! Framework 2 (2.1) Scala with Slick made easy (With example)라는 글이었고 이 글은 Github에 예제도 올라와 있어서 참고하기에 괜찮았다. 이 예제는 Cake 패턴을 이용하고 있고 나에겐 약간 어색하게 느껴지는 Dal(Data Access Layer)라는 클래스를 사용해서 디비연결을 관리하고 있었고 테스트하는 방법까지 깔끔하게 설명하고 있었다. Play 2.0도 많이 모르고 Cake패턴도 잘 모르는지라 소스구조를 자세히 이해할 수 있지만 잘 돌아갔는데 막상 실제 개발을 하려고 보니 첫번재 테스트케이스는 잘 돌아갔지만 두번째 테스트케이스를 작성하니 세션이 종료되었다는 오류가 발생했다.(예제를 위한 예제일 뿐인가. ㅠㅠ) 그리고 이 예제에서는 테스트케이스마다 FakeApplication(additionalConfiguration = inMemoryDatabase())를 사용하고 있는데(이는 Play가 제공하는 기능인듯하다.) 이 FakeApplication이 내부에서 어떻게 동작하든지 상관없이 데이터접근 계층을 테스트하는데 어플리케이션을 실행한다는 게 나로써는 좀 이상해 보였다.

그러다가 Getting Started with Play 2.1 , Scala 2.10 and Slick 0.11.1라는 글을 찾아서 여기에서 제시하는 방법으로 변경했고 지금은 잘 동작하고 있다.

Slick 의존성 추가

Play 자체를 설명하기 위한 글은 아니므로 지난번에 소개한 Activatorhello-play로 Play 프로젝트를 구성한다. 다음은 프로젝트의 sbt설정인 project/Build.scala파일이다.

import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {

  val appName         = "hello-play"
  val appVersion      = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Select Play modules
    jdbc,      // The JDBC connection pool and the play.api.db API
    //anorm,     // Scala RDBMS Library
    //javaJdbc,  // Java database API
    //javaEbean, // Java Ebean plugin
    //javaJpa,   // Java JPA plugin
    //filters,   // A set of built-in filters
    javaCore,  // The core Java API

    // WebJars pull in client-side web libraries
    "org.webjars" % "webjars-play" % "2.1.0",
    "org.webjars" % "bootstrap" % "2.3.1",

    // for slick
    "com.typesafe.slick" %% "slick" % "1.0.0",
    "postgresql" % "postgresql" % "9.1-901-1.jdbc4",
    "org.scalatest" % "scalatest_2.10" % "1.9.1" % "test"

    // Add your own project dependencies in the form:
    // "group" % "artifact" % "version"
  )

  val main = play.Project(appName, appVersion, appDependencies).settings(
    scalaVersion := "2.10.1"
    // Add your own project settings here
  )

}

JDBC를 사용해야 하므로 의존성에서 jdbc의 주석을 풀어주고(기본으로는 주석처리가 되어 있다.) for slick부분에 의존성을 추가한다. slick을 추가하고 사용할 RDBMS의 jdbc 드라이버를 추가했다.(여기서는 PostgreSQL), 그리고 Play의 기본 테스트프레임워크인 spec2 대신 ScalaTest를 사용하기 위해서 ScalaTest도 추가했다.

모델 작성

model 패키지를 추가하고 다음과 같은 User.scala를 작성한다.

package models

import slick.driver.PostgresDriver.simple._

case class User(
  id: String,
  name: String,
  email: String
)

object Users extends Table[User]("users") {
  def id = column[String]("id")
  def name = column[String]("name")
  def email = column[String]("email")
  def * = id ~ name ~ email <> (User, User.unapply _)
}

이는 일반적인 Slick의 모델 클래스이고 User 모델은 id, name, email 컬럼을 가지고 디비에서는 users라는 테이블명을 가진다. * 메서드는 모델 객체를 매핑해주는 함수인데 여기서는 크게 신경쓰지 않아도 된다. 그리고 PostgreSQL을 사용할 것이므로 Slick의 PostgresDriver를 임포트했다. 이제 app 디렉토리 아래에 Global이라는 파일을 만들어서 다음의 내용을 추가한다.

import play.api.db.DB
import play.api.GlobalSettings

import slick.driver.PostgresDriver.simple._
import Database.threadLocalSession

import play.api.Application
import play.api.Play.current
import models.Users

object Global extends GlobalSettings {

  override def onStart(app: Application) {

    lazy val database = Database.forDataSource(DB.getDataSource())

    database .withSession {
      Users.ddl.create
    }
  }
}

Play 2.0을 이제 막 접해서 자세히는 모르지만 Global은 플레이의 전역 설정을 하는 부분으로 보이고 여기서 어플리케이션을 시작할 때 데이터베이스를 설정하게 된다. 그래서 데이터베이스 연결을 위한 객체를 생성하고 최초 연결시 DDL, 즉 테이블을 정의에 따라 생성한다. 앞에서 User 모델에 대한 객체만 생성하였지만 Slick이 자동으로 DDL 문을 생성하고 여기서 디비에 테이블을 생성한다.

테스트 작성

테스트를 작성하기 전에 앞에서 작성했던 models/User.scala파일의 Users 객체에 다음과 같이 addfindAll 메서드를 추가한다.

object Users extends Table[User]("users") {
  def id = column[String]("id")
  def name = column[String]("name")
  def email = column[String]("email")
  def * = id ~ name ~ email <> (User, User.unapply _)

  def add(user: User)(implicit session: Session) = {
    Users.insert(user)
  }

  def findAll()(implicit session: Session) = {
    (for {
      user <- Users
    } yield user).list

  }
}

슬릭은 아직 파악중이라 자세히 설명하기는 어렵지만 위와 같이 Session이 필요하므로 함수마다 implicit 파라미터가 필요하다. add 함수는 User 모델의 객체를 받아서 users테이블에 추가하고 findAll함수에서는 users테이블의 데이터를 모두 조회해서 List로 변환해서 반환한다. Slick 자체를 설명하는 글은 아니지만 위처럼 디비를 조회할 때 스칼라의 컬렉션을 다루듯이 for comprehension같은 문법으로 SELECT 문을 작성한다.(참고로 여기서 사용한 방법은 Slick의 lifted embedding을 사용한 방법이다.)

테스트 폴더에 models 패키지를 추가하고 UserSpec.scala 파일을 다음의 내용으로 추가한다.

package models

import org.scalatest.FunSpec
import org.scalatest.matchers.ShouldMatchers
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession

class UserSpec extends FunSpec with ShouldMatchers {

  describe("example") {
    it("사용자를 추가하고 조회한다") {
      Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver") withSession {
        // given
        Users.ddl.create
        Users.add(new User("outsider", "Outsider", "example@gamil.com"))
        // when
        val results = Users.findAll
        // then
        results.size should equal(1)
      }
    }
  }
}

앞에서 얘기했듯이 ScalaTest로 작성한 유닛테스트고 사용자를 추가한뒤에 다시 가져오는 테스트케이스이다. 그리고 코드에서 보듯이 어플리케이션은 PostgreSQL을 사용하지만 유닛테스트에서는 H2 데이터베이스를 사용하고 있다.

SBT에서 실행한 테스트가 성공한 화면

SBT에서 테스트를 실행하면 위와 같이 정상적으로 실행되는 것을 볼 수 있다. 다만 hello-play 템플릿을 생성하면 기본으로 생기는 통합테스트가 몇가지 있는데 어플리케이션을 실행하는 다른 테스트와 충돌이 일어나므로 실행이 제대로 안되면 다른 어플리케이션 테스트는 제거한 뒤에 모델을 테스트해야 한다.(이 문제에 대한 해결은 나중에...)

Play 2.1의 디비설정

Play 어플리케이션에서 데이터베이스를 사용하려면 conf/application.conf에서 데이터베이스 설정부분에 주석을 풀고 다음과 같이 수정해 주어야 한다.

db.default.driver=org.postgresql.Driver
db.default.url="jdbc:postgresql://localhost:5432/example"
db.default.user=sa
db.default.password="PASSWORD"

여기서는 PostgreSQL을 사용했지만 다른 데이터베이스를 사용한다면 그에 맞게 설정해 주어야 한다.(물론 각 모델에서 임포트하는 드라이버도 수정해 주어야 한다.)

2013/06/06 23:59 2013/06/06 23:59