Gin 기초 #4 응답 처리 — JSON, 상태 코드, 에러

4 분 소요

지난 글에서 요청을 받아 검증하는 법을 다뤘습니다. 이번 글은 반대 방향, 즉 응답을 만드는 법을 정리합니다. JSON 외의 응답 형식, 상태 코드를 다루는 법, 그리고 API 전체가 일관된 에러 형식을 갖도록 만드는 패턴까지 봅니다.

  • #1 시작과 첫 서버
  • #2 라우팅과 핸들러
  • #3 요청 바인딩과 검증
  • #4 응답 처리 — JSON, 상태 코드, 에러 ← 이번 글
  • #5 미들웨어
  • #6 데이터베이스 연동 (GORM)
  • #7 프로젝트 구조와 미니 REST API

JSON 응답 다시 보기 #

가장 많이 쓰는 응답은 JSON입니다. c.JSON은 상태 코드와 데이터를 받습니다.

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)               // YAML

c.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": "인증 필요"})
return

Abort 계열과 미들웨어의 관계는 #5 미들웨어에서 자세히 다루겠습니다.

마무리 #

이번 글에서 정리한 내용입니다.

  • JSON은 c.JSON, 구조체의 omitempty로 빈 필드를 감출 수 있음
  • 상태 코드는 http.StatusCreated처럼 표준 상수로, 의미에 맞게 선택
  • 본문 없는 응답은 c.Status, 문자열은 c.String, 바이트는 c.Data
  • 파일은 c.File, 다운로드는 c.FileAttachment
  • 리다이렉트는 c.Redirect, 헤더는 c.Header
  • 에러 응답은 형식을 하나로 정하고 헬퍼로 통일
  • 요청을 즉시 끝낼 때는 c.AbortWithStatusJSON

다음 글(#5 미들웨어)에서는 로깅, 복구, 인증처럼 여러 핸들러에 공통으로 거는 처리를 미들웨어로 묶는 법을 정리하겠습니다.

X