오픈소스 프로젝트를 살펴보면, 각 코드 파일에 라이센스가 명시되어 있는 것을 볼 수 있습니다. 이 라이센스는 프로젝트에 사용된 코드의 사용 범위와 조건을 정의하여, 코드를 법적으로 보호하는 중요한 역할을 합니다. 라이센스를 각 코드 파일에 일일이 추가하는 작업은 번거로울 수 있습니다. 이러한 불편을 해소하기 위해 Google에서는 addlicense 도구를 제공하고 있으며, 이 도구를 사용하면 코드에 라이센스를 효율적으로 추가할 수 있습니다. 본 문서에서는 addlicense를 사용하여 Go 언어의 코드 파일에 라이센스를 추가하는 방법을 설명하겠습니다. 비록 예시는 Go 언어로 진행하지만, addlicense는 다양한 프로그래밍 언어에 적용 가능합니다.
이번 문서에서는 설치 방식의 사용법과 Docker Container 형식의 사용법에 대하여 정리하였습니다. 본인의 환경에 맞는 방법을 선택하여 진행하시면 됩니다.
addlicense 설치 방식의 사용법
Google에서 제공하는 addlicense 도구는 Go 언어로 개발되어 있습니다. 따라서 이 도구를 사용하기 전에는 Go 언어가 시스템에 설치되어 있어야 합니다. Go 언어가 이미 설치되어 있지 않다면, 사용하기 전에 반드시 Go 언어의 설치를 먼저 진행해야 합니다. Go 언어 설치는 공식 웹사이트에서 지원하는 안내에 따라 진행할 수 있습니다.
# addlicense 설치
go install github.com/google/addlicense@latest
addlicense 설치 확인
# 설치 확인
ls $(go env GOPATH)/bin
addlicense --help
addlicense 설치 확인 시 문제 발생한 경우
만약 command not found 발생 시 PATH를 추가합니다. (이미 추가되어있는 분은 수행하지 않습니다.)
# zsh: command not found: addlicense와 같은 문제가 발생할 경우 PATH에 go bin위치 추가
export PATH=$PATH:$(go env GOPATH)/bin
addlicense를 이용하여 라이센스 추가하기
프로젝트 디렉토리로 이동 후 실행
# addlicense 사용법 **로 모든 경로를 지정합니다.
addlicense -c '프로젝트명' -l apache ./**/*.go
addlicense 삭제
더 이상 addlicense가 필요 없을 경우 삭제하는 방법입니다.
# addlicense 삭제 방법
ls $(go env GOPATH)/bin
rm $(go env GOPATH)/bin/addlicense
Docker container 방식의 사용법
개발자로 근무하면서 다양한 도구와 라이브러리를 설치하고 관리하는 것은 종종 번거롭고 시간이 많이 소모되는 일입니다. 특히 설치한 프로그램을 나중에 제거할 때 발생하는 문제들로 인해 작업 효율이 떨어질 수 있습니다. 이러한 문제를 해결하기 위해 많은 개발자들이 설치와 설정, 실행을 간편화하는 방법으로 Container를 많이 사용하고 있습니다. Container를 사용하면 필요할 때만 특정 환경을 구성하고 작업이 끝나면 컨테이너를 간편하게 제거할 수 있습니다. 이번 섹션에서는 Docker 컨테이너를 사용하여 addlicense 도구를 실행하는 방법을 소개하겠습니다.
도커 이미지 다운로드
docker pull ghcr.io/google/addlicense:latest
컨테이너로 실행
docker run -it -v ${PWD}:/src ghcr.io/google/addlicense -c "프로젝트명" **/*.go
ubuntu 환경에서 golang version upgrade를 하는 방법에 대하여 간단하게 정리하였습니다.
0. 작업 준비
# apt update를 진행합니다.
sudo apt-get update
# wget이 설치되었는지 확인합니다.
# 다음과 같은 결과가 나오면 wget이 설치된 것 입니다.
# /usr/bin/wget
which wget
# wget이 없다면 설치하여 줍니다.
sudo apt-get install wget
go work를 사용하기 위해 반듯이 go1.18 이상의 go가 설치되어 있어야 합니다. go의 workspace는 회사에서 다른 개발자들과 협업을 하게 되면 모듈의 의존성에 대한 문제가 종종 발생하게 됩니다. 이럴 때 이러한 문제를 해결할 수 있는 것이 바로 go의 workspace입니다. 간단한 예시를 들자면 example이라는 모듈을 현재 회사에서 사용하고 있는데 example이라는 모듈에 기능을 추가하거나 기능을 변경할 경우 협업하고 있는 다른 개발자들에게 영향을 미치게 해서는 안 되는 경우가 있습니다. 이런 경우 "go work"를 통하여 다른 개발자들에게 영향을 미치지 않고 example 모듈을 수정하고 수정된 모듈을 이용하여 프로젝트 개발을 진행할 수 있습니다.
해당 글은 아래 링크 go document문서를 기반으로 똑같이 따라 하며 진행하였으며 개인적인 생각과 설명이 추가된 내용입니다. go work에 대한 원본 및 자세한 내용을 원하신다면 아래 공식 문서를 확인하거나 "$ go help work"를 통하여 보다 정확한 정보를 확인할 수 있습니다.
$ cd ~/workspace/hello
$ go run example.com/hello
# 결과 #
olleH
workspace 생성하기
go work 명령어를 이용하여 workspace를 생성합니다. (작업 디렉토리는 ~/workspace입니다.)
$ cd ~/workspace
$ go work init ./hello
위 명령어를 수행하게 되면 다음과 같이 go.work파일이 생성됩니다. 참고로 저는 go1.20를 사용하고 있습니다.
go 1.20
use ./hello
모듈 다운받아 작업 진행하기
이제는 협업하는 다른 개발자들에게 모듈을 수정해도 영향을 미치지 않도록 모듈을 다운받아 모듈을 수정하는 작업을 진행할 예정입니다. 저장소 복제하기 (작업 디렉토리는 ~/workspace입니다.)
$ cd ~/workspace
$ git clone https://go.googlesource.com/example
workspace에 모듈 추가하기
$ go work use ./example/hello
위 작업을 진행하면 go.work파일이 다음과 같이 수정됩니다.
go 1.20
use (
./hello
./example/hello
)
모듈 수정 작업 진행하기
~/workspace/example/hello/reverse에 int.go라는 파일을 생성하여 다음과 같이 코드를 작성합니다.
package reverse
import "strconv"
// Int returns the decimal reversal of the integer i.
func Int(i int) int {
i, _ = strconv.Atoi(String(strconv.Itoa(i)))
return i
}
지금까지 같이 협업하는 개발자들에게 모듈 수정으로 영향을 미치지 않고 모듈을 수정하여 개발을 진행하도록 하는 workspace에 대하여 알아봤습니다.
마지막으로
개발을 완료하고 협업하고 있는 모든 개발자들이 github에 push 하고 merge 작업을 진행할 때 반듯이 go.work 파일이 있다면 버전을 확인 후 작업을 진행한 개발자들과 패키지 버전에 대한 논의를 진행 후 merge 작업을 진행해야 build 과정에서 패키지 버전에 대한 이슈를 예방할 수 있습니다.
위 링크에서 최신 버전(현재 최신 버전은 1.19.2)의 linux-amd64.tar.gz에 우클릭하여 링크 주소를 복사하고 wget으로 다운로드합니다.
# go 다운로드
wget https://golang.org/dl/go1.19.2.linux-amd64.tar.gz
# 압축 풀기
sudo tar -C /usr/local -xvf go1.19.2.linux-amd64.tar.gz
# 환경변수 설정
mkdir ~/go
echo "export GOROOT=/usr/local/go" >> ~/.profile
echo "export PATH=$PATH:/usr/local/go/bin" >> ~/.profile
echo "export GOPATH=$HOME/go" >> ~/.profile
source ~/.profile
echo $GOPATH
# go version 확인하기
go version
reflect는 여러 가지의 사용방법이 있겠지만 저는 종종 협업을 하면서 상대방이 보낸 데이터의 타입 및 종류에 대하여 구분이 필요한 경우 사용하였습니다.
reflect를 이용하여 타입 가져오기
인터페이스 형식으로 데이터를 받아 받은 데이터의 타입을 구분할 수 있습니다.
package main
import (
"fmt"
"reflect"
)
func ShowType(i interface{}) {
fmt.Println(reflect.ValueOf(i).Type())
}
func main() {
num := 100
ShowType(num)
job := "student"
ShowType(job)
}
/* 출력결과
int
string
*/
코드설명
reflect.ValueOf().Type()을 사용하여 해당 타입이 어떤 타입인지 알 수 있습니다.
reflect를 이용하여 구조체 멤버 변수 확인하기
reflect를 사용하여 인터페이스로 받은 구조체의 멤버 변수들을 확인하도록 하겠습니다. 여기서 중요한 건 멤버 변수가 없는 일반 변수의 인터페이스를 넘기면 panic이 발생합니다.
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Job string `json:"job"`
}
func (p Person) Print() {
fmt.Println(p.Name, " is ", p.Job)
}
func ShowMetaData(i interface{}) {
elements := reflect.ValueOf(i).Elem()
for index := 0; index < elements.NumField(); index++ {
typeField := elements.Type().Field(index)
fmt.Println(typeField.Name, typeField.Type, typeField.Tag, elements.Field(index))
}
}
func main() {
p := &Person{
Name: "Han",
Age: 30,
Job: "Developer",
}
ShowMetaData(p)
// panic 발생
//num := 100
//ShowMetaData(num)
}
/* 출력결과
Name string json:"name" Han
Age int json:"age" 30
Job string json:"job" Developer
*/
코드설명
ShowMetaData() 함수를 보면 interface의 value를 reflect.Value().Elem()을 통하여 elements들을 가져옵니다. 그리고
elements.Type().Field()를 통하여 현재 필드의 데이터를 StructField형식으로 가져올 수 있습니다. StructField의 구조는 다음과 같습니다.
StructField의 구조
// A StructField describes a single field in a struct.
type StructField struct {
// Name is the field name.
Name string
// PkgPath is the package path that qualifies a lower case (unexported)
// field name. It is empty for upper case (exported) field names.
// See https://golang.org/ref/spec#Uniqueness_of_identifiers
PkgPath string
Type Type // field type
Tag StructTag // field tag string
Offset uintptr // offset within struct, in bytes
Index []int // index sequence for Type.FieldByIndex
Anonymous bool // is an embedded field
}
Name : 구조체 멤버 변수의 이름
Type : 구조체 맴버변수의 타입
Tag : 구조체 맴버변수의 태그
Offset : 구조체 멤버 변수의 오프셋(맴버변수의 메모리 위치)
Index : 구조체 맴버변수의 선언 순서
reflect를 이용하여 멤버 변수 가져오기
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Job string `json:"job"`
Salary int `json:"salary"`
}
func (p Person) Print() {
fmt.Println(p.Name, " is ", p.Job)
}
func (p *Person) SetSalary(salary int) {
p.Salary = salary
}
func (p Person) GetName() string {
return p.Name
}
func ShowMethod(i interface{}) {
value := reflect.ValueOf(i)
for index := 0; index < value.NumMethod(); index++ {
fmt.Printf("method name : %s\n", value.Type().Method(index).Name) // 1번
fmt.Printf("method type : %v\n\n", value.Method(index).Type()) // 2번
}
}
func main() {
p := &Person{
Name: "Han",
Age: 30,
Job: "Developer",
}
ShowMethod(p)
}
/* 출력결과
method name : GetName
method type : func() string
method name : Print
method type : func()
method name : SetSalary
method type : func(int)
*/
코드설명
1번 주석을 보면 Type().Method()를 통하여 구조체의 메서드의 타입을 가져올 수 있습니다. 가져온 메서드 타입으로 메서드 명을 가져올 수 있습니다.
2번 주석을 보면 Method().Type()을 통해 실제 메서드의 구성을 알 수 있습니다.
우리는 살면서 종종 정해진 시간에 정해진 일이 자동으로 수행되었으면 하는 일들이 많이 있습니다. Go언어에서는 이러한 일을 cron이 해결해 줍니다.
먼저 프로젝트 디렉토리에서 다음 명령어로 모듈을 가져옵니다.
> go get github.com/robfig/cron
package main
import (
"fmt"
"github.com/robfig/cron"
"time"
)
const (
CronSpec = "30 * * * * *"
)
var (
Data int
)
func PrintData() {
t := time.Now().Format(time.ANSIC)
fmt.Println(t, " : ", Data)
Data++
}
func main() {
c := cron.New()
c.AddFunc(CronSpec, PrintData)
c.Start()
time.Sleep(time.Minute * 10)
c.Stop()
}
/* 출력결과
Tue Jun 15 15:15:30 2021 : 0
Tue Jun 15 15:16:30 2021 : 1
Tue Jun 15 15:17:30 2021 : 2
*/
코드설명
cron.New()를 통하여 cron을 생성하고 AddFunc()를 통해 동작할 시간대와 동작할 함수를 등록합니다. "30 * * * * *"을 cron 시간으로 등록하였습니다. 이것은 출력결과를 보면 알 수 있듯이 매시간 매분 30초 일때 마다 함수가 동작합니다.
Cron Spec 구조
CronSpec을 "30 * * * * *"로 설정하였습니다. 여기서 많이 착가할 수 있는 부분이 30초 마다 동작을 수행하는 것으로 오해할 수 있습니다. 그러나 이것은 매 시간 매분 30초가 되었을 경우 수행하는 것을 의미합니다. 이제 Cron spec에 구조에 대하여 알아보겠습니다.
가장 왼쪽 부터 초, 분, 시, 일, 월, 요일 로 구성되어 있습니다.
Seconds
Minute
Hour
Day of month
Month
Week
0 ~ 59
0 ~ 59
0 ~ 23
1 ~ 31
1 ~ 12
0 ~ 6
예제
"0 0 0 * * *" : 매일 0시 0분 0초에 수행합니다.
"0 0 0 1 1 *" : 1월 1일 0시 0분 0초에 수행합니다.
Cron Spec에서 사용되는 문자
별표 *
별표는 눈치채셨겠지만 모든 값을 뜻합니다.
콤마 ,
콤마는 해당 spec에 여러개의 값을 설정하는데 사용합니다. "10,20,30 * * * * *" 이렇게 설정을 한다면 매시 매분 10초, 20초, 30초 일때 수행합니다.
마이너스 -
마이너스는 값의 범위를 지정합니다. "10-15 * * * * *"일 경우 매시 매분 10초 11초 12초 13초 14초 15초 에 수행합니다.
슬래쉬 /
슬래쉬의 경우 해당 범위에서 수행하는 간격을 의미 합니다. "10-50/5 * * * * *" 일 경우 매시 매분에서 10초에서 50초 사이에 5초에 한번씩 수행합니다.
기존 Metric의 코드와 크게 다르지 않다. prometheus.CounterVec를 통하여 메트릭을 생성하고 메트릭을 생성할 때 상수로 선언된 LabelAppName, LabelFuncName을 통하여 application_name, function_name이라는 Label을 등록하였습니다. 그리고 각각의 RunFunc1(), RunFunc2() 함수로 각각 별도의 function_name의 Label을 등록하였습니다.
Promql로 구분하여 검색하기
기존처럼 Metric 이름으로 검색하면 run_func1, run_func2가 모두 검색됩니다.
my_test_count
Label에 run_func1을 지정하여 특정 Label의 Metric을 검색하면 run_func1만 검색됩니다.
Counter는 개수를 측정하거나 증가하는 값을 측정할 때 사용합니다. 카운터는 0으로 초기화는 가능하지만 값을 감소할 수 없고 증가만 할 수 있습니다. 대표적으로 함수 호출 횟수 및 접속 요청 수, 실패 개수를 측정할 때 많이 사용할 수 있습니다.
Gauge 게이지
Gauge는 현재의 값을 나타내는 데 사용합니다. 값이 증가할 수 있으며 감소할 수도 있습니다. 증가 감소 모두 가능하다 보니 여러 방면에서 사용할 수 있습니다. 대표적으로 현재 메모리 사용량, CPU 사용 시간, 저장된 데이터의 수, 스레드 개수 등 여러 방면에서 사용합니다.
Summary 서머리
Summary는 _count, _sum을 제공하여 평균을 측정할 수 있으며, 슬라이딩 시간 윈도우를 측정할 때 사용합니다.
Histogram 히스토그램
Histogram은 시간 동안의 값을 측정하고 측정한 값을 통하여 quantile을 측정할 수 있습니다.
Go언어로 Metric 생성하기
먼저 다음 명령으로 모듈을 받습니다.
go get github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promauto
go get github.com/prometheus/client_golang/prometheus/promhttp
Counter 생성하기
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
"time"
)
var (
MyCounter prometheus.Counter
)
func Init() {
MyCounter = promauto.NewCounter(prometheus.CounterOpts{
Name: "my_test_count",
Help: "my test count",
})
}
func RunCounter() {
for i := 0; ; i++ {
MyCounter.Inc()
//MyCounter.Add(10)
time.Sleep(time.Second * 2)
}
}
func main() {
Init()
go RunCounter()
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":30001", nil)
}
코드설명
promauto.NewCounter를 통하여 카운터를 생성하고 카운터의 이름을 my_test_count로 만들었다. 카운터는 2초에 한 번씩 MyCounter.Inc()를 통하여 1씩 증가하고 있다. MyCounter.Add()를 이용하면 1 이상의 값을 증가시킬 수도 있다.
확인하기
Gauge 생성하기
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
"time"
)
var (
MyGauge prometheus.Gauge
)
func Init() {
MyGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "my_test_gauge",
Help: "my test gauge",
})
}
func RunGauge() {
for i := 0; ; i++ {
MyGauge.Set(float64(i))
time.Sleep(time.Second * 2)
}
}
func main() {
Init()
go RunGauge()
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":30001", nil)
}
코드설명
promauto.NewGauge를 통하여 my_test_gauge를 생성하였습니다. 2초마다 i의 값으로 gauge값을 세팅합니다.
Summary 생성하기
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"math/rand"
"net/http"
"time"
)
var (
MySummary prometheus.Summary
)
func Init() {
MySummary = promauto.NewSummary(prometheus.SummaryOpts{
Name: "my_test_summary",
Help: "my test summary",
})
}
func RunSummary() {
for {
MySummary.Observe(float64(rand.Int63n(10)))
time.Sleep(time.Second * 2)
}
}
func main() {
Init()
go RunSummary()
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":30001", nil)
}
코드설명
promauto.NewSummary를 통하여 my_test_summary를 생성하고 2초마다 0 ~ 10까지의 난수로 Observe를 통하여 값을 등록합니다.
확인하기
Histogram 생성하기
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"math/rand"
"net/http"
"time"
)
var (
MyHistogram prometheus.Histogram
)
func Init() {
MyHistogram = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "my_test_histogram",
Help: "my test histogram",
})
}
func RunHistogram() {
for {
MyHistogram.Observe(rand.Float64())
time.Sleep(time.Second * 2)
}
}
func main() {
Init()
go RunHistogram()
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":30001", nil)
}
코드설명
역시나 promauto.NewHistogram을 통해 생성합니다. 2초마다 난수를 등록합니다.
확인하기
다른 방식의 Metric 생성
지금까지 promauto를 통하여 Metric을 생성했습니다. 그러나 promauto를 사용하지 않고 다른 방식으로 Metric을 생성하는 방법을 알아보겠습니다.
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
"time"
)
var (
MyCounter prometheus.Counter
)
func Init() {
MyCounter = prometheus.NewCounter(prometheus.CounterOpts{
Name: "my_test_count",
Help: "my test count",
})
}
func RunFunc() {
for {
MyCounter.Inc()
time.Sleep(time.Second * 2)
}
}
func main() {
Init()
prometheus.MustRegister(MyCounter)
go RunFunc()
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":30001", nil)
}
코드설명
promauto.NewCounter가 아닌 prometheus.NewCounter를 통하여 Metric을 생성했습니다. promauto를 사용하지 않으면 prometheus.MustRegister를 통하여 Collector를 등록해야 됩니다.