Go os.Exit() vs panic() — 복구 불가 상태와 즉시 종료의 선택 기준

1. 문제 정의

원문 질문: When to use os.Exit() and panic()? 출처: StackOverflow 28472922

Go에서 프로그램을 비정상적으로 끝내는 도구는 크게 두 가지입니다. os.Exit(code)과 panic(value)입니다. 둘 다 “비정상 종료"라는 점은 같지만, 동작 방식이 근본적으로 달라서 상황별로 골라 써야 합니다. 이 글은 “복구 불가 상태"와 “즉시 종료"라는 두 선택 기준을 분기표로 정리해, 내 코드에 어떤 함수가 맞는지 판단할 수 있게 돕는 것이 목적입니다.

증상 fingerprint

  • 발생 선행 조건: 프로그램 실행 중 복구 불가능한 내부 상태(불변식 위반, 범위를 벗어난 슬라이스 인덱싱, 타입 단언 실패 등)를 만났거나, 사용자 입력 오류·테스트에서 전체 종료가 필요한 상황
  • 관련 도구/프레임워크: Go 표준 라이브러리 os 패키지(os.Exit), 내장 함수 panic/recover
  • 흔한 오해 1: “os.Exit도 지연(deferred) 함수가 실행된다” — 사실이 아니며, os.Exit은 지연 함수를 건너뜁니다.
  • 흔한 오해 2: “panic은 항상 프로그램을 죽인다” — recover로 잡으면 고루틴을 계속 살릴 수 있습니다.
  • 빠른 판단 기준: “종료 전에 정리(defer)를 반드시 실행해야 하는가?” → 그렇다면 panic, 아니라면 os.Exit을 후보로 고려합니다.

2. 먼저 확인할 것

코드를 수정하기 전에 다음 두 가지부터 확인하시길 권합니다.

  1. 여기서 프로그램 전체를 끝내야 하는 상황인가? 대부분의 Go 코드는 오류를 error로 반환해 호출자가 처리하게 하는 것이 기본입니다. panic/os.Exit은 그 “마지막 수단"인지 먼저 판단합니다.
  2. 종료 전에 실행돼야 하는 defer가 있는가? 리소스 정리(파일 닫기, 연결 해제, 버퍼 flush)가 필요하다면 os.Exit은 이들을 건너뛰기 때문에 안전하지 않습니다.

검증 명령(사전 점검) — 자신의 Go 환경에서 두 함수의 시그니처를 확인합니다.

1
2
3
go version
go doc os.Exit
go doc builtin.panic

참고: go doc의 출력 형식은 Go 버전에 따라 미세하게 달라질 수 있습니다. 버전과 무관하게 os.Exit이 func Exit(code int)이고 panic이 func panic(v any)인지 확인하시면 됩니다.

3. 원인별 분기표 (해결책 비교)

“무엇 때문에 종료하려는가"에 따라 선택이 갈립니다. 아래 표의 판단 순서는 상단(운영/스크립트)에서 하단(프로그래머 실수)으로 내려갈수록 panic에 가까워집니다.

상황권장 도구선택 이유
스크립트에서 종료 코드로 성공/실패를 전달하고 싶을 때os.Exit(0) 또는 os.Exit(에러코드)종료 코드를 다른 프로그램이 읽어 후속 처리를 할 수 있음
실행이 잘 끝나서(예: -h 도움말 출력 후) 그대로 종료os.Exit(0)정상 종료 코드 반환
사용자 입력 오류처럼 예상 가능한 오류로 정리 종료os.Exit(에러코드)“나쁜 종료 코드는 사용자를 위한 것” — 카오스와 무관한 통제된 종료
데드락, 슬라이스 범위 초과, nil 포인터 등 프로그래머 실수panic스택 추적을 남겨 잘못된 코드 위치를 찾게 해 줌
불변식(전제 조건) 위반처럼 절대 일어나면 안 되는 상태panic“panic은 나(개발자)를 위한 것” — 프로덕션 전에 잡히는 버그임
defer로 반드시 정리한 뒤 종료해야 할 때panic (+ 필요시 recover)지연 함수 실행 후 스택 해제가 가능
테스트에서 이후 테스트가 전부 실패할 게 확실할 때os.Exit(1) (일부 사례)즉시 중단해 유의미 없는 연쇄 실패를 줄임 (단, 테스트 간 독립성이 원칙)

원인 후보 매트릭스

원인 후보확인 방법맞는 경우의 증상해결 방향
defer 정리를 건너뛰고 싶지 않다호출부에 defer 블록이 있는지 확인os.Exit 사용 시 파일/연결이 닫히지 않음panic 사용
종료 코드를 제어하고 싶다셸에서 echo $? / 실행 결과 확인os.Exit(3) 후 종료 코드가 3으로 남음os.Exit(코드) 사용
스택 추적이 필요하다panic 실행 시 로그 확인스택 트레이스가 출력됨panic 사용
error 반환으로 처리 가능한 한정 오류함수 시그니처가 error 반환을 이미 지원하는지 확인오류를 호출자가 처리해야 하는 상황panic/os.Exit 미사용 — error 반환
마지막에 defer 실행 후 되살아나야 함recover 필요 여부크래시 후 정리 후 계속 동작해야 하는 일부 요청panic + defer + recover

위 표의 “해결 방향"은 일반적인 점검 기준이며, 실제 적용은 프로젝트의 오류 처리 규약(에러핸를 error로 반환 vs 패닉으로 표현)에 따라 달라질 수 있습니다.

4. 상황별 해결책

4-1) os.Exit으로 종료 코드 제어하기 (예상 가능한 종료)

동일한 실행에서 종료 코드를 설정해 셸이 결과를 판단하게 하려면 os.Exit을 사용합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "flag"
    "fmt"
    "os"
)

func main() {
    help := flag.Bool("h", false, "도움말 출력")
    flag.Parse()

    if *help {
        fmt.Println("사용법: mytool [옵션]")
        os.Exit(0) // 정상 종료, 코드 0
    }

    // 사용자 입력 오류 같은 예상 가능한 오류
    if flag.NArg() == 0 {
        fmt.Fprintln(os.Stderr, "인수가 필요합니다")
        os.Exit(2) // 오류 종료 코드: 셸이 읽을 수 있음 (0이 아닌 값)
    }
}

이처럼 os.Exit(0)은 “정상 종료”, `os.Exit(비영)“은 “오류 종료"를 도구의 계약으로 만들 수 있습니다.

4-2) panic으로 불가리 상태 표현하기 (프로그래머 실수)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

func assertPositive(v int) {
    if v <= 0 {
        panic("불변식 위반: 값은 항상 양수여야 합니다") // 스택 추적 출력
    }
}

func main() {
    assertPositive(-5)
}

panic은 실행 지점의 **스택 추적(stack trace)**을 출력해 어느 코드 줄이 전제 조건을 깨뜨렸는지 추적할 수 있게 합니다. “panic은 나(개발자)를 위한 것"이라는 표현처럼, 프로덕션에 서버 오기 전에 잡혀야 하는 로직 버그에 적합합니다.

4-3) defer 정리가 필요하고, 가능하면 되살아나야 할 때

리소스를 반드시 정리한 뒤 종료하고 싶다면 panic을 선택하고, HTTP 요청처럼 크래시를 요청 단위로 격리해야 한다면 recover를 함께 사용합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func worker(name string) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("요청 %s 복구: %v\n", name, r)
        }
    }()
    defer fmt.Println("정리: ", name) // 항상 실행됨
    panic("내부 버그")
}

func main() {
    worker("A")
    worker("B")
    fmt.Println("프로세스 종료")
}

검증 환경: go1.18.1 linux/arm64 (Ubuntu 22.04). 아래 출력은 go run worker.go를 실제 실행해 확인한 결과입니다.

1
2
3
4
5
6
정리:  A
요청 A 복구: 내부 버그
정리:  B
요청 B 복구: 내부 버그
프로세스 종료
(exit code 0)

위 출력은 worker가 첫 panic 후 recover로 복구되고 다시 worker("B")가 실행되는 흐름입니다. 실제 실행에서 정리: 두 줄과 프로세스 종료까지 모두 출력되어, defer 정리 → recover 복구 → 고루틴/함수가 계속 동작하는 것을 확인했습니다. 만약 os.Exit을 썼다면 정리: 라인이 출력되지 않고 프로세스가 즉시 죽습니다 (아래 4-4 참고).

4-4) 실패한 해결책 — os.Exit으로 defer 정리를 잃은 사례

os.Exit을 “종료를 빠르게 하는 보편적인 도구"처럼 무분별하게 쓰면, 같은 함수에 defer로 예약한 정리가 실행되지 않는 실패를 만듭니다. 아래 코드는 흔히 저지르는 잘못된 방식입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"fmt"
	"os"
)

func main() {
	defer func() {
		fmt.Println("정리: flush & close")
	}()
	fmt.Println("작업 완료")
	os.Exit(0)
	fmt.Println("이 줄은 실행되지 않음")
}

검증 환경: go1.18.1 linux/arm64 (Ubuntu 22.04). 실제 실행 결과:

1
2
작업 완료
(exit code 0)

정리: flush & close가 출력되지 않은 것을 확인할 수 있습니다. os.Exit은 즉시 프로세스를 종료하므로 같은 함수의 defer를 건너뛰고, 종료 코드가 0(정상)으로 남아 외부에서는 “정리가 완료된 정상 종료"로 오인됩니다. 해결책으로 제시한 예제(3-2, 4-3)에서 worker가 recover 없이 크래시하고, 이를 “깔끔한 종료"처럼 보이게 하려고 os.Exit으로 바꾸는 것이 전형적인 오해입니다.

왜 권장하지 않는가: 종료 경로에 반드시 실행돼야 하는 정리(버퍼 flush, 파일/연결 닫기, 로그 후킹)가 있다면, os.Exit은 그 정리를 조용히 지웁니다. recover로 복구할 일이 없고 종료 코드 제어가 필요해도, 정리를 수행한 뒤 종료하도록 감싸야 합니다. 정리 실행 + 종료 코드를 모두 원하면 panic + defer(정리) + recover(독자 종료) 조합을 씁니다. 아래를 실제 실행으로 확인했습니다 (go run panic_exit.go):

1
2
3
정리: flush (defer로 실행됨)
복구 후 명시적 종료: 치명적 불변식 위반
(exit code 1)

defer가 실행되고 나서 recover 안에서 os.Exit(1)로 종료 코드를 제어하므로, “정리는 됐지만 오류 코드는 남기는” 종료가 가능합니다. 종료 코드 반환 자체를 포기하고 스택 추적을 원한다면 panic을 그대로 두면 됩니다.

5. 선택 이유 — 왜 이 해결책이 맞는가

두 함수의 결정적 차이점을 표로 다시 정리하면 다음과 같습니다.

특징os.Exitpanic
지연(deferred) 함수 실행실행하지 않음실행함 (스택 해제 중)
종료 코드 지정가능 (os.Exit(code))불가 (통상 2, 스택 추적만)
스택 추적없음출력됨
복구 가능 여부불가 (즉시 프로세스 종료)recover로 복구 가능
권장 사용처스크립트 종료 코드, 예상 가능한 오류, 도움말 출력 후불변식 위반, 프로그래머 실수

버전별 차이 — Go 1.x에서 os.Exit/panic 동작은 안정적

두 함수의 핵심 동작(os.Exit은 defer를 건너뛰고 즉시 종료, panic은 defer를 실행하며 스택을 풀고 종료 코드 2)은 Go 1.x 전반에서 안정적으로 유지됩니다. 이 글의 실행 검증은 go1.18.1 linux/arm64에서 수행했지만, 버전에 따라 달라지는 세부 차이를 정리하면 다음과 같습니다.

항목Go 1.18 이전Go 1.18+영향
panic 시그니처 표기func panic(v interface{})func panic(v any) (any는 interface{} 별칭)go doc builtin.panic 출력 표기가 바뀜 — 동작 동일
os.Exit 동작·종료 코드 계약func Exit(code int), defer 미실행동일변화 없음
종결(terminating) 문 인정panic·log.Panic은 인정, os.Exit은 미인정동일“missing return” 컴파일 오류 회피에 영향
기본 go vet (os.Exit in main)검출 안 함(기본 vet)검출 안 함운영 점검이 vet에 의존하기 어려움

여기서 주의할 실무 포인트 두 가지를 짚어 둡니다.

  • log.Fatalf/log.Fatal은 내부적으로 os.Exit(1)을 호출합니다. 따라서 log.Fatal 역시 defer 정리를 건너뜁니다. 반면 log.Panicf/log.Panic은 로그를 남긴 뒤 panic을 호출하므로 defer 정리가 실행되고 스택 추적도 남습니다. 이 구분은 Go 버전과 무관하게 안정적입니다.
  • go doc os.Exit에 따르면 “portability 를 위해 status code를 [0, 125] 범위로 두라는” 권고가 있습니다. 이것도 과거부터 유지된 지침입니다.

결론적으로 os.Exit vs panic의 선택 자체는 Go 버전에 좌우되지 않습니다. 버전에 따라 달라지는 것은 go doc 출력의 타입 표기(interface{} → any) 정도이며, 선택 기준(복구 가능성 × defer 필요 여부)은 모든 1.x 버전에서 동일합니다.

주의: panic과 끝나는 함수의 컴파일 동작

한 가지 기술적 함정이 있습니다. 반환값이 있는 함수에서 모든 분기가 return/panic으로 끝나야 하는 경우, panic(과 내부적으로 panic을 호출하는 log.Panic)은 종결(terminating) 문으로 인정되지만 os.Exit(과 내부적으로 os.Exit(1)을 호출하는 log.Fatal)은 종결 문으로 인정되지 않아 컴파일러가 “missing return at end of function” 오류를 낼 수 있습니다.

1
2
3
4
5
6
func div(a, b int) int {
    if b == 0 {
        panic("division by zero") // ✅ 컴파일 허용 (종결 문으로 인정)
    }
    return a / b
}

이 때문에 ‘모든 분기가 종료되는 함수’ 안에서는 os.Exit보다 panic이 더 자연스럽게 들어맞는 경우가 있습니다. (환경·버전에 따라 다를 수 있으므로, 코드 리뷰 시 실제 컴파일 결과로 확인하시길 권장합니다.)

검증 명령(해결 후)

수정 후 아래 명령으로 컴파일·실행·동작을 확인합니다.

1
2
3
4
5
6
7
8
# 컴파일 검증
go build ./...

# 예제 실행
go run main.go

# 테스트 스위트 (테스트에서 os.Exit/panic 사용 시)
go test ./...
  • go test 통과 여부로 오류 처리 경로가 회귀하지 않았는지 확인합니다.
  • os.Exit을 테스트에서 사용했다면, 해당 테스트가 다른 테스트와 독립적인지(단순히 코드 의존이 아닌지) 한 번 더 점검합니다.

6. 예방 조치 & DevTrace 결론

재발 방지 체크

  • 기본은 error 반환: 대부분의 오류는 panic/os.Exit이 아니라 함수가 error를 돌려주게 설계합니다.
  • recover는 필요한 곳에만: 전역적으로 남용하면 버그를 숨길 수 있으므로, HTTP 핸들러 진입점처럼 “요청 단위 격리"가 의미 있는 지점에만 둡니다.
  • os.Exit 사용 전에 defer 점검: 종료 경로에 반드시 실행해야 할 정리가 있다면 panic(또는 정리 후 명시적 종료)을 선택합니다.
  • 운영 로그 모니터링: panic의 스택 추적이 로그에 쌓이도록 하고, “panic은 프로덕션 전에 잡힐 버그"라는 관점에서 반복 발생 시 코드 결함으로 추적합니다.

DevTrace verdict

이 문제의 핵심은 “복구 가능성"이라는 축이며, panic은 지연 함수 실행과 스택 추적을 제공하고 os.Exit은 종료 코드 제어와 즉시 종료를 제공한다는 점에서, deferred 정리가 필요하면 os.Exit 대신 panic을 선택해야 한다는 판단이 이 글의 핵심입니다.


원문 출처: When to use os.Exit() and panic()? — StackOverflow StackOverflow 답변(작성자: Kota, icza, 4hens 등)의 내용을 바탕으로 독자 분석 및 일반적인 Go 오류 처리 기준을 추가해 정리했습니다. 검증 환경이나 사용자 코드 규약에 따라 판단이 달라질 수 있습니다.