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

배경

사내 API 서버를 Node.js에서 Go로 마이그레이션하는 작업을 시작했다. 동시성 처리와 메모리 사용량 개선이 주 목적이었다.

인터페이스의 암묵적 구현

Java 출신이라 implements 키워드가 없는 게 어색했다. Go는 메서드 시그니처만 일치하면 자동으로 인터페이스를 만족한다.

type Writer interface {
    Write([]byte) (int, error)
}

type FileLogger struct {
    path string
}

// Writer 인터페이스를 명시하지 않아도 자동 구현
func (f *FileLogger) Write(data []byte) (int, error) {
    // 구현
    return len(data), nil
}

실제 활용

로깅 시스템 리팩토링에 적용했다. 기존 코드는 구체 타입에 의존했는데, 인터페이스로 추상화하니 테스트가 쉬워졌다.

type Logger interface {
    Info(string)
    Error(string)
}

type APIHandler struct {
    logger Logger  // 구체 타입 대신 인터페이스
}

func NewAPIHandler(logger Logger) *APIHandler {
    return &APIHandler{logger: logger}
}

테스트에서는 mock 구조체를 만들어서 주입하면 됐다. 별도 라이브러리 없이도 간단했다.

type MockLogger struct {
    messages []string
}

func (m *MockLogger) Info(msg string) {
    m.messages = append(m.messages, msg)
}

func (m *MockLogger) Error(msg string) {
    m.messages = append(m.messages, msg)
}

인터페이스 분리 원칙

io.Reader, io.Writer 같은 표준 라이브러리 인터페이스를 보면서 작고 집중된 인터페이스가 얼마나 강력한지 알게 됐다. 하나의 메서드만 가진 인터페이스도 많았다.

결과적으로 패키지 간 의존성이 줄고 테스트 코드 작성이 수월해졌다. Go의 덕 타이핑 방식에 익숙해지는 데 시간이 좀 걸렸지만, 이제는 오히려 더 유연하다고 느낀다.