- 출처: https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection
- 개요
Go의 표준 라이브러리의 인터페이스인 Writer를 이용하여 Dependency injection(이하 'DI')에 대해 알아본다. DI를 이용하면 test에 얼마나 많은 도움이 되는지 이해한다.
- Dependency injection
한국의 국룰 프레임워크인 Spring을 써본적이 있다면 DI 라는말을 들어본적이 있을것이다. 용어도 멋있기 때문에 오해가 있는데 DI에 관한 약간의 오해를 줄이고자 한다.
우선 DI를 하기 위해 반드시 프레임워크가 필요한것은 아니다. 또한 코드나 디자인을 복잡하게 하지 않는다. test를 용이하게 하고, 함수를 더 일반적인 목적으로 작성할 수 있도록 해준다.
이번 예제에서는 이름을 전달받아 인사를 하는 간단한 함수를 작성한다.
func Greet(name string) {
fmt.Printf("Hello, %s", name)
}
한 줄로 끝나는 정말 간단한 함수이다. 다만 인삿말을 표준 출력으로 출력해주고 있기 때문에 Test가 난감하다.
이렇게 되면 출력에 대한 의존성을 주입해야 한다. test에서는 어디서 또는 어떻게 출력하는지에 초점을 맞추는게 아니기 때문에 이런 상세 구현(Concrete type)형 보다는 인터페이스형을 인자로 받아야 한다. 인터페이스형을 받으면 상세 구현부를 변경할 수 있으므로 출력하는 부분을 제어해서 test가 가능해진다.
위의 문장을 보고나서 모르는 단어가 많고 어렵다라는 생각으로 포기 하지 않길 바란다. 위의 문장을 보고 한번에 이해했다면 아마 다른 언어나 프레임워크를 공부해본적이 있는 사람일것이고, 몰라도 앞으로의 내용을 보고 천천히 이해하면 된다.
잠시 fmt.Printf가 어떻게 생겼는지 살펴보도록 하자.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
함수를 살펴보면 Printf에서는 단순히 Fprintf를 호출해주고 있다. 첫번째 인자로는 os.Stdout을 넘겨주고 있다. os.Stdout이 정확히 무엇이며 Fprintf는 1번째 인자로 어떤 형을 받고 있는지 살펴보자.
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf)
p.free()
return
}
............
type Writer interface {
Write(p []byte) (n int, err error)
}
위의 코드로 보아 os.Stdout은 io.Writer 인터페이스를 구현한다는것을 알 수 있다. Go 코드를 작성하다보면 Writer 인터페이스를 많이 볼 수 있는데 이 인터페이스는 "해당 데이터를 어딘가에다 넣어라"라는 의미의 범용적인 목적의 함수이기 때문이다.
위의 결과를 종합해보면 결국 Greet 함수는 "인삿말을 Writer 인터페이스를 사용해서 어딘가에 넣어라"라는 동작을 수행한다고 볼 수 있다. 이 추상화된 인터페이스를 사용해서 코드를 좀 더 test 가능하도록 그리고 재사용이 가능하도록 만들어보자.
func TestGreet(t *testing.T) {
buffer := bytes.Buffer{}
Greet(&buffer, "Chris")
got := buffer.String()
want := "Hello, Chris"
if got != want {
t.Errorf("got %q want %q", got, want)
}
}
bytes package의 Buffer형은 Write(p []byte) (n int, err error) 함수를 구현하고 있기 때문에 Writer 인터페이스를 구현하고 있다. 그래서 우리만의 Writer로서 Greet에 이를 보내고, Greet 함수가 수행되고 난 뒤에는 buffer에 무엇이 쓰여졌는지를 검사해줄 수 있다.
Compile만 통과하고 test가 제대로 실패하는지를 검사하기 위해 아래처럼 코드를 작성해준다.
func Greet(writer *bytes.Buffer, name string) {
fmt.Printf("Hello, %s", name)
}
이제 통과하도록 코드를 작성해보자.
func Greet(writer *bytes.Buffer, name string) {
fmt.Fprintf(writer, "Hello, %s", name)
}
인삿말을 buffer로 보내기 위해 writer를 사용해주고 있다. 이전에 사용한 Printf는 표준 출력으로 데이터를 보내지만 Fprintf는 writer를 받아서 문자열을 해당 writer로 보낸다. 이제 test가 성공할것이다.
현재 코드에서 writer는 bytes.Buffer에 대한 pointer형이다. 기술적으로 보면 아무런 하자가 없지만 사용성이 좋다고 볼 수는 없다. 이유를 알아보기 위해 Greet 함수를 이용해서 표준 출력으로 출력하는 Go 어플리케이션 코드를 작성해보자.
func main() {
Greet(os.Stdout, "Elodie")
}
위의 코드를 실행하면 아래와 같은 오류가 날것이다.
./di.go:14:7: cannot use os.Stdout (type *os.File) as type *bytes.Buffer in argument to Greet
앞에서 fmt.Fprintf는 writer 인터페이스를 구현하는 bytes.Buffer와 os.Stdout형을 모두 인자로 받을 수 있다고 설명했었다. 만약 더 범용적인 목적의 인터페이스를 사용하도록 아래처럼 코드를 변경하면 Go application과 test에서 모두 사용이 가능해진다.
package main
import (
"fmt"
"os"
"io"
)
func Greet(writer io.Writer, name string) {
fmt.Fprintf(writer, "Hello, %s", name)
}
func main() {
Greet(os.Stdout, "Elodie")
}
결국 처음에 단순하게 구현했을 때에는 Greet이 인삿말만 인자로 받아 표준 출력으로 바로 인삿말을 출력했었다. 지금은 "데이터(인삿말)을 어딘가에 넣을것인지(표준 출력 or buffer에 저장)"에 대한 동작을 추상화시킨 writer 인터페이스를 추가로 받게 되어서 main과 test에서 둘 다 사용할 수 있는 함수가 되었다.
- io.Writer
io.Writer를 이용하면 좀 더 많은곳에서 유용하게 사용할 수 있다. 아래 코드를 살펴보자.
package main
import (
"fmt"
"io"
"log"
"net/http"
)
func Greet(writer io.Writer, name string) {
fmt.Fprintf(writer, "Hello, %s", name)
}
func MyGreeterHandler(w http.ResponseWriter, r *http.Request) {
Greet(w, "world")
}
func main() {
log.Fatal(http.ListenAndServe(":5000", http.HandlerFunc(MyGreeterHandler)))
}
위의 코드를 실행시켜고 http://localhost:5000에 접속하면 인삿말을 응답으로 받을 수 있다.
코드에서 중요한점은 http 코드에 대한 상세구현이 아니라 http.ResponseWriter 도 Writer 인터페이스를 구현했다는 점이다. io.Writer 인터페이스 덕분에 http 응답에도 인삿말을 쓸 수 있고, http용 Greet 함수를 추가적으로 구현하지 않고 재사용할 수 있다.
'Language > Go' 카테고리의 다른 글
Go - Concurrency (0) | 2022.01.04 |
---|---|
Go - mocking (0) | 2022.01.02 |
Go - maps (0) | 2021.12.31 |
Go - pointers & errors (0) | 2021.12.26 |
Go - struct, method, interface (0) | 2021.12.24 |
댓글