본문 바로가기
Language/Go

Effective Go - Interfaces and other types

by ocwokocw 2022. 5. 26.

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

- Interfaces

Go에서 interface는 객체의 행동을 지정하는 방법이다. 만약 A가 B를 할 수 있다면 A는 여기에 사용될 수 있다는것을 의미한다. Go에서 1개 혹은 2개의 메소드들로 구성된 interface 들을 어렵지 않게 볼 수 있는데, Write 메소드를 구현하는 io.Writer interface 처럼 이름은 대체적으로 해당 메소드들로 부터 파생된다.
 
하나의 형은 여러 개의 interface들을 구현할 수 있다. 예를 들어 아래 코드에서 Sequence는 Len(), Less(i, j int) bool, Swap(i, j int)를 포함하는 sort.Interface를 구현하는 collection의 경우 sort 패키지의 루틴으로 정렬이 가능하며, custom formatter도 가지므로 두 가지를 인터페이스를 만족한다.
 
type Sequence []int

// Methods required by sort.Interface.
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
    copy := make(Sequence, 0, len(s))
    return append(copy, s...)
}

// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
    s = s.Copy() // Make a copy; don't overwrite argument.
    sort.Sort(s)
    str := "["
    for i, elem := range s { // Loop is O(N²); will fix that in next example.
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}
 

- Conversions

위의 코드에서 Sequence의 String 메소드는 Sprint가 slice에 대해 이미 수행했던 작업을 다시 수행하고 있다. 만약 Sprint를 호출하기 전에 Sequence를 일반적인 []int 형태로 변환하면 재작업을 하지 않음으로서 속도를 향상시킬 수 있다.
 
func (s Sequence) String() string {
    s = s.Copy()
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}
 
이 메소드는 String 메소드로부터 Sprintf 호출을 안전하게 하기 위한 변환 기법의 또 다른 예라고 할 수 있다. Sequence와 []int 두 형은 형의 이름만 무시하면 같다고 볼 수 있기 때문에 그들 사이의 변환은 이상적이다. 변환은 새로운 값을 만들지 않고 기존 값에 새로운 유형이 있는것처럼 일시적으로 동작한다.
 
Go에서 메소드의 다른 set에 접근하기 위해 표현식 형을 변환하는 동작은 빈번하게 일어난다. 예를 들어 sort.IntSlice를 이용하면 전체 예제를 아래와 같이 줄일 수 있다.
 
type Sequence []int

// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
    s = s.Copy()
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}
 
Sorting과 printing 2개의 interface 들을 구현하는 Sequence 대신에 data가 여러 형(Sequence, sort.IntSlice와 []int)으로 변환되는 성질을 이용하고 있다. 이런 형태가 실제로 많이 사용 되는 형태는 아니지만 더 효율적이다.
 

- Interface conversions and type assertions

변환을 할 때 type switch 형태를 많이 이용한다. 아래 예제 코드는 fmt.Printf가 type switch를 사용하여 어떻게 값을 문자열로 변환 하는지를 간단히 나타낸것이다. 이미 string 이라면 실제 string 값을 그대로 반환하고, 만약 String 메소드를 갖고 있다면 해당 메소드를 호출한 결과를 반환한다.
 
type Stringer interface {
    String() string
}

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}
 
첫 case는 concrete 값을 찾는다. 2번째 case 에서는 interface를 다른 interface로 변환한다.
 
위와 같이 여러 가지 형이 아니라 하나의 형만 신경쓰고 싶다면 어떻게 해야할까? 이럴 경우 위의 switch 문에서 하나의 case만 검사해도 동작하긴 하지만 더 간단하게 쓰고 싶다면 type assertion 문법을 쓰면 된다.
 
str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}
 
위의 코드에서 ok는 value가 string인지 안전하게 확인해주는 역할을 한다. 만약 value가 string이 아니어도 str의 형은 여전히 string 형을 유지하는데 zero value(string형의 zero value인 빈 문자열)로 초기화된다.
 

- Generality

만약 어떤 type이 인터페이스를 구현하긴 하지만 인터페이스에 정의된 외의 메소드들이 exported 되지 않으면 해당 type을 exported할 필요가 없다. 이렇게 인터페이스만 package 외부로 드러낸다는것은 인터페이스에 기술된 이외의 행동이 필요 없다는것을 더 분명하게 해주기 때문이다.
 
이런 경우 생산자(Constructor)는 실제 구현 type 보다는 인터페이스를 반환해주는것이 좋다. 예를 들어 crc32.NewIEEE와 adler32.New hash 라이브러리는 모두 인터페이스 형인 hash.Hash32를 반환한다. Go에서 Adler-32를 CRC-32 알고리즘으로 대체 하려면 생산자 호출을 변경해야 하는데, 이렇게 해야 나머지 코드들이 알고리즘 변경의 영향을 받지 않기 때문이다.
 
이와 비슷하게 다양한 crypto package의 스트리밍 암호화 알고리즘을 같이 엮여있는 block 암호 알고리즘에서 분리해낼 수 있다. crypto/cipher package의 Block 인터페이스는 데이터의 단일 블록 암호화를 제공하는 블록 암호의 행동을 기술하고 있다. 그리고 bufio package와 비슷하게 이 인터페이스를 구현하는 cipher package는 블록 암호화의 구현 세부사항을 알 필요 없이 Stream 인터페이스로 표현되는 스트리밍 암호화를 만드는데 사용된다.
 
crypto/cipher 인터페이스는 아래와 같이 생겼다.
 
type Block interface {
    BlockSize() int
    Encrypt(dst, src []byte)
    Decrypt(dst, src []byte)
}

type Stream interface {
    XORKeyStream(dst, src []byte)
}
 
아래는 블록 암호화를 스트리밍 암호화로 변환하는 counter mode(CTR) stream에 대한 정의이다. 블록 암호화의 세부사항은 추상화되어있음을 알 수 있다.
 
// NewCTR returns a Stream that encrypts/decrypts using the given Block in
// counter mode. The length of iv must be the same as the Block's block size.
func NewCTR(block Block, iv []byte) Stream
 
NewCTR은 하나의 특정 암호화 알고리즘이나 데이터 소스에만 적용되지 않고 Block 인터페이스를 구현하는 어떤 구현체나 어떠한 Stream 에도 적용이 가능하다. 인터페이스를 반환하고 있으므로 CTR을 다른 암호화 모드로 변환할 수 있다. 생산자 호출은 변경해야하지만 이외의 코드에서는 어차피 결과로 반환한 Stream을 다루기 때문에 영향을 받지 않는다.
 

- Interfaces and methods

메소드를 가진다는것은 인터페이스를 만족할 수 있다는것을 의미한다. http package에는 Handler 인터페이스가 정의되어 있다. 어떤 객체라도 Handler 를 구현하면 HTTP 요청을 처리할 수 있다.
 
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
 
ResponseWriter는 client 에게 응답을 반환하기 위해 필요한 메소드들에 접근할 수 있는 기능을 제공하는 인터페이스이다. 해당 메소드들은 표준 Write 메소드를 포함하고 있어서 http.ResponseWriter는 io.Writer가 사용되는 곳이라면 동일하게 사용이 가능하다. Request는 client로 부터 받은 요청을 파싱할 수 있다.
 
간단한 설명을 위해 HTTP 요청이 언제나 GET이라고 가정해보자. 아래 코드는 페이지에 방문한 횟수를 카운팅하는 코드이다.
 
// Simple counter server.
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)
}
 
앞서 언급한것을 상기하면서 Fprintf가 http.ResponseWriter에 어떻게 출력하는지 유의하면서 코드를 보면 이해가 수월할것이다. 만약 테스트용이 아닌 실제 서버라면 ctr.n은 sync나 atomic 같은 package를 사용해서 동시성 접근에 대한 보호가 필요할것이다.
 
아래는 위의 counter server를 url에 mapping하는 코드이다.
 
import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)
 
Counter를 struct로 정의하긴했지만 필요한게 멤버가 integer 하나 뿐이라면 더 간단하게 할수도 있다.
 
// Simpler counter server.
type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    *ctr++
    fmt.Fprintf(w, "counter = %d\n", *ctr)
}
 
만약 프로그램에서 특정 페이지에 방문한 사실을 알아야 할 필요가 있어서 내부적인 상태를 가진다면 어떻게 해야할까? channel을 사용하면 된다.
 
// A channel that sends a notification on each visit.
// (Probably want the channel to be buffered.)
type Chan chan *http.Request

func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ch <- req
    fmt.Fprint(w, "notification sent")
}
 
만약 server 바이너리를 실행할 때 사용되는 인자를 /args에 표시하고 싶다고 가정해보자. 이 요구사항을 구현하는 함수를 작성하는 일은 간단하다.
 
func ArgServer() {
    fmt.Println(os.Args)
}
 
이 기능을 어떻게 HTTP 서버기능으로 변경할까? ArgServer를 값을 무시하는 어떤 유형의 메소드로 만들 수도 있지만 더 간결한 방법이 있다. pointer와 인터페이스를 제외한 타입을 위한 메소드를 정의할 수 있기 때문에 함수를 위한 메소드를 작성할 수 있다. http package를 살펴보면 아래와 같은 코드가 있다.
 
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers.  If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}
 
HandlerFunc 는 ServeHTTP 메소드를 가진 타입이라서 해당 타입의 값은 HTTP 요청을 서비스할 수 있다. 해당 메소드를 자세히 살펴보면 receiver f는 함수이고 메소드는 f를 호출한다. 언뜻 보면 이상해보일지 모르지만 receiver가 채널이 되고, 메소드가 채널에서 보내는것과 다를바가 없다.
 
ArgServer를 HTTP server로 만들기 위해서는 해당 함수 시그니처를 갖도록 변경해야 한다.
 
// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}
 
이제 ArgServer는 HandlerFunc와 같은 시그니처를 갖게 되어서 ServeHTTP 메소드에 접근하기 위한 타입으로 변경될 수 있다. 이 기능을 /args url에 바인딩 하는 코드는 간단하다.
 
http.Handle("/args", http.HandlerFunc(ArgServer))
 
누군가 /args 페이지에 방문하면 해당 페이지의 handler는 HandlerFunc형의 ArgServer값을 갖게 된다. HTTP server는 ArgServer를 receiver로 하는 해당 타입의 ServeHTTP 메소드를 실행하게 되고, ArgServer를 실행(HandlerFunc.ServeHTTP 안의 f(w, req)가 실행되므로)하게 된다.
 
 

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

Effective Go - Concurrency - 1  (0) 2022.06.01
Effective Go - Embedding  (0) 2022.05.29
Effective Go - Methods  (0) 2022.05.17
Effective Go - Array and Slice  (0) 2022.05.08
Effective Go - New 와 Make  (0) 2022.05.07

댓글