Provider

Provider는 제가 만들고 있는 고 언어에서 의존성 주입을 위한 컨테이너입니다.

정의

type Provider struct {
	constructors map[reflect.Type]map[reflect.Value]struct{}
	container    map[reflect.Type]any
	lock         sync.RWMutex
}

Providerconstructorscontainer를 가지고 있습니다.

  1. constructors는 생성자를 저장하는 맵입니다.
  2. container는 실제로 생성된 인스턴스를 저장하는 맵입니다.

생성자 등록

func (p *Provider) Register(constructFunction ...any) error {
	p.lock.Lock()
	defer p.lock.Unlock()
	for _, con := range constructFunction {
		if err := p.register(con); err != nil {
			return err
		}
	}
	return nil
}

func (p *Provider) register(constructFunction any) error {
	args, _, err := analyzeConstructor(constructFunction)
	if err != nil {
		return err
	}

	for _, arg := range args {
		if _, ok := p.constructors[arg]; !ok {
			p.constructors[arg] = make(map[reflect.Value]struct{})
		}
		p.constructors[arg][reflect.ValueOf(constructFunction)] = struct{}{}
	}

	return nil
}

func analyzeConstructor(constructFunction any) ([]reflect.Type, []reflect.Type, error) {
	if reflect.TypeOf(constructFunction).Kind() != reflect.Func {
		return nil, nil, ErrNotAFunction{}
	}

	constructor := reflect.ValueOf(constructFunction)
	var args []reflect.Type

	for i := 0; i < constructor.Type().NumIn(); i++ {
		args = append(args, constructor.Type().In(i))
	}

	var returns []reflect.Type

	for i := 0; i < constructor.Type().NumOut(); i++ {
		returns = append(returns, constructor.Type().Out(i))
	}

	return args, returns, nil
}

Register 메서드는 생성자를 등록하는 메서드입니다.
...any로 가변인자를 받아서 여러개의 생성자를 등록할 수 있습니다.
내부에 정의된 register 메서드를 호출해서 각 함수를 constructors에 저장합니다.

register는 내부에 다시 analyzeConstructor를 호출해서 생성자의 인자와 반환값을 분석합니다.

보면 알겠지만, 함수의 시그니처는 func Constructure(p1 T1, p2 T2, ...) (r1 R1, r2, R2 ... , error)로 되어있어야 합니다.

매개변수가 하나도 정의되어 있지 않다면, 동작하지 않습니다.

생성자 호출

func getContextType() reflect.Type {
	return reflect.TypeOf((*context.Context)(nil)).Elem()
}

func (p *Provider) Construct(ctx context.Context) error {
	p.lock.Lock()
	defer p.lock.Unlock()

	p.container[getContextType()] = ctx
	count := 0
	for len(p.constructors) > 0 {
		for arg, constructors := range p.constructors {
		ConsLoop:
			for con := range constructors {
				args := make([]reflect.Value, con.Type().NumIn())
				for i := 0; i < con.Type().NumIn(); i++ {
					at := con.Type().In(i)
					v, ok := p.container[at]
					if !ok {
						continue ConsLoop
					}
					args[i] = reflect.ValueOf(v)
				}

				returns := con.Call(args)

				for _, ret := range returns {
					if ret.Type().Kind().String() == "error" {
						if !ret.IsNil() {
							return ret.Interface().(error)
						}
					}

					p.container[ret.Type()] = ret.Interface()
				}

				count++

				delete(p.constructors[arg], con)
			}

			if len(p.constructors[arg]) == 0 {
				delete(p.constructors, arg)
			}
		}

		if count == 0 {
			return ErrMaybeCyclicDependency{}
		}
	}

	return nil
}

Construct 메서드는 생성자를 호출하는 메서드입니다.

constructors에 저장된 생성자를 하나씩 호출하면서, 인자로 필요한 값이 container에 있는지 확인하고, 있다면 호출합니다.

반환값이 있을 경우, 타입을 검사하여 container에 저장합니다.

이 과정을 반복하면서, constructors에 저장된 생성자가 모두 호출되면 종료합니다.
만약 이전 호출과 비교하여 호출된 생성자가 없다면, 순환 의존성이 있을 수 있으므로 에러를 반환합니다.

사용 예시

func JustRun(provider *Provider, function any) error {
	provider.lock.RLock()
	defer provider.lock.RUnlock()

	args, rets, err := analyzeConstructor(function)
	if err != nil {
		return err
	}

	if len(rets) != 1 || rets[0].String() != "error" {
		return ErrInvalidFunctionReturn{}
	}

	reflectArgs := make([]reflect.Value, len(args))
	for i, arg := range args {
		v, ok := provider.container[arg]
		if !ok {
			return ErrNotProvided{arg}
		}
		reflectArgs[i] = reflect.ValueOf(v)
	}

	reflectReturns := reflect.ValueOf(function).Call(reflectArgs)

	if !reflectReturns[0].IsNil() {
		return reflectReturns[0].Interface().(error)
	}

	return nil
}

JustRun은 프로바이더에 존재하는 인스턴스를 사용하여 함수를 호출하는 메서드입니다.

function은 생성자와 비슷하게 func Function(p1 T1, p2 T2, ...) (error)로 되어있어야 합니다.
JustRunfunction을 호출하고, 반환값이 에러라면 에러를 반환합니다.

마치며

이 라이브러리를 통해 쉽게 인스턴스를 생성하고 활용할 수 있게 되었으면 합니다.

한가지 아쉬운 점은 아직 로직이 효율적이지 않아, 앱 스타트가 느릴 수 있다는 점입니다.