---
term: '데이터베이스 트리거 (BeforeSave와 AfterSave)'
seoTitle: '데이터베이스 트리거란? beforeSave와 afterSave 훅 설명'
headline: '데이터베이스 트리거(BeforeSave와 AfterSave)란?'
slug: database-triggers-beforesave-aftersave
category: backend-compute
shortDefinition: '데이터베이스 트리거는 데이터 이벤트가 발생하면 자동으로 실행되어 저장 전에는 검증하고 저장 후에는 반응하는 코드 훅입니다.'
relatedTerms:
  - cloud-code-serverless-functions
  - webhooks
  - crud-operations
  - real-time-live-queries
contrastsWith:
  - webhooks
aboutTerms:
  - 'beforeSave'
  - 'afterSave'
  - 'BEFORE/AFTER Triggers'
faq:
  - question: '데이터베이스 트리거란 무엇입니까?'
    answer: '특정 테이블이나 클래스에서 삽입, 수정, 삭제 같은 데이터 이벤트가 발생할 때 자동으로 실행되는 절차적 코드입니다. 고전적인 형태는 SQL 데이터베이스 내부에 있고, 현대적인 애플리케이션 수준의 형태는 백엔드가 모든 쓰기 앞뒤로 실행하는 beforeSave나 afterSave 같은 훅 함수입니다.'
  - question: 'BEFORE 트리거와 AFTER 트리거의 차이는 무엇입니까?'
    answer: '할 수 있는 일과 실행 시점이 다릅니다. BEFORE는 쓰기보다 먼저 실행되어 검증하거나, 들어오는 데이터를 수정하거나, 작업 전체를 중단할 수 있습니다. AFTER는 쓰기가 성공한 뒤에 실행되므로 이미 일어난 일을 바꿀 수 없고, 기록하고 집계하고 알리는 식으로 반응만 할 수 있습니다. 실패한 쓰기에 대해서는 AFTER가 절대 실행되지 않습니다.'
  - question: '트리거와 저장 프로시저의 차이는 무엇입니까?'
    answer: '호출 방식이 다릅니다. 저장 프로시저는 명시적으로 호출되고, 파라미터를 받아 결과를 반환합니다. 트리거는 호출되는 일이 없습니다. 테이블에 연결된 채 파라미터 없이, 해당 이벤트가 발생하면 자동으로 실행됩니다. 절차적 메커니즘은 같지만 활성화 모델은 정반대입니다.'
  - question: '로직은 트리거에 두어야 합니까, 애플리케이션 코드에 두어야 합니까?'
    answer: '합의된 답은 혼합형입니다. 검증, 무결성, 감사처럼 모든 쓰기 경로에서 지켜져야 하는 규칙은 트리거(또는 훅)에, 복잡한 워크플로는 애플리케이션 서비스에 둡니다. 애플리케이션 수준의 훅은 현대적인 절충안으로, 트리거의 보장을 실제 프로그래밍 언어와 버전 관리와 함께 제공합니다.'
  - question: '트리거는 성능을 떨어뜨립니까?'
    answer: '트리거는 쓰기 경로 안에서 동기적으로 실행되므로 느린 트리거는 모든 저장을 느리게 만들고, 행 수준 트리거는 대량 작업에서 실행 횟수가 행 수만큼 늘어납니다. 10만 행을 수정하면 10만 번 실행됩니다. before 훅은 가볍게 유지하고, 느린 반응은 after 훅이나 백그라운드 작업으로 넘기는 것이 좋습니다.'
  - question: '트리거가 무한 루프를 일으킬 수 있습니까?'
    answer: '그렇습니다. 잘 알려진 문제입니다. 자기 테이블에 쓰는 트리거는 자신을 다시 실행시키고, 방금 처리한 객체를 다시 저장하는 afterSave는 무언가가 깨질 때까지 재귀합니다. 쓰기 전에 실제로 무엇이 바뀌었는지 확인해서 막아야 하며, 종료 조건 없이 트리거를 일으킨 객체를 그 객체의 after 훅에서 다시 저장해서는 안 됩니다.'
  - question: '트리거에서 외부 서비스를 호출할 수 있습니까?'
    answer: 'SQL 트리거는 사실상 데이터베이스 밖으로 나갈 수 없고, 나가서도 안 됩니다. 이것이 애플리케이션 수준 훅의 대표적인 장점입니다. JavaScript로 작성한 afterSave는 백엔드 프로세스에서 전체 생태계를 활용해 실행되므로 푸시 알림을 보내고, 어떤 API든 호출하고, 발신 웹훅을 실행할 수 있습니다.'
  - question: 'beforeSave는 무엇에 사용합니까?'
    answer: '쓰기 전에 실행되어야 하는 세 가지 작업에 사용합니다. 검증(오류를 던지면 모든 클라이언트에서 저장이 실패), 정규화(필드의 공백 제거, 길이 자르기, 표준 형식 변환), 기본값이나 계산된 값 설정입니다. 부수 효과는 여기에 두면 안 됩니다. 나중에 저장이 실패해도 부수 효과는 이미 일어난 뒤이기 때문입니다.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'PostgreSQL — CREATE TRIGGER'
    url: 'https://www.postgresql.org/docs/current/sql-createtrigger.html'
  - name: 'MySQL — Trigger Syntax and Examples'
    url: 'https://dev.mysql.com/doc/refman/8.4/en/trigger-syntax.html'
  - name: 'Cloud Code triggers guide'
    url: 'https://docs.parseplatform.org/cloudcode/guide/#beforesave-triggers'
  - name: 'Sequelize — Hooks lifecycle'
    url: 'https://sequelize.org/docs/v6/other-topics/hooks/'
  - name: 'Database trigger — Wikipedia'
    url: 'https://en.wikipedia.org/wiki/Database_trigger'
cta:
  title: '모든 쓰기가 따르는 규칙'
  text: 'Back4app의 어느 클래스에든 beforeSave와 afterSave를 등록하면 SDK, REST, GraphQL, 관리 스크립트까지 모든 클라이언트에 규칙이 적용됩니다. 검증, 정규화, 집계, 알림, 외부 호출까지 모두 JavaScript로 처리합니다.'
  linkText: '무료로 시작하기'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-10-31'
translationKey: database-triggers-beforesave-aftersave
---

**데이터베이스 트리거는 데이터 이벤트가 발생하면 자동으로 실행되어 저장 전에는 검증하고 저장 후에는 반응하는 코드 훅입니다.** 이 개념에는 하나의 원칙을 공유하는 두 계보가 있습니다. 데이터베이스 엔진 내부에 사는 절차적 코드인 고전적인 **SQL 트리거**와, 백엔드에서 클래스별로 등록하는 JavaScript인 현대적인 **애플리케이션 수준 훅**(`beforeSave`, `afterSave`, `beforeDelete`)입니다. 두 형태가 담고 있는 약속은 같습니다. 어느 클라이언트, SDK, 스크립트가 수행했든 규칙은 *모든* 쓰기에 대해 실행됩니다. 바로 이것이 클라이언트 측 검증이 결코 약속할 수 없는 부분입니다.

## 핵심 요약

| 질문 | 답변 |
| --- | --- |
| 원칙 | 데이터 이벤트에 묶인 코드 – 자동, 테이블/클래스 단위, 모든 쓰기 경로 |
| Before = | 검증 · 정규화 · 기본값 – 쓰기를 아직 바꾸거나 **중단**할 수 있음 |
| After = | 반응 – 집계, 알림, 동기화. 쓰기는 이미 끝났으므로 멱등성 필수 |
| 두 가지 형태 | 엔진 내부의 SQL 트리거 · 백엔드 프로세스의 JS 훅 |
| 고전적인 버그 | 숨은 로직 · 자기 자신을 계속 실행시키는 무한 루프 · 모든 저장을 막는 느린 트리거 |

## 훅 모델의 실제 동작

검증 규칙과 그 집행을 양쪽 끝에서 보여 줍니다. 서버가 규칙을 한 번 정의하면 어디에 있는 클라이언트든 모두 이를 물려받습니다:

**JavaScript:**

```javascript
// JavaScript — Cloud Code (cloud/main.js)
// beforeSave: validate + normalize — can still change or ABORT the write
Parse.Cloud.beforeSave('Review', (req) => {
  const stars = req.object.get('stars');
  if (stars < 1 || stars > 5) throw 'Stars must be between 1 and 5';
  const comment = req.object.get('comment');
  if (comment && comment.length > 500) {
    req.object.set('comment', comment.slice(0, 497) + '…');
  }
});

// afterSave: side effects — the write already happened; be idempotent
Parse.Cloud.afterSave('Review', async (req) => {
  if (req.object.existed()) return; // count only NEW reviews, once
  await updateAverageStars(req.object.get('movie'));
});
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// The hook fires for EVERY write path — this client included
final review = ParseObject('Review')
  ..set('movie', 'Arrival')
  ..set('stars', 9); // invalid — no client-side check needed
final response = await review.save();
print(response.error?.message); // "Stars must be between 1 and 5"
// The beforeSave hook rejected it server-side. A forged REST call,
// another SDK, an admin script — same rule, same rejection.
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// The hook fires for EVERY write path — this client included
var review = Review()
review.movie = "Arrival"
review.stars = 9 // invalid — no client-side check needed
do {
    _ = try await review.save()
} catch {
    print(error.localizedDescription) // "Stars must be between 1 and 5"
}
// The beforeSave hook rejected it server-side. A forged REST call,
// another SDK, an admin script — same rule, same rejection.
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// The hook fires for EVERY write path — this client included
val review = ParseObject("Review")
review.put("movie", "Arrival")
review.put("stars", 9) // invalid — no client-side check needed
try {
    review.save()
} catch (e: ParseException) {
    println(e.message) // "Stars must be between 1 and 5"
}
// The beforeSave hook rejected it server-side. A forged REST call,
// another SDK, an admin script — same rule, same rejection.
```

전체 훅 계열은 같은 형태를 데이터 수명 주기 전체로 확장합니다. `beforeSave`/`afterSave`, `beforeDelete`(Photo가 남아 있는 Album의 삭제 차단)와 `afterDelete`(하위 객체 정리), 그리고 쿼리를 재작성하고 결과에서 필드를 제거하는 `beforeFind`/`afterFind`까지, [CRUD](/glossary/ko/crud-operations/) 전체를 감싸는 구조입니다.

## 고전적인 형태: SQL 트리거

```sql
CREATE TRIGGER audit_price_change
AFTER UPDATE ON products
FOR EACH ROW                              -- 행 수준: 영향받는 행마다 실행
WHEN (OLD.price IS DISTINCT FROM NEW.price)
EXECUTE FUNCTION log_price_change();      -- OLD와 NEW에 두 버전이 모두 담김
```

[PostgreSQL](https://www.postgresql.org/docs/current/sql-createtrigger.html)과 [MySQL](https://dev.mysql.com/doc/refman/8.4/en/trigger-syntax.html) 레퍼런스에 따르면 모든 데이터베이스가 공유하는 분류 체계는 다음과 같습니다. **실행 시점**은 BEFORE(`NEW`를 수정하거나 중단할 수 있음), AFTER(커밋된 행에 반응), INSTEAD OF(주로 뷰에서 작업 자체를 대체)로 나뉩니다. **이벤트**는 삽입, 수정, 삭제입니다. **단위**는 행 수준과 문장 수준으로 나뉩니다(10개 행을 수정하면 행 트리거는 10번, 문장 트리거는 한 번 실행). 새겨 둘 만한 의미론이 두 가지 있습니다. SQL 트리거는 쓰기와 *같은 트랜잭션 안에서* 실행되므로 트리거가 실패하면 작업 전체가 롤백됩니다. 그리고 AFTER 트리거는 실제로 성공한 쓰기에 대해서만 실행됩니다.

## before vs. after: 작업에 따른 선택

| | before 훅 | after 훅 |
| --- | --- | --- |
| 데이터 변경 가능 여부 | **가능** – 수정, 기본값 지정, 자르기 | 불가 – 이미 기록됨 |
| 쓰기 중단 가능 여부 | **가능** – 오류를 던지면 저장 실패 | 불가 |
| 적합한 용도 | 검증, 정규화, 계산된 필드 | 카운터, 알림, 동기화, 감사 |
| 부적합한 용도 | **부수 효과** – 나중에 저장이 실패해도 효과는 이미 발생 | 쓰기를 막았어야 하는 모든 것 |
| 실패 시 동작 | 작업을 거부하고 클라이언트에 오류 반환 | 대개 결과를 기다리지 않음(fire-and-forget) – 오류는 로그에 남음 |
| 원칙 | 빠르게 – 모든 저장을 막아 세움 | **멱등성** – 다시 실행될 수 있음 |

어느 설명 글도 짚지 않는, 여기서 따라 나오는 결론이 있습니다. before 훅 안의 부수 효과는 구조적으로 버그입니다(이메일이 발송된 뒤 저장이 실패합니다). 그리고 멱등하지 않은 after 훅은 재시도 한 번이면 중복되는 카운터입니다. Back4app 같은 훅 시스템에서 `afterSave`는 클라이언트가 이미 응답을 받은 뒤에 완료됩니다. 반응은 설계상 비동기이므로, 그 실패는 감내할 수 있어야 하고 로그로 남아야 합니다.

```mermaid
flowchart LR
  accTitle: before 훅과 after 훅을 거치는 쓰기 수명 주기
  accDescr: 어느 클라이언트에서 온 쓰기든 먼저 before 훅을 통과하며, before 훅은 쓰기를 검증하거나 수정하거나 중단할 수 있습니다. 허용되면 데이터베이스가 쓰기를 커밋하고, 이어서 after 훅이 카운터, 알림, 웹훅 같은 부수 효과로 반응합니다. 이 부수 효과는 두 번 이상 실행될 수 있으므로 멱등해야 합니다.
  C["모든 클라이언트<br/>SDK · REST · 스크립트"] --> B{"beforeSave<br/>검증 · 정규화"}
  B -->|"throw"| X["저장 거부 –<br/>클라이언트에 오류"]
  B -->|"허용 (수정되었을 수 있음)"| W[("쓰기 커밋")]
  W --> A["afterSave<br/>부수 효과, 멱등"]
  A --> R["카운터 · 알림 ·<br/>발신 웹훅"]
```

## 훅 vs. SQL 트리거

| | 애플리케이션 훅 (beforeSave/afterSave) | SQL 트리거 |
| --- | --- | --- |
| 언어 | JavaScript + 전체 SDK와 생태계 | SQL / 절차적 SQL 방언 |
| 외부 호출 | 가능 – API, 푸시, [웹훅](/glossary/ko/webhooks/) | 사실상 불가 – 해서도 안 됨 |
| 위치 | 자체 코드베이스: 버전 관리, 테스트, 배포 | 스키마: 데이터베이스 내부 |
| 트랜잭션 | before 훅이 쓰기를 통제하고, after 훅은 응답 후 실행 | 같은 트랜잭션 – 실패하면 롤백 |
| 적용 범위 | 백엔드 API를 거치는 모든 요청 | 출처와 무관하게 테이블에 대한 모든 쓰기 |
| 사각지대 | 데이터베이스에 직접 쓰면 우회됨 | 애플리케이션 디버거에서 로직이 보이지 않음 |

마지막 행이 솔직한 대칭입니다. 각 형태의 보장은 자기 계층으로 한정됩니다. SQL 트리거는 제멋대로 열린 psql 세션까지 잡아내지만 애플리케이션 도구에서 로직을 숨깁니다. API 수준 훅은 백엔드를 거치는 모든 클라이언트 경로에 적용되지만 데이터베이스 직접 접근에는 적용되지 않습니다. 그래서 API 게이트웨이를 소유한(모든 트래픽이 그곳을 거치는) 플랫폼이 실질적으로 두 장점을 모두 얻습니다. ORM에 관한 각주도 여기에 덧붙여 둡니다. [ORM 훅](https://sequelize.org/docs/v6/other-topics/hooks/)은 ORM을 거칠 때만 실행되며, 대량 작업과 원시 SQL은 그대로 지나쳐 갑니다.

## 함정, 솔직하게

**숨은 로직**은 고전적인 함정입니다. 트리거가 조용히 값을 바꾸는 동안 개발자는 몇 시간씩 자기 코드를 디버깅합니다. 트리거는 호출 지점에서 보이지 않으므로 문서화하고 수를 적게 유지해야 합니다. **무한 루프**: 자기 테이블에 쓰는 트리거(또는 자기 객체를 저장하는 `afterSave`)는 자신을 다시 실행시킵니다. 무엇이 바뀌었는지를 기준으로 막고, 재귀가 스스로 종료 조건을 찾아내기 전에 종료 조건을 정해 두어야 합니다. **동기 비용**: before 훅은 모든 쓰기의 지연 시간 안에 자리합니다. 200ms짜리 훅은 모든 저장을 200ms 느리게 만들고, 대량 쓰기에서는 행 수준 실행이 행 수만큼 늘어납니다. **연쇄 트리거**: 트리거가 트리거를 실행하고 그 트리거가 또 트리거를 실행하면 삽입 하나가 고고학 발굴 작업이 됩니다. 수십 년의 실무에서 나온 총괄 원칙은 이렇습니다. 트리거는 *규칙*을 집행합니다. 트리거가 *워크플로*를 조율하기 시작하는 순간, 그 워크플로를 [함수](/glossary/ko/cloud-code-serverless-functions/)나 [작업](/glossary/ko/background-jobs-task-schedulers/)으로 옮기고 트리거는 큐에 넣는 일만 하게 해야 합니다.

## 일반적인 사용 사례

- **모든 클라이언트가 따르는 검증** – 별점 1–5 규칙을 위조된 요청에도, 앞으로 나올 SDK에도 똑같이 집행합니다.
- **감사 추적** – 누가 언제 무엇을 바꿨는지를 협조적인 클라이언트가 아니라 이벤트 자체가 기록합니다.
- **비정규화된 카운터와 계산된 필드** – 평균, 개수, 검색용 중복 값을 쓰기 시점에 정확하게 유지합니다.
- **연쇄 무결성** – 하위 객체가 있으면 삭제를 거부하거나, 삭제 후 하위 객체를 정리합니다.
- **반응형 부수 효과** – `afterSave`가 [푸시 알림](/glossary/ko/push-notifications-apns-fcm/)이나 [발신 웹훅](/glossary/ko/webhooks/)을 실행해 데이터베이스 이벤트가 연동 이벤트가 됩니다.

## 어떤 훅을 어디에? 결정 매트릭스

| 작업 | 훅 |
| --- | --- |
| 잘못된 데이터 거부 | beforeSave – 오류 던지기 |
| 공백 제거, 기본값, 표준 형식 변환 | beforeSave – 값 수정 |
| 카운터나 집계 값 갱신 | afterSave – 멱등하게 |
| 사람이나 시스템에 알림 | afterSave → 푸시 / 웹훅 / 큐 |
| 위험한 삭제 차단 | beforeDelete |
| 삭제 후 정리 | afterDelete |
| 사용자별 쿼리 범위 강제 | beforeFind |
| 길거나 느린 반응 | afterSave는 [작업](/glossary/ko/background-jobs-task-schedulers/)을 *큐에 넣기만* 함 – 작업을 인라인으로 처리하지 않음 |

## 한계와 트레이드오프

- **보이지 않는 것은 자동화의 대가입니다.** 아무도 호출하지 않는 로직은 아무도 기억하지 않는 로직입니다. 명명 규칙, 문서, 코드 리뷰가 이 마법을 감사 가능한 상태로 유지합니다.
- **훅은 자기 계층으로 한정됩니다.** API 훅은 데이터베이스 직접 쓰기를 놓치고, SQL 트리거는 아무것도 놓치지 않지만 애플리케이션 도구에서는 보이지 않습니다. 어느 사각지대를 선택했는지 알고 있어야 합니다.
- **쓰기 지연 시간이 예산입니다.** 모든 before 훅이 이 예산을 씁니다. 쿼리를 측정하듯 훅도 측정해야 합니다.
- **after 훅은 최종 일관성을 따릅니다.** 카운터는 몇 밀리초 늦게 반영되고 두 번 실행될 수 있습니다. 읽기(와 재시도)를 그에 맞게 설계해야 합니다.
- **트리거는 제약 조건을 대체하지 않습니다.** 고유 인덱스, 외래 키, [ACL](/glossary/ko/access-control-lists-acl/)이 더 싸고 더 이른 단계에서 집행합니다. 트리거는 선언적 규칙이 멈추는 곳에서 이어받습니다.

## Back4app에서의 트리거

Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. 여기서 트리거는 [Cloud Code](/glossary/ko/cloud-code-serverless-functions/)의 데이터 이벤트 계열입니다. `Parse.Cloud.beforeSave('Review', …)`를 한 번 등록하면(코드 탭에 전체 패턴이 있습니다) REST, GraphQL, 모든 SDK, 대시보드를 통해 들어오는 모든 쓰기에 규칙이 적용됩니다. API 게이트웨이가 데이터로 가는 유일한 경로이기 때문입니다. 훅은 풍부한 컨텍스트(`request.object`, `request.original`, `request.user`, 마스터 키 여부)를 받으므로 관리자에게는 검증을 다르게 적용할 수 있고, `beforeFind`는 ACL 위에 사용자별 쿼리 범위를 얹을 수 있으며, after 훅은 SQL 트리거가 닿지 못하는 반응, 즉 푸시 알림, 발신 웹훅, 작업 큐 등록에 JavaScript 생태계 전체를 활용할 수 있습니다. 코드와 함께 배포되고, git으로 버전 관리되며, 다른 함수처럼 테스트할 수 있습니다. 트리거의 보장은 그대로 누리면서 트리거의 고고학은 겪지 않습니다.
