무엇인가

요청하는 쪽과 제공하는 쪽을 갈라 두는 구조다. 서버는 자원을 쥐고 기다리고, 클라이언트가 필요할 때 요청한다.

계층화가 “한 프로그램 안의 방향”이라면 이쪽은 주체가 둘로 나뉜다는 점이 다르다. 그래서 둘 사이에 늘 경계가 있다 — 프로세스든 네트워크든.

구조

graph LR
    C1[클라이언트 A] -->|요청| S[서버]
    C2[클라이언트 B] -->|요청| S
    C3[클라이언트 C] -->|요청| S
    S -->|응답| C1
    S -->|응답| C2
    S -->|응답| C3
    S --- R[(공유 자원)]

C++로 보기

소켓을 걷어내고 구조만 남기면 이렇다. 요점은 클라이언트가 서버의 내부 상태를 직접 만지지 못하고 오직 요청·응답으로만 닿는다는 것이다.

#include <iostream>
#include <string>
#include <unordered_map>
 
struct Request {
    std::string verb;   // "GET" 또는 "SET"
    std::string key;
    std::string value;
};
 
struct Response {
    int code;           // 200 / 404
    std::string body;
};
 
class KeyValueServer {
    std::unordered_map<std::string, std::string> store_;
 
  public:
    // 클라이언트가 닿을 수 있는 유일한 통로
    Response handle(const Request& req) {
        if (req.verb == "SET") {
            store_[req.key] = req.value;
            return {200, "ok"};
        }
        if (req.verb == "GET") {
            auto it = store_.find(req.key);
            if (it == store_.end()) return {404, "not found"};
            return {200, it->second};
        }
        return {400, "bad verb"};
    }
};
 
class Client {
    KeyValueServer& server_;   // 실제로는 소켓 핸들이 들어갈 자리
    std::string name_;
 
  public:
    Client(KeyValueServer& s, std::string name) : server_(s), name_(std::move(name)) {}
 
    void set(const std::string& k, const std::string& v) {
        auto res = server_.handle({"SET", k, v});
        std::cout << name_ << " SET " << k << " -> " << res.code << '\n';
    }
    void get(const std::string& k) {
        auto res = server_.handle({"GET", k, ""});
        std::cout << name_ << " GET " << k << " -> " << res.code << ' ' << res.body << '\n';
    }
};
 
int main() {
    KeyValueServer server;
    Client a{server, "A"};
    Client b{server, "B"};
 
    a.set("lang", "C++");
    b.get("lang");        // 다른 클라이언트가 쓴 값을 본다 — 상태는 서버에 있다
    b.get("missing");
}

Request/Response를 값 타입으로 둔 것이 중요하다. 경계를 넘는 것은 데이터뿐이고 객체 참조가 넘어가지 않으므로, 이 자리에 나중에 소켓이나 HTTP를 끼워도 구조가 안 바뀐다.

언제 쓰나

  • 자원을 한곳에서 관리해야 할 때(데이터 일관성, 접근 제어).
  • 클라이언트가 여럿이고 서로 같은 상태를 봐야 할 때.
  • 클라이언트 쪽 장비가 약하고 무거운 일을 넘기고 싶을 때.

대가

  • 서버가 단일 장애점이다. 죽으면 전부 멈춘다.
  • 부하가 서버로 몰린다. 늘리려면 복제·분산이 필요하고, 그 순간 상태 공유 문제가 새로 생긴다.
  • 네트워크가 끼면 지연과 실패를 항상 다뤄야 한다. 위 예제가 감추고 있는 부분이 정확히 이것이다.

실제로 만나는 곳

HTTP, 데이터베이스, 게임 서버, X11 같은 디스플레이 서버. 사실상 오늘날 대부분의 네트워크 프로그램이 여기 속한다.