Web API 설계
REST 원칙과 상태 코드를 사용합니다.
주소는 대상, 메서드는 동작
2.1부터 여기까지 만든 주소를 다시 보면 규칙이 하나 있습니다. 주소에는 무엇을 다루는지만 적었고, 무엇을 할지는 메서드로 적었습니다. 이것이 REST 라고 부르는 방식의 뼈대입니다.
GET /getTodos GET /getTodoById?id=1 POST /createTodo POST /deleteTodo?id=1
GET /todos GET /todos/1 POST /todos DELETE /todos/1
오른쪽은 주소를 외우지 않아도 짐작할 수 있습니다. 새 대상이 늘어도 같은 규칙을 따르므로 문서를 덜 읽어도 됩니다. 왼쪽은 이름을 하나씩 알아야 합니다.
| 메서드 | 하는 일 | 여러 번 불러도 같은가 |
|---|---|---|
| GET | 읽습니다. 바꾸지 않습니다. | 같습니다 |
| POST | 새로 만듭니다. | 다릅니다 — 부를 때마다 생깁니다 |
| PUT | 통째로 바꿉니다. | 같습니다 |
| PATCH | 일부만 바꿉니다. | 대개 같습니다 |
| DELETE | 지웁니다. | 같습니다 — 이미 지운 것을 또 지워도 결과는 없어진 상태입니다 |
마지막 칸이 중요합니다. 네트워크가 끊겨 부르는 쪽이 다시 보낼 수 있기 때문입니다. GET·PUT·DELETE 는 다시 보내도 되지만 POST 는 그렇지 않습니다.
상태 코드로 결과를 말합니다
본문에 "성공": true 같은 것을 담지 않습니다. 성공했는지는 상태 코드가 이미 말하고 있습니다. 부르는 쪽은 본문을 열어 보지 않고도 처리 방향을 정할 수 있습니다.
| 코드 | 언제 | 돌려주는 것 |
|---|---|---|
| 200 OK | 읽기·고치기가 잘 됐습니다. | 결과 |
| 201 Created | 새로 만들었습니다. | 만든 것 + Location 머리글 |
| 204 No Content | 됐는데 돌려줄 것이 없습니다. | 없음 |
| 400 Bad Request | 보낸 것이 잘못됐습니다. | 어디가 틀렸는지 |
| 401 Unauthorized | 누구인지 모릅니다. | — |
| 403 Forbidden | 누구인지는 알지만 권한이 없습니다. | — |
| 404 Not Found | 그런 것이 없습니다. | — |
| 409 Conflict | 지금 상태와 맞지 않습니다. | 까닭 |
| 500 Server Error | 서버가 잘못했습니다. | 추적 번호 |
401과 403을 자주 섞습니다. 로그인하지 않았으면 401, 로그인했는데 남의 것을 건드리려 하면 403 입니다. 4.2에서 다시 봅니다.
[HttpPost] public ActionResult<Todo> Add(Todo todo) { if (_todos.Any(t => t.Id == todo.Id)) return Conflict(new ProblemDetails { Title = "이미 있는 번호입니다.", Status = 409 }); _todos.Add(todo); return CreatedAtAction(nameof(Get), new { id = todo.Id }, todo); }
이 코드는 서버가 있어야 하므로 브라우저에서 실행할 수 없습니다.
PUT /todos 가 404가 아니라 405 인 것을 보십시오. 주소는 있는데 그 메서드를 받는 자리가 없다는 뜻입니다. 부르는 쪽이 주소를 잘못 적은 것인지 메서드를 잘못 선택한 것인지 구분할 수 있습니다.
오류는 정해진 모양으로 돌려줍니다
오류마다 본문의 모양이 다르면 받는 쪽이 자리마다 다르게 읽어야 합니다. ASP.NET Core 는 ProblemDetails 라는 정해진 모양을 사용합니다. 표준(RFC 9457)에 적힌 것이라 다른 언어로 만든 것과도 맞습니다.
// GET /todos/99 Content-Type: application/problem+json { "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5", "title": "Not Found", "status": 404, "traceId": "00-ce0eb0fcc66f4ae15dcee974c62eeab7-…" }
500의 본문에 예외 메시지가 없습니다. 일부러 낸 예외의 문구는 "일부러 낸 오류입니다." 였는데 나가지 않았습니다. 예외 메시지에는 파일 경로나 연결 문자열이 섞일 수 있어 밖으로 내보내지 않는 것이 기본입니다.
대신 traceId 가 있습니다. 이 번호를 서버 기록에서 찾으면 그 요청에서 무엇이 있었는지 알 수 있습니다. 사용자에게 이 번호를 보여 주고 문의할 때 적어 달라고 하면 원인을 훨씬 빨리 찾습니다.
이 모양을 켜려면 Program.cs 에 세 줄이 필요합니다.
builder.Services.AddProblemDetails(); // Build() 앞 app.UseExceptionHandler(); // 뒤 — 예외를 500 으로 바꿉니다 app.UseStatusCodePages(); // 뒤 — 본문 없는 응답에 모양을 채웁니다
직접 만들어 돌려줄 수도 있습니다. 위 409가 그렇게 한 것입니다.
Detail 에 사람이 읽을 설명을 담고, 표준에 없는 값은
Extensions 에 넣습니다.
날짜는 어떤 글자로 나가나
API 를 설계할 때 자주 어긋나는 자리입니다. C# 의 날짜 형식이 여럿인데 JSON 으로 나갈 때의 모양이 저마다 다릅니다. 아래를 실행해 확인해보십시오.
using System.Text.Json; var utc = new DateTime(2026, 8, 29, 13, 59, 56, DateTimeKind.Utc); var 모름 = new DateTime(2026, 8, 29, 13, 59, 56, DateTimeKind.Unspecified); var 오프셋 = new DateTimeOffset(2026, 8, 29, 13, 59, 56, TimeSpan.FromHours(9)); var 날짜 = new DateOnly(2026, 8, 29); Console.WriteLine(JsonSerializer.Serialize(utc)); Console.WriteLine(JsonSerializer.Serialize(모름)); Console.WriteLine(JsonSerializer.Serialize(오프셋)); Console.WriteLine(JsonSerializer.Serialize(날짜));
코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.
둘째 줄에 시간대가 없습니다. 받는 쪽은 이것이 서울 시각인지
런던 시각인지 알 수 없습니다. DateTime 을 그냥 만들면
Unspecified 가 되므로 이 모양으로 나갑니다.
- 시각을 주고받는다면
DateTimeOffset을 사용하거나DateTime을 UTC 로 맞춥니다. - 날짜만 뜻한다면
DateOnly를 사용합니다. 생일이나 마감일에 시각이 붙으면 시간대에 따라 하루가 밀립니다.
ProblemDetails 와 DateOnly
AddProblemDetails() 는 .NET 7에서 들어왔습니다.
그 전에는 400 응답에만 이 모양이 붙고 404·500 은 본문이 비어 있어, 받는
쪽이 자리마다 다르게 읽어야 했습니다.
DateOnly 와
TimeOnly 는 .NET 6에서 들어왔습니다. 다만
JSON 으로 주고받을 수 있게 된 것은 .NET 7 부터입니다.
.NET 6에서는 변환기를 직접 만들어 붙여야 했습니다.
직접 해보기
아래 주소를 대상 중심으로 고쳐보세요. 메서드도 함께 적습니다.
POST /markTodoDone?id=1 POST /searchTodos?keyword=우유
아래 넷에 어떤 코드를 돌려줄지 적어보세요.
① 할일을 지웠는데 돌려줄 것이 없습니다. ② 로그인한 사람이 남의 할일을 지우려 합니다. ③ 이미 끝난 할일을 다시 끝내려 합니다. ④ 지우려는 번호가 없습니다.
마감일을 DateTime 으로 두고 서울에서
2026-09-01 로 저장했습니다. 이것이 런던에 있는
사용자에게 8월 31일로 보이는 과정을 적어보세요.
- 주소에는 대상만 적고 동작은 메서드로 적습니다.
- 성공 여부는 본문이 아니라 상태 코드가 말합니다. 401과 403은 다릅니다.
- 주소는 있는데 메서드가 없으면 404가 아니라 405 입니다.
- 오류는
AddProblemDetails()로 모양을 하나로 맞춥니다. 500 은 예외 메시지 대신 traceId 를 돌려줍니다. - 시각은
DateTimeOffset·UTC 로, 날짜는DateOnly로 주고받습니다.