
Go 언어는 try-catch나 throw 같은 예외(Exception) 구문을 제공하지 않습니다. 대신 단순하고 예측 가능한 흐름 제어를 위해 defer, panic, recover 세 가지 키워드를 사용합니다. 이는 Go의 핵심 철학 — 단순함(Simplicity), 명확성(Clarity), 가독성(Readability) — 을 그대로 반영한 설계입니다.
목차
- 예외 처리의 철학: 왜 try-catch가 없는가
- defer: 지연 실행과 자원 정리
- panic: 런타임 에러의 명시적 발생
- recover: 패닉에서의 복구
- 세 키워드의 실행 순서와 동작 원리
- 실무 예제: 안전한 파일 처리
- 다른 언어와의 비교 분석
- 결론: 단순함이 만드는 안정성
예외 처리의 철학: 왜 try-catch가 없는가
C++, Java, Python 등 대부분의 언어는 try-catch 구문으로 예외를 처리합니다. 하지만 Go는 의도적으로 예외(Exception) 메커니즘을 제거하고 명시적 오류 반환을 선택했습니다.
Go 설계자의 철학
"Errors are values." — Rob Pike
Go에서 오류(Error)는 예외적인 흐름이 아니라 하나의 값(Value)입니다. 즉, 함수는 오류를 반환하고 호출자는 이를 명시적으로 처리해야 합니다.
f, err := os.Open("config.json")
if err != nil {
log.Fatal(err)
}
defer f.Close()
이 방식은 코드가 다소 길어지더라도, 프로그램의 모든 오류를 명확하게 드러내는 장점이 있습니다.
예외 없는 설계의 이점
1. 예측 가능한 제어 흐름
- 예외로 인한 예상치 못한 점프가 없음
- 모든 오류 경로가 명시적으로 표현됨
2. 성능 최적화
- 예외 처리 오버헤드 제거
- 스택 언와인딩 비용 없음
3. 코드 가독성
- 오류 처리 로직이 명확히 보임
- 함수의 실패 가능성이 시그니처에 드러남
defer: 지연 실행과 자원 정리
기본 개념
defer 키워드는 "이 함수가 종료될 때 실행하라"는 의미입니다. defer로 지정된 문장은 현재 함수가 return되기 직전에 실행됩니다.
package main
import "os"
func main() {
f, err := os.Open("example.txt")
if err != nil {
panic(err)
}
// main 함수 마지막에 파일 close 실행
defer f.Close()
// 파일 읽기
data := make([]byte, 1024)
f.Read(data)
println(string(data))
}
위 예제에서 defer f.Close()는 main 함수가 끝날 때 실행됩니다. 따라서 이후 코드에서 에러가 발생하더라도 항상 파일이 닫히는 것을 보장합니다.
defer의 특징
| 특성 | 설명 | 예시 |
|---|---|---|
| 실행 시점 | 함수 종료 직전 (return 직전) | 리소스 정리 |
| 실행 순서 | 후입선출(LIFO) | 마지막 defer가 먼저 실행 |
| 주 사용처 | 파일 닫기, 락 해제, 리소스 정리 | defer f.Close() |
| 유사 개념 | C#/Java의 finally 블록 | 정리 작업 보장 |
defer 실행 순서
func multipleDefers() {
defer println("first")
defer println("second")
defer println("third")
}
// 출력:
// third
// second
// first
defer의 고급 사용법
1. 함수 파라미터 캡처
func deferExample() {
x := 1
defer fmt.Println(x) // x=1이 캡처됨
x = 2
defer fmt.Println(x) // x=2가 캡처됨
}
// 출력: 2, 1
2. 명명된 반환값 수정
func namedReturn() (result int) {
defer func() {
result++
}()
return 1
}
// 반환값: 2 (defer에서 1 증가)
3. 뮤텍스 자동 해제
var mu sync.Mutex
func safeOperation() {
mu.Lock()
defer mu.Unlock() // 함수 종료 시 반드시 해제
// 크리티컬 섹션
criticalWork()
}
panic: 런타임 에러의 명시적 발생
panic의 핵심 개념
Go의 내장 함수 panic()은 프로그램을 즉시 중단시키는 함수입니다. 현재 함수의 실행을 멈추고, 등록된 defer들을 모두 실행한 뒤 상위 함수로 전파됩니다.
package main
import "os"
func main() {
// 잘못된 파일명을 사용
openFile("invalid-file.txt")
// openFile() 안에서 panic이 실행되면
// 아래 println 문장은 실행되지 않음
println("Done")
}
func openFile(filename string) {
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer f.Close()
// 파일 처리 로직...
}
panic 사용 시점
적절한 사용
- 프로그램이 계속 실행될 수 없는 치명적 오류
- 라이브러리 내부의 불변성 위반
- 개발 중 디버깅 목적
부적절한 사용
- 일반적인 오류 상황
- 사용자 입력 검증 실패
- 네트워크 오류 등 예상 가능한 실패
panic의 전파 과정
func level1() {
defer fmt.Println("level1 defer")
level2()
fmt.Println("level1 after level2") // 실행되지 않음
}
func level2() {
defer fmt.Println("level2 defer")
level3()
fmt.Println("level2 after level3") // 실행되지 않음
}
func level3() {
defer fmt.Println("level3 defer")
panic("something went wrong")
fmt.Println("level3 after panic") // 실행되지 않음
}
// 출력:
// level3 defer
// level2 defer
// level1 defer
// panic: something went wrong
recover: 패닉에서의 복구
recover의 핵심 개념
recover()는 panic으로 인해 발생한 패닉 상태를 복원하는 내장 함수입니다. 단, recover는 반드시 defer 함수 안에서 호출해야 합니다.
package main
import (
"fmt"
"os"
)
func main() {
// 잘못된 파일명을 사용
openFile("invalid-file.txt")
// recover에 의해 이 문장이 실행됨
println("Program continues...")
}
func openFile(filename string) {
// defer 함수: panic 발생 시 실행됨
defer func() {
if r := recover(); r != nil {
fmt.Println("OPEN ERROR:", r)
}
}()
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer f.Close()
// 파일 처리 로직...
}
위 예제에서는 os.Open()이 실패하면 panic이 발생하지만, recover를 통해 프로그램이 비정상 종료되지 않고 정상 흐름으로 복귀합니다.
recover 사용 패턴
1. 기본 패턴
func safeOperation() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from: %v\n", r)
}
}()
// 위험한 작업 수행
dangerousOperation()
}
2. 에러로 변환하는 패턴
func safeCall() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic occurred: %v", r)
}
}()
riskyOperation()
return nil
}
3. 웹 서버 미들웨어 패턴
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("Handler panic: %v", err)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
세 키워드의 실행 순서와 동작 원리
실행 순서 예제
func executionOrder() {
defer println("defer 1")
defer println("defer 2")
fmt.Println("Before panic")
panic("Something went wrong")
defer println("defer 3") // 실행되지 않음
fmt.Println("After panic") // 실행되지 않음
}
// 출력:
// Before panic
// defer 2
// defer 1
// panic: Something went wrong
동작 원리 요약
| 단계 | 동작 | 설명 |
|---|---|---|
| defer 등록 | 함수 실행 중 defer 문 만남 | 스택에 등록, 즉시 실행 안 함 |
| panic 발생 | panic() 함수 호출 | 현재 함수 실행 즉시 중단 |
| defer 실행 | 등록된 defer들을 LIFO 순서로 실행 | 자원 정리 작업 수행 |
| recover 체크 | defer 내부에서 recover() 호출 시 | panic 상태를 포착하고 복구 |
| 전파 또는 복구 | recover 없으면 상위로 전파 | 프로그램 종료 또는 정상 복귀 |
panic과 recover의 상호작용
func panicRecoverFlow() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\n", r)
// 여기서 다시 panic을 발생시킬 수도 있음
// panic(fmt.Sprintf("re-panic: %v", r))
}
}()
defer fmt.Println("Second defer")
defer fmt.Println("First defer")
panic("Original panic")
fmt.Println("This will not be printed")
}
// 출력:
// First defer
// Second defer
// Recovered: Original panic
실무 예제: 안전한 파일 처리
예제 1: 기본적인 파일 처리
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
data := make([]byte, 1024)
n, err := file.Read(data)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}
fmt.Printf("Read %d bytes: %s\n", n, string(data[:n]))
return nil
}
예제 2: panic과 recover를 활용한 안전한 처리
func safeFileProcessor(filename string) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("파일 처리 중 오류 발생: %v\n", r)
}
}()
file, err := os.Open(filename)
if err != nil {
panic(fmt.Sprintf("파일 열기 실패: %v", err))
}
defer file.Close()
data := make([]byte, 1024)
_, err = file.Read(data)
if err != nil {
panic(fmt.Sprintf("파일 읽기 실패: %v", err))
}
// JSON 파싱 등 추가 처리...
processData(data)
fmt.Println("파일 처리 완료")
}
func processData(data []byte) {
// 데이터 처리 중 panic 발생 가능
if len(data) == 0 {
panic("빈 데이터입니다")
}
// 실제 처리 로직...
}
예제 3: 데이터베이스 트랜잭션 처리
func executeTransaction(db *sql.DB, queries []string) (err error) {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("트랜잭션 시작 실패: %w", err)
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
err = fmt.Errorf("트랜잭션 중 panic 발생: %v", r)
} else if err != nil {
tx.Rollback()
} else {
err = tx.Commit()
}
}()
for _, query := range queries {
_, err := tx.Exec(query)
if err != nil {
return fmt.Errorf("쿼리 실행 실패: %w", err)
}
}
return nil
}
예제 4: 웹 서버에서의 안전한 핸들러
func safeHandler(handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("Handler panic: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", 500)
}
}()
handler(w, r)
}
}
// 사용 예
func main() {
http.HandleFunc("/api/data", safeHandler(dataHandler))
http.ListenAndServe(":8080", nil)
}
func dataHandler(w http.ResponseWriter, r *http.Request) {
// 위험한 작업 수행
data := processRequest(r)
w.Write(data)
}
다른 언어와의 비교 분석
언어별 예외 처리 메커니즘
| 언어 | 예외 구조 | 복구 방식 | 설계 철학 |
|---|---|---|---|
| Java | try-catch-finally |
예외 객체 기반 | 캡슐화된 예외 처리 |
| Python | try-except-finally |
동적 예외 처리 | 표현력 중심 |
| C# | try-catch-finally |
강타입 예외 | 안전성과 생산성 |
| Rust | Result<T, E> |
타입 기반 | 메모리 안전성 |
| Go | defer-panic-recover |
값 기반 복구 | 단순함 + 명시성 |
Java vs Go 비교
Java의 예외 처리
// Java
public String readFile(String filename) throws IOException {
FileReader file = null;
try {
file = new FileReader(filename);
// 파일 읽기 로직
return content;
} catch (IOException e) {
logger.error("File read error", e);
throw e;
} finally {
if (file != null) {
try {
file.close();
} catch (IOException e) {
logger.error("File close error", e);
}
}
}
}
Go의 명시적 오류 처리
// Go
func readFile(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", fmt.Errorf("파일 열기 실패: %w", err)
}
defer func() {
if closeErr := file.Close(); closeErr != nil {
log.Printf("파일 닫기 실패: %v", closeErr)
}
}()
data, err := io.ReadAll(file)
if err != nil {
return "", fmt.Errorf("파일 읽기 실패: %w", err)
}
return string(data), nil
}
Python vs Go 비교
Python의 예외 처리
# Python
def process_data(data):
try:
result = risky_operation(data)
return result
except ValueError as e:
print(f"값 오류: {e}")
return None
except Exception as e:
print(f"예상치 못한 오류: {e}")
return None
finally:
cleanup_resources()
Go의 함수형 오류 처리
// Go
func processData(data []byte) (interface{}, error) {
defer cleanupResources()
result, err := riskyOperation(data)
if err != nil {
// 구체적인 오류 타입 확인
if errors.Is(err, ErrInvalidValue) {
return nil, fmt.Errorf("값 오류: %w", err)
}
return nil, fmt.Errorf("예상치 못한 오류: %w", err)
}
return result, nil
}
Rust vs Go 비교
Rust의 Result 타입
// Rust
use std::fs::File;
use std::io::Read;
fn read_file(filename: &str) -> Result<String, std::io::Error> {
let mut file = File::open(filename)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
Go의 간단한 오류 처리
// Go
func readFile(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", err
}
defer file.Close()
contents, err := io.ReadAll(file)
if err != nil {
return "", err
}
return string(contents), nil
}
결론: 단순함이 만드는 안정성
Go의 오류 제어는 다음 세 가지 원칙을 지향합니다:
1. 명시적 오류 처리 (Explicit over Implicit)
// 모든 오류가 명시적으로 처리됨
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err // 오류를 명시적으로 반환
}
defer file.Close()
// 추가 처리 로직...
return nil
}
2. 자원 정리의 보장
// defer를 통한 확실한 자원 정리
func databaseOperation() error {
db, err := sql.Open("mysql", connectionString)
if err != nil {
return err
}
defer db.Close() // 함수 종료 시 반드시 실행
// 데이터베이스 작업...
return nil
}
3. 제어 가능한 복구
// 필요한 경우에만 panic/recover 사용
func criticalOperation() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("치명적 오류 발생: %v", r)
}
}()
// 위험한 작업 수행
performRiskyTask()
return nil
}
Go의 오류 처리가 주는 이점
1. 예측 가능성
- 모든 오류 경로가 명시적으로 표현됨
- 예상치 못한 제어 흐름 점프가 없음
2. 성능
- 예외 처리 오버헤드 제거
- 빠른 실행 속도
3. 유지보수성
- 간단하고 이해하기 쉬운 코드
- 오류 처리 로직이 명확히 보임
4. 안정성
- 자원 누수 방지
- 예상 가능한 프로그램 동작
모범 사례 요약
defer 사용 시:
- 자원을 획득한 직후에 defer로 해제 코드를 작성
- 여러 defer가 있을 때 실행 순서(LIFO)를 고려
- defer 내에서 에러를 처리할 때는 로깅을 고려
panic 사용 시:
- 정말 복구 불가능한 상황에만 사용
- 라이브러리 코드에서는 가급적 panic 대신 error 반환
- main 함수에서는 panic 보다는 log.Fatal 사용 고려
recover 사용 시:
- 반드시 defer 함수 내에서만 호출
- recover 후에는 적절한 정리 작업 수행
- 필요시 panic을 error로 변환하여 반환
Go는 복잡한 try-catch 대신 예측 가능한 단순함으로 안정성을 확보합니다. 이는 "적은 것이 많은 것"이라는 Go의 철학을 잘 보여주는 사례입니다.
참고 자료:
'Golang' 카테고리의 다른 글
| [Golang] Go의 Goroutine과 Channel: 공유하지 말고 소통하라 (0) | 2025.11.10 |
|---|---|
| [Golang] Go에 삼항연산자(?:)가 없는 이유: 언어 설계 철학과 가독성 우선주의 (0) | 2025.11.09 |