1. Hooks Mechanism

Hooks 메커니즘은 데이터베이스 작업에서 특정 변경이 발생하기 전이나 후에 사용자 정의 로직을 추가하는 방법입니다. 데이터베이스 스키마를 수정할 때 새로운 노드를 추가하거나 노드간의 연결을 삭제하거나 다중 노드를 삭제하는 등의 작업을 할 때, Hooks를 사용하여 데이터 유효성 검사, 로그 기록, 권한 확인 또는 기타 사용자 정의 작업을 수행할 수 있습니다. 이는 데이터 일관성과 비즈니스 규칙 준수를 보장하는 중요한 역할을 합니다. 따라서 기존 비즈니스 로직을 변경하지 않고도 개발자가 추가 기능을 추가할 수 있게 합니다.

2. Hook 등록 방법

2.1 전역 Hooks 및 지역 Hooks

전역 hooks (런타임 hooks)은 그래프의 모든 유형의 작업에 대해 유효합니다. 로깅 및 모니터링과 같은 애플리케이션 전체에 로직 추가에 적합합니다. 지역 hooks (스키마 hooks)는 특정 유형 스키마 내에서 정의되며 해당 스키마 유형과 일치하는 변이 작업에만 적용됩니다. 지역 hooks를 사용하면 특정 노드 유형과 관련된 모든 로직을 스키마 정의 내의 한 곳, 즉 스키마에 집중시킬 수 있습니다.

2.2 Hooks 등록 단계

코드에 hook를 등록하는 것은 일반적으로 다음 단계를 포함합니다:

  1. Hook 함수 정의. 이 함수는 ent.Mutator를 인수로 사용하고 ent.Mutator를 반환합니다. 예를 들어, 간단한 로깅 hook을 생성하는 경우:
logHook := func(next ent.Mutator) ent.Mutator {
    return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
        // 변이 작업 전에 로그 출력
        log.Printf("변이 전: 유형=%s, 작업=%s\n", m.Type(), m.Op())
        // 변이 작업 수행
        v, err := next.Mutate(ctx, m)
        // 변이 작업 후에 로그 출력
        log.Printf("변이 후: 유형=%s, 작업=%s\n", m.Type(), m.Op())
        return v, err
    })
}
  1. 클라이언트에서 hook 등록. 전역 hooks의 경우 클라이언트의 Use 메서드를 사용하여 등록할 수 있습니다. 지역 hooks의 경우 유형의 Hooks 메서드를 사용하여 스키마에 등록할 수 있습니다.
// 전역 hook 등록
client.Use(logHook)

// 지역 hook 등록, User 유형에만 적용
client.User.Use(func(next ent.Mutator) ent.Mutator {
    return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
        // 특정 로직 추가
        // ...
        return next.Mutate(ctx, m)
    })
})
  1. 여러 hooks를 연결할 수 있으며, 등록된 순서대로 실행됩니다.

3. Hooks 실행 순서

Hooks의 실행 순서는 클라이언트에 등록된 순서에 따라 결정됩니다. 예를 들어, client.Use(f, g, h)는 변이 작업에서 f(g(h(...)))의 순서로 실행됩니다. 이 예에서 f가 먼저 실행되고, 그 다음에 g, 마지막으로 h가 실행됩니다.

중요한 점은 런타임 hooks (런타임 hooks)가 스키마 hooks (스키마 hooks)보다 우선적으로 실행된다는 것입니다. 즉, gh가 스키마에서 정의된 hooks이고 fclient.Use(...)를 사용하여 등록된 경우 실행 순서는 f(g(h(...)))가 됩니다. 이로써 로깅과 같은 전역 논리가 다른 hooks보다 먼저 실행되는 것이 보장됩니다.

4. Hooks로 인한 문제 해결

Hooks를 사용하여 데이터베이스 작업을 사용자 정의하는 경우, import 순환 문제가 발생할 수 있습니다. 이는 일반적으로 스키마 hooks를 사용하려고 할 때 발생하는데, ent/schema 패키지가 ent 코어 패키지를 도입할 수 있기 때문입니다. 만일 ent 코어 패키지가 또한 ent/schema를 가져오려고 하면 순환 종속성이 형성됩니다.

순환 종속성의 원인

순환 종속성은 주로 스키마 정의와 생성된 엔터티 코드 간의 양방향 의존성에서 발생합니다. 이는 ent/schemaent에 의존하기 때문에 발생하고, 엔터티 코드는 또한 그 안에 정의된 스키마 정보에 액세스해야 하기 때문에 ent 코어 패키지에 의존하기 때문입니다.

순환 참조 해결

만약 순환 참조 오류가 발생한다면, 다음 단계를 따를 수 있습니다:

  1. 먼저, ent/schema에서 사용된 모든 후크를 주석 처리합니다.
  2. 그다음, ent/schema에서 정의된 사용자 정의 타입을 새 패키지로 이동시킵니다. 예를 들어, ent/schema/schematype이라는 패키지를 만들 수 있습니다.
  3. go generate ./... 명령을 실행하여 ent 패키지를 업데이트합니다. 이로써 새 패키지 경로를 가리키며, 스키마의 타입 참조를 업데이트합니다. 예를 들어, schema.Tschematype.T로 변경합니다.
  4. 이전에 주석 처리된 후크 참조를 해제하고 다시 go generate ./... 명령을 실행합니다. 이 단계에서는 코드 생성이 오류 없이 진행되어야 합니다.

이러한 단계를 따라 순환 참조 문제를 해결하여, 스키마의 로직과 후크의 구현이 원활하게 진행될 수 있습니다.

5. 후크 도우미 함수 사용

ent 프레임워크는 후크 실행 타이밍을 제어하는 데 도움이 되는 일련의 후크 도우미 함수를 제공합니다. 자주 사용되는 후크 도우미 함수의 예시는 다음과 같습니다:

// UpdateOne 및 DeleteOne 작업에 대해 HookA만 실행
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)

// Create 작업 중에는 HookB를 실행하지 않음
hook.Unless(HookB(), ent.OpCreate)

// "status" 필드를 수정하고 "dirty" 필드를 지울 때에만 HookC 실행
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty")))

// Update (다중) 작업에서 "password" 필드를 수정하지 못하게 함
hook.If(
    hook.FixedError(errors.New("password cannot be edited on update many")),
    hook.And(
        hook.HasOp(ent.OpUpdate),
        hook.Or(
            hook.HasFields("password"),
            hook.HasClearedFields("password"),
        ),
    ),
)

이러한 도우미 함수들을 사용하면 다양한 작업에 대한 후크 활성화 조건을 정확하게 제어할 수 있습니다.

6. 트랜잭션 후크

트랜잭션 후크를 사용하면 트랜잭션이 커밋(Tx.Commit)되거나 롤백(Tx.Rollback)될 때 특정 후크를 실행할 수 있습니다. 이는 데이터 일관성과 작업의 원자성을 보장하는 데 매우 유용합니다.

트랜잭션 후크 예시

client.Tx(ctx, func(tx *ent.Tx) error {
    // 트랜잭션 후크 등록 - commit 전에 hookBeforeCommit이 실행됩니다.
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // 실제 커밋 전 로직을 여기에 배치할 수 있습니다.
            fmt.Println("커밋 전")
            return next.Commit(ctx, tx)
        })
    })

    // 트랜잭션 내에서 일련의 작업 수행...

    return nil
})

위 코드는 트랜잭션 내에서 커밋 전에 실행되는 트랜잭션 후크를 등록하는 방법을 보여줍니다. 이 후크는 모든 데이터베이스 작업이 실행된 후 실제로 트랜잭션이 커밋되기 전에 호출됩니다.