사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
데이터 변경
조회수를 하나 올리거나, 여러 행을 한꺼번에 보관 처리하거나, 사용자가 연 레코드 하나를 고쳐야 할 때가 있습니다. 쓰는 길은 두 가지이고, 어느 쪽을 고르느냐에 따라 훅이 실행되는지가 갈립니다.
문서 경로
문서를 읽어 와 고친 뒤 저장합니다. 그래서 훅이 실행되고, 삭제라면 cascade도 함께 일어납니다.
await this.updatePost(id, data);쿼리 쓰기
SQL 문 하나를 원자적으로 보내고 아무것도 읽지 않습니다. 빠르고 동시 요청에도 안전하지만 훅은 실행되지 않습니다.
await this.Post.updateOne(filter, change);무엇이 실행되는가
문서마다 훅이나 cascade가 돌아야 한다면 문서 경로를 씁니다. 아래 표에는 세 가지 말이 나옵니다:
용어설명
schema.preschema.post
스키마 훅입니다.
_onSchema에 등록하고, 문서를 저장하거나 삭제할 때마다 실행됩니다._preCreate_postCreate_preUpdate_postUpdate_preRemove_postRemove
서비스 훅입니다. service에 override하며, 생성된
create<Model>, update<Model>, remove<Model>을 거칠 때만 실행됩니다.cascade
field에 선언하는 연쇄 삭제입니다. 지운 문서에 딸린 문서가 함께 지워집니다.
메서드
스키마 훅
서비스 훅
cascade
문서 경로: 읽고, 고치고, 저장
update<Model>(id, data)
✓
✓
service에 생성되는 메서드입니다. 레코드 하나를 고칠 때 기본으로 씁니다.
remove<Model>(id)
✓
✓
✓
service에 생성되는 메서드입니다. 문서를 soft 삭제한 뒤 cascade를 실행합니다.
pickAndWrite(id, data)
✓
model에서 문서를 집어 값을 넣고 저장합니다.
pickOneAndWrite(query, data)는 쿼리로 집습니다.doc.set(data).save()
✓
이미 문서를 들고 있을 때 같은 일을 직접 쓰는 방식입니다.
쿼리 쓰기: SQL 문 하나, 아무것도 읽지 않음
updateOne · updateMany
가장 최근에 만든 행 하나, 또는 맞는 행 전부를 고칩니다.
removeOne · removeMany
가장 최근에 만든 행 하나, 또는 맞는 행 전부를 soft 삭제합니다.
updateById · removeById
같은 쿼리 쓰기를 id 하나로 좁힌 것입니다.
update<Filter>(…).set(…)
필터마다 생성됩니다.
remove<Filter>, updateOne<Filter>, removeOne<Filter>도 같습니다.bulkWrite(operations)
updateOne 작업 목록을 하나씩 차례로 실행합니다.✓실행됨실행되지 않음


쿼리 쓰기는 훅을 실행하지 않고, 따라서 cascade도 일어나지 않습니다.
- 뒤따라 실행되는 것이 없습니다. 스키마 훅도, 서비스 훅(
_postRemove포함)도,cascade도 돌지 않습니다. 지운 행의 파일, 자식 문서, 카운터가 그대로 남고, 삭제가 soft라서 아무도 그 손실을 알려 주지 않습니다. updateById와removeById는 문서 경로처럼 보일 뿐입니다. id 하나로 좁힌 같은 쿼리 쓰기라서 아무것도 실행하지 않습니다.updateOne과removeOne은 항상 가장 최근에 만든 행을 건드리고 개수만 돌려줍니다. "이런 행은 많아야 하나"일 때 쓰는 것이지, 큐에서 다음 항목을 꺼내는 용도가 아닙니다.- live 목록도 알지 못합니다.
.live()slice는 같은 훅으로 변경을 받으므로 쿼리 쓰기는 전달되지 않습니다. upsert가 새로 넣은 행만 예외입니다.
두 가지 작성법
쿼리 쓰기는 모두 필터를 먼저 받고, 그다음에 바꿀 내용을 받습니다. 바꿀 내용은 객체나 builder 함수로 씁니다:
| 형태 | 언제 | 예 |
|---|---|---|
| 객체 | 값만 넣을 때 씁니다. 값을 그대로 쓰면 set과 같습니다. | { status: "published", pinned: true } |
| builder 함수 | inc, addToSet 같은 연산자가 필요할 때 씁니다. | ({ inc }) => ({ viewNum: inc(1) }) |
model 클래스 안에서는 이렇게 씁니다:
apps/myapp/lib/post/post.document.ts
- builder가 받는 인자에 연산자가 모두 들어 있습니다. 필터에서
q가 조건을 담는 것과 같습니다. 쓰는 것만 꺼내면 되고, import할 것은 없습니다. - key는 field 경로입니다.
"profile.city"처럼 쓰면 객체 field 안쪽에 쓰고, 선언하지 않은 field를 가리키면 에러가 납니다. - 기본 컬럼 네 개에는
set과unset만 씁니다.id,createdAt,updatedAt,removedAt은inc를 비롯한 다른 연산자를 거절합니다.


builder는 동기로 실행됩니다. 비밀번호 해시처럼 await가 필요한 값은 호출 전에 계산해 두고, builder 안에서는 그 값을 참조만 합니다.
카운터와 집합
숫자와 배열 연산자는 데이터베이스 안에서 실행됩니다. 같은 카운터를 동시에 올리는 두 요청도 둘 다 반영되고, 서로의 변경을 덮어쓰지 않습니다:
apps/myapp/lib/post/post.document.ts
- 숫자:
inc,mul,min,max. field가 없으면inc와mul은 0에서 시작하고,min과max는 값을 그대로 씁니다. 인자 없는inc()는 1을 더합니다. - 배열:
push,addToSet,pull. 배열이 없으면 빈 배열에서 시작합니다.push는 항상 붙이고,addToSet은 없을 때만 붙이며,pull은 같은 요소를 모두 뺍니다. updateMany는 건드린 행 수를modifiedCount로 돌려줍니다. SQL 문 하나로 모두 처리됩니다.


집합에는 단순 값만 담으세요.
addToSet과 pull은 요소를 값으로 비교하므로 id, 문자열, 숫자에서는 믿을 수 있지만 객체에서는 그렇지 않습니다.Upsert
upsert는 맞는 행을 고치고, 맞는 행이 없으면 새 행을 넣습니다. 세 번째 인자로
{ upsert: true }를 넘깁니다:apps/myapp/lib/stat/stat.document.ts
맞는 행이 없으면 호출의 각 부분은 이렇게 쓰입니다:
| 호출의 부분 | 새 행에서는 |
|---|---|
| { key: "daily-visits" } | 필터에 값으로 쓴 조건은 새 행에 그대로 들어갑니다. q.oneOf() 같은 조건은 빠집니다. |
| total: inc(1) | 연산자는 빈 값에 적용되므로 total은 1에서 시작합니다. |
| status: setOnInsert("active") | 이 삽입 때만 쓰입니다. 맞는 행을 찾아 고칠 때는 무시됩니다. |
| 반환값 | upsertedId에 새 id가 담기고 matchedCount는 0입니다. |
- upsert는
updateOne,updateById,bulkWrite에서만 됩니다.updateMany는 옵션을 받지 않습니다. - 삽입할 때는
"create"스키마 훅만 실행됩니다."save"훅과_postCreate같은 서비스 훅은 실행되지 않습니다.
SQL로 바뀌는 방식
모든 연산자는
_doc 컬럼 위의 JSON 표현식 하나로 겹쳐지고, 쓸 때마다 updatedAt이 갱신됩니다. 데이터베이스는 이 변경 전체를 SQL 문 하나로 적용합니다.연산자하는 일 · SQL
값 그대로
field에 값을 넣습니다.
set의 줄임입니다.set(value)
field에 값을 넣습니다.
unset()
field를 지웁니다.
inc(by = 1)
by만큼 더합니다. field가 없으면 0에서 시작합니다.mul(by)
by를 곱합니다. field가 없으면 0으로 봅니다.min(value)
저장된 값과
value 중 작은 쪽을 남깁니다.max(value)
저장된 값과
value 중 큰 쪽을 남깁니다.push(value)
배열 끝에 붙입니다. 배열이 없으면 빈 배열에서 시작합니다.
addToSet(value)
같은 요소가 아직 없을 때만 붙입니다.
pull(value)
value와 같은 요소를 모두 뺍니다.setOnInsert(value)
upsert가 새 행을 넣을 때만 field에 값을 넣습니다.
중첩 경로
점으로 이은 key는 객체 field 안쪽에 씁니다.
여러 연산 함께
연산자 여러 개가 표현식 하나로 겹쳐집니다.
- 개념만 보이도록 줄인 SQLite 기준 SQL입니다. Postgres는 같은 일을 하는
jsonb함수를 씁니다. - 모든 연산자는 변경 전 문서를 읽습니다. 그래서 한 호출 안의 변경은 모두 같은 원래 값을 봅니다.
팁과 주의할 점
- 카운터와 대량 상태 변경에는 쿼리 쓰기를 씁니다. 단, 삭제 부수효과가 없는 model에서만입니다. 훅, cascade, 도메인 로직이 돌아야 한다면 문서 경로를 쓰세요.
- 원자적 쓰기는 model 클래스에 둡니다.
<model>.document.ts에 쓰고!!modifiedCount를 돌려주면 service는 성공 여부만 받습니다. - builder 형태를 쓰세요. update 헬퍼를 module scope에서 import하지 않습니다.
q.search()는 쓰기의 필터에 쓸 수 없습니다. 검색이 들어간 필터로 쓰면 에러가 나므로, id를 먼저 찾은 뒤 id로 씁니다.bulkWrite는 작업을 하나씩 실행합니다. 작업마다 SQL 문이 따로 실행되고, 결과의 개수는 합산됩니다.
쓰기가 돌려주는 값
쿼리 쓰기는 모두 같은 모양의 결과를 돌려줍니다. 쓰기가 반드시 어떤 행에 닿아야 한다면
modifiedCount를 확인하세요:acknowledgedboolean
SQL 문이 실행되면
true입니다.matchedCountnumber
필터에 맞은 행 수입니다. upsert로 새로 넣었다면 0입니다.
modifiedCountnumber
쓰기가 바꾼 행 수입니다. upsert로 넣은 행도 셉니다.
upsertedIdstring | nulloptional
upsert로 새 행을 넣었을 때의 id입니다. 아니면
null이거나 없습니다.이어서 읽기