Go 서버에서 mongoDB에 데이터를 넣을 때 추가할 수 있는 신기한 기능(?)을 리팩토링 코드 리뷰를 받으면서 알게되어 그 내용을 정리하여 글을 쓰고자 한다.
리뷰받은 코드는 다음과 같다.
types.DataToUpdateInMongo{
...
TotalTradingVolume: totalTradingVolume.String()
...
}
여기서 TotalTradingVolume 엄청나게 큰 숫자 값(블록체인 서비스다보니 기본 단위가 10^18 이기 때문)이다. 이래저래 사칙연산도 필요하다 보니 decimal.Deciaml 타입인 totalTradingVolume을 변수로 계산을 마친 후, 이제 mongo db 저장을 위해 최종 구조체에 값을 저장하려고 하는 코드이다.
mongo에 이렇게 큰 숫자값을 정확성을 유지하면서 저장하기 위해 우리 서비스는 서버에서 decimal로 계산하는 값들을 모두 string 타입으로 변환하여 저장하고 있다. 그리고 여기서 문제의 코드 구조체에는 정말 많은 deicmal 데이터들을 모두 string으로 바꿔서 담아주고 있었는데, 이 과정을 그저 decimal의 String() 메서드로 처리한 것이 코드리뷰에서 지적한 비효율성이었다.
db 객체 생성 시 decimal decoder 설정하여 사용하는 것이 좋을 것 같습니다.
아예 DB 객체를 생성할 때 decoder를 설정할 수 있다니? 새로운 사실이었다!
즉, DB 객체에 커스텀 설정을 통해 decimal로 들어오는 데이터를 자동으로 string으로 변환을 해주도록, 그리고 db에 string 타입으로 저장되어 있지만 decimal 값을 가지고 있는 데이터를 불러올 때 자동으로 decimal로 변환을 해주도록 설정이 가능하다!
그래서 mongodb 객체 코드를 다시 자세히 살펴보았다.
if client, err = mongo.Connect(ctx, options.Client().ApplyURI(config.DataSource)); err != nil {
return err
} else if err = m.client.Ping(ctx, nil); err != nil {
return err
} else {
db := m.client.Database(marketDBConfig.DB)
...
}
사족이지만 Ping은 아래와 같은 방식으로도 가능하다고 하더라. (출처)
// Send a ping to confirm a successful connection
var result bson.M
if err := client.Database("admin").RunCommand(context.TODO(), bson.D{{"ping", 1}}).Decode(&result); err != nil {
panic(err)
}
fmt.Println("Pinged your deployment. You successfully connected to MongoDB!")
여기서 client를 통해 Database를 만드는 코드를 보면 아래와 같이 설명되어 있다.
// Database returns a handle for a database with the given name configured with the given DatabaseOptions.
func (c *Client) Database(name string, opts ...*options.DatabaseOptions) *Database {
return newDatabase(c, name, opts...)
}
DatabaseOptions ! 여기서 DB 별로 옵션을 넣어줄 수 있었다.
우리가 살펴볼 DatabaseOptions은 다음과 같다.
// DatabaseOptions represents options that can be used to configure a Database.
type DatabaseOptions struct {
// ReadConcern is the read concern to use for operations executed on the Database. The default value is nil, which means that
// the read concern of the Client used to configure the Database will be used.
ReadConcern *readconcern.ReadConcern
// WriteConcern is the write concern to use for operations executed on the Database. The default value is nil, which means that the
// write concern of the Client used to configure the Database will be used.
WriteConcern *writeconcern.WriteConcern
// ReadPreference is the read preference to use for operations executed on the Database. The default value is nil, which means that
// the read preference of the Client used to configure the Database will be used.
ReadPreference *readpref.ReadPref
// Registry is the BSON registry to marshal and unmarshal documents for operations executed on the Database. The default value
// is nil, which means that the registry of the Client used to configure the Database will be used.
Registry *bsoncodec.Registry
}
그 외에도 Max/Min Pool Size, IdelTime, Socket 관련 옵션을 조절할 수 있는 ClientOptions, CollectionOptions 등이 있다. (참고: https://pkg.go.dev/go.mongodb.org/mongo-driver@v1.11.4/mongo/options)
아무튼, DatabseOptions에서 Registry의 설명을 보면 문서의 marshal, unmarshal 과 관련된 기능임을 알 수 있고, 이 옵션을 통해 decimal, string 타입 사이의 변환 기능을 정의해주면 된다.
field Registry *bsoncodec.Registry
Registry is the BSON registry to marshal and unmarshal documents for operations executed on the Database. The default value is nil, which means that the registry of the Client used to configure the Database will be used.
Registry 필드는 bsoncodec 패키지에 정의된 Registry 타입 값을 받는다.
위의 패키지 링크 속 Overview 설명을 참고하면 자세한 내용을 알 수 있다. 공부 겸 설명을 모두 번역해 보았다.
Overview
The types in this package enable a flexible system for handling this encoding and decoding.
The codec system is composed of two parts:
1) ValueEncoders and ValueDecoders that handle encoding and decoding Go values to and from BSON representations.
2) A Registry that holds these ValueEncoders and ValueDecoders and provides methods for retrieving them.
이 패키지의 유형을 사용하면 이 인코딩 및 디코딩을 처리하기 위한 유연한 시스템을 사용할 수 있습니다.
코덱 시스템은 두 부분으로 구성됩니다.
1) BSON 표현과의 Go 값 인코딩 및 디코딩을 처리하는 ValueEncoders 및 ValueDecoders.
2) 이러한 ValueEncoder 및 ValueDecoder를 보유하고 이를 검색하기 위한 방법을 제공하는 레지스트리입니다.
ValueEncoders and ValueDecoders
ValueEncoder 인터페이스는 Go 유형을 BSON 타입으로 인코딩할 수 있는 유형으로 구현(implement)될 수 있습니다. 인코딩할 값은 reflect.Value 로 제공되며, bsonrw.ValueWriter는 EncodeValue 메서드 내에서 실제로 BSON 표현을 생성하는데 사용됩니다. 편의를 위해 ValueEncoderFunc를 제공하고 있는데, 올바른 서명(correct signature)이 있는 함수를 ValueEncoder로 사용할 수 있습니다.
ValueDecoder 인터페이스는 ValueEncoder의 반대입니다. 이 인터페이스를 구현할 때, 디코더가 받는 값이 설정 가능(settable)한지 확인해야합니다. ValueEncoderFunc와 유사하게, ValueDecoder로서 올바른 서명이 있는 함수를 사용할 수 있도록 ValueDecoderFunc을 제공합니다. DecodeContext 인스턴스가 제공되며 EncodeContext와 유사한 기능을 제공합니다.
Registry
Registry는 ValueEncoders, ValueDecoders 및 type map을 위한 저장소입니다. (다양한 사용자 정의 인코더 및 디코더 등록에 대한 예시는 레지스트리 유형 문서를 참고.)
레지스트리에는 세가지 주요 유형의 코덱(codec)이 있을 수 있습니다:
1. Encoders/Decoders 유형
RegisterTypeDecoder 메소드를 사용하여 등록할 수 있습니다. 등록된 타입과 정확하게 일치하는 타입의 값을 인코딩/디코딩할 때 등록된 코덱이 호출됩니다. 예를 들어, 등록된 타입이 인터페이스일 경우, 타입이 인터페이스인 값을 인코딩하거나 디코딩할 때 코덱이 호출되지만, 인터페이스를 구현하는 구체적인 타입(concrete types that implement the interface)의 값에 대해서는 코덱이 호출되지 않습니다.
2. Hook encoders/decoders
RegisterHookEncoder 및 RegisterHookDecoder 메서드를 사용하여 Hook encoders/decoders를 등록할 수 있씁니다. 이러한 메서드는 인터페이스 타입만 허용하며, 등록된 코당은 해당 유형이 인터페이스를 구현하는 값(value)을 인코딩하거나 디코딩할 때 호출됩니다. Driver에 정의된 Hook의 예시는 bson.Marshaler 입니다. 드라이버는 값의 구체적인 타입(value's concrete type)에 관계 없이 bson.Marshaler를 구현(implement)하는 모든 타입의 값에 대해 MarshalBSON 메서드를 호출합니다.
3. Type map entries
BSON 타입을 Go 타입과 연결(associate)하는데 사용할 수 있습니다. 이러한 타입 연관(type associations)은 bson.D/bson.M 또는 interface{} 타입의 구조체 필드로 디코딩할 때 사용됩니다. 예를 들어, 기본적으로 BSON int32 및 int64 값은 bson.D로 디코딩할 때 각각 Go int32 및 int64 인스턴스로 디코딩됩니다. 다음 코드는 이러한 값이 Go int 인스턴스로 디코딩되도록 동작을 변경합니다.
intType := reflect.TypeOf(int(0)) registry.RegisterTypeMapEntry(bsontype.Int32, intType).RegisterTypeMapEntry(bsontype.Int64, intType)
4. Kind encoder/decoders
RegisterDefaultEncoder 및 RegisterDefaultDecoder 메서드를 사용하여 등록할 수 있습니다. 인코딩 및 디코딩 값의 reflect.Kind가 등록된 reflect.Kind와 일치하는 값일 때 등록된 코덱이 호출됩니다. (단, 값의 타입이 등록된 타입(registered type) 또는 hook 인코더/디코더와 먼저 일치하지 않아야 합니다.) 특정 종류(kind)에 대한 모든 값의 동작을 변경하려면 이러한 메서드를 사용해야 합니다.
Registry Lookup Procedure(조회 절차)
레지스트리에서 인코더를 조회할 때 우선순위 규칙은 다음과 같습니다.
1. 값의 정확한 타입과 일치하는 등록된 인코더
2. 값 또는 값에 대한 포인터로 구현되는 인터페이스의 등록된 hook 인코더.
값이 여러 hook와 일치하는 경우(ex. 타입이 bsoncodec.Marshaler 및 bsoncoder.ValueMarshaler를 구현함), 등록된 첫번째 hook가 선택됩니다.
bson.NewRegistry를 사용하여 구성된 레지스트리에는 bsoncodec.Marshaler, bsoncodec.ValueMarshaler 및 bsoncoder.Proxy 인터페이스에 대해 등록된 드라이버 정의 후크(driver-defined hooks)가 있으므로 이러한 후크가 새 후크보다 우선됩니다.
3. 값의 종류(kind)에 대해 등록된 kind 인코더.
이러한 조회가 모두 끝났음에도 인코더를 찾지 못하면 ErrNoEncoder 유형의 오류가 반환됩니다.
디코더에도 동일한 우선순위 규칙이 적용됩니다. 단, 디코더가 발견되지 않으면 ErrNoDecoder 유형의 오류가 반환된다는 점만 다릅니다.
DefaultValueEncoders and DefaultValueDecoders
DefaultValueDecoders의 타입은 기본 패키지(primitive package) 내의 모든 타입을 포함하여 광범위한 Go 타입을 처리하기 위한 전체 ValueEncoders 및 ValueDecoders 세트를 제공합니다. 이러한 코덱을 더 쉽게 등록할 수 있도록 각 타입에 대한 도우미 메서드가 제공됩니다.
DefaultValueEncoders 타입의 경우, 메서드는 RegisterDefaultEncoders이며, DefaultValueDecoders 타입의 경우 메서드는 RegisterDefaultDecoders. 이 메서드는 각 BSON 유형에 대한 type map 항목 등록도 처리합니다.
즉, 우선 인코더, 디코더 설정을 위해 registry 객체부터 만들어야한다.
registry와 관련한 패키지를 확인해 보면 Registry 외에 RegistryBuilder 라는 타입이 별도로 존재하고 있다.
그리고 Builder의 메소드에 위에 설명한 encoder, decoder register 메서드들이 있다.
따라서 Builder를 이용해 필요한 인코더, 디코더를 등록하고 registry 객체를 최종적으로 만들어(build)해줘야 한다는걸 알 수 있다.
Q. 굳이 Builder가 따로 있어야 하는 이유는 무엇일까? -> Go 객체 생성 패턴과 연관이 있을 것 같은데 추가 공부를 해봐야겠다.
// A RegistryBuilder is used to build a Registry. This type is not goroutine
// safe.
type RegistryBuilder struct {
typeEncoders map[reflect.Type]ValueEncoder
interfaceEncoders []interfaceValueEncoder
kindEncoders map[reflect.Kind]ValueEncoder
typeDecoders map[reflect.Type]ValueDecoder
interfaceDecoders []interfaceValueDecoder
kindDecoders map[reflect.Kind]ValueDecoder
typeMap map[bsontype.Type]reflect.Type
}
// A Registry is used to store and retrieve codecs for types and interfaces. This type is the main
// typed passed around and Encoders and Decoders are constructed from it.
type Registry struct {
typeEncoders map[reflect.Type]ValueEncoder
typeDecoders map[reflect.Type]ValueDecoder
interfaceEncoders []interfaceValueEncoder
interfaceDecoders []interfaceValueDecoder
kindEncoders map[reflect.Kind]ValueEncoder
kindDecoders map[reflect.Kind]ValueDecoder
typeMap map[bsontype.Type]reflect.Type
mu sync.RWMutex
}
// NewRegistryBuilder creates a new empty RegistryBuilder.
func NewRegistryBuilder() *RegistryBuilder {
return &RegistryBuilder{
typeEncoders: make(map[reflect.Type]ValueEncoder),
typeDecoders: make(map[reflect.Type]ValueDecoder),
interfaceEncoders: make([]interfaceValueEncoder, 0),
interfaceDecoders: make([]interfaceValueDecoder, 0),
kindEncoders: make(map[reflect.Kind]ValueEncoder),
kindDecoders: make(map[reflect.Kind]ValueDecoder),
typeMap: make(map[bsontype.Type]reflect.Type),
}
}
func buildDefaultRegistry() *Registry {
rb := NewRegistryBuilder()
defaultValueEncoders.RegisterDefaultEncoders(rb)
defaultValueDecoders.RegisterDefaultDecoders(rb)
return rb.Build()
}
아래와 같이 NewRegistryBuilder 함수를 이용해 RegistryBuilder 객체를 만들어줬다.
그리고 기본 Go 타입들의 처리를 위해 DefaultValueEncoders, Decoders를 빌더에 우선적으로 등록을 해주었다.
손쉽게 buildDefaultRegistry() 함수를 사용하여 이를 처리할 수 있지만, 우리는 DefaultValue에 존재하지 않은 decimal 타입의 디코더도 넣어줘야하기 때문에 buildDefaultRegistry() 함수를 사용하지 않았다. (DefaultValueEncoders/Decoders 코드를 보면 decimal 타입은 존재하지 않는다.)
rb := bsoncodec.NewRegistryBuilder()
bsoncodec.DefaultValueEncoders{}.RegisterDefaultEncoders(rb)
bsoncodec.DefaultValueDecoders{}.RegisterDefaultDecoders(rb)
이제 우리가 원하는 Decimal 타입의 디코더를 추가해보자. 그 반대인 인코더는 추후 구현 예정
var reflectDecimal = reflect.TypeOf(decimal.Decimal{})
rb.RegisterTypeDecoder(reflectDecimal, bsoncodec.ValueDecoderFunc(decimalDecoder))
RegisterTypeDecoder를 이용해 decimal 디코더를 만들었다. 그런데 여기서 인자로 reflect.Type과 ValueDecoder를 받는다.
ValueDecoder는 아래와 같은 타입의 인터페이스이다.
// ValueDecoder is the interface implemented by types that can handle the decoding of a value.
type ValueDecoder interface {
DecodeValue(DecodeContext, bsonrw.ValueReader, reflect.Value) error
}
그렇다면 decimal 타입을 위한 ValueDecoder는 어떻게 만들 수 있을까?!
DefaultValueDecoder 코드들을 참고했을 때, 크데 아래 두가지 방법으로 ValueDecoder를 만들고 있었다.
1. ValueDecoderFunc 이용
// ValueDecoderFunc is an adapter function that allows a function with the correct signature to be
// used as a ValueDecoder.
type ValueDecoderFunc func(DecodeContext, bsonrw.ValueReader, reflect.Value) error
: ValueDecoder 함수를 직접 만드는 방법
2. decoderAdapter 이용
// decodeAdapter allows two functions with the correct signatures to be used as both a ValueDecoder and typeDecoder.
type decodeAdapter struct {
ValueDecoderFunc
typeDecoderFunc
}
: ValueDecoder와 TypeDecoder로 모두 사용할 수 있는 adapter 타입을 사용하는 방법
굳이 TypeDecoder까지 호환되는 adapter를 사용할 이유는 없기에, 첫번째 방법인 ValueDecoderFunc을 구현하기로 했다.
// decimalDecoder decimal.Decimal Decoder
func decimalDecoder(ec bsoncodec.DecodeContext, vw bsonrw.ValueReader, val reflect.Value) error {
if !val.IsValid() || val.Type() != reflectDecimal {
return bsoncodec.ValueEncoderError{Name: "decimalDecoder", Types: []reflect.Type{reflectDecimal}, Received: val}
}
if sValue, err := vw.ReadString(); err == nil {
val.Addr().Interface().(*decimal.Decimal).UnmarshalJSON([]byte(sValue))
} else {
return err
}
return nil
}
이 역시, DefaultValueDecoder 중 DDcoderValue 라는 ValueDecoder 함수를 참고했다.
먼저 value의 타입을 확인하고, ValueReader를 통해 값을 읽어온다. 우리의 경우 decimal 타입을 가진 값은 string 타입으로 mongo에 저장되어 있기에 ReadString 메소드를 사용했다.
그 이후, value(decimal 값)를 가진 주솟값을 Interface 형식으로 가져와서 decimal의 포인터 형태로 바꿔준다.
그리고 읽은 string 값을 바이트 배열로 바꿔서 json unmarshaler를 이용해 해당 포인터에 decimal 값을 넣어준다.
여기서 주의사항이있다. (인코더를 구현하였다면 필요없는 주의사항)
우리는 디코더만 구현한 상태이다. 따라서 데이터베이스에 데이터를 업데이트하는 인코딩 과정에는 디코더가 사용되지 않는다. 디코더만 사용해서 우리가 원하는 기능을 동작시키려면 구조체를 json으로 marshalling 하여 이 과정에서 커스텀 디코더가 호출이 되어 사용되도록 해야한다!
따라서 아래와 같은 함수를 이용하여 json marshal, unmarshal 과정을 거친 Go interface struct를 쿼리에 사용하였다.
func ToJSON(t interface{}) (interface{}, error) {
var v interface{}
if bytes, err := json.Marshal(t); err != nil {
return nil, err
} else if err := json.Unmarshal(bytes, &v); err != nil {
return nil, err
} else {
return v, nil
}
}
if v, err := ToJSON(&StructToUpdate{
...
}); err != nil {
return nil, err
} else {
return mongo.NewUpdateOneModel().SetFilter(bson.M{"key": key}).SetUpdate(bson.M{"$set": v}).SetUpsert(true), nil
}
그리고 rawValue에 대한 코덱도 추가해주었다.
var primitiveCodecs bson.PrimitiveCodecs
primitiveCodecs.RegisterPrimitiveCodecs(rb)
var tRawValue = reflect.TypeOf(RawValue{})
var tRaw = reflect.TypeOf(Raw(nil))
var primitiveCodecs PrimitiveCodecs
// PrimitiveCodecs is a namespace for all of the default bsoncodec.Codecs for the primitive types
// defined in this package.
type PrimitiveCodecs struct{}
// RegisterPrimitiveCodecs will register the encode and decode methods attached to PrimitiveCodecs
// with the provided RegistryBuilder. if rb is nil, a new empty RegistryBuilder will be created.
func (pc PrimitiveCodecs) RegisterPrimitiveCodecs(rb *bsoncodec.RegistryBuilder) {
if rb == nil {
panic(errors.New("argument to RegisterPrimitiveCodecs must not be nil"))
}
rb.
RegisterTypeEncoder(tRawValue, bsoncodec.ValueEncoderFunc(pc.RawValueEncodeValue)).
RegisterTypeEncoder(tRaw, bsoncodec.ValueEncoderFunc(pc.RawEncodeValue)).
RegisterTypeDecoder(tRawValue, bsoncodec.ValueDecoderFunc(pc.RawValueDecodeValue)).
RegisterTypeDecoder(tRaw, bsoncodec.ValueDecoderFunc(pc.RawDecodeValue))
}
// Raw is a wrapper around a byte slice. It will interpret the slice as a
// BSON document. This type is a wrapper around a bsoncore.Document. Errors returned from the
// methods on this type and associated types come from the bsoncore package.
type Raw []byte
// RawValue represents a BSON value in byte form. It can be used to hold unprocessed BSON or to
// defer processing of BSON. Type is the BSON type of the value and Value are the raw bytes that
// represent the element.
//
// This type wraps bsoncore.Value for most of it's functionality.
type RawValue struct {
Type bsontype.Type
Value []byte
r *bsoncodec.Registry
}
최종적으로 DatabaseOptions 객체 안에 커스텀한 Registery를 넣어준다.
RegistryBuilder의 Build() 메서드를 사용하면 빌더를 이용해 등록한 인코더/디코더를 모두 담은 Registry 객체를 리턴해준다.
opt := options.DatabaseOptions{
Registry: rb.Build(),
}
그렇게 완성된 최종 코드는 다음과 같다.
var primitiveCodecs bson.PrimitiveCodecs
rb := bsoncodec.NewRegistryBuilder()
bsoncodec.DefaultValueEncoders{}.RegisterDefaultEncoders(rb)
bsoncodec.DefaultValueDecoders{}.RegisterDefaultDecoders(rb)
rb.RegisterTypeDecoder(reflectDecimal, bsoncodec.ValueDecoderFunc(decimalDecoder))
primitiveCodecs.RegisterPrimitiveCodecs(rb)
m.databaseOptions = &options.DatabaseOptions{
Registry: rb.Build(),
}
* 위 코드 중 deprecated 주석이 달린 코드가 있기 때문에 업데이트가 필요.
시간이 될 때 업데이트 된 코드를 추가하겠다!
헷갈리는 개념 되짚어보기
Encoder: 자기자신을 외부의 표현 형태로 변환
ex) 앱 안에서 사용하는 struct의 객체를 JSON으로 바꿀 때
Decoder: 외부의 표현 형태로부터 특정 환경에서 사용할 수 있는 타입으로 변환
ex) API의 response로 온 JSON data를 앱 안에서 사용할 수 있게 decode
'Language > Go' 카테고리의 다른 글
[Go] field 이름을 기준으로 field 초기화하기 with reflect (3) | 2024.11.08 |
---|---|
[Go] encoding/json 패키지 - json.Decoder, Unmarshal & io.ReadAll (1) | 2023.10.31 |