서버

Gateway 서버

앱이 부르는 HTTP 진입점. 경로 접두사로 서버를 골라 요청을 그대로 넘긴다. 업무 로직 · DB · 자기 API 가 없다. Go 1.25 표준 라이브러리만(net/http · httputil.ReverseProxy), 의존성은 없다. HTTP :8003, gRPC 는 없다. 서버 레이어(api → domain → storage · clients)의 예외다. 자기 API 가 없어 openapi 를 만들지 않으니 gen 도 없다. 레포 전체 규칙은 루트 AGENTS.md, 이 파일이 이 서버에서는 우선한다.

구조

main.go                  # 진입점: 환경 변수(HOST · PORT · <서버>_URL) → New → listen
gateway.go               # 경로 표(routes) · New(Config) http.Handler · 서버마다 ReverseProxy · 오류 { "error" }
gateway_specs_test.go    # 경로 표의 줄마다 specs (FakeUpstream · 랜덤 포트의 실제 gateway)
go.mod                   # module gateway, go 1.25 (의존성 없음, go.sum 없음)
package.json             # 스크립트 (go 명령을 감싼다)
.env.template            # 환경 변수 (.env 로 복사, 선택)
Dockerfile               # 이미지 (golang 으로 빌드, distroless nonroot 에서 실행. 컨텍스트는 이 폴더)

명령

pnpm dev          # go run . (.env 가 있으면 읽는다). 넘길 서버가 떠 있어야 응답이 온다, 아니면 502
pnpm test         # go test ./... (DB · 다른 서버 없이)
pnpm typecheck    # go vet ./...
pnpm lint         # gofmt -l 이 비어 있는지 + go vet
pnpm format       # gofmt -w .
docker build .    # 이미지 (이 폴더에서)

규칙

  • 경로 표는 gateway.go 의 routes 하나다. 접두사는 서버마다 하나이고 겹치지 않는다.

    접두사 서버 주소 (환경 변수, 개발 기본)
    /api/notes notes NOTES_URL, http://127.0.0.1:8000
    /api/tags tags TAGS_URL, http://127.0.0.1:8002
    /api/activity activity ACTIVITY_URL, http://127.0.0.1:8001
  • 앱이 부를 서버를 더하면 routes 에 {접두사, 이름, 개발 기본 주소} 한 줄을 더한다. 주소 변수(<이름>_URL) · 테스트의 Fake 는 그 줄에서 따라온다. .env.template · docker/docker-compose.yaml 의 gateway environment 에도 주소를 적는다.

  • 접두사는 경로 조각 단위로 맞춘다(http.ServeMux 에 /api/notes · /api/notes/ 둘 다 등록). /api/notesx 는 notes 로 가지 않는다. .. · // 가 든 경로는 ServeMux 가 정리한 경로로 301 을 준다.

  • 메서드 · 경로 · 쿼리 · 본문 · 헤더를 그대로 넘기고(SetURL · SetXForwarded, X-Forwarded-* 는 gateway 가 다시 쓴다), 서버의 상태 코드 · 헤더 · 본문을 그대로 돌려준다. 서버의 오류(400 · 404 · 502)도 바꾸지 않는다.

  • gateway 가 만드는 오류는 둘, 다른 서버와 같은 { "error" } (application/json; charset=utf-8): 표에 없는 경로(/ 포함) 404 {"error":"not found"}, 서버에 닿지 못하면 502 {"error":"<이름> 서버에 연결할 수 없습니다"}(원인은 로그 한 줄).

  • 바인딩은 HOST(기본 127.0.0.1) · PORT(기본 8003). 이미지는 HOST=0.0.0.0. 밖에 열기 전에 인증 · TLS.

  • 시간 제한은 ReadHeaderTimeout · IdleTimeout 만 둔다. ReadTimeout · WriteTimeout 은 긴 응답(스트리밍)을 끊으니 두지 않는다.

  • 업무 로직은 두지 않는다. 생기면 그 일을 맡은 서버로. 인증 · 요청 제한 · 접근 로그를 붙인다면 여기 New 가 자리다(docs/guides/auth.md).

  • 서버 ↔ 서버(gRPC)는 gateway 를 거치지 않는다.

테스트

  • 자기 자원이 없어 specs 는 경로 표의 줄마다 묶는다: TestNotes = /api/notes → notes · TestTags · TestActivity · TestUnknownPath. 테스트 이름은 한글_밑줄 조건 · 결과(t.Run("하위_경로의_DELETE를_tags_서버로_넘기고_204를_돌려준다", ...)).
  • startGateway(t) 가 routes 의 서버마다 FakeUpstream(httptest)을 띄우고, 그 주소를 가리키는 실제 New(cfg) 를 랜덤 포트로 띄운다. 테스트마다 새로 띄우고 t.Cleanup 으로 닫으니 reset() 이 없다. mock 은 쓰지 않는다(ADR-0003).
  • Fake 가 정하는 것은 돌려줄 상태 코드 · 본문(respond), 보는 것은 받은 요청(requests(): 메서드 · 경로 · 쿼리 · 본문 · 헤더 일부). 서버가 꺼진 경우는 Fake 를 Close() 하고 부른다.
  • 넘긴 응답 본문은 바이트 그대로 비교하고, gateway 의 오류는 JSON 필드로 비교한다. 엉뚱한 서버를 부르지 않았는지는 Fake 의 받은 요청 수로 본다.
GitHub 에서 고치기