Skip to content

인터셉터

인터셉터 생성 및 사용하기.

인터셉터란?

인터셉터는 요청 전/후에 실행되는 로직입니다.

  • 트랜잭션 관리
  • 로깅
  • 인증/인가
  • 요청 검증

라이프사이클

인터셉터는 3단계 라이프사이클을 가집니다.

인터페이스

go
type Interceptor interface {
    PreHandle(ctx ExecutionContext, meta HandlerMeta) error
    PostHandle(ctx ExecutionContext, meta HandlerMeta)
    AfterCompletion(ctx ExecutionContext, meta HandlerMeta, err error)
}
메서드실행 시점반환용도
PreHandle컨트롤러 실행 전error인증, 검증, 트랜잭션 시작
PostHandle컨트롤러 성공 후없음응답 가공
AfterCompletion항상 (성공/실패)없음리소스 정리, 커밋/롤백

전역 인터셉터 vs 라우트 인터셉터

Spine은 두 가지 레벨의 인터셉터를 지원합니다.

구분전역 인터셉터라우트 인터셉터
적용 범위모든 요청특정 라우트만
등록 방법app.Interceptor()route.WithInterceptors()
용도CORS, 로깅, 트랜잭션인증, 권한 검사
실행 순서먼저 실행전역 이후 실행

전역 인터셉터

모든 요청에 적용되는 인터셉터입니다.

로깅 인터셉터 예제

go
// interceptor/logging_interceptor.go
package interceptor

import (
    "log"
    "github.com/NARUBROWN/spine/core"
)

type LoggingInterceptor struct{}

func (i *LoggingInterceptor) PreHandle(ctx core.ExecutionContext, meta core.HandlerMeta) error {
    log.Printf("[REQ] %s %s%s.%s",
        ctx.Method(),
        ctx.Path(),
        meta.ControllerType.Name(),
        meta.Method.Name,
    )
    return nil
}

func (i *LoggingInterceptor) PostHandle(ctx core.ExecutionContext, meta core.HandlerMeta) {
    log.Printf("[RES] %s %s OK",
        ctx.Method(),
        ctx.Path(),
    )
}

func (i *LoggingInterceptor) AfterCompletion(ctx core.ExecutionContext, meta core.HandlerMeta, err error) {
    if err != nil {
        log.Printf("[ERR] %s %s : %v",
            ctx.Method(),
            ctx.Path(),
            err,
        )
    }
}

전역 인터셉터 등록

go
func main() {
    app := spine.New()
    
    // 전역 인터셉터 — 모든 요청에 적용
    app.Interceptor(
        &interceptor.LoggingInterceptor{},
    )
    
    app.Run(":8080")
}

라우트 인터셉터

특정 라우트에만 적용되는 인터셉터입니다.

인증 인터셉터 예제

go
// interceptor/auth_interceptor.go
package interceptor

import (
    "github.com/NARUBROWN/spine/core"
    "github.com/NARUBROWN/spine/pkg/httperr"
)

type AuthInterceptor struct{}

func (i *AuthInterceptor) PreHandle(ctx core.ExecutionContext, meta core.HandlerMeta) error {
    token := ctx.Header("Authorization")
    
    if token == "" {
        return httperr.Unauthorized("인증이 필요합니다.")
    }
    
    user, err := validateToken(token)
    if err != nil {
        return httperr.Unauthorized("유효하지 않은 토큰입니다.")
    }
    
    ctx.Set("currentUser", user)
    return nil
}

func (i *AuthInterceptor) PostHandle(ctx core.ExecutionContext, meta core.HandlerMeta) {}

func (i *AuthInterceptor) AfterCompletion(ctx core.ExecutionContext, meta core.HandlerMeta, err error) {}

func validateToken(token string) (map[string]string, error) {
    // 토큰 검증 로직
    return map[string]string{"id": "1", "name": "Alice"}, nil
}

라우트 인터셉터 등록

route.WithInterceptors()를 사용합니다.

go
import (
    "github.com/NARUBROWN/spine"
    "github.com/NARUBROWN/spine/pkg/route"
)

func main() {
    app := spine.New()
    
    app.Constructor(
        NewUserController,
    )
    
    // 인증 필요 없는 라우트
    app.Route(
        "POST",
        "/login",
        (*UserController).Login,
    )
    
    // 인증 필요한 라우트
    app.Route(
        "GET",
        "/users/:id",
        (*UserController).GetUser,
        route.WithInterceptors(&interceptor.AuthInterceptor{}),
    )
    
    // 인증 필요한 라우트
    app.Route(
        "PUT",
        "/users/:id",
        (*UserController).UpdateUser,
        route.WithInterceptors(&interceptor.AuthInterceptor{}),
    )
    
    app.Run(":8080")
}

전역 + 라우트 인터셉터 조합

실제 애플리케이션에서는 두 가지를 함께 사용합니다.

go
func main() {
    app := spine.New()
    
    app.Constructor(
        NewUserController,
    )
    
    // 전역 인터셉터 — 모든 요청에 적용
    app.Interceptor(
        &interceptor.LoggingInterceptor{},
        cors.New(cors.Config{
            AllowOrigins: []string{"*"},
            AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
        }),
    )
    
    // 공개 라우트 — 전역 인터셉터만 적용
    app.Route("POST", "/login", (*UserController).Login)
    app.Route("POST", "/signup", (*UserController).Signup)
    
    // 인증 필요 라우트 — 전역 + Auth 인터셉터
    app.Route(
        "GET",
        "/users/:id",
        (*UserController).GetUser,
        route.WithInterceptors(&interceptor.AuthInterceptor{}),
    )
    
    app.Route(
        "GET",
        "/me",
        (*UserController).GetMe,
        route.WithInterceptors(&interceptor.AuthInterceptor{}),
    )
    
    app.Run(":8080")
}

실행 순서

전역 인터셉터가 먼저, 라우트 인터셉터가 나중에 실행됩니다.

등록 예시

go
// 전역 인터셉터
app.Interceptor(
    &interceptor.LoggingInterceptor{},   // 전역 1
    &interceptor.CORSInterceptor{},      // 전역 2
)

// 라우트 인터셉터
app.Route(
    "GET",
    "/users/:id",
    (*UserController).GetUser,
    route.WithInterceptors(&interceptor.AuthInterceptor{}),  // 라우트 1
)

실행 흐름

Request (GET /users/1)

   ├─→ Logging.PreHandle     (전역 1)
   ├─→ CORS.PreHandle        (전역 2)
   ├─→ Auth.PreHandle        (라우트 1)

   ├─→ UserController.GetUser

   ├─→ Auth.PostHandle       (라우트 1)
   ├─→ CORS.PostHandle       (전역 2)
   ├─→ Logging.PostHandle    (전역 1)

   ├─→ Auth.AfterCompletion       (라우트 1)
   ├─→ CORS.AfterCompletion       (전역 2)
   └─→ Logging.AfterCompletion    (전역 1)
   
Response
  • PreHandle: 전역 → 라우트 순서
  • PostHandle: 라우트 → 전역 역순
  • AfterCompletion: 라우트 → 전역 역순

에러 처리

PreHandle에서 에러 반환

PreHandle에서 에러를 반환하면 요청이 중단됩니다.

go
func (i *AuthInterceptor) PreHandle(ctx core.ExecutionContext, meta core.HandlerMeta) error {
    token := ctx.Header("Authorization")
    if token == "" {
        return httperr.Unauthorized("인증이 필요합니다.")
    }
    return nil
}
Request (GET /users/1, 토큰 없음)

   ├─→ Logging.PreHandle     ✓
   ├─→ CORS.PreHandle        ✓
   ├─→ Auth.PreHandle        ✗ (에러 반환)

   ├─→ Auth.AfterCompletion
   ├─→ CORS.AfterCompletion
   └─→ Logging.AfterCompletion
   
Response (401 Unauthorized)

ExecutionContext

요청 컨텍스트에서 값을 저장하고 조회합니다.

메서드

메서드설명
Context()context.Context 반환
Method()HTTP 메서드 (GET, POST 등)
Path()요청 경로
Header(name)헤더 값 조회
Set(key, value)값 저장
Get(key)값 조회

인터셉터 간 데이터 전달

go
// AuthInterceptor — 사용자 정보 저장
func (i *AuthInterceptor) PreHandle(ctx core.ExecutionContext, meta core.HandlerMeta) error {
    token := ctx.Header("Authorization")
    user, _ := validateToken(token)
    ctx.Set("currentUser", user)
    return nil
}

// 컨트롤러에서 조회하려면 ExecutionContext를 주입받아야 함
// 또는 다른 인터셉터에서 조회
func (i *AuditInterceptor) PreHandle(ctx core.ExecutionContext, meta core.HandlerMeta) error {
    user, ok := ctx.Get("currentUser")
    if ok {
        log.Printf("User %v accessing %s", user, ctx.Path())
    }
    return nil
}

HandlerMeta

실행될 핸들러의 메타 정보입니다.

필드타입설명
ControllerTypereflect.Type컨트롤러 타입
Methodreflect.Method핸들러 메서드
Interceptors[]Interceptor라우트에 바인딩된 인터셉터

사용 예시

go
func (i *LoggingInterceptor) PreHandle(ctx core.ExecutionContext, meta core.HandlerMeta) error {
    log.Printf("컨트롤러: %s", meta.ControllerType.Name())  // UserController
    log.Printf("메서드: %s", meta.Method.Name)              // GetUser
    return nil
}

의존성 주입이 필요한 인터셉터

생성자가 있는 인터셉터는 Constructor에 먼저 등록합니다.

트랜잭션 인터셉터 예제

go
// interceptor/tx_interceptor.go
package interceptor

import (
    "github.com/NARUBROWN/spine/core"
    "github.com/uptrace/bun"
)

type TxInterceptor struct {
    db *bun.DB
}

func NewTxInterceptor(db *bun.DB) *TxInterceptor {
    return &TxInterceptor{db: db}
}

func (i *TxInterceptor) PreHandle(ctx core.ExecutionContext, meta core.HandlerMeta) error {
    tx, err := i.db.BeginTx(ctx.Context(), nil)
    if err != nil {
        return err
    }
    ctx.Set("tx", tx)
    return nil
}

func (i *TxInterceptor) PostHandle(ctx core.ExecutionContext, meta core.HandlerMeta) {}

func (i *TxInterceptor) AfterCompletion(ctx core.ExecutionContext, meta core.HandlerMeta, err error) {
    v, ok := ctx.Get("tx")
    if !ok {
        return
    }
    
    tx := v.(*bun.Tx)
    if err != nil {
        tx.Rollback()
    } else {
        tx.Commit()
    }
}

등록 (전역)

go
app.Constructor(
    NewDB,
    interceptor.NewTxInterceptor,
)

app.Interceptor(
    (*interceptor.TxInterceptor)(nil),  // 타입으로 참조
)

등록 (라우트)

go
app.Constructor(
    NewDB,
    interceptor.NewTxInterceptor,
)

app.Route(
    "POST",
    "/orders",
    (*OrderController).CreateOrder,
    route.WithInterceptors((*interceptor.TxInterceptor)(nil)),  // 타입으로 참조
)

등록 방법 정리

전역 인터셉터

방식코드사용 시점
인스턴스 직접 전달&interceptor.LoggingInterceptor{}의존성 없음
타입으로 참조(*interceptor.TxInterceptor)(nil)의존성 있음
go
app.Interceptor(
    &interceptor.LoggingInterceptor{},      // 인스턴스
    (*interceptor.TxInterceptor)(nil),      // 타입 참조
)

라우트 인터셉터

방식코드사용 시점
인스턴스 직접 전달&interceptor.AuthInterceptor{}의존성 없음
타입으로 참조(*interceptor.TxInterceptor)(nil)의존성 있음
go
app.Route(
    "GET",
    "/users/:id",
    (*UserController).GetUser,
    route.WithInterceptors(
        &interceptor.AuthInterceptor{},         // 인스턴스
        (*interceptor.TxInterceptor)(nil),      // 타입 참조
    ),
)

핵심 정리

개념설명
전역 인터셉터app.Interceptor() — 모든 요청에 적용
라우트 인터셉터route.WithInterceptors() — 특정 라우트만
실행 순서전역 → 라우트 (Post/After는 역순)
3단계 라이프사이클PreHandle → PostHandle → AfterCompletion
에러 시 중단PreHandle 에러 → 컨트롤러 스킵
컨텍스트 공유ctx.Set() / ctx.Get()으로 데이터 전달

다음 단계