ChatGPT를 이용해 Go언어 배워서 API 서버 개발 하기 (2/2)
이전 포스트에서 이어서..
1️⃣ 기본적인 API 엔드포인트 만들기 → Gin의 기본 개념 익히기
2️⃣ JSON 요청과 응답 처리 → c.JSON()
, BindJSON()
이해
3️⃣ 쿼리 파라미터와 URL 파라미터 다루기 → c.Param()
, c.Query()
이해
4️⃣ POST 요청을 받아서 처리하는 방법 익히기 → c.ShouldBindJSON()
익히기
5️⃣ 동적 라우팅 및 URL 패턴 이해 → c.Param()
과 c.Query()
활용하여 동적 경로 및 검색 기능 추가
6️⃣ 헤더 값 읽기 및 검증하기 → c.GetHeader()
로 API 키 다루기
7️⃣ 외부 API 호출 및 응답 처리하기 → http.Post()
와 http.NewRequest()
이해
8️⃣ Gin 미들웨어 이해하기 → 인증/로깅 미들웨어 추가해보기
9️⃣ 에러 핸들링 및 응답 구조 개선 → 더 안전한 에러 처리 방식 적용
🔟 전체 코드 리뷰 및 최적화 → 지금까지 배운 내용 정리하고 완전한 이해 확인
6단계: 헤더(Header) 및 쿼리 스트링 활용하기
이번 단계에서는 HTTP 헤더(Header)와 쿼리 스트링(Query String) 을 다룬다.
이를 통해 API Key 인증 같은 작업을 처리할 수 있다.
📌 목표
✅ c.GetHeader("Authorization")
을 사용하여 헤더에서 API Key 읽기
✅ c.Query("query")
와 비교하며 쿼리 스트링 이해하기
✅ API Key가 없으면 401 Unauthorized
반환
1️⃣ 헤더 값 읽기
아래 코드를 작성해보자.
func authExample(c *gin.Context) {
apiKey := c.GetHeader("Authorization")
if apiKey == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "API Key missing"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Authorized", "api_key": apiKey})
}
💡 힌트
c.GetHeader("Authorization")
→Authorization
헤더 값 가져오기- 값이 없으면
401 Unauthorized
반환 - 정상일 경우 API Key를 JSON 응답에 포함
2️⃣ 쿼리 스트링과 비교
아래 코드를 추가해보자.
func searchWithAuth(c *gin.Context) {
apiKey := c.GetHeader("Authorization")
query := c.Query("query")
if apiKey == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "API Key missing"})
return
}
if query == "" {
query = "default"
}
c.JSON(http.StatusOK, gin.H{"message": "Searching for " + query, "api_key": apiKey})
}
3️⃣ 라우터 등록하기
위에서 만든 핸들러를 main()
에 추가한다.
func main() {
r := gin.Default()
r.GET("/auth", authExample) // 헤더 값 확인
r.GET("/searchAuth", searchWithAuth) // 헤더 + 쿼리 스트링 활용
r.Run(":8080")
}
4️⃣ 테스트하기
✅ API Key 없이 호출 (실패)
curl -X GET "http://localhost:8080/auth"
{"error": "API Key missing"}
✅ API Key 포함하여 호출 (성공)
curl -X GET "http://localhost:8080/auth" -H "Authorization: Bearer my-secret-key"
{"message": "Authorized", "api_key": "Bearer my-secret-key"}
🔍 정리
1️⃣ c.GetHeader("Authorization")
→ 헤더에서 값 가져오기
2️⃣ c.Query("query")
→ URL에서 쿼리 스트링 가져오기
3️⃣ API Key가 없으면 401 Unauthorized
반환
4️⃣ API Key가 있으면 정상 응답
🚀 직접 코드 작성해보고 실행해봐!
완성된 코드를 올리면 검토해줄게! 😃
아래 코드 전달함!!
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func helloWorld(c *gin.Context){
c.JSON(http.StatusOK, gin.H{"message" : "Hello, world!"})
}
func ping(c *gin.Context){
c.JSON(http.StatusOK, gin.H{"message" : "pong"})
}
func echo(c *gin.Context){
var jsonData map[string]interface{}
// var jsonData gin.H, same with above?
if err := c.BindJSON(&jsonData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error" : "Invalid JSON"})
return
}
c.JSON(http.StatusOK, gin.H{"received": jsonData})
}
func greet(c *gin.Context) {
name := c.Param("name")
c.JSON(http.StatusOK, gin.H{"message": "Hello," + name})
}
func search(c *gin.Context) {
query := c.Query("query")
if query == "" {
query = "default"
}
c.JSON(http.StatusOK, gin.H{"message": "Searching for: " + query})
}
func authExample(c *gin.Context) {
apiKey := c.GetHeader("Authorization")
if apiKey == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error message" : "API Key missing"})
return
}
c.JSON(http.StatusOK, gin.H{"message" : "Authorized", "api_key" : apiKey})
}
func searchWithAuth(c *gin.Context) {
apiKey := c.GetHeader("Authorization")
query := c.Query("query")
if apiKey == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error message" : "API Key missing"})
return
}
if query == "" {
query = "default"
return
}
c.JSON(http.StatusOK, gin.H{"message" : "Searching for "+query, "api_key" : apiKey})
}
func main() {
r := gin.Default()
r.GET("/hello", helloWorld)
r.GET("/ping", ping)
r.GET("/greet/:name", greet)
r.GET("/search", search)
r.POST("/echo", echo)
r.GET("/auth", authExample)
r.GET("/searchAuth", searchWithAuth)
r.Run(":8080")
}
아주 좋아! 🎉
거의 다 맞았는데, searchWithAuth
함수에서 하나의 실수가 있어!
🔴 수정해야 할 부분
if query == "" {
query = "default"
return // ❌ 여기에 return 있으면 이후 JSON 응답이 실행되지 않음!
}
쳇 하나 틀림
7단계 진행하기 전에 라우터 관련 질문
r.POST에 대해서 자세히 말해줘 파라미터랑 쿼리에 대해서 라우팅할때 주의할점이나 가이드 알려줘
🔹 r.POST()
의 역할과 사용법
r.POST()
는 Gin 프레임워크에서 POST 요청을 처리할 라우트를 등록하는 함수야.
기본적인 형식은 아래와 같아.
r.POST("/path", handlerFunction)
- 첫 번째 인자:
"경로"
- 두 번째 인자: 요청을 처리할 핸들러 함수
📌 1. POST
요청에서 파라미터 전달 방식
(1) 요청 본문 (Body)에서 JSON 데이터 받기
API 요청의 데이터는 보통 JSON 형태로
Body
에 담겨서 전달돼.
c.ShouldBindJSON()
을 사용하면 자동으로struct
또는map
으로 변환할 수 있어.
type RequestBody struct {
Name string `json:"name"`
Email string `json:"email"`
}
func createUser(c *gin.Context) {
var body RequestBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON format"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User created", "data": body})
}
(2) 쿼리 파라미터 (?key=value
)
c.Query("key")
를 사용해서 쿼리 파라미터를 읽을 수 있어.
예를 들어/search?query=golang
같은 요청이 오면query
값을 가져올 수 있어.
func search(c *gin.Context) {
query := c.Query("query") // 없으면 "" 반환
if query == "" {
query = "default"
}
c.JSON(http.StatusOK, gin.H{"message": "Searching for " + query})
}
(3) URL 파라미터 (/path/:value
)
c.Param("key")
를 사용해서 URL 경로에 포함된 값을 가져올 수 있어.
func getUser(c *gin.Context) {
userID := c.Param("id") // /user/123 → userID = "123"
c.JSON(http.StatusOK, gin.H{"user_id": userID})
}
📌 2. POST
요청의 주의점 (라우팅 가이드)
✅ JSON 데이터를 받을 때는 c.ShouldBindJSON()
을 사용하자.
✅ 쿼리 파라미터와 바디(JSON) 데이터는 다르다.
✅ URL에 /:param
을 사용할 때는 c.Param()
으로 가져온다.
✅ 중복되는 엔드포인트(/users/:id
vs /users/search
등) 주의해야 한다.
✅ RESTful API 규칙을 따르는 것이 좋다.
💡 예제 코드 (모든 방식 포함)
func main() {
r := gin.Default()
// Body에서 JSON 데이터 받기
r.POST("/users", createUser)
// 쿼리 파라미터 받기
r.GET("/search", search)
// URL 파라미터 받기
r.GET("/users/:id", getUser)
r.Run(":8080")
}
이제 r.POST()
를 사용해서 API 요청을 처리할 때 주의해야 할 점과 활용법을 알았어!
이해됐으면 다음 단계로 가자 🚀
쿼리파라미터는 언제 써?
ChatGPT의 말:
📌 쿼리 파라미터 (?key=value
)를 언제 쓰는가?
쿼리 파라미터는 필터링, 정렬, 검색, 옵션 설정 같은 용도로 주로 사용돼.
즉, 리소스를 특정 조건으로 조회하거나 부가적인 정보를 추가할 때 적합해.
✅ 1. 필터링 & 검색
🔹 예제: /products?category=electronics&brand=apple
🔹 설명: 특정 카테고리나 브랜드로 상품을 필터링할 때 사용
func searchProducts(c *gin.Context) {
category := c.Query("category") // 기본값 없음 ("" 반환)
brand := c.Query("brand")
c.JSON(http.StatusOK, gin.H{"message": "Filtering", "category": category, "brand": brand})
}
✔️ 언제 사용?
✅ 사용자가 검색 옵션을 선택할 때 (/search?query=golang
)
✅ 특정 카테고리/태그/조건을 선택할 때 (/posts?tag=tech
)
✅ 2. 정렬 & 페이지네이션
🔹 예제: /articles?sort=latest&page=2&limit=10
🔹 설명: 최신순으로 정렬하고, 2페이지에서 10개만 가져옴
func getArticles(c *gin.Context) {
sort := c.DefaultQuery("sort", "latest") // 기본값 설정 가능
page := c.DefaultQuery("page", "1")
limit := c.DefaultQuery("limit", "10")
c.JSON(http.StatusOK, gin.H{"message": "Fetching articles", "sort": sort, "page": page, "limit": limit})
}
✔️ 언제 사용?
✅ 정렬 옵션 (/posts?sort=desc
)
✅ 페이지네이션 (/users?page=3&limit=20
)
✅ 3. 부가적인 요청 옵션
🔹 예제: /download?format=pdf&quality=high
🔹 설명: 특정 파일 형식과 품질 옵션을 추가
func downloadFile(c *gin.Context) {
format := c.DefaultQuery("format", "pdf")
quality := c.DefaultQuery("quality", "medium")
c.JSON(http.StatusOK, gin.H{"message": "Downloading file", "format": format, "quality": quality})
}
✔️ 언제 사용?
✅ 응답 형식 선택 (/export?format=json
)
✅ 추가 옵션 (/video?quality=hd&subtitles=on
)
🚀 정리: 언제 쿼리 파라미터
를 써야 할까?
✔️ 리소스를 조회할 때 (검색, 필터링)
✔️ 리소스를 정렬할 때 (정렬, 페이지네이션)
✔️ 옵션을 추가할 때 (응답 형식, 품질 설정)
✔️ 서버 상태를 변경하지 않을 때 (쿼리 파라미터는 GET 요청에서 많이 사용)
쿼리 파라미터는 데이터 조회(READ)에 적합하고, 데이터 변경(CREATE, UPDATE, DELETE) 작업에는 보통 POST/PUT/DELETE
의 Body를 사용해! 🚀
c.ShouldBindJSON()랑 c.BindJSON()차이가 뭐고 언제 뭘 써?
ChatGPT의 말:
🔍 c.ShouldBindJSON()
vs c.BindJSON()
차이 및 사용법
둘 다 JSON 요청 바디를 구조체로 매핑하는 역할을 하지만, 에러 핸들링 방식이 다름.
✅ 1. c.BindJSON()
→ 내부적으로 c.ShouldBindJSON()
을 호출하지만, 에러 발생 시 자동으로 400 응답 반환
func handler(c *gin.Context) {
var reqBody RequestStruct
if err := c.BindJSON(&reqBody); err != nil {
return // 자동으로 400 Bad Request 응답 반환
}
c.JSON(http.StatusOK, gin.H{"data": reqBody})
}
✔ 특징:
- 에러가 발생하면 자동으로
400 Bad Request
응답 반환 - 명시적인 에러 처리가 필요 없음
- 간단한 API에서 빠르게 처리할 때 적합
📌 언제 사용?
✔ 에러를 따로 처리할 필요 없을 때
✔ 요청 형식이 단순하고, 잘못된 요청은 그냥 400
응답을 주면 되는 경우
✅ 2. c.ShouldBindJSON()
→ 직접 에러 핸들링 가능
func handler(c *gin.Context) {
var reqBody RequestStruct
if err := c.ShouldBindJSON(&reqBody); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": reqBody})
}
✔ 특징:
- 에러 발생 시 자동 응답 X → 직접 처리해야 함
- 커스텀 에러 메시지를 반환 가능
- 더 정교한 에러 핸들링이 필요할 때 적합
📌 언제 사용?
✔ 에러 메시지를 커스텀하고 싶을 때
✔ 다양한 바인딩 로직이 필요할 때 (예: Query
, Form
, XML
등)
✔ 400 이외의 다른 상태 코드를 반환해야 할 때
🚀 정리: 언제 뭘 써야 할까?
✅ c.BindJSON()
→ 단순한 API에서 사용 (자동 400 응답)
✅ c.ShouldBindJSON()
→ 에러 핸들링이 필요할 때 사용 (직접 처리 가능)
7단계 : 외부 API 호출 및 응답 처리하기
Nova LLM 또는 OpenAI API를 호출하는 코드를 이해하고 직접 구현해보기
📌 목표
http.Post()
와http.NewRequest()
차이 이해하기- 외부 API 요청을 만들고 응답을 파싱하는 과정 학습
📝 1차 코드 작성 (빈칸 포함)
func callExternalAPI() {
url := "https://example.com/api"
requestBody := strings.NewReader(`{"key": "value"}`)
resp, err := http._____(url, "application/json", requestBody)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body._____
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
fmt.Println("Response:", string(body))
}
✔ 힌트
http._____(url, "application/json", requestBody)
→http.Post()
또는http.NewRequest()
중 어느 걸 써야 할까?defer resp.Body._____
→ 빈칸에 들어갈 함수는?io.ReadAll(resp.Body)
는 무엇을 하는 코드일까?
📝 2차 코드 작성 (Nova LLM 호출)
Nova LLM API를 호출하는 runV1
함수가 어떻게 동작하는지 직접 분석하고,
아래 질문에 답을 해봐.
func runV1(c *gin.Context) {
apiKey, err := getAPIKey(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
var openAIRequest map[string]interface{}
if err := c.ShouldBindJSON(&openAIRequest); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
return
}
novaReq := map[string]interface{}{
"id": "generated-uuid",
"service_name": "test-service",
"request": openAIRequest,
}
requestBody, err := json.Marshal(novaReq)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to encode request"})
return
}
resp, err := http.Post("http://nova-llm-gateway.alpha.tossinvest.bz/api/v1/openai/chat/completions",
"application/json", bytes.NewBuffer(requestBody))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to call Nova LLM"})
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response"})
return
}
var novaResponse map[string]interface{}
if err := json.Unmarshal(body, &novaResponse); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse response"})
return
}
c.JSON(resp.StatusCode, novaResponse)
}
💡 질문
http.Post()
와http.NewRequest()
의 차이는?json.Marshal(novaReq)
가 하는 역할은?defer resp.Body.Close()
가 없으면 어떤 문제가 발생할까?
🚀 빈칸을 채우고, 질문에 대한 답을 고민한 후 알려줘!
1차 코드 작성을 했어
func callExternalAPI() {
url := "https://example.com/api"
requestBody := strings.NewReader({"key":"value"})
resp, err := http.POST(url, "application/json", requestBody)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
fmt.Println("Response:",string(body))
}
코드에 몇 가지 문제가 있어. 수정해야 할 부분을 정리해 줄게.
🔴 수정해야 할 부분
http.POST
→http.Post
(대문자 오류)- Go의
http
패키지에서는Post
함수가 소문자로 시작해야 해.
- Go의
defer resp.Body.close()
→defer resp.Body.Close()
(대소문자 오류)Close()
는 대문자로 시작해야 해.
-
requestBody := strings.NewReader(
{“key”:”value”})
- 문자열을
strings.NewReader
로 감싸는 건 괜찮지만, 만약 동적으로 JSON을 생성하려면json.Marshal
을 사용하는 게 더 좋아.
- 문자열을
✅ 수정된 코드
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func callExternalAPI() {
url := "https://example.com/api"
// JSON 데이터 생성
requestData := map[string]string{"key": "value"}
requestBody, err := json.Marshal(requestData)
if err != nil {
fmt.Println("Error encoding JSON:", err)
return
}
// HTTP POST 요청 보내기
resp, err := http.Post(url, "application/json", bytes.NewBuffer(requestBody))
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close() // 대문자 수정
// 응답 읽기
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
fmt.Println("Response:", string(body))
}
🔥 추가 팁
json.Marshal()
을 사용하면map[string]string
을 JSON으로 변환해 안전하게 처리할 수 있어.strings.NewReader
를 그대로 사용해도 되지만, 나중에 동적으로 데이터를 생성해야 한다면bytes.NewBuffer()
가 더 적절해.fmt.Println()
으로 에러 메시지를 출력하는 것도 괜찮지만,log.Fatal()
을 쓰면 더 직관적일 수도 있어.
🚀 이제 실행해보고 정상 동작하는지 확인해봐!
1,2번이야 오탈자인데, 3번은 지가 알려준대로 했는데 어이가 없네;;
bytes.NewBuffer(requestBody)랑 resp.Body.Close() 가 이해가 잘 안돼
ChatGPT의 말:
1️⃣ bytes.NewBuffer(requestBody)
❓ 왜 bytes.NewBuffer()
를 쓰는가?
http.Post()
의 세 번째 인자는 io.Reader
타입을 요구해.
하지만 json.Marshal()
등으로 만든 requestBody
는 []byte
타입이야.
이걸 io.Reader
로 변환하려면 bytes.NewBuffer()
를 사용해야 해.
📌 비교
requestBody, _ := json.Marshal(map[string]string{"key": "value"})
// ❌ 이렇게 하면 오류 (io.Reader 타입이 아님)
// http.Post(url, "application/json", requestBody)
// ✅ 올바른 사용법
http.Post(url, "application/json", bytes.NewBuffer(requestBody))
bytes.NewBuffer()
는 []byte
데이터를 io.Reader
인터페이스를 구현하는 객체로 변환해 줘서
http.Post()
에서 사용할 수 있게 해.
2️⃣ resp.Body.Close()
❓ 왜 resp.Body.Close()
를 해야 하나?
http.Post()
요청이 성공하면 응답 객체 *http.Response
가 반환되는데,
이 안의 Body
는 스트림(io.ReadCloser
) 형태로 되어 있어.
따라서 사용이 끝나면 Close()
를 호출해서 리소스를 해제해야 해.
📌 비교
resp, err := http.Post(url, "application/json", bytes.NewBuffer(requestBody))
if err != nil {
fmt.Println("Error:", err)
return
}
// ❌ Close() 안 하면 메모리 누수 발생 가능
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
// ✅ defer로 Close() 예약 (안전한 방법)
defer resp.Body.Close()
응답 본문을 다 읽고 나면 반드시 Close()
를 호출해야 리소스가 정리돼.
특히 defer resp.Body.Close()
를 쓰면 함수가 끝날 때 자동으로 닫혀서 안전해.
왜 http.Post()의 세 번째 인자는 io.Reader 타입을 요구해? 그리고 io.Reader가 뭐야?
ChatGPT의 말:
1️⃣ 왜 http.Post()
의 세 번째 인자는 io.Reader
타입을 요구할까?
http.Post(url string, contentType string, body io.Reader)
함수에서
세 번째 인자인 body
는 요청 본문(request body)을 의미해.
Go의 표준 라이브러리에서는 HTTP 요청의 본문을 스트림(stream) 방식으로 처리해.
📌 스트림이 필요한 이유
- HTTP 요청은 보통 큰 데이터를 포함할 수 있어 (예: 파일 업로드, JSON 데이터).
- 한 번에 모든 데이터를 메모리에 올리는 방식이 아니라,
필요할 때마다 데이터를 읽어오는 방식(스트리밍)으로 처리하는 게 더 효율적이야. io.Reader
는 이 스트림 방식으로 데이터를 읽을 수 있도록 해.
2️⃣ io.Reader
가 뭐야?
io.Reader
는 Go의 인터페이스(interface) 중 하나야.
이 인터페이스를 구현한 타입은 읽기 기능(Read method)을 제공해야 해.
type Reader interface {
Read(p []byte) (n int, err error)
}
📌 즉, Read()
메서드를 가진 타입은 모두 io.Reader
로 쓸 수 있어.
// 응답 읽기 body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(“Error reading response:”, err)
return
} 이 블록도 설명해줘
ChatGPT의 말:
📌 io.ReadAll(resp.Body)
블록 설명
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
이 블록은 HTTP 응답(response) 본문을 읽어오는 코드야.
1️⃣ io.ReadAll()
이 하는 일
func ReadAll(r io.Reader) ([]byte, error)
io.ReadAll()
은io.Reader
에서 모든 데이터를 읽고[]byte
로 반환해.- 즉,
resp.Body
(HTTP 응답 본문)를 한 번에 다 읽어서body
변수에 저장해. - 메모리 사용량이 크지 않은 경우에 적절해.
2️⃣ resp.Body
가 io.Reader
인 이유
resp, err := http.Post(url, "application/json", bytes.NewBuffer(requestBody))
defer resp.Body.Close()
http.Post()
를 호출하면resp.Body
는io.Reader
타입이야.- 즉, 응답 본문을 스트리밍 방식으로 읽어야 해.
- 하지만
io.ReadAll()
은 한 번에 모든 데이터를[]byte
로 변환해.
3️⃣ 에러 핸들링 (err != nil
체크)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
- 네트워크 장애 또는 서버 오류로 인해 응답을 읽지 못하면
err
가 발생. - 예를 들어, 서버가 응답을 보내기 전에 연결이 끊어지면 오류가 나.
- 이때 에러를 출력하고 함수 실행을 중단(return)해.
4️⃣ io.ReadAll()
vs io.Copy()
차이
io.ReadAll()
→ 응답을 전부 읽고[]byte
로 저장 (소규모 데이터에 적합)io.Copy(dst, src)
→ 스트리밍 방식으로 읽으면서 바로 저장 (대용량 데이터에 적합)
예제:
// 스트리밍 방식으로 읽고 바로 출력
io.Copy(os.Stdout, resp.Body)
즉, 작은 JSON 응답이면 io.ReadAll()
이 적절하고, 큰 데이터라면 io.Copy()
를 고려해야 해.
body의 타입은 뭐야? 어떻게 읽고 표현해? unmarshal 해야하나?
📌 body
의 타입과 처리 방법
body, err := io.ReadAll(resp.Body)
이 코드에서 body
의 타입은 []byte
(바이트 슬라이스)야.
즉, body
는 바이트 데이터로 된 JSON 응답 본문이야.
1️⃣ 어떻게 읽고 표현할까?
body
는 바이트 슬라이스([]byte
)라서, 일반적으로 문자열로 변환해서 출력할 수 있어.
fmt.Println(string(body)) // JSON 응답을 문자열로 출력
하지만 Go에서 JSON을 다룰 땐 json.Unmarshal()
로 변환하는 게 더 좋다.
2️⃣ json.Unmarshal()
을 사용해야 하는 이유
서버 응답이 JSON 형식이라면, 이를 Go의 구조체(struct
) 또는 맵(map[string]interface{}
)으로 변환하는 게 일반적이야.
예제 1: map[string]interface{}
로 변환 (유연한 방식)
var jsonResponse map[string]interface{} // 동적 JSON 파싱
err = json.Unmarshal(body, &jsonResponse)
if err != nil {
fmt.Println("Error parsing JSON:", err)
return
}
fmt.Println(jsonResponse) // 파싱된 JSON 출력
map[string]interface{}
를 사용하면 키-값 쌍을 동적으로 저장할 수 있어.- 단점: 타입 안정성이 없음 (값의 타입을 직접 확인해야 함).
예제 2: struct
로 변환 (타입 안정성 보장)
type APIResponse struct {
Message string `json:"message"`
}
var jsonResponse APIResponse
err = json.Unmarshal(body, &jsonResponse)
if err != nil {
fmt.Println("Error parsing JSON:", err)
return
}
fmt.Println(jsonResponse.Message) // 특정 필드만 사용 가능
- 구조체(
struct
)를 사용하면 타입이 정해져 있어서 안전해. - 하지만 JSON 필드가 변하면 코드도 수정해야 하는 단점이 있음.
3️⃣ 언제 json.Unmarshal()
을 써야 할까?
✅ 서버 응답이 JSON이고, 특정 필드를 다뤄야 할 때
✅ Go 코드에서 JSON 값을 쉽게 접근하고 조작하고 싶을 때
✅ JSON 데이터를 구조체(struct
)로 변환해서 타입 안정성을 유지하고 싶을 때
❌ 단순히 응답을 콘솔에 찍어볼 때는 string(body)
로 변환하면 됨.
좋았어!
1차 코드는 이제 이해했고
아까 2차 코드에 대한 질문에 대한 답을 할께💡 질문
http.Post()와 http.NewRequest()의 차이는?
코드에 없는걸 왜 물어봐?
모름;;json.Marshal(novaReq)가 하는 역할은?
novaReq에 들어있는 해시값, id, service_name, request(이건 유저가 보낸 바디)
을 json 화 하는 것. 이것을 나중에 byte화하여 http post로 보냄defer resp.Body.Close()가 없으면 어떤 문제가 발생할까?
메모리 누수?내가 틀린 부분이나 부족한 부분이 있다면 냉철하게 다 말해줘
✅ 피드백 (틀린 부분 & 부족한 부분)
1️⃣ ❌ http.Post()
와 http.NewRequest()
의 차이
➡ 코드에 없는 걸 왜 물어보냐고 했는데, 사실 네가 이해해야 할 중요한 개념이야.
➡ 둘 다 HTTP 요청을 보내는 방법인데, http.NewRequest()
는 더 유연함.
http.Post() |
http.NewRequest() + client.Do(req) |
|
---|---|---|
사용 편의성 | 간단한 요청에 적합 | 헤더, 메서드 설정 가능 |
헤더 추가 가능 여부 | ❌ 직접 추가 불가 | ✅ req.Header.Set() 으로 설정 가능 |
지원 HTTP 메서드 | POST만 지원 | GET, POST, PUT 등 모든 메서드 지원 |
권장 사용 시점 | 간단한 JSON API 요청 | API 키, 인증 헤더 필요할 때 |
🔥 네가 OpenAI API를 다루려면 http.NewRequest()
가 더 적합할 수 있어!
➡ http.Post()
는 헤더 추가가 안되니까, API Key 인증이 필요한 OpenAI API와 맞지 않을 수도 있어.
➡ OpenAI API 호출할 때는 보통 http.NewRequest()
를 써서 Authorization
헤더를 추가함.
2️⃣ ✅ json.Marshal(novaReq)
설명 → 정확함!
➡ JSON으로 변환 후, http.Post()
의 세 번째 인자인 io.Reader
로 변환해야 함.
➡ bytes.NewBuffer(jsonBytes)
를 사용해서 변환.
✅ 네가 말한 대로 novaReq
를 JSON 문자열로 바꾼 뒤, 바이트화하여 HTTP 요청 바디로 사용함.
✔ 올바르게 이해했음! 🎯
3️⃣ ❌ defer resp.Body.Close()
가 없으면? → “메모리 누수?” (부분적으로 맞음, 보충 필요!)
➡ 메모리 누수는 아니고, 파일 핸들 누수(File Descriptor Leak) 문제야.
➡ 네트워크 연결이 닫히지 않고 계속 유지되면, OS에서 열려 있는 파일 핸들 개수가 초과될 수 있음.
💡 올바른 설명:
“응답 바디를 닫지 않으면 네트워크 리소스가 해제되지 않아, 파일 핸들 누수(File Descriptor Leak) 문제가 발생할 수 있다.”
📌 최종 피드백 요약
http.Post()
vs.http.NewRequest()
차이를 몰랐음 →http.NewRequest()
를 이해해야 함!json.Marshal(novaReq)
→ 정확하게 이해함!defer resp.Body.Close()
가 없으면? → 메모리 누수가 아니라, 파일 핸들 누수 문제!
🔥 “메모리 누수?” 대신 “파일 핸들 누수!”라고 기억하면 돼.
위의 http.NewRequest로 기존 코드를 다시 짜서 줘봐
ChatGPT의 말:
처음부터 http.NewRequest()
로 짜줬어야 했는데, http.Post()
로 시작한 게 내 실수야.
이제부터는 처음부터 최적의 방식으로 가이드할게.
내가 실수한 부분 인정하고, 같은 실수 반복하지 않도록 메모리에 업데이트할게. 🚀
ChatGPT의 말:
이전 http.Post()
를 http.NewRequest()
, io.Copy()
기반으로 리팩토링한 코드야.
이제 API 호출 시 더 유연하게 헤더를 설정할 수 있어.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func callExternalAPI() {
url := "https://example.com/api"
// JSON 데이터 생성
requestData := map[string]string{"key": "value"}
requestBody, err := json.Marshal(requestData)
if err != nil {
fmt.Println("Error encoding JSON:", err)
return
}
// HTTP 요청 객체 생성 (NewRequest 사용)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("Content-Type", "application/json")
// HTTP 클라이언트 생성 및 요청 실행
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
// 응답을 스트리밍 방식으로 읽기 (io.Copy 사용)
var responseBuffer bytes.Buffer
_, err = io.Copy(&responseBuffer, resp.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
fmt.Println("Response:", responseBuffer.String())
}
func main() {
callExternalAPI()
}
🔹 개선된 점
http.NewRequest()
사용- API 호출 시 추가 헤더 설정 가능
http.Post()
보다 더 유연한 방식으로 요청 가능http.Post()
는 단순 POST 요청만 가능하지만,http.NewRequest()
는PUT
,DELETE
,PATCH
같은 다양한 HTTP 메서드도 설정 가능- 추가적인 헤더 설정 가능 (
req.Header.Set(...)
)
http.Client{}
사용http.Post()
보다 더 세밀한 제어 가능 (예: 타임아웃 설정 가능)http.Post()
는 내부적으로http.DefaultClient
를 사용해서 요청을 보냄.http.NewRequest()
를 쓰면 직접 HTTP 클라이언트를 설정하고, 재사용 가능한 클라이언트를 활용할 수 있음.
defer resp.Body.Close()
추가- 응답 본문을 안전하게 닫아 메모리 누수 방지
- 요청을 미리 만들고 보낼 수 있음
- 요청을 미리 만들어두고 필요할 때
client.Do(req)
로 보낼 수 있어서 유연함.
- 요청을 미리 만들어두고 필요할 때
이제 http.NewRequest()
기반으로 API 호출이 개선됐어!
이제 이 코드가 더 확장 가능하고 유지보수하기 좋은 구조야. 🚀
8단계 : Gin 미들웨어 이해하기 → 인증/로깅 미들웨어 추가
이제 Gin
의 미들웨어(Middleware) 를 다룰 거야.
미들웨어는 요청이 라우트 핸들러에 도달하기 전에 실행되는 함수야.
💡 목표
- API 키 인증 미들웨어 추가 → 특정 API 경로에 인증 요구
- 요청 로깅 미들웨어 추가 → 요청이 올 때마다 로그 출력
- 미들웨어가 실행되는 순서 이해
✅ 1단계: 기본 미들웨어 구조 이해
Gin에서 미들웨어는 func(c *gin.Context)
형태로 정의돼.
요청이 들어오면 미들웨어 → 핸들러 실행 → 응답 반환 순서로 처리돼.
func ExampleMiddleware(c *gin.Context) {
fmt.Println("🔹 미들웨어 실행됨!")
c.Next() // 다음 핸들러 실행
}
c.Next()
가 실행되면 다음 핸들러로 넘어감.
c.Abort()
를 사용하면 핸들러 실행을 중단할 수도 있어.
🔹 미들웨어와 핸들러 차이
✅ 미들웨어: 요청이 컨트롤러(핸들러)로 가기 전에 실행되는 함수
✅ 핸들러: 실제 요청을 처리하는 함수
💡 미들웨어 → 핸들러 실행 흐름
- 클라이언트 요청
- 미들웨어 실행 →
c.Next()
호출해야 핸들러 실행됨 - 핸들러 실행 → 응답 생성
- 미들웨어 후처리 (필요할 경우)
- 클라이언트에 응답 반환
🔹 예제 코드
func LoggerMiddleware(c *gin.Context) {
fmt.Println("🔹 요청 들어옴!")
c.Next() // 다음 핸들러로 진행
fmt.Println("🔹 응답 반환됨!")
}
func helloWorld(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, world!"})
}
func main() {
r := gin.Default()
// 미들웨어 적용
r.Use(LoggerMiddleware)
// 핸들러 등록
r.GET("/hello", helloWorld)
r.Run(":8080")
}
🚀 실행 흐름
1️⃣ 클라이언트 /hello
요청 → LoggerMiddleware 실행
→ "🔹 요청 들어옴!"
출력
2️⃣ c.Next()
호출 → helloWorld 핸들러 실행
→ "Hello, world!"
응답 생성
3️⃣ 핸들러 실행 후 다시 미들웨어로 돌아옴
→ "🔹 응답 반환됨!"
출력
4️⃣ 클라이언트에 응답 반환
그럼 미들웨어를 등록할땐 r.Use 이런식으로 함수를 등록한다는 거지? 그럼 모든 핸들러에 대해서 미들웨어가 실행되는거야? 미들웨어가 3개 있으면 어떤 핸들러가 수행되든 미들웨어3개가 수행돼?
🔹 미들웨어 등록과 실행 방식
✅ r.Use()
를 사용하면 모든 핸들러에 대해 미들웨어가 실행됨
✅ 미들웨어가 여러 개 등록되면 등록된 순서대로 실행됨
💡 예제 코드
func LoggerMiddleware(c *gin.Context) {
fmt.Println("🔹 요청 로깅 중...")
c.Next()
fmt.Println("🔹 응답 반환됨!")
}
func AuthMiddleware(c *gin.Context) {
token := c.GetHeader("Authorization")
if token != "valid-token" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
fmt.Println("🔹 인증 성공!")
c.Next()
}
func helloWorld(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, world!"})
}
func main() {
r := gin.Default()
// 🔥 모든 핸들러에 대해 실행되는 미들웨어
r.Use(LoggerMiddleware, AuthMiddleware)
r.GET("/hello", helloWorld)
r.Run(":8080")
}
🚀 실행 흐름
1️⃣ 클라이언트 /hello
요청
2️⃣ LoggerMiddleware 실행 → "🔹 요청 로깅 중..."
출력
3️⃣ AuthMiddleware 실행
- API 키 확인 (
"valid-token"
아니면 즉시 종료 →401 Unauthorized
) "🔹 인증 성공!"
출력
4️⃣ 핸들러 실행 (helloWorld
)"Hello, world!"
응답 생성
5️⃣ 응답 반환 후 다시 미들웨어 실행"🔹 응답 반환됨!"
출력
📌 미들웨어 3개 등록하면?
✅ 핸들러가 실행될 때 모든 미들웨어가 순서대로 실행됨
✅ c.Next()
호출하면 다음 미들웨어 실행 → 마지막에 핸들러 실행
✅ c.Abort()
호출하면 핸들러 실행 중단 (즉시 응답 반환)
💡 즉, r.Use()
에 등록한 미들웨어는 모든 요청에 대해 실행됨 🚀
✅ 2단계: API 키 인증 미들웨어 추가
🔹 API 키를 검증하는 미들웨어를 추가하자.
아래처럼 AuthMiddleware
를 만들어 특정 경로에 적용하면 돼.
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
apiKey := c.GetHeader("Authorization")
if apiKey == "" || !strings.HasPrefix(apiKey, "Bearer ") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing or invalid API Key"})
c.Abort() // ❌ 요청 중단 (핸들러 실행 안 됨)
return
}
c.Next() // ✅ 인증 성공 → 다음 핸들러 실행
}
}
이제 특정 엔드포인트에 적용하면 돼.
r := gin.Default()
r.Use(AuthMiddleware()) // 모든 요청에 적용
✔️ 특정 라우트에만 적용하고 싶다면?
r.POST("/secure-endpoint", AuthMiddleware(), secureHandler)
✅ 3단계: 요청 로깅 미들웨어 추가
요청이 올 때마다 메서드, 경로, 응답 코드를 로깅하자.
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Printf("📥 요청: %s %s\n", c.Request.Method, c.Request.URL.Path)
c.Next() // 다음 핸들러 실행
fmt.Printf("📤 응답: %d\n", c.Writer.Status())
}
}
모든 요청에 적용:
r := gin.Default()
r.Use(LoggerMiddleware()) // 모든 요청 로깅
✅ 4단계: 미들웨어 적용된 최종 코드
아래는 인증 & 로깅 미들웨어가 적용된 코드야.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// **🔹 API 키 인증 미들웨어**
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
apiKey := c.GetHeader("Authorization")
if apiKey == "" || !strings.HasPrefix(apiKey, "Bearer ") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing or invalid API Key"})
c.Abort()
return
}
c.Next()
}
}
// **🔹 요청 로깅 미들웨어**
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Printf("📥 요청: %s %s\n", c.Request.Method, c.Request.URL.Path)
c.Next()
fmt.Printf("📤 응답: %d\n", c.Writer.Status())
}
}
// **🔹 API 엔드포인트**
func securedEndpoint(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Secure Data"})
}
func main() {
r := gin.Default()
// **🚀 미들웨어 적용**
r.Use(LoggerMiddleware()) // 모든 요청 로깅
// 인증 미들웨어 적용 (특정 엔드포인트)
r.GET("/secure", AuthMiddleware(), securedEndpoint)
r.Run(":8080")
}
🛠️ 5단계: 테스트 (cURL)
1️⃣ API 키 없이 요청 (❌ 401 Unauthorized)
curl -X GET http://localhost:8080/secure
2️⃣ 올바른 API 키 포함 요청 (✅ 200 OK)
curl -X GET http://localhost:8080/secure \
-H "Authorization: Bearer valid-api-key"
🎯 최종 정리
c.Next()
→ 미들웨어 실행 후 다음 핸들러 실행c.Abort()
→ 이후 핸들러 실행 중단- 인증 미들웨어 → 특정 요청만 허용
- 로깅 미들웨어 → 모든 요청 로깅
이제 Gin 미들웨어 개념을 확실히 잡았어! 🔥
9단계(에러 핸들링)로 넘어갈 준비됐어? 🚀
이쯤에서 갑자기 문득 든 생각이, 이미 코드 이해가 얼추 되는데..? 뭔가 이상
물어봄
근데 우리 8단계가 미들웨어 이해하는거잖아, 코드에는 미들웨어가 없는데, 이건 개선 파트아냐? 일단 이해를 하고 개선하자고 했으니까 개선하는 부분이 왜 10단계 안에 있나 궁금해
말귀를 못 알아들어서 다시
개선을 위한 이해를 왜 지금 하냐는 거지 일단 코드를 이해하기 위해서 10단계 학습 계획을 세운거잖아 오히려 11단계에 미들웨어를 이해하고 기존 코드를 개선하면 돼지 지금 그러면 7단계까지만 해도 기존 코드는 이해되는거 아니냐는게 내 말이야
완전히 맞아! 미들웨어는 개선을 위한 개념이니까, 10단계 이후에 학습하는 게 더 적절해.
즉, 10단계까지는 기존 코드를 완벽하게 이해하는 게 우선이고,
미들웨어는 11단계 이후에 다뤄야 맞는 흐름이야.
📌 정리하면:
✅ 10단계까지 → 기존 코드 “이해”에 집중 (변경 X, 리팩토링 X)
✅ 11단계 이후 → 개선 (미들웨어 적용, 코드 정리, 최적화)
💡 그럼 8단계를 어떻게 할까?
👉 8단계는 미들웨어가 아니라, 기존 코드에서 부족한 개념 보충하기!
예를 들면:
http.NewRequest()
사용법 좀 더 깊게 보기defer
와io.Copy()
차이 더 이해하기- OpenAI API 호출 방식 완벽히 익히기
이런 식으로 기존 코드 “이해”에만 집중하는 방향으로 조정하면 좋겠어.
이제 8단계에서 어떤 부분을 보충하고 싶은지 의견을 줘봐! 🚀
하 짜증 ㅋㅋ
일단 고랭을 이해하기 위해선 미들웨어나 에러 핸들링을 하는게 맞지만 원래 목적은 그게 아니었으므로, 여기서 정리!
기존 코드를 나와 학습한 개념으로 다시 구현해달라고 함
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/swaggo/files"
"github.com/swaggo/gin-swagger"
_ "app/docs"
)
// 🔹 CloudConfig에서 API Key 기반으로 Service Name 조회 (Mock 데이터)
func getServiceName(apiKey string) (string, error) {
mockServiceNames := map[string]string{
"valid-api-key": "test-service",
}
serviceName, exists := mockServiceNames[apiKey]
if !exists {
return "", fmt.Errorf("invalid API key")
}
return serviceName, nil
}
// 🔹 API Key 추출 함수 (Authorization 헤더에서 "Bearer " 제거)
func getAPIKey(c *gin.Context) (string, error) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
return "", fmt.Errorf("missing or invalid API Key")
}
return strings.TrimPrefix(authHeader, "Bearer "), nil
}
// @Summary Proxy to Nova LLM API
// @Description OpenAI 형식의 요청을 Nova LLM을 통해 전달
// @Accept json
// @Produce json
// @Param request body map[string]interface{} true "Request Body"
// @Success 200 {object} map[string]interface{} "Success"
// @Failure 400 {object} map[string]string "Bad Request"
// @Failure 500 {object} map[string]string "Internal Server Error"
// @Router /v1/models/chat/completions [post]
func runV1(c *gin.Context) {
// 1️⃣ API Key 검증 및 추출
apiKey, err := getAPIKey(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
// 2️⃣ API Key를 기반으로 Service Name 조회
serviceName, err := getServiceName(apiKey)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid API Key"})
return
}
// 3️⃣ 요청 Body 파싱
var openAIRequest map[string]interface{}
if err := c.ShouldBindJSON(&openAIRequest); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
return
}
// 4️⃣ Nova LLM이 요구하는 형식으로 변환
novaReq := map[string]interface{}{
"id": "generated-uuid",
"service_name": serviceName,
"request": openAIRequest,
}
// 5️⃣ JSON 변환
requestBody, err := json.Marshal(novaReq)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to encode request"})
return
}
// 6️⃣ Nova LLM으로 API 요청 보내기 (http.NewRequest 사용)
client := &http.Client{}
req, err := http.NewRequest("POST", "http://nova-llm-gateway.alpha.tossinvest.bz/api/v1/openai/chat/completions",
bytes.NewBuffer(requestBody))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request"})
return
}
req.Header.Set("Content-Type", "application/json")
// 7️⃣ 요청 실행
resp, err := client.Do(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to call Nova LLM"})
return
}
defer resp.Body.Close()
// 8️⃣ 응답 읽기 (io.Copy 사용)
var buf bytes.Buffer
_, err = io.Copy(&buf, resp.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response"})
return
}
// 9️⃣ JSON 변환 및 응답 반환
var novaResponse map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &novaResponse); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse response"})
return
}
c.JSON(resp.StatusCode, novaResponse)
}
// @Summary Proxy to OpenAI API
// @Description OpenAI로 직접 요청 전달
// @Accept json
// @Produce json
// @Param request body map[string]interface{} true "Request Body"
// @Success 200 {object} map[string]interface{} "Success"
// @Failure 400 {object} map[string]string "Bad Request"
// @Failure 500 {object} map[string]string "Internal Server Error"
// @Router /v2/models/chat/completions [post]
func runV2(c *gin.Context) {
// 1️⃣ API Key 검증 및 추출
apiKey, err := getAPIKey(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
// 2️⃣ 요청 Body 파싱
var openAIRequest map[string]interface{}
if err := c.ShouldBindJSON(&openAIRequest); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
return
}
// 3️⃣ JSON 변환
requestBody, err := json.Marshal(openAIRequest)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to encode request"})
return
}
// 4️⃣ OpenAI로 API 요청 보내기 (http.NewRequest 사용)
client := &http.Client{}
req, err := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions",
bytes.NewBuffer(requestBody))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request"})
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
// 5️⃣ 요청 실행
resp, err := client.Do(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to call OpenAI"})
return
}
defer resp.Body.Close()
// 6️⃣ 응답 읽기 (io.Copy 사용)
var buf bytes.Buffer
_, err = io.Copy(&buf, resp.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response"})
return
}
// 7️⃣ JSON 변환 및 응답 반환
var openAIResponse map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &openAIResponse); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse response"})
return
}
c.JSON(resp.StatusCode, openAIResponse)
}
// @Summary Hello World Endpoint
// @Description 간단한 헬로 월드 API
// @Accept json
// @Produce json
// @Success 200 {object} map[string]string "Success"
// @Router /v1/hello [get]
func helloWorld(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, World!"})
}
// @title ML API Gateway
// @version 1.0
// @description MLP API Gateway
// @host localhost:10100
func main() {
r := gin.Default()
// Swagger UI 추가
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// 엔드포인트 등록
r.GET("/v1/hello", helloWorld)
r.POST("/v1/models/chat/completions", runV1) // Nova LLM 경유
r.POST("/v2/models/chat/completions", runV2) // OpenAI 직접 호출
r.Run(":8080")
}
좋아! 여태까지 내가 학습한 내용을 요약 정리해줘 나처럼 고랭을 모르는 사람들에게 너가 준 코드를 설명하기 위함이야
ChatGPT의 말:
📌 고랭(Golang) 기반 API 서버 개발 요약 정리
(👨💻 대상: Golang을 처음 접하는 개발자들)
1️⃣ Golang 기본 개념 및 Gin 프레임워크 이해
🔹 Gin이란?
- Go 언어의 웹 프레임워크 (FastAPI 같은 역할)
gin.Default()
→ 기본 로깅 및 복구 미들웨어 포함r.GET()
→ GET 요청,r.POST()
→ POST 요청 처리c.JSON()
→ JSON 응답을 반환하는 함수
✅ 기본적인 “Hello, World!” API
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func helloWorld(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, World!"})
}
func main() {
r := gin.Default()
r.GET("/hello", helloWorld)
r.Run(":8080")
}
✅ 핵심 개념
c *gin.Context
→ HTTP 요청 & 응답 관리c.JSON(http.StatusOK, gin.H{...})
→ JSON 응답gin.H{}
→map[string]interface{}
의 축약 표현
2️⃣ JSON 요청 & 응답 처리
🔹 POST 요청을 받아 JSON 데이터를 처리하기
func echo(c *gin.Context) {
var jsonData map[string]interface{} // JSON을 받을 변수
if err := c.BindJSON(&jsonData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"})
return
}
c.JSON(http.StatusOK, gin.H{"received": jsonData})
}
✅ 핵심 개념
c.BindJSON()
→ 요청 바디를 JSON으로 파싱- 에러가 발생하면
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"})
반환
3️⃣ URL 및 쿼리 파라미터 다루기
🔹 URL 파라미터 (/greet/:name
)
func greet(c *gin.Context) {
name := c.Param("name") // URL에서 name 값 가져오기
c.JSON(http.StatusOK, gin.H{"message": "Hello, " + name})
}
요청 예시:
curl -X GET "http://localhost:8080/greet/Alice"
응답:
{"message": "Hello, Alice"}
🔹 쿼리 파라미터 (/search?query=golang
)
func search(c *gin.Context) {
query := c.Query("query") // "query" 파라미터 값 가져오기
if query == "" {
query = "default"
}
c.JSON(http.StatusOK, gin.H{"message": "Searching for: " + query})
}
요청 예시:
curl -X GET "http://localhost:8080/search?query=golang"
응답:
{"message": "Searching for: golang"}
✅ 핵심 개념
c.Param("name")
→ URL 경로에서 값 추출c.Query("query")
→ URL 쿼리 파라미터 값 가져오기
4️⃣ API 인증 (헤더 처리)
🔹 헤더에서 API Key 검증
func authExample(c *gin.Context) {
apiKey := c.GetHeader("Authorization") // 헤더에서 API 키 가져오기
if apiKey == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "API Key missing"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Authorized", "api_key": apiKey})
}
요청 예시:
curl -X GET "http://localhost:8080/auth" -H "Authorization: Bearer valid-key"
응답:
{"message": "Authorized", "api_key": "Bearer valid-key"}
✅ 핵심 개념
c.GetHeader("Authorization")
→ HTTP 헤더 값 가져오기- API Key가 없으면
401 Unauthorized
응답
5️⃣ 외부 API 호출 (http.NewRequest 사용)
🔹 외부 API 호출 및 응답 처리 (Nova LLM / OpenAI)
func callAPI(c *gin.Context) {
url := "https://example.com/api"
requestBody, _ := json.Marshal(map[string]string{"key": "value"})
client := &http.Client{}
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "API call failed"})
return
}
defer resp.Body.Close()
var buf bytes.Buffer
io.Copy(&buf, resp.Body) // 대용량 데이터 처리
c.JSON(resp.StatusCode, gin.H{"response": buf.String()})
}
✅ 핵심 개념
http.NewRequest("POST", url, bytes.NewBuffer(body))
→ 요청 생성client.Do(req)
→ 요청 실행io.Copy(&buf, resp.Body)
→ 대용량 데이터 스트리밍 처리
💡 학습한 주요 개념 정리
✅ Gin 기본 개념: gin.Default()
, c.JSON()
, c.Param()
, c.Query()
✅ JSON 요청 처리: c.BindJSON()
, c.ShouldBindJSON()
✅ 헤더 검증: c.GetHeader()
활용
✅ 외부 API 호출: http.NewRequest()
, client.Do(req)
✅ 대용량 응답 처리: io.Copy()
활용
✅ 프로젝트 구조화: cmd/
, internal/
, configs/
등 분리
이제 너는 Gin을 사용한 API 서버 개발과 외부 API 호출을 확실히 이해했어!
이제 Swagger 문서 추가, 미들웨어, 에러 핸들링 최적화를 배우면 완전한 API 서버를 만들 수 있어 🚀🔥
추가로 궁금한 점 있으면 질문해줘!
gin.Context가 핵심같은데 c.JSON부터 c.뭐로 시작을 많이 하잖아 주요 함수들? 사례들 알려주고 참고할수있는 공식문서나 자료 줘
ChatGPT의 말:
✅ gin.Context
주요 함수 & 사례 정리
gin.Context
는 Gin에서 HTTP 요청 & 응답을 다루는 핵심 객체야.
핸들러 내부에서 c.메서드()
형식으로 호출해서 사용해.
1️⃣ 요청(Request) 관련
메서드 | 설명 | 예제 |
---|---|---|
c.Param(key) |
URL 파라미터 값 가져오기 | c.Param("id") → /users/:id |
c.Query(key) |
쿼리 스트링 값 가져오기 | c.Query("name") → ?name=John |
c.DefaultQuery(key, default) |
쿼리 스트링 값 없으면 기본값 설정 | c.DefaultQuery("lang", "en") |
c.PostForm(key) |
application/x-www-form-urlencoded 값 가져오기 |
c.PostForm("username") |
c.ShouldBindJSON(&obj) |
JSON 요청 바디를 구조체로 매핑 | c.ShouldBindJSON(&user) |
c.GetHeader("Header-Name") |
요청 헤더 값 가져오기 | c.GetHeader("Authorization") |
2️⃣ 응답(Response) 관련
메서드 | 설명 | 예제 |
---|---|---|
c.JSON(status, obj) |
JSON 응답 반환 | c.JSON(200, gin.H{"message": "Hello"}) |
c.String(status, text) |
문자열 응답 반환 | c.String(200, "Hello World") |
c.HTML(status, template, data) |
HTML 응답 반환 | c.HTML(200, "index.html", data) |
c.Redirect(status, url) |
다른 URL로 리다이렉트 | c.Redirect(302, "/home") |
c.AbortWithStatus(status) |
응답하고 핸들러 실행 중단 | c.AbortWithStatus(401) |
3️⃣ 컨텍스트(Context) 제어
메서드 | 설명 | 예제 |
---|---|---|
c.Next() |
다음 미들웨어 실행 | c.Next() |
c.Abort() |
이후 핸들러 실행 중단 | c.Abort() |
c.Set(key, value) |
컨텍스트에 데이터 저장 | c.Set("user", userObj) |
c.Get(key) |
저장된 데이터 가져오기 | user, exists := c.Get("user") |
https://github.com/gin-gonic/gin/blob/master/context.go
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Params Params
handlers HandlersChain
index int8
fullPath string
engine *Engine
params *Params
skippedNodes *[]skippedNode
// This mutex protects Keys map.
mu sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]any
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs
// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
// queryCache caches the query result from c.Request.URL.Query().
queryCache url.Values
// formCache caches c.Request.PostForm, which contains the parsed form data from POST, PATCH,
// or PUT body parameters.
formCache url.Values
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests.
sameSite http.SameSite
}
생각해보니 swag 해야 되지 않나..?해서 아래처럼 이야기
웅 좋아 우리 오히려 단계8,9,10은 swag공부하는게 어때? swag는 자동생성이긴 하지만 주석도 작성해야하고 무엇보다 지금 너가 작성해준 주석을 이해를 못하겠어
좋아! 단계 8, 9, 10을 Swag 학습으로 바꾸자.
✅ 목표: Swag가 어떻게 동작하는지 이해하고, API 문서를 올바르게 작성하는 것!
🔟 새로운 8~10단계 학습 계획
✅ 8️⃣ Swag의 기본 개념과 설치 → Swag의 역할 이해 & 설치 확인
✅ 9️⃣ Swag 주석 이해하고 직접 작성하기 → @Summary
, @Param
등
✅ 🔟 Swag UI 실행 및 검토 → 생성된 문서가 제대로 동작하는지 확인
8단계 : Swag의 기본 개념과 설치
1️⃣ Swag가 뭔데?
- Swag는 Go 코드에서 OpenAPI 문서를 자동 생성하는 도구야.
@Summary
,@Param
같은 주석을 분석해서 API 문서를 만들어.- 생성된 문서는 Swagger UI를 통해 쉽게 볼 수 있어.
2️⃣ Swag 설치 확인
이미 swaggo/swag
을 설치했으니까, 아래 명령어로 Swag가 있는지 확인해봐.
swag --version
출력 예시:
swag version v1.16.4
3️⃣ Swag 문서 생성하기
Swag 주석을 코드에 작성한 후, 아래 명령어를 실행하면 Swagger 문서가 생성돼.
swag init -g main.go
실행하면 docs/
폴더가 생기고, swagger.json
, swagger.yaml
, docs.go
파일이 생성돼.
📌 👉 8단계 진행 방법
swag --version
으로 Swag 설치 확인swag init -g main.go
실행 후docs/
폴더 생성 확인- Swagger UI에서 문서가 표시되는지 확인 (
http://localhost:8080/swagger/index.html
)
🛠 이걸 다 해보고 결과 알려줘!
(설치 문제 있으면 해결해 줄게) 🚀
9️단계 : Swag 주석 이해하고 직접 작성하기
Swag는 주석을 기반으로 API 문서를 자동 생성해.
아래 주석들을 하나씩 살펴보자. 👀
1️⃣ @Summary
& @Description
✅ API의 간단한 설명 & 상세 설명을 추가할 때 사용.
// @Summary 사용자에게 "Hello, World!" 메시지 반환
// @Description 이 API는 단순히 "Hello, World!" 메시지를 반환합니다.
2️⃣ @host
& @BasePath
✅ API의 기본 호스트 및 경로 설정.
// @host localhost:8080
// @BasePath /v1
📌 @host
→ API 서버의 기본 도메인 (Swagger UI에서 호출할 기본 주소)
📌 @BasePath
→ 모든 엔드포인트 앞에 붙을 기본 경로
3️⃣ @Router
(라우팅 설정)
✅ 어떤 HTTP 메서드로 호출할 수 있는지 명시
// @Router /v1/hello [get]
🔹 형식
@Router <경로> [<HTTP 메서드>]
📌 예시
// @Router /v1/models/chat/completions [post]
→ /v1/models/chat/completions
경로에서 POST 요청을 받음
4️⃣ @Accept
& @Produce
✅ API가 어떤 형식의 데이터를 받을 수 있고 반환하는지 정의.
// @Accept json
// @Produce json
5️⃣ @Param
(요청 값 설명)
✅ API의 파라미터(쿼리, URL, 바디 등)를 정의.
// @Param name path string true "사용자 이름"
// @Param query query string false "검색어"
6️⃣ @Success
, @Failure
(응답 설명)
✅ API가 성공하면 어떤 응답을 반환하는지 정의.
// @Success 200 {object} map[string]string "성공 시 반환 값"
// @Failure 400 {object} map[string]string "잘못된 요청"
@Success 200
→ 성공했을 때 응답@Failure 400
→ 잘못된 요청일 때 응답
✅ @Success 200 {object} map[string]interface{} "Success"
설명
// @Success 200 {object} map[string]interface{} “Success”
-
@Success 200
- HTTP 상태 코드 200을 의미 (성공적인 응답)
-
{object}
- 반환되는 데이터 타입이 객체(JSON)임을 의미
string
,array
,boolean
같은 타입이면{string}
,{array}
사용
-
map[string]interface{}
- Go에서 실제 반환하는 타입
- key-value 형식의 JSON 객체를 의미
- 예:
{ "message": "Hello, World!" }
-
"Success"
- Swagger UI에서 API 응답 설명
"Success"
는 이 응답이 정상적인 성공 응답임을 의미
![[blog/images/blog/_posts/2025-03-06-training-go-and-build-api-server-2/IMG-20250306120503985.png]]
📝 예제
// @Success 200 {object} map[string]interface{} “요청이 성공하면 JSON 객체 반환”
📌 위 주석이 Swagger 문서에 나타나면,
📌 응답이 { "message": "Hello, World!" }
같은 JSON 객체임을 알 수 있음
📌 9단계 실습 목표
1️⃣ 기존 코드의 helloWorld
, ping
, search
, echo
에 Swag 주석 추가
2️⃣ swag init -g main.go
실행해서 Swagger 문서 갱신
3️⃣ http://localhost:8080/swagger/index.html
에서 확인
🔹 직접 코드에 주석 추가해보고 결과 알려줘!
(잘못된 부분 있으면 바로 잡아줄게 🚀)
댓글남기기