요청과 응답은 데이터를 주고받는 형식이 아니다

vx_developer·6일 전

개발하다가

목록 보기
24/30
post-thumbnail

웹 개발을 처음 배울 때 요청과 응답은 보통 “클라이언트가 서버에 요청을 보내면 서버가 결과를 응답한다”라고 설명한다.

const response = await fetch("/api/tasks");
const tasks = await response.json();

클라이언트가 작업 목록을 요청하고 서버가 JSON 데이터를 반환한다. 웹이 어떤 방향으로 통신하는지 이해하기에는 충분한 설명이다.

하지만 실제 서비스에서는 요청을 보냈고 응답을 받았다는 사실만으로 기능이 완성되지 않는다.

  • 사용자는 어떤 행동을 원했는가?
  • 서버는 요청을 이해하고 처리할 수 있는가?
  • 요청이 두 번 도착하면 같은 작업을 두 번 수행해야 하는가?
  • 서버가 200을 반환했다면 업무도 성공한 것인가?
  • 처리 결과가 아직 확정되지 않았다면 무엇을 응답해야 하는가?
  • 클라이언트는 응답을 받은 뒤 무엇을 해야 하는가?

요청과 응답은 단순히 데이터를 왕복시키는 봉투가 아니다. 서로 다른 시스템이 한 번의 상호작용을 같은 의미로 이해하도록 만드는 메시지다.

실제 서비스에서 요청은 사용자가 원하는 변화와 그 판단에 필요한 맥락을 전달하는 메시지이며, 응답은 서버가 판단한 결과와 클라이언트의 다음 행동을 전달하는 메시지다.


요청은 데이터를 보내는 일이 아니라 의도를 전달하는 일이다

협업 Todo 서비스에서 Chris가 작업 하나를 완료하려고 한다.

데이터 변경만 생각하면 다음과 같은 요청을 만들 수 있다.

await fetch("/api/tasks/task_42", {
  method: "PATCH",
  body: JSON.stringify({
    status: "completed",
  }),
});

이 요청은 status 값을 바꾸고 싶다는 사실은 보여준다. 그러나 사용자가 왜 그 값을 바꾸는지, 서버가 어떤 규칙을 적용해야 하는지는 드러내지 않는다.

작업 완료는 단순한 문자열 변경이 아닐 수 있다.

  • 보관된 작업은 완료할 수 없다.
  • 프로젝트 구성원만 작업을 완료할 수 있다.
  • 완료한 사용자와 완료 시각을 기록해야 한다.
  • 승인 대상 작업은 검토 절차를 먼저 거쳐야 한다.

사용자의 의도를 직접 표현하면 서버가 적용할 규칙도 분명해진다.

await fetch("/api/tasks/task_42/complete", {
  method: "POST",
});

이 요청은 “상태 컬럼을 바꿔 달라”가 아니라 “이 작업을 완료해 달라”는 의미를 전달한다.

서버는 현재 작업 상태와 요청자의 권한을 확인한 뒤 완료가 가능한지 판단할 수 있다.

좋은 요청은 서버의 저장 구조를 원격으로 조작하지 않는다. 사용자가 서비스에서 수행하려는 행동을 표현한다.


요청에는 서버가 판단하는 데 필요한 맥락만 담아야 한다

새 작업을 만드는 요청을 생각해 보자.

await fetch("/api/tasks", {
  method: "POST",
  body: JSON.stringify({
    title: "Prepare sprint demo",
    status: "open",
    createdBy: "user_chris",
    createdAt: new Date().toISOString(),
  }),
});

모든 값을 클라이언트가 보내면 편해 보인다. 하지만 status, createdBy, createdAt은 서버가 결정하거나 신뢰할 수 있는 정보에서 가져와야 한다.

클라이언트가 보내야 하는 것은 사용자의 의도를 판단하는 데 필요한 입력이다.

type CreateTaskRequest = {
  title: string;
  assigneeId?: string;
  dueAt?: string;
};

await fetch("/api/tasks", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    title: "Prepare sprint demo",
    dueAt: "2026-09-18T07:00:00.000Z",
  } satisfies CreateTaskRequest),
});

작성자는 인증된 세션에서 확인할 수 있다. 생성 시각은 서버 시각을 사용하고, 초기 상태는 서비스 규칙으로 결정할 수 있다.

요청에서 필드를 줄이는 목적은 전송량을 조금 아끼는 데 있지 않다.

누가 어떤 값을 결정할 책임이 있는지 분명하게 만드는 데 있다.

결정할 책임
작업 제목사용자
담당자 후보사용자 요청 + 서버 권한 확인
작성자인증된 세션
생성 시각서버
초기 상태서비스 규칙
작업 ID서버 또는 데이터 저장 계층

요청에는 사용자가 선택할 수 있는 값만 담고, 서버가 소유해야 하는 사실은 서버에서 만든다.


URL, 메서드, 헤더와 본문은 서로 다른 질문에 답한다

HTTP 요청은 하나의 문자열이나 JSON 객체가 아니다.

await fetch("/api/projects/project_7/tasks", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    title: "Prepare sprint demo",
  }),
});

각 요소는 서로 다른 의미를 전달한다.

요소답하는 질문
URL어떤 리소스나 기능을 대상으로 하는가?
메서드어떤 종류의 행동을 요청하는가?
헤더요청을 어떻게 해석하고 처리해야 하는가?
본문행동을 수행하는 데 어떤 입력이 필요한가?
인증 정보누가 요청했는가?

모든 값을 본문에 넣을 수도 있다.

{
  "action": "create",
  "resource": "task",
  "projectId": "project_7",
  "contentType": "json",
  "userId": "user_chris"
}

그러나 이렇게 하면 전송 규약, 대상, 사용자 신원과 업무 입력의 경계가 흐려진다.

요청의 각 부분을 구분하면 서버는 공통 처리와 업무 처리를 나눌 수 있다. 인증 미들웨어는 신원을 확인하고, 요청 파서는 형식을 해석하며, 서비스 로직은 실제 작업 생성 규칙을 판단한다.

요청 형식은 단순한 문법이 아니다. 정보를 어느 책임에 배치할지 결정한 결과다.


클라이언트에서 만든 요청도 서버에 도착하면 외부 입력이다

프론트엔드에 타입을 정의했다고 생각해 보자.

type CreateTaskRequest = {
  title: string;
  dueAt?: string;
};

이 타입은 개발자가 올바른 요청을 작성하도록 돕는다. 그러나 실제 네트워크 요청을 보장하지는 않는다.

사용자는 개발자 도구나 별도의 프로그램으로 요청을 직접 만들 수 있다. 오래된 앱이 이전 형식의 요청을 보낼 수도 있고, 네트워크 중간의 데이터가 손상될 수도 있다.

서버는 도착한 값을 다시 검증해야 한다.

const createTaskSchema = z.object({
  title: z.string().trim().min(1).max(120),
  dueAt: z.string().datetime().optional(),
});

const parsed = createTaskSchema.safeParse(
  await request.json()
);

if (!parsed.success) {
  return Response.json(
    {
      ok: false,
      code: "INVALID_TASK_INPUT",
      fields: parsed.error.flatten().fieldErrors,
    },
    { status: 400 }
  );
}

클라이언트 검증은 빠른 피드백을 제공한다. 서버 검증은 시스템의 규칙과 데이터를 보호한다.

두 검증은 중복이 아니라 목적이 다르다.

요청은 우리 화면에서 출발했더라도 서버의 신뢰 경계를 통과하기 전까지 외부 입력이다.


같은 요청이 다시 도착할 수 있다는 사실까지 설계해야 한다

Chris가 작업 생성 버튼을 눌렀지만 응답을 받기 전에 네트워크가 끊겼다고 생각해 보자.

클라이언트는 작업이 생성되지 않았다고 판단하고 같은 요청을 다시 보낼 수 있다. 그러나 첫 번째 요청이 이미 서버에서 처리되었다면 작업이 두 개 생긴다.

await createTask({
  title: "Prepare sprint demo",
});

await createTask({
  title: "Prepare sprint demo",
});

요청의 재전송은 예외적인 상황이 아니다. 모바일 네트워크, 프록시, 타임아웃과 사용자의 연속 클릭 때문에 언제든 발생할 수 있다.

중복 수행이 위험한 기능은 요청을 식별할 수 있어야 한다.

const idempotencyKey = crypto.randomUUID();

await fetch("/api/tasks", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": idempotencyKey,
  },
  body: JSON.stringify({
    title: "Prepare sprint demo",
  }),
});

서버는 같은 키로 이미 처리한 요청이 있는지 확인한다.

const previous =
  await requestRepository.findByKey(
    currentUser.id,
    idempotencyKey
  );

if (previous) {
  return Response.json(previous.body, {
    status: previous.status,
  });
}

같은 요청을 여러 번 받아도 효과가 한 번만 발생하도록 만드는 성질을 멱등성이라고 한다.

모든 요청에 멱등성 키가 필요한 것은 아니다. 목록 조회처럼 반복해도 상태가 바뀌지 않는 요청은 보통 자연스럽게 다시 보낼 수 있다.

결제, 주문, 작업 생성처럼 중복 실행이 실제 문제를 만드는 기능은 재전송을 정상 흐름으로 보고 설계해야 한다.

클라이언트가 응답을 받지 못했다는 사실은 서버가 요청을 처리하지 않았다는 뜻이 아니다.


응답은 서버 데이터의 복사본이 아니라 처리 결과다

작업 생성 후 데이터베이스 엔티티를 그대로 반환할 수 있다.

return Response.json(taskRow);

그러나 클라이언트가 필요한 것은 내부 행 전체가 아니다.

요청이 성공했는지, 어떤 작업이 만들어졌는지, 화면을 어떤 상태로 갱신해야 하는지가 필요하다.

type CreateTaskResponse = {
  ok: true;
  task: {
    id: string;
    title: string;
    status: "open";
    assignee: {
      id: string;
      displayName: string;
    } | null;
    dueAt: string | null;
    createdAt: string;
  };
};

응답 모델은 저장 구조가 아니라 소비자의 다음 행동을 기준으로 만든다.

클라이언트는 반환된 작업을 목록에 추가하거나 상세 화면으로 이동할 수 있다. 서버가 정규화한 제목, 확정한 초기 상태와 생성한 ID도 즉시 반영할 수 있다.

응답에 데이터가 많을수록 친절한 것은 아니다. 내부 필드가 불필요하게 노출되고, 클라이언트가 우연히 그 구조에 의존할 수 있다.

좋은 응답은 처리 결과를 이해하고 다음 상태로 이동하는 데 필요한 사실을 전달한다.


전송 성공과 업무 성공은 같은 의미가 아니다

서버에서 JSON을 받았다고 해서 요청한 행동이 성공한 것은 아니다.

const response = await fetch(
  "/api/tasks/task_42/complete",
  { method: "POST" }
);

네트워크 연결은 성공했지만 서버가 작업 완료를 거절할 수 있다.

  • 사용자가 로그인하지 않았다.
  • 작업이 존재하지 않는다.
  • 다른 프로젝트의 작업이다.
  • 이미 보관된 작업이다.
  • 승인 절차가 끝나지 않았다.

HTTP 상태와 응답 본문은 서로 보완해야 한다.

return Response.json(
  {
    ok: false,
    code: "TASK_REQUIRES_APPROVAL",
    message: "A reviewer must approve this task first.",
  },
  { status: 409 }
);

HTTP 상태는 응답의 큰 범주를 전달한다. 업무 오류 코드는 서비스 안에서 무엇이 실패했는지 구체화한다.

if (response.status === 401) {
  redirectToLogin();
}

const result = await response.json();

if (
  !result.ok &&
  result.code === "TASK_REQUIRES_APPROVAL"
) {
  showApprovalRequiredMessage();
}

200, 400, 500만으로 모든 의미를 표현하려 하면 클라이언트가 메시지 문자열을 해석하게 된다.

반대로 모든 업무 상태를 HTTP 코드만으로 표현하려 하면 도메인의 구체적인 차이를 담기 어렵다.

전송 규약의 상태와 서비스의 결과를 함께 사용해야 한다.


실패 응답은 원인보다 다음 행동을 분명하게 해야 한다

다음 응답은 서버에서 문제가 발생했다는 사실만 알려준다.

{
  "error": "Something went wrong"
}

클라이언트는 입력을 고쳐야 하는지, 로그인해야 하는지, 최신 데이터를 다시 불러와야 하는지 알 수 없다.

실패를 클라이언트의 대응 방식에 따라 구분할 수 있다.

type CompleteTaskResult =
  | {
      ok: true;
      task: TaskSummary;
    }
  | {
      ok: false;
      code: "UNAUTHENTICATED";
    }
  | {
      ok: false;
      code: "FORBIDDEN";
    }
  | {
      ok: false;
      code: "TASK_NOT_FOUND";
    }
  | {
      ok: false;
      code: "TASK_ALREADY_CHANGED";
      currentTask: TaskSummary;
    }
  | {
      ok: false;
      code: "TEMPORARILY_UNAVAILABLE";
      retryAfterSeconds: number;
    };
오류 코드클라이언트의 다음 행동
UNAUTHENTICATED로그인 화면으로 이동한다
FORBIDDEN권한이 없음을 안내한다
TASK_NOT_FOUND목록에서 제거하거나 다시 조회한다
TASK_ALREADY_CHANGED최신 상태를 보여주고 사용자에게 확인받는다
TEMPORARILY_UNAVAILABLE안내된 시간 이후 재시도한다

내부 데이터베이스 오류나 스택 트레이스를 그대로 보내서는 안 된다. 클라이언트가 해결할 수 없고 시스템 구조나 민감한 정보만 노출할 수 있다.

실패 응답의 목적은 서버의 사정을 모두 설명하는 것이 아니다. 소비자가 안전한 다음 행동을 선택하도록 돕는 것이다.


응답은 요청 시점이 아니라 처리 완료 시점의 사실을 전달한다

Chris와 Victoria가 같은 작업을 동시에 수정한다고 생각해 보자.

Chris의 화면에는 작업 상태가 open으로 보인다. 그러나 요청이 서버에 도착하기 직전에 Victoria가 작업을 보관했을 수 있다.

await fetch("/api/tasks/task_42/complete", {
  method: "POST",
});

서버는 클라이언트 화면에 남아 있는 과거 상태가 아니라 현재 저장된 상태를 기준으로 판단해야 한다.

const task = await taskRepository.findById(taskId);

if (task.status === "archived") {
  return {
    ok: false,
    code: "TASK_ALREADY_CHANGED",
    currentTask: toTaskSummary(task),
  };
}

응답에는 서버가 최종적으로 확인하거나 변경한 사실을 담는다.

return {
  ok: true,
  task: {
    id: task.id,
    status: task.status,
    completedAt: task.completedAt?.toISOString(),
    version: task.version,
  },
};

클라이언트는 자신이 보낸 값을 그대로 화면에 믿고 표시하기보다 서버가 확정한 결과와 동기화할 수 있다.

요청은 사용자의 희망을 전달한다. 응답은 서버가 현재 상태와 규칙을 기준으로 확정한 사실을 전달한다.


모든 요청이 즉시 최종 결과를 반환할 수 있는 것은 아니다

작업 목록을 CSV로 내보내는 기능은 데이터가 많으면 몇 분이 걸릴 수 있다.

HTTP 연결을 계속 유지하며 기다리게 하면 타임아웃과 재시도 문제가 커진다.

서버는 요청을 접수한 뒤 처리 상태를 반환할 수 있다.

return Response.json(
  {
    ok: true,
    job: {
      id: "export_91",
      status: "queued",
      statusUrl: "/api/exports/export_91",
    },
  },
  { status: 202 }
);

클라이언트는 작업 상태를 조회한다.

const result = await fetch(
  "/api/exports/export_91"
).then((response) => response.json());

if (result.job.status === "completed") {
  showDownloadButton(result.job.downloadUrl);
}

이때 최초 응답은 내보내기가 끝났다는 뜻이 아니다. 서버가 요청을 받아 비동기 작업을 시작했다는 뜻이다.

응답은 항상 최종 데이터를 담아야 하는 것이 아니다. 현재까지 확정된 처리 상태와 이후 결과를 확인하는 방법을 정확히 알려주면 된다.


타임아웃은 실패가 아니라 결과를 모르는 상태일 수 있다

클라이언트가 10초 안에 응답을 받지 못해 타임아웃을 발생시켰다고 생각해 보자.

try {
  await createTask(input);
} catch (error) {
  showError("Task creation failed");
}

이 문구는 사실과 다를 수 있다.

클라이언트가 응답을 받지 못한 것뿐이며 서버에서는 작업이 생성되었을 수 있다.

따라서 네트워크 오류를 업무 실패로 즉시 단정하면 안 된다.

showWarning(
  "We could not confirm the result. Checking the latest task list."
);

await refetchTasks();

중복 생성이 위험하다면 같은 멱등성 키로 재시도하거나 요청 상태를 조회해야 한다.

요청과 응답 사이에는 다음 세 상태가 존재할 수 있다.

  1. 서버가 요청을 거절했다.
  2. 서버가 요청을 처리하고 결과를 응답했다.
  3. 클라이언트가 결과를 확인하지 못했다.

세 번째 상태를 첫 번째 상태와 같게 다루면 중복 실행이나 잘못된 오류 안내가 발생한다.

분산된 시스템에서 “응답을 받지 못했다”는 것은 “실패했다”가 아니라 “결과를 아직 모른다”일 수 있다.


요청과 응답에는 하나의 상호작용을 추적할 단서가 필요하다

사용자가 오류를 신고했는데 로그에는 수많은 작업 요청이 섞여 있을 수 있다.

요청마다 추적 ID를 부여하면 한 번의 흐름을 연결해서 볼 수 있다.

const requestId =
  request.headers.get("X-Request-Id") ??
  crypto.randomUUID();

logger.info({
  requestId,
  route: "/api/tasks",
  userId: currentUser.id,
});

응답에도 같은 식별자를 담을 수 있다.

return Response.json(result, {
  headers: {
    "X-Request-Id": requestId,
  },
});

추적 ID는 오류 내용을 사용자에게 전부 공개하지 않으면서도 운영자가 관련 로그를 찾게 돕는다.

다만 추적을 위해 요청 본문 전체를 무조건 기록해서는 안 된다. 비밀번호, 토큰, 개인정보와 같은 값은 제외하거나 마스킹해야 한다.

관측 가능성도 요청과 응답 설계의 일부다. 문제가 생긴 상호작용을 안전하게 찾을 수 있어야 운영 중 약속을 확인할 수 있다.


요청과 응답의 흐름은 책임의 경계를 보여준다

작업 완료 요청의 전체 흐름은 다음과 같다.

flowchart TD
    A[사용자 행동] --> B[요청 생성]
    B --> C[인증과 입력 검증]
    C --> D[서비스 규칙 판단]
    D --> E[상태 변경]
    E --> F[결과 응답]
    F --> G[화면과 로컬 상태 갱신]

각 단계는 다른 책임을 가진다.

단계핵심 책임
사용자 행동무엇을 하려는지 결정한다
요청 생성의도와 필요한 입력을 전송 가능한 형태로 만든다
인증과 검증신원과 외부 입력을 확인한다
서비스 규칙 판단현재 상태에서 행동이 가능한지 결정한다
상태 변경확정된 결과를 저장한다
결과 응답성공, 실패와 현재 사실을 전달한다
클라이언트 갱신응답을 근거로 다음 화면과 행동을 선택한다

프론트엔드가 업무 성공을 미리 결정해서도 안 되고, API 핸들러가 화면의 다음 동작까지 결정해서도 안 된다.

요청과 응답은 각 책임을 하나로 섞는 구조가 아니다. 서로 다른 책임이 같은 상호작용의 의미를 이어받도록 만드는 경계다.


요청과 응답을 설계하기 전에 물어봐야 할 질문

요청이 사용자의 의도를 표현하는가

  1. 사용자는 어떤 상태 변경이나 조회를 원하는가?
  2. 요청이 업무 행동이 아니라 데이터베이스 필드 변경을 노출하고 있지는 않은가?
  3. 서버가 판단하는 데 필요한 입력만 포함되어 있는가?
  4. 서버가 결정해야 할 값을 클라이언트가 보내고 있지는 않은가?
  5. URL, 메서드, 헤더와 본문의 책임이 구분되어 있는가?

외부 입력을 안전하게 다루는가

  1. 필수 값, 허용 범위와 형식이 명확한가?
  2. 프론트엔드 타입과 별도로 서버 검증이 존재하는가?
  3. 사용자 신원을 요청 본문이 아니라 신뢰할 수 있는 인증 정보에서 가져오는가?
  4. 허용되지 않은 추가 필드를 무시하거나 거절하는 정책이 있는가?
  5. 민감한 값이 로그에 그대로 남지 않는가?

재전송과 동시성을 고려했는가

  1. 같은 요청이 두 번 도착하면 어떤 결과가 발생하는가?
  2. 중복 실행이 위험한 기능에 멱등성 전략이 있는가?
  3. 응답을 받지 못했을 때 안전하게 결과를 확인할 수 있는가?
  4. 서버가 최신 상태를 기준으로 규칙을 다시 검사하는가?
  5. 다른 사용자의 변경과 충돌했을 때 현재 상태를 전달하는가?

응답이 다음 행동을 안내하는가

  1. 처리 성공 여부와 확정된 결과가 명확한가?
  2. HTTP 상태와 업무 오류 코드가 일관되게 사용되는가?
  3. 실패 유형에 따라 클라이언트가 다른 행동을 선택할 수 있는가?
  4. 비동기 처리라면 현재 상태와 결과 확인 방법을 알려주는가?
  5. 내부 구현이나 민감한 오류 정보가 노출되지 않는가?

운영 중 상호작용을 확인할 수 있는가

  1. 요청과 응답을 연결할 추적 식별자가 있는가?
  2. 처리 시간과 실패 유형을 관찰할 수 있는가?
  3. 로그에 민감한 입력이 제거되어 있는가?
  4. 타임아웃을 업무 실패와 구분할 수 있는가?
  5. 실제 응답이 문서화된 구조와 의미를 지키는가?

흔한 실수는 요청과 응답의 의미를 잃게 만든다

요청 본문에 모든 값을 넣는다

{
  "action": "complete",
  "taskId": "task_42",
  "userId": "user_chris",
  "completedAt": "2026-09-14T01:00:00.000Z"
}

대상, 신원, 사용자 입력과 서버가 결정할 사실의 경계가 사라진다.

각 값의 책임에 따라 URL, 인증 정보, 본문과 서버 로직으로 나눈다.


프론트엔드 타입을 서버 검증으로 착각한다

const input = body as CreateTaskRequest;

타입 단언은 실제 값을 검사하지 않는다.

서버의 신뢰 경계에서 런타임 스키마로 형식과 허용 범위를 검증한다.


응답을 받지 못하면 요청이 실패했다고 단정한다

catch {
  await createTask(input);
}

첫 요청이 이미 처리되었다면 중복 데이터가 생길 수 있다.

멱등성 키를 재사용하거나 최신 상태를 조회해 결과를 확인한다.


모든 성공에 빈 응답만 반환한다

return new Response(null, { status: 200 });

클라이언트는 서버가 확정한 ID, 상태와 시각을 알 수 없어 별도 조회가 필요해질 수 있다.

다음 화면을 정확히 갱신하는 데 필요한 결과를 반환한다.


모든 실패를 500으로 반환한다

return Response.json(
  { error: "Failed" },
  { status: 500 }
);

잘못된 입력, 권한 부족, 상태 충돌과 서버 장애를 구분할 수 없다.

전송 상태와 안정적인 업무 오류 코드를 사용해 소비자의 대응 방식을 구분한다.


데이터베이스 객체를 그대로 응답한다

return Response.json(taskEntity);

내부 컬럼과 관계가 외부 의존성이 되고 민감한 값이 노출될 수 있다.

처리 결과와 다음 행동에 필요한 공개 응답 모델로 변환한다.


요청과 응답의 핵심은 상호작용의 의미에 있다

요청은 클라이언트가 서버에 데이터를 보내는 것이고, 응답은 서버가 결과를 돌려주는 것이다.

const response = await fetch("/api/tasks");
const tasks = await response.json();

이 정의는 웹 통신의 방향을 이해하기에 충분하다.

그러나 실제 서비스에서 요청과 응답을 설계할 때는 다음을 함께 결정해야 한다.

  • 사용자가 요청하는 행동과 의도는 무엇인가?
  • 그 행동을 판단하는 데 어떤 입력과 맥락이 필요한가?
  • 어떤 값은 클라이언트가 보내고 어떤 값은 서버가 결정하는가?
  • 외부 입력을 어디에서 어떤 규칙으로 검증하는가?
  • 같은 요청이 다시 도착해도 안전한가?
  • 응답은 서버가 확정한 어떤 사실을 전달하는가?
  • 실패한 소비자가 어떤 다음 행동을 선택할 수 있는가?
  • 응답을 받지 못했을 때 결과를 어떻게 확인하는가?
  • 오래 걸리는 처리는 현재 상태와 완료 방법을 어떻게 알리는가?
  • 운영 중 하나의 상호작용을 어떻게 추적하는가?

좋은 요청은 데이터베이스 변경 명령을 그대로 보내지 않는다. 사용자가 원하는 행동과 서버의 판단에 필요한 입력을 전달한다.

좋은 응답은 내부 데이터의 복사본을 무조건 반환하지 않는다. 서버가 현재 상태와 규칙을 기준으로 판단한 결과를 알려주고, 클라이언트가 다음 흐름을 안전하게 선택하게 한다.

요청을 보냈지만 응답을 받지 못할 수도 있고, 전송은 성공했지만 업무 규칙 때문에 행동은 거절될 수도 있다. 처리가 접수되었지만 아직 끝나지 않았을 수도 있다.

따라서 요청과 응답을 설계한다는 것은 JSON의 모양만 정하는 일이 아니다.

결국 다음 질문에 답하는 일이다.

사용자가 원하는 행동을 서버가 오해 없이 판단하고, 그 결과를 클라이언트가 다음 행동으로 이어갈 수 있게 하려면 무엇을 주고받아야 하는가?

요청과 응답은 데이터를 주고받는 형식이 아니다.

서로 다른 시스템이 하나의 상호작용을 같은 의미로 완성하기 위한 대화다.

profile
Vision eXperience Developer

0개의 댓글