
개발 과정에서 HTTP 통신은 빼놓을 수 없는 중요한 부분입니다. Go 언어는 이러한 웹 환경에서의 작업을 위한 강력한 표준 라이브러리를 제공하고 있어, go http 패키지를 활용하면 복잡한 네트워크 통신을 놀랍도록 간단하게 구현할 수 있습니다.
필자는 go http 패키지를 활용하여 간략한 임시 서버를 구성하거나, 봇 API를 구성할 때 주로 많이 활용합니다. 특히 마이크로서비스 아키텍처에서 서비스 간 통신이 필요할 때마다 go http 패키지의 강력함을 실감하곤 합니다.
Go 언어의 기본 문법을 이제 막 마스터하셨다면, 다음 단계로 네트워크 프로그래밍에 도전해보는 것은 어떨까요? 이 글에서는 Go 언어의 HTTP 패키지를 활용한 서버 및 클라이언트 프로그래밍에 대해 살펴보겠습니다. 사실 이전 글에서 http 패키지를 활용한 간단한 서버를 만들어 보았는데요. 시리즈 특성 상 server, request를 모두 알아야 시도할 수 있는 부분들이 있으니 함께 다루어보도록 하겠습니다.
Go HTTP
Go 언어는 표준 라이브러리에 net/http
패키지를 포함하고 있어 별도의 프레임워크 없이도 웹 서버 및 클라이언트를 구축할 수 있습니다. 이는 go http 패키지가 얼마나 포괄적이고 강력한지 보여주는 좋은 예시입니다.
필자는 프로젝트를 시작할 때마다 외부 라이브러리에 의존하기보다 가능한 한 표준 라이브러리만으로 해결할 수 있는 방법을 먼저 고민합니다. 특히 go 언어 http 패키지는 기본적인 기능만으로도 상당히 많은 요구사항을 충족시킬 수 있어 애용하고 있습니다.
HTTP 패키지는 크게 두 부분으로 나눌 수 있습니다:
- HTTP 클라이언트: 외부 서버에 요청을 보내는 데 사용됩니다.
- HTTP 서버: 클라이언트의 요청을 처리하는 서버를 구축하는 데 사용됩니다.
이 두 부분을 차례대로 살펴보겠습니다.
Go에서 HTTP 요청 보내기
Go 언어에서 HTTP 요청을 보내는 방법은 크게 세 가지가 있습니다:
http.Get()
,http.Post()
등의 간편 함수 사용http.Client
인스턴스를 직접 사용- 커스텀 HTTP 클라이언트 구성하기
각각의 방법에 대해 자세히 알아보겠습니다.
간편 함수를 사용한 HTTP 요청
가장 간단한 방법은 http.Get()
, http.Post()
등의 함수를 사용하는 것입니다.
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
// GET 요청 보내기
resp, err := http.Get("https://api.example.com/data")
if err != nil {
log.Fatalf("HTTP 요청 실패: %v", err)
}
defer resp.Body.Close()
// 응답 본문 읽기
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("응답 본문 읽기 실패: %v", err)
}
fmt.Printf("응답 상태 코드: %d\n", resp.StatusCode)
fmt.Printf("응답 본문: %s\n", body)
}
위 코드는 지정된 URL로 GET 요청을 보내고 응답을 출력합니다. http.Get()
함수는 내부적으로 기본 HTTP 클라이언트를 사용하며, 응답 또는 오류를 반환합니다. 응답을 받은 후에는 반드시 resp.Body.Close()
를 호출하여 리소스를 해제해야 합니다. 이를 위해 defer
키워드를 사용하여 함수가 종료될 때 자동으로 호출되도록 합니다.
필자는 간단한 API 호출이 필요할 때 이 방법을 주로 사용합니다. 복잡한 설정이 필요 없고 코드가 간결해져 빠르게 프로토타입을 만들 때 유용합니다.
POST 요청 예시
POST 요청을 보내려면 http.Post()
함수를 사용할 수 있습니다:
package main
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
// 전송할 데이터
postData := []byte(`{"name":"John","age":30}`)
// POST 요청 보내기
resp, err := http.Post(
"https://api.example.com/users",
"application/json",
bytes.NewBuffer(postData),
)
if err != nil {
log.Fatalf("HTTP POST 요청 실패: %v", err)
}
defer resp.Body.Close()
// 응답 본문 읽기
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("응답 본문 읽기 실패: %v", err)
}
fmt.Printf("응답 상태 코드: %d\n", resp.StatusCode)
fmt.Printf("응답 본문: %s\n", body)
}
이 예시에서는 JSON 데이터를 POST 요청으로 보내고 있습니다. http.Post()
함수는 URL, 컨텐츠 타입, 그리고 요청 본문을 포함하는 io.Reader
를 매개변수로 받습니다.
HTTP 클라이언트 인스턴스 직접 사용하기
더 세밀한 제어가 필요한 경우 http.Client
인스턴스를 직접 사용할 수 있습니다:
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
)
func main() {
// 커스텀 HTTP 클라이언트 생성
client := &http.Client{
Timeout: 10 * time.Second, // 타임아웃 설정
}
// 요청 객체 생성
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
if err != nil {
log.Fatalf("요청 객체 생성 실패: %v", err)
}
// 헤더 추가
req.Header.Add("User-Agent", "Go HTTP Client")
req.Header.Add("Authorization", "Bearer token123")
// 요청 보내기
resp, err := client.Do(req)
if err != nil {
log.Fatalf("HTTP 요청 실패: %v", err)
}
defer resp.Body.Close()
// 응답 본문 읽기
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("응답 본문 읽기 실패: %v", err)
}
fmt.Printf("응답 상태 코드: %d\n", resp.StatusCode)
fmt.Printf("응답 본문: %s\n", body)
}
이 방법을 사용하면 타임아웃, 리다이렉션 정책, 쿠키 정책 등 다양한 클라이언트 설정을 커스터마이즈할 수 있습니다. 또한 http.NewRequest()
를 사용하여 요청 객체를 생성하고 헤더, 쿼리 파라미터, 쿠키 등을 세밀하게 제어할 수 있습니다.
필자는 인증이 필요한 API를 호출할 때 이 방식을 주로 사용합니다. 토큰을 헤더에 추가하거나, 특정 타임아웃 설정이 필요한 경우 이 방법이 더 유연하게 대응할 수 있기 때문입니다.
커스텀 HTTP 클라이언트 구성하기
더 복잡한 시나리오에서는 go server 와의 통신을 위해 HTTP 클라이언트를 완전히 커스터마이즈할 수 있습니다:
package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
)
func main() {
// 커스텀 트랜스포트 생성
transport := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // 주의: 프로덕션에서는 사용하지 말 것
},
}
// 커스텀 클라이언트 생성
client := &http.Client{
Transport: transport,
Timeout: 15 * time.Second,
}
// 요청 생성 및 전송
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
if err != nil {
log.Fatalf("요청 객체 생성 실패: %v", err)
}
resp, err := client.Do(req)
if err != nil {
log.Fatalf("HTTP 요청 실패: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("응답 본문 읽기 실패: %v", err)
}
fmt.Printf("응답 상태 코드: %d\n", resp.StatusCode)
fmt.Printf("응답 본문: %s\n", body)
}
이 예시에서는 http.Transport
를 사용하여 연결 풀링, TLS 설정, 압축 등을 제어합니다. 이 방법은 고성능 애플리케이션이나 특수한 네트워크 요구사항이 있는 경우에 유용합니다.
필자는 마이크로서비스 아키텍처에서 서비스 간 통신이 많은 경우, 연결 풀링을 최적화하기 위해 이러한 커스텀 설정을 자주 사용합니다. 특히 go 언어 server와 통신할 때 이러한 최적화가 성능에 큰 영향을 미칠 수 있습니다.
HTTP 서버 구현하기 (복습)
이제 HTTP 요청을 보내는 클라이언트 측면을 살펴보았으니, go http를 사용하여 서버를 구현하는 방법을 알아보겠습니다.
기본 HTTP 서버 구현
Go에서 HTTP 서버를 구현하는 가장 기본적인 방법은 다음과 같습니다:
package main
import (
"fmt"
"log"
"net/http"
)
// 핸들러 함수
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
// 라우트 등록
http.HandleFunc("/hello", helloHandler)
// 서버 시작
fmt.Println("서버가 8080 포트에서 시작되었습니다...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("서버 시작 실패: %v", err)
}
}
이 코드는 8080 포트에서 HTTP 서버를 시작하고, /hello
경로로 요청이 들어오면 “Hello, World!”라는 메시지를 응답합니다. http.HandleFunc()
를 사용하여 URL 경로와 핸들러 함수를 연결합니다.
필자는 개발 중 빠르게 테스트 서버를 띄워야 할 때 이렇게 간단한 코드로 서버를 구현합니다. 몇 줄의 코드만으로 HTTP 서버가 동작하는 모습을 보면 go http 패키지의 강력함을 실감할 수 있습니다.
ServeMux를 사용한 라우팅
더 복잡한 라우팅이 필요한 경우 http.ServeMux
를 사용할 수 있습니다:
package main
import (
"fmt"
"log"
"net/http"
)
func homeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "환영합니다! 홈 페이지입니다.")
}
func userHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "사용자 페이지입니다.")
}
func main() {
// 새로운 ServeMux 생성
mux := http.NewServeMux()
// 핸들러 등록
mux.HandleFunc("/", homeHandler)
mux.HandleFunc("/user", userHandler)
// 서버 시작
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
fmt.Println("서버가 8080 포트에서 시작되었습니다...")
if err := server.ListenAndServe(); err != nil {
log.Fatalf("서버 시작 실패: %v", err)
}
}
http.ServeMux
는 URL 경로에 따라 요청을 적절한 핸들러로 라우팅하는 HTTP 요청 멀티플렉서입니다. 이를 사용하면 여러 경로에 대한 처리를 한 곳에서 관리할 수 있습니다.
미들웨어 구현
Go의 HTTP 서버에서 미들웨어를 구현하는 것도 간단합니다.
미들웨어가 무엇인지는 추후에 다룰 것이며 지금은 재밌게 실행해보고 살펴보도록 합니다:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// 로깅 미들웨어
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
// 다음 핸들러 호출
next.ServeHTTP(w, r)
// 요청 처리 후 로깅
log.Printf(
"%s %s %s",
r.Method,
r.RequestURI,
time.Since(startTime),
)
})
}
// 인증 미들웨어
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 간단한 인증 체크 (실제로는 더 복잡한 로직이 필요)
token := r.Header.Get("Authorization")
if token != "secret-token" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// 인증 성공 시 다음 핸들러 호출
next.ServeHTTP(w, r)
})
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
// 핸들러와 미들웨어 설정
helloHandlerWithMiddleware := loggingMiddleware(authMiddleware(http.HandlerFunc(helloHandler)))
// 라우트 등록
http.Handle("/hello", helloHandlerWithMiddleware)
// 서버 시작
fmt.Println("서버가 8080 포트에서 시작되었습니다...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("서버 시작 실패: %v", err)
}
}
이 예시에서는 로깅과 인증을 처리하는 두 가지 미들웨어를 구현했습니다. 미들웨어는 기존 핸들러를 감싸는 함수로, 요청 처리 전후에 추가 로직을 실행할 수 있도록 합니다.
필자는 go 언어 http 서버를 구현할 때 이러한 미들웨어 패턴을 자주 활용합니다. 특히 로깅, 인증, 압축, CORS 등의 공통 기능을 재사용 가능한 미들웨어로 구현하면 코드 중복을 줄이고 관심사를 분리할 수 있어 유지보수가 용이해집니다.
REST API 서버 구현 예시
go server를 사용하여 간단한 REST API를 구현해보겠습니다:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
)
// 사용자 구조체
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
// 메모리 DB 역할을 하는 사용자 맵
var users = map[int]User{
1: {ID: 1, Name: "Alice", Age: 25},
2: {ID: 2, Name: "Bob", Age: 30},
3: {ID: 3, Name: "Charlie", Age: 35},
}
// 사용자 핸들러
func userHandler(w http.ResponseWriter, r *http.Request) {
// JSON 응답 헤더 설정
w.Header().Set("Content-Type", "application/json")
// URL에서 ID 추출
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 3 {
http.Error(w, "잘못된 경로", http.StatusBadRequest)
return
}
if parts[2] == "" {
// GET /users/ : 모든 사용자 목록
if r.Method == "GET" {
userList := make([]User, 0, len(users))
for _, user := range users {
userList = append(userList, user)
}
json.NewEncoder(w).Encode(userList)
return
}
// POST /users/ : 새 사용자 생성
if r.Method == "POST" {
var newUser User
if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 새 ID 할당
maxID := 0
for id := range users {
if id > maxID {
maxID = id
}
}
newUser.ID = maxID + 1
// 저장
users[newUser.ID] = newUser
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newUser)
return
}
http.Error(w, "지원하지 않는 메서드", http.StatusMethodNotAllowed)
return
}
// 특정 사용자 조회/수정/삭제
idStr := parts[2]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "잘못된 ID", http.StatusBadRequest)
return
}
// 사용자 존재 여부 확인
user, exists := users[id]
if !exists {
http.Error(w, "사용자를 찾을 수 없음", http.StatusNotFound)
return
}
// GET /users/{id} : 특정 사용자 조회
if r.Method == "GET" {
json.NewEncoder(w).Encode(user)
return
}
// PUT /users/{id} : 사용자 정보 수정
if r.Method == "PUT" {
var updatedUser User
if err := json.NewDecoder(r.Body).Decode(&updatedUser); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// ID는 URL에서 가져온 것으로 유지
updatedUser.ID = id
users[id] = updatedUser
json.NewEncoder(w).Encode(updatedUser)
return
}
// DELETE /users/{id} : 사용자 삭제
if r.Method == "DELETE" {
delete(users, id)
w.WriteHeader(http.StatusNoContent)
return
}
http.Error(w, "지원하지 않는 메서드", http.StatusMethodNotAllowed)
}
func main() {
// API 라우트 등록
http.HandleFunc("/users/", userHandler)
// 서버 시작
fmt.Println("REST API 서버가 8080 포트에서 시작되었습니다...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("서버 시작 실패: %v", err)
}
}
이 예시는 사용자 정보를 관리하는 간단한 REST API 서버를 구현합니다. /users/
경로로 GET 요청을 보내면 모든 사용자 목록을 반환하고, POST 요청으로 새 사용자를 생성할 수 있습니다. 또한 /users/{id}
경로로 특정 사용자를 조회, 수정, 삭제할 수 있습니다.
필자는 go http 패키지로 REST API를 구현할 때, 위와 같은 구조로 시작하고 필요에 따라 기능을 확장합니다. 실제 프로젝트에서는 데이터베이스 연동, 인증, 로깅 등의 기능을 추가하게 되겠지만, 기본 구조는 간단하게 유지할 수 있습니다.
HTTP 파일 업로드 처리
HTTP를 통한 파일 업로드 처리도 go 언어 http 패키지로 쉽게 구현할 수 있습니다:
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
)
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 최대 10MB 파일 크기 제한
r.ParseMultipartForm(10 << 20)
// 폼에서 파일 가져오기
file, handler, err := r.FormFile("uploadFile")
if err != nil {
http.Error(w, "파일을 찾을 수 없습니다: "+err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
// 파일 정보 출력
fmt.Printf("업로드된 파일: %+v\n", handler.Filename)
fmt.Printf("파일 크기: %+v\n", handler.Size)
fmt.Printf("MIME 타입: %+v\n", handler.Header)
// 업로드 디렉토리 생성
uploadDir := "./uploads"
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
http.Error(w, "업로드 디렉토리 생성 실패: "+err.Error(), http.StatusInternalServerError)
return
}
// 파일 생성
dst, err := os.Create(filepath.Join(uploadDir, handler.Filename))
if err != nil {
http.Error(w, "파일 생성 실패: "+err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
// 업로드된 파일을 새로 생성한 파일에 복사
if _, err := io.Copy(dst, file); err != nil {
http.Error(w, "파일 저장 실패: "+err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "파일 '%s'가 성공적으로 업로드되었습니다", handler.Filename)
}
func main() {
// 파일 업로드 핸들러 등록
http.HandleFunc("/upload", uploadHandler)
// 정적 파일 제공 (업로드 폼)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `
<!DOCTYPE html>
<html>
<head>
<title>Go File Upload</title>
</head>
<body>
<h1>파일 업로드</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="uploadFile" />
<input type="submit" value="업로드" />
</form>
</body>
</html>
`)
})
// 서버 시작
fmt.Println("파일 업로드 서버가 8080 포트에서 시작되었습니다...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("서버 시작 실패: %v", err)
}
}
이 예시에서는 multipart/form-data
형식의 폼 데이터에서 파일을 추출하여 서버의 로컬 디렉토리에 저장합니다. r.ParseMultipartForm()
을 사용하여 최대 파일 크기를 제한하고, r.FormFile()
을 사용하여 업로드된 파일을 가져옵니다.
필자는 파일 업로드 기능을 구현할 때, 항상 최대 파일 크기 제한, 허용된 파일 타입 검증, 안전한 파일명 생성 등의 보안 조치를 추가합니다. go http 패키지는 이러한 작업을 수행하는 데 필요한 모든 기능을 제공합니다.
실전 응용: 간단한 프록시 서버 구현
go server를 사용하여 간단한 프록시 서버를 구현해보겠습니다:
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/url"
)
func proxyHandler(w http.ResponseWriter, r *http.Request) {
// 프록시할 대상 URL
targetURL := r.URL.Query().Get("url")
if targetURL == "" {
http.Error(w, "url 쿼리 파라미터가 필요합니다", http.StatusBadRequest)
return
}
// URL 유효성 검사
target, err := url.Parse(targetURL)
if err != nil {
http.Error(w, "잘못된 URL: "+err.Error(), http.StatusBadRequest)
return
}
// 새 요청 생성
proxyReq, err := http.NewRequest(r.Method, target.String(), r.Body)
if err != nil {
http.Error(w, "프록시 요청 생성 실패: "+err.Error(), http.StatusInternalServerError)
return
}
// 원본 요청의 헤더 복사
for name, values := range r.Header {
for _, value := range values {
proxyReq.Header.Add(name, value)
}
}
// 클라이언트 생성 및 요청 전송
client := &http.Client{}
resp, err := client.Do(proxyReq)
if err != nil {
http.Error(w, "프록시 요청 실패: "+err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
// 응답 헤더 복사
for name, values := range resp.Header {
for _, value := range values {
w.Header().Add(name, value)
}
}
// 상태 코드 설정
w.WriteHeader(resp.StatusCode)
// 응답 본문 복사
if _, err := io.Copy(w, resp.Body); err != nil {
log.Printf("응답 본문 복사 중 오류: %v", err)
}
}
func main() {
http.HandleFunc("/proxy", proxyHandler)
// 간단한 사용법 페이지
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `
<!DOCTYPE html>
<html>
<head>
<title>Go Proxy Server</title>
</head>
<body>
<h1>Go 프록시 서버</h1>
<p>사용법: /proxy?url=https://example.com</p>
</body>
</html>
`)
})
fmt.Println("프록시 서버가 8080 포트에서 시작되었습니다...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("서버 시작 실패: %v", err)
}
}
이 프록시 서버는 `/proxy?url=https://example.com`과 같은 형식의 요청을 받아 지정된 URL로 요청을 전달하고 그 응답을 클라이언트에게 반환합니다. 요청 및 응답 헤더를 모두 복사하여 원본 서버와의 통신을 가능한 한 그대로 유지합니다. 필자는 go http 패키지를 활용하여 이러한 프록시 서버를 구축한 경험이 있습니다. 특히 API 게이트웨이나 리버스 프록시 등의 간단한 네트워크 도구를 구현할 때 매우 효과적이었습니다.
웹소켓 서버 구현
웹소켓 통신을 사용하는 서버를 구현하기 위해서는 `gorilla/websocket` 패키지를 사용할 수 있습니다:
go
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// 모든 오리진 허용 (실제 프로덕션에서는 보안상의 이유로 제한하는 것이 좋음)
CheckOrigin: func(r *http.Request) bool { return true },
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
// HTTP 연결을 웹소켓으로 업그레이드
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("웹소켓 업그레이드 실패: %v", err)
return
}
defer conn.Close()
log.Printf("클라이언트 연결됨: %s", conn.RemoteAddr())
// 메시지 처리 루프
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
log.Printf("메시지 읽기 오류: %v", err)
break
}
// 받은 메시지 출력
log.Printf("받은 메시지: %s", p)
// 메시지 에코백
if err := conn.WriteMessage(messageType, p); err != nil {
log.Printf("메시지 쓰기 오류: %v", err)
break
}
}
}
func main() {
// 웹소켓 핸들러 등록
http.HandleFunc("/ws", wsHandler)
// 웹소켓 클라이언트 제공
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `
<!DOCTYPE html>
<html>
<head>
<title>Go WebSocket</title>
<script>
let socket;
function connect() {
socket = new WebSocket("ws://" + window.location.host + "/ws");
socket.onopen = function() {
console.log("웹소켓 연결됨");
document.getElementById("status").textContent = "연결됨";
};
socket.onmessage = function(e) {
const messages = document.getElementById("messages");
const li = document.createElement("li");
li.textContent = "서버: " + e.data;
messages.appendChild(li);
};
socket.onclose = function() {
console.log("웹소켓 연결 종료");
document.getElementById("status").textContent = "연결 끊김";
};
}
function sendMessage() {
const input = document.getElementById("messageInput");
const message = input.value;
if (message && socket) {
socket.send(message);
const messages = document.getElementById("messages");
const li = document.createElement("li");
li.textContent = "나: " + message;
messages.appendChild(li);
input.value = "";
}
}
window.onload = connect;
</script>
</head>
<body>
<h1>Go WebSocket 예제</h1>
<div>
상태: <span id="status">연결 중...</span>
</div>
<div>
<input type="text" id="messageInput" placeholder="메시지 입력">
<button onclick="sendMessage()">전송</button>
</div>
<ul id="messages"></ul>
</body>
</html>
`)
})
fmt.Println("웹소켓 서버가 8080 포트에서 시작되었습니다...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("서버 시작 실패: %v", err)
}
}
이 예시에서는 gorilla/websocket
패키지를 사용하여 웹소켓 서버를 구현했습니다. HTTP 연결을 웹소켓 연결로 업그레이드하고, 양방향 통신을 가능하게 합니다. 클라이언트로부터 받은 메시지를 그대로 되돌려 보내는 에코 서버입니다.
필자는 실시간 채팅, 알림 시스템, 라이브 대시보드 등을 구현할 때 웹소켓을 자주 활용합니다. go http 패키지와 gorilla/websocket
의 조합은 이러한 실시간 애플리케이션을 구축하는 데 매우 효과적입니다.
HTTP/2 서버 구현
Go의 표준 라이브러리는 HTTP/2도 지원합니다. HTTPS를 설정하면 자동으로 HTTP/2가 활성화됩니다:
package main
import (
"fmt"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from HTTP/2 server! Protocol: %s", r.Proto)
}
func main() {
http.HandleFunc("/", helloHandler)
fmt.Println("HTTP/2 서버가 8443 포트에서 시작되었습니다...")
// TLS를 사용하여 서버 시작
// 참고: 인증서 파일이 필요합니다. 테스트용으로는 다음 명령으로 생성할 수 있습니다:
// go run $(go env GOROOT)/src/crypto/tls/generate_cert.go --host localhost
err := http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil)
if err != nil {
log.Fatalf("서버 시작 실패: %v", err)
}
}
이 예시는 TLS를 사용하여 HTTP/2 서버를 시작합니다. 클라이언트가 HTTP/2를 지원하면 자동으로 HTTP/2 프로토콜을 사용하게 됩니다.
요약
이 글에서 살펴본 것처럼, Go 언어의 표준 라이브러리에 포함된 go http 패키지는 웹 애플리케이션 개발에 필요한 대부분의 기능을 제공합니다. 단순한 요청부터 복잡한 웹 서버 구현까지, go http는 효율적이고 간결한 API를 제공합니다.
필자는 go http를 활용하여 다양한 웹 서비스와 API를 개발해왔습니다. 그 과정에서 Go 언어의 동시성 모델과 결합된 go http 패키지의 성능과 확장성에 항상 감탄하게 됩니다. 특히 고부하 환경에서도 안정적으로 동작하는 go 언어 server를 구축할 수 있다는 점이 큰 장점입니다.
Go 언어의 기본 문법을 이제 막 마스터한 개발자들에게 go http 패키지는 실전 응용의 첫 단계로 매우 적합합니다. 표준 라이브러리만으로도 충분히 강력한 웹 애플리케이션을 구축할 수 있으며, 필요에 따라 서드파티 라이브러리를 추가하여 기능을 확장할 수도 있습니다.
앞으로 여러분의 Go 언어 여정에서 go http 패키지가 큰 도움이 되길 바랍니다. 웹 개발의 세계는 넓고 다양하지만, Go의 간결함과 강력함을 통해 효율적으로 탐험할 수 있을 것입니다.