Gin 기초 #4 응답 처리 — JSON, 상태 코드, 에러
지난 글에서 요청을 받아 검증하는 법을 다뤘습니다. 이번 글은 반대 방향, 즉 응답을 만드는 법을 정리합니다. JSON 외의 응답 형식, 상태 코드를 다루는 법, 그리고 API 전체가 일관된 에러 형식을 갖도록 만드는 패턴까지 봅니다.
- #1 시작과 첫 서버
- #2 라우팅과 핸들러
- #3 요청 바인딩과 검증
- #4 응답 처리 — JSON, 상태 코드, 에러 ← 이번 글
- #5 미들웨어
- #6 데이터베이스 연동 (GORM)
- #7 프로젝트 구조와 미니 REST API
JSON 응답 다시 보기 #
가장 많이 쓰는 응답은 JSON입니다. c.JSON은 상태 코드와 데이터를 받습니다.
c.JSON(http.StatusOK, gin.H{"message": "ok"})구조체를 그대로 넘겨도 됩니다. json 태그에 따라 직렬화됩니다.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
c.JSON(http.StatusOK, User{ID: 1, Name: "커티스"})
// {"id":1,"name":"커티스"}omitempty를 붙인 Email은 값이 비어 있어서 출력에서 빠졌습니다. 응답에서 빈 필드를 감추고 싶을 때 자주 씁니다.
들여쓰기된 JSON이 필요하면 c.IndentedJSON을, HTML 특수문자를 이스케이프하지 않은 그대로 내보내려면 c.PureJSON을 씁니다. 다만 운영 API에서는 용량을 줄이기 위해 보통 c.JSON을 그대로 씁니다.
상태 코드 #
상태 코드는 숫자 대신 net/http의 상수를 쓰는 편이 읽기 좋습니다.
http.StatusOK // 200
http.StatusCreated // 201
http.StatusNoContent // 204
http.StatusBadRequest // 400
http.StatusUnauthorized // 401
http.StatusForbidden // 403
http.StatusNotFound // 404
http.StatusInternalServerError // 500리소스를 새로 만들었으면 201 Created, 삭제처럼 돌려줄 본문이 없으면 204 No Content를 쓰는 식으로 의미에 맞게 고릅니다.
c.Status(http.StatusNoContent) // 204, 본문 없음JSON 외의 응답 형식 #
JSON만 응답하는 것은 아닙니다. 상황에 따라 여러 형식을 쓸 수 있습니다.
c.String(http.StatusOK, "안녕, %s", name) // 일반 텍스트
c.Data(http.StatusOK, "image/png", bytes) // 바이트 그대로
c.XML(http.StatusOK, data) // XML
c.YAML(http.StatusOK, data) // YAMLc.String은 포맷 문자열을 받아 fmt.Sprintf처럼 동작합니다. c.Data는 Content-Type과 바이트 슬라이스를 직접 넘겨, 이미지나 PDF 같은 임의의 바이너리를 응답할 때 씁니다.
파일 응답 #
파일을 그대로 내려주려면 c.File을 씁니다.
c.File("./static/report.pdf")브라우저가 다운로드하도록 파일 이름을 지정하려면 c.FileAttachment를 씁니다.
c.FileAttachment("./static/report.pdf", "월간보고서.pdf")이렇게 하면 Content-Disposition 헤더가 설정되어, 브라우저가 지정한 이름으로 저장 창을 띄웁니다.
리다이렉트 #
다른 URL로 보내려면 c.Redirect를 씁니다.
c.Redirect(http.StatusFound, "/login") // 302영구 이동이면 http.StatusMovedPermanently(301)를 씁니다.
헤더 설정 #
응답 헤더는 c.Header로 직접 지정합니다.
c.Header("X-Request-ID", requestID)
c.Header("Cache-Control", "no-store")일관된 에러 응답 만들기 #
API가 커지면 에러 응답 형식이 핸들러마다 제각각이 되기 쉽습니다. 어떤 곳은 {"error": "..."}, 어떤 곳은 {"message": "..."} 식으로 갈리면 클라이언트가 처리하기 어렵습니다. 형식을 하나로 정해 두는 편이 좋습니다.
먼저 에러 응답 구조체와 헬퍼를 정의합니다.
type ErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
}
func respondError(c *gin.Context, status int, code, message string) {
c.JSON(status, ErrorResponse{Code: code, Message: message})
}핸들러에서는 이 헬퍼만 호출합니다.
func getUser(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
respondError(c, http.StatusBadRequest, "INVALID_ID", "id는 숫자여야 합니다")
return
}
user, found := findUser(id)
if !found {
respondError(c, http.StatusNotFound, "USER_NOT_FOUND", "사용자를 찾을 수 없습니다")
return
}
c.JSON(http.StatusOK, user)
}이렇게 하면 모든 에러 응답이 같은 모양을 갖습니다. 클라이언트는 code 값으로 분기하고, message를 사용자에게 보여 줄 수 있습니다. 중급 시리즈에서는 이 패턴을 미들웨어 기반의 중앙 에러 처리로 발전시키겠습니다.
Abort — 처리 중단 #
미들웨어 안에서 또는 핸들러 초입에서 요청을 즉시 끝내야 할 때가 있습니다. c.AbortWithStatusJSON은 응답을 보내면서 이후 핸들러 체인의 실행을 막습니다.
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "인증 필요"})
returnAbort 계열과 미들웨어의 관계는 #5 미들웨어에서 자세히 다루겠습니다.
마무리 #
이번 글에서 정리한 내용입니다.
- JSON은
c.JSON, 구조체의omitempty로 빈 필드를 감출 수 있음 - 상태 코드는
http.StatusCreated처럼 표준 상수로, 의미에 맞게 선택 - 본문 없는 응답은
c.Status, 문자열은c.String, 바이트는c.Data - 파일은
c.File, 다운로드는c.FileAttachment - 리다이렉트는
c.Redirect, 헤더는c.Header - 에러 응답은 형식을 하나로 정하고 헬퍼로 통일
- 요청을 즉시 끝낼 때는
c.AbortWithStatusJSON
다음 글(#5 미들웨어)에서는 로깅, 복구, 인증처럼 여러 핸들러에 공통으로 거는 처리를 미들웨어로 묶는 법을 정리하겠습니다.