본문 바로가기
Language/Go

Effective Go - Embedding

by ocwokocw 2022. 5. 29.

- 출처: https://go.dev/doc/effective_go#embedding

- Embedding

Go는 일반적으로 subclassing type 개념을 제공하지는 않지만, struct나 interface 내에서 type을 embedding 하면 해당 type이 구현하는 행위들을 가져올 수 있다.
 
Interface embedding은 매우 간단하다. 아래  코드는 io.Reader와 io.Writer 이다.
 
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}
 
io package는 이런 메소드들을 구현할 수 있는 객체들을 지정하는 다른 interface도 노출한다. 예를 들어 io.ReadWriter는 Read와 Write를 모두 포함하는 interface이다. 물론 Read, Write 메소드를 명시적으로 나열해서 io.ReadWriter를 정의할 수도 있지만 2 가지 interface를 이용하여 하나의 새로운 interface를 정의하는것이 더 쉽고, 상관관계로서 의미를 갖는다.
 
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
    Reader
    Writer
}
 
ReadWriter는 Reader가 할 수 있는것과 Writer가 할 수 있는것을 모두 할 수 있다. interface 내에서는 interface만 embedding 될 수 있다.
 
struct에도 동일한 기조를 적용할 수 있지만 더 광범위한 의미가 있다. bufio package는 bufio.Reader와 bufio.Writer 2개의 struct type을 갖는데, package io와 유사한 interface를 구현한다. 그리고 bufio는 buffered reader/writer를 구현하는데, embedding을 사용하여 reader와 writer를 하나의 struct로 병합한다. struct내의 type 목록을 갖고 있지만 해당 field의 이름을 갖지 않는다.
 
// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}
 
embedded된 요소들은 struct에 대한 pointer인데, 사용되기 전에 유효한 struct를 가리키도록 초기화되어야 한다. ReadWriter struct는 아래처럼 작성될 수도 있다.
 
type ReadWriter struct {
    reader *Reader
    writer *Writer
}
 
그러나 field의 메소드를 승격시켜서 io interface를 만족시키기 위해서는 아래처럼 forwarding 메소드가 필요하다.
 
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
    return rw.reader.Read(p)
}
 
struct를 직접 embedding하면 이런 상황을 피할 수 있다. bufio.ReadWriter는 bufio.Reader와 bufio.Writer의 메소드들을 갖게될뿐만 아니라 io.Reader, io.Writer, io.ReadWriter interface도 만족하게 된다.
 
embedding이 subclassing과 다른점이 있다. 어떤 type을 embed할 때 해당 type의 메소드는 외부 type의 메소드가 되지만, 호출될 때는 해당 메소드의 receiver는 외부 type이 아니라 내부 type이다. 예제에서 bufio.ReadWriter의 Read method가 실행될 때도 receiver는 ReadWriter 자신이 아니라 ReadWriter의 reader field가 된다.
 
embedding은 편의상 이점도 있다. 아래 코드의 struct는 embedded field 와 함께 이름을 갖는 일반 field도 함께 존재한다.
 
type Job struct {
    Command string
    *log.Logger
}
 
Job type은 Print, Printf, Println 과 *log.Logger의 다른 메소드를 갖고 있다. Logger type에 field 명을 부여할 수도 있었지만 그렇게 할 필요는 없다. 초기화만 해주면 job에 대한 log를 남길 수 있다.
 
job.Println("starting now...")
 
Logger는 Job의 일반적인 field 라서 Job을 위한 생산자에서 일반적인 방법으로 초기화할 수 있다.
 
func NewJob(command string, logger *log.Logger) *Job {
    return &Job{command, logger}
}
 
또는 아래처럼 복합 리터럴로도 초기화가 가능하다.
 
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
 
만약 embedded field 를 직접 참조해야 한다면 package 한정자를 무시한 field type 이름이 ReadWriter struct의 Read 메소드처럼 field 이름으로 사용된다. 만약 Job 의 변수 job의 *log.Logger에 접근할 필요가 있을 때, job.Logger 와 같이 사용할 수 있는데, Logger의 메소드를 정제해야 하는 상황일 경우 유용하다.
 
func (job *Job) Printf(format string, args ...interface{}) {
    job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
 
type을 embedding 하면 이름 충돌 문제가 발생하지만 이를 해결하기 위한 규칙은 간결하다. 우선 field나 메소드 X는 더 깊이 중첩된 type의 중첩된 부분의 어떤 다른 요소 X를 덮어쓴다. 만약 log.Logger가 Command 라는 이름을 가진 field나 메소드를 갖고 있을 때, Job의 Command field가 이를 지배한다.
 
두번째로 만약 같은 이름이 같은 깊이의 수준에서 나타나면 일반적으로는 error로 취급된다. 만약 Job struct가 Logger 라는 이름의 다른 field나 메소드를 포함하고 있다면 log.Logger를 포함하는건 잘못된 경우이다. 그러나 만약 중복된 이름이 type 정의하는 부분 이외의 프로그램에서 언급되지 않았다면 이런 경우는 괜찮다. 이런 자격은 외부에서 포함된 유형의 변경사항으로 부터 일부분 보호 기능을 제공한다. 만약 다른 하위 type의 다른 field 와 충돌나는 field가 추가되어도 두 field들이 모두 사용 되지 않는다면 괜찮다.
 
 

'Language > Go' 카테고리의 다른 글

Effective Go - Concurrency - 2  (0) 2022.06.01
Effective Go - Concurrency - 1  (0) 2022.06.01
Effective Go - Interfaces and other types  (0) 2022.05.26
Effective Go - Methods  (0) 2022.05.17
Effective Go - Array and Slice  (0) 2022.05.08

댓글