일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 그라파나
- Play2 로 웹 개발
- Play2
- 스칼라 강좌
- 하이퍼레저 패브릭
- 주키퍼
- Actor
- Akka
- 엔터프라이즈 블록체인
- play 강좌
- 파이썬
- 파이썬 동시성
- 하이브리드앱
- 블록체인
- hyperledger fabric
- 파이썬 데이터분석
- play2 강좌
- 파이썬 강좌
- 플레이프레임워크
- 이더리움
- Adapter 패턴
- 스위프트
- 파이썬 머신러닝
- 스칼라 동시성
- akka 강좌
- Golang
- Hyperledger fabric gossip protocol
- 스칼라
- 안드로이드 웹뷰
- CORDA
- Today
- Total
HAMA 블로그
[Golang] recover 는 언제 사용하나? 본문
Go언어는 예외처리가 없으며, 에러에 대해서 가장 가까운 위치에서 명시적으로 체크하고 넘어가는 것을 권장하는 언어 이다. 이 행위를 강제하진 않기 때문에 좀 더 단순하나 문제가 발생 할 소지를 없애기 위해 에러 체킹을 강제화 하는 언어(OCaml,Scala등)에 비해 안정성은 좀 떨어진다고 말 할 수도 있겠다. 하지만 이런 것은 팀의 코딩컨벤션으로 항상 체크하고 넘어가면 되는 문제로 생각 할 수도 있을 것이다.
참고로 예외처리는 굉장히 어려운 주제이며 예외 처리에 대한 6가지 화두 이 글을 통해서 말한바 있다.
Panic 예제
func test () int {
arr := [] int {}
element := arr[5]
return element
}
func main() {
test()
fmt.Println("smooth exit")
}
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/
'Go' 카테고리의 다른 글
Go 언어에서 포인터는 언제 사용 해야 하나? (0) | 2019.10.11 |
---|---|
go microservices (0) | 2019.02.28 |
고성능을 위한 GO (0) | 2019.02.13 |
[이더리움에서 배우는 Go언어] select 의 거의 모든 패턴들 (1) | 2019.02.08 |
[이더리움으로 배우는 GO언어] 자료구조 & 컬렉션 (0) | 2019.01.15 |