서비스 함수 내부의 비즈니스 흐름을 선언적 블록(sequence)으로 분해하는 언어 독립적 개념.

문제

기존 SSOT는 함수 외부까지만 커버한다:

기존 SSOT커버 범위함수 내부?
OpenAPIAPI 경로, 파라미터, 응답 스키마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 nilnull → return
guard exists존재 → return
post/put/deleteDB 에러 → 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에서 확인할 수 있다.