같은 원본을 읽는다고 같은 실행 환경이 필요한 것은 아니다
DB 동기화와 공개 자산 발행은 메타데이터 정규화, 원본 구조 검증과 checksum 계산을 공유했다. 하지만 공개 자산을 만드는 데 DataSource와 JPA, 저장소, API 컨트롤러는 필요하지 않았다.
당시 공통 결과는 PostReadModelPreparedCandidate, 전체 검사는 PostReadModelCandidatePreflight라는
이름으로 Read Model 패키지에 있었다. 자산 발행도 같은 Spring 구성을 사용해 불필요하게 MySQL을
시작했다.
공통 계산을 복제하면 두 경로가 다른 규칙으로 원본을 해석할 수 있다. 준비 과정은 공유하되 MySQL과 파일 시스템 자산 발행의 실행 환경은 나누기로 했다.
source -> 공통 preparation
|-> Read Model -> MySQL
`-> asset -> filesystem준비 로직을 중립 패키지로 옮겼다
content.post.preparation 패키지를 만들고 이름을 다음처럼 바꿨다.
PostReadModelPreparedCandidate -> PreparedPostCandidate
PostReadModelCandidatePreflight -> PostCandidatePreflightPostCandidatePreparer는 파싱한 후보를 받아 메타데이터를 정규화하고 경로 관계를 검증한 뒤 checksum을
계산한다. 결과에는 정규화한 메타데이터와 rawBody, sourcePath, checksum만 있다. Preflight는 모든 후보를
준비한 뒤 중복 slug와 sourcePath를 거부한다. DB 쓰기와 파일 시스템 활성화, JPA와 HTTP DTO는 모른다.
Read Model 처리기에는 준비된 후보를 create·update·touch하는 상태 변경만 남겼다. 엔티티 변경은
기존처럼 assembler에 모았다. CREATED, UPDATED, TOUCHED, DELETED의 의미와 API, 스키마도 바꾸지
않았다.
의존이 없어도 Spring은 DB를 시작했다
Asset 패키지가 Read Model을 import하지 않아도 같은 애플리케이션 진입점을 사용하면 Spring 자동 설정이 DataSource와 Hibernate, 저장소, Flyway를 만들 수 있다. 코드가 DB를 호출하지 않는다는 것과 DB 없이 실행할 수 있다는 것은 다른 계약이었다.
docker,asset-sync를 WebApplicationType.NONE인 모드로 만들고 다음 자동 설정을 제외했다.
DataSource
Hibernate JPA
JPA repositories
Flyway이 모드에는 원본 로더와 준비 로직, 자산 발행기와 실행기만 남겼다. DB 상태 변경과 공개 API는 없어야 했다.
세 모드의 기능을 테스트로 고정했다
| 기능 | app | sync | asset-sync |
|---|---|---|---|
| Source와 preparation | O | O | O |
| DataSource·JPA | O | O | X |
| Read Model 상태 변경 | O | O | X |
| 일회성 DB 실행기 | X | O | X |
| Public API | O | X | X |
| Asset publisher | X | X | O |
컨텍스트 통합 테스트는 운영 진입점으로 각 모드를 띄워 빈의 존재와 부재를 확인한다. Asset
모드는 DB 환경이 없어도, 일부러 127.0.0.1:1의 JDBC URL을 줘도 snapshot과 current symlink를 만든다.
별도 아키텍처 테스트는 asset -> readmodel, preparation -> readmodel import와 post 패키지 cycle이 0인지,
preparation이 JPA나 Spring Data를 import하지 않는지 검사한다. 컴파일 시점 의존성과 실행 시 활성화는
서로 다른 테스트가 맡는다.
엔티티 변경과 빈 활성화도 좁혔다
PostReadModel의 public setter 22개를 package-private로 바꿨다. JPA field access와 protected constructor는
유지하고 create와 update, touch, retirement는 assembler와 Read Model 서비스만 수행하게 했다. 테스트도
setter를 다시 열거나 reflection을 사용하지 않고 운영 상태 변경 경로를 따랐다.
@Profile("!sync")처럼 넓은 음수 조건도 줄였다. asset-sync는 sync가 아니므로 의도하지 않은 빈까지
활성화할 수 있기 때문이다.
- 공개 API와 시작 실행기는 Servlet web에서만 활성화한다.
- Webhook은 Servlet web과 enabled property를 함께 요구한다.
- 일회성 실행기와 자산 실행기는 각각
sync,asset-sync프로파일을 명시적으로 요구한다. - 운영과 같은 app 테스트는 Docker 프로파일과 Testcontainers MySQL로 실제 구성을 올린다.
DB 의존 경계를 자산 모드에서 막는 !asset-sync는 명시적인 제외 조건으로 남겼다.
운영에서도 전용 asset-sync를 사용했다
애플리케이션만 고쳐도 Worker가 기존 DB 의존 sync 서비스를 계속 사용하면 경계가 완성되지 않는다.
Compose에 전용 서비스를 추가했다.
asset-sync
posts checkout: read-only
asset root: read-write
MySQL dependency: 없음
datasource 환경: 없음
public origin: 없음
network: noneApp과 sync, asset-sync는 같은 변경 불가능한 백엔드 digest를 사용하지만 실행 책임은 다르다. Worker 순서도 자산 준비, DB 동기화, 자산 활성화, 점검과 캐시 무효화로 나뉜다.
첫 운영 검증은 이전 환경 변수에서 멈췄다
전용 서비스의 첫 모의 실행은 상태 변경 전에 실패했다. 읽기 권한 검사가 자산용 원본 경로 대신 서비스에서 제거한 이전 resync 환경 변수를 읽고 있었다.
검사를 HOUKAGO_ASSET_PUBLICATION_POSTS_ROOT에 맞추고 shell 회귀 테스트를 추가했다. 공식 재시도는 자산
준비와 DB 동기화, 활성화, API·자산 점검, 캐시 무효화까지 성공했다. 결과는
CREATED=0, UPDATED=0, TOUCHED=277, DELETED=0이었고 app과 MySQL은 재시작되지 않았다. 277은 당시
후보가 기존 상태와 같았다는 관찰값이다.
의존성을 줄인 대신 검증할 경계가 늘었다
패키지 import와 cycle, 모드별 컨텍스트, Compose, Worker fixture를 각각 관리해야 한다. 같은 원본을 두 프로세스가 읽어 일부 계산도 반복한다. 전체 재동기화의 행별 트랜잭션과 DB 동기화 뒤 자산 활성화 전의 두 리비전이 잠시 어긋나는 구간도 남아 있다.
그래도 자산 발행이 MySQL 가용성과 web 설정 때문에 실패할 이유를 제거하고 DB 상태 변경과 파일 시스템 자산 발행의 성공을 따로 확인할 수 있게 됐다. 같은 원본을 준비하는 계약은 공유하되 서로 다른 부수 효과가 같은 실행 환경을 가져야 한다는 가정은 버린 작업이었다.