Echo 프레임워크
Echo는 Go의 대표적인 고성능 웹 프레임워크 중 하나입니다. Gin과 함께 가장 많이 사용되며, 미들웨어 커스터마이징과 컨텍스트 확장에 강점이 있습니다. 타입 안전한 데이터 바인딩과 풍부한 내장 미들웨어가 특징입니다.
설치 및 기본 사용
go get github.com/labstack/echo/v4
go get github.com/labstack/echo/v4/middleware
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// 내장 미들웨어 등록
e.Use(middleware.Logger()) // 요청 로깅
e.Use(middleware.Recover()) // panic 복구
e.GET("/ping", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "pong"})
})
e.Logger.Fatal(e.Start(":8080"))
}
라우팅
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
// HTTP 메서드별 라우트
e.GET("/users", listUsers)
e.POST("/users", createUser)
e.GET("/users/:id", getUser) // 경로 파라미터
e.PUT("/users/:id", updateUser)
e.DELETE("/users/:id", deleteUser)
// 경로 파라미터 접근
e.GET("/posts/:id/comments/:cid", func(c echo.Context) error {
postID := c.Param("id") // 경로 파라미터
commentID := c.Param("cid")
return c.String(http.StatusOK, "post="+postID+", comment="+commentID)
})
// 쿼리 파라미터
e.GET("/search", func(c echo.Context) error {
q := c.QueryParam("q") // 단일 쿼리 파라미터
page := c.QueryParam("page")
if page == "" { page = "1" }
return c.JSON(http.StatusOK, echo.Map{"q": q, "page": page})
})
// 와일드카드
e.GET("/static/*", func(c echo.Context) error {
return c.String(http.StatusOK, c.Param("*"))
})
e.Logger.Fatal(e.Start(":8080"))
}
func listUsers(c echo.Context) error {
return c.JSON(http.StatusOK, echo.Map{"users": []any{}})
}
func createUser(c echo.Context) error { return c.JSON(http.StatusCreated, echo.Map{"id": 1}) }
func getUser(c echo.Context) error { return c.JSON(http.StatusOK, echo.Map{"id": c.Param("id")}) }
func updateUser(c echo.Context) error { return c.JSON(http.StatusOK, echo.Map{"updated": true}) }
func deleteUser(c echo.Context) error { return c.NoContent(http.StatusNoContent) }
데이터 바인딩 및 유효성 검사
package main
import (
"net/http"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)
// CustomValidator validator 라이브러리 연동
type CustomValidator struct {
validator *validator.Validate
}
func (cv *CustomValidator) Validate(i any) error {
return cv.validator.Struct(i)
}
type CreatePostRequest struct {
Title string `json:"title" validate:"required,min=1,max=200"`
Content string `json:"content" validate:"required"`
Status string `json:"status" validate:"omitempty,oneof=draft published"`
}
type QueryParams struct {
Page int `query:"page" validate:"omitempty,min=1"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100"`
Sort string `query:"sort" validate:"omitempty,oneof=created_at title"`
}
func main() {
e := echo.New()
// validator 등록
e.Validator = &CustomValidator{validator: validator.New()}
// JSON 바인딩 + 유효성 검사
e.POST("/posts", func(c echo.Context) error {
var req CreatePostRequest
// Bind: JSON/Form/XML 자동 감지
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, echo.Map{
"error": echo.Map{"code": "BAD_REQUEST", "message": err.Error()},
})
}
// 유효성 검사
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusUnprocessableEntity, echo.Map{
"error": echo.Map{"code": "VALIDATION_ERROR", "message": err.Error()},
})
}
return c.JSON(http.StatusCreated, echo.Map{"data": req})
})
// 쿼리 파라미터 바인딩
e.GET("/posts", func(c echo.Context) error {
var q QueryParams
if err := c.Bind(&q); err != nil {
return c.JSON(http.StatusBadRequest, echo.Map{"error": err.Error()})
}
if q.Page == 0 { q.Page = 1 }
if q.Limit == 0 { q.Limit = 20 }
return c.JSON(http.StatusOK, echo.Map{"page": q.Page, "limit": q.Limit})
})
e.Logger.Fatal(e.Start(":8080"))
}
내장 미들웨어
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// 요청/응답 로깅
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: `{"time":"${time_rfc3339}","method":"${method}","uri":"${uri}","status":${status},"latency":"${latency_human}"}` + "\n",
}))
// panic 복구
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
StackSize: 1 << 10, // 1KB 스택 트레이스
}))
// CORS
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://example.com", "http://localhost:3000"},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
AllowHeaders: []string{"Content-Type", "Authorization"},
}))
// 요청 ID
e.Use(middleware.RequestID())
// Gzip 압축
e.Use(middleware.Gzip())
// Body 크기 제한 (2MB)
e.Use(middleware.BodyLimit("2M"))
// Rate Limiting (초당 20 요청)
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))
// 정적 파일 서빙
e.Static("/static", "public")
e.GET("/", func(c echo.Context) error {
// 요청 ID 접근
id := c.Response().Header().Get(echo.HeaderXRequestID)
return c.JSON(200, echo.Map{"request_id": id})
})
e.Logger.Fatal(e.Start(":8080"))
}
커스텀 컨텍스트 확장
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
// AppContext echo.Context를 확장한 커스텀 컨텍스트
type AppContext struct {
echo.Context
UserID int64
Role string
}
// 커스텀 컨텍스트 미들웨어
func customContextMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cc := &AppContext{
Context: c,
UserID: 0,
Role: "guest",
}
return next(cc)
}
}
// 커스텀 컨텍스트를 사용하는 핸들러
func profileHandler(c echo.Context) error {
cc := c.(*AppContext) // 타입 단언
return c.JSON(http.StatusOK, echo.Map{
"user_id": cc.UserID,
"role": cc.Role,
})
}
func main() {
e := echo.New()
e.Use(customContextMiddleware)
e.GET("/profile", profileHandler)
e.Logger.Fatal(e.Start(":8080"))
}
그룹 및 미들웨어 체인
package main
import (
"net/http"
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger(), middleware.Recover())
// 공개 라우트 그룹
public := e.Group("/api")
public.POST("/login", loginHandler)
public.POST("/register", registerHandler)
// 인증 필요 그룹
private := e.Group("/api/v1")
private.Use(jwtMiddleware)
private.GET("/me", meHandler)
private.GET("/users", listUsersHandler)
// 관리자 전용 그룹 (두 개의 미들웨어 체인)
admin := e.Group("/api/admin")
admin.Use(jwtMiddleware, adminOnlyMiddleware)
admin.GET("/stats", statsHandler)
e.Logger.Fatal(e.Start(":8080"))
}
func jwtMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
auth := c.Request().Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
return c.JSON(http.StatusUnauthorized, echo.Map{
"error": echo.Map{"code": "UNAUTHORIZED", "message": "Bearer 토큰이 필요합니다"},
})
}
// 실제로는 JWT 파싱 후 사용자 정보를 c.Set으로 저장
c.Set("user_id", int64(1))
return next(c)
}
}
func adminOnlyMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// 실제로는 사용자 역할 확인
return next(c)
}
}
func loginHandler(c echo.Context) error { return c.JSON(http.StatusOK, echo.Map{"token": "..."}) }
func registerHandler(c echo.Context) error { return c.JSON(http.StatusCreated, echo.Map{"id": 1}) }
func meHandler(c echo.Context) error { return c.JSON(http.StatusOK, echo.Map{"id": c.Get("user_id")}) }
func listUsersHandler(c echo.Context) error { return c.JSON(http.StatusOK, echo.Map{"users": []any{}}) }
func statsHandler(c echo.Context) error { return c.JSON(http.StatusOK, echo.Map{"users": 100}) }
실전 예제: 파일 업로드 서버
package main
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
const uploadDir = "./uploads"
const maxFileSize = 10 << 20 // 10MB
func init() {
os.MkdirAll(uploadDir, 0755)
}
// uploadHandler 단일 파일 업로드
func uploadHandler(c echo.Context) error {
// 업로드된 파일 가져오기
file, err := c.FormFile("file")
if err != nil {
return c.JSON(http.StatusBadRequest, echo.Map{
"error": echo.Map{"code": "BAD_REQUEST", "message": "file 필드가 필요합니다"},
})
}
// 파일 크기 제한
if file.Size > maxFileSize {
return c.JSON(http.StatusRequestEntityTooLarge, echo.Map{
"error": echo.Map{"code": "FILE_TOO_LARGE", "message": "파일 크기는 10MB 이하여야 합니다"},
})
}
// 확장자 검증
ext := strings.ToLower(filepath.Ext(file.Filename))
allowedExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".pdf": true}
if !allowedExts[ext] {
return c.JSON(http.StatusBadRequest, echo.Map{
"error": echo.Map{"code": "INVALID_FILE_TYPE", "message": "허용된 파일 형식: jpg, png, gif, pdf"},
})
}
// 파일 열기
src, err := file.Open()
if err != nil {
return c.JSON(http.StatusInternalServerError, echo.Map{"error": echo.Map{"code": "SERVER_ERROR"}})
}
defer src.Close()
// 고유한 파일명 생성 (경로 순회 공격 방지)
safeFilename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
dstPath := filepath.Join(uploadDir, safeFilename)
// 파일 저장
dst, err := os.Create(dstPath)
if err != nil {
return c.JSON(http.StatusInternalServerError, echo.Map{"error": echo.Map{"code": "SERVER_ERROR"}})
}
defer dst.Close()
if _, err = io.Copy(dst, src); err != nil {
os.Remove(dstPath) // 실패 시 삭제
return c.JSON(http.StatusInternalServerError, echo.Map{"error": echo.Map{"code": "SAVE_FAILED"}})
}
return c.JSON(http.StatusOK, echo.Map{
"data": echo.Map{
"filename": safeFilename,
"original_name": file.Filename,
"size": file.Size,
"url": "/uploads/" + safeFilename,
},
})
}
// multiUploadHandler 다중 파일 업로드
func multiUploadHandler(c echo.Context) error {
form, err := c.MultipartForm()
if err != nil {
return c.JSON(http.StatusBadRequest, echo.Map{"error": echo.Map{"code": "BAD_REQUEST"}})
}
files := form.File["files"]
results := make([]echo.Map, 0, len(files))
for _, file := range files {
src, err := file.Open()
if err != nil {
continue
}
defer src.Close()
ext := filepath.Ext(file.Filename)
name := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
dst, _ := os.Create(filepath.Join(uploadDir, name))
io.Copy(dst, src)
dst.Close()
results = append(results, echo.Map{
"filename": name,
"original": file.Filename,
"size": file.Size,
})
}
return c.JSON(http.StatusOK, echo.Map{"data": results, "count": len(results)})
}
func main() {
e := echo.New()
e.Use(middleware.Logger(), middleware.Recover())
// 파일 업로드 엔드포인트
e.POST("/upload", uploadHandler)
e.POST("/upload/multiple", multiUploadHandler)
// 업로드된 파일 서빙
e.Static("/uploads", uploadDir)
e.Logger.Fatal(e.Start(":8080"))
}
Gin vs Echo 비교
| 항목 | Gin | Echo |
|---|---|---|
| 성능 | 매우 빠름 | 빠름 |
| API 스타일 | c *gin.Context | echo.Context 인터페이스 |
| 컨텍스트 확장 | c.Set/c.Get (map 기반) | 인터페이스 임베딩으로 타입 안전 확장 |
| 내장 미들웨어 | 적음 (생태계 풍부) | 많음 (JWT, CORS, 압축 등) |
| 바인딩 | ShouldBind + validator | Bind + 커스텀 Validator |
| 에러 처리 | 직접 처리 | e.HTTPErrorHandler로 중앙화 |
| 인기도 | 더 높음 | 높음 |
고수 팁
1. 중앙 집중식 에러 처리: Echo의 e.HTTPErrorHandler를 오버라이드하면 모든 에러를 한 곳에서 처리할 수 있습니다
e.HTTPErrorHandler = func(err error, c echo.Context) {
code := http.StatusInternalServerError
message := "서버 내부 오류"
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = fmt.Sprintf("%v", he.Message)
}
c.JSON(code, echo.Map{"error": echo.Map{"code": http.StatusText(code), "message": message}})
}
2. echo.Map vs 구조체: 빠른 프로토타이핑에는 echo.Map이 편하지만, API 응답이 복잡해지면 구조체를 정의하는 것이 유지보수에 좋습니다
3. 미들웨어 순서: Echo에서 e.Use()로 등록된 미들웨어는 등록 순서대로 실행됩니다. Logger는 Recover 이후에 배치해 panic도 로깅하도록 하세요