엔지니어링 가이드
다른 서버 부르기
다른 서버 부르기
notes 가 메모를 저장하기 전에 tags 에 태그를 묻는 흐름이 예다. 새 호출도 순서가 같다: proto → 받는 서버 구현 → 부르는 쪽 clients → 실패했을 때 할 일 → 테스트 → 연결(환경 변수 · compose · smoke). 명령은 모두 저장소 루트에서 돌린다.
- web → gateway HTTP
- gateway → notes
- notes → tags gRPC TagService.
Suggest - notes → activity gRPC ActivityService.
Record
서버끼리는 gateway 를 거치지 않는다. gateway 는 앱이 들어오는 문이고, 서버 사이는 gRPC 로 직접 부른다(ADR-0002).
1. proto
contract/proto/app/<도메인>/v1/<도메인>.proto 에 서비스 · 메시지를 쓴다. 디렉터리가 패키지다(app/tag/v1 → app.tag.v1).
service TagService {
rpc Suggest(SuggestRequest) returns (SuggestResponse);
}
message SuggestRequest { string text = 1; }
message SuggestResponse { repeated string tags = 1; }
pnpm gen 이 buf lint 뒤 언어마다 코드를 만든다. 필드 번호는 바꾸거나 다시 쓰지 않는다(ADR-0002). → contracts.md
2. 받는 서버
받는 서버의 api 층에 서비스를 구현하고 domain 을 부른다. tags(FastAPI)는 server/api/rpc/tags.py, activity(Spring)는 api/grpc/ActivityGrpcService.java.
테스트는 실제 gRPC 연결로 부른다: tests/grpc/TagService/Suggest_specs.py, 묶음 이름은 gRPC 경로 /app.tag.v1.TagService/Suggest.
3. 부르는 쪽: clients 레이어
다른 서버는 외부 시스템이다. 부르는 서버의 clients/ 에 서버 하나당 모듈 하나를 두고, 밖에는 작은 인터페이스와 교체 지점만 보인다.
// servers/notes/src/clients/tags.ts
export interface Tags {
suggest(text: string): Promise<string[]>
}
// 만들 때는 접속하지 않는다 (처음 부를 때 연결). 서버가 멈춰도 쓰기가 계속 기다리지 않게 시간 제한
const client = createClient(TagService, createGrpcTransport({ baseUrl: config.tagsGrpc, defaultTimeoutMs: 3_000 }))
const grpcTags: Tags = { suggest: async (text) => (await client.suggest({ text })).tags }
export let tags: Tags = grpcTags
/** 테스트가 Fake 로 바꿀 때만 쓴다 */
export const useTags = (t: Tags) => (tags = t)
주소는 환경 변수 <서버>_GRPC(여기서는 TAGS_GRPC, 기본 http://127.0.0.1:9002)로 받는다.
4. 실패하면 어떻게 할지
다른 서버는 언젠가 멈춘다. 그때 응답을 어떻게 할지는 domain 이 호출마다 정한다. 예시의 두 호출은 답이 다르다.
| 호출 | 실패하면 | 이유 |
|---|---|---|
tags Suggest (저장 전) |
저장하지 않고 502 태그 서버에 연결할 수 없습니다 |
태그는 메모의 일부다. 태그 없이 저장하면 나중에 채울 방법이 없다 |
activity Record (저장 뒤) |
경고만 남기고 201 · 200 · 204 | 기록은 부가 정보다. 기록 때문에 이미 저장한 메모를 실패로 돌려주면 사용자는 다시 보내고 메모가 둘이 된다 |
// servers/notes/src/domain/note.ts
export async function create(text: string) {
const note = await notes.create(text, await suggest(text)) // 실패하면 던진다 → 502
await record('note.created', note.id) // 실패하면 console.warn 만
return note
}
다시 시도 · 큐에 넣고 나중에 보내기 같은 선택도 있다. 템플릿에는 없고, 고르면 ADR 로 남긴다. 정한 것은 그 서버 AGENTS.md 에 적는다.
5. 테스트: 다른 서버는 Fake
부르는 서버의 테스트는 다른 서버를 띄우지 않는다. 교체 지점에 Fake 를 넣고, Fake 로는 외부 조건(돌려줄 값 · 다음 한 번 실패 · 받은 인자)만 정한다. 실패를 502 로 바꾸는 것은 실제 domain 이 한다. 그 연결이 검증 대상이다. → testing.md
// servers/notes/tests/notes/POST_specs.ts
test('태그_서버에_연결할_수_없으면_502_Bad_Gateway를_반환하고_저장하지_않는다', async () => {
fakeTags.failNext()
const res = await sendJson('POST', '/api/notes', { text: '안녕' })
assert.equal(res.status, 502)
assert.deepEqual(res.body, { error: '태그 서버에 연결할 수 없습니다' })
assert.deepEqual(await list(), [])
})
Fake 는 tests/support/fakes.ts 에 두고 tests/support/api.ts 가 한 번 넣고 테스트마다 reset() 한다. mock(mock.module)은 쓰지 않는다(ADR-0003).
6. 연결
- 부르는 서버의
.env.template에 주소(# TAGS_GRPC=http://127.0.0.1:9002). docker/docker-compose.yaml의 부르는 서버에TAGS_GRPC: "http://tags:9002"(compose 네트워크 안의 서비스 이름).docker/smoke.sh에 흐름 하나: 실제 이미지끼리 이어지는지만 본다(notes 덩어리가 태그가 붙는지, activity 에 기록이 남는지 본다).pnpm drop은<서버>_GRPC로 부르는 쪽을 찾아, 부르는 서버가 남아 있으면 받는 서버를 빼지 않는다.- 꼭 필요한 호출이면 루트
pnpm dev의 기본 조합에 받는 서버를 넣는다(지금은 notes · tags · activity 를 같이 띄운다).
확인
pnpm gen && pnpm typecheck
pnpm --filter @app/tags test # 받는 쪽: 실제 gRPC
pnpm --filter @app/notes test # 부르는 쪽: Fake 로 성공 · 실패
pnpm stack # 실제 이미지끼리 (smoke)