사진 게시판 API 만들기 [4]

Perfect 미니 프로젝트 [4]

  1. 게시물 정보 API
  2. 게시물 목록 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

업로드한 사용자 이름 

description

string 

이미지 설명 

title

string 

이미지 제목 

article_id

string 

게시물 고유 식별자

게시물 정보를 받기 위해서 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

조회하고자 하는 페이지 번호

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 

전송된 페이지 

total_page

integer 

전체 페이지 수 

Y

articles item object 형태

Key 

자료형 

비고 

필수여부 

image_url

string

이미지 URL

Y

user_name

string

업로드한 사용자 이름 

description

string 

이미지 설명 

title

string 

이미지 제목 

article_id

string 

게시물 고유 식별자

클라이언트에서 요청할 때 부가정보 전달을 위해서 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 받기   

댓글 남기기

Close