AI를 쓰면 boilerplate를 만드는 비용이 거의 0처럼 느껴질 때가 있다.
controller 하나, DTO 몇 개, repository와 service, Redis adapter, 공통 UI component 정도는 다시 만들어도 금방 나오기 때문이다.
그래서 처음에는 크게 신경 쓰지 않았다.
그런데 프로젝트 수가 늘어나고 같은 패턴을 몇 번 반복하자 이상한 비용이 보이기 시작했다.
코드는 빨리 생성됨
하지만 매번 다시 설명해야 함
비슷한 코드가 여러 repository에 생김
하지만 서로 조금씩 달라짐
검증했던 구조를 다시 생성함
하지만 다시 review해야 함
결국 "AI가 빨리 써준다"는 것과 "다시 만들 가치가 있다"는 것은 다른 문제였다.
그래서 개인 프로젝트에서 반복해서 쓰는 코드를 하나의 소프트웨어 자산으로 보기 시작했다.
재사용의 출발점은 generic framework가 아니었다
처음부터 거대한 공통 framework를 만들려는 것은 아니다.
오히려 과거에 비슷한 시도를 하면서 한계를 이미 경험했다.
PhotoGram에는 한때 generic CRUD service 계열의 실험이 있었다.
공통 JPA persistence, DTO/entity mapping, save/update 같은 반복을 줄이는 데는 잘 맞았다.
하지만 프로젝트가 커지면서 다음 의미까지 generic layer가 삼키기 시작하면 문제가 생겼다.
ownership
authorization
attachment cleanup
transaction policy
event semantics
domain invariant
이런 것은 "모든 entity에 공통"인 척하기 어렵다.
결국 얻은 학습은 단순했다.
반복되는 mechanics
!= 반복되는 domain meaning
mechanics는 재사용하거나 생성할 수 있다.
domain meaning은 concrete service가 소유해야 한다.
가장 좋은 자산화 사례는 실제 consumer가 있는 코드였다
현재 내 코드 중 가장 명확한 재사용 성공 사례는 grid-masonry다.
처음부터 library로 설계한 것이 아니라 제품 안에서 실제 문제를 해결하다가 deterministic masonry geometry가 제품 밖에서도 독립적인 contract를 가질 수 있다는 것을 발견했다.
흐름은 다음과 같았다.
product implementation
→ reusable boundary 발견
→ 별도 repository 추출
→ contract / test 정리
→ 실제 consumer 검증
→ package / release
이 순서가 중요했다.
먼저 "언젠가 쓸 것 같은" abstraction을 만든 것이 아니라 실제 반복과 consumer가 있었다.
그래서 자산화 기준도 이쪽에 맞췄다.
REUSE SHOULD FOLLOW EVIDENCE
NOT ANTICIPATION
NestTemplate에서 원했던 것도 사실 runtime abstraction이 아니었다
예전에 NestTemplate이라는 실험을 한 적이 있다.
겉으로 보면 NestJS 프로젝트 template처럼 보이지만 내가 원했던 핵심은 조금 달랐다.
예를 들어:
nest set architecture mvc
nest g module user
처럼 architecture preset을 고르면 module 하나를 만드는 명령으로 서로 연결된 파일들이 같이 나오는 형태였다.
user/
├─ user.module.ts
├─ user.controller.ts
├─ user.service.ts
├─ user.entity.ts
└─ dto/
├─ create-user.dto.ts
├─ update-user.dto.ts
└─ user-response.dto.ts
그리고 field도 여러 파일에 반복해서 입력하고 싶지 않았다.
User
- id
- email
- nickname
- createdAt
를 한 번 정의하면:
canonical field definition
↓
Entity
Create DTO
Update DTO
Response DTO
validation metadata
accessor
로 필요한 surface에 투영하는 형태다.
지금 다시 보면 중요한 것은 "Nest 전용 만능 framework"가 아니었다.
architecture preset
+ relationship rule
+ deterministic generation
이 더 정확한 문제였다.
Generate와 Patch는 위험도가 다르다
code generator를 생각하면 기존 source까지 자동으로 고치고 싶어진다.
예를 들어 module을 생성한 뒤 기존 app module의 import를 알아서 수정하거나, decorator와 config를 merge하는 식이다.
하지만 이 둘은 같은 자동화가 아니다.
GENERATE
= 새 path에 deterministic file 생성
PATCH / SYNC
= 기존 source 의미를 읽고 변경
Generate는 비교적 닫힌 문제다.
입력 schema와 template, target path가 있으면 결과를 예측하기 쉽다.
반면 Patch는 기존 code structure와 local convention을 이해해야 한다.
import merge, decorator update, config mutation, existing code rewrite 같은 순간부터 의미 해석이 들어간다.
그래서 초기 자산 materialization은 가능한 한 Generate 쪽에 두고, Patch/Sync는 실제 반복 필요성이 확인됐을 때 별도 문제로 다루는 편이 낫다고 봤다.
Capability와 기술 이름도 분리해야 했다
재사용 자산을 정리하다 보면 흔히 기술 이름을 capability처럼 부른다.
Redis module
Gmail module
PostgreSQL module
하지만 장기적으로는 의미를 분리하는 편이 낫다.
예를 들어 mail은:
Mail capability
↓
MailPort
├─ SMTP adapter
├─ Gmail adapter
└─ SES adapter
cache도:
Cache capability
├─ Redis
└─ in-memory
처럼 볼 수 있다.
MAIL != GMAIL
CACHE != REDIS
PERSISTENCE != POSTGRESQL
특정 기술은 교체 가능한 adapter가 될 수 있고, 제품이 원하는 capability 의미는 그 위에 남는다.
이렇게 해두면 reusable asset이 특정 프로젝트의 infrastructure choice를 강제하지 않는다.
UI도 framework 하나에 묶이지 않을 수 있다
sorune-ui를 생각하면서도 비슷한 결론에 도달했다.
React component library 하나만 만들겠다는 것보다 내가 반복해서 사용하는 시각 언어와 interaction pattern을 여러 presentation target에 재사용하는 쪽이 더 맞았다.
React
HTML
JSP
Thymeleaf
Vue
가 모두 같은 runtime component를 공유할 필요는 없다.
대신 token, layout rule, interaction contract, component semantics, reference implementation 같은 자산을 공유할 수 있다.
framework-specific implementation과 design authority를 같은 것으로 만들지 않는 것이다.
AI 시대에는 자산화가 context compression 역할도 한다
이 방향을 정리하면서 가장 크게 달라진 부분은 AI 비용을 보는 방식이었다.
AI가 다음 코드를 매번 다시 쓰는 것은 어렵지 않다.
module skeleton
controller skeleton
service skeleton
DTO duplicate fields
entity duplicate fields
getter/setter
dependency injection wiring
common config
adapter setup
하지만 새 판단도 거의 없다.
그럼에도 매번 prompt에 구조를 설명하고, 결과를 읽고, 작은 차이를 수정하고, 다시 테스트한다.
AI가 실제로 시간을 써야 하는 부분은 오히려 다음이다.
domain invariant
business rule
ownership
authorization
transaction semantics
special query
exceptional integration
migration / policy decision
그래서 장기적으로 원하는 흐름은 다음과 같다.
BEFORE
AI
→ boilerplate
→ wiring
→ common config
→ known adapter
→ domain logic
AFTER
accepted reusable asset
→ boilerplate / wiring / known adapter materialization
AI
→ domain delta
→ exceptional behavior
→ business semantics
이건 단순한 코드 재사용보다 context와 output을 압축하는 방법에 가깝다.
"다시 500줄 만들어줘"
보다
"검증된 asset X를 사용하고 이번 domain delta만 구현해"
가 더 싸고 검토하기도 쉽다.
모든 것을 지금 당장 자산으로 만들지는 않는다
재사용 이야기를 시작하면 금방 범위가 커진다.
asset inventory
→ registry
→ universal CLI
→ framework adapter
→ automatic project composition
→ asset platform
하지만 이걸 한 번에 만들면 "중복 구현을 줄이기 위한 시스템" 자체가 새로운 대형 제품이 된다.
그래서 현재 원칙은 반대다.
1. 이미 존재하는 자산을 찾는다
2. reusable / candidate / product-specific / legacy를 구분한다
3. 실제 반복되는 capability를 확인한다
4. 반복이 증명된 것부터 추출한다
5. generator는 기계적인 관계에만 붙인다
두 번째, 세 번째 실제 consumer가 생기기 전에는 common engine을 서둘러 만들지 않는다.
바퀴를 다시 만들지 않는 것과 abstraction을 많이 만드는 것은 다르다
이 작업의 목적을 한 문장으로 줄이면 "바퀴를 다시 만들지 않는다"가 맞다.
하지만 그 해결책이 모든 코드를 generic하게 만드는 것은 아니다.
DO NOT REIMPLEMENT KNOWN MECHANICS
+
DO NOT ERASE DOMAIN MEANING
이 두 조건을 같이 만족해야 한다.
반복되는 파일 관계와 boilerplate는 기계가 처리한다.
이미 검증된 capability는 reusable asset으로 붙인다.
그 위에서 각 제품은 자기 domain invariant를 계속 소유한다.
AI를 많이 쓰기 시작하면서 오히려 이 구분이 더 중요해졌다.
코드를 생성하는 비용이 싸졌기 때문에 재사용의 필요성이 사라진 것이 아니라, 비슷하지만 서로 다른 코드가 무한히 증식할 가능성이 커졌기 때문이다.
앞으로 자산화의 기준은 "만들 수 있는가"가 아니라 "이미 반복되었고, 독립된 contract가 있으며, 다시 검증할 가치가 없는 mechanics인가"에 두려고 한다.