Go 언어 유효성 검사 완벽 가이드 validator와 ozzo-validation 비교 분석

Go 언어 유효성 검사 완벽 가이드 validator와 ozzo-validation 비교 분석

 

소프트웨어 개발에서 데이터 유효성 검사(validation)는 단순히 선택이 아닌 필수적인 과정입니다.

처리에 앞서 입력 데이터가 요구되는 표준을 충족하는지 확인하는 것은 애플리케이션의 안정성과 데이터 무결성을 보장하는 핵심적인 역할을 합니다.

특히 사용자의 입력을 직접 다루는 웹 애플리케이션에서는 '절대 사용자를 믿지 말라'는 원칙에 따라 견고한 방어막을 구축해야 합니다.

Go 언어 생태계에는 이러한 유효성 검사 프로세스를 용이하게 하는 여러 라이브러리가 존재하는데요, 그중에서도 'validator'와 'ozzo-validation'이 가장 널리 사용됩니다.

이 글에서는 두 라이브러리의 특징과 사용법, 그리고 차이점을 깊이 있게 파고들어 어떤 상황에 어떤 라이브러리를 선택하는 것이 좋을지 명확한 가이드라인을 제시하겠습니다.

Struct 태그 기반의 간결함 validator


'validator' 라이브러리는 Go Playground 커뮤니티에서 개발했으며, 구조체(struct) 필드 유효성 검사를 위한 사실상의 표준으로 자리 잡았습니다.


가장 큰 특징은 구조체 태그(struct tag)를 사용하여 검사 규칙을 정의함으로써 코드를 매우 간결하고 가독성 높게 만든다는 점입니다.

설치


먼저 프로젝트에 'validator'를 추가하기 위해 다음 명령어를 사용합니다.

go get github.com/go-playground/validator/v10

기본 사용법


유효성 검사 태그와 함께 구조체를 정의하는 방법은 다음과 같습니다.

package main

import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

type User struct {
    Name  string `validate:"required,min=3,max=32"`
    Email string `validate:"required,email"`
    Age   int    `validate:"gte=18,lte=100"`
}

func main() {
    validate := validator.New()

    user := &User{
        Name:  "Alice",
        Email: "alice@example.com",
        Age:   30,
    }

    err := validate.Struct(user)
    if err != nil {
        fmt.Println("유효성 검사 오류:", err)
    } else {
        fmt.Println("유효성 검사 통과!")
    }
}

 

이 예제에서 User 구조체의 각 필드는 다음과 같은 검사 규칙을 가집니다.

Name 필드는 반드시 존재해야 하며('required'), 길이는 3에서 32자 사이여야 합니다.

Email 필드는 반드시 존재해야 하며('required'), 유효한 이메일 형식이어야 합니다.

Age 필드는 18 이상('gte=18') 100 이하('lte=100')의 값이어야 합니다.

실용적인 오류 처리


실제 애플리케이션에서는 단순히 오류를 출력하는 것만으로는 부족합니다.


어떤 필드에서 어떤 규칙을 위반했는지 파악하여 사용자에게 친절한 피드백을 제공해야 합니다.


`validator`가 반환하는 오류는 `validator.ValidationErrors` 타입으로 캐스팅하여 상세한 오류 정보를 얻을 수 있습니다.

func main() {
    validate := validator.New()

    // 유효하지 않은 데이터
    user := &User{
        Name:  "Al",
        Email: "invalid-email",
        Age:   15,
    }

    err := validate.Struct(user)
    if err != nil {
        // 오류를 ValidationErrors 타입으로 변환합니다.
        validationErrors, ok := err.(validator.ValidationErrors)
        if !ok {
            fmt.Println("오류 타입 변환 실패:", err)
            return
        }

        // 각 필드의 오류를 순회하며 출력합니다.
        for _, fieldError := range validationErrors {
            fmt.Printf("필드: %s, 규칙: %s, 값: '%v'\n", 
                fieldError.Field(), 
                fieldError.Tag(), 
                fieldError.Value())
        }
    }
}
// 출력 결과:
// 필드: Name, 규칙: min, 값: 'Al'
// 필드: Email, 규칙: email, 값: 'invalid-email'
// 필드: Age, 규칙: gte, 값: '15'

 

이렇게 필드별 오류를 추출하면, API 응답 시 필드명을 키로, 오류 메시지를 값으로 하는 JSON 객체를 만들어 프론트엔드에 전달할 수 있습니다.

커스텀 유효성 검사 함수


내장된 태그 외에 직접 유효성 검사 함수를 만들어 등록할 수도 있습니다.


예를 들어, 문자열이 특정 접두사로 시작하는지 확인하는 규칙을 추가해 보겠습니다.

import "strings"

// startsWithGo 함수는 FieldLevel 인터페이스를 인자로 받습니다.
// FieldLevel은 현재 검사 중인 필드에 대한 다양한 정보를 제공합니다.
func startsWithGo(fl validator.FieldLevel) bool {
    return strings.HasPrefix(fl.Field().String(), "Go")
}

func main() {
    validate := validator.New()
    // 'startsWithGo'라는 이름으로 커스텀 함수를 등록합니다.
    validate.RegisterValidation("startsWithGo", startsWithGo)

    type Language struct {
        Name string `validate:"startsWithGo"`
    }

    lang := &Language{Name: "Golang"}
    err := validate.Struct(lang)
    if err != nil {
        fmt.Println("유효성 검사 오류:", err)
    } else {
        fmt.Println("유효성 검사 통과!")
    }
}

코드 기반의 유연함 ozzo-validation


Go 생태계의 또 다른 강력한 유효성 검사 라이브러리는 'ozzo-validation'입니다.


구조체 태그에 의존하는 'validator'와 달리, 'ozzo-validation'은 코드를 사용하여 검사 규칙을 정의하므로 더 큰 유연성과 제어권을 제공합니다.

설치


프로젝트에 'ozzo-validation'을 추가하는 명령어는 다음과 같습니다.

go get github.com/go-ozzo/ozzo-validation/v4

기본 사용법


'ozzo-validation'을 사용하여 구조체를 정의하고 검사하는 방법은 다음과 같습니다.

package main

import (
    "fmt"
    "github.com/go-ozzo/ozzo-validation/v4"
    "github.com/go-ozzo/ozzo-validation/v4/is"
)

type User struct {
    Name  string
    Email string
    Age   int
}

// 구조체에 Validate 메서드를 구현하는 것이 일반적인 패턴입니다.
func (u User) Validate() error {
    return validation.ValidateStruct(&u,
        // 각 필드에 대한 규칙을 체이닝 방식으로 정의합니다.
        validation.Field(&u.Name, validation.Required, validation.Length(3, 32)),
        validation.Field(&u.Email, validation.Required, is.Email),
        validation.Field(&u.Age, validation.Min(18), validation.Max(100)),
    )
}

func main() {
    user := User{
        Name:  "Alice",
        Email: "alice@example.com",
        Age:   30,
    }

    err := user.Validate()
    if err != nil {
        fmt.Println("유효성 검사 오류:", err)
    } else {
        fmt.Println("유효성 검사 통과!")
    }
}

 

이 접근 방식에서는 User 구조체에 Validate 메서드를 구현하고, validation 패키지의 함수들을 사용하여 규칙을 명시적으로 선언합니다.

조건부 유효성 검사


'ozzo-validation'의 가장 큰 장점 중 하나는 다른 필드의 값에 따라 검사 규칙을 동적으로 적용하는 '조건부 유효성 검사'입니다.


예를 들어, '국가' 필드가 'USA'일 때만 '주(State)' 필드를 필수로 만들어 보겠습니다.

type Address struct {
    Street  string
    City    string
    State   string
    Country string
}

func (a Address) Validate() error {
    return validation.ValidateStruct(&a,
        validation.Field(&a.Street, validation.Required),
        validation.Field(&a.City, validation.Required),
        validation.Field(&a.Country, validation.Required, is.CountryCode),
        // Country 필드가 "US"일 때만 State 필드에 Required 규칙을 적용합니다.
        validation.Field(&a.State, validation.When(a.Country == "US", validation.Required)),
    )
}

 

이러한 복잡한 로직은 구조체 태그만으로는 구현하기 매우 까다롭습니다.

두 라이브러리, 어떤 것을 선택할까


두 라이브러리 모두 Go에서 데이터 유효성 검사를 위한 강력한 도구이지만, 접근 방식에 뚜렷한 차이가 있습니다.

 

'validator'는 구조체 태그를 사용하여 코드가 간결해지고, 모델 정의와 검사 규칙을 한곳에서 볼 수 있다는 장점이 있습니다.

특히 Gin과 같은 웹 프레임워크와의 통합이 매끄러워 간단한 유효성 검사 시나리오에서 매우 효율적입니다.

하지만 복잡한 조건부 로직이나 동적 규칙이 필요해지면 구조체 태그만으로는 한계에 부딪히고 코드가 복잡해질 수 있습니다.

 

'ozzo-validation'은 코드로 규칙을 정의하므로 훨씬 더 높은 유연성과 명확성을 제공합니다.

복잡한 유효성 검사 시나리오에서 특히 강점을 보이며, 컴파일 타임에 규칙을 확인할 수 있어 잘못된 태그로 인한 런타임 오류 가능성을 줄여줍니다.

하지만 모든 규칙을 코드로 작성해야 하므로 간단한 경우에는 'validator'보다 코드가 길어질 수 있습니다.

 

결론적으로 프로젝트의 필요에 따라 선택이 달라집니다.

 

단순한 CRUD API와 같이 직관적인 유효성 검사가 대부분이라면, 'validator'가 빠르고 간결한 솔루션을 제공합니다.

비즈니스 로직이 복잡하여 필드 간 의존성이 높고 조건부 규칙이 많이 필요하다면, 'ozzo-validation'이 더 적합하고 유지보수하기 좋은 코드를 만들어 줄 것입니다.

결론


데이터 유효성 검사는 소프트웨어 애플리케이션의 무결성과 신뢰성을 보장하는 데 필수적입니다.


Go 생태계는 'validator'와 'ozzo-validation'처럼 강력한 라이브러리를 제공하여 이 과정을 용이하게 합니다.


두 라이브러리의 특징과 차이점을 이해하면, 개발자는 특정 사용 사례에 가장 적합한 도구를 선택하여 더 유지보수하기 쉽고 오류 없는 코드 베이스를 구축할 수 있습니다.