인터페이스 충족 규칙
Coddy GO 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 107개 중 43번째.
Go는 암시적 인터페이스 만족을 위해 덕 타이핑(duck typing)을 사용하지만, 타입이 실제로 인터페이스를 만족하는지 여부를 결정하는 구체적인 규칙이 있습니다. 이러한 규칙을 이해하면 미묘한 버그를 피하는 데 도움이 됩니다.
가장 중요한 규칙은 포인터 리시버(pointer receivers)와 관련이 있습니다. 메서드가 포인터 리시버로 정의된 경우, 해당 타입에 대한 포인터만이 인터페이스를 충족하며 값 자체는 충족하지 않습니다:
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // 포인터 리시버
return "Saved: " + d.Name
}
func Process(s Saver) {
fmt.Println(s.Save())
}
Save()가 포인터 리시버를 가지고 있기 때문에, *Document만이 Saver를 만족합니다:
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // 작동함 - 포인터가 인터페이스를 충족함
// Process(doc) // 컴파일 에러 - 값이 인터페이스를 충족하지 않음
}
하지만 그 반대의 경우는 더 유연합니다. 만약 메서드가 값 리시버(value receiver)를 가지고 있다면, 값 타입과 포인터 타입 모두 인터페이스를 충족합니다. Go는 값 리시버 메서드를 호출할 때 자동으로 포인터를 역참조(dereference)합니다:
func (d Document) Info() string { // 값 리시버
return d.Name
}
// Document와 *Document 모두 Info()를 요구하는 인터페이스를 충족합니다.
이러한 비대칭성이 존재하는 이유는 Go가 (역참조를 통해) 포인터에서 항상 값을 가져올 수 있지만, 값에서 항상 포인터를 가져올 수는 없기 때문입니다(값의 주소를 가져올 수 없는 경우가 있을 수 있습니다). 이 규칙을 기억해 두면 인터페이스를 작업할 때 혼란스러운 컴파일러 오류를 방지할 수 있습니다.
챌린지
쉬움포인터 및 값 리시버가 인터페이스 만족(satisfaction)에 어떤 영향을 미치는지 보여주는 구성 시스템을 구축해 보겠습니다. 리시버 선택에 따라 값, 포인터 또는 둘 다 인터페이스와 함께 사용될 수 있는지 여부가 결정되는 타입을 만들게 됩니다.
코드는 세 개의 파일로 구성됩니다:
config.go:GetValue() string과SetValue(string)이라는 두 가지 메서드를 요구하는Configurable인터페이스를 정의합니다. 그런 다음 두 가지 구성 타입을 만듭니다:Value필드를 가진ReadOnlyConfig—GetValue()에는 값 리시버를 사용하고(Value를 반환),SetValue()에는 포인터 리시버를 사용합니다(Value를 업데이트).Data필드를 가진Setting—두 메서드 모두에 값 리시버를 사용합니다(GetValue는 Data를 반환하고, SetValue는 아무것도 변경하지 않고 "Cannot modify"만 출력합니다).
processor.go:Configurable과 새로운 값 문자열을 인자로 받는ProcessConfig함수를 만듭니다. 이 함수는GetValue()를 사용하여 현재 값을 출력하고, 새로운 값으로SetValue()를 호출한 다음, 변경 사항을 보여주기 위해 값을 다시 출력해야 합니다.main.go: 입력에서 구성 세부 정보를 읽고 인터페이스 만족 규칙을 보여줍니다:ReadOnlyConfig를 생성하고ProcessConfig에 포인터를 전달합니다 (SetValue가 포인터 리시버를 가지므로 필수입니다).Setting을 생성하고ProcessConfig에 값을 직접 전달합니다 (두 메서드 모두 값 리시버를 가지므로 작동합니다).
다음 입력이 제공됩니다:
- Line 1: ReadOnlyConfig의 초기 값
- Line 2: ReadOnlyConfig에 설정할 새 값
- Line 3: Setting의 초기 값
- Line 4: Setting에 시도할 새 값
ProcessConfig 함수는 다음 형식으로 출력해야 합니다:
Current: [value]
Current: [value after SetValue]예를 들어, debug, production, localhost, remote가 주어지면 출력은 다음과 같아야 합니다:
Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhostReadOnlyConfig는 포인터를 전달했기 때문에 실제로 값이 변경되는 반면, Setting은 변경되지 않은 상태로 유지되는 점에 주목하세요 (값 리시버를 사용하는 SetValue는 원본을 수정할 수 없습니다). 핵심 통찰은 ReadOnlyConfig 값 자체로는 Configurable을 만족시킬 수 없으며 포인터만 가능하다는 점입니다. 반면 Setting 값은 모든 메서드가 값 리시버를 사용하므로 직접 작동합니다.
치트 시트
Go는 암시적 인터페이스 만족을 위해 덕 타이핑(duck typing)을 사용하지만, 포인터 및 값 리시버에 관한 구체적인 규칙이 있습니다:
포인터 리시버 규칙 (Pointer Receiver Rule)
메서드가 포인터 리시버를 가지고 있다면, 해당 타입의 포인터만이 인터페이스를 만족합니다:
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // pointer receiver
return "Saved: " + d.Name
}
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // works - pointer satisfies interface
// Process(doc) // compile error - value doesn't satisfy interface
}값 리시버 규칙 (Value Receiver Rule)
메서드가 값 리시버를 가지고 있다면, 값 타입과 포인터 타입 모두 인터페이스를 만족합니다:
func (d Document) Info() string { // value receiver
return d.Name
}
// Both Document and *Document satisfy an interface requiring Info()이러한 비대칭성이 존재하는 이유
Go는 포인터로부터 언제나 값을 얻을 수 있지만(역참조를 통해), 값으로부터 언제나 포인터를 얻을 수 있는 것은 아니기 때문입니다(값이 주소 지정이 가능하지 않을 수 있음).
직접 해보기
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// 입력 읽기
scanner.Scan()
rocInitial := scanner.Text()
scanner.Scan()
rocNew := scanner.Text()
scanner.Scan()
settingInitial := scanner.Text()
scanner.Scan()
settingNew := scanner.Text()
// TODO: rocInitial 값으로 ReadOnlyConfig 생성
// TODO: ProcessConfig에 포인터를 전달 (SetValue가 포인터 리시버를 가지므로 필요함)
// TODO: settingInitial 값으로 Setting 생성
// TODO: ProcessConfig에 값을 직접 전달 (두 메서드 모두 값 리시버를 가지므로 가능함)
// 사용하지 않는 변수 에러를 방지하기 위해 이 변수들을 사용함
_ = rocInitial
_ = rocNew
_ = settingInitial
_ = settingNew
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.
객체 지향 프로그래밍의 모든 레슨
8에러 처리와 OOP
error 인터페이스사용자 정의 에러 타입에러 래핑 (fmt.Errorf)센티넬 에러errors.Is()와 errors.As()Panic, Defer, Recover요약 - 파일 파서