본문 바로가기
프로그래밍/Golang

Golang Interface - 암시적 인터페이스: 코드 유연성과 재사용성

by slowin 2024. 7. 27.

Golang 인터페이스 소개

Go 언어의 인터페이스는 다른 언어들과 비교하여 독특한 특징을 가지고 있습니다. Go의 인터페이스는 암시적으로 구현되며, 이는 Go의 유연성과 간결성을 높이는 핵심 요소입니다.

1. 인터페이스의 기본 개념

인터페이스는 메서드의 집합을 정의합니다. 구조체나 타입이 이 메서드들을 모두 구현하면, 해당 인터페이스를 구현한 것으로 간주됩니다. Go에서는 이를 명시적으로 선언할 필요가 없습니다.

기본 문법

인터페이스는 암묵적으로 구현

예제출처 : 링크

package main

import "fmt"

type I interface {
	M()
}

type T struct {
	S string
}

func (t T) M() {
	fmt.Println(t.S)
}

func main() {
	var i I = T{"hello"}
	i.M()
}

이 코드의 핵심은 Go 언어에서 인터페이스 구현이 암묵적으로 이루어진다는 점입니다.

구조체나 타입이 인터페이스가 요구하는 모든 메서드를 가지고 있다면, 그 타입은 자동으로 해당 인터페이스를 구현한 것으로 간주됩니다. 이는 코드의 유연성을 높이고, 불필요한 선언을 줄여 코드를 간결하게 만듭니다.

이러한 방식은 "덕 타이핑(duck typing)"으로 볼 수 있습니다. "오리처럼 걷고 오리처럼 꽥꽥거리면, 그것은 오리다"라는 원칙과 유사하게, Go에서는 "인터페이스가 요구하는 메서드를 모두 가지고 있다면, 그것은 해당 인터페이스를 구현한 것이다"라고 볼 수 있습니다.

 

3. 인터페이스 이름 지정 규칙

출처

 

  • 단일 메서드 인터페이스: 메서드 이름에 "-er" 접미사를 붙이거나 유사한 방식으로 행위자 명사를 만듭니다.
    • 예: Reader, Writer, Formatter, CloseNotifier 등.
  • 표준 메서드 이름: Read, Write, Close, Flush, String 등의 메서드 이름은 표준적인 시그니처와 의미를 가집니다. 혼란을 피하기 위해, 메서드가 같은 의미와 시그니처를 가지지 않는 한 이러한 이름을 사용하지 마세요.
  • 일관성: 만약 당신의 타입이 잘 알려진 타입의 메서드와 같은 의미를 가진 메서드를 구현한다면, 같은 이름과 시그니처를 사용하세요. 예를 들어, 문자열 변환 메서드는 ToString이 아닌 String으로 이름 짓습니다.
    • Go 언어에는 이미 널리 사용되는 표준 인터페이스와 메서드 이름들이 있습니다.
    • 만약 개발도중 만든 타입이 이러한 표준 인터페이스나 메서드와 유사한 기능을 수행한다면, 기존의 이름을 그대로 사용하는 것이 좋습니다.
    • 특히 String() 메서드는 Go에서 객체를 문자열로 변환할 때 사용하는 표준 메서드입니다.
type Person struct {
    Name string
    Age  int
}

// 좋은 예:
func (p Person) String() string {
    return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}

// 나쁜 예:
func (p Person) ToString() string {
    return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}

 

빈 인터페이스

Go에서 interface{}는 모든 타입을 받을 수 있는 빈 인터페이스입니다.

func PrintAnything(v interface{}) {
    fmt.Println(v)
}

타입 단언(Type Assertion)

인터페이스 값의 실제 타입을 확인하고 접근할 수 있습니다.

var w Writer = File{}
f, ok := w.(File)
if ok {
    // f는 File 타입입니다.
}

4. Golang 인터페이스 실제 사용 사례

인터페이스 임베딩

Go에서는 인터페이스를 다른 인터페이스에 포함시킬 수 있습니다.

type ReadWriter interface {
    Reader
    Writer
}

type Reader interface {
    Read(p []byte) (n int, err error)
}

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

인터페이스를 이용한 의존성 주입

type Database interface {
    Get(id string) (string, error)
    Set(id string, value string) error
}

type Service struct {
    db Database
}

func NewService(db Database) *Service {
    return &Service{db: db}
}

인터페이스를 활용한 테스트

type mockDatabase struct{}

func (m mockDatabase) Get(id string) (string, error) {
    return "mock data", nil
}

func (m mockDatabase) Set(id string, value string) error {
    return nil
}

func TestService(t *testing.T) {
    s := NewService(mockDatabase{})
    // 테스트 로직...
}

인터페이스를 이용한 다형성

type Shape interface {
    Area() float64
}

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func PrintArea(s Shape) {
    fmt.Printf("Area: %f\n", s.Area())
}

// 사용
r := Rectangle{Width: 3, Height: 4}
c := Circle{Radius: 5}
PrintArea(r)
PrintArea(c)

error 인터페이스

Go의 error 타입은 인터페이스입니다. 이를 활용해 커스텀 에러를 만들 수 있습니다.

type MyError struct {
    When time.Time
    What string
}

func (e MyError) Error() string {
    return fmt.Sprintf("%v: %v", e.When, e.What)
}

func run() error {
    return MyError{
        When: time.Now(),
        What: "something went wrong",
    }
}

 

5. Interfaces and methods

출처 : (go.dev)effective_go

Go 언어의 인터페이스는 매우 유연하며, 거의 모든 타입에 메서드를 붙일 수 있기 때문에 거의 모든 것이 인터페이스를 만족시킬 수 있습니다. 이러한 유연성을 보여주는 몇 가지 예시를 살펴보겠습니다.

 

HTTP 핸들러 인터페이스

net/http 패키지에서 정의된 Handler 인터페이스는 이러한 유연성의 좋은 예입니다.

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

func Handle(pattern string, handler Handler) {
	if use121 {
		DefaultServeMux.mux121.handle(pattern, handler)
	} else {
		DefaultServeMux.register(pattern, handler)
	}
}

이 인터페이스를 구현하는 어떤 객체든 HTTP 요청을 처리할 수 있습니다.

다음은 몇 가지 다양한 구현 방식입니다.

 

5.1. 구조체를 이용한 구현

type Counter struct {
    n int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ctr.n++
    fmt.Fprintf(w, "counter = %d\n", ctr.n)
}

 

5.2. 정수를 이용한 구현

type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    *ctr++
    fmt.Fprintf(w, "counter = %d\n", *ctr)
}

 

5.3. 채널을 이용한 구현

type Chan chan *http.Request

func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ch <- req
    fmt.Fprint(w, "notification sent")
}

 

 

5.4. 함수를 이용한 구현

http.HandlerFunc 타입을 사용하면 일반 함수를 HTTP 핸들러로 사용할 수 있습니다.

func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}

http.Handle("/args", http.HandlerFunc(ArgServer))

이 예시에서 ArgServer 함수는 http.HandlerFunc 타입으로 변환되어 Handler 인터페이스를 구현하게 됩니다.

  • 이 방식은 간단한 핸들러를 빠르게 구현할 때 유용합니다.
  • HandlerFunc는 함수 타입을 http.Handler 인터페이스로 변환하는 어댑터 역할을 합니다.
  • ArgServer 함수는 HandlerFunc 타입으로 타입 변환되어 http.Handler 인터페이스를 구현하게 됩니다.
  • 이 방식은 기존 함수를 쉽게 HTTP 핸들러로 사용할 수 있게 해주며, 람다 함수를 사용할 때도 편리합니다.

정리

Go 언어의 인터페이스는 간결하면서도 강력한 도구입니다. 암시적 구현을 통해 코드의 유연성을 높이고, 다양한 고급 기능을 통해 복잡한 시스템을 효과적으로 설계할 수 있게 해줍니다. 인터페이스를 잘 활용하면 테스트 용이성, 모듈화, 그리고 코드의 재사용성을 크게 향상시킬 수 있습니다.