Gin
Email checker for Go Gin. Email verification in Gin HTTP handlers.
고성능 이메일 유효성 검사를 위해 Gin 애플리케이션에 EmailVerify를 통합하세요. 이 가이드는 미들웨어 패턴, 동시 검증 및 프로덕션 배포를 다룹니다.
설치
Gin 프레임워크와 EmailVerify 의존성을 설치하세요.
go get github.com/gin-gonic/gin
go get github.com/emailverify/emailverify-go
go get github.com/go-playground/validator/v10
go get github.com/go-redis/redis/v8
go get github.com/spf13/viper
go get gorm.io/gorm
go get gorm.io/driver/postgresrequire (
github.com/gin-gonic/gin v1.9.0
github.com/emailverify/emailverify-go v0.1.0
github.com/go-playground/validator/v10 v10.15.0
github.com/go-redis/redis/v8 v8.11.5
github.com/spf13/viper v1.16.0
gorm.io/gorm v1.25.0
gorm.io/driver/postgres v1.5.0
)설정
Viper를 사용한 환경 설정
환경 관리를 위해 Viper를 사용하여 애플리케이션을 설정하세요:
package config
import (
"log"
"github.com/spf13/viper"
)
type Config struct {
EmailVerifyAPIKey string
DatabaseURL string
RedisURL string
ServerPort string
Environment string
RateLimit int
RateLimitWindow int
}
func LoadConfig() *Config {
viper.SetDefault("SERVER_PORT", "8000")
viper.SetDefault("ENVIRONMENT", "development")
viper.SetDefault("RATE_LIMIT", 100)
viper.SetDefault("RATE_LIMIT_WINDOW", 3600)
viper.BindEnv("EMAILVERIFY_API_KEY")
viper.BindEnv("DATABASE_URL")
viper.BindEnv("REDIS_URL")
viper.BindEnv("SERVER_PORT")
viper.BindEnv("ENVIRONMENT")
cfg := &Config{
EmailVerifyAPIKey: viper.GetString("EMAILVERIFY_API_KEY"),
DatabaseURL: viper.GetString("DATABASE_URL"),
RedisURL: viper.GetString("REDIS_URL"),
ServerPort: viper.GetString("SERVER_PORT"),
Environment: viper.GetString("ENVIRONMENT"),
RateLimit: viper.GetInt("RATE_LIMIT"),
RateLimitWindow: viper.GetInt("RATE_LIMIT_WINDOW"),
}
if cfg.EmailVerifyAPIKey == "" {
log.Fatal("EMAILVERIFY_API_KEY가 필요합니다")
}
return cfg
}.env 파일
로컬 개발을 위한 .env 파일 생성:
EMAILVERIFY_API_KEY=your_api_key_here
DATABASE_URL=postgres://user:password@localhost/emailverify
REDIS_URL=redis://localhost:6379/0
SERVER_PORT=8000
ENVIRONMENT=development
RATE_LIMIT=100
RATE_LIMIT_WINDOW=3600미들웨어
이메일 검증 미들웨어
요청 유효성 검사 및 로깅을 위한 미들웨어 생성:
package middleware
import (
"github.com/gin-gonic/gin"
"log"
"time"
)
func EmailVerificationLogging() gin.HandlerFunc {
return func(c *gin.Context) {
startTime := time.Now()
c.Next()
duration := time.Since(startTime)
statusCode := c.Writer.Status()
log.Printf(
"[%s] %s %s - 상태: %d - 소요시간: %v",
time.Now().Format(time.RFC3339),
c.Request.Method,
c.Request.URL.Path,
statusCode,
duration,
)
}
}
func ErrorHandling() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
log.Printf("패닉 복구: %v", err)
c.JSON(500, gin.H{"error": "내부 서버 오류"})
}
}()
c.Next()
}
}속도 제한 미들웨어
API 남용을 방지하기 위한 속도 제한 구현:
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"context"
"strconv"
"time"
)
type RateLimiter struct {
client *redis.Client
limit int
window int
}
func NewRateLimiter(client *redis.Client, limit int, window int) *RateLimiter {
return &RateLimiter{
client: client,
limit: limit,
window: window,
}
}
func (rl *RateLimiter) Limit() gin.HandlerFunc {
return func(c *gin.Context) {
clientIP := c.ClientIP()
key := "ratelimit:" + clientIP
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
count, err := rl.client.Incr(ctx, key).Result()
if err != nil {
c.JSON(500, gin.H{"error": "속도 제한 확인 실패"})
c.Abort()
return
}
if count == 1 {
rl.client.Expire(ctx, key, time.Duration(rl.window)*time.Second)
}
if count > int64(rl.limit) {
c.JSON(429, gin.H{
"error": "요청이 너무 많습니다",
"retry_after": rl.window,
})
c.Abort()
return
}
c.Header("X-RateLimit-Limit", strconv.Itoa(rl.limit))
c.Header("X-RateLimit-Remaining", strconv.Itoa(rl.limit - int(count)))
c.Next()
}
}핸들러
단일 이메일 검증 핸들러
개별 이메일 검증을 위한 핸들러 생성:
package handlers
import (
"github.com/gin-gonic/gin"
"emailverify-app/services"
"net/http"
)
type VerifyEmailRequest struct {
Email string `json:"email" binding:"required,email"`
}
type VerifyEmailResponse struct {
Email string `json:"email"`
Status string `json:"status"`
Score float64 `json:"score"`
Details interface{} `json:"details"`
Cached bool `json:"cached"`
Timestamp string `json:"timestamp"`
}
func VerifyEmail(verificationService *services.EmailVerificationService) gin.HandlerFunc {
return func(c *gin.Context) {
var req VerifyEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "잘못된 요청",
"details": err.Error(),
})
return
}
result, cached, err := verificationService.VerifyEmail(c.Request.Context(), req.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "검증 실패",
"details": err.Error(),
})
return
}
response := VerifyEmailResponse{
Email: req.Email,
Status: result.Status,
Score: result.Score,
Details: result.Details,
Cached: cached,
Timestamp: result.Timestamp,
}
c.JSON(http.StatusOK, response)
}
}대량 이메일 검증 핸들러
배치 이메일 검증을 위한 핸들러 생성:
package handlers
import (
"github.com/gin-gonic/gin"
"emailverify-app/services"
"net/http"
"time"
)
type BulkVerifyRequest struct {
Emails []string `json:"emails" binding:"required,min=1"`
MaxResults *int `json:"max_results,omitempty"`
}
type BulkVerifyResponse struct {
Total int `json:"total"`
Verified int `json:"verified"`
Failed int `json:"failed"`
Results []VerifyEmailResponse `json:"results"`
DurationMs float64 `json:"duration_ms"`
}
func BulkVerifyEmails(verificationService *services.EmailVerificationService) gin.HandlerFunc {
return func(c *gin.Context) {
var req BulkVerifyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "잘못된 요청",
"details": err.Error(),
})
return
}
startTime := time.Now()
// 지정된 경우 결과 제한
emails := req.Emails
if req.MaxResults != nil && *req.MaxResults > 0 && *req.MaxResults < len(emails) {
emails = emails[:*req.MaxResults]
}
results := make([]VerifyEmailResponse, 0, len(emails))
failed := 0
for _, email := range emails {
result, cached, err := verificationService.VerifyEmail(c.Request.Context(), email)
if err != nil {
failed++
continue
}
results = append(results, VerifyEmailResponse{
Email: email,
Status: result.Status,
Score: result.Score,
Details: result.Details,
Cached: cached,
Timestamp: result.Timestamp,
})
}
duration := time.Since(startTime)
response := BulkVerifyResponse{
Total: len(emails),
Verified: len(results),
Failed: failed,
Results: results,
DurationMs: duration.Seconds() * 1000,
}
c.JSON(http.StatusOK, response)
}
}요청 바인딩 및 유효성 검사
커스텀 검증기
커스텀 규칙으로 유효성 검사 확장:
package validators
import (
"github.com/go-playground/validator/v10"
"strings"
)
type CustomValidator struct {
validator *validator.Validate
}
func NewCustomValidator() *CustomValidator {
v := validator.New()
v.RegisterValidationFunc("not_disposable", func(fl validator.FieldLevel) bool {
email := fl.Field().String()
domain := strings.Split(email, "@")[1]
blockedDomains := map[string]bool{
"tempmail.com": true,
"10minutemail.com": true,
"guerrillamail.com": true,
}
return !blockedDomains[domain]
})
return &CustomValidator{validator: v}
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}유효성 검사 태그가 있는 요청 모델
유효성 검사 태그가 있는 요청 모델 정의:
package models
type EmailVerificationRequest struct {
Email string `json:"email" binding:"required,email,not_disposable"`
}
type BulkVerificationRequest struct {
Emails []string `json:"emails" binding:"required,min=1,max=1000,dive,email"`
Cache bool `json:"cache,omitempty"`
}
type VerificationQuery struct {
CacheTTL int `form:"cache_ttl" binding:"omitempty,min=0,max=86400"`
}데이터베이스 통합
GORM 모델
검증 결과 저장을 위한 데이터베이스 모델 정의:
package models
import (
"gorm.io/gorm"
"time"
)
type VerificationResult struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex;not null;type:varchar(254)"`
Status string `gorm:"not null;type:varchar(50)"`
Score float64
Disposable bool
SmtpValid bool
FormatValid bool
EmailProvider string `gorm:"type:varchar(100)"`
RiskLevel string `gorm:"type:varchar(20)"`
VerifiedAt time.Time `gorm:"autoCreateTime"`
CachedAt time.Time `gorm:"autoUpdateTime"`
}
type Database struct {
db *gorm.DB
}
func NewDatabase(dsn string) (*Database, error) {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
// 모델 자동 마이그레이션
if err := db.AutoMigrate(&VerificationResult{}); err != nil {
return nil, err
}
return &Database{db: db}, nil
}
func (d *Database) SaveVerificationResult(email string, result interface{}) error {
return d.db.Create(result).Error
}
func (d *Database) GetVerificationResult(email string) (*VerificationResult, error) {
var result VerificationResult
err := d.db.Where("email = ?", email).First(&result).Error
return &result, err
}캐싱
Redis 캐싱 서비스
검증 결과를 위한 Redis 캐싱 구현:
package services
import (
"github.com/go-redis/redis/v8"
"context"
"time"
"encoding/json"
)
type CacheService struct {
client *redis.Client
ttl time.Duration
}
func NewCacheService(client *redis.Client, ttl time.Duration) *CacheService {
return &CacheService{
client: client,
ttl: ttl,
}
}
func (cs *CacheService) Get(ctx context.Context, key string, dest interface{}) error {
val, err := cs.client.Get(ctx, key).Result()
if err != nil {
return err
}
return json.Unmarshal([]byte(val), dest)
}
func (cs *CacheService) Set(ctx context.Context, key string, value interface{}) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
return cs.client.Set(ctx, key, data, cs.ttl).Err()
}
func (cs *CacheService) Delete(ctx context.Context, key string) error {
return cs.client.Del(ctx, key).Err()
}
func (cs *CacheService) Clear(ctx context.Context) error {
return cs.client.FlushDB(ctx).Err()
}고루틴과 동시성
동시 검증
고루틴과 워커 풀을 사용한 동시 이메일 검증:
package services
import (
"context"
"sync"
)
type VerificationJob struct {
Email string
Done chan VerificationResult
}
type VerificationWorkerPool struct {
jobs chan VerificationJob
results chan VerificationResult
wg sync.WaitGroup
}
func NewVerificationWorkerPool(workerCount int, service *EmailVerificationService) *VerificationWorkerPool {
pool := &VerificationWorkerPool{
jobs: make(chan VerificationJob, 100),
results: make(chan VerificationResult, 100),
}
for i := 0; i < workerCount; i++ {
pool.wg.Add(1)
go pool.worker(context.Background(), service)
}
return pool
}
func (p *VerificationWorkerPool) worker(ctx context.Context, service *EmailVerificationService) {
defer p.wg.Done()
for job := range p.jobs {
result, _, err := service.VerifyEmail(ctx, job.Email)
if err != nil {
job.Done <- VerificationResult{Error: err.Error()}
} else {
job.Done <- *result
}
}
}
func (p *VerificationWorkerPool) VerifyBatch(ctx context.Context, emails []string) []VerificationResult {
results := make([]VerificationResult, len(emails))
done := make(chan VerificationResult, len(emails))
for _, email := range emails {
job := VerificationJob{
Email: email,
Done: done,
}
p.jobs <- job
}
for i := 0; i < len(emails); i++ {
results[i] = <-done
}
return results
}
func (p *VerificationWorkerPool) Close() {
close(p.jobs)
p.wg.Wait()
}에러 처리
에러 응답 모델
일관된 에러 처리 정의:
package models
import (
"github.com/gin-gonic/gin"
"net/http"
)
type ErrorResponse struct {
Error string `json:"error"`
Details string `json:"details,omitempty"`
StatusCode int `json:"status_code"`
Timestamp string `json:"timestamp"`
}
func RespondError(c *gin.Context, statusCode int, message string, details string) {
c.JSON(statusCode, ErrorResponse{
Error: message,
Details: details,
StatusCode: statusCode,
Timestamp: time.Now().Format(time.RFC3339),
})
}
func RespondSuccess(c *gin.Context, statusCode int, data interface{}) {
c.JSON(statusCode, data)
}테스트
테이블 기반 테스트
테이블 기반 테스트로 핸들러와 서비스 테스트:
package handlers
import (
"testing"
"bytes"
"encoding/json"
"github.com/gin-gonic/gin"
"net/http"
"net/http/httptest"
)
func TestVerifyEmail(t *testing.T) {
tests := []struct {
name string
email string
expectedStatus int
expectedBody map[string]interface{}
}{
{
name: "유효한 이메일",
email: "test@example.com",
expectedStatus: http.StatusOK,
expectedBody: map[string]interface{}{
"status": "valid",
},
},
{
name: "유효하지 않은 이메일",
email: "invalid-email",
expectedStatus: http.StatusBadRequest,
expectedBody: map[string]interface{}{
"error": "잘못된 요청",
},
},
{
name: "빈 이메일",
email: "",
expectedStatus: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
router := gin.Default()
reqBody := VerifyEmailRequest{Email: tt.email}
body, _ := json.Marshal(reqBody)
req := httptest.NewRequest("POST", "/verify", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("예상 상태 %d, 실제 %d", tt.expectedStatus, w.Code)
}
})
}
}
func TestBulkVerifyEmails(t *testing.T) {
tests := []struct {
name string
emails []string
expectedTotal int
expectedMinVerified int
}{
{
name: "여러 유효한 이메일",
emails: []string{"test1@example.com", "test2@example.com"},
expectedTotal: 2,
expectedMinVerified: 1,
},
{
name: "빈 목록",
emails: []string{},
expectedTotal: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 테스트 구현
})
}
}프로덕션 설정
우아한 종료
프로덕션용 우아한 종료 구현:
package main
import (
"context"
"github.com/gin-gonic/gin"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
router := gin.Default()
// 라우트 등록
setupRoutes(router)
server := &http.Server{
Addr: ":8000",
Handler: router,
}
// 고루틴에서 서버 시작
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("서버 오류: %v", err)
}
}()
// 인터럽트 시그널 대기
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("서버 종료 중...")
// 타임아웃이 있는 우아한 종료
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("서버 강제 종료됨: %v", err)
}
log.Println("서버 종료됨")
}로깅
프로덕션용 구조화된 로깅 설정:
package logging
import (
"log/slog"
"os"
)
func SetupLogger(environment string) {
var level slog.Level
if environment == "production" {
level = slog.LevelInfo
} else {
level = slog.LevelDebug
}
logger := slog.New(
slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
}),
)
slog.SetDefault(logger)
}Docker 배포
Dockerfile
프로덕션용 멀티 스테이지 Dockerfile 생성:
# 빌드 스테이지
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# 런타임 스테이지
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:8000/health || exit 1
CMD ["./main"]