Go 언어 - 인터페이스와 에러 처리

.


Background

  필자는 작년 SW마에스트로 활동을 하면서 처음으로 Terraform을 접하게 되었다. Terraform이란 클라우드 인프라 관리 도구로 이는 Go 언어로 작성되어 있어, 늘 흥미를 가지고 있던 언어 중 하나였다. 관심만 가지고 있다가 최근에 졸업 작품을 진행하면서 Go 언어에 대한 관심이 더욱 깊어졌고, 그 활용 범위가 클라우드 인프라 관리뿐만 아니라 마이크로서비스, 네트워크 프로그래밍, DevOps 도구 개발 등 다양한 분야로 확장되어 있음을 알게 되었다. 이에 Go 언어의 기초부터 실제 활용까지 체계적으로 학습하고 공유하고자 본 포스트를 작성하게 되었다.


Interface

  Go의 인터페이스는 다른 언어들과 달리 매우 유연하고 강력하다. 인터페이스는 메서드의 집합으로 정의되며, 어떠한 타입이 이 메서드를 모두 구현하면 자동으로 해당 인터페이스를 만족하게 된다.

type Writer interface {
  Write([]byte) (int, error)
}

  위 코드는 Writer 인터페이스를 정의한다. Write 메서드를 가진 모든 타입은 자동으로 이 인터페이스를 구현하게 된다.

  인터페이스의 이러한 특성은 코드의 유연성과 재사용성을 크게 향상시킨다. 필자는 이를 덕 타이핑(duck typing)의 정적인 버전이라고 생각한다.

type FileWriter struct{}

func (fw FileWriter) Write(data []byte) (int, error) {
    // 파일에 쓰는 로직
    return len(data), nil
}

type ConsoleWriter struct{}

func (cw ConsoleWriter) Write(data []byte) (int, error) {
    // 콘솔에 출력하는 로직
    return len(data), nil
}

func writeData(w Writer, data []byte) {
    w.Write(data)
}

func main() {
    fw := FileWriter{}
    cw := ConsoleWriter{}

    writeData(fw, []byte("Hello"))
    writeData(cw, []byte("World"))
}

  위 예제에서 FileWriterConsoleWriter는 모두 Writer 인터페이스를 구현하고 있다. 따라서 writeData 함수는 두 타입 모두를 인자로 받을 수 있다.

Error handling

  Go의 에러 처리 방식은 상당히 독특하다. Go는 예외(exception)를 사용하지 않고, 대신 에러를 값으로 처리한다. 이는 “오류는 예외적인 상황이 아니라 프로그램의 정상적인 흐름의 일부”라는 Go의 철학을 반영한다.

func readFile(filename string) ([]byte, error) {
    // 파일 읽기 로직
    if 에러발생 {
        return nil, errors.New("파일을 읽을 수 없습니다")
    }
    return 데이터, nil
}

func main() {
    data, err := readFile("example.txt")
    if err != nil {
        fmt.Println("에러 발생:", err)
        return
    }
    // 데이터 처리
}

  이러한 Go의 에러 처리 방식은 개발자로 하여금 에러 처리를 명시적으로 하도록 강제하여, 더 안정적인 프로그램을 작성할 수 있게 한다. 이는 프로그램의 흐름을 더 명확히 이해하고 디버깅을 용이하게 만든다.

panic & recover

  Go에서는 일반적인 에러 처리 외에도 panic과 recover라는 메커니즘을 제공한다. 이는 예외적인 상황에서 사용되며, 프로그램의 정상적인 실행 흐름을 중단시키고 복구하는 데 사용된다.

panic

  panic은 프로그램이 더 이상 진행할 수 없는 심각한 오류 상황에서 사용된다. panic이 발생하면 현재 함수의 실행이 중단되고, defer 함수들이 실행된 후 프로그램이 종료된다.

func divide(a, b int) int {
    if b == 0 {
        panic("0으로 나눌 수 없습니다")
    }
    return a / b
}

recover

  recover는 panic으로 인한 프로그램의 비정상 종료를 방지하고 제어를 다시 획득하는 데 사용된다. recover는 반드시 defer 함수 내에서 호출해야 한다.

func safeDivide(a, b int) (result int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("패닉 복구:", r)
            result = 0
        }
    }()
    return divide(a, b)
}

func main() {
    fmt.Println(safeDivide(10, 2))  // 출력: 5
    fmt.Println(safeDivide(10, 0))  // 출력: 패닉 복구: 0으로 나눌 수 없습니다
                                    //       0
}

panic vs recover

  필자는 panic과 recover를 사용할 때 주의가 필요하다고 생각한다. 이들은 일반적인 에러 처리 방식을 대체하는 것이 아니라, 정말로 예외적인 상황에서만 사용해야 한다. 대부분의 경우 error를 반환하는 일반적인 에러 처리 방식을 사용하는 것이 더 Go스러운(관용적인) 방식이다.

panic과 recover의 주요 사용 사례

  • 초기화 실패: 프로그램 시작 시 필수적인 초기화가 실패했을 때
  • 프로그래밍 오류: 배열 범위를 벗어난 접근 등 개발자의 실수로 인한 오류
  • 예상치 못한 상태: 절대 발생해서는 안 되는 상황에 도달했을 때

Summary

  Go의 인터페이스와 에러 처리 방식은 언어의 핵심 특징 중 하나다. 이들은 Go의 간결성, 명확성, 그리고 실용성이라는 설계 철학을 잘 보여준다. 인터페이스를 통해 유연하고 모듈화된 코드를 작성할 수 있으며, 명시적인 에러 처리를 통해 더 안정적인 프로그램을 만들 수 있다.