앞으로 토이 프로젝트를 하면서 틈틈이 공부한 내용을 적용 및 정리해보고자 한다.
https://product.kyobobook.co.kr/detail/S000211704725
100 Go Mistakes Go 100가지 실수 패턴과 솔루션 | 테이바 하사니 - 교보문고
100 Go Mistakes Go 100가지 실수 패턴과 솔루션 | 모든 Go 개발자가 꼭 읽어야 할 책이다 Go 개발자들이 흔히 만나는 일반적인 실수를 피하고, 생산성과 효율성을 최대한 끌어내자!고 프로그래머가 고
product.kyobobook.co.kr
오늘의 주제는 #11 함수형 옵션 패턴~
우리의 코드는 한번 짜고나서 끝이 아니다. 항상 요구사항과 리팩토링 등의 이유로 코드를 고쳐야한다.
이 과정에서 함수의 파라미터에 변화가 필요한 경우가 발생한다.
이런 변동사항에 대응하기 위해 함수 시그니처가 변해야한다!
함수 시그니처
아래의 함수를 고려해보자.
func IsAirConditoner(temp int) bool {
return temp >= 30
}
어떤 사람이 30도 이상인 경우 에어컨을 킨다고 할 때, 이를 판단하는 간단한 함수이다.
이 함수를 기반으로 어떤 아파트에서 에어컨이 하나라도 켜지고 있는지 확인하기 위한 함수로 아래와 같은 함수가 있다.
func anyAirConditioner(temps []int, condition func(int) bool) bool {
for _, t := range(temps) {
if condition(t) {
return true
}
}
return false
}
그러나 사람들이 가난해진 나머지 그들의 지갑 사정을 고려하기 시작했다!
이제 사람들은 그들의 통장잔고가 300만원 이상이어야만 에어컨을 키게 되었다.
위 사항을 고려하면, 다음과 같이 함수를 고칠 수 있다.
func IsAirConditoner(temp, money int) bool {
return temp >= 30 && money >= 300 // 만원 단위
}
변경이 어렵지는 않지만, 함수 시그니쳐가 변함으로서 기존의 anyAirConditoner 또한 수정해주어야 한다.
isAirConditioner 를 사용하는 함수가 많을수록 그 귀찮음과 버그는 배가 된다!
Golang 에는 모든 매개변수에는 값이 전달되어야만 한다.
variadic parameter 이라 불리는 ... 연산자를 통해 변수의 개수를 조절할 수 있지만,
모두 같은 타입이어야하며, 사실상 배열(슬라이스)을 입력받는 것과 크게 다르지 않다. (함수 호출시의 파라미터의 형태가 조금 달라진다.)
func example1(nums ...int) {
...
}
func example(nums []int) {
...
}
...
example1(1, 2, 3, 4, 5)
example2([]int{1, 2, 3, 4, 5})
즉, 정해진 개수의 매개변수를 전달해야하며, 이에 따라 기본값 설정도 불가능하다.
이를 위해 Golang 에서는 Config struct 를 사용할 수 있다.
이러한 구조체를 함수 매개변수로 하는 함수는 변동사항에 있음에도 함수 시그니쳐를 바꾸지 않아도 된다.
Config struct
type Config struct {
temperature int
money int
...
}
func isAirConditioner(cfg Config) bool {
return cfg.temperature >= 30 && cfg.money >= 300
}
Config 구조체에 field 가 추가되더라도 함수의 시그니처가 변하지 않아도 된다. 원하는 field 만 사용하면 된다!
하지만, Config 구조체 사용자가 직접 구조체를 설정해서 코드를 짜는 방식은 바람직하지 않다.
우리는 Config 의 field 는 상황에 따라 검증로직이 필요할 수 있다.
예컨데, 온도는 -100도 이상 40도 이하 라는 조건이라든가(-100도 이하 40도 이상은 지구가 잘못된게 아닌 이상 입력이 잘못된 것이다!)
나의 통장잔고는 0원 이상이라든가(마이너스 통장도 있는데요 라고 태클걸지 말자)
혹은 입력이 없을 시에 기본값 설정이 필요할 수 있다.
통장잔고 입력이 없을 시, 기본값 0원 설정의 예시가 있다.
이러한 설정의 반영을 Config 구조체를 사용하는 곳에서 바로 작성하는 것은 바람직하지 않다.
1. 코드가 너무 복잡해진다.
clean code 의 내용 중에는
"function do one thing." -clean code
라는 내용이 있다.
한 함수에 Config 검증 로직과 실제 함수의 동작, 두 기능이 동시에 존재하게 된다.
2. 디버깅이 힘들어진다.
위의 Config 를 그대로 사용하며, 온도 조건을 -100 이상 40도 이하 로 검증하고자 했다고 가정해보자.
type Config struct {
temperature int
... // 그 외는 잠깐 무시하자
}
func ex1(cfg Config) error {
if !(-100 <= cfg.temperature && cfg.temperature <= 40) {
return errors.New("온도 조건이 잘못되었습니다!")
}
...
}
func ex2(cfg Config) error {
if !(-100 < cfg.temperature && cfg.temperature <= 40) {
return errors.New("온도 조건이 잘못되었습니다!")
}
...
}
위의 코드에서 문제점이 한눈에 보일까?
사실 2번째 함수의 온도 조건 체크가 잘못되었다. -100도 이상이 아닌 초과이다.
만약 온도체크가 필요한 함수가 수십개만 되더라도, 검증로직 확인에만 큰 비용이 들어간다.
clean code 의 내용을 고려하여, 새로운 헬퍼함수로 분리하여 구현하고 싶어진다.
이에 대한 고전적인 방법에는 GoF 의 빌더 패턴이 있다.
해결책 - 빌더 패턴
최종적으로 Config 구조체를 반환해주는 Builder 함수와,
Config 의 옵션을 설정해주는 헬퍼함수들의 구성으로 사용자가 원하는대로 Config 세팅을 할 수 있다.
type Config struct {
temperature int
money int
}
type ConfigBuilder struct {
temperature *int
money *int
}
func (b *ConfigBuilder) setTemp(temp int) *ConfigBuilder {
b.temperature = &temp
return b
}
func (b *ConfigBuilder) setMoney(money int) *ConfigBuilder {
b.money = &money
return b
}
func (b *ConfigBuilder) build() (*Config, error) {
var cfg Config
// temperature
if b.temperature == nil {
return nil, errors.New("온도 설정이 비었습니다.") // 비어있으면 안된다!
} else {
if !(-100 <= *b.temperature && *b.temperature <= 40) {
return nil, errors.New("온도 설정이 이상합니다!")
} else {
cfg.temperature = *b.temperature
}
}
// money
if b.money == nil {
cfg.money = 0 // 비어있어도 된다!
} else {
if !(*b.money < 0) {
return nil, errors.New("통장잔고 설정이 이상합니다!")
} else {
cfg.money = *b.money
}
}
return &cfg, nil
}
앞서 논의한 모든 점들이 반영되어 있다.
온도, 통장잔고의 검증로직, 잔고의 기본값 설정 모두 반영하며, 코드의 재활용으로 이 부분의 로직만 디버깅하면 된다.
함수에 사용할 때도 비교적 깔끔해진다.
builder := ConfigBuilder{}
cfg, err := builder.
setTemp(35).
setMoney(500).
build()
if err != nil {
return err
}
example(cfg)
하지만, 이 방법에도 문제점은 존재한다.
우리는 메소드 체이닝을 사용하기 위해 각 헬퍼(설정)함수는 빌더를 리턴해야만 한다.
즉, 각각의 field 를 설정할 때, 에러를 검출하고 싶어도 에러 객체를 반환할 수 없기 때문에,
모든 에러 검증 로직은 빌드할 때 확인되어야한다.
함수형 옵션 패턴을 사용하면 이를 해결할 수 있다.
함수형 옵션 패턴
이 패턴에서는 헬퍼함수가 어떤 함수를 반환한다.
옵션의 설정값과 그 설정값을 검증 및 설정하는 함수의 조합으로 클로저가 반환된다.
이 옵션을 사용하는 함수는 이 클로저들을 받아,
순서대로 함수를 실행해 매 함수마다 에러를 확인한다.
type setCfg func(*Config) error
func WithTemperature(temp int) setCfg {
return func(c *Config) error {
if !(-100 <= temp && temp <= 40) {
return errors.New("온도 설정이 이상합니다!")
}
c.temperature = temp
return nil
}
}
모든 헬퍼함수는 setCfg 라는 타입의 함수를 반환해야한다.
이 setCfg 는 Config 를 세팅하는 함수 시그니처로 각 field 에 대한 헬퍼함수를 모아서 각각 실행하면서 에러를 검출해내는 것이다.
func example(setCfgs ...setCfg) error {
var cfg Config
for _, setCfg := range setCfgs {
err := setCfg(&cfg)
if err != nil {
return err
}
}
...
}
위와 같이 간단하게 구현 가능하다. for 문이 끝나고나면, 입력받은 모든 세팅값이 설정되며 그 과정에서 에러를 반환해낼 수 있다.
example(WithTemperature(50),
WithMoney(300))
example() // <- temperature 는 필수라서 error 반환
example(WithTemperature(50)) // <- money는 기본값으로 세팅
위와 같이 실제 옵션처럼 유연하게 활용가능해진다.
하지만, 이 방법에도 단점이 존재한다.
옵션값을 입력받지 않고 기본값을 사용하는 경우에는, 그 로직을 헬퍼함수에서 구현할 수 없다.
왜냐하면 애당초 그 함수를 사용하지 않기 때문이다.
위의 예시에서 money 는 기본값으로 0을 설정한다고 정했지만, 이를 헬퍼함수에 반영할 수 없으므로,
사용하는 함수(이 경우에는 example 함수) 에서 옵션 설정후 값이 비었는지 확인 후 세팅하는 추가 작업이 필요하다.
하지만, 함수 사용자 혹은 라이브러리 사용자에게 있어 가장 깔끔한 형태를 갖춰주기 때문에,
Golang 에서는 관용적으로, 다양한 라이브러리에서 사용하고 있다고 한다.
각 패턴에는 장단점이 있으니, 필요한 적재적소에 사용하는 것이 중요하다.
마지막으로 내 토이 프로젝트에 어떤 식으로 사용하고 있는지 소개하고 마치고자 한다.
https://github.com/cmj7271/discord-AI-bot/pull/7/commits
Option pattern by cmj7271 · Pull Request #7 · cmj7271/discord-AI-bot
funtional option pattern 을 적용해봄 (반쯤은 공부용)
github.com
// main.go
err := discord.Run(discord.WithToken(token))
위의 함수형 옵션 패턴을 활용하여 디스코드 봇의 token을 설정하여 봇을 실행시킨다.
후에 추가적인 옵션 추가에는 간단히 With~() 을 구현하고, 사용하는 것으로 간단히 수정가능하다.
프로젝트 여담
공부의 목적으로 다양한 세팅에 도전해보고 있다.
docker, docker-compose, 린터, github-action 등 배포 관련 내용이다.
아직까지 docker, docker-compose 의 장점은 못 느끼고 있다. (아직 코드가 별로 없어서?)
그런데 린터와 github-action 의 조합은 생각보다 유용하다고 느껴진다.
현재 구성은 PR 을 올리면, action 에 의해 자동으로 golangci-lint 가 돌아간다.
혼자 하는 프로젝트이다보니 리뷰어나 멘토 없이 혼자 하게 되어 놓치는 부분이 없을까 걱정되지만,
린트가 알아서 에러를 잡아주니 한결 편하게 작업할 수 있는거 같다.
당장 위 링크의 PR 에서도 나한테 "왜 에러 처리 안함?" 이라고 거부하여 수정 후 다시 PR을 올렸다.
앞으로도 느리지만 꾸준히 공부해보고자 한다. 아자아자 화이팅
'공부 > 서적' 카테고리의 다른 글
독서의 흔적(프로그래머의 뇌) - 23/12/20 (1) | 2023.12.21 |
---|