refactor: main.go 서버 초기화 로직을 internal/server/server.go로 분리
Fiber 앱 설정, 미들웨어, rate limiter를 server 패키지로 추출. main.go는 DB 연결, DI, 서버 시작, graceful shutdown만 담당. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
110
internal/server/server.go
Normal file
110
internal/server/server.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"a301_server/pkg/apperror"
|
||||
"a301_server/pkg/metrics"
|
||||
"a301_server/pkg/middleware"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// New creates a configured Fiber app with all global middleware applied.
|
||||
func New() *fiber.App {
|
||||
app := fiber.New(fiber.Config{
|
||||
StreamRequestBody: true,
|
||||
BodyLimit: 4 * 1024 * 1024 * 1024, // 4GB
|
||||
ErrorHandler: middleware.ErrorHandler,
|
||||
})
|
||||
app.Use(middleware.RequestID)
|
||||
app.Use(middleware.Metrics)
|
||||
app.Get("/metrics", metrics.Handler)
|
||||
app.Use(logger.New(logger.Config{
|
||||
Format: `{"time":"${time}","status":${status},"latency":"${latency}","method":"${method}","path":"${path}","ip":"${ip}","reqId":"${locals:requestID}"}` + "\n",
|
||||
TimeFormat: "2006-01-02T15:04:05Z07:00",
|
||||
}))
|
||||
app.Use(middleware.SecurityHeaders)
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: "https://a301.tolelom.xyz",
|
||||
AllowHeaders: "Origin, Content-Type, Authorization, Idempotency-Key, X-API-Key",
|
||||
AllowMethods: "GET, POST, PUT, PATCH, DELETE",
|
||||
AllowCredentials: true,
|
||||
}))
|
||||
return app
|
||||
}
|
||||
|
||||
// AuthLimiter returns a rate limiter for auth endpoints (10 req/min per IP).
|
||||
func AuthLimiter() fiber.Handler {
|
||||
return limiter.New(limiter.Config{
|
||||
Max: 10,
|
||||
Expiration: 1 * time.Minute,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
return c.IP()
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return apperror.ErrRateLimited
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// APILimiter returns a rate limiter for general API endpoints (60 req/min per IP).
|
||||
func APILimiter() fiber.Handler {
|
||||
return limiter.New(limiter.Config{
|
||||
Max: 60,
|
||||
Expiration: 1 * time.Minute,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
return c.IP()
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return apperror.ErrRateLimited
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ChainUserLimiter returns a rate limiter for chain transactions (20 req/min per user).
|
||||
func ChainUserLimiter() fiber.Handler {
|
||||
return limiter.New(limiter.Config{
|
||||
Max: 20,
|
||||
Expiration: 1 * time.Minute,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
if uid, ok := c.Locals("userID").(uint); ok {
|
||||
return "chain_user:" + strconv.FormatUint(uint64(uid), 10)
|
||||
}
|
||||
return "chain_ip:" + c.IP()
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return apperror.ErrRateLimited
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// HealthCheck returns a handler that reports server liveness.
|
||||
func HealthCheck() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
// ReadyCheck returns a handler that verifies DB and Redis connectivity.
|
||||
func ReadyCheck(db *gorm.DB, rdb *redis.Client) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "db pool"})
|
||||
}
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "db"})
|
||||
}
|
||||
if err := rdb.Ping(c.Context()).Err(); err != nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "redis"})
|
||||
}
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
}
|
||||
125
main.go
125
main.go
@@ -4,7 +4,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -14,21 +13,15 @@ import (
|
||||
"a301_server/internal/chain"
|
||||
"a301_server/internal/download"
|
||||
"a301_server/internal/player"
|
||||
"a301_server/internal/server"
|
||||
|
||||
_ "a301_server/docs" // swagger docs
|
||||
|
||||
"github.com/tolelom/tolchain/core"
|
||||
"a301_server/pkg/apperror"
|
||||
"a301_server/pkg/config"
|
||||
"a301_server/pkg/database"
|
||||
"a301_server/pkg/metrics"
|
||||
"a301_server/pkg/middleware"
|
||||
"a301_server/routes"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
)
|
||||
|
||||
// @title One of the Plans API
|
||||
@@ -68,12 +61,12 @@ func main() {
|
||||
}
|
||||
log.Println("Redis 연결 성공")
|
||||
|
||||
// 의존성 주입
|
||||
// ── 의존성 주입 ──────────────────────────────────────────────────
|
||||
|
||||
authRepo := auth.NewRepository(db)
|
||||
authSvc := auth.NewService(authRepo, rdb)
|
||||
authHandler := auth.NewHandler(authSvc)
|
||||
|
||||
// Chain (blockchain integration)
|
||||
chainClient := chain.NewClient(config.C.ChainNodeURL)
|
||||
chainRepo := chain.NewRepository(db)
|
||||
chainSvc, err := chain.NewService(chainRepo, chainClient, config.C.ChainID, config.C.OperatorKeyHex, config.C.WalletEncryptionKey)
|
||||
@@ -82,46 +75,35 @@ func main() {
|
||||
}
|
||||
chainHandler := chain.NewHandler(chainSvc)
|
||||
|
||||
// username → userID 변환 (게임 서버 내부 API용)
|
||||
chainSvc.SetUserResolver(func(username string) (uint, error) {
|
||||
userResolver := func(username string) (uint, error) {
|
||||
user, err := authRepo.FindByUsername(username)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return user.ID, nil
|
||||
})
|
||||
}
|
||||
chainSvc.SetUserResolver(userResolver)
|
||||
|
||||
// 회원가입 시 블록체인 월렛 자동 생성
|
||||
authSvc.SetWalletCreator(func(userID uint) error {
|
||||
_, err := chainSvc.CreateWallet(userID)
|
||||
return err
|
||||
})
|
||||
|
||||
// Player Profile
|
||||
playerRepo := player.NewRepository(db)
|
||||
playerSvc := player.NewService(playerRepo)
|
||||
playerSvc.SetUserResolver(func(username string) (uint, error) {
|
||||
user, err := authRepo.FindByUsername(username)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return user.ID, nil
|
||||
})
|
||||
playerSvc.SetUserResolver(userResolver)
|
||||
playerHandler := player.NewHandler(playerSvc)
|
||||
|
||||
// 회원가입 시 플레이어 프로필 자동 생성
|
||||
authSvc.SetProfileCreator(func(userID uint) error {
|
||||
return playerSvc.CreateProfile(userID)
|
||||
})
|
||||
|
||||
// 초기 admin 계정 생성 (콜백 등록 후 실행)
|
||||
if err := authSvc.EnsureAdmin(config.C.AdminUsername, config.C.AdminPassword); err != nil {
|
||||
log.Printf("admin 계정 생성 실패: %v", err)
|
||||
} else {
|
||||
log.Printf("admin 계정 확인 완료: %s", config.C.AdminUsername)
|
||||
}
|
||||
|
||||
// Boss Raid
|
||||
brRepo := bossraid.NewRepository(db)
|
||||
brSvc := bossraid.NewService(brRepo, rdb)
|
||||
brSvc.SetRewardGranter(func(username string, tokenAmount uint64, assets []core.MintAssetPayload) error {
|
||||
@@ -145,91 +127,20 @@ func main() {
|
||||
dlSvc := download.NewService(dlRepo, config.C.GameDir)
|
||||
dlHandler := download.NewHandler(dlSvc, config.C.BaseURL)
|
||||
|
||||
// 미들웨어 인스턴스 생성 (DI)
|
||||
// ── 서버 + 라우트 설정 ───────────────────────────────────────────
|
||||
|
||||
app := server.New()
|
||||
|
||||
authMw := middleware.Auth(rdb, config.C.JWTSecret)
|
||||
serverAuthMw := middleware.ServerAuth(config.C.InternalAPIKey)
|
||||
idempotencyReqMw := middleware.IdempotencyRequired(rdb)
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
StreamRequestBody: true,
|
||||
BodyLimit: 4 * 1024 * 1024 * 1024, // 4GB
|
||||
ErrorHandler: middleware.ErrorHandler,
|
||||
})
|
||||
app.Use(middleware.RequestID)
|
||||
app.Use(middleware.Metrics)
|
||||
app.Get("/metrics", metrics.Handler)
|
||||
app.Use(logger.New(logger.Config{
|
||||
Format: `{"time":"${time}","status":${status},"latency":"${latency}","method":"${method}","path":"${path}","ip":"${ip}","reqId":"${locals:requestID}"}` + "\n",
|
||||
TimeFormat: "2006-01-02T15:04:05Z07:00",
|
||||
}))
|
||||
app.Use(middleware.SecurityHeaders)
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: "https://a301.tolelom.xyz",
|
||||
AllowHeaders: "Origin, Content-Type, Authorization, Idempotency-Key, X-API-Key",
|
||||
AllowMethods: "GET, POST, PUT, PATCH, DELETE",
|
||||
AllowCredentials: true,
|
||||
}))
|
||||
routes.Register(app, authHandler, annHandler, dlHandler, chainHandler, brHandler, playerHandler,
|
||||
server.AuthLimiter(), server.APILimiter(), server.HealthCheck(), server.ReadyCheck(db, rdb),
|
||||
server.ChainUserLimiter(), authMw, serverAuthMw, idempotencyReqMw)
|
||||
|
||||
// Rate limiting: 인증 관련 엔드포인트 (로그인/회원가입/리프레시)
|
||||
authLimiter := limiter.New(limiter.Config{
|
||||
Max: 10,
|
||||
Expiration: 1 * time.Minute,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
return c.IP()
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return apperror.ErrRateLimited
|
||||
},
|
||||
})
|
||||
// ── 백그라운드 워커 ──────────────────────────────────────────────
|
||||
|
||||
// Rate limiting: 일반 API
|
||||
apiLimiter := limiter.New(limiter.Config{
|
||||
Max: 60,
|
||||
Expiration: 1 * time.Minute,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
return c.IP()
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return apperror.ErrRateLimited
|
||||
},
|
||||
})
|
||||
|
||||
// Health check handlers
|
||||
healthCheck := func(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
readyCheck := func(c *fiber.Ctx) error {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "db pool"})
|
||||
}
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "db"})
|
||||
}
|
||||
if err := rdb.Ping(c.Context()).Err(); err != nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "redis"})
|
||||
}
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
|
||||
// Rate limiting: 체인 트랜잭션 (유저별 분당 20회)
|
||||
chainUserLimiter := limiter.New(limiter.Config{
|
||||
Max: 20,
|
||||
Expiration: 1 * time.Minute,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
if uid, ok := c.Locals("userID").(uint); ok {
|
||||
return "chain_user:" + strconv.FormatUint(uint64(uid), 10)
|
||||
}
|
||||
return "chain_ip:" + c.IP()
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return apperror.ErrRateLimited
|
||||
},
|
||||
})
|
||||
|
||||
routes.Register(app, authHandler, annHandler, dlHandler, chainHandler, brHandler, playerHandler, authLimiter, apiLimiter, healthCheck, readyCheck, chainUserLimiter, authMw, serverAuthMw, idempotencyReqMw)
|
||||
|
||||
// Background: stale dedicated server detection
|
||||
go func() {
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
@@ -238,7 +149,6 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
// Background: retry failed rewards
|
||||
rewardWorker := bossraid.NewRewardWorker(
|
||||
brRepo,
|
||||
func(username string, tokenAmount uint64, assets []core.MintAssetPayload) error {
|
||||
@@ -251,7 +161,8 @@ func main() {
|
||||
)
|
||||
rewardWorker.Start()
|
||||
|
||||
// Graceful shutdown
|
||||
// ── Graceful shutdown ────────────────────────────────────────────
|
||||
|
||||
go func() {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
@@ -261,7 +172,6 @@ func main() {
|
||||
if err := app.ShutdownWithTimeout(10 * time.Second); err != nil {
|
||||
log.Printf("서버 종료 실패: %v", err)
|
||||
}
|
||||
// Redis 연결 정리
|
||||
if rdb != nil {
|
||||
if err := rdb.Close(); err != nil {
|
||||
log.Printf("Redis 종료 실패: %v", err)
|
||||
@@ -269,7 +179,6 @@ func main() {
|
||||
log.Println("Redis 연결 종료 완료")
|
||||
}
|
||||
}
|
||||
// MySQL 연결 정리
|
||||
if sqlDB, err := db.DB(); err == nil {
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
log.Printf("MySQL 종료 실패: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user