/img/avatar.jpg

DevContainer로 개발환경 구성하기

Dev Container

Dev Container는 도커 컨테이너를 이용하여 개발 환경을 구축하는 방법입니다.
이 방식을 이용하면 쉽게 어느 곳에서나 동일한 개발 환경을 구축할 수 있습니다.
이 글에서는 Go 언어를 사용하는 개발 환경을 구축하는 방법을 소개합니다.

devcontainer 폴더 구성

프로젝트 폴더 내에 .devcontainer 폴더를 생성합니다.
그리고 다시 한번 그 안에 원하는 개발 환경 이름으로 폴더를 생성합니다.

$ mkdir -p .devcontainer/practice-go

devcontainer 설정 파일 작성

.devcontainer/practice-go 폴더 내에 devcontainer.json 파일을 생성합니다.
저희는 전반적인 구성을 docker-compose로 하기 때문에 docker-compose.yml 파일을 참조하도록 설정합니다.

고 언어의 의존성 주입을 위한 Provider

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에 저장합니다.

git, buf, 그리고 패키지 매니저로 프로토버퍼 관리하기

프로토버퍼?

프로토버퍼는 구글에서 개발한 직렬화 라이브러리입니다.
프로토버퍼는 다양한 언어를 지원하며, 단일 IDL(Interface Description Language)을 사용하여 여러 언어에 대한 DTO 및 시리얼라이저를 생성할 수 있습니다.

그렇기에 서버와 클라이언트 사이에 서로 다른 언어를 사용하더라도, 같은 IDL만 공유할 경우 서로가 주고 받는 메시지의 종류만 안다면, 별도의 DTO를 문서를 보고 만들 필요 없이 바로 사용할 수 있습니다.

프로토버퍼의 문제점

프로토버퍼는 .proto 확장자를 가진 IDL 파일을 각 언어에 맞게 컴파일해서 사용해야한다는 단점이 있습니다.

그래서 프로토버퍼 파일 버전이 상이할 경우에, 같은 메시지(DTO)를 주고 받는 다더라도, 예외가 발생할 수 있어 주의가 필요합니다.

vcpkg를 cgo에 사용하기

vcpkg?

vcpkg는 Microsoft에서 발표한 C/C++ 패키지 매니저입니다.
vcpkg의 레포에 등록된 패키지들을 install을 통해 손쉽게 설치하고 이용할 수 있습니다.

vcpkg 설치

vcpkg는 전역으로 사용할 수 있지만 이번 글에서는 프로젝트 내에서 사용하도록 설정하겠습니다.

mkdir prac
git clone https://github.com/microsoft/vcpkg.git

prac 폴더를 생성한 후, vcpkg를 클론합니다.
클론하면 vcpkg 폴더가 생성되고, 최초 1회 bootstrap-vcpkg.sh(or 윈도우의 경우, bootstrp-vcpkg.bat)을 실행해야 합니다.

제티

제티 (Jetti)

제티는 제가 프로젝트를 구조적으로 관리하고, 생산성을 조금이라도 높이기 위해 만든 코드 생성기입니다.

제티의 지향점

제티는 다음과 같은 지향점을 가지고 있습니다.

  1. 고 언어 프로젝트 구조에 대해 어느 정도 강제성을 부여합니다.
  2. 코드를 작성할 때, 귀찮은 부분을 최대한 코드 생성을 통해 줄여줍니다.
  3. 까먹고 하지 못 했다는 이유로 해야할 것을 하지 못하는 일을 최대한 줄여줍니다.
  4. cgo 의존성이나 외부 툴 설치에 대한 간단한 지원을 제공합니다.

설치

제티는 go install을 통해 설치할 수 있습니다.

구조체 임베딩과 프로모션, 그리고 상속

구조체 임베딩

구조체 임베딩은 구조체를 다른 구조체의 필드로 사용하는 것을 말합니다. 예를 들어 다음과 같은 구조체가 있다고 가정해봅시다.

type Person struct {
    Name string
    Age int
}

그리고 이 구조체를 다른 구조체의 필드로 사용한다면 다음과 같이 사용할 수 있습니다.

type Student struct {
    Person
    Grade int
}

그러면 마치 Student 구조체에 Person 구조체의 필드가 포함된 것처럼 사용할 수 있습니다.

Context 패키지 (실전)

기능 복기

TODO 컨텍스트의 경우에 특이 케이스이므로 여기에 포함시키지 않겠습니다.

컨텍스트 생성

package main

import "context"

func main() {
	ctx := context.Background()
}

context.Background() 를 통해 새로운 컨텍스트를 생성할 수 있습니다.
반드시 이 컨텍스트를 생성할 때는, 모든 작업의 최상단에서 생성해야 합니다.

취소 가능한 컨텍스트 생성

  1. context.WithCancel()
package main

import "context"

func main() {
	ctx := context.Background()

	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
}

context.WithCancel() 을 통해 취소 가능한 컨텍스트를 생성할 수 있습니다.
이 함수는 새로운 컨텍스트와 취소 함수를 반환합니다.
취소 함수를 통해 현재 컨텍스트를 포함한 하위 컨텍스트들에게 취소 시그널을 줄 수 있습니다.
시그널은 ctx.Done() 메서드에서 반환된 <-chan struct{}에서 받을 수 있습니다.

Context 패키지 (이론)

패키지 구성

Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.

컨텍스트 패키지는 Context 타입을 구현하고 있는 패키지입니다. 이 컨텍스트 타입은 인터페이스로 실제로는 하부에 크게 5가지 타입의 컨텍스트 구현체가 존재합니다.

컨텍스트 종류

  1. 아무 상태도 가지지 않은, backgroundCtxtodoCtx
  2. 취소 시그널을 보내거나 무시할 수 있는, cancelCtxwithoutCancelCtx
  3. 특정 시간만 동작하거나 특정 시각까지만 동작하는, timerCtx
  4. 키와 값을 저장하는, valueCtx
  5. 사후 처리를 담당하는, afterFuncCtx

backgroud, todo

아마 세상에서 가장 많이 쓰이는 컨텍스트입니다.