Go · 심화
동시성과 서버 설계로 깊어지는 Go
인터페이스 설계 - 작게 받고 구체적으로 돌려주기
소비자 쪽 인터페이스 정의, io.Reader·io.Writer 조합, 인터페이스 임베딩, nil 인터페이스와 nil 포인터 함정, 테스트 대역
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 타입 매개변수와 제약을 다뤘다. 제약도 인터페이스 문법으로 쓰지만, 이 장에서 다루는 것은 값을 추상화하는 인터페이스다. 인터페이스를 얼마나 작게 만들고 어디에 선언하느냐에 따라 코드의 결합도와 테스트 난이도가 크게 달라진다. 배달 주문 중계 서비스에서 주문을 배달원 쪽으로 넘기는 부분을 예로 들어, 필요한 만큼만 받고 구체 타입을 돌려주는 설계를 익힌다.
- 인터페이스를 구현하는 쪽이 아니라 쓰는 쪽(소비자)에서 정의하는 이유와 방법을 설명할 수 있다.
- io.Reader와 io.Writer를 조합해 입출력 경로를 코드 수정 없이 바꿀 수 있다.
- 인터페이스 임베딩으로 작은 인터페이스를 합쳐 쓸 수 있다.
- nil 인터페이스와 nil 포인터를 담은 인터페이스가 다르다는 점을 알고 함정을 피할 수 있다.
- 테스트 대역(test double)을 직접 만들어 외부 의존 없이 동작을 검증할 수 있다.
문제 상황
중계 서비스의 첫 버전은 주문을 받아 배달원 호출 모듈로 넘기는 함수 하나로 시작했다. 호출 모듈은 화면 출력, 파일 기록, 외부 호출 같은 여러 구현이 생길 예정이었다. 그래서 호출 모듈 패키지에 다음과 같은 큰 인터페이스를 먼저 만들어 두었다.
type DispatchService interface {
Dispatch(o Order) error
Cancel(id int) error
Status(id int) (string, error)
List() []Order
Close() error
}
중계 함수가 실제로 쓰는 메서드는 Dispatch 하나뿐이다. 그런데 테스트에서 대역을 만들려면 메서드 다섯 개를 모두 구현해야 한다. 구현체에 메서드가 하나 늘면 인터페이스와 모든 대역이 함께 바뀐다. 입력도 문제다. 주문 목록을 파일에서 읽는 함수와 문자열에서 읽는 함수를 따로 만들다 보니 같은 파싱 코드가 두 벌이 되었다.
이 장에서는 이 두 문제를 해결한다. 인터페이스를 쓰는 쪽에서 필요한 메서드만 골라 선언하고, 입력과 출력은 표준 라이브러리의 작은 인터페이스로 받는다.
소비자 쪽에서 작게 정의하기
Go 의 인터페이스는 메서드 집합이 맞으면 자동으로 만족된다. 구현 타입이 인터페이스를 알 필요가 없으므로, 인터페이스는 그것을 필요로 하는 패키지에 두는 편이 자연스럽다. 중계 함수가 필요한 것은 주문 하나를 넘기는 능력뿐이므로 그 함수 옆에 다음과 같이 선언한다.
type Dispatcher interface {
Dispatch(o Order) error
}
이렇게 하면 구현체 패키지는 중계 패키지를 가져올 필요가 없다. 의존 방향이 한쪽으로 정리되고, 대역을 만들 때 메서드 하나만 구현하면 된다. 반대로 함수가 돌려주는 값은 인터페이스가 아니라 구체 타입으로 한다. 호출한 쪽이 구체 타입의 다른 메서드와 필드를 쓸 수 있고, 필요하면 자기 쪽에서 더 좁은 인터페이스로 받으면 되기 때문이다.
| 기준 | 구현 쪽에 선언 | 소비자 쪽에 선언 | 비고 |
|---|---|---|---|
| 메서드 수 | 구현체 전체를 반영해 커진다 | 쓰는 메서드만 담아 작다 | 작을수록 대역이 쉽다 |
| 의존 방향 | 소비자가 구현 패키지를 가져옴 | 구현이 소비자를 몰라도 됨 | 순환 가져오기 예방 |
| 대역 작성 | 메서드를 모두 구현 | 필요한 메서드만 구현 | 함수 어댑터도 가능 |
| 반환 타입 | 인터페이스를 돌려주기 쉬움 | 구체 타입을 돌려줌 | 호출자가 좁혀 받음 |
함수 하나를 인터페이스로 바꿔 주는 어댑터도 흔히 쓴다. 표준 라이브러리의 http.HandlerFunc 와 같은 방식이다. 이 장의 코드에서는 DispatcherFunc 가 그 역할을 한다. 함수 타입에도 메서드를 붙일 수 있다는 점을 이용한다.
io.Reader 와 io.Writer 조합하기
입력을 받는 함수의 매개변수를 os.File 이나 string 으로 정하면 입력 원천이 고정된다. 메서드 하나짜리 io.Reader 를 받으면 파일, 문자열, 네트워크 연결, 압축 해제기가 모두 들어온다. 출력도 io.Writer 가 같은 역할을 한다. 이 장의 ParseOrders 는 io.Reader 를 받고, WriterDispatcher 는 io.Writer 에 기록한다. 표준 라이브러리는 이 둘을 겹쳐 쓰는 도구를 제공한다.
| 도구 | 받는 것 | 하는 일 | 예제에서의 쓰임 |
|---|---|---|---|
| strings.NewReader | string | 문자열을 Reader 로 바꾼다 | 주문 텍스트 입력 |
| io.TeeReader | Reader, Writer | 읽은 내용을 Writer 에도 복사한다 | 원본 보관본 만들기 |
| io.LimitReader | Reader, 바이트 수 | 읽기 상한을 건다 | 입력 크기 제한 |
| io.MultiWriter | Writer 여러 개 | 한 번 쓰면 모두에 쓴다 | 화면과 줄 수 세기 동시 출력 |
이 도구들은 모두 인터페이스를 받아 인터페이스를 만족하는 값을 돌려준다. 그래서 ParseOrders 를 고치지 않고도 입력에 상한을 걸거나 복사본을 남길 수 있다. 감싸는 순서는 안쪽이 원천에 가깝다. 예제에서는 문자열 Reader 를 TeeReader 로 감싸고, 다시 LimitReader 로 감싼다.
인터페이스 임베딩
작은 인터페이스를 합쳐 더 큰 인터페이스를 만들 때는 임베딩(embedding)을 쓴다. 표준 라이브러리의 io.ReadWriter 와 io.ReadCloser 가 같은 방식이다. 이 장에서는 주문을 넘기고, 끝나면 닫아야 하는 구현체를 위해 다음 인터페이스를 둔다.
type DispatchCloser interface {
Dispatcher
io.Closer
}
묶음 전송 구현체는 내부 버퍼에 모았다가 Close 에서 한 번에 내보낸다. 이런 구현체를 다루는 RelayAndClose 만 DispatchCloser 를 요구하고, 단순히 넘기기만 하는 Relay 는 Dispatcher 만 요구한다. 같은 구현체가 두 함수에 모두 들어가면서도 각 함수는 필요한 만큼만 요구한다. 임베딩은 이렇게 필요한 조각을 이름 붙여 합치는 용도로 쓰고, 처음부터 큰 덩어리를 만들지는 않는다.
nil 인터페이스와 nil 포인터 함정
인터페이스 값은 내부적으로 (동적 타입, 동적 값) 한 쌍이다. 인터페이스가 nil 과 같다는 것은 두 칸이 모두 비어 있다는 뜻이다. 구체 타입의 nil 포인터를 인터페이스에 담으면 타입 칸에는 포인터 타입이 들어가고 값 칸만 nil 이 된다. 이 값은 nil 과 같지 않다.
가장 흔한 사고는 오류를 돌려주는 함수에서 나온다. 다음 함수는 문제가 없을 때 pe 가 nil 이므로 nil 을 돌려주는 것처럼 보이지만, 반환 타입이 error 이므로 타입 칸이 채워진 값이 나간다. 호출자의 if err != nil 이 참이 되어 정상 경로가 오류 처리로 빠진다.
func badCheck(found bool) error {
var pe *ProblemError
if found {
pe = &ProblemError{OrderID: 1}
}
return pe // 타입 칸이 채워진 error
}
해결은 인터페이스를 돌려주는 함수에서 성공 경로에 리터럴 nil 을 직접 돌려주는 것이다. 오류 변수를 구체 포인터 타입으로 선언해 두고 마지막에 돌려주는 패턴을 피하면 된다. 이 함정의 배경은 Go FAQ 의 해당 항목에서 확인할 수 있다.
테스트 대역
소비자 쪽 인터페이스가 작으면 대역은 몇 줄이면 된다. 호출을 기록하고 지정한 순번에서 오류를 내는 대역이 중계 로직을 검증하기에 충분하다. 외부 호출이나 파일 없이 실패 경로까지 다룰 수 있다. 완성 코드의 fakeDispatcher 가 그 예이며, 실제 go test 에서는 다음처럼 쓴다. 테스트 파일 구성은 기본서에서 다룬 방식을 따르고, 표 주도 방식은 뒤에서 다시 다룬다.
func TestRelayStopsOnError(t *testing.T) {
orders := []Order{{ID: 1}, {ID: 2}, {ID: 3}}
f := &fakeDispatcher{failAt: 2}
sent, err := Relay(orders, f)
if err == nil || sent != 1 {
t.Fatalf("sent=%d err=%v", sent, err)
}
}
대역을 위해 모킹 라이브러리를 들이지 않아도 된다. 호출 인자를 모아 두는 것만 필요하면 DispatcherFunc 로 클로저를 감싸는 쪽이 더 짧다.
완성 코드
아래는 main.go 한 파일이다. go run main.go 로 실행한다.
package main
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"strings"
)
type Order struct {
ID int
Store string
Address string
}
func (o Order) String() string {
return fmt.Sprintf("#%d %s -> %s", o.ID, o.Store, o.Address)
}
// 소비자(Relay)가 필요한 만큼만 선언한 인터페이스
type Dispatcher interface {
Dispatch(o Order) error
}
// 작은 인터페이스를 임베딩으로 합친 인터페이스
type DispatchCloser interface {
Dispatcher
io.Closer
}
// 함수를 Dispatcher 로 바꾸는 어댑터
type DispatcherFunc func(Order) error
func (f DispatcherFunc) Dispatch(o Order) error { return f(o) }
// io.Reader 하나만 받아 주문 목록을 만든다.
func ParseOrders(r io.Reader) ([]Order, error) {
var orders []Order
sc := bufio.NewScanner(r)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" {
continue
}
store, addr, ok := strings.Cut(line, ",")
if !ok {
return nil, fmt.Errorf("형식 오류: %q", line)
}
orders = append(orders, Order{
ID: len(orders) + 1,
Store: strings.TrimSpace(store),
Address: strings.TrimSpace(addr),
})
}
return orders, sc.Err()
}
func Relay(orders []Order, d Dispatcher) (int, error) {
sent := 0
for _, o := range orders {
if err := d.Dispatch(o); err != nil {
return sent, fmt.Errorf("주문 %d: %v", o.ID, err)
}
sent++
}
return sent, nil
}
func RelayAndClose(orders []Order, dc DispatchCloser) (sent int, err error) {
defer func() {
if cerr := dc.Close(); err == nil {
err = cerr
}
}()
return Relay(orders, dc)
}
// io.Writer 에 바로 기록하는 구현체
type WriterDispatcher struct{ w io.Writer }
func NewWriterDispatcher(w io.Writer) *WriterDispatcher {
return &WriterDispatcher{w: w}
}
func (d *WriterDispatcher) Dispatch(o Order) error {
_, err := fmt.Fprintf(d.w, "배달 요청 %v\n", o)
return err
}
// 버퍼에 모았다가 Close 에서 내보내는 구현체
type BufferedDispatcher struct{ bw *bufio.Writer }
func NewBufferedDispatcher(w io.Writer) *BufferedDispatcher {
return &BufferedDispatcher{bw: bufio.NewWriter(w)}
}
func (d *BufferedDispatcher) Dispatch(o Order) error {
_, err := fmt.Fprintf(d.bw, "묶음 전송 %v\n", o)
return err
}
func (d *BufferedDispatcher) Close() error { return d.bw.Flush() }
var (
_ Dispatcher = (*WriterDispatcher)(nil)
_ DispatchCloser = (*BufferedDispatcher)(nil)
)
// 줄바꿈 수를 세는 Writer
type lineCounter struct{ n int }
func (c *lineCounter) Write(p []byte) (int, error) {
c.n += bytes.Count(p, []byte("\n"))
return len(p), nil
}
// 테스트 대역: 호출을 기록하고 지정한 주문에서 실패한다.
type fakeDispatcher struct {
got []Order
failAt int
}
func (f *fakeDispatcher) Dispatch(o Order) error {
if o.ID == f.failAt {
return errors.New("배달원 없음")
}
f.got = append(f.got, o)
return nil
}
type ProblemError struct{ OrderID int }
func (e *ProblemError) Error() string {
return fmt.Sprintf("문제 있는 주문 %d", e.OrderID)
}
func badCheck(found bool) error {
var pe *ProblemError
if found {
pe = &ProblemError{OrderID: 1}
}
return pe
}
func goodCheck(found bool) error {
if found {
return &ProblemError{OrderID: 1}
}
return nil
}
func main() {
const data = "골목치킨, 한빛로 12\n\n모퉁이분식 , 달빛길 3\n새벽국밥,별빛로 7\n"
var audit bytes.Buffer
src := io.LimitReader(io.TeeReader(strings.NewReader(data), &audit), 1024)
orders, err := ParseOrders(src)
if err != nil {
fmt.Println("파싱 실패:", err)
return
}
fmt.Println("주문 수:", len(orders))
fmt.Println("원본 보관본 일치:", audit.String() == data)
lines := &lineCounter{}
w := io.MultiWriter(os.Stdout, lines)
sent, err := Relay(orders, NewWriterDispatcher(w))
fmt.Println("전송:", sent, "오류:", err)
fmt.Println("출력 줄 수:", lines.n)
fmt.Println("--- 묶음 전송 ---")
sent, err = RelayAndClose(orders, NewBufferedDispatcher(os.Stdout))
fmt.Println("전송:", sent, "오류:", err)
fmt.Println("--- 테스트 대역 ---")
fake := &fakeDispatcher{failAt: 2}
sent, err = Relay(orders, fake)
fmt.Println("전송:", sent, "오류:", err)
fmt.Println("기록:", fake.got)
var stores []string
collect := DispatcherFunc(func(o Order) error {
stores = append(stores, o.Store)
return nil
})
_, _ = Relay(orders, collect)
fmt.Println("가게:", strings.Join(stores, "/"))
fmt.Println("--- nil 함정 ---")
bad := badCheck(false)
fmt.Printf("badCheck: err != nil 은 %v, 타입 %T\n", bad != nil, bad)
good := goodCheck(false)
fmt.Printf("goodCheck: err != nil 은 %v\n", good != nil)
var wd *WriterDispatcher
var d Dispatcher = wd
fmt.Println("nil 포인터를 담은 인터페이스 == nil:", d == nil)
}
줄별 해설
- Dispatcher, DispatchCloser: Dispatcher 는 Relay 가 필요로 하는 메서드 하나만 담는다. DispatchCloser 는 Dispatcher 와 io.Closer 를 임베딩해 RelayAndClose 만을 위해 합친 것이다.
- DispatcherFunc: 함수 타입에 Dispatch 메서드를 붙여 클로저를 Dispatcher 로 쓰게 한다. 메서드 본문은 자기 자신을 호출할 뿐이다.
- ParseOrders: io.Reader 만 받으므로 입력 원천을 모른다. strings.Cut 으로 쉼표 앞뒤를 나누고 TrimSpace 로 공백을 지운다. 빈 줄은 건너뛰기 때문에 ID 는 유효한 주문만 센 번호가 된다.
- Relay: 첫 오류에서 멈추고 그때까지 보낸 건수를 함께 돌려준다. 오류에는 주문 번호를 붙인다. 오류를 감싸는 정식 방법은 다음 장에서 다룬다.
- RelayAndClose: 이름 있는 반환값과 defer 를 써서, Relay 가 성공했을 때에 한해 Close 의 오류를 돌려준다. Relay 의 오류가 있으면 그것을 우선한다.
- var ( _ Dispatcher = (*WriterDispatcher)(nil) ... ): 구현체가 인터페이스를 만족하는지 컴파일 시점에 확인하는 관용구다. 메서드가 빠지면 이 줄에서 컴파일 오류가 난다. 값은 쓰지 않는다.
- BufferedDispatcher: bufio.Writer 에 쌓고 Close 에서 Flush 한다. Close 를 부르지 않으면 쌓인 내용이 나가지 않는다.
- lineCounter: Write 하나만 구현해 io.Writer 가 된다. io.MultiWriter 에 넣으면 화면 출력과 함께 줄 수가 올라간다.
- fakeDispatcher: failAt 과 같은 ID 의 주문에서 오류를 내고, 그 전 주문은 got 에 기록한다.
- main 의 입력 부분: 문자열 Reader 를 TeeReader 로 감싸 audit 에 복사본을 남기고, 다시 LimitReader 로 상한을 건다. 파싱이 끝나면 audit 은 원본과 같다.
- badCheck / goodCheck: 둘 다 찾은 문제가 없는 경로를 탄다. badCheck 는 error 에 nil 포인터를 담아 돌려주므로 != nil 이 참이다.
- 마지막 세 줄: nil 인 *WriterDispatcher 를 Dispatcher 변수에 넣으면 d == nil 이 거짓이 된다. 타입 칸이 채워지기 때문이다.
실행 결과
$ go run main.go
주문 수: 3
원본 보관본 일치: true
배달 요청 #1 골목치킨 -> 한빛로 12
배달 요청 #2 모퉁이분식 -> 달빛길 3
배달 요청 #3 새벽국밥 -> 별빛로 7
전송: 3 오류: <nil>
출력 줄 수: 3
--- 묶음 전송 ---
묶음 전송 #1 골목치킨 -> 한빛로 12
묶음 전송 #2 모퉁이분식 -> 달빛길 3
묶음 전송 #3 새벽국밥 -> 별빛로 7
전송: 3 오류: <nil>
--- 테스트 대역 ---
전송: 1 오류: 주문 2: 배달원 없음
기록: [#1 골목치킨 -> 한빛로 12]
가게: 골목치킨/모퉁이분식/새벽국밥
--- nil 함정 ---
badCheck: err != nil 은 true, 타입 *main.ProblemError
goodCheck: err != nil 은 false
nil 포인터를 담은 인터페이스 == nil: false
실무에서 자주 틀리는 것
1. 구현 패키지에 큰 인터페이스를 미리 만든다
틀린 코드는 구현체의 모든 메서드를 인터페이스로 옮겨 두고 소비자가 그것을 가져다 쓰는 방식이다. 소비자가 하나만 써도 대역은 다섯 개를 구현해야 한다.
// 틀림: 쓰지 않는 메서드까지 요구한다
func Relay(orders []Order, d DispatchService) error { ... }
고친 코드는 소비자 쪽에서 쓰는 메서드만 선언한다.
// 고침: 필요한 것만 선언한다
type Dispatcher interface {
Dispatch(o Order) error
}
func Relay(orders []Order, d Dispatcher) (int, error) { ... }
2. 생성자가 인터페이스를 돌려준다
생성자가 인터페이스를 돌려주면 호출자는 구체 타입의 Close 같은 다른 메서드를 쓰려고 타입 단언을 해야 한다.
// 틀림
func NewBufferedDispatcher(w io.Writer) Dispatcher {
return &BufferedDispatcher{bw: bufio.NewWriter(w)}
}
// 고침: 구체 타입을 돌려주고, 받는 쪽이 좁혀서 쓴다
func NewBufferedDispatcher(w io.Writer) *BufferedDispatcher {
return &BufferedDispatcher{bw: bufio.NewWriter(w)}
}
3. 조건부로 만든 포인터를 인터페이스로 그대로 돌려준다
포인터 변수를 선언해 두고 조건이 맞을 때만 채운 뒤 그대로 돌려주면, 조건이 맞지 않을 때 호출자가 nil 이 아닌 값을 받는다. 호출자가 메서드를 부르면 nil 포인터 역참조로 패닉이 난다.
// 틀림
func pick(name string) Dispatcher {
var d *WriterDispatcher
if name == "writer" {
d = NewWriterDispatcher(os.Stdout)
}
return d
}
// 고침: 없는 경우는 리터럴 nil 을 돌려준다
func pick(name string) Dispatcher {
if name == "writer" {
return NewWriterDispatcher(os.Stdout)
}
return nil
}
4. 버퍼 구현체의 Close 를 빠뜨린다
Dispatch 를 모두 호출해도 Close 에서 Flush 하기 전에는 아무것도 출력되지 않는다. 프로그램이 오류 없이 끝나지만 결과가 비어 있다.
// 틀림: Flush 가 일어나지 않는다
sent, err := Relay(orders, NewBufferedDispatcher(os.Stdout))
// 고침: Close 까지 묶은 함수를 쓴다
sent, err := RelayAndClose(orders, NewBufferedDispatcher(os.Stdout))
한눈에 보기
| 주제 | 규칙 | 이 장의 예 | 어기면 |
|---|---|---|---|
| 선언 위치 | 쓰는 쪽에서 선언한다 | Dispatcher | 대역이 커진다 |
| 크기 | 메서드 한두 개로 작게 유지한다 | Dispatch 하나 | 변경이 번진다 |
| 반환 | 구체 타입을 돌려준다 | NewBufferedDispatcher | 타입 단언이 늘어난다 |
| 입출력 | Reader 와 Writer 로 받는다 | ParseOrders | 원천이 고정된다 |
| 임베딩 | 필요한 조각만 합친다 | DispatchCloser | 불필요한 요구가 생긴다 |
| nil | 인터페이스 반환 경로는 리터럴 nil 을 쓴다 | goodCheck | nil 비교가 어긋난다 |
| 대역 | 작은 구조체나 함수 어댑터로 만든다 | fakeDispatcher | 테스트가 외부에 묶인다 |
io 패키지의 인터페이스 목록은 pkg.go.dev/io 에서 확인할 수 있다.
연습 문제
- 주문이 들어올 때 가게에 알림을 보내는 Announce(orders []Order, n Notifier) 함수를 만들고자 한다. 소비자 쪽에서 Notifier 인터페이스를 정의하고, 알림 메시지를 모아 두는 대역을 작성하라.
- 다음 코드의 출력을 예측하라.
var p *ProblemError var err error = p fmt.Println(err == nil, p == nil) - ParseOrders 를 고치지 않고, 머리말 줄 "# 주문 목록" 과 본문 두 줄을 이어서 읽어 주문으로 만들려면 입력을 어떻게 구성하는가. 머리말은 형식 오류가 나지 않도록 처리 방법도 함께 쓰라.
- BufferedDispatcher 가 DispatchCloser 를 만족하는지 컴파일 시점에 확인하는 한 줄을 쓰고, Close 메서드를 지우면 어떻게 되는지 설명하라.
정답과 해설
1. 쓰는 메서드는 하나이므로 인터페이스도 하나만 둔다.
type Notifier interface {
Notify(msg string) error
}
func Announce(orders []Order, n Notifier) error {
for _, o := range orders {
if err := n.Notify("새 주문 " + o.Store); err != nil {
return err
}
}
return nil
}
type fakeNotifier struct{ msgs []string }
func (f *fakeNotifier) Notify(msg string) error {
f.msgs = append(f.msgs, msg)
return nil
}
테스트에서는 Announce 를 부른 뒤 msgs 의 길이와 내용을 확인하면 된다. 실제 알림 구현체가 무엇이든 Notify 만 맞으면 들어간다.
2. 출력은 false true 다. err 에는 타입 칸이 *ProblemError 로 채워져 있어 nil 과 같지 않고, p 자체는 nil 포인터이므로 p == nil 은 참이다.
3. io.MultiReader 로 두 Reader 를 이어 붙인다. 다만 "# 주문 목록" 줄은 쉼표가 없어 ParseOrders 가 형식 오류를 낸다. 머리말을 입력 앞에 붙이지 말고 별도로 처리하거나, ParseOrders 가 # 로 시작하는 줄을 건너뛰도록 한 줄을 추가한다.
if strings.HasPrefix(line, "#") {
continue
}
이 줄을 빈 줄 검사 바로 뒤에 넣고, 입력은 io.MultiReader(strings.NewReader("# 주문 목록\n"), strings.NewReader(body)) 로 만든다. 입력 쪽을 바꾸는 것은 Reader 조합으로 해결되지만, 형식 규칙을 바꾸는 것은 파서의 몫이라는 점이 요점이다.
4. 확인 줄은 var _ DispatchCloser = (*BufferedDispatcher)(nil) 이다. Close 를 지우면 이 줄에서 "missing method Close" 계열의 컴파일 오류가 나고, RelayAndClose 호출 지점에서도 같은 이유로 오류가 난다. 실행 전에 구현 누락을 잡는 것이 이 관용구의 목적이다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.