서비스 함수 내부의 비즈니스 흐름을 선언적 블록(sequence)으로 분해하는 언어 독립적 개념.
문제
기존 SSOT는 함수 외부까지만 커버한다:
| 기존 SSOT | 커버 범위 | 함수 내부? |
|---|---|---|
| OpenAPI | API 경로, 파라미터, 응답 스키마 | X |
| SQL DDL | 테이블 구조, 인덱스, 제약 | X |
함수 내부 — “조회 -> 검증 -> 생성 -> 응답"이라는 비즈니스 흐름은 선언할 곳이 없다. 구현 코드를 읽어야만 알 수 있다. 이 빈 공간을 sequence가 채운다.
SSOT 계층 구조
SSaC는 기존 SSOT 위에 쌓이는 세 번째 계층이다.
OpenAPI → API 경계 (경로, 파라미터, 응답)
SQL DDL → 데이터 경계 (테이블, 제약, 인덱스)
SSaC → 함수 내부 (비즈니스 흐름, sequence 선언)
─────────────────────────────────────────────────
구현 코드 → 코드젠이 생성 (에러 핸들링, 보일러플레이트)
위 세 계층이 선언을 담당하고, 구현 코드는 선언에서 파생된다. 사람이 작성하는 것은 선언뿐이다.
핵심 개념
sequence
함수 내부의 실행 블록을 타입화한 선언 단위.
what(뭘 하는가)만 선언하고, how(어떻게 하는가)는 코드젠이 채운다.
10개 고정 타입
| 타입 | 역할 | 예시 |
|---|---|---|
| authorize | 권한 검증 | 액션/리소스/ID 기반 정책 질의 |
| get | 리소스 조회 | Model.FindByID(id) -> result |
| guard nil | 결과가 null이면 종료 | 없으면 에러 후 종료 |
| guard exists | 결과가 있으면 종료 | 이미 존재하면 에러 후 종료 |
| post | 리소스 생성 | Model.Create(fields…) -> result |
| put | 리소스 수정 | Model.Update(id, fields…) |
| delete | 리소스 삭제 | Model.Delete(id) |
| password | 비밀번호 비교 | hash 비교 후 실패 시 종료 |
| call | 외부 호출 | CRUD로 표현 안 되는 리소스 조작, 실패 시 종료 |
| response | 응답 반환 | json, view, redirect |
타입이 닫혀 있으므로 타입별 템플릿 매칭으로 코드 생성이 가능하다. LLM 없이 심볼릭 코드젠이 성립하는 근거.
model의 범위
model은 DB 테이블에 한정되지 않는다. CRUD로 다룰 수 있는 모든 리소스가 model이다. DB 조회, 파일 존재 확인, 외부 API 호출 모두 get/post/put/delete로 동일하게 표현한다.
문법 예시 (Go PoC)
언어 독립적 개념이나, 참조 구현으로 Go 코멘트 문법을 사용한다. AI가 Go AST를 이미 알고 있으므로 추가 학습 비용이 없다.
단순 (생성)
// @sequence get
// @model Project.FindByID
// @param ProjectID request
// @result project Project
// @sequence guard nil project
// @message "프로젝트가 존재하지 않습니다"
// @sequence post
// @model Session.Create
// @param ProjectID request
// @param Command request
// @result session Session
// @sequence response json
// @var session
func CreateSession(w http.ResponseWriter, r *http.Request) {}
복합 (삭제 + 권한 + 검증 + 외부 호출)
// @sequence authorize
// @action delete
// @resource project
// @id ProjectID
// @sequence get
// @model Project.FindByID
// @param ProjectID request
// @result project Project
// @sequence guard nil project
// @message "프로젝트가 존재하지 않습니다"
// @sequence get
// @model Session.CountByProjectID
// @param ProjectID request
// @result sessionCount int
// @sequence guard exists sessionCount
// @message "하위 세션이 존재하여 삭제할 수 없습니다"
// @sequence call
// @component notification
// @param project.OwnerEmail
// @param "프로젝트가 삭제됩니다"
// @sequence call
// @func cleanupProjectResources
// @param project
// @result cleaned bool
// @sequence delete
// @model Project.Delete
// @param ProjectID request
// @sequence response json
func DeleteProject(w http.ResponseWriter, r *http.Request) {}
코드젠 결과
위 sequence 선언에서 파생되는 구현:
func CreateSession(w http.ResponseWriter, r *http.Request) {
// get
project, err := projectModel.FindByID(projectID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// guard nil
if project == nil {
http.Error(w, "프로젝트가 존재하지 않습니다", http.StatusNotFound)
return
}
// post
session, err := sessionModel.Create(projectID, command)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// response json
json.NewEncoder(w).Encode(map[string]interface{}{
"session": session,
})
}
토큰 비용 비교
| sequence 선언 | 구현 코드 | |
|---|---|---|
| 줄 수 | 10~15줄 | 30~100줄 |
| 표현 내용 | what (뭘 하는가) | how (어떻게 하는가) |
| 에러 핸들링 | 없음 (코드젠이 생성) | 전부 포함 |
| 라이브러리 의존 | 없음 | import, 초기화, 호출 |
| 변경 시점 | 비즈니스 흐름 변경 시 | 코드젠 재실행으로 자동 반영 |
사람이 유지보수하는 것은 sequence 선언뿐이다. 구현 코드는 선언이 바뀌면 코드젠을 다시 돌려 덮어쓴다.
설계 원칙
early return — 모든 타입의 공통 계약
sequence가 선형 나열로 성립하는 전제. response를 제외한 모든 sequence는 실패 시 즉시 탈출한다. call도 예외가 아니다. 분기 없이 위에서 아래로 읽으면 끝이다.
| 타입 | 실패 조건 |
|---|---|
| authorize | 권한 없음 → return |
| get | 조회 에러 → return |
| guard nil | null → return |
| guard exists | 존재 → return |
| post/put/delete | DB 에러 → return |
| password | 불일치 → return |
| call | 실패 → return |
| response | 마지막 (반환) |
call이 guard와 동일한 계약을 따르므로, call 뒤에 별도의 guard를 붙일 필요가 없다. 코드젠이 모든 call에 에러 체크 + early return을 자동 생성한다.
@transaction
함수 레벨 메타데이터. sequence 타입(10개)이 아니라 함수 전체의 트랜잭션 wrapping 여부를 선언한다.
// @transaction
// @sequence get
// @model Account.FindByID
// @param AccountID request
// @result account Account
// ...
early return 구조와 결합하면 트랜잭션 경계는 자연스럽다:
@transaction 선언됨
→ guard 실패 → rollback + return
→ 마지막 sequence 성공 → commit
→ 에러 발생 → rollback + return
분기가 없으므로 트랜잭션 경계도 “함수 전체"로 단일하다. begin/end 위치를 지정할 필요가 없고, @transaction 유무만으로 코드젠이 wrapping 여부를 결정한다. 트랜잭션이 2개 필요하다면 책임이 2개라는 뜻이다. 함수를 분리하라.
call — model의 특수형
call은 model과 동일한 본질을 가진다. model이 CRUD(get/post/put/delete)로 표현되는 리소스 조작이라면, call은 CRUD로 표현 안 되는 리소스 조작이다. 둘 다 외부 의존이 있는 작업을 호출하고, 실패하면 return한다.
리소스 조작
+-- model (CRUD) → get/post/put/delete
+-- call (비CRUD) → component 또는 func
@component— 등록제. 반복 패턴이 3번 이상 나타나면 승격한다.@func— 고유 로직. 사람 또는 AI가 직접 구현한다.
분기 후 합류, 루프 내 조건부 처리 등 복잡한 로직은 sequence를 확장하지 않고 call로 위임한다. 위임하되, 계약은 동일하다 — 실패 시 탈출.
@message
모든 sequence 타입에 선택적으로 붙일 수 있는 메타데이터. 실패 시 사용자에게 전달할 메시지를 지정한다.
// @sequence guard nil project
// @message "프로젝트가 존재하지 않습니다"
// @sequence post
// @model Session.Create
// @param ProjectID request
// @param Command request
// @result session Session
// @message "세션 생성에 실패했습니다"
@message를 생략하면 코드젠이 타입+모델명에서 기본 메시지를 자동 생성한다. 커스텀 메시지가 필요한 경우에만 선언하면 된다.
순수 함수는 서비스 계층에 존재하지 않는다
sequence에 선언되는 모든 블록은 외부 의존이 있는 작업이다. 포맷 변환, 값 계산 등 순수 기능은 서비스 계층이 아니라 모델 내부 또는 호출되는 함수 내부에서 처리한다. 서비스 함수는 조율(orchestration)만 하고, 순수 계산은 호출 대상에게 속한다.
타입 참조
SSaC는 자체 타입을 정의하지 않는다. 기존 SSOT에서 산출된 타입을 참조한다.
| 원천 | 예시 |
|---|---|
| DB 스키마 (SQL DDL) | Project, Session |
| API 명세 (OpenAPI) | CreateSessionRequest |
| 인터페이스 정의 | FileSystem, Cache |
명명 규칙
| 구분 | 규칙 | 예시 |
|---|---|---|
| 타입 | PascalCase, SSOT에서 가져옴 | Project, Session |
| 변수 | camelCase, @result로 선언 | project, sessionCount |
| request 필드 | PascalCase, 요청에서 추출 | ProjectID, Command |
| 예약 변수 | camelCase, 프레임워크 제공 | currentUser, config |
타입과 변수의 대소문자가 다르므로 선언만 보고 구별할 수 있다. project는 변수, Project는 타입이다.
언어 확장
SSaC는 언어 독립적 개념이다. Go는 참조 구현(PoC)이며, 동일한 sequence 선언에서 다른 언어의 코드를 생성할 수 있다.
sequence 선언 (공통)
|
+-- Go 코드젠
+-- Python 코드젠
+-- TypeScript 코드젠
+-- ...
스펙(타입 정의, 문법, 검증 규칙)과 코드젠은 분리된다.
Go 참조 구현은 GitHub에서 확인할 수 있다.