[Vapor/Swift] CRUD 기능 구현하기

Ryan (Geonhee) Son·2021년 7월 7일
0

데이터베이스 마이그레이션 작업에 모델 DTO 추가까지 진행해 보았습니다. 이제 실제로 만들고(Create), 읽고(Read), 업데이트하고(Update), 삭제할(Delete) 수 있는 API를 서버에서 구현해볼까요?

변경사항이 일어날 때마다 배포하여 테스트하기에는 시간이 많이 소요되니 로컬 환경에서 진행하도록 하겠습니다. 로컬 환경의 DB 구성 방법은 이 포스팅을 참고해주세요.

CRUD 기능 구현

Controller 파일 생성 및 타입 작성

먼저 클라이언트측에서 Request를 보내면 이를 처리할 메인 로직을 담당해줄 Controller 타입을 만듭니다. App 폴더 안에 Controllers라는 폴더를 만들고 <SchemaName>Controller.swift 파일을 생성해주세요. 저는 ProjectItemController.swift를 만들었습니다.

아래와 같이 FluentVapor를 import 해주세요.

import Fluent
import Vapor

이제 파일 이름과 동일한 구조체를 만들어보겠습니다. 이 컨트롤러는 routes.swift를 도와 묶음으로 일을 처리해주는 타입이므로 RouteCollection 프로토콜을 채택해줍니다.

RouteCollection 프로토콜은 아래와 같이 boot(routes:) 메서드 구현을 요구하는데, 이 메서드는 이후 과정에서 routes.swiftroutes(_:)register(collection:)을 실행하여 컨트롤러를 사용해줄겁니다. 구조가 이해되시죠? 컨트롤러에서 이를 구현하지 않는다면 routes(_:) 함수 안에 컨트롤러의 모든 업무를 정의해주어야 할 것입니다. 아주 비대한 함수를 만들게 되겠지요. 지금은 boot(routes:) 메서드의 body를 빈 코드블럭으로 남겨주세요.

이 과정까지 완료하시면 아래와 같은 구조가 되었을 것입니다.

struct ProjectItemController: RouteCollection {
    func boot(routes: RoutesBuilder) throws { }
}

Create

초기 구성이 끝났으니 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를 반환하여 클라이언트가 정상적으로 요청이 처리되었음을 확인할 수 있도록 해줍니다.

Read

다음으로 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

Update는 HTTP PUTPATCH 메서드에 대응하는 메서드입니다. 이미 데이터베이스에 존재하는 모델 인스턴스를 대상으로 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

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)
}

URL 지정 및 CRUD 메서드 적용

CRUD 기능을 구현했다면 이제 우리 서버의 어떤 URL에 어떤 HTTP 메서드로 접근하면 해당 작업들을 할 수 있는지를 정의해야겠죠? 최초에 Controller를 만들며 빈 코드블럭으로 남겨두었던 boot(routes:) 메서드를 이용하면 이 작업을 하실 수 있습니다.

먼저 메서드가 전달인자로 받은 routesRoutesBuilder 타입으로, 내부의 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)
}

위의 예시를 도식으로 나타내면 아래와 같습니다.

route에 Controller 등록

이제 각 HTTP 메서드를 통해 요청할 수 있는 URL을 구성(route)했으니 이 컨트롤러를 routes.swift에 있는 routes(_:) 메서드에 등록만 하면 서버가 정상 작동될 것입니다.

// routes.swift
import Vapor
import Fluent

func routes(_ app: Application) throws {
    try app.register(collection: ProjectItemController())
}

기능 확인

기능 확인을 하기 전에 아래 사항들이 잘 적용되어 있는지 확인해주세요.

사전 작업

  1. 로컬 PostgreSQL을 사용하도록 DB 구성 (configure.swift) - 관련 포스팅
    해당 포스팅의 등록 (Register)과 같은 내용으로 작성되어 있으면 됩니다.
  2. 추가적인 모델 타입, 마이그레이션 타입 필드 변경이 있었다면 마이그레이션 작업 완료 - 위 관련 포스팅에서 revert 후 migration
  3. Controller의 boot(routes:) 메서드 완성 및 routes.swiftroutes(_:) 메서드에 컨트롤러 등록 (본 포스팅)
  4. Postman과 같이 Request를 보낼 수 있는 툴 준비
  5. Xcode vapor 프로젝트 Run (command + R)

기능 검증

Create

  1. POST 방식으로 요청할 수 있도록 옵션 선택
  2. Request 송신 툴의 URL에 http://localhost:8080/projectItem과 같이 POST 요청이 가능한 URL을 작성
  3. Body에 JSON을 담아 보낼 수 있도록 설정
  4. Body 내용 작성
  5. 요청 송신

Response의 HTTP Status Code가 200 번대인지, 서버가 요청한대로 응답하였는지 확인해보세요.

Read

  1. GET 방식으로 요청할 수 있도록 옵션 선택
  2. Request 송신 툴의 URL에 http://localhost:8080/projectItems/doing과 같이 GET 요청이 가능한 URL을 작성
  3. 요청 송신

Update

Create와 동일한 방식으로 요청을 만들어주되 몇 가지 파라미터는 의도적으로 제외하고 요청을 송신해봅시다.
1. PATCH 방식으로 요청할 수 있도록 옵션 선택
2. Request 송신 툴의 URL에 http://localhost:8080/projectItem과 같이 PATCH 요청이 가능한 URL을 작성
3. Body에 JSON을 담아 보낼 수 있도록 설정
4. Body 내용 작성
5. 요청 송신

POST 요청과 마찬가지로 Response의 HTTP Status Code가 200 번대인지, 서버가 요청한대로 응답하였는지 확인해보세요.

Delete

삭제 작업은 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 데이터를 검증하는 방법을 알아보겠습니다.

profile
합리적인 해법 찾기를 좋아합니다.

0개의 댓글