Go의 비밀스러운 삶: 인터페이스
Source: Dev.to
암묵적 계약의 힘
화요일 아침 안개가 끼었다. 이선은 커피와 작은 비스코티 상자를 들고 보관소로 내려갔다.
엘리노어가 고개를 들었다. “오늘은 이탈리안인가?”
“제빵사가 비스코티는 커피잔에 딱 맞게 설계됐다고 했어—형태는 기능을 따른다.”
그녀가 미소 지었다. “완벽해. 오늘은 인터페이스에 대해 이야기할 거야—Go가 어떤 것이든 그것이 할 수 있는 일을 정의하는 방식이야.”
간단한 예제
package main
import "fmt"
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return "Meow!"
}
func main() {
dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}
fmt.Println(dog.Name, "says:", dog.Speak())
fmt.Println(cat.Name, "says:", cat.Speak())
}
Output
Buddy says: Woof!
Whiskers says: Meow!
두 개의 서로 다른 타입—Dog와 Cat. 두 타입 모두 문자열을 반환하는 Speak() 메서드를 가지고 있지만, 서로 관련 없는 타입이다.
인터페이스 정의하기
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return "Meow!"
}
func MakeItSpeak(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}
MakeItSpeak(dog)
MakeItSpeak(cat)
}
Output
Woof!
Meow!
Speaker는 계약을 정의한다: Speak() string 메서드를 가진 모든 타입은 인터페이스를 만족한다. MakeItSpeak 함수는 어떤 Speaker든 받아들이며, Dog와 Cat 모두 해당한다.
Note: Go에서는 타입이 인터페이스를 구현한다는 것을 명시적으로 선언하지 않는다. 메서드가 일치하면 구현은 암묵적이다.
왜 암묵적인가?
- 유연성: 이미 존재하는 타입이 인터페이스를 만족하도록 정의할 수 있다. 그 타입들이 인터페이스가 만들어지기 전에 작성되었더라도 가능하다.
- 사전 결합 없음: 타입이 관계를 선언하도록 강요되지 않으므로 코드가 느슨하게 결합된다.
실제 예시: 다양한 대상에 쓰기
package main
import (
"fmt"
"os"
)
type Writer interface {
Write([]byte) (int, error)
}
type ConsoleWriter struct{}
func (cw ConsoleWriter) Write(data []byte) (int, error) {
n, err := fmt.Println(string(data))
return n, err
}
func WriteMessage(w Writer, message string) {
w.Write([]byte(message))
}
func main() {
console := ConsoleWriter{}
WriteMessage(console, "Hello, Console!")
file, _ := os.Create("output.txt")
defer file.Close()
WriteMessage(file, "Hello, File!")
}
Writer 인터페이스는 단일 메서드 Write([]byte) (int, error)를 가진다.
ConsoleWriter가 이를 구현한다.- 표준 라이브러리의
os.File도 이 인터페이스를 구현한다. 이 파일은 우리 인터페이스가 존재하기 훨씬 전에 작성되었다.
따라서 WriteMessage는 Writer를 만족하는 어떤 타입—콘솔, 파일, 네트워크 연결, 메모리 버퍼 등—과도 동작한다.
인터페이스를 이용한 다형성
package main
import "fmt"
type Shape interface {
Area() float64
Perimeter() float64
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * 3.14159 * c.Radius
}
func PrintShapeInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
func main() {
rect := Rectangle{Width: 5, Height: 3}
circle := Circle{Radius: 4}
PrintShapeInfo(rect)
PrintShapeInfo(circle)
}
Output
Area: 15.00, Perimeter: 16.00
Area: 50.27, Perimeter: 25.13
Shape은 Area()와 Perimeter()를 요구한다. Rectangle과 Circle 모두 이를 만족하므로 PrintShapeInfo는 구체적인 타입을 알 필요 없이 모든 Shape에 대해 동작한다.
이는 덕 타이핑과 비슷하지만, Go는 컴파일 시점에 구현을 검사해 타입 안전성을 제공한다.
빈 인터페이스 (any)
package main
import "fmt"
func PrintAnything(v any) {
fmt.Println(v)
}
func main() {
PrintAnything(42)
PrintAnything("hello")
PrintAnything(true)
PrintAnything([]int{1, 2, 3})
}
Output
42
hello
true
[1 2 3]
any는 빈 인터페이스 interface{}의 별칭이다. 모든 타입은 최소한 메서드가 0개이므로 모두 이를 만족한다. 컴파일 타임 타입 안전성을 없애므로 남용하지 말아야 한다.
타입 어설션
package main
import "fmt"
func Describe(v any) {
// 콤마‑ok 형태의 타입 어설션
if str, ok := v.(string); ok {
fmt.Printf("String: %s (length %d)\n", str, len(str))
return
}
if num, ok := v.(int); ok {
fmt.Printf("Integer: %d (doubled: %d)\n", num, num*2)
return
}
fmt.Printf("Unknown type: %T\n", v)
}
func main() {
Describe("hello")
Describe(42)
Describe(true)
}
Output
String: hello (length 5)
Integer: 42 (doubled: 84)
Unknown type: bool
ok 검사를 생략하고 str := v.(string)처럼 쓰면 어설션이 실패했을 때 프로그램이 패닉한다.
타입 스위치
package main
import "fmt"
func Describe(v any) {
switch val := v.(type) {
case string:
fmt.Printf("String: %s (length %d)\n", val, len(val))
case int:
fmt.Printf("Integer: %d (doubled: %d)\n", val, val*2)
case bool:
fmt.Printf("Boolean: %t\n", val)
default:
fmt.Printf("Unknown type: %T\n", val)
}
}
func main() {
Describe("hello")
Describe(42)
Describe(true)
Describe(3.14)
}
Output
String: hello (length 5)
Integer: 42 (doubled: 84)
Boolean: true
Unknown type: float64
switch val := v.(type) 구문은 여러 가능한 구체 타입을 깔끔하게 처리한다.
인터페이스 계층 구조 (구체적 → 일반)
-
구체적 인터페이스
type Reader interface { Read([]byte) (int, error) }Read([]byte) (int, error)를 구현한 타입만Reader를 만족한다. -
빈 인터페이스 (
any)type any interface{}모든 타입이 이를 만족하므로 가장 일반적인 인터페이스다. 정말 모든 타입을 받아야 할 때만 사용하라.