🔥 인터페이스 값과 nil 기본 값

605자
7분

Go 언어에서는 인터페이스 안에 실제 값이 nil이더라도 메서드를 nil 수신자로 호출할 수 있어요. 다른 언어에서는 이런 경우 null 포인터 예외가 발생하겠지만, Go에서는 nil 수신자로 호출되었을 때 우아하게 처리하도록 메서드를 작성하는 것이 일반적이에요.

다음 예제 코드를 살펴보면서 자세히 알아보도록 해요.

go
package main
 
import "fmt"
 
type I interface {
	M()
}
 
type T struct {
	S string
}
 
func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}
 
func main() {
	var i I
 
	var t *T
	i = t
	describe(i)
	i.M()
 
	i = &T{"hello"}
	describe(i)
	i.M()
}
 
func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}
 
go
package main
 
import "fmt"
 
type I interface {
	M()
}
 
type T struct {
	S string
}
 
func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}
 
func main() {
	var i I
 
	var t *T
	i = t
	describe(i)
	i.M()
 
	i = &T{"hello"}
	describe(i)
	i.M()
}
 
func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}
 

먼저 I 인터페이스를 정의했어요. 이 인터페이스는 M() 메서드를 가지고 있죠.

go
type I interface {
	M()
}
 
go
type I interface {
	M()
}
 

그리고 T 구조체를 정의하고, 이 구조체는 S라는 문자열 필드를 가지고 있어요.

go
type T struct {
	S string
}
 
go
type T struct {
	S string
}
 

T 구조체는 I 인터페이스를 구현하기 위해 M() 메서드를 가지고 있죠. 이 메서드는 수신자가 nil인 경우를 우아하게 처리해요.

go
func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}
 
go
func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}
 
  • 만약 t가 nil이면, "<nil>"을 출력하고 메서드를 종료해요.
  • nil이 아니라면 t.S를 출력하죠.

main 함수에서는 인터페이스 값 i를 선언하고, 이를 nil 값과 non-nil 값으로 설정해 보면서 M() 메서드를 호출해 볼 거예요.

go
func main() {
	var i I
 
	var t *T
	i = t
	describe(i)
	i.M()
 
	i = &T{"hello"}
	describe(i)
	i.M()
}
 
go
func main() {
	var i I
 
	var t *T
	i = t
	describe(i)
	i.M()
 
	i = &T{"hello"}
	describe(i)
	i.M()
}
 
  1. 먼저 i를 nil 값인 t로 설정하고, describe(i)i.M()을 호출해요.
    • describe(i)i의 값과 타입을 출력하죠.
    • i.M()은 nil 수신자로 M() 메서드를 호출하게 되고, "<nil>"이 출력될 거예요.
  2. 그 다음엔 i를 non-nil 값인 &T{"hello"}로 설정하고, 다시 describe(i)i.M()을 호출해요.
    • 이번에는 iT 구조체를 가리키고 있으므로, i.M()"hello"를 출력할 거예요.

describe 함수는 인터페이스 값의 실제 값과 타입을 출력해 주는 헬퍼 함수예요.

go
func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}
 
go
func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}
 

이 예제를 실행하면 다음과 같은 결과를 볼 수 있을 거예요.

text
(<nil>, *main.T)
<nil>
(&{hello}, *main.T)
hello
text
(<nil>, *main.T)
<nil>
(&{hello}, *main.T)
hello

첫 번째 describe(i) 호출에서는 i가 nil 값을 가지고 있지만, 타입은 *main.T임을 알 수 있죠. 그리고 i.M() 호출에서는 "<nil>"이 출력되요.

두 번째 describe(i) 호출에서는 i&{hello}라는 값을 가지고 있고, 타입은 여전히 *main.T예요. 이번에 i.M() 호출에서는 "hello"가 출력되죠.

이 예제를 통해 우리는 인터페이스 값 내부의 실제 값이 nil이더라도 메서드 호출이 가능하며, nil 수신자를 적절히 처리할 수 있음을 배웠어요. 또한 nil 실제 값을 가지는 인터페이스 값 자체는 nil이 아님을 알 수 있었죠. 이 부분을 좀 더 얘기해 볼게요.

코드의 이 부분을 다시 살펴보면,

go
var i I
 
var t *T
i = t
describe(i)
 
go
var i I
 
var t *T
i = t
describe(i)
 

여기서 t*T 타입의 nil 값이에요. 그리고 it로 초기화되죠. 그럼 i는 어떤 값을 가질까요?

describe(i) 호출 결과를 보면,

text
(<nil>, *main.T)
text
(<nil>, *main.T)

i의 값은 <nil>로 표시되지만, 타입은 *main.T로 나와요. 이는 i 자체는 nil이 아니라는 것을 보여주죠.

i는 인터페이스 타입 I의 변수예요. 인터페이스 값은 실제 값(value)과 타입(type) 정보를 모두 가지고 있어요. 여기서 i의 실제 값은 nil이지만, 타입은 *main.T예요.

따라서 "nil 실제 값을 가지는 인터페이스 값 자체는 nil이 아님"이란 말은, i가 nil 값을 가진 *main.T 타입을 가리키고 있지만, i 자체는 nil이 아니라는 것을 의미해요.

이는 다음 코드에서도 확인할 수 있어요.

go
if i == nil {
    fmt.Println("i is nil")
} else {
    fmt.Println("i is not nil")
}
 
go
if i == nil {
    fmt.Println("i is nil")
} else {
    fmt.Println("i is not nil")
}
 

이 코드를 실행하면 "i is not nil"이 출력될 거예요. 왜냐하면 i 자체는 nil이 아니기 때문이죠.

이 개념은 인터페이스를 사용할 때 주의해야 할 중요한 부분이에요. 인터페이스 값이 nil인지 확인할 때는, 인터페이스 값 자체가 nil인지 확인해야지, 인터페이스 값이 가리키는 실제 값이 nil인지를 확인하면 안 되죠.

즉, i는 nil 값을 가진 *main.T 타입을 가리키는, nil이 아닌 인터페이스 값이에요. 이렇게 인터페이스 값과 실제 값을 구분하는 것이 중요해요.

이렇게 Go 언어에서는 nil 인터페이스 값과 nil 기본 값을 유연하게 처리할 수 있어요. 개발자들은 이를 활용하여 더 견고하고 우아한 코드를 작성할 수 있게 되죠.

YouTube 영상

채널 보기
NestJS 커스텀 예외 만들기 - 에러 처리 깔끔하게 하는 법 | NestJS 가이드
리더 펑터 - 함수도 펑터다! | 프로그래머를 위한 카테고리 이론
List 펑터 - 왜 map은 for 루프보다 강력한가? | 프로그래머를 위한 카테고리 이론
함수형 미들웨어 | NestJS 가이드
커스텀 예외 필터 만들기 | NestJS 가이드
NestJS 표준 예외 처리와 HttpException | NestJS 가이드
NestJS 파이프가 뭔가요? 컨트롤러를 보호하는 방법 | NestJS 가이드
펑터 타입 클래스 | 프로그래머를 위한 카테고리 이론