[RestSharp 실전 시리즈] 2편. POST · PUT · DELETE와 JWT 인증
실무형 RestSharp API 통신 패턴 정리
시리즈 전체 목차 1편. RestSharp 기본 설정 · GET 요청 · 서버 응답 구조 2편. POST · PUT · DELETE와 JWT 인증 ← 현재 글 3편. 에러 처리와 그리드 CRUD 저장 패턴
1. 소개
1편에서 GET 요청으로 데이터를 읽어오는 방법을 다뤘는데, 실무에서는 쓰기 작업이 훨씬 복잡해요. 데이터를 Body에 담아 보내야 하고, 인증 토큰도 매 요청에 붙여야 하고, 등록인지 수정인지에 따라 메서드도 달라지죠.
이번 편에서는 그 부분을 다뤄요.
- JWT 토큰 발급 → 전역 보관 → 모든 요청에 헤더로 추가
- POST · PUT · DELETE 각각의 패턴
- 하나의 Save 버튼으로 등록/수정을 처리하는 통합 패턴
- 여러 테이블 데이터를 한 번에 묶어서 보내는 복합 요청
1편의 서버 응답 규격(scs, list, data, code, msg)을 그대로 사용해요.
2. 기본 사용 방법
JWT 인증 헤더 추가
RestSharp에서 요청 헤더는 AddHeader()로 추가해요.
// Bearer 방식 (일반적인 JWT 표준)
req.AddHeader("Authorization", "Bearer " + token);
// 커스텀 헤더 방식 (서버 구현에 따라 다름)
req.AddHeader("X-AUTH-TOKEN", token);
Body 전송 방식 두 가지
// 방식 1: AddStringBody — 문자열을 그대로 Body로 전송
req.AddStringBody(JsonConvert.SerializeObject(dto), ContentType.Json);
// 방식 2: AddBody — 객체 또는 문자열을 Body로 전송
req.AddBody(jObject.ToString(), ContentType.Json);
두 방식 모두 동작은 같아요. 실무에서는 상황에 따라 구분해서 쓰는 게 가독성에 좋아요.
| 메서드 | 언제 쓰나 |
|---|---|
AddStringBody |
DTO를 직렬화한 문자열을 전송할 때 |
AddBody |
JObject처럼 이미 조립된 JSON을 전송할 때 |
3. 실무 적용 방법
로그인 — 토큰 발급
모든 API 요청에 토큰을 붙이려면 먼저 토큰을 발급받아야 해요. 로그인 요청만 헤더 없이 날리고, 응답에서 받은 토큰을 전역에 보관해요.
public class LoginDto
{
public string username { get; set; }
public string password { get; set; }
}
public class TokenResponseDto
{
public string token { get; set; }
public string expires { get; set; }
}
private void Login(string username, string password)
{
RestClient client = new RestClient("https://api.example.com");
RestRequest req = new RestRequest("/auth/login", Method.Post);
JObject param = new JObject();
param.Add("username", username);
param.Add("password", password);
req.AddStringBody(param.ToString(), ContentType.Json);
// 로그인 요청은 인증 헤더 없음
RestResponse response = client.Execute(req);
if (response.IsSuccessful)
{
JObject jObject = JObject.Parse(response.Content);
if ((bool)jObject["scs"])
{
TokenResponseDto tokenDto = JsonConvert.DeserializeObject<TokenResponseDto>(
jObject["data"].ToString());
// 전역에 저장 — 이후 모든 요청에서 사용
AppSession.Token = tokenDto.token;
MessageBox.Show("로그인 성공");
}
else
{
MessageBox.Show(jObject["msg"].ToString());
}
}
}
POST — 단순 등록
private void InsertProduct()
{
ProductDto dto = new ProductDto
{
name = txtName.Text,
price = int.Parse(txtPrice.Text),
category = cboCategory.SelectedValue.ToString()
};
RestClient client = new RestClient("https://api.example.com");
RestRequest req = new RestRequest("/products", Method.Post);
req.AddStringBody(JsonConvert.SerializeObject(dto), ContentType.Json);
req.AddHeader("Authorization", "Bearer " + AppSession.Token);
RestResponse response = client.Execute(req);
if (response.IsSuccessful)
{
JObject jObject = JObject.Parse(response.Content);
if ((bool)jObject["scs"])
{
MessageBox.Show("등록되었습니다.");
SearchProducts(); // 목록 새로고침
}
else
{
MessageBox.Show(jObject["msg"].ToString());
}
}
}
POST — 복합 데이터 전송 (JObject 조합)
상품 정보와 관련 이미지, 옵션 목록을 한 번의 요청으로 묶어서 보낼 때 JObject를 조합하는 패턴이에요.
private void SaveProductWithOptions()
{
RestClient client = new RestClient("https://api.example.com");
RestRequest req = new RestRequest("/products/detail", Method.Post);
JObject reqParam = new JObject();
reqParam.Add("product", JsonConvert.SerializeObject(productDto));
reqParam.Add("insertOptions", JsonConvert.SerializeObject(insertOptionList));
reqParam.Add("updateOptions", JsonConvert.SerializeObject(updateOptionList));
reqParam.Add("deleteOptions", JsonConvert.SerializeObject(deleteOptionList));
req.AddBody(reqParam.ToString(), ContentType.Json);
req.AddHeader("Authorization", "Bearer " + AppSession.Token);
RestResponse response = client.Execute(req);
// ... 응답 처리
}
서버로 전송되는 JSON:
{
"product": { "productId": -1, "name": "신상품", "price": 15000 },
"insertOptions": [ { "optionName": "색상", "optionValue": "빨강" } ],
"updateOptions": [ { "optionId": 5, "optionValue": "파랑" } ],
"deleteOptions": [ { "optionId": 3 } ]
}
등록/수정 통합 Save 패턴
실무에서 폼 화면은 "신규 등록"과 "기존 데이터 수정"이 같은 화면인 경우가 많아요. 저장 버튼 하나로 두 케이스를 처리하는 패턴이에요.
private void btnSave_Click(object sender, EventArgs e)
{
RestClient client = new RestClient("https://api.example.com");
RestRequest req;
// ID가 0 이상이면 수정(PUT), 음수 초기값이면 신규 등록(POST)
if (_dto.productId >= 0)
req = new RestRequest("/products", Method.Put);
else
req = new RestRequest("/products", Method.Post);
req.AddStringBody(JsonConvert.SerializeObject(_dto), ContentType.Json);
req.AddHeader("Authorization", "Bearer " + AppSession.Token);
RestResponse response = client.Execute(req);
if (response.IsSuccessful)
{
JObject jObject = JObject.Parse(response.Content);
if ((bool)jObject["scs"])
{
MessageBox.Show("저장되었습니다.");
SearchProducts();
}
else
{
MessageBox.Show(jObject["msg"].ToString());
}
}
}
PUT — 수정
private void UpdateProduct(ProductDto dto)
{
RestClient client = new RestClient("https://api.example.com");
RestRequest req = new RestRequest("/products", Method.Put);
req.AddStringBody(JsonConvert.SerializeObject(dto), ContentType.Json);
req.AddHeader("Authorization", "Bearer " + AppSession.Token);
RestResponse response = client.Execute(req);
if (response.IsSuccessful)
{
JObject jObject = JObject.Parse(response.Content);
if ((bool)jObject["scs"])
MessageBox.Show("수정되었습니다.");
else
MessageBox.Show(jObject["msg"].ToString());
}
}
DELETE — 단일 삭제
DELETE에서도 Body를 전송할 수 있어요. URL 경로에 ID를 포함하는 방식 외에, DTO로 추가 정보를 같이 보내야 할 때 Body를 사용해요.
private void DeleteProduct(ProductDto dto)
{
RestClient client = new RestClient("https://api.example.com");
RestRequest req = new RestRequest("/products", Method.Delete);
req.AddBody(JsonConvert.SerializeObject(dto), ContentType.Json);
req.AddHeader("Authorization", "Bearer " + AppSession.Token);
RestResponse response = client.Execute(req);
if (response.IsSuccessful)
{
JObject jObject = JObject.Parse(response.Content);
if ((bool)jObject["scs"])
MessageBox.Show("삭제되었습니다.");
else
MessageBox.Show(jObject["msg"].ToString());
}
}
DELETE — 다중 삭제 (ID 목록 전송)
그리드에서 체크된 여러 행을 한 번에 삭제할 때 ID 목록을 배열로 전송해요.
private void DeleteSelectedProducts()
{
List<long> deleteIds = new List<long>();
for (int i = 0; i < gridView.RowCount; i++)
{
if ((bool)gridView.GetRowCellValue(i, "checked"))
deleteIds.Add((long)gridView.GetRowCellValue(i, "productId"));
}
if (deleteIds.Count == 0)
{
MessageBox.Show("삭제할 항목을 선택해주세요.");
return;
}
RestClient client = new RestClient("https://api.example.com");
RestRequest req = new RestRequest("/products/batch", Method.Delete);
req.AddStringBody(JsonConvert.SerializeObject(deleteIds), ContentType.Json);
req.AddHeader("Authorization", "Bearer " + AppSession.Token);
RestResponse response = client.Execute(req);
// ... 응답 처리
}
서버로 전송되는 Body:
[1001, 1002, 1003]
4. 주의사항
주의 1 — ID 초기값은 왜 음수(-1)를 쓰나요
Save 통합 패턴에서 productId >= 0으로 신규/수정을 구분했는데, 처음 보면 의아할 수 있어요.
0을 쓰면 안 되는 이유가 있어요. DB의 AUTO_INCREMENT ID는 보통 1부터 시작하지만, 데이터에 따라 0이 유효한 ID 값으로 쓰이는 경우도 있거든요. 반면 음수는 실제 DB ID로 절대 나올 수 없으니 "아직 저장 안 된 신규 레코드"를 표현하는 값으로 안전해요.
// DTO 초기화 시
ProductDto _dto = new ProductDto { productId = -1 }; // 신규 상태
// 기존 데이터 로드 시
_dto = JsonConvert.DeserializeObject<ProductDto>(jObject["data"].ToString()); // productId가 실제 값으로 채워짐
주의 2 — DELETE에서 Body 전송이 안 된다고 느낄 때
HTTP 표준에서 DELETE에 Body를 포함하는 건 원래 정의되지 않아서, 일부 서버 프레임워크나 중간 프록시에서 무시하거나 거부하는 경우가 있어요.
서버가 Spring/Express 같은 일반적인 프레임워크라면 대부분 잘 받아요. 하지만 혹시 DELETE Body가 안 받아진다면 서버 설정을 확인해 보는 게 먼저예요. 클라이언트(RestSharp) 쪽에서는 정상 전송하고 있어요.
// 확실히 하려면 URL에 ID 포함하는 방식이 더 표준적
RestRequest req = new RestRequest("/products/" + productId, Method.Delete);
// Body 없이 URL로만 처리
주의 3 — 토큰 만료를 무시하면 안 돼요
토큰을 전역에 보관해서 재사용하는 방식이다 보니, 오래된 토큰으로 요청을 계속 보내는 경우가 생길 수 있어요. 서버에서 scs: false와 인증 에러 코드를 내려보내줬을 때 로그인 화면으로 되돌리는 처리를 꼭 해야 해요. 이 부분은 3편 에러 처리에서 자세히 다뤄요.
5. 성능 / 구조 팁
공통 헤더는 한 곳에서 처리하세요
매 요청마다 req.AddHeader("Authorization", ...) 를 반복하는 건 실수할 여지가 많아요. RestClient의 DefaultParameters에 등록해 두면 모든 요청에 자동으로 붙어요.
RestClientOptions options = new RestClientOptions("https://api.example.com");
RestClient client = new RestClient(options);
// 모든 요청에 자동으로 붙는 기본 헤더
client.AddDefaultHeader("Authorization", "Bearer " + AppSession.Token);
// 이후 요청에서는 AddHeader 불필요
RestRequest req = new RestRequest("/products", Method.Post);
req.AddStringBody(JsonConvert.SerializeObject(dto), ContentType.Json);
// Authorization 헤더는 자동으로 포함됨
단, 토큰이 갱신될 때마다 RestClient 인스턴스도 다시 만들어야 해요. 요청마다 새로 만드는 방식이라면 매번 헤더 추가도 크게 문제없어요.
복합 요청은 단위 테스트가 중요해요
JObject를 직접 조립해서 보내는 복합 요청은 오타 하나로 서버에서 null 오류가 날 수 있어요. JSON 키 이름을 서버 API 스펙과 정확히 맞추고, 가능하면 단위 테스트로 JSON 구조를 검증해 두는 게 좋아요.
// 키 이름을 상수로 관리하면 실수를 줄일 수 있어요
private const string KEY_PRODUCT = "product";
private const string KEY_INSERT_OPTIONS = "insertOptions";
private const string KEY_UPDATE_OPTIONS = "updateOptions";
private const string KEY_DELETE_OPTIONS = "deleteOptions";
JObject reqParam = new JObject();
reqParam.Add(KEY_PRODUCT, JsonConvert.SerializeObject(productDto));
reqParam.Add(KEY_INSERT_OPTIONS, JsonConvert.SerializeObject(insertOptionList));
6. 정리
- JWT 토큰은 로그인 후 전역에 보관하고, 모든 요청에서
AddHeader()로 추가해요 - Body 전송은
AddStringBody(DTO 직렬화) 또는AddBody(JObject 조합) 둘 다 사용 가능해요 - 신규/수정 통합 Save 패턴은 ID 초기값을 음수(-1)로 두고
>= 0조건으로 분기해요 - 복수 테이블 데이터는 JObject로 묶어서 한 번의 POST로 전송할 수 있어요
- DELETE에도 Body 전송이 가능하지만, 서버 설정에 따라 동작이 달라질 수 있어요
다음 편 예고
3편은 이 시리즈의 마무리예요. 솔직히 가장 중요한 내용이기도 해요. HTTP 실패와 비즈니스 실패를 나눠서 처리하는 2단계 에러 처리 패턴, 그리드에서 변경된 행만 골라서 insert/update/delete로 분류한 다음 한 번에 서버로 보내는 배치 저장 패턴까지 다뤄요. 이 두 패턴을 알면 실무 화면의 80%는 커버할 수 있어요.
시리즈 전체 목차 1편. RestSharp 기본 설정 · GET 요청 · 서버 응답 구조 2편. POST · PUT · DELETE와 JWT 인증 ← 현재 글 3편. 에러 처리와 그리드 CRUD 저장 패턴
'Backend > C#' 카테고리의 다른 글
| [RestSharp 실전 시리즈] 1편. 기본 설정 · GET 요청 · 서버 응답 구조 (0) | 2026.06.02 |
|---|---|
| DevExpress XtraGrid - 엔터 키로 다음 셀 이동하기 (0) | 2026.05.21 |