/img/avatar.jpg

프로젝트 설계에 대해

개요

코드를 구조화하는 건 생각보다 어렵지 않다고 생각합니다. 하지만 생각보다 어렵기도 하죠. 그래서 사실 이걸 어떻게 표현해야할 지는 모르겠는데, 그냥 편하게 아키텍처를 만드는 것에서 프로젝트 구조를 짜는 것, 코드를 작성하는 것까지의 제 나름대로의 룰을 정리해보려고 합니다.

모듈 혹은 서비스를 분리하세요.

모듈을 서비스와 동치해서 서술합니다.

아키텍처나 프로젝트 전반에서 한번에 거대한 문제를 해결하려고 하지 않습니다.
흔히 말하는 분할-정복 방식을 적용합니다. 각 문제를 분할하여 모듈화하고, 각 모듈을 독립적으로 개발 후 통합합니다.
이런 구조를 가지면 여러 레이어로 나누어진 모듈의 역할이 분리된 만큼 많은 이점이 생깁니다.

HTTP API 에러 쓰는 법

왜?

HTTP API에 에러 쓰는 건 개인 취향 혹은 조직에서 정한 룰 아닌가요?

저도 그렇게 생각하고 있습니다.
모름지기 학사 과정을 수료했든, 학원을 수료했든, 부트캠프를 수료했든, 백엔드 개발자라면 알아서 잘 남길 수 있을 것입니다.

그럼 왜 이 글이 있는 거죠?

RFC에 HTTP API의 에러에 관한 내용이 있더라구요. 흥미로워서 가져왔습니다.

RFC7807

RFC7807은 Problem Details for HTTP APIs라는 이름의 문서입니다.
이름 그대로 HTTP API에서 발생하는 오류 응답 형식을 적어놓은 문서죠.

이 문서에서는 application/problem+json MIME 타입을 소개합니다.

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 구조체의 필드가 포함된 것처럼 사용할 수 있습니다.