오래된 VB.NET 프로젝트를 열어 보면 웹 요청은 대부분 HttpWebRequest로 짜여 있습니다. 동작은 하지만 요청 하나 보내는 데 스트림 열고, 헤더 넣고, 응답 스트림 읽고, 닫는 코드가 줄줄이 따라옵니다. .NET 쪽에서도 이제는 HttpWebRequest를 새 코드에 쓰지 말라고 권하고 있어서, 새로 짜거나 손볼 일이 생기면 다른 도구로 옮기는 게 낫습니다. 많이 쓰는 후보들을 정리해 둡니다.
HttpClient - 기본으로 들어 있는 표준
System.Net.Http.HttpClient는 지금 .NET에서 기본으로 권장하는 HTTP 클라이언트입니다. 비동기 메서드가 중심이고 헤더, 쿠키, 인증을 다룰 수 있으며 코드도 HttpWebRequest보다 훨씬 짧습니다.
Dim client As New HttpClient()
Dim response = Await client.GetAsync("https://example.com")
Dim content = Await response.Content.ReadAsStringAsync()
Await를 쓰니 이 코드는 Async 메서드 안에 있어야 합니다. 그리고 많이들 하는 실수가 요청할 때마다 New HttpClient()를 만드는 겁니다. HttpClient는 내부 연결을 재사용하도록 만들어져 있어서, 매번 새로 만들고 버리면 요청이 많을 때 소켓이 고갈될 수 있습니다. 클래스 수준에 Shared로 하나 두고 계속 쓰는 게 기본 사용법입니다.
.NET Framework 4.x에서 오래된 사이트와 통신이 안 되면 TLS 버전 문제인 경우가 많습니다. 이럴 땐 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12를 요청 전에 한 번 지정해 주면 대부분 해결됩니다.
RestSharp - REST API 호출에 특화
JSON API를 많이 호출한다면 RestSharp가 편합니다. 요청 객체에 헤더, 파라미터, 본문을 붙여 가는 방식이고 JSON 직렬화와 인증 처리가 들어 있습니다. NuGet 패키지 관리자 콘솔에서 설치합니다.
Install-Package RestSharp
Dim client = New RestClient("https://example.com")
Dim request = New RestRequest("endpoint", Method.GET)
request.AddHeader("User-Agent", "...")
Dim response = client.Execute(request)
이 예제는 예전 버전의 문법입니다. 최근 버전에서는 열거형이 Method.Get처럼 바뀌었고 ExecuteAsync 같은 비동기 메서드 위주로 정리됐습니다. 블로그 예제를 가져다 쓰는데 컴파일이 안 된다면 대부분 버전 차이 때문이니, 설치된 RestSharp 버전부터 확인해 보세요.
Flurl.Http - 체이닝으로 짧게
Flurl.Http는 HttpClient를 감싸서 URL 문자열에 바로 메서드를 이어 붙이는 문법을 제공합니다. 헤더와 쿼리 파라미터를 한 줄로 쌓아 갈 수 있어 읽기 좋습니다.
Install-Package Flurl.Http
Dim json = Await "https://example.com"
.WithHeader("User-Agent", "...")
.GetStringAsync()
C# 예제를 옮긴 형태라, VB.NET에서는 줄 앞에 점이 오는 연속이 컴파일 오류를 낼 수 있습니다. 그럴 땐 각 줄 끝에 _를 붙이거나 한 줄로 이어 쓰면 됩니다. 문자열에 확장 메서드가 붙는 구조라 Imports Flurl.Http도 잊지 말아야 합니다.
WebClient - 간단하지만 구식
System.Net.WebClient는 .NET에 기본으로 들어 있고, 파일 하나 내려받거나 페이지 내용을 문자열로 가져오는 정도라면 이게 제일 짧습니다.
Dim client As New WebClient()
Dim content As String = client.DownloadString("https://example.com")
대신 동기식이 기본이고 쿠키나 헤더를 세밀하게 다루기는 불편합니다. 이것도 지금은 레거시로 분류되니 간단한 도구 정도에만 쓰는 게 좋습니다.
그 밖에 Edge.js로 VB.NET에서 Node.js를 불러 axios나 puppeteer를 쓰는 방법도 있습니다. 자바스크립트로 렌더링되는 페이지처럼 실제 브라우저가 필요한 경우에 고려해 볼 수 있지만, 구성이 복잡해서 일반적인 요청에는 과합니다.
| 목적 | 추천 |
|---|---|
| REST API를 많이 호출 | RestSharp, Flurl |
| 브라우저처럼 세밀하게 제어 | curl, puppeteer |
| 비동기, 표준 HTTP | HttpClient |
| 단순 동기 요청 | WebClient, HttpWebRequest |
MSXML2.ServerXMLHTTP.6.0만 403을 받은 경우
VBA 시절 습관으로 VB.NET에서도 CreateObject("MSXML2.ServerXMLHTTP.6.0")을 쓰는 경우가 있습니다. 아래는 네이버 지도 검색 응답(JSON)을 받아 파일로 저장하던 함수입니다.
Private Function xWinHttp(Optional mesode As String = "GET", Optional URL As String = Nothing, Optional Hadders As String = Nothing, Optional enQuery As String = "") As Object
Dim T As String = "", O As Object
Do
If Not isWork Then Return Nothing
If isDelay AndAlso delayValue > 0 Then
program1472.Functions.Time.Delay(delayValue)
End If
Dim agent As String = userAgent()
Try
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12
Static xhr As Object = CreateObject("MSXML2.ServerXMLHTTP.6.0")
xhr.Open(mesode.ToUpper(), URL, False)
xhr.setOption(2, 13056)
xhr.setRequestHeader("User-Agent", agent)
xhr.setRequestHeader("Accept", "application/json, text/plain, */*")
xhr.setRequestHeader("Accept-Language", "ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4")
xhr.setRequestHeader("Referer", $"https://map.naver.com/p/search/{enQuery}?c=15.00%2C0%2C0%2C0%2Cdh")
xhr.setRequestHeader("Cache-Control", "no-cache")
xhr.setRequestHeader("Pragma", "no-cache")
xhr.setRequestHeader("Expires", "Sat, 01 Jan 2000 00:00:00 GMT")
xhr.setRequestHeader("Connection", "keep-alive")
xhr.setRequestHeader("Sec-Fetch-Dest", "empty")
xhr.setRequestHeader("Sec-Fetch-Mode", "cors")
xhr.setRequestHeader("Sec-Fetch-Site", "same-origin")
xhr.setRequestHeader("TE", "trailers")
xhr.send
T = xhr.responseText
Catch ex As Exception
T = ""
userAgentOriginalList.Remove(agent)
LOG(ex.Message)
Threading.Thread.Sleep(1200) : Application.DoEvents()
End Try
If T.IndexOf(">일시적인 서비스 오류가 발생했습니다.<") > -1 Then
userAgentOriginalList.Remove(agent)
T = "" : LOG("[ERR] 과도한 트래픽으로 인한 일시적인 서비스 오류가 발생했습니다.")
program1472.Functions.Time.Delay(12000, CType(MainForm, fMain).LOG_LIST)
xWinHttp(, $"https://map.naver.com/p/search/{enQuery}?c=15.00%2C0%2C0%2C0%2Cdh")
End If
Loop Until T.Length > 0
O = JSON.DeserializeObject(T)
JSON.AddValue(O, "url", URL)
Dim timestamp As String = DateTime.Now.ToString("yyyyMMdd_HHmmssfffff")
Dim fileName As String = IO.Path.Combine(APP_EXPORT_PATH, $"{timestamp}.json")
IO.File.WriteAllText(fileName, O.ToString)
Return O
End Function
setOption(2, 13056)은 SSL 인증서 오류를 무시하는 옵션입니다. 응답을 받을 때까지(빈 응답이면 다시) 반복하고, "일시적인 서비스 오류" 페이지가 오면 12초를 쉬었다가 다시 시도하는 구조입니다. userAgent(), LOG, JSON 같은 함수는 원래 프로젝트에 있는 도우미입니다. 그리고 오류 처리 쪽의 xWinHttp(, ...) 재귀 호출은 반환값을 버리기 때문에, 요청과 파일 저장이 한 번 더 일어날 뿐 그 결과는 쓰이지 않습니다. 재시도 로직을 정리할 때 이 부분은 빼는 편이 깔끔합니다.
문제는 같은 헤더로 같은 요청을 WinHttp.WinHttpRequest, HttpWebRequest, curl로 보내면 정상 응답이 오는데, MSXML2.ServerXMLHTTP.6.0으로 보낼 때만 아래처럼 nginx의 403 응답이 돌아왔다는 점입니다.
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
정확히 어떤 차이로 막히는지는 확인하지 못했습니다. 헤더를 똑같이 맞춰도 컴포넌트마다 실제로 전송되는 요청 모양이 조금씩 다를 수 있으니, 이렇게 특정 클라이언트만 거부당하면 원인을 파고들기보다 정상 동작하는 쪽(HttpClient나 WinHttpRequest)으로 바꾸는 게 빠릅니다. 이런 이유로 이 사이트 요청에는 ServerXMLHTTP를 쓰지 않습니다.
외부 서비스 데이터를 반복해서 가져오는 프로그램이라면 요청 간격을 넉넉히 두고, 서비스가 공식 API를 제공하는지 먼저 확인하는 게 순서입니다. 서버가 트래픽 제한을 걸었다는 신호(위의 "일시적인 서비스 오류" 같은 응답)가 오면 바로 재시도하지 말고 충분히 쉬었다가 진행하는 것도 기본입니다.