Go 인터페이스와 덕 타이핑 이해하기

배경

회사에서 마이크로서비스 일부를 Node에서 Go로 마이그레이션하는 작업을 맡았다. Java 경험이 있어서 정적 타입 언어는 익숙했지만, Go의 인터페이스는 처음엔 이해가 안 갔다.

Java vs Go 인터페이스

Java에서는 명시적으로 implements 키워드를 사용한다.

public class Dog implements Animal {
    public void Speak() { ... }
}

Go는 다르다. 인터페이스를 만족하는 메서드만 구현하면 자동으로 인터페이스를 구현한 것으로 간주된다.

type Speaker interface {
    Speak() string
}

type Dog struct{}

func (d Dog) Speak() string {
    return "Woof!"
}

// Dog는 자동으로 Speaker를 구현

실제 활용

HTTP 핸들러를 추상화할 때 유용했다.

type Handler interface {
    Handle(w http.ResponseWriter, r *http.Request) error
}

type UserHandler struct {
    db *sql.DB
}

func (h UserHandler) Handle(w http.ResponseWriter, r *http.Request) error {
    // 실제 로직
    return nil
}

func WrapHandler(h Handler) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if err := h.Handle(w, r); err != nil {
            http.Error(w, err.Error(), 500)
        }
    }
}

장점

  1. 의존성 역전이 쉽다. 외부 라이브러리 타입에 대해 내 패키지에서 인터페이스를 정의하고 사용할 수 있다.
  2. 테스트용 목(mock) 작성이 간단하다.
  3. 작은 인터페이스 조합이 자연스럽다. io.Reader, io.Writer 같은 단일 메서드 인터페이스가 강력한 이유다.

주의점

빈 인터페이스 interface{}는 any 타입처럼 동작한다. 남발하면 타입 안정성을 잃는다. 꼭 필요한 경우만 사용하고, 가능하면 구체적인 인터페이스를 정의하는 게 좋다.