서버
Gateway 서버
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/notesnotes NOTES_URL,http://127.0.0.1:8000/api/tagstags TAGS_URL,http://127.0.0.1:8002/api/activityactivity ACTIVITY_URL,http://127.0.0.1:8001 -
앱이 부를 서버를 더하면
routes에{접두사, 이름, 개발 기본 주소}한 줄을 더한다. 주소 변수(<이름>_URL) · 테스트의 Fake 는 그 줄에서 따라온다..env.template·docker/docker-compose.yaml의 gatewayenvironment에도 주소를 적는다. -
접두사는 경로 조각 단위로 맞춘다(
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 의 받은 요청 수로 본다.