본문 바로가기
-----ETC-----/C++ 네트워크 프로그래밍 시리즈

[C++ 네트워크 프로그래밍] Day 11: RESTful API 설계와 구현

by cogito21_cpp 2024. 8. 1.
반응형

RESTful API 소개

REST(Representational State Transfer)는 웹 서비스 설계 아키텍처입니다. RESTful API는 REST 아키텍처 스타일을 따르는 API입니다. RESTful API는 클라이언트와 서버 간의 통신을 단순화하고 표준화하는 데 사용됩니다. HTTP를 기반으로 하며, URL을 통해 리소스를 식별하고, HTTP 메서드를 사용하여 리소스를 조작합니다.

RESTful API의 주요 특징

  1. 리소스 기반: 모든 것은 리소스로 취급되며, 각 리소스는 고유한 URL로 식별됩니다.
  2. HTTP 메서드 사용: RESTful API는 HTTP 메서드를 사용하여 리소스를 조작합니다.
    • GET: 리소스 조회
    • POST: 리소스 생성
    • PUT: 리소스 전체 수정
    • PATCH: 리소스 일부 수정
    • DELETE: 리소스 삭제
  3. 상태 없음: 서버는 클라이언트의 상태를 유지하지 않습니다. 각 요청은 독립적이며, 필요한 모든 정보를 포함해야 합니다.
  4. 표현 방식: 리소스는 다양한 형태로 표현될 수 있습니다. 일반적으로 JSON 또는 XML 형식이 사용됩니다.

RESTful API 설계

RESTful API를 설계할 때는 리소스와 엔드포인트를 정의하고, 각 엔드포인트에서 수행할 작업을 HTTP 메서드와 매핑합니다.

리소스 예시

  1. 사용자 (User)
    • URL: /users
    • 메서드:
      • GET /users: 모든 사용자 조회
      • GET /users/{id}: 특정 사용자 조회
      • POST /users: 새로운 사용자 생성
      • PUT /users/{id}: 특정 사용자 정보 수정
      • DELETE /users/{id}: 특정 사용자 삭제

RESTful API 구현

Boost.Beast를 사용하여 간단한 RESTful API 서버를 구현하겠습니다. 사용자 정보를 관리하는 API를 구현할 것입니다.

API 서버 구현

User.h

#ifndef USER_H
#define USER_H

#include <string>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>

class User {
public:
    User(const std::string& name, int age)
        : id_(boost::uuids::random_generator()()), name_(name), age_(age) {}

    std::string getId() const {
        return boost::uuids::to_string(id_);
    }

    std::string getName() const {
        return name_;
    }

    int getAge() const {
        return age_;
    }

    void setName(const std::string& name) {
        name_ = name;
    }

    void setAge(int age) {
        age_ = age;
    }

private:
    boost::uuids::uuid id_;
    std::string name_;
    int age_;
};

#endif // USER_H

 

UserManager.h

#ifndef USERMANAGER_H
#define USERMANAGER_H

#include "User.h"
#include <unordered_map>
#include <mutex>
#include <memory>

class UserManager {
public:
    std::shared_ptr<User> createUser(const std::string& name, int age) {
        std::lock_guard<std::mutex> lock(mutex_);
        auto user = std::make_shared<User>(name, age);
        users_[user->getId()] = user;
        return user;
    }

    std::shared_ptr<User> getUser(const std::string& id) {
        std::lock_guard<std::mutex> lock(mutex_);
        auto it = users_.find(id);
        if (it != users_.end()) {
            return it->second;
        }
        return nullptr;
    }

    bool deleteUser(const std::string& id) {
        std::lock_guard<std::mutex> lock(mutex_);
        return users_.erase(id) > 0;
    }

private:
    std::unordered_map<std::string, std::shared_ptr<User>> users_;
    std::mutex mutex_;
};

#endif // USERMANAGER_H

 

AsyncRestServer.h

#ifndef ASYNCRESTSERVER_H
#define ASYNCRESTSERVER_H

#include "UserManager.h"
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/strand.hpp>
#include <boost/config.hpp>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#include <iostream>

namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = net::ip::tcp;

class AsyncRestServer : public std::enable_shared_from_this<AsyncRestServer> {
public:
    AsyncRestServer(net::io_context& ioc, tcp::endpoint endpoint)
        : ioc_(ioc), acceptor_(net::make_strand(ioc)), userManager_(std::make_shared<UserManager>()) {
        beast::error_code ec;

        acceptor_.open(endpoint.protocol(), ec);
        if (ec) {
            std::cerr << "오류: " << ec.message() << std::endl;
            return;
        }

        acceptor_.set_option(net::socket_base::reuse_address(true), ec);
        if (ec) {
            std::cerr << "오류: " << ec.message() << std::endl;
            return;
        }

        acceptor_.bind(endpoint, ec);
        if (ec) {
            std::cerr << "오류: " << ec.message() << std::endl;
            return;
        }

        acceptor_.listen(net::socket_base::max_listen_connections, ec);
        if (ec) {
            std::cerr << "오류: " << ec.message() << std::endl;
            return;
        }

        do_accept();
    }

private:
    net::io_context& ioc_;
    tcp::acceptor acceptor_;
    std::shared_ptr<UserManager> userManager_;

    void do_accept() {
        acceptor_.async_accept(
            net::make_strand(ioc_),
            [self = shared_from_this()](beast::error_code ec, tcp::socket socket) {
                if (!ec) {
                    std::make_shared<session>(std::move(socket), self->userManager_)->run();
                }
                self->do_accept();
            });
    }

    class session : public std::enable_shared_from_this<session> {
        tcp::socket socket_;
        beast::flat_buffer buffer_;
        http::request<http::string_body> req_;
        std::shared_ptr<UserManager> userManager_;

    public:
        session(tcp::socket socket, std::shared_ptr<UserManager> userManager)
            : socket_(std::move(socket)), userManager_(std::move(userManager)) {}

        void run() {
            do_read();
        }

    private:
        void do_read() {
            auto self = shared_from_this();
            http::async_read(socket_, buffer_, req_,
                [self](beast::error_code ec, std::size_t bytes_transferred) {
                    boost::ignore_unused(bytes_transferred);
                    if (!ec) {
                        self->handle_request();
                    }
                });
        }

        void handle_request() {
            auto const bad_request = [this](beast::string_view why) {
                http::response<http::string_body> res{ http::status::bad_request, req_.version() };
                res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
                res.set(http::field::content_type, "text/html");
                res.keep_alive(req_.keep_alive());
                res.body() = std::string(why);
                res.prepare_payload();
                return res;
            };

            auto const not_found = [this](beast::string_view target) {
                http::response<http::string_body> res{ http::status::not_found, req_.version() };
                res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
                res.set(http::field::content_type, "text/html");
                res.keep_alive(req_.keep_alive());
                res.body() = "The resource '" + std::string(target) + "' was not found.";
                res.prepare_payload();
                return res;
            };

            auto const server_error = [this](beast::string_view what) {
                http::response<http::string_body> res{ http::status::internal_server_error, req_.version() };
                res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
                res.set(http::field::content_type, "text/html");
                res.keep_alive(req_.keep_alive());
                res.body() = "An error occurred: '" + std::string(what) + "'";
                res.prepare_payload();
                return res;
            };

            try {
                if (req_.method() == http::verb::get) {
                    if (req_.target() == "/users") {
                        // 모든 사용자 조회 로직 구현
                    } else if (req_.target().starts_with("/users/")) {
                        // 특정 사용자 조회 로직 구현
                    }
                } else if (req_.method() == http::verb::post) {
                    if (req_.

target() == "/users") {
                        // 새로운 사용자 생성 로직 구현
                    }
                } else if (req_.method() == http::verb::delete_) {
                    if (req_.target().starts_with("/users/")) {
                        // 특정 사용자 삭제 로직 구현
                    }
                }
            } catch (const std::exception& e) {
                do_write(server_error(e.what()));
                return;
            }

            do_write(bad_request("Invalid request"));
        }

        void do_write(http::response<http::string_body>&& res) {
            auto self = shared_from_this();
            http::async_write(socket_, res,
                [self](beast::error_code ec, std::size_t bytes_transferred) {
                    boost::ignore_unused(bytes_transferred);
                    self->socket_.shutdown(tcp::socket::shutdown_send, ec);
                    self->socket_.close();
                });
        }
    };
};

#endif // ASYNCRESTSERVER_H

 

main.cpp

#include <iostream>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/strand.hpp>
#include <boost/config.hpp>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#include "AsyncRestServer.h"

namespace net = boost::asio;
using tcp = net::ip::tcp;

int main() {
    try {
        auto const address = net::ip::make_address("0.0.0.0");
        auto const port = static_cast<unsigned short>(std::atoi("12345"));
        net::io_context ioc{ 1 };

        std::make_shared<AsyncRestServer>(ioc, tcp::endpoint{ address, port })->run();
        ioc.run();
    }
    catch (std::exception const& e) {
        std::cerr << "예외 발생: " << e.what() << std::endl;
    }

    return EXIT_SUCCESS;
}

 

설명

위의 코드는 Boost.Beast를 사용하여 간단한 RESTful API 서버를 구현한 예제입니다. 이 서버는 사용자의 정보를 관리하는 API를 제공합니다.

  • User 클래스: 사용자 정보를 나타내는 클래스입니다. 이름과 나이를 포함합니다.
  • UserManager 클래스: 사용자 정보를 관리하는 클래스입니다. 사용자를 생성, 조회, 삭제하는 기능을 제공합니다.
  • AsyncRestServer 클래스: HTTP 서버를 구현한 클래스입니다. 클라이언트의 HTTP 요청을 비동기적으로 처리하고, 적절한 응답을 반환합니다.
  • session 클래스: 클라이언트와의 세션을 관리하는 클래스입니다. HTTP 요청을 처리하고, 응답을 생성합니다.

이제 열한 번째 날의 학습을 마쳤습니다. RESTful API의 기본 개념과 Boost.Beast를 사용하여 간단한 RESTful API 서버를 구현하는 방법을 학습했습니다.

질문이나 피드백이 있으면 언제든지 댓글로 남겨 주세요. 내일은 "웹 소켓 프로그래밍 기초"에 대해 학습하겠습니다.

반응형