init: 클린 아키텍처 백엔드
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# adapter-graphql — 설계 결정 참조
|
||||
|
||||
인바운드 GraphQL 어댑터 **스켈레톤 머시너리** 모듈. 패키지 루트:
|
||||
`dev.caskeleton.adapter.inbound.graphql`.
|
||||
|
||||
허용/금지 의존, 모듈 규칙, 설정 knob, 테스트 명령 같은 **모듈 규칙**은
|
||||
[CLAUDE.md](CLAUDE.md) 가 SSOT 다. 이 문서는 코드 주석에서 덜어낸 **설계 결정의 근거**를
|
||||
모아둔 참조용 기록이다.
|
||||
|
||||
---
|
||||
|
||||
## 왜 스켈레톤에 health 스키마 + 컨트롤러만 두는가
|
||||
|
||||
Spring for GraphQL 은 schema-first 다. 빈 스키마로는 부팅이 실패하므로 스켈레톤은
|
||||
`src/main/resources/graphql/skeleton.graphqls` 에 최소 스키마(`type Query { _health: String! }`)를
|
||||
싣고, `HealthGraphqlController` 가 그 필드를 상태 토큰(`UP`)으로 resolve 한다. web 어댑터의
|
||||
`HealthcheckController` 와 동일 원칙 — 스켈레톤은 **RPC/쿼리 0개**의 feature 로도 health 표면만으로
|
||||
부팅한다. 프로덕션 feature 쿼리/뮤테이션을 스켈레톤에 두지 않는다.
|
||||
|
||||
## 기능(feature)은 어떻게 기여하는가 — machinery/feature 분리
|
||||
|
||||
스켈레톤은 **WorkLog 를 이름으로 알지 못한다.** Spring for GraphQL 이 두 축으로 자동 합성한다:
|
||||
|
||||
- **스키마**: `classpath:graphql/**/*.graphqls` 를 전부 병합한다. sample 모듈의
|
||||
`worklog.graphqls` 가 스켈레톤의 `skeleton.graphqls` 와 자동으로 합쳐진다.
|
||||
- **resolver(핸들러)**: 컨텍스트의 모든 `@Controller` 의 `@QueryMapping`/`@MutationMapping`
|
||||
메서드를 바인딩한다. sample 의 `WorkLogGraphqlController` 가 스켈레톤을 수정하지 않고 등록된다.
|
||||
|
||||
`sample-portfolio` 를 지우면 스켈레톤은 여전히 health 스키마만으로 부팅한다(web 과 동일한
|
||||
disposability 보장).
|
||||
|
||||
## 에러 매핑 — web `GlobalExceptionHandler` / gRPC 인터셉터의 GraphQL 형제
|
||||
|
||||
`GraphqlExceptionResolver` 는 `DataFetcherExceptionResolverAdapter` 를 확장해, 데이터 페처가
|
||||
동기적으로 던진 예외 중 안정적 `ApiErrorCode` 를 실은 것(전송-중립 hook `ApiErrorCarrier` 구현)을
|
||||
`GraphQLError` 로 변환한다. 데이터 페처는 web 컨트롤러처럼 "그냥 던지기만" 하고, 이 resolver 가
|
||||
와이어 계약을 단일 소유한다.
|
||||
|
||||
- **`ErrorType` 분류**: `errorCode().category()` 를 GraphQL `ErrorType` 으로 매핑한다(아래 표).
|
||||
정확한 `code`/`category` 는 error `extensions{code, category}` 로 실어 클라이언트가 switch 하게
|
||||
한다(gRPC 가 status trailer 에 싣는 것과 동형).
|
||||
- **`ApiErrorCode` 추출**: shared-contract 의 `PersistenceFailureException` /
|
||||
`DependencyFailureException`(outbound 어댑터에서 올라온 분류된 실패)과 feature 예외(자신의 도메인
|
||||
`ApiErrorCode` 를 실은 것)를 단일 `instanceof ApiErrorCarrier` 분기로 인식한다.
|
||||
- **leak 방지**: 인식된 코드는 안정적 `code` 문자열만 error message/extensions 로 노출하고, raw
|
||||
예외 메시지(SQLState/업스트림 세부를 담을 수 있음)는 절대 클라이언트에 내보내지 않는다.
|
||||
- **비-`ApiErrorCarrier`** 예외는 `null` 을 반환해 다른
|
||||
`DataFetcherExceptionResolver` 빈(예: sample 의 도메인 예외 resolver)과 Spring 기본 처리로
|
||||
넘긴다.
|
||||
|
||||
`Category → ErrorType` 표(설계 스펙 Error Mapping SSOT):
|
||||
|
||||
| `Category` | GraphQL `ErrorType` |
|
||||
|---|---|
|
||||
| VALIDATION | BAD_REQUEST |
|
||||
| AUTH | UNAUTHORIZED |
|
||||
| AUTHZ | FORBIDDEN |
|
||||
| NOT_FOUND | NOT_FOUND |
|
||||
| CONFLICT | BAD_REQUEST |
|
||||
| RATE_LIMIT | BAD_REQUEST |
|
||||
| TRANSIENT_DEPENDENCY | INTERNAL_ERROR |
|
||||
| PERMANENT_DEPENDENCY | INTERNAL_ERROR |
|
||||
| DATA_INTEGRITY | INTERNAL_ERROR |
|
||||
| INTERNAL | INTERNAL_ERROR |
|
||||
|
||||
## 의존성 버전 — strict locking
|
||||
|
||||
gRPC 와 달리 spring-graphql / graphql-java 는 Spring Boot BOM 이 관리한다. 그래서 이 모듈은
|
||||
버전 명시도, 모듈 스코프 platform import 도 필요 없다 — `build.gradle` 은 BOM-managed 좌표만
|
||||
선언하고, per-module `gradle.lockfile` 이 strict locking 으로 정확한 버전을 고정한다.
|
||||
|
||||
## 설정 — 프레임워크 `spring.graphql.*`
|
||||
|
||||
이 모듈은 자체 `@ConfigurationProperties` 를 두지 않는다. path, graphiql, introspection, schema
|
||||
location 은 프레임워크 `spring.graphql.*` 로 composition-root `application.yml` 에서 설정한다
|
||||
(모듈별 `yml` 없음). 정말 필요한 knob 이 생기기 전까지 커스텀 설정 클래스는 두지 않는다.
|
||||
Reference in New Issue
Block a user