이전 챕터에서 net/http로 가장 단순한 API 서버를 만들었다면,
이번에는 그 서버를 **조금 더 “실제 서비스에 가까운 형태”**로 확장해본다.
이번 챕터의 핵심은 다음 두 가지다.
- 라우팅 로직을 어떻게 정리하는가
- 공통 로직을 미들웨어 형태로 어떻게 분리하는가
프레임워크 없이도
구조적으로 정리된 서버 코드를 만드는 게 목표다.
왜 라우팅 분리가 필요한가
단순 예제에서는 이런 코드가 문제 없어 보인다.
http.HandleFunc("/ping", pingHandler)
http.HandleFunc("/users", userHandler)
http.HandleFunc("/orders", orderHandler)
하지만 엔드포인트가 늘어나면 곧 한계가 온다.
- main 함수가 비대해짐
- 핸들러 간 책임이 섞임
- 테스트가 어려워짐
그래서 실무에서는
라우팅 정의와 핸들러 구현을 분리하는 게 일반적이다.
기본적인 라우팅 분리 구조
가장 단순하면서 많이 쓰이는 구조는 다음이다.
cmd/server/main.go
internal/handler/
├── ping.go
└── user.go
main.go
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/ping", handler.Ping)
mux.HandleFunc("/users", handler.Users)
http.ListenAndServe(":8080", mux)
}
- 라우팅만 담당
- 실제 로직은 handler 패키지로 위임
이렇게 하면
서버 전체 흐름이 한눈에 들어온다.
[이미지: Go 라우팅 분리 구조]
ServeMux 이해하기
http.ServeMux는 Go 기본 라우터다.
mux := http.NewServeMux()
- URL path 기반 매칭
- 가장 긴 패턴 우선 매칭
- 성능, 안정성 면에서 충분히 검증됨
복잡한 REST 라우팅이 아니라면
ServeMux만으로도 충분한 경우가 많다.
Handler를 함수에서 구조체로 확장하기
의존성이 늘어나면
단순 함수형 핸들러는 한계가 있다.
type UserHandler struct {
service *UserService
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// handler logic
}
이 방식의 장점은 명확하다.
- 의존성 주입 가능
- 테스트 시 mock 대체 가능
- 상태 없는 handler 구조 유지
실무에서는 이 패턴을 기본으로 가져가는 경우가 많다.
미들웨어란 무엇인가
미들웨어는
요청과 핸들러 사이에 끼어드는 공통 로직이다.
대표적인 예시는 다음과 같다.
- 로깅
- 인증 / 인가
- 요청 시간 측정
- panic 복구
모든 핸들러에서 반복되는 코드를
한 번만 작성할 수 있게 해준다.
Go에서의 미들웨어 기본 형태
Go에서 미들웨어는 보통 이런 형태를 가진다.
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
- 입력: http.Handler
- 출력: http.Handler
이 형태만 기억해두면
미들웨어는 얼마든지 조합할 수 있다.
[이미지: Go 미들웨어 호출 흐름]
미들웨어 적용하기
handler := Logging(http.HandlerFunc(pingHandler))
mux.Handle("/ping", handler)
혹은 헬퍼 함수로 감싸서 사용한다.
func wrap(h http.Handler) http.Handler {
return Logging(h)
}
이렇게 하면
라우팅 정의부에서 미들웨어 적용 여부가 한눈에 보인다.
여러 미들웨어 조합하기
handler := Logging(Auth(pingHandler))
실행 순서는 다음과 같다.
- Logging
- Auth
- 실제 handler
⚠️ 주의할 점
- 순서가 의미를 가진다
- 인증 → 로깅 → 비즈니스 로직 같은 흐름을 의식해야 한다
미들웨어 체인은
읽기 쉬운 순서로 구성하는 것이 중요하다.
context를 통한 값 전달
미들웨어에서 값을 전달할 때는
context.Context를 사용한다.
ctx := context.WithValue(r.Context(), "userID", id)
next.ServeHTTP(w, r.WithContext(ctx))
핸들러에서는 다음처럼 꺼내 쓴다.
userID := r.Context().Value("userID")
⚠️ 주의할 점
- context는 요청 수명 동안만 유효
- 키는 충돌 방지를 위해 전용 타입 사용 권장
REST API 형태로 정리해보기
이제 구조를 REST 형태로 보면 다음과 같다.
GET /users
POST /users
GET /users/{id}
ServeMux는 path parameter를 직접 지원하지 않기 때문에
이 단계에서 보통 선택지는 두 가지다.
- 직접 파싱해서 처리
- 라우팅 프레임워크 도입
중요한 건
프레임워크 도입이 “문제 해결”을 위한 선택이어야지,
출발점이 되면 안 된다는 점이다.
이 단계에서 얻어야 할 감각
이 챕터에서 가장 중요한 건 문법이 아니다.
- 라우팅은 조립
- 핸들러는 역할
- 미들웨어는 공통 관심사 분리
이 개념들이 머릿속에 자리 잡으면,
Gin이나 Echo를 쓰더라도
“왜 이런 구조인지”가 자연스럽게 보인다.
정리
Go HTTP 서버 구조화의 핵심은
책임 분리와 흐름 가시성이다.
- 라우팅과 로직 분리
- 미들웨어로 공통 코드 제거
- context로 요청 범위 데이터 전달
여기까지 왔다면,
단순 예제를 넘어서 실제 서비스 코드의 형태가 보이기 시작했을 것이다.
다음 챕터에서는 이 구조 위에서
간단한 CRUD API 구현 + 테스트 코드 작성으로 이어가면
실전 감각을 확실히 잡을 수 있다.
'backend' 카테고리의 다른 글
| Go에서 DB 연동하기: database/sql과 Repository 패턴으로 구조 잡기 (0) | 2026.02.01 |
|---|---|
| Go로 간단한 CRUD API 만들기: REST 구조와 테스트 코드까지 연결하기 (1) | 2026.01.31 |
| Go로 간단한 HTTP API 서버 만들기: net/http 기반 기본 구조 이해하기 (0) | 2026.01.27 |
| Go로 간단한 HTTP API 서버 만들기: net/http 기반 기본 구조 이해하기 (0) | 2026.01.25 |
| Go 성능 분석과 최적화 입문: pprof로 병목 지점 찾는 방법 (1) | 2026.01.24 |