몽고 DB의 저장소를 CRUD를 API화 하는 방법과
그것을 Swagger를통해 문서자동화를 하여 웹 인터페이스에서 NoSQL의 저장소를 CRUD를 해보는것을 실습해보겠습니다.
src :
Mongo DB 설치
직접 설치하거나, 도커를 이용한 방법 두가지가 있습니다.
Mongo On Ubuntu
https://docs.mongodb.com/v3.2/tutorial/install-mongodb-on-ubuntu/
Docker with Kinematic
https://docs.docker.com/kitematic/
자신의 개발 환경에 도커+Kinematic이 설치가 되어있다고 하면 설치가 각각 다른 오픈 프로젝트 서비스를 원클릭으로 이용할수가 있다.
이것은 설치에대한 고통을 줄이고 여러가지 DB에대한 학습 시간을 비약적으로 단축시킬수가 있습니다.
build.sbt
메이븐의 라이브러리 디펜던시에 해당하는 부분으로, SBT는 더 심플하게
의존 라이브러리를 표현합니다.
val reactiveMongoVer = "0.16.0-play26" // mongo + swagger libraryDependencies ++= Seq( "org.reactivemongo" %% "play2-reactivemongo" % reactiveMongoVer, "io.swagger" %% "swagger-play2" % "1.6.0", "org.webjars" % "swagger-ui" % "3.2.2", )
application.conf
spring boot과 비교하자면, application.propertis에 해당하는 설정화부분으로
몽고 db와 swagger사용을 위해 아래와같은 설정이 필요합니다.
play.modules { # By default, Play will load any class called Module that is defined # in the root package (the "app" directory), or you can define them # explicitly below. # If there are any built-in modules that you want to enable, you can list them here. #enabled += my.application.Module enabled += "play.modules.reactivemongo.ReactiveMongoModule" enabled += "play.modules.swagger.SwaggerModule" # If there are any built-in modules that you want to disable, you can list them here. #disabled += "" } swagger.api.uri = "http://localhost:9000" mongodb.uri = "mongodb://localhost:32774/todo-app"
routes
playframework의 특징중하나로, 웹주소에 관련한 endpoin설정이 코드와 분리되어 기본으로 설정화가 되어있습니다.
# Routes # This file defines all application routes (Higher priority routes first) # ~~~~ GET /todos controllers.TodoController.getAllTodos GET /todos/:id controllers.TodoController.getTodo(id: reactivemongo.bson.BSONObjectID) + nocsrf POST /todos controllers.TodoController.createTodo + nocsrf PATCH /todos/:id controllers.TodoController.updateTodo(id: reactivemongo.bson.BSONObjectID) + nocsrf DELETE /todos/:id controllers.TodoController.deleteTodo(id: reactivemongo.bson.BSONObjectID) # Swagger docs GET / controllers.ApiDocsController.redirectToDocs GET /swagger.json controllers.ApiHelpController.getResources GET /api-docs controllers.ApiDocsController.redirectToDocs
Todo Repository
Todo Model을 CRUD하는 Repository를 아래와같이 구현합니다.
case class Todo(_id: Option[BSONObjectID], title: String, completed: Option[Boolean]) object JsonFormats{ import play.api.libs.json._ implicit val todoFormat: OFormat[Todo] = Json.format[Todo] } class TodoRepository @Inject()(implicit ec: ExecutionContext, reactiveMongoApi: ReactiveMongoApi){ import JsonFormats._ def todosCollection: Future[JSONCollection] = reactiveMongoApi.database.map(_.collection("todos")) def getAll(limit: Int = 100): Future[Seq[Todo]] = todosCollection.flatMap(_.find( selector = Json.obj(/* Using Play JSON */), projection = Option.empty[JsObject]) .cursor[Todo](ReadPreference.primary) .collect[Seq](limit, Cursor.FailOnError[Seq[Todo]]()) ) def getTodo(id: BSONObjectID): Future[Option[Todo]] = todosCollection.flatMap(_.find( selector = BSONDocument("_id" -> id), projection = Option.empty[BSONDocument]) .one[Todo]) def addTodo(todo: Todo): Future[WriteResult] = todosCollection.flatMap(_.insert(todo)) def updateTodo(id: BSONObjectID, todo: Todo): Future[Option[Todo]] = { val selector = BSONDocument("_id" -> id) val updateModifier = BSONDocument( f"$$set" -> BSONDocument( "title" -> todo.title, "completed" -> todo.completed) ) todosCollection.flatMap( _.findAndUpdate(selector, updateModifier, fetchNewObject = true) .map(_.result[Todo]) ) } def deleteTodo(id: BSONObjectID): Future[Option[Todo]] = { val selector = BSONDocument("_id" -> id) todosCollection.flatMap(_.findAndRemove(selector).map(_.result[Todo])) } }
ApiDocsController
문서화 페이지 컨트롤러는 이 코드면 충분하며, 나머지는 실제 서비스 컨트롤러에
어노테이션이 셋팅되어 API문서 자동화가 됩니다.
class ApiDocsController @Inject()(cc: ControllerComponents, configuration: Configuration) extends AbstractController(cc) { def redirectToDocs = Action { val basePath = configuration.underlying.getString("swagger.api.uri") Redirect( url = "/assets/lib/swagger-ui/index.html", queryString = Map("url" -> Seq(s"$basePath/swagger.json")) ) } }
TodoController
TodoRepository를 이용하는 Controller코드를 작성합니다.
API어노테이션으로 최종 Endpoint및 작동방식들이 결정되면서 Swagger의 문서화와 자동 연동이됩니다.
@Api(value = "/todos") class TodoController @Inject()(cc: ControllerComponents, todoRepo: TodoRepository) extends AbstractController(cc) { @ApiOperation( value = "Find all Todos", response = classOf[Todo], responseContainer = "List" ) def getAllTodos = Action.async { todoRepo.getAll().map { todos => Ok(Json.toJson(todos)) } } @ApiOperation( value = "Get a Todo", response = classOf[Todo] ) @ApiResponses(Array( new ApiResponse(code = 404, message = "Todo not found") ) ) def getTodo(@ApiParam(value = "The id of the Todo to fetch") todoId: BSONObjectID) = Action.async { todoRepo.getTodo(todoId).map { maybeTodo => maybeTodo.map { todo => Ok(Json.toJson(todo)) }.getOrElse(NotFound) } } @ApiOperation( value = "Add a new Todo to the list", response = classOf[Void], code = 201 ) @ApiResponses(Array( new ApiResponse(code = 400, message = "Invalid Todo format") ) ) @ApiImplicitParams(Array( new ApiImplicitParam(value = "The Todo to add, in Json Format", required = true, dataType = "models.Todo", paramType = "body") ) ) def createTodo() = Action.async(parse.json) { _.body.validate[Todo].map { todo => todoRepo.addTodo(todo).map { _ => Created } }.getOrElse(Future.successful(BadRequest("Invalid Todo format"))) } @ApiOperation( value = "Update a Todo", response = classOf[Todo] ) @ApiResponses(Array( new ApiResponse(code = 400, message = "Invalid Todo format") ) ) @ApiImplicitParams(Array( new ApiImplicitParam(value = "The updated Todo, in Json Format", required = true, dataType = "models.Todo", paramType = "body") ) ) def updateTodo(@ApiParam(value = "The id of the Todo to update") todoId: BSONObjectID) = Action.async(parse.json){ req => req.body.validate[Todo].map { todo => todoRepo.updateTodo(todoId, todo).map { case Some(todo) => Ok(Json.toJson(todo)) case _ => NotFound } }.getOrElse(Future.successful(BadRequest("Invalid Json"))) } @ApiOperation( value = "Delete a Todo", response = classOf[Todo] ) def deleteTodo(@ApiParam(value = "The id of the Todo to delete") todoId: BSONObjectID) = Action.async { req => todoRepo.deleteTodo(todoId).map { case Some(todo) => Ok(Json.toJson(todo)) case _ => NotFound } } }
Swagger을 이용한 저장소 이용 Test
NOSQL 기반의 DB에 우리가 원하는 모델을 정의하고
API화하여 웹인터페이스를 활용하여 테스트할수 있는 방법을 이용할수가 있습니다.
UnitTest for MongoDB
유닛테스트는 덤으로 알아보자..
PlayWithMongoSpec
trait PlayWithMongoSpec extends PlaySpec with GuiceOneAppPerSuite { override def fakeApplication = new GuiceApplicationBuilder() .configure( "mongodb.uri" -> "mongodb://localhost:32774/todos-test" ) .build() lazy val reactiveMongoApi = app.injector.instanceOf[ReactiveMongoApi] }
TodoIntegrationSpec
class TodoIntegrationSpec extends PlayWithMongoSpec with BeforeAndAfter { var todos: Future[JSONCollection] = _ before { //Init DB await { todos = reactiveMongoApi.database.map(_.collection("todos")) todos.flatMap(_.insert[Todo](ordered = false).many(List( Todo(_id = None, title = "Test todo 1", completed = Some(false)), Todo(_id = None, title = "Test todo 2", completed = Some(true)), Todo(_id = None, title = "Test todo 3", completed = Some(false)), Todo(_id = None, title = "Test todo 4", completed = Some(true)) ))) } } after { //clean DB todos.flatMap(_.drop(failIfNotFound = false)) } "Get all Todos" in { val Some(result) = route(app, FakeRequest(GET, "/todos")) val resultList = contentAsJson(result).as[List[Todo]] resultList.length mustEqual 4 status(result) mustBe OK } "Add a Todo" in { val payload = Todo(_id = None, title = "Test newly added todo", completed = Some(true)) val Some(result) = route(app, FakeRequest(POST, "/todos").withJsonBody(Json.toJson(payload))) status(result) mustBe CREATED } "Delete a Todo" in { val query = BSONDocument() val Some(todoToDelete) = await(todos.flatMap( _.find(query, Option.empty[JsObject]).one[Todo])) val todoIdToDelete = todoToDelete._id.get.stringify val Some(result) = route(app, FakeRequest(DELETE, s"/todos/$todoIdToDelete")) status(result) mustBe OK } "Update a Todo" in { val query = BSONDocument() val payload = Json.obj( "title" -> "Todo updated" ) val Some(todoToUpdate) = await(todos.flatMap( _.find(query, Option.empty[BSONDocument]).one[Todo])) val todoIdToUpdate = todoToUpdate._id.get.stringify val Some(result) = route(app, FakeRequest(PATCH, s"/todos/$todoIdToUpdate").withJsonBody(payload)) val updatedTodo = contentAsJson(result).as[Todo] updatedTodo.title mustEqual "Todo updated" status(result) mustBe OK } }
UnitTest 실행
Junit과 유사하게 Scala Test(Spec) 도 test하위에 집결됩니다. | OR OR 소스에서 우클릭 하여 TestSpec 수행 |
---|
스칼라 프로젝트의 UnitTest를 실행하는 명령어는 , sbt run (프로젝트디렉토리) 이며
IDE에서는 위와같이 sbtTask또는 ScalaTest를 추가하여 IDE에서 관리할수가 있습니다.
[info] ScalaTest [info] Run completed in 21 seconds, 483 milliseconds. [info] Total number of tests run: 6 [info] Suites: completed 2, aborted 0 [info] Tests: succeeded 6, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [info] Passed: Total 6, Failed 0, Errors 0, Passed 6 [success] Total time: 26 s, completed 2018. 10. 29 오전 1:14:30