backend

Gin으로 API 서버 옮기기: 기존 net/http 구조 그대로 활용하기

mirabo01 2026. 2. 2. 22:28

이전 챕터에서
net/http + handler / service / repository 구조로 CRUD API를 만들었다면,
이번에는 그 구조를 Gin 프레임워크로 옮겨보는 단계다.

이번 챕터의 핵심은 분명하다.

  • Gin 문법을 많이 아는 것 ❌
  • 기존 구조를 깨지 않고 프레임워크만 교체 ⭕️

즉,
“Gin을 쓰면 코드 구조가 어떻게 달라지는가”가 아니라
**“달라지지 않아야 하는 부분은 무엇인가”**에 초점을 둔다.


왜 이 타이밍에 Gin을 보는가

Gin은 Go 웹 프레임워크 중 가장 많이 사용된다.
하지만 실무에서 Gin을 잘 쓰는 사람들의 공통점은 이거다.

Gin 이전에 net/http 흐름을 한 번은 이해했다

이 과정을 거치면

  • Gin의 Context가 왜 필요한지
  • 미들웨어가 어디에 끼어드는지
  • 테스트를 어떻게 해야 하는지

가 훨씬 자연스럽게 연결된다.


이번 챕터에서 바꾸지 않을 것

먼저 바꾸지 않을 것부터 명확히 하자.

  • service 계층
  • repository 계층
  • model 구조
  • 비즈니스 로직

즉, HTTP 레이어만 Gin으로 교체한다.

handler (변경 대상)
service (유지)
repository (유지)

이 기준이 무너지면
프레임워크 종속 코드가 빠르게 퍼지기 시작한다.


Gin 도입 후 전체 구조

cmd/server/main.go
internal/handler/
internal/service/
internal/repository/
internal/model/

디렉터리 구조는 그대로 유지한다.
바뀌는 건 main.go와 handler 구현 방식뿐이다.

[이미지: net/http → Gin 전환 구조 비교]


main.go: 서버 시작 코드 변화

net/http 버전

mux := http.NewServeMux()
mux.HandleFunc("/users", handler.List)
http.ListenAndServe(":8080", mux)

Gin 버전

r := gin.New()
r.GET("/users", handler.List)
r.Run(":8080")

차이는 명확하다.

  • ServeMux → Router
  • HandlerFunc → Gin handler
  • ListenAndServe → Run

하지만 서버 역할 자체는 동일하다.


Gin handler의 기본 형태

Gin에서는 handler 시그니처가 이렇게 바뀐다.

func (h *UserHandler) List(c *gin.Context) {
    users, err := h.service.ListUsers()
    if err != nil {
        c.JSON(500, gin.H{"error": "internal error"})
        return
    }

    c.JSON(200, users)
}

차이점은 다음 정도다.

  • http.ResponseWriter, *http.Request → *gin.Context
  • 응답 작성이 훨씬 간결해짐
  • 상태 코드 + JSON 한 줄로 처리 가능

Gin Context는 무엇을 대신하는가

Gin의 Context는 사실상 다음을 묶어둔 객체다.

  • 요청 정보 (*http.Request)
  • 응답 작성 기능
  • path / query / body 접근
  • middleware 공유 데이터

즉,

net/http에서 흩어져 있던 것들을 한 객체로 모아둔 래퍼

라고 이해하면 과하지 않다.


요청 파라미터 처리 비교

Query Parameter

// net/http
id := r.URL.Query().Get("id")

// Gin
id := c.Query("id")

JSON Body

// Gin
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
    c.JSON(400, gin.H{"error": "invalid body"})
    return
}

실제로 써보면
Gin이 HTTP 레이어 보일러플레이트를 많이 줄여준다는 걸 체감하게 된다.

[이미지: Gin 요청 처리 흐름]


라우팅 그룹 활용하기

Gin에서는 라우팅을 그룹으로 묶을 수 있다.

api := r.Group("/api")
{
    api.GET("/users", handler.List)
    api.POST("/users", handler.Create)
}
  • URL 구조가 한눈에 들어옴
  • 버전 관리(/v1)에도 유리

이건 ServeMux에서는 직접 구현해야 했던 부분이다.


미들웨어 적용 방식 비교

Gin 미들웨어 기본 형태

func LoggingMiddleware(c *gin.Context) {
    log.Println(c.Request.Method, c.Request.URL.Path)
    c.Next()
}
r.Use(LoggingMiddleware)
  • 전역 미들웨어
  • 그룹 단위 미들웨어
  • 특정 라우트 전용 미들웨어

모두 명확하게 표현 가능하다.

이 구조는
이전 챕터에서 만든 net/http 미들웨어 개념과 완전히 동일한 역할이다.


중요한 설계 기준: Gin을 service로 넘기지 말 것

⚠️ 가장 중요한 주의사항

  • *gin.Context를 service로 넘기지 않는다
  • service는 HTTP를 몰라야 한다
  • handler에서 데이터만 추출해서 전달한다
// ❌ 나쁜 예
service.DoSomething(c)

// ⭕️ 좋은 예
service.DoSomething(userID)

이 기준 하나만 지켜도
프레임워크 종속이 급격히 줄어든다.


Gin 환경에서의 테스트 전략

Gin handler도
httptest로 충분히 테스트 가능하다.

w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/users", nil)
router.ServeHTTP(w, req)
  • 실제 서버 실행 불필요
  • 기존 테스트 전략 그대로 유지 가능

즉,
프레임워크 교체가 테스트 구조를 깨지 않는다.


net/http → Gin 전환의 핵심 정리

이 챕터에서 얻어야 할 감각은 이거다.

  • Gin은 생산성을 높여주는 도구
  • 구조를 대신 설계해주지는 않는다
  • 잘 만든 구조 위에서 써야 효과가 난다

Gin을 썼다고 해서
코드가 자동으로 “좋아지지는 않는다”.


정리

Gin 전환의 핵심은
**“프레임워크를 바꿔도 구조는 그대로 유지되는가”**다.

  • handler만 Gin에 맞게 수정
  • service / repository는 그대로 유지
  • HTTP 편의 기능만 선택적으로 활용

이 기준을 지키면

  • Gin → Echo 전환
  • HTTP → gRPC 확장

같은 작업도 훨씬 수월해진다.

다음 챕터로는

  • PostgreSQL 연동 + migration
  • 인증/인가(JWT) 미들웨어 설계
  • 실무용 에러 처리 전략(API 에러 규약)

중 하나로 이어가면 좋다.