Perfect 미니 프로젝트 [4]
- 게시물 정보 API
- 게시물 목록 API
CRUD 중 Read에 해당하는 API를 만들어 봅니다!!
2017년 8월 현재 Swift 3 / Perfect 최신버전 2.0.x 환경에서 진행함을 알려드립니다.
지난 내용 돌아보기
게시물 정보 API
지난 번에 올려둔 게시물의 정보를 가져오는 API를 구성해보려고 합니다. 일단 처음 라우팅 해 둔대로 게시물 정보를 가져오는 API의 핸들러는 articleInfoHandler(request:response:)
함수입니다. 게시물 정보 API 데이터 모양을 살펴볼까요?
요청(Request)
- HTTP Method : GET
- Content-Type: application/json
매개변수 없음
응답(Response)
매개변수 |
자료형 |
비고 |
필수여부 |
image_url |
string |
이미지 URL |
Y |
user_name |
string |
업로드한 사용자 이름 |
Y |
description |
string |
이미지 설명 |
N |
title |
string |
이미지 제목 |
Y |
article_id |
string |
게시물 고유 식별자 |
Y |
게시물 정보를 받기 위해서 GET
메서드로 요청하고, PATH의 마지막에 게시물 고유번호를 보내게 되어있습니다.
그럼 우리도 받을 때 게시물 고유번호를 받고, DB에서 고유번호를 통해 게시물을 찾아 보내주면 됩니다~!
일단 요청정보를 통해 게시물 고유번호를 받아보겠습니다.
Souces/handlers.swift 파일의 articleInfoHandler(request:response:)
함수 내부에 코드를 작성해봅니다.
response.setHeader(.contentType, value: ContentsType.json) // 요청 경로에서 이미지 고유번호를 가져옵니다 guard let articleId: String = request.pathComponents.last else { sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"need article id"]) return }
이제 가져온 게시물 고유번호를 통해 MongoDB에서 문서를 찾아야합니다. find
메서드를 사용하여 쿼리를 보내면 DB는 커서정보를 보내줍니다. 이 커서는 우리가 파일탐색기에서 커서를 옮겨가며 파일을 지정하는 것처럼 DB에 저장되어있는 문서의 위치를 지정하여 가리키는 정보입니다. 위의 코드 아래에 이어서 작성합니다.
// MongoDB에서 문서를 찾으려면 쿼리를 위한 BSON 객체를 만들 필요가 있습니다. // 새로 생성한 BSON 객체에 고유번호를 넣어서 쿼리 객체를 생성합니다 let query: BSON = BSON() let oid: BSON.OID = BSON.OID(articleId) query.append(oid: oid) // 사용한 BSON 문서는 닫아줍니다 defer { query.close() } // DB 컬렉션에 쿼리를 전송하여 해당 문서를 가리키는 커서를 가져옵니다 guard let cursor: MongoCursor = DB.collection?.find(query: query, limit: 1) else { sendCompleteResponse(response, status: .internalServerError) return }
커서정보를 받아왔으면 이제 문서를 JSON 형태로 변환하여 클라이언트에게 전송해주면 됩니다. 자세한 설명은 주석으로 달려있으니, 위 코드 아래에 이어서 작성해봅니다.
do { // 커서가 가리키는 객체들을 모두 JSON 문자열로 변경한 후, // JSON 문자열을 딕셔너리 배열로 변환해 봅니다 guard let articleDocumentDictionaries = try cursor.jsonString.jsonDecode() as? [[String : Any]] else { sendCompleteResponse(response, status: .internalServerError) return } // 우리는 하나의 게시물 정보만 필요하므로 배열에서 첫 번째 요소를 꺼내옵니다. // 만약 배열이 비어있다면 게시물을 찾지 못했을 확률이 큽니다 guard let article = articleDocumentDictionaries.first else { sendCompleteResponse(response, status: .notFound) return } // 꺼내온 게시물 정보를 응답해줍니다 sendCompleteResponse(response, status: .ok, jsonBody: article) } catch { sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.message : error.localizedDescription]) return }
짠! 벌써 끝났어요!! 우하하 한 번 테스트해봅니다.
(주소의 마지막 경로에 위치한 게시물 고유번호는 여러분의 DB에서 부여한 번호에 따라 다릅니다)
헛... 그런데 내려온 JSON 형식을 보아하니, 문서에 정리되어 있는 것처럼 깔끔하지가 않군요. DB에서 관리하고 있는 _id
객체라던지... null
상태로 내려온 description
이라던지... 마음에 들지 않는 부분이 너무 많습니다. 게다가 이미지의 URL이 아닌 서버 내부의 경로가 내려오고 있죠. 이것들을 다 바꿔주고 싶습니다.
그런데, 생각해보니 우리는 나중에 게시물 목록을 내려주는 API도 구성해야 합니다. 아마 중복작업들이 일어날 것 같으니 그냥 미리 빼서 함수로 만들어 놓는 것이 좋을 것 같아요. 이런 사전처리들을 위해서 Souces/handlers.swift 파일의 맨 하단에 함수를 하나 추가해봅니다.
// 쿼리를 통해 데이터베이스에서 문서 정보를 딕셔너리 형태로 변환하여 반환합니다. // 딕셔너리에 불필요한 정보를 제거하고, 필요한 정보를 형식에 맞게 다시 넣어줍니다 private func articleDocumentDictionaries(query: BSON = BSON(), skip: Int = 0, limit: Int = 0) -> [[String: Any]]? { // 사용한 BSON 문서는 나중에 닫아줍니다 defer { query.close() } // DB 컬렉션에 쿼리를 전송하여 해당 문서를 가리키는 커서를 가져옵니다 guard let cursor: MongoCursor = DB.collection?.find(query: query, skip: skip, limit: limit) else { return nil } // 반환할 딕셔너리 배열 생성 var documentDictionaries: [[String : Any]] = [] do { // 커서가 가리키는 문서를 쭉 돌면서 수행됩니다 try cursor.enumerated().forEach { (pair: (offset: Int, element: BSON)) in // 사용한 BSON 문서는 나중에 닫아줍니다 defer { pair.element.close() } var iterator = pair.element.iterator() // 문서 내부에서 고유번호를 가져오기 위해 key에 해당하는 값을 가져옵니다 guard iterator?.find(key: "_id") == true, let oid = iterator?.currentValue?.oid?.description else { return } // DB 문서를 JSON 문자열로 변환 한 후 디코드하여 딕셔너리 인스턴스로 변경해줍니다 guard var article: [String : Any] = try pair.element.asString.jsonDecode() as? [String : Any] else { return } // 이미지 파일 이름을 가져온 후 guard let imageFileName: String = (article[JSONKey.imagePath] as? String)?.lastFilePathComponent else { return } // 응답 데이터에는 이미지 경로 대신 이미지 URL을 넣어줍니다 article.removeValue(forKey: JSONKey.imagePath) article[JSONKey.imageUrl] = server.serverAddress + ":\(server.serverPort)" + "/image/" + imageFileName // _id 키대신 우리가 원하는 형식으로 게시물 고유번호를 넣어줍니다 article.removeValue(forKey: "_id") article[JSONKey.articleId] = oid // 딕셔너리 내부에 Null 값이 있다면 해당 키를 삭제해줍니다 article.forEach({ (pair: (key: String, value: Any)) in if type(of: pair.value) == JSONConvertibleNull.self { article.removeValue(forKey: pair.key) } }) // 반환할 배열에 넣어줍니다 documentDictionaries.append(article) } // 배열 반환 return documentDictionaries } catch { return nil } }
그리고 다시 사진 게시물 정보 핸들러 함수를 깔끔하게 정리해봅니다.
// 사진 게시물 정보 func articleInfoHandler(request: HTTPRequest, response: HTTPResponse) { response.setHeader(.contentType, value: ContentsType.json) // 요청 경로에서 이미지 고유번호를 가져옵니다 guard let articleId: String = request.pathComponents.last else { sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"need article id"]) return } // MongoDB에서 문서를 찾으려면 쿼리를 위한 BSON 객체를 만들 필요가 있습니다. // 새로 생성한 BSON 객체에 고유번호를 넣어서 쿼리 객체를 생성합니다 let query: BSON = BSON() let oid: BSON.OID = BSON.OID(articleId) query.append(oid: oid) // 게시물 정보를 담은 딕셔너리 배열을 가져온 후 첫 번째 요소를 꺼냅니다 guard let articleDocumentDictionary = articleDocumentDictionaries(query: query, limit: 1)?.first else { sendCompleteResponse(response, status: .internalServerError) return } // 모든 작업을 완료하고 JSON 응답 sendCompleteResponse(response, status: .ok, jsonBody: articleDocumentDictionary) }
그리고 동작확인!
크아~ 우리가 원하던 대로 동작합니다!!
## 게시물 목록 API
게시물 목록을 가져오는 것은 이제 일도 아니군요! 미리 함수를 만들어 뒀으니 전체 게시물 목록도 손쉽게 가져올 수 있습니다. 물론 우리가 원하는 깔끔한 형태로요~
DB 컬렉션의 find
메서드의 매개변수로 전달하는 query
객체가 아무 정보도 가지고 있지 않다면 컬렉션에 존재하는 모든 문서를 가져올 수 있습니다. skip
은 찾은 문서 중 지나갈 문서의 수, limit
는 커서가 가리키게 될 문서의 최대 개수입니다.
게시물 목록을 가져오기 위한 API 정보입니다.
사진 게시물 목록
요청(Request)
- HTTP Method : GET
- Content-Type: application/json
매개변수 |
자료형 |
값의 범위/기본 값 |
비고 |
필수여부 |
page |
integer |
0 |
조회하고자 하는 페이지 번호 |
N |
user_name |
string |
특정 사용자의 게시물만 받아오고자 할 때 |
N |
|
articles_per_page |
integer |
1~100 / 10 |
각 페이지 당 게시물 수 |
N |
응답(Response)
Key |
자료형 |
비고 |
필수여부 |
articles |
json object array (string) |
게시물 정보 배열 |
Y |
articles_per_page |
integer |
각 페이지 당 게시물 수 |
Y |
current_page |
integer |
전송된 페이지 |
Y |
total_page |
integer |
전체 페이지 수 |
Y |
articles item object 형태
Key |
자료형 |
비고 |
필수여부 |
image_url |
string |
이미지 URL |
Y |
user_name |
string |
업로드한 사용자 이름 |
Y |
description |
string |
이미지 설명 |
N |
title |
string |
이미지 제목 |
Y |
article_id |
string |
게시물 고유 식별자 |
Y |
클라이언트에서 요청할 때 부가정보 전달을 위해서 page
, user_name
, articles_per_page
매개변수를 사용하는군요. 일단 이 매개변수를 가져오는 코드를 작성해봅니다.
사진 게시물 목록을 핸들링하는 articleListHandler(request:, response:)
함수에 기존의 내용을 지워주고 새로운 코드를 작성합니다.
response.setHeader(.contentType, value: ContentsType.json) // 요청된 페이지 번호 let page: Int = Int(request.param(name: JSONKey.page) ?? "0") ?? 0 // 잘못된 범위라면 응답처리 if page < 0 { sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message : "wrong page number"]) return } // 한 페이지에 담길 게시물 개수 let articlesPerPage: Int = Int(request.param(name: JSONKey.articlesPerPage) ?? "10") ?? 10 // 잘못된 범위라면 응답처리 if articlesPerPage < 1 || articlesPerPage > 100 { sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message : "wrong paging size"]) return } // 특정 사용자의 게시물만 검색하고자 할 때 요청하는 사용자 이름 let userName: String? = request.param(name: JSONKey.userName)
자 이렇게 요청 정보에서 필요한 정보들을 가져왔습니다. 이제 이 정보들을 기반으로 검색할 쿼리를 만들어줍니다. 위 코드 바로 아래에 이어서 코드를 작성합니다.
// 쿼리에 사용할 BSON 객체 let query: BSON = BSON() // 만약 사용자 이름에 대해 검색하고 한다면 BSON 객체에 값 추가 if let name = userName { query.append(key: JSONKey.userName, string: name) }
쿼리는 역시나 간단합니다. 그러면 쿼리를 통해 검색된 문서의 총 개수를 알아봅니다. 검색결과는 MongoResult
라는 열거형 타입으로 반환됩니다. 그리고 그 연관 값으로 결과 값을 알아낼 수 있지요.
// 쿼리를 통해 조건에 해당하는 문서가 총 몇개인지 확인 guard let countResult = DB.collection?.count(query: query) else { sendCompleteResponse(response, status: .internalServerError) return } // 총 문서의 개수를 저장할 상수 let totalCount: Int // count 결과에 따른 처리 if case .replyInt(let count) = countResult { totalCount = count } else if case .error(_, _, let message) = countResult { sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.message : message]) return } else { sendCompleteResponse(response, status: .internalServerError) return }
마지막으로 쿼리를 통해 검색한 문서들을 우리가 원하는 범위에서 뽑아내어 딕셔너리 형태로 반환받습니다.
// 반환할 데이터 딕셔너리 var responseData : [String : Any] = [:] responseData[JSONKey.articles] = articleDictionaries responseData[JSONKey.articlesPerPage] = articlesPerPage responseData[JSONKey.currentPage] = page responseData[JSONKey.totalPage] = totalCount / articlesPerPage // 모든 작업을 완료하고 JSON 응답 sendCompleteResponse(response, status: .ok, jsonBody: responseData)
빠밤! 참 쉽죠? 헤헿
Read까지 해봤군요! 다음엔 Update를 할까요 Delete를 할까요?
오늘은 여기까지~!
다음에 또 만나요~~ 😀
## 참고문서
* [PerfectDocs - HTTPRequest](http://perfect.org/docs/HTTPRequest.html)
* [PerfectDocs - MongoDB](http://perfect.org/docs/MongoDB.html)
by yagom
---
facebook : https://www.facebook.com/yagompage
facebook group : https://www.facebook.com/groups/yagom
p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.
RSS Feed 받기