티스토리 뷰
❖ Clean Go Code
- 해당 링크의 go언어의 클린코드 내용을 요약한 것입니다.
- URL : https://github.com/Pungyeon/clean-go-article
❖ 왜 깨끗한 코드를 작성해야 할까?
- 가독성 높은 코드는 개발자가 작업을 이해하는 데 필요한 시간을 절약해 줍니다.
- 클린 코드 작성은 가독성뿐만 아니라 일관성과 재사용성을 강화해 유지보수에도 이점을 제공합니다.
- 클린 코드를 생각하지 않고 편리하게 코드를 작성하게 되면 코드 리뷰라 테스트가 어려워질 수 있습니다.
- 클린 코드를 작성하는 데 초기 비용이 들 수 있지만, 유지보수 비용을 감안하면 장기적으로 오히려 시간을 절약할 수 있습니다.
❖ 클린코드 소개
☻테스트 주도 개발
- 짧은 주기로 테스트를 통과하는 코드를 작성하고 이를 반복해서 기능을 개발해 나가는 것입니다.
- 단일 책임(Singlr Responsibility Principle)을 갖는 짧은 함수 단위로 작성하는 것이 좋습니다.
- 예를 들어 40줄인 함수보다 4줄인 함수를 테스트하고 이해하는 것이 훨씬 쉽습니다.
- 테스트 주도 개발의 주기
- 테스트를 작성한다.
- 테스트가 실패하면 통과하도록 만든다.
- 코드를 리팩토링 합니다.
- 이를 반복합니다.
☻네이밍 규칙
● Comments
- 불필요한 주석은 달지 마자.
- 코드가 잘 작성되었더라도 논리가 복잡할 경우, 이를 설명하는 주석이 유용합니다.
- 반복문의 구조를 설명하는 주석은 초보자에게 유용할 수 있지만, 경험 많은 개발자들에게는 불필요할 수 있습니다.
// iterate over the range 0 to 9
// and invoke the doSomething function
// for each iteration
for i := 0; i < 10; i++ {
doSomething(i)
}
- 코드 또한 깨끗한 코드로 간주하지 않습니다.
- 반복문이 기술적으로 무엇을 하는지 설명하고, 함수가 수행하는 기능에 더 의미 있는 이름을 부여해야 합니다.
for workerID := 0; workerID < 10; workerID++ {
instantiateThread(workerID)
}
- 변수와 함수 이름을 변경하면 코드가 수행하는 작업을 자연스럽게 설명할 수 있습니다.
- 주석을 코드에 직접 매핑할 필요 없이 수행중인 작업을 더 명확하게 이해할 수 있습니다
- 주석 작성에 익숙해지고, 현재 수행 중인 작업을 설명하는 주석을 줄일수록 코드는 더욱 깔끔해집니다.
● 함수 네이밍
- 함수가 수행하는 기능이 구체적일수록 함수 이름은 일반적이어야 합니다
- 함수명은 높은 추상화 수준에서 간략하게 작성해야 합니다.
- 예를 들어, Parse 함수를 만든다고 할 때, 그 최상위 수준의 이름은 다음과 같이 설정될 수 있습니다.
func Parse(filepath string) (Config, error) {
switch fileExtension(filepath) {
case "json":
return parseJSON(filepath)
case "yaml":
return parseYAML(filepath)
case "toml":
return parseTOML(filepath)
default:
return Config{}, ErrUnknownFileExtension
}
}
- fileExtension은 좀 더 구체적입니다.
- 최고 수준의 추상화에서 구체적인 이름을 작성한다면 읽기가 어렵습니다.
DetermineFileExtensionAndParseConfigurationFile
func fileExtension(filepath string) string {
segments := strings.Split(filepath, ".")
return segments[len(segments)-1]
}
● 변수 네이밍
- 변수명의 경우 함수와 달리 구체적이어야 합니다.
- 변수의 범위가 작아질수록 해당 변수가 무엇을 나타내는지 명확해지기 때문입니다.
- 함수의 범위가 작을 경우 변수명을 더 작게 사용할 수 있습니다.
- b라는 변수의 범위가 너무 작아서 무엇을 나타내는지 알 수 있습니다.
func PrintBrandsInList(brands []BeerBrand) {
for _, b := range brands {
fmt.Println(b)
}
}
- 범위가 커진다면 구체적인 이름으로 선언하는 것이 좋습니다.
func BeerBrandListToBeerList(beerBrands []BeerBrand) []Beer {
var beerList []Beer
for _, brand := range beerBrands {
for _, beer := range brand {
beerList = append(beerList, beer)
}
}
return beerList
}
- 짧은 변수와 긴 변수를 섞어 사용하여 일관적이지도 않아 더 지저분한 코드가 됩니다.
func BeerBrandListToBeerList(b []BeerBrand) []Beer {
var bl []Beer
for _, beerBrand := range b {
for _, beerBrandBeerName := range beerBrand {
bl = append(bl, beerBrandBeerName)
}
}
return bl
}
☻클린 함수
● 함수 길이
- 함수를 가능한 짧게 만드는 것이다.
- 짧은 함수(go에서는 일반적으로 5~8줄)를 작성하여 자연스럽게 읽는 코드를 만들 수 있습니다.
var (
NullItem = Item{}
ErrInsufficientPrivileges = errors.New("user does not have sufficient privileges")
)
func GetItem(ctx context.Context, json []bytes) (Item, error) {
order, err := NewItemFromJSON(json)
if err != nil {
return NullItem, err
}
if !GetUserFromContext(ctx).IsAdmin() {
return NullItem, ErrInsufficientPrivileges
}
return db.GetItem(order.ItemID)
}
- 더 작은 함수를 사용하면 들여 쓰기 지옥도 제거됩니다.
- 들여쓰기 지옥은 다른 개발자가 코드 흐름을 이해하기 어렵게 만듭니다.
- if문이 확장되면 어떤 문이 무엇을 반환하는지 파악하기 어렵습니다.
func GetItem(extension string) (Item, error) {
if refIface, ok := db.ReferenceCache.Get(extension); ok {
if ref, ok := refIface.(string); ok {
if itemIface, ok := db.ItemCache.Get(ref); ok {
if item, ok := itemIface.(Item); ok {
if item.Active {
return Item, nil
} else {
return EmptyItem, errors.New("no active item found in cache")
}
} else {
return EmptyItem, errors.New("could not cast cache interface to Item")
}
} else {
return EmptyItem, errors.New("extension was not found in cache reference")
}
} else {
return EmptyItem, errors.New("could not cast cache reference interface to Item")
}
}
return EmptyItem, errors.New("reference not found in cache")
}
- if문의 중첩대신 최대한 빨리 반환되는 구조로 작성을 합니다.
func GetItem(extension string) (Item, error) {
refIface, ok := db.ReferenceCache.Get(extension)
if !ok {
return EmptyItem, errors.New("reference not found in cache")
}
ref, ok := refIface.(string)
if !ok {
// return cast error on reference
}
itemIface, ok := db.ItemCache.Get(ref)
if !ok {
// return no item found in cache by reference
}
item, ok := itemIface.(Item)
if !ok {
// return cast error on item interface
}
if !item.Active {
// return no item active
}
return Item, nil
}
- 함수 추출을 통해 더 작은 함수로 분할할 수 있습니다.
- 들여 쓰기 지옥은 테스트코드 작성하기도 어렵습니다.
- 함수 추출을 하게 되면 테스트코드는 더 쉽게 작성이 가능합니다.
- 전체적으로 더 많은 코드 라인이 발생하지만 코드 자체를 읽기는 훨씬 쉬워졌습니다.
- 이렇게 코드를 작성하면 한 번에 3~5줄만 읽어도 되므로 낮은 수준의 기능을 더 쉽게 이해할 수 있습니다.
func GetItem(extension string) (Item, error) {
ref, ok := getReference(extension)
if !ok {
return EmptyItem, ErrReferenceNotFound
}
return getItemByReference(ref)
}
func getReference(extension string) (string, bool) {
refIface, ok := db.ReferenceCache.Get(extension)
if !ok {
return EmptyItem, false
}
return refIface.(string), true
}
func getItemByReference(reference string) (Item, error) {
item, ok := getItemFromCache(reference)
if !item.Active || !ok {
return EmptyItem, ErrItemNotFound
}
return Item, nil
}
func getItemFromCache(reference string) (Item, bool) {
if itemIface, ok := db.ItemCache.Get(ref); ok {
return EmptyItem, false
}
return itemIface.(Item), true
}
● 함수 파라미터
- 함수의 파라미터는 하나 또는 두 개의 입력 매개변수만 포함되어야 합니다.
- 어떤 예외적인 경우에는 세 개가 허용될 수 있지만 여기서부터는 리팩토링을 고려해야 합니다.
- 이 함수는 6개의 입력 매개변수를 사용하는데, 이는 상당히 많은 수입니다.
- 주석 덕분에 코드 기능을 이해할 수 있습니다. 그렇다면 매개변수를 추가할 때마다 주석을 달아줘야 합니다.
- 주석이 없다면 네 번째, 다섯 번째 인수가 무엇을 나타내는지 확인하기 어렵습니다.
- 많은 입력 매개변수로 인해 실수로 인자의 순서가 바뀌어 에러가 발생할 수도 있습니다.
q, err := ch.QueueDeclare(
"hello", // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
q, err := ch.QueueDeclare("hello", false, false, false, false, nil)
- 이러한 입력 매개변수를 대신해 ‘옵션’(struct)으로 바꾸는 것이 좋습니다.
- 속성의 순서도 더 이상 중요하지 않으므로 입력 값의 잘못된 순서는 더 이상 문제가 되지 않습니다.
type QueueOptions struct {
Name string
Durable bool
DeleteOnExit bool
Exclusive bool
NoWait bool
Arguments []interface{}
}
q, err := ch.QueueDeclare(QueueOptions{
Name: "hello",
Durable: false,
DeleteOnExit: false,
Exclusive: false,
NoWait: false,
Arguments: nil,
})
● 가변 범위
- 변수가 전역적이고 변경 가능하다면 해당 값은 어느 부분에서나 변경될 수 있습니다.
- 범위가 큰 비전역 변수가 어떻게 문제를 일으키는지 예를 들어 보겠습니다.
- Golang scope issue(https://idiallo.com/blog/golang-scopes) 에서 가져온 코드이며 variable shadowing 문제가 발생합니다.
- val, err := doComplex()에서 val은 새로운 변수를 선언하며 첫 번째 선언된 변수와 상관이 없습니다.
func doComplex() (string, error) {
return "Success", nil
}
func main() {
var val string
num := 32
switch num {
case 16:
// do nothing
case 32:
val, err := doComplex()
if err != nil {
panic(err)
}
if val == "" {
// do something else
}
case 64:
// do nothing
}
fmt.Println(val)
}
- 이를 리팩토링 한다면 변수의 범위는 축소하여 큰 범위의 변수의 값을 이해하지 않도록 합니다.
- val은 더 이상 수정되지 않으며 범위도 축소되었습니다.
func getStringResult(num int) (string, error) {
switch num {
case 16:
// do nothing
case 32:
return doComplex()
case 64:
// do nothing
}
return "", nil
}
func main() {
val, err := getStringResult(32)
if err != nil {
panic(err)
}
if val == "" {
// do something else
}
fmt.Println(val)
}
● 변수 선언
- 변수 사용은 사용하는 곳에 최대한 가깝게 선언하여 가독성을 향상합니다.
- C 프로그래밍에서는 변수 선언을 다음과 같이 정의하는 것을 흔히 볼 수 있습니다.
func main() {
var err error
var items []Item
var sender, receiver chan Item
items = store.GetItems()
sender = make(chan Item)
receiver = make(chan Item)
for _, item := range items {
...
}
}
- 우리 뇌의 단기 기억도 용량이 제한되어 있습니다.
- 어떤 변수가 어떻게 변경이 되어가는지 추적을 하다 보면 이해하기가 더 어려워집니다.
- 따라서 변수를 가능한 사용하는 곳 가까운 곳에 선언하는 것이 좋습니다.
func main() {
var sender chan Item
sender = make(chan Item)
go func() {
for {
select {
case item := <-sender:
// do something
}
}
}()
}
- 익명 함수를 호출하여 그 결과를 저장한다면 더 나은 결과를 얻을 수 있습니다.
func main() {
sender := func() chan Item {
channel := make(chan Item)
go func() {
for {
select { ... }
}
}()
return channel
}
}
- 익명 함수를 지정함수로 변경할 수도 있습니다.
func main() {
sender := NewSenderChannel()
}
func NewSenderChannel() chan Item {
channel := make(chan Item)
go func() {
for {
select { ... }
}
}()
return channel
}
- 변수를 속성으로 사용하여 구조체를 만드는 것도 좋은 방법입니다.
- 외부에서 sender 변수를 변경할 수 없습니다.
- 사용자가 구현에 대해 걱정하지 않기 때문에, 우리는 패키지의 구현을 언제든지 변경할 수 있습니다.
type Sender struct {
sender chan Item
}
func NewSender() *Sender {
return &Sender{
sender: NewSenderChannel(),
}
}
func (s *Sender) Send(item Item) {
s.sender <- item
}
func NewSenderChannel() chan Item {
channel := make(chan Item)
go func() {
for {
select { ... }
}
}()
return channel
}
---
func main() {
sender := NewSender()
sender.Send(&Item{})
}