엔지니어링

go-lookslike로 데이터 형태 테스트

Elastic에서 개발한 새로운 오픈 소스 go 테스트/스키마 검증 라이브러리를 소개합니다. 바로 Lookslike입니다. Lookslike를 사용하면 JSON 스키마와 비슷하지만 좀 더 강력하고 좀 더 Go스러운 방식으로 golang 데이터 구조의 형태를 대조할 수 있습니다. Lookslike는 기존의 어떤 go 테스트 라이브러리에서도 지원하지 않았던 여러 작업을 수행합니다.

그럼, 예제를 통해 직접 그 기능을 확인해보겠습니다.

// 이 라이브러리를 사용하면 데이터 구조가 특정 스키마와 비슷하거나 정확하게 일치하는지 확인할 수 있습니다.
// 예를 들어 아래 코드를 사용하여 애완동물이 개인지 고양이인지를 테스트할 수 있습니다.

// rover는 개 이름입니다.
rover := map[string]interface{}{"name": "rover", "barks": "often", "fur_length": "long"}
// pounce는 고양이 이름입니다.
pounce := map[string]interface{}{"name": "pounce", "meows": "often", "fur_length": "short"}

// 여기에서 검사기를 정의합니다.
// 여기서는 다음을 포함하는 항목을 개라고 정의합니다.
// 1. 기본 제공되는 `IIsNonEmptyString` 정의를 사용하는, 비어 있지 않은 문자열인 "name" 키
// 2. 값이 "long" 또는 "short"인 "fur_length" 키
// 3. 값이 "often" 또는 "rarely"인 "bars" 키
// 4. 이를 엄격한 매처로 정의합니다. 즉 나열된 키 이외의
//   키가 있는 경우 오류로 간주해야 합니다.
dogValidator := lookslike.Strict(lookslike.MustCompile(map[string]interface{}{
	"name":       isdef.IsNonEmptyString,
	"fur_length": isdef.IsAny(isdef.IsEqual("long"), isdef.IsEqual("short")),
	"barks": isdef.IsAny(isdef.IsEqual("often"), isdef.IsEqual("rarely")),
}))

result := dogValidator(rover)
fmt.Printf("Checked rover, validation status %t, errors: %v\n", result.Valid, result.Errors())
result = dogValidator(pounce)
fmt.Printf("Checked pounce, validation status %t, errors: %v\n", result.Valid, result.Errors())

위의 코드를 실행하면 아래에 표시된 내용이 출력됩니다.

Checked rover, validation status true, errors: []
Checked pounce, validation status false, errors: [@Path 'barks': expected this key to be present @Path 'meows': unexpected field encountered during strict validation]

여기서 우리는 예상대로 개인 "rover"가 일치하고, 고양이인 "pounce"는 일치하지 않아 2건의 오류가 발생하는 것을 알 수 있습니다. 한 가지 오류는 barks라는 키가 정의되어 있지 않았다는 것이고 다른 한 가지 오류는 예상하지 못한 추가 키인 meows가 있었다는 것입니다.

Lookslike는 보통 테스트 환경에서 사용되므로, 적절하게 형식이 지정된 테스트 출력을 생성하는 testslike.Test 도우미가 있습니다. 위의 예에서 마지막 줄을 아래와 같이 변경하기만 하면 됩니다.

testslike.Test(t, dogValidator, rover)
testslike.Test(t, dogValidator, pounce)

컴포지션

검사기를 결합할 수 있다는 것이 Lookslike의 핵심 개념입니다. 별도의 고양이 검사기와 개 검사기를 원하지만 각 검사기에 namefur_length와 같은 공통 필드를 다시 정의하고 싶지는 않다고 가정해 보겠습니다. 아래 예를 통해 이를 살펴보겠습니다.

pets := []map[string]interface{}{
	{"name": "rover", "barks": "often", "fur_length": "long"},
	{"name": "lucky", "barks": "rarely", "fur_length": "short"},
	{"name": "pounce", "meows": "often", "fur_length": "short"},
	{"name": "peanut", "meows": "rarely", "fur_length": "long"},
}

// 모든 애완동물이 "fur_length" 속성을 가지고 있지만 고양이만 야옹거리고 개는 짖는 것을 확인할 수 있습니다.
// lookslike에서는 lookslike.Compose를 사용하여 이를 간결하게 인코딩할 수 있습니다.
// 또한, "meows" 및 "barks" 둘 다 동일한 열거형 값을 포함하고 있습니다.
// IsAny 컴포지션을 사용하여 IsDef를 구성하는 것으로 시작하겠습니다.
// 그러면 IsDef 인수의 논리적 'or'인 새로운 IsDef가 생성됩니다.

isFrequency := isdef.IsAny(isdef.IsEqual("often"), isdef.IsEqual("rarely"))

petValidator := MustCompile(map[string]interface{}{
	"name":       isdef.IsNonEmptyString,
	"fur_length": isdef.IsAny(isdef.IsEqual("long"), isdef.IsEqual("short")),
})
dogValidator := Compose(
	petValidator,
	MustCompile(map[string]interface{}{"barks": isFrequency}),
)
catValidator := Compose(
	petValidator,
	MustCompile(map[string]interface{}{"meows": isFrequency}),
)

for _, pet := range pets {
	var petType string
	if dogValidator(pet).Valid {
		petType = "dog"
	} else if catValidator(pet).Valid {
		petType = "cat"
	}
	fmt.Printf("%s is a %s\n", pet["name"], petType)
}

// 출력:
// rover는 개입니다
// lucky는 개입니다
// pounce는 고양이입니다
// peanut은 고양이입니다

Lookslike를 만든 이유

Lookslike는 Elastic의 Heartbeat 프로젝트에서 만들어졌습니다. Hearbeat는 Uptime 솔루션 뒤에 있는 에이전트로, 엔드포인트를 핑한 다음 작동 여부를 보고합니다. Heartbeat의 최종 출력은 golang 코드베이스에 map[string]interface{} 유형으로 표시되는 Elasticsearch 문서입니다. 이러한 출력 문서를 테스트하기 위해 이 라이브러리를 만들게 되었으며, 이제는 Beats 코드베이스의 다른 곳에서도 사용됩니다.

우리가 직면했던 문제는 다음과 같습니다.

  • 일부 필드에는 정밀하지 않게 대조해야 하는 데이터가 있었습니다. 실행에 걸린 시간을 측정하는 monitor.duration을 예로 들 수 있습니다. 이는 실행별로 다를 수 있습니다. 데이터를 느슨하게 대조할 수 있는 방법이 필요했습니다.
  • 어떤 테스트에서나 많은 필드가 다른 테스트와 공유되었으며 일부분만 서로 달랐습니다. 서로 다른 필드 정의를 구성하여 코드 중복을 줄이고자 했습니다.
  • 개별 필드 실패를 개별 오류로 표시하는 우수한 테스트 출력을 원했고, 이에 따라 testslike 테스트 도우미가 탄생했습니다.

이러한 상황을 고려하여 다음과 같은 디자인 의사 결정을 내렸습니다.

  • 스키마는 유연해야 하고, 개발자가 새로운 매처를 손쉽게 생성할 수 있어야 한다.
  • 모든 스키마가 구성 가능하고 중첩 가능해야 한다. 즉, 문서를 다른 문서 내에 중첩하는 경우 일련의 코드를 복제하지 않고도 스키마를 결합할 수 있어야 합니다.
  • 테스트 실패를 쉽게 알아볼 수 있도록 테스트 도우미의 품질이 우수해야 한다.

핵심 유형

Lookslike의 아키텍처는 ValidatorIsDef의 두 가지 주요 유형을 중심으로 이루어집니다. Validator는 주어진 스키마를 컴파일한 결과입니다. 임의의 데이터 구조를 취하고 결과를 반환하는 함수입니다. IsDef는 개별 필드를 대조하는 데 사용되는 유형입니다. 이러한 유형을 구분하는 이유가 궁금하실 수 있는데요. 실제로 나중에 이러한 유형을 병합할지도 모릅니다. 그러나 이를 구분하는 주된 이유는 IsDef는 문서 구조에서의 위치에 대한 추가 인수를 가져오므로 해당 컨텍스트를 기반으로 고급 검증을 수행할 수 있기 때문입니다. Validator 함수는 별도의 컨텍스트를 가져오지는 않지만 실행하기에 더 간편합니다(interface{}만 있으면 검증 가능).

사용자 정의 IsDef를 구성하는 예제는 소스 파일을 참조하십시오. 자체 소스에 새로운 IsDef를 추가하여 확장할 수 있습니다.

실무 사례

우리는 Lookslike를 Beats에서 광범위하게 사용합니다. 이 github 검색에서 많은 사용 사례를 확인할 수 있습니다.

도움이 필요합니다!

Lookslike에 관심이 있으시면 리포지토리에서 풀 요청을 제출해 주세요! 특히 좀 더 포괄적인 IsDef 세트가 필요합니다.

자세히 알아보기

Lookslike를 문서화하기 위해 많은 노력을 기울였습니다. godoc.org에서 Lookslike 문서를 찾아볼 수 있습니다.