저는 HoneyDrunk 프로젝트를 구축하기 위해 Codex와 Claude를 광범위하게 사용합니다. 이들은 구현 코드의 상당 부분을 작성해 줍니다. 저는 동작 방식을 설명하고, 반환되는 결과를 검사한 다음, 그것이 제가 만들고자 하는 시스템에 속하는지 결정합니다.
Pocket Quests는 마지막 부분(시스템 설계)에 얼마나 많은 작업이 숨어 있는지 상기시켜 주는 유용한 경험이었습니다.
백엔드가 커짐에 따라, 저는 변형 및 변경 바인딩 추상화(mutation and change-binding abstractions), 역사적 값과의 반복적인 비교, 전체 집합을 조립하는 대규모 메서드들, 그리고 비즈니스 서비스에 섞여 들어간 필드별 매핑 등을 계속 접하게 되었습니다. 각 부분은 제가 해당 작업을 이해하기 전에 추가적으로 알아야 할 무언가를 더했습니다.
저를 좌절시킨 것은 동일한 일반적인 구조를 반복해서 설명해야 한다는 점이었습니다. 저는 퀘스트 완료가 무엇을 해야 하는지에 대해 논의하고 싶었습니다. 대신, 유효성 검사가 어디에 속하는지, 매핑이 왜 별도의 파일이 필요한지, 그리고 EF Core가 이미 추적하는 변경 사항 주변에 왜 또 다른 메커니즘을 구축해야 하는지를 설명하는 데 시간을 소비했습니다.
생성된 문서가 KISS, SOLID, DRY를 나열할 수는 있습니다. 하지만 저는 여전히 코드를 열어 이러한 원칙들이 어떤 결정에 영향을 미쳤는지 확인해야 합니다.
바로 그 지점에서 아키텍처의 소유권이 매우 구체적이 됩니다. 누가 어떤 책임들이 함께 속해야 하는지, 어떤 의존성이 허용되는지, 그리고 새로운 추상화가 자리를 얻을 가치가 있는지 결정해야 합니다. 제 프로젝트에서 이 책임은 저 자신에게 남아 있습니다.
제가 원했던 구조는 간단했습니다. 최소한의 API 엔드포인트가 비즈니스 서비스 인터페이스를 호출합니다. 서비스는 비즈니스 작업을 소유하며, 정적 유효성 검사기(static validators), 분리된 매핑 확장(separate mapping extensions), 그리고 데이터 서비스 인터페이스를 호출합니다. 데이터 서비스는 EF Core와 DbContext를 통해 쿼리와 영속성을 처리합니다.
엔드포인트는 HTTP 경계를 이해하기 쉽게 만들어야 합니다. 서비스는 비즈니스 작업을 이해하기 쉽게 만들어야 합니다. 제가 쿼리를 검사해야 할 때, 어디에서 찾을 수 있는지 알아야 합니다.
이는 해당 애플리케이션에 대한 의도적인 선택입니다. 다른 팀은 동일한 책임을 다르게 구성하고 그럴 만한 좋은 이유가 있을 수 있습니다. 제가 우려하는 점은, 선택된 구조가 충분히 일관성을 유지하여 다음 기능 구현 시 또 다른 아키텍처 논의를 필요로 하지 않을지 여부입니다.
매핑(Mapping)이 좋은 예인데, 하찮아 보이다가 메서드의 절반을 채울 때까지는 아무 문제 없어 보입니다.
퀘스트의 제목, 설명, 카테고리 및 기타 선택된 값을 영속성 모델(persistence model)로 복사하는 것은 일상적인 매핑 작업입니다. 저는 이것이 명명된 매핑 확장(named mapping extension)으로 구현되기를 바랍니다. 그러면 서비스가 그 주변에 대한 결정들—요청 유효성 검사, 계정 식별, 적용 가능한 규칙 선택, 기록해야 할 내용 결정, 데이터 서비스 호출 등—을 보여줄 수 있습니다.
하지만 추출에는 한계가 있습니다. 퀘스트가 새로운 이력 개정(historical revision)이 필요한지 여부를 결정하는 것은 비즈니스 동작입니다. 어떤 보상 규칙이 적용되는지를 선택하는 것도 비즈니스 동작입니다. 매퍼는 이러한 결정들을 입력으로 받아야 합니다. 이것들을 ToEntity라는 메서드로 옮기면 찾기가 더 어려워집니다.
전체 애그리게이트(aggregate)를 구성하는 대규모 메서드에도 동일하게 적용됩니다. 일부 조정 작업은 서비스에 속해야 하고, 순수한 도메인 규칙은 도메인 모델(domain model)에 남아 있을 수 있습니다. 하지만 이러한 조정 작업이 수십 개의 할당문 사이에 묻혀 있으면, 그 작업이 실제로 무엇을 약속하는지 파악하기 어렵습니다. 필드 복사 작업을 분리하면 나머지 비즈니스 결정들을 검토하기가 더 쉬워집니다.
Pocket Quests의 경우 이력 데이터는 특별한 주의가 필요합니다. 퀘스트 완료, 완료 취소, 그리고 지연되어 도착한 액션 처리 등은 이전 사실에 의존할 수 있습니다. EF(Entity Framework)는 추적된 속성(tracked property)이 변경되었다는 것은 감지할 수 있지만, 제품이 또 다른 불변 개정(immutable revision)을 필요로 하는지 또는 어떤 이전 용어들이 해당 액션을 지배해야 할지는 결정할 수 없습니다.
이러한 결정에는 명시적인 소유자가 필요합니다. 여러 경로가 동일한 이유로 동일한 기록 값을 비교할 경우, 저는 그 규칙을 한 곳에 집중시키고 싶습니다. 또한 결합하기 전에 어떤 차이가 있는지 이해하고 싶습니다. 유사한 두 가지 비교는 서로 다른 비즈니스 질문을 나타낼 수 있습니다.
일반적인 업데이트의 경우, 영속성 경로는 일반적이어야 합니다: 추적된 엔티티를 로드하고, 의도하는 속성을 변경하며, EF가 변경 사항을 감지하도록 둡니다. 별도의 변경 바인딩 메커니즘은 단순히 이러한 할당들을 다시 설명하는 것 이상의 실제 문제를 해결해야 합니다.
트랜잭션 역시 같은 제약이 필요합니다. 하나의 SaveChanges 호출로 처리되는 간단한 작업은 일반적으로 관계형 공급자(relational provider)를 통해 필요한 트랜잭션 동작을 가집니다. 명시적인 트랜잭션 제어는 작업의 일관성 요구 사항을 따라야 합니다. Pocket Quests에는 히스토리, 진행 상황, 영수증을 조정하는 커맨드들이 있으며, 잠금(locking) 및 재시도(retry) 문제가 있습니다. 그러한 요구사항들은 더 많은 주의를 정당화합니다. 단일 쓰기가 자동으로 또 다른 트랜잭션 래퍼를 정당화하지는 않습니다.
저는 EF 구성이 똑같이 읽기 쉬웠으면 합니다. 중요한 계약과 차이점들, 즉 키(keys), 관계(relationships), 길이(lengths), 정밀도(precision), 동시성(concurrency), 그리고 필수 인덱스 동작을 구성해야 합니다. 일반적인 관례는 그대로 두어야 합니다. 한 줄을 제거하는 것조차 결과 모델 및 스키마에 미치는 영향을 이해해야 합니다.
이것은 리포지토리 레이아웃까지 이어집니다. Quest 엔티티, 매핑, 쿼리, 그리고 SQL 테이블은 각자의 프로젝트 내에서 일관된 도메인 폴더 아래에 위치해야 합니다. 계층을 가로질러 기능을 추적하는 것은 예측 가능해야 합니다.
테이블 및 컬럼 설명도 여기서 중요합니다. 타임스탬프는 서버가 행을 삽입한 시점인지, 아니면 오프라인 작업이 실제로 발생한 시점인지를 의미할 수 있습니다. 그 구분이 동작에 영향을 미칩니다. 메타데이터는 이를 설명해야 합니다. 인덱스는 제가 지적할 수 있는 쿼리나 제약 조건 뒤에 있어야 합니다.
소유권은 Pocket Quests를 넘어 확장됩니다. HoneyDrunk이 공유 리포지토리를 공유했기 때문에, 저는 해당 리포지토리에서 재사용 가능한 컴포넌트를 원합니다. 또 다른 헬퍼를 추가하기 전에, 에이전트가 이미 존재하는 것을 살펴보고 그것이 실제로 필요한 작업을 수행하는지 확인하기를 바랍니다.
퀘스트 진행 규칙은 제품에 속해야 합니다. 재사용 가능한 영속성 메커니즘은 공유 라이브러리에 속할 수 있습니다. 의존성은 그 소유권을 반영해야 합니다. 그럴듯한 패키지 이름이나 카탈로그 항목이 이 기능이 존재하고 이 사용 사례에 대해 작동한다는 충분한 증거가 될 수는 없습니다.
이것이 제가 검토에서 KISS, SOLID, 그리고 DRY를 의미하는 바입니다. 저는 동작을 따라갈 수 있어야 합니다. 각 컴포넌트는 일관된 역할을 가져야 합니다. 동일한 정책은 하나의 유지되는 구현체를 가져야 합니다. 모든 곳에 인터페이스를 추가하거나 관련 없는 헬퍼들을 공유 패키지에 밀어 넣는 것은 이 중 어느 것도 확립하지 못합니다.
또한 저는 이러한 기대치들이 다음 세션에서도 살아남기를 원합니다. 루트 AGENTS.md는 표준적 기준과 리포지토리의 특정 경계에 대해 언급해야 합니다. 여러 사본을 유지하는 것은 나중에 동일한 결정을 수정할 수 있는 여러 장소를 제공합니다.
이것은 Codex와 Claude에게 명확한 시작점을 제공합니다. 저는 여전히 지침들이 사용 가능했는지, 그리고 구현체가 그것들을 따르는지 확인해야 합니다. 표준을 작성하는 것은 한 단계이며, 그것을 기존 코드 전반에 적용하는 것은 추가적인 작업이 필요합니다.
CI(Continuous Integration), Sonar, 그리고 검토가 그 작업을 돕습니다. 테스트는 동작을 확인할 수 있습니다. 분석은 중복 및 기타 문제를 식별할 수 있습니다. 검토는 요청을 그 의존성들을 통해 따라가고 설계를 질문할 수 있습니다. 녹색 결과는 실행된 체크에 대해 유용한 무언가를 알려줄 뿐입니다. 그것이 책임이 이 리포지토리에 속하는지, 또는 추상화가 유지할 가치가 있는지 결정할 수는 없습니다.
저는 더 많은 것을 구축하기 위해 두 도구를 계속 사용하고 싶습니다. 또한 결과 코드가 제가 돌아왔을 때, 또는 다른 에이전트가 다음 기능을 시작했을 때도 이해하기 쉬기를 원합니다.
즉, 저는 여전히 경계(boundaries), 트레이드오프(trade-offs), 그리고 그 결과를 수용할지 여부를 결정하는 권한을 가집니다. AI는 구현(implementation)을 작성할 수 있습니다. 하지만 그것이 왜 그렇게 구축되었는지 설명할 수 있어야 합니다.