관리 메뉴

HAMA 블로그

[Golang] recover 는 언제 사용하나? 본문

Go

[Golang] recover 는 언제 사용하나?

[하마] 이승현 (wowlsh93@gmail.com) 2019. 2. 22. 19:51


Go언어는 예외처리가 없으며, 에러에 대해서 가장 가까운 위치에서 명시적으로 체크하고 넘어가는 것을 권장하는 언어 이다. 이 행위를 강제하진 않기 때문에 좀 더 단순하나 문제가 발생 할 소지를 없애기 위해 에러 체킹을 강제화 하는 언어(OCaml,Scala등)에 비해 안정성은 좀 떨어진다고 말 할 수도 있겠다. 하지만 이런 것은 팀의 코딩컨벤션으로 항상 체크하고 넘어가면 되는 문제로 생각 할 수도 있을 것이다. 

참고로 예외처리는 굉장히 어려운 주제이며 예외 처리에 대한 6가지 화두 이 글을 통해서 말한바 있다.

Panic 예제

func test () int {
   arr := [] int {}
   element := arr[5]
   return element
}

func main() {
   test()
   fmt.Println("smooth exit")

}
test함수에서 인덱스 범위 밖의 배열을 참조 했으므로, panic이 발생하여 정상종료되지 못하고 뻣어버리는데 
package main

import (
   "fmt"
)

func test() int {
   arr := [] int {}
   defer func() {
      v := recover()
      fmt.Println("recovered:", v)
   }()

   x := arr[5]
   return x
}

func main() {
   result := test()
   fmt.Printf("smooth exit %d", result)

}

이렇게 내부에 recover 를 해주면 panic 이 상쇄되며 리턴값으로 타입의 기본값이 들어가게 된다.

package main

import (
   "fmt"
   "runtime/debug"
)

func r() {
   if r := recover(); r != nil {
      fmt.Println("Recovered", r)
      debug.PrintStack()
   }
}

func a() {
   defer r()
   n := []int{5, 7, 4}
   fmt.Println(n[3])
   fmt.Println("normally returned from a")
}

func main() {
   a()
   fmt.Println("normally returned from main")
}

문제가 발생 했을 당시의 스택트레이스를 확인 할 수도 있을 것이다.
근데 여기서 고민이 그럼 모든 함수에 저런것을 해야한다고 생각하면 정내미가 떨어 질 수 있을 것이다.

그럼 언제 고의적 panic을 해줘야하나? 

1. 에러인가 예외인가?
2. 죽일 것 인가? 살릴 것 인가? 
3. 가까운 곳에서 즉시 처리할 것인가? 상위로 위임할 것인가?
4. 강제적으로 처리 시킬 것인가? 아닌가?  (체크드 / 언체크드) 
5. 모두 되돌릴 것인가? 이미 벌어진 일은 무시,포기할 것인가? 
6.  리턴으로 처리 또는 예외 개념 도입  

예전에 작성한 예외처리에 대한 화두 6가지를 보면, 2번이 이것에 해당되는 내용일 거 같다. 내용은 아래와 같은데 

최근 특정 솔루션에서 나는 예외에 대해 고민하길 포기해버렸다.  따라서 예외를 단순히
 프로그램이 죽지 않게 하기위해 예외처리를 한다. 잘 죽는 부분은 이렇다.

* JSON 변환을 하는 부분  
* 소켓 처리부분, 대부분의 io 부분. (외부의 파일이 어떻게 될지 모름)
* 널포인터가 없을거라고 확신하지 못하는 부분 모두 (이건 좀 너무 광범위한데 C++ 로 개발할때  ASSERT 로 일단 도배한다.) 
* 여러 쓰레드가 접근되는 부분   
* 사용자가 주는 값에 대해 신뢰해선 안되는 부분 
* 컬렉션 처리중 먼가 냄새가 나는 부분

뭐 더 많겠지만 요즘 내가 짜는 프로그램은 이런거 같다. 이렇게 써 놓고 보니 예외를 예외로 생각하지 않고 프로그램 시퀀스의 일부분으로 좀 더 오버해서 즉 if 문 처럼 생각하고 쓰는게 아닌가 싶기도 한데 좀 더 고찰해 볼 시간은 없었다.

암튼 위의 리스트에서 느껴지듯이 쓰레드,IO 등에서 문제가 생길 소지가 많은데 그래서 웹개발/서버개발에 있어서는 죽이지 않기 위해 예외처리를 하는 경우가 많은거 같다. "일처리 프로세스 과정이 복잡하지 않고 하나의 시퀀스에 대해 완료하지 못하더라도 큰 타격을 받지 않으며 어차피  다음에 시도하면 되는 것은 그냥 생활의 일부분이야 ~~그런 사소한 이유로 프로그램을 죽일 순 없어. 프로그램 죽는것은 내가 죽는것" 이런 마음가짐이었던 거다.

근데 그냥 빨리 죽여서 문제를 빨리 찾으려는 응용프로그램도 많으며 예러가 발생해선 절대 안되는 프로그램이 있다. 공장이나 의료쪽에서 사용되는 응용프로그램들이 그러한데 이런 프로그램에서는 저 위의 예처럼 모든 경우에 대해 예외처리를 하여 자신도 모르게 넘어가면 안된다. 예외가 로그에 남겠지만 그것만으로 부족하다. 개발자에게  문제가 생겼다는것을 즉시 각인 시켜야한다. 프로그램을 그 즉시 죽임으로써 말이다. 그리고 최대한 에러/예외가 나지 발생하지  않도록 설계를 해야하며 오류를 잡아야한다. 사실 이런 프로그램 경우 웹이나 게임처럼 외부와의 잦은 커뮤니케이션이 없고 안정된 하드웨어 등 시스템  때문에  예외가 발생할 가능성은 크게 적다.  

뭐 이런 글 이었는데 여기서 적용 할 수 있을 거 같다. 즉 계속 살아 있으면 안될때 Panic을 일으켜야 한다는 것이다. 먼가 문제가 있어도 걍 넘기거나 다시 시도해도 되는 많은 경우에는 필요 없을 것이다.  즉 초기에 중요한 설정을 하는데 실패 했을 경우 라든지, 중간에 입력된 상태를 기반으로 추후의 작업을 오래 동안 해야 할 경우. 중간에 입력된 값이 nil이라면 결과에 영향을 크게 미칠 것이고, 그 결과에 따라서 커다란 재산상/신체상 위협이 가해질 경우. 


그럼 언제 recover 를 해줘야하나? 

위와 다음과 같다고 보면 될 거 같다. Panic을 반드시 일으켜야 하는 상황이 아니면서 아래처럼 예외가 발생될 확률이 매우 높은곳으로 보면 될 거 같다.

* JSON 변환을 하는 부분  
* 소켓 처리부분, 대부분의 io 부분. (외부의 파일이 어떻게 될지 모름)
* 널포인터가 없을거라고 확신하지 못하는 부분 모두 (이건 좀 너무 광범위한데 C++ 로 개발할때  ASSERT 로 일단 도배한다.) 
* 여러 쓰레드가 접근되는 부분   
* 사용자가 주는 값에 대해 신뢰해선 안되는 부분 
* 컬렉션 처리중 먼가 냄새가 나는 부분

아래는 다양한 경우에 대한 예를 살펴보자. (해당 예제는 아래 레퍼런스에서 가져왔다)

예1) 프로그램 종료되는 것을 피하자.

굉장히 일반적인 경우의 예로  클라이언트-서버 동시성 프로그래밍에서 문제가 생겼을때 그냥 회피하는 방식이다.

package main

import "errors"
import "log"
import "net"

func main() {
	listener, err := net.Listen("tcp", ":12345")
	if err != nil {
		log.Fatalln(err)
	}
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Println(err)
		}
		// Handle each client connection in a new goroutine.
		go ClientHandler(conn)
	}
}

func ClientHandler(c net.Conn) {
	defer func() {
		if v := recover(); v != nil {
			log.Println("capture a panic:", v)
			log.Println("avoid crashing the program")
		}
		c.Close()
	}()
	panic(errors.New("just a demo.")) // a demo-purpose panic
}

클라이언트 하나 죽었다고, 모두다 죽으면 안되니깐~

예2) 고루틴 충돌시 자동적으로 재시작하기

고루틴에서 Panic이 감지되었을때 새로운 고루틴을 만들 수 있다.

package main

import "log"
import "time"

func shouldNotExit() {
	for {
		time.Sleep(time.Second) // simulate a workload
		// Simulate an unexpected panic.
		if time.Now().UnixNano() & 0x3 == 0 {
			panic("unexpected situation")
		}
	}
}

func NeverExit(name string, f func()) {
	defer func() {
		if v := recover(); v != nil { // a panic is detected.
			log.Println(name, "is crashed. Restart it now.")
			go NeverExit(name, f) // restart
		}
	}()
	f()
}

func main() {
	log.SetFlags(0)
	go NeverExit("job#A", shouldNotExit)
	go NeverExit("job#B", shouldNotExit)
	select{} // blocks here for ever
} 

예3) 롱점프 Statements 시뮬레이션을 위한panic/recover Calls

일반적으로 이러한 방법은 권장되지 않지만 때때로 우리는 crossing-function long jump statements 과 crossing-function returns 를 시뮬레이션하는 방법으로 패닉/회복을 사용할 수 있다. 이 방법은 코드 가독성과 실행 효율성 모두에 해를 끼친다. 유일한 이점은 때때로 코드를 덜 장황하게 보이게 할 수 있다는 것이다.

다음 예에서 내부 함수에 패닉이 발생하면 실행이 지연된 호출로 점프한다.

package main

import "fmt"

func main() {
	n := func () (result int)  {
		defer func() {
			if v := recover(); v != nil {
				if n, ok := v.(int); ok {
					result = n
				}
			}
		}()

		func () {
			func () {
				func () {
					// ...
					panic(123) // panic on succeeded
				}()
				// ...
			}()
		}()
		// ...
		return 0
	}()
	fmt.Println(n) // 123
}

예4) 에러체크를 줄이기 위한  panic/recover 콜 

package main

import "fmt"

func doTask(n int) {
	if n%2 != 0 {
		// Create a demo-purpose panic.
		panic(fmt.Errorf("bad number: %v", n))
	}
	return
}

func doSomething() (err error) {
	defer func() {
		// The second optional return must be present here,
		// otherwise, the assertion will panic if no errors occur.
		err, _ = recover().(error)
	}()

	doTask(22)
	doTask(98)
	doTask(100)
	doTask(53)
	return nil
}

func main() {
	fmt.Println(doSomething()) // bad number: 53
}

위 코드는 아래보다는 덜 장황하다.

func doTask(n int) error {
	if n%2 != 0 {
		return fmt.Errorf("bad number: %v", n)
	}
	return nil
}

func doSomething() (err error) {
	err = doTask(22)
	if err != nil {
		return
	}
	err = doTask(98)
	if err != nil {
		return
	}
	err = doTask(100)
	if err != nil {
		return
	}
	err = doTask(53)
	if err != nil {
		return
	}
	return
}

레퍼런스:

https://go101.org/article/panic-and-recover-use-cases.html
https://golangbot.com/panic-and-recover/

Comments