refactor: 각 어댑터터별 리펙토링 진행

This commit is contained in:
DongHyeonka
2026-08-24 18:26:40 +09:00
parent e98b56eb03
commit 0137263441
439 changed files with 31935 additions and 4719 deletions
@@ -0,0 +1,70 @@
# ADR-GQL-001 — GraphQL context stays inbound; object authorization moves to application-core; the persisted-operation store stays an inbound SPI
- Status: Accepted
- Date: 2026-08-24
- Review: `docs/reviews/2026-08-14-graphql-module-code-review.md` GQL-026
## Context
The GraphQL leaf's own documentation described three things crossing its boundary: a
`GraphQlRequestContext` with a deadline propagated into application, JPA, Mongo and the HTTP client;
object authorization decided inside the transport; and a persisted-operation registry implemented by
an external durable store.
Two of those invert the dependency direction. If `application-core` or an outbound adapter
implements a type that lives in `adapter:inbound:graphql`, the registry edge that says inbound
depends on application is satisfied while the real compile-time dependency runs the other way.
The third is a business rule in the wrong layer: whether an actor may see an object is a decision
about the domain, and GraphQL is one of four transports this skeleton ships.
## Decision
Three different answers, because the three problems are not the same problem.
**GraphQL context stays inbound-local.** It is mapped explicitly onto application command fields —
actor, tenant, deadline — rather than travelling as a type. Nothing outside the leaf references
`GraphQlRequestContext`, and the boundary test is that grep returns nothing outside it.
**Object authorization moves to `application-core`.** `ObjectAccessPolicy`, `ObjectAccessRequest`
and `ObjectAccessDecision` are transport-neutral and live with the other application policies;
`ApplicationObjectAuthorization` in the GraphQL leaf is the bridge that calls them. This is the one
of the three that was a real layering defect, and it is fixed rather than documented.
**The persisted-operation store stays an inbound-owned SPI.** `GraphQlPersistedOperationRegistry`
remains in `advanced/persisted`, and no leaf outside GraphQL implements it.
## Consequences
The third decision is the one that needs defending, because it leaves the reported risk in place
rather than removing it.
The risk is conditional: the direction inverts only when something outside the leaf implements the
interface. Nothing does. The template ships an in-memory registry and no durable one, because it
ships no persisted-operation store at all.
The alternative was to introduce a generic operational key-value store port owned by a neutral
contract holder, with the GraphQL adapter owning only the key and value mapping. That port would
have exactly one interface, zero implementations and one speculative consumer — a new abstraction
whose shape is guessed from a requirement nobody has stated. This repository has spent a full
remediation pass deleting controls that existed and were reached by nothing, and inventing a port
for a store that does not exist is how the next one of those gets written.
So the decision is to leave the SPI where it is and to move it when a durable store is actually
built. Moving it then is a rename across one leaf and one new adapter, which is cheaper than
carrying a wrong abstraction until then. What must not happen in the meantime is an outbound leaf
implementing the inbound interface, because that is the moment the direction actually inverts, and
it would happen in a commit whose diff looks like an implementation rather than a layering change.
The composition root wires these and owns no business or storage policy of its own.
## Enforcement
`verifyCleanArchitectureDependencies` and `modules.json` hold the leaf's edges to
`domain-core`, `application-core` and `shared-contract`. `ObjectAccessPolicyTest` covers the
application-side policy and `ApplicationObjectAuthorizationTest` the bridge.
The condition this ADR turns on — that nothing outside the GraphQL leaf implements the
persisted-operation SPI — is a claim about the whole repository, so it is checked at the
composition root rather than inside the leaf, next to the other GraphQL boundary rules in
`app-bootstrap`'s architecture suite.
@@ -0,0 +1,67 @@
# ADR-JPA-006 — `audit` is the canonical technical audit model; `auditing` stays a frozen candidate
- Status: Accepted
- Date: 2026-08-24
- Review: `docs/reviews/2026-08-14-jpa-module-code-review.md` JPA-022
## Context
Two complete technical-audit mechanisms live in this leaf and they disagree about the schema.
`audit/AuditableEntity` stamps `created_at`/`created_by`/`updated_at`/`updated_by` with an actor
column of length 256, captured through explicit `initializeAudit`/`applyModification` calls and an
`AuditContextPort`. `auditing/AuditMetadata` is a Spring Data embeddable that stamps
`created_*`/`modified_*` with an actor column of length 64, captured by `@CreatedDate` and friends
through an `AuditorAware`.
Only the first is real: it is what the sample entities extend and what the migrations were written
for. `JpaAuditingConfiguration` is not a Spring `@Configuration`, and nothing in production
constructs any of the three `auditing` types.
The review asked for one canonical model with a migration or activation decision. The failure mode
it was protecting against is specific: an author of a new entity picks whichever package they find
first, and column names, actor lengths and capture lifecycles then diverge per table.
## Decision
`audit/AuditableEntity` is canonical. `auditing` stays in the tree as a candidate and is excluded
from the Stable capability report.
The candidate is not deleted and not promoted. Deleting it would discard a working Spring Data
integration that a deployment preferring declarative auditing would want. Promoting it would mean
either renaming `modified_*` to `updated_*` and widening the actor column — a schema migration of
every audited table to gain nothing a caller asked for — or moving the sample entities onto
`modified_*`, which is the same migration in the other direction.
Neither is worth doing now. What the divergence actually needed was not consolidation but a rule
that an entity cannot straddle the two, and that rule is cheaper than either migration.
## Consequences
Two audit mechanisms remain readable in one leaf, and a reader has to be told which one is live.
That cost is paid in this document, in the package javadoc and in a test whose name says so.
Two failure modes stay silent unless they are asserted, so both are:
- The candidate acquires a stereotype and starts stamping in every deployment that has this module
on the classpath, including the ones whose tables have no `modified_*` columns — where the result
is a failed startup rather than a feature.
- Somebody "harmonises" the two by editing one side's column names, at which point the schema a
deployed table was migrated for and the schema its entity expects diverge with no migration
between them.
If the candidate is ever promoted, it is promoted atomically: forward migration, sample conversion,
`AuditContextPort → AuditorAware` and `Clock → DateTimeProvider` bridges land together, and this
ADR is superseded rather than amended.
Bulk and native updates stamp nothing under either mechanism. That is a property of JPA, not of the
choice made here, so it is enforced separately rather than assumed away.
## Enforcement
`JpaAuditMechanismRule.entitiesUseExactlyOneAuditMechanism` and
`bulkUpdatesOfAuditedEntitiesStampAudit`, run against the real production graph by
`JpaProductionArchitectureTest` at the composition root — not against fixtures, which is how the
earlier version of this rule pack passed while applying to nothing. `AuditingCandidateStatusTest`
asserts the candidate carries no composing stereotype and that the two column sets stay distinct.
`JpaAuditMechanismRuleTest` exercises the rules' own negative cases.