init: 클린 아키텍처 백엔드

This commit is contained in:
DongHyeonka
2026-07-24 14:29:36 +09:00
parent 9eed16d097
commit 821fe00c32
971 changed files with 74769 additions and 1 deletions
+76
View File
@@ -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 이 생기기 전까지 커스텀 설정 클래스는 두지 않는다.