데이터베이스 마이그레이션 작업에 모델 DTO 추가까지 진행해 보았습니다. 이제 실제로 만들고(Create), 읽고(Read), 업데이트하고(Update), 삭제할(Delete) 수 있는 API를 서버에서 구현해볼까요?
변경사항이 일어날 때마다 배포하여 테스트하기에는 시간이 많이 소요되니 로컬 환경에서 진행하도록 하겠습니다. 로컬 환경의 DB 구성 방법은 이 포스팅을 참고해주세요.
먼저 클라이언트측에서 Request를 보내면 이를 처리할 메인 로직을 담당해줄 Controller
타입을 만듭니다. App
폴더 안에 Controllers
라는 폴더를 만들고 <SchemaName>Controller.swift
파일을 생성해주세요. 저는 ProjectItemController.swift
를 만들었습니다.
아래와 같이 Fluent
와 Vapor
를 import 해주세요.
import Fluent
import Vapor
이제 파일 이름과 동일한 구조체를 만들어보겠습니다. 이 컨트롤러는 routes.swift
를 도와 묶음으로 일을 처리해주는 타입이므로 RouteCollection
프로토콜을 채택해줍니다.
RouteCollection
프로토콜은 아래와 같이 boot(routes:)
메서드 구현을 요구하는데, 이 메서드는 이후 과정에서 routes.swift
의 routes(_:)
가 register(collection:)
을 실행하여 컨트롤러를 사용해줄겁니다. 구조가 이해되시죠? 컨트롤러에서 이를 구현하지 않는다면 routes(_:)
함수 안에 컨트롤러의 모든 업무를 정의해주어야 할 것입니다. 아주 비대한 함수를 만들게 되겠지요. 지금은 boot(routes:)
메서드의 body를 빈 코드블럭으로 남겨주세요.
이 과정까지 완료하시면 아래와 같은 구조가 되었을 것입니다.
struct ProjectItemController: RouteCollection {
func boot(routes: RoutesBuilder) throws { }
}
초기 구성이 끝났으니 Controller 타입 안에 HTTP POST
메서드에 대응하는 Create 메서드를 작성해보겠습니다. 이전 포스팅에서 작성한 이니셜라이저를 이용해서 PostProjectItem
타입으로 디코딩된 인스턴스로부터 ProjectItem
타입의 인스턴스를 만들어서 저장해주시고 해당 내용을 response body로 반환해주시면 됩니다.
func create(req: Request) throws -> EventLoopFuture<ProjectItem> {
let exist = try req.content.decode(PostProjectItem.self)
let newProjectItem = ProjectItem(exist)
return newProjectItem.save(on: req.db).map{ (result) -> ProjectItem in
return newProjectItem
}
}
여기에서 반환하실 때 save(on:)
메서드를 create(on:)
메서드로 대체하실 수 있습니다. save(on:)
메서드는 데이터베이스에 ID가 존재하지 않을 때는 create(on:)
메서드로, 존재할 때는 update(on:)
메서드로 작동하는 메서드입니다.
요청에 따라 모델 인스턴스를 DB에 생성하면 map(_:)
메서드의 클로저를 통해 response에 담아줄 body를 반환하여 클라이언트가 정상적으로 요청이 처리되었음을 확인할 수 있도록 해줍니다.
다음으로 HTTP GET
메서드에 대응하는 간단한 read용 메서드를 구현해보겠습니다.
먼저 조건 없이 스키마 안의 모든 모델 인스턴스를 반환하는 메서드는 아래와 같이 쿼리 결과를 모두 가져오는 방식으로 작성하시면 됩니다.
func read(req: Request) throws -> EventLoopFuture<[ProjectItem]> {
return ProjectItem.query(on: req.db).all()
}
저는 클라이언트의 Request에서 Path parameter
를 이용해 요청한 parameter가 포함된 모델 인스턴스를 필터하여 제공하는 메서드를 작성해보았습니다.
func read(req: Request) throws -> EventLoopFuture<[ProjectItem]> {
let validProgress = ["todo", "doing", "done"]
guard let progress = req.parameters.get("progress"), validProgress.contains(progress) else {
throw Abort(.badRequest)
}
return ProjectItem.query(on: req.db).filter(\.$progress == progress).all()
}
필터 작업은 위와 같은 방식으로 수행하시면 됩니다.
Update는 HTTP PUT
과 PATCH
메서드에 대응하는 메서드입니다. 이미 데이터베이스에 존재하는 모델 인스턴스를 대상으로 Request하기 때문에 요청한 내용으로부터 id
를 식별하고 DB에서 id
를 기준으로 해당 인스턴스를 찾아 수정한 후 업데이트 해줍니다.
func update(req: Request) throws -> EventLoopFuture<ProjectItem> {
let exist = try req.content.decode(PatchProjectItem.self)
return ProjectItem.find(exist.id, on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { item in
if let title = exist.title { item.title = title }
if let content = exist.content { item.content = content }
if let progress = exist.progress { item.progress = progress }
if let deadlineDate = exist.deadlineDate { item.deadlineDate = deadlineDate }
if let index = exist.index { item.index = index }
return item.update(on: req.db).map { return item }
}
}
Delete는 HTTP DELETE
메서드에 대응하는 메서드로, 아래와 같이 데이터베이스에 있는 데이터를 삭제할 수 있도록 구성해주시면 됩니다.
func delete(req: Request) throws -> EventLoopFuture<HTTPStatus> {
guard req.headers.contentType == .json else {
throw HTTPError.invalidContentType
}
let exist = try req.content.decode(DeleteProjectItem.self)
return ProjectItem.find(exist.id, on: req.db)
.unwrap(or: HTTPError.invalidID)
.flatMap { $0.delete(on: req.db) }
.transform(to: .ok)
}
CRUD 기능을 구현했다면 이제 우리 서버의 어떤 URL에 어떤 HTTP 메서드로 접근하면 해당 작업들을 할 수 있는지를 정의해야겠죠? 최초에 Controller를 만들며 빈 코드블럭으로 남겨두었던 boot(routes:)
메서드를 이용하면 이 작업을 하실 수 있습니다.
먼저 메서드가 전달인자로 받은 routes
는 RoutesBuilder
타입으로, 내부의 grouped(_:)
메서드로 https://some-server-name.domain-name.com/somePath
의 형태로 경로를 지정해주실 수 있고, group(_:)
메서드를 통해 Path parameter를 설정해주실 수 있습니다. 아래 예시를 보시죠.
func boot(routes: RoutesBuilder) throws {
// https://some-server-name.domain-name.com/projectItems
let projectItems = routes.grouped("projectItems")
// https://some-server-name.domain-name.com/projectItems/:progress
// :progress에 작성되는 내용은 path parameter 형식으로 서버에 전달됨
projectItems.group(":progress") { projectItem in
// 위의 URL로 get 요청 가능
projectItem.get(use: read)
}
// https://some-server-name.domain-name.com/projectItem
let projectItem = routes.grouped("projectItem")
// 위의 URL로 post, patch, delete 요청 가능
projectItem.post(use: create)
projectItem.patch(use: update)
projectItem.delete(use: delete)
}
위의 예시를 도식으로 나타내면 아래와 같습니다.
이제 각 HTTP 메서드를 통해 요청할 수 있는 URL을 구성(route)했으니 이 컨트롤러를 routes.swift
에 있는 routes(_:)
메서드에 등록만 하면 서버가 정상 작동될 것입니다.
// routes.swift
import Vapor
import Fluent
func routes(_ app: Application) throws {
try app.register(collection: ProjectItemController())
}
기능 확인을 하기 전에 아래 사항들이 잘 적용되어 있는지 확인해주세요.
configure.swift
) - 관련 포스팅등록 (Register)
과 같은 내용으로 작성되어 있으면 됩니다.boot(routes:)
메서드 완성 및 routes.swift
내 routes(_:)
메서드에 컨트롤러 등록 (본 포스팅)POST
방식으로 요청할 수 있도록 옵션 선택 http://localhost:8080/projectItem
과 같이 POST
요청이 가능한 URL을 작성Response의 HTTP Status Code가 200 번대인지, 서버가 요청한대로 응답하였는지 확인해보세요.
GET
방식으로 요청할 수 있도록 옵션 선택 http://localhost:8080/projectItems/doing
과 같이 GET
요청이 가능한 URL을 작성Create와 동일한 방식으로 요청을 만들어주되 몇 가지 파라미터는 의도적으로 제외하고 요청을 송신해봅시다.
1. PATCH
방식으로 요청할 수 있도록 옵션 선택
2. Request 송신 툴의 URL에 http://localhost:8080/projectItem
과 같이 PATCH
요청이 가능한 URL을 작성
3. Body에 JSON을 담아 보낼 수 있도록 설정
4. Body 내용 작성
5. 요청 송신
POST 요청과 마찬가지로 Response의 HTTP Status Code가 200 번대인지, 서버가 요청한대로 응답하였는지 확인해보세요.
삭제 작업은 DB에 등록된 해당 아이템의 ID만 제공해주면 삭제 작업이 가능하도록 DTO를 작성했었습니다.
1. DELETE
방식으로 요청할 수 있도록 옵션 선택
2. Request 송신 툴의 URL에 http://localhost:8080/projectItem
과 같이 DELETE
요청이 가능한 URL을 작성
3. Body에 JSON을 담아 보낼 수 있도록 설정
4. Body 내용 작성
5. 요청 송신
Response의 HTTP Status Code가 200 번대인지 확인해보세요.
이번 시간에는 CRUD 기능을 구현해보았습니다. 하지만 아직 DB를 사용할 수 있게끔 인터페이스만 구성한 상황인데요, 그래서 다음 포스팅에서는 Validatable
프로토콜을 이용해 클라이언트가 보낸 Body 데이터를 검증하는 방법을 알아보겠습니다.