Go의 인터페이스: 행동으로 타입을 설계하기
Go의 인터페이스: 행동으로 타입을 설계하기
Go의 타입 체계에서 **인터페이스(interface)**는 특별한 위치를 차지해요. 자바처럼 클래스 계층을 선언해서 써야 하는 게 아니라, "이런 메서드를 갖고 있으면 이 인터페이스에 맞는 타입"이라는 구조적(structural) 방식이라서 코드가 훨씬 유연해져요. 객체의 행동(메서드 집합)을 먼저 정해 두고, 그 행동을 갖춘 어떤 타입이든 그 자리에 들어갈 수 있게 만들 수 있어요.
Go 인터페이스의 흐름은 공식 Effective Go 문서에서 가장 체계적으로 설명돼요. 관용적인 인터페이스 이름 짓기부터 메서드·타입 단언·임베딩까지, "인터페이스는 메서드의 집합"이라는 한 줄로 시작해서 차근차근 살펴볼게요.
인터페이스는 메서드의 집합
인터페이스는 객체의 행동을 지정하는 방법이에요. "이런 걸 할 수 있으면 여기서 쓸 수 있다"는 방식이죠. 간단한 예를 하나 볼게요. Fprintf는 Write 메서드를 갖춘 어떤 대상이든 출력할 수 있어요. Go 코드에서는 메서드가 한두 개뿐인 인터페이스가 흔하고, 보통 메서드 이름에서 따온 이름을 붙여요 — Write를 구현하는 것의 인터페이스는 io.Writer가 되는 식이에요.
한 타입은 여러 인터페이스를 구현할 수 있어요. 아래 Sequence는 정렬에 쓰이는 sort.Interface(Len, Less, Swap 메서드)와, 출력을 위한 String 메서드 양쪽을 만족해요.
type Sequence []int
// Methods required by sort.Interface.
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
copy := make(Sequence, 0, len(s))
return append(copy, s...)
}
// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
s = s.Copy() // Make a copy; don't overwrite argument.
sort.Sort(s)
str := "["
for i, elem := range s { // Loop is O(N²); will fix that in next example.
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
인터페이스 이름 짓기
관례상 메서드가 하나뿐인 인터페이스는 메서드 이름에 -er을 붙여 사물을 나타내는 명사로 짓는 경우가 많아요. Reader, Writer, Formatter, CloseNotifier 같은 이름들이 그 예예요.
Read, Write, Close, Flush, String 같은 이름들은 표준 시그니처와 의미를 갖고 있어요. 헷갈리지 않으려면, 그 메서드와 같은 시그니처와 의미가 아니라면 그 이름을 쓰지 말아야 해요. 반대로 잘 알려진 타입의 메서드와 같은 의미를 갖는 메서드라면 같은 이름과 시그니처를 쓰는 게 좋아요 — 문자열 변환 메서드는 ToString이 아니라 String으로 짓는 식이에요.
Go에서 대문자 시작 이름은 패키지 밖으로 내보내지는(exported) 이름이에요. 패키지에 import되면 패키지 이름이 그 내용을 접근하는 이름이 되므로, 패키지 이름은 짧고 간결하며 연상을 불러일으키는 단어 하나가 좋아요. 관례상 패키지 이름은 소문자 단어 하나이고, 밑줄이나 MixedCaps가 필요 없어요.
인터페이스 변환과 타입 단언
switch는 한 형태의 변환이에요. 인터페이스를 받아 각 case에 대해 그 case의 타입으로 변환하죠. fmt.Printf 아래의 코드가 값의 타입을 보고 문자열로 바꾸는 단순화된 예를 볼게요. 이미 문자열이면 인터페이스가 들고 있는 실제 문자열 값을, String 메서드가 있으면 그 메서드 호출 결과를 돌려줘요.
type Stringer interface {
String() string
}
var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
관심 있는 타입이 하나뿐이라면 **타입 단언(type assertion)**을 쓸 수 있어요. 타입 단언은 인터페이스 값에서 지정한 명시적 타입의 값을 뽑아내요. 문법은 value.(typeName)이고, 결과는 정적 타입이 typeName인 새 값이에요.
str := value.(string)
하지만 값이 문자열을 담지 않는다면 실행 시 오류로 프로그램이 죽어요. 그것을 방지하려면 "comma, ok" 관용구로 안전하게 검사할 수 있어요.
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
단언이 실패해도 str은 타입이 string인 값으로 존재하지만, 그 값은 제로 값인 빈 문자열이에요.
내보내기와 인터페이스 설계
타입이 오직 인터페이스를 구현하기 위해 존재하고, 그 인터페이스 너머로 내보낼 메서드가 없다면 타입 자체를 내보낼 필요가 없어요. 인터페이스만 내보내면 그 값에 인터페이스가 설명한 것보다 더 흥미로운 행동이 없다는 게 명확해져요.
이런 경우 생성자(constructor)는 구현 타입 대신 인터페이스 값을 돌려주는 게 좋아요. 예를 들어 crc32.NewIEEE와 adler32.New는 둘 다 hash.Hash32 인터페이스 타입을 돌려줘요. CRC-32 알고리즘을 Adler-32로 바꾸려면 생성자 호출만 바꾸면 되고, 나머지 코드는 알고리즘 변경에 영향을 받지 않아요.
type Block interface {
BlockSize() int
Encrypt(dst, src []byte)
Decrypt(dst, src []byte)
}
type Stream interface {
XORKeyStream(dst, src []byte)
}
NewCTR(block Block, iv []byte) Stream은 특정 암호화 알고리즘과 데이터 소스가 아니라 Block 인터페이스의 어떤 구현과도 Stream을 새로 만들 수 있어요. 생성자가 인터페이스 값을 돌려주기 때문에 CTR 암호화를 다른 모드로 바꾸는 게 지역적인 변경이 돼요.
인터페이스와 메서드
거의 모든 것에 메서드를 붙일 수 있으니, 거의 모든 것으로 인터페이스를 만족시킬 수 있어요. net/http 패키지의 Handler 인터페이스가 좋은 예인데, ServeHTTP를 구현하는 어떤 객체든 HTTP 요청을 처리할 수 있어요.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ResponseWriter는 그 자체로 클라이언트에 응답을 보내는 데 필요한 메서드에 접근하게 해주는 인터페이스예요. 표준 Write 메서드를 포함하므로, http.ResponseWriter는 io.Writer가 쓰일 수 있는 곳 어디든 쓸 수 있어요.
인터페이스가 메서드의 집합이라는 점 덕분에, struct·정수·채널·함수 중 무엇으로도 HTTP 서버를 만들 수 있어요. 함수에도 메서드를 정의할 수 있으니, http.HandleFunc처럼 일반 함수를 핸들러로 감싸는 어댑터 타입(HandlerFunc)을 만들어 쓸 수 있어요. 함수를 HandlerFunc 타입으로 변환하면 그 함수가 ServeHTTP 메서드를 갖게 되고, HTTP 서버가 그 메서드를 호출하는 구조예요.
더 알아보기 (Learn more)
- Effective Go — Interfaces (원문)
- The Go Language Specification — Interfaces: 정확한 언어 규칙
- A Tour of Go — Interfaces: 인터페이스 직접 실습