Go에 상속이 없는 이유
Coddy GO 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 107개 중 34번째.
Java와 C++ 같은 전통적인 객체 지향 언어는 타입 간에 코드를 공유하기 위해 상속을 사용합니다. 자식 클래스는 부모 클래스를 확장하며, 모든 필드와 메서드를 상속받습니다. Go는 의도적으로 이 기능을 생략했습니다.
상속은 타입 간의 강한 결합을 만듭니다. 부모 클래스가 변경되면 모든 자식 클래스가 영향을 받습니다.
깊은 상속 계층 구조는 이해하고 유지 관리하기 어려워집니다. "취약한 기본 클래스 문제(fragile base class problem)"는 기본 클래스에 대한 수정이 파생 클래스를 예기치 않게 손상시킬 때 발생합니다.
Go의 설계자들은 다른 길을 선택했습니다: 상속보다는 구성(composition over inheritance). "개는 동물이다(Dog is an Animal)"라고 말하는 대신, Go는 "개는 동물과 같은 행동을 가지고 있다(has)"라고 말하도록 권장합니다. 이러한 미묘한 변화는 더 유연하고 유지보수가 쉬운 코드로 이어집니다.
Go는 이미 배운 두 가지 메커니즘을 통해 코드 재사용을 달성합니다:
- 인터페이스는 구현 세부 사항 없이 동작 계약을 정의합니다
- 구조체 임베딩(Struct embedding)을 사용하면 타입이 다른 타입을 포함하고 해당 메서드들을 재사용할 수 있습니다.
다음 비교를 살펴보겠습니다. 전통적인 OOP에서는 class Dog extends Animal과 같이 작성할 수 있습니다. Go에서는 Dog 내부에 Animal 구조체(struct)를 임베딩하고 공유 인터페이스를 구현합니다. 그 결과, 타입 간의 결합도는 낮아지면서 유사한 기능을 제공하게 됩니다.
이 장에서는 struct 임베딩을 심도 있게 탐구하며, Go가 상속의 단점 없이 어떻게 상속의 이점을 취하는지 보여줍니다.
챌린지
쉬움상속 대신 Go의 컴포지션(composition) 방식을 보여주는 알림 시스템을 구축해 보겠습니다. 클래스 계층 구조 대신 인터페이스와 구조체 임베딩(struct embedding)을 통해 동작을 공유하는 타입을 만들게 됩니다.
코드는 다음 세 개의 파일로 구성됩니다:
notifier.go: 단일 메서드Notify(message string) string을 가진Notifier인터페이스를 정의합니다. 또한 다른 타입에 임베딩될Name필드를 가진BaseNotifier구조체를 만듭니다.BaseNotifier에 대괄호 안에 알림 서비스의 이름이 접두어로 붙은 메시지를 반환하는Format(message string) string메서드를 추가합니다.channels.go:BaseNotifier를 임베딩하고Notifier인터페이스를 구현하는 두 가지 알림 채널 타입을 만듭니다:EmailNotifier: 추가 필드Address포함SMSNotifier: 추가 필드Phone포함
Notify메서드는 임베딩된Format메서드를 사용해야 하며, 출력에 해당 채널의 특정 정보를 포함해야 합니다.main.go: 모든Notifier와 메시지를 인자로 받아Notify를 호출한 결과를 반환하는SendAlert함수를 만듭니다. 입력에서 알림 세부 정보를 읽고, 두 종류의 알림 객체를 생성한 다음, 인터페이스를 통해 이들이 어떻게 상호 호환되어 사용될 수 있는지 보여줍니다.
다음 입력값이 제공됩니다:
- Line 1: 이메일 알림 서비스 이름
- Line 2: 이메일 주소
- Line 3: SMS 알림 서비스 이름
- Line 4: 전화번호
- Line 5: 알림 메시지
BaseNotifier의 Format 메서드는 다음을 반환해야 합니다:
[Name] message각 Notify 메서드는 다음을 반환해야 합니다:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
예를 들어, Alerts, user@mail.com, Urgent, 555-1234, Server down이 입력으로 주어지면 출력은 다음과 같아야 합니다:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server down두 알림 타입이 임베딩을 통해 BaseNotifier의 Format 메서드를 어떻게 재사용하는지, 그리고 각자 고유한 Notify 구현을 어떻게 제공하는지 확인해 보세요. SendAlert 함수는 구체적인 타입을 알지 못해도 모든 Notifier와 함께 작동합니다. 이것이 바로 상속보다 컴포지션을 우선하는 방식(composition over inheritance)이 실제로 작동하는 모습입니다.
치트 시트
Go는 타입 간의 코드를 공유하기 위해 상속보다는 구성(composition over inheritance)을 사용하며, 이를 통해 전통적인 객체 지향 상속의 긴밀한 결합(tight coupling)과 취약한 기본 클래스(fragile base class) 문제를 방지합니다.
클래스 계층 구조 대신, Go는 다음을 통해 코드 재사용을 달성합니다:
- 인터페이스(Interfaces) - 구현 세부 사항 없이 동작 계약을 정의합니다.
- 구조체 임베딩(Struct embedding) - 타입이 다른 타입을 포함하고 해당 메서드를 재사용할 수 있도록 합니다.
"개는 동물이다(a Dog is an Animal)"(상속)라고 말하는 대신, Go는 "개는 동물과 같은 행동을 가진다(a Dog has Animal-like behaviors)"(구성)라고 표현하는 것을 권장합니다.
구조체 임베딩 예제
임베딩될 기본 구조체를 정의합니다:
type BaseNotifier struct {
Name string
}
func (b BaseNotifier) Format(message string) string {
return "[" + b.Name + "] " + message
}필드와 메서드를 재사용하기 위해 다른 타입에 기본 구조체를 임베딩합니다:
type EmailNotifier struct {
BaseNotifier // 임베딩된 구조체
Address string
}
func (e EmailNotifier) Notify(message string) string {
// 임베딩된 Format 메서드를 직접 호출할 수 있습니다
return "Email to " + e.Address + ": " + e.Format(message)
}여러 타입이 구현할 수 있는 인터페이스를 정의합니다:
type Notifier interface {
Notify(message string) string
}인터페이스를 사용하여 서로 다른 구체적인 타입들을 상호 교환적으로 작업합니다:
func SendAlert(n Notifier, message string) string {
return n.Notify(message)
}이 접근 방식은 상속과 유사한 기능을 제공하지만 타입 간의 결합도는 더 낮습니다.
직접 해보기
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert은 임의의 Notifier와 메시지를 인자로 받아, Notify를 호출한 결과를 반환합니다
// TODO: SendAlert 함수를 구현하세요
func main() {
scanner := bufio.NewScanner(os.Stdin)
// 이메일 알림자 이름 읽기
scanner.Scan()
emailName := scanner.Text()
// 이메일 주소 읽기
scanner.Scan()
emailAddress := scanner.Text()
// SMS 알림자 이름 읽기
scanner.Scan()
smsName := scanner.Text()
// 전화번호 읽기
scanner.Scan()
phoneNumber := scanner.Text()
// 알림 메시지 읽기
scanner.Scan()
alertMessage := scanner.Text()
// TODO: emailName과 emailAddress를 사용하여 EmailNotifier를 생성하세요
// TODO: smsName과 phoneNumber를 사용하여 SMSNotifier를 생성하세요
// TODO: SendAlert을 사용하여 두 알림자 모두를 통해 alertMessage를 전송하세요
// 그리고 결과를 출력하세요
// 사용되지 않는 변수 경고 억제 (변수를 사용할 때 이 부분을 제거하세요)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.
객체 지향 프로그래밍의 모든 레슨
8에러 처리와 OOP
error 인터페이스사용자 정의 에러 타입에러 래핑 (fmt.Errorf)센티넬 에러errors.Is()와 errors.As()Panic, Defer, Recover요약 - 파일 파서