• yundream
  • 2018-01-22 03:57:13
  • 2018-01-21 13:21:54
  • 48411

Contents

Go는 흐름을 제어하기 위한 일반적인 메커니즘인 if, for, switch, goto를 제공한다. 이 외에 고루틴(goroutine)을 실행하기 위한 go 문이 있다. 이외에도 defer, panic, recover이 있다. 여기에서는 defer를 다룬다.

defer

defer는 go에서 제공하는 흐름 제어 메커니즘(Control flow mechanism) 이다. defer문을 이용해서 기술한 함수는 함수 호출 목록에 푸시(Push)되고, 호출한 함수가 반환된 후에 실행된다. defer 여러 목적으로 사용 할 수 있는데, 일반적으로 열려있는 함수를 닫거나 Mutex 잠금 해제 , 열린 파일 닫기등의 작업을 할 수 있다. 아래 코드를 보자.

func BillCustomer(c *Customer) error {
    c.mutex.Lock()
    defer c.mutex.Unlock()
    
    if err := c.Bill(); err != nil {
        return err
    }
    
    if err := c.Notify(); err != nil {
        return err
    }
    
    // ... do more stuff ...
    
    return nil
}
BillCustomer 함수가 리턴될 때, mutex 잠금을 해제하도록 했다. 함수의 반환 지점이 여러 개일 때, 각각의 반환 시점에 mutex 잠금을 해제할 필요가 없으므로 코드를 간결하게 유지 할 수 있다.

Java 언어의 경우 try/catch 의 finally 블럭이 defer와 비슷한 일을 할 수 있겠지만 그 보다 훨씬 직관적이고 단순하다.

여러 개의 defer를 사용하기

하나의 함수에서 두 개 이상의 defer를 사용 할 수 있다. defer를 호출하면 지연 호출될 함수의 목록이 만들어지며, 호출한 함수가 반환하기 전에 실행된다.

package main

import (
	"fmt"
)

func main() {
	defer func() {
		fmt.Println("Defer 1")
	}()
	defer func() {
		fmt.Println("Defer 2")
	}()
	
	fmt.Println("I'm not deferred!")
}
		

아마도 "defer 1"이 먼저 실행되는 걸 예상하겠지만, 역순으로 "defer 2"가 먼저 실행된다. 위 코드를 실행하면 결과는 아래와 같다.
# go run defers.go
I'm not deferred!
defer 2
defer 1
defer가 역순으로 실행된 다는 것을 알고 있다고 하더라도, 하나의 함수에 여러 개의 defer 문을 사용하는 좋은 습관은 아니다. 함수가 끝날 때 어떤 일이 발생할지를 추적하는게 어려워질 수 있기 때문이다.

아래 코드를 실행해 보면, defer의 이러한 성질을 확실히 확인할 수 있다.
package main

import "fmt"

func main() {
    for i:=0; i < 4; i++ {
        defer fmt.Println(i)
    }
}
	
		

defer를 사용할 때

앞서 말 했듯이 defer는 리소스를 해제하는데 특히 유용하다.

defer를 사용하는데 유일한 문제점은 많은 비용이 드는 작업으로 응용 프로그램의 성능이 저하 될 수 있다는 것이다.

소프트웨어 개발은 Trade Off 의 연속이다. 가독성을 높이면 성능이 떨어지는 경우는 자주 볼 수 있다. defer의 경우 성능저하는 나노초 단위로 측정된다. 하지만 이 것을 사용함으로써 얻는 코드의 간결함, 유지 보수성을 생각해보면 사용하는게 훨씬 낫다. 물론 나노초단위의 누적된 성능저하가 문제를 일으킬 수 있는 소프트웨어라면 defer 사용을 피해야 할 것이다.

내 의견은 사용 할 수 있다면 defer를 사용하라는 거다. 아래와 같은 코드라면 defer를 사용 할 필요가 없을 테다.
func Add(i int) int {
    mu.Lock()
    defer mu.Unlock()
    val += i
    return val
}
아래와 같이 작성하면 된다.
func Add(i int) {
    mu.Lock()
    val += i
    mu.Unlock()
    return val 
}
그러나 아래와 같이 두 개 이상의 반환문이 들어간다면 defer를 사용하는게 좋다.
func Divide(i int) error {
    mu.Lock()
    defer mu.Unlock()
    
    if i == 0 {
        return errors.New("Can't divide by zero!")
    }
    
    val /= i
    return nil
}

마무리

defer를 사용할지는 개인의 선호에 달려있다. 다만 일관성을 유지할 필요는 있다. defer를 사용하지 않는 다면, 다른 개발자들은 해당 코드를 좀 더 주의 깊게 볼 것이다. defer를 사용하고 있다면, 개발자는 자원의 해제 같은 것들 보다는 코드의 다른 부분에 주의를 기울일 것이다. 어떤 함수에서는 defer를 사용하고 다른 함수에서는 그렇지 않다면, 코드를 보는 개발자는 혼란스러워 할 것이다.

코드에 일관성이 없을 경우, defer 로 connect를 해제하고 다른 에러처리에서도 connect를 해제하는 식으로 두번 자원을 해제하는 코드가 들어 갈 수도 있다. 이런 코드는 잘 작동 할 수도 있지만 nil을 참조하면서 뻗어버릴 수도 있다. 일관성을 유지하도록 주의해야 한다.

참고