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"))
}
위 예제에서 FileWriter
와 ConsoleWriter
는 모두 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의 간결성, 명확성, 그리고 실용성이라는 설계 철학을 잘 보여준다. 인터페이스를 통해 유연하고 모듈화된 코드를 작성할 수 있으며, 명시적인 에러 처리를 통해 더 안정적인 프로그램을 만들 수 있다.