Menu
Coddy logo textTech

인터페이스 충족 규칙

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가 (역참조를 통해) 포인터에서 항상 값을 가져올 수 있지만, 값에서 항상 포인터를 가져올 수는 없기 때문입니다(값의 주소를 가져올 수 없는 경우가 있을 수 있습니다). 이 규칙을 기억해 두면 인터페이스를 작업할 때 혼란스러운 컴파일러 오류를 방지할 수 있습니다.

challenge icon

챌린지

쉬움

포인터 및 값 리시버가 인터페이스 만족(satisfaction)에 어떤 영향을 미치는지 보여주는 구성 시스템을 구축해 보겠습니다. 리시버 선택에 따라 값, 포인터 또는 둘 다 인터페이스와 함께 사용될 수 있는지 여부가 결정되는 타입을 만들게 됩니다.

코드는 세 개의 파일로 구성됩니다:

  • config.go: GetValue() stringSetValue(string)이라는 두 가지 메서드를 요구하는 Configurable 인터페이스를 정의합니다. 그런 다음 두 가지 구성 타입을 만듭니다:
    • Value 필드를 가진 ReadOnlyConfigGetValue()에는 값 리시버를 사용하고(Value를 반환), SetValue()에는 포인터 리시버를 사용합니다(Value를 업데이트).
    • Data 필드를 가진 Setting—두 메서드 모두에 값 리시버를 사용합니다(GetValue는 Data를 반환하고, SetValue는 아무것도 변경하지 않고 "Cannot modify"만 출력합니다).
  • processor.go: Configurable과 새로운 값 문자열을 인자로 받는 ProcessConfig 함수를 만듭니다. 이 함수는 GetValue()를 사용하여 현재 값을 출력하고, 새로운 값으로 SetValue()를 호출한 다음, 변경 사항을 보여주기 위해 값을 다시 출력해야 합니다.
  • main.go: 입력에서 구성 세부 정보를 읽고 인터페이스 만족 규칙을 보여줍니다:
    1. ReadOnlyConfig를 생성하고 ProcessConfig포인터를 전달합니다 (SetValue가 포인터 리시버를 가지므로 필수입니다).
    2. 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: localhost

ReadOnlyConfig는 포인터를 전달했기 때문에 실제로 값이 변경되는 반면, 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
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨