예전 이와 유사한 포스팅을 한 적이 있다. (https://guiwoo.tistory.com/53)
당시 타입 변환 과 타입단언에 대해 궁금함이 생겨 작성했었는데. 이와 상당히 유사한 내용을 다룬다.
인터페이스의 변환
type Mover interface {
Move()
}
type Locker interface {
Lock()
Unlock()
}
type MoveLocker interface {
Locker
Mover
}
type bike struct{}
func (*bike) Move() {
fmt.Println("bike is moving")
}
func (*bike) Lock() {
fmt.Println("Locking the bike")
}
func (*bike) Unlock() {
fmt.Println("Unlocking the bike")
}
인터페이스 두개 mover와 locker를 설정하고 두 개를 모두 가지는 MoveLocker를 설정한다.
bike는 제공되는 모든 인터페이스를 만족하고
func TestBike(t *testing.T){
var (
ml MoveLocker
m Mover
)
ml = &bike{}
m = ml
ml = m
}
위 코드에서 ml 은 bike의 구현체로 설정이 가능하다. m = ml 까지도 인터페이스 변환이 가능하지만 m을 다시 ml로 변환은 불가능하다.
왜? mover 타입인 m 은 MoveLocker의 인터페이스를 충족해 줄 수 없기 때문에 불가능하다.
타입단언
그렇지만 MoveLocker 는 bike라는 구현체로 되어있음을 알고 있기 때문에 이를 명시적으로 타입을 변환할 수 있다.
b, ok := m.(bike)
fmt.Println("Does m has value of bike?:", ok)
ml = b
해당 타입 단언 때문에 지난번 포스팅을 작성했었다. 우리의 프로젝트 경우 echo 를 사용하고 미들웨어를 가져다가 컨택스트를 중간에 한번 커스텀하게 작성해서 다음 echo context로 랩핑 해서 넘겨준다.
실제 라우팅 핸들러에서는 ehco context를 받아서 위와 같이 타입단언으로 타입을 꺼내와서 사용하게 된다.
middleware ("custom echo context wrapping next()") => routing hanlder( c,_ := c.(*customContext)) 이런식이다.
인터페이스 오염
매번 인터페이스를 사용할 때 의문이 들곤 한다. 과연 이 인터페이스가 필요한가?라는 의문이다.
인터페이스의 잘못된 사용을 인터페이스 오염이라고 부르고 어떠한 경우에 이게 해당되는지 몇 가지 가이드라인을 제공하고 있다.
- 유저가 API의 실제 구현 디테일을 작성한다.
- API가 유지보수가 필요한 다양한 구현을 가지고 있다.
- API의 일부분이 변화할 수 있고 디커플링이 필요로 할 때 사용한다.
- 오직 테스트를 위해서만 사용한다.
- 변화로부터 쉽게 대응할 수 없다.
- 인터페이스가 코드를 더 좋게 만들어주지는 않는다.
모조품 만들기
모킹을 함에 있어 고에서는 인터페이스가 있어야 적용이 가능하다.
그렇다면 TDD를 하기 위해서 인터페이스를 적용해 코드를 디커플링 해야 하는가?
/*
*
Server
*/
type PubSub struct {
host string
}
func New(host string) *PubSub {
return &PubSub{
host,
}
}
func (ps *PubSub) Publish(key string, v interface{}) error {
fmt.Println("Actual PubSub: Publish")
return nil
}
func (ps *PubSub) Subscribe(key string) error {
fmt.Println("Actual PubSub: Subscribe")
return nil
}
해당 PubSub을 모조품을 만들기 위해 이 패키지 위에 Publish와 Subscribe를 만족하는 인터페이스를 작성하고 테스트해야 할까?
type publisher interface {
Publish(key string, v interface{}) error
Subscribe(key string) error
}
type mock struct{}
func (m *mock) Publish(key string, v interface{}) error {
fmt.Println("Mock PubSub: Publish")
return nil
}
func (m *mock) Subscribe(key string) error {
fmt.Println("Mock Subscribe: subscribe")
return nil
}
var _ publisher = (*mock)(nil)
func Test_Mocking(t *testing.T) {
pubs := []publisher{
&mock{},
New("localhost"),
}
for _, p := range pubs {
p.Publish("key", "value")
p.Subscribe("key")
}
}
이렇게 위의 코드처럼 코드를 호출하는 부에서 해당하는 인터페이스를 정의하고 사용할 수 있다.
고 언어 이기 때문에 이러한 분리가 가능해진다. 왜? 인터페이스 선언 방식에 있다.
해당 구조체 가 인터페이스를 만족한다면 너도 쟤도 모두 인터페이스 타입이 될 수 있는 것이다.
인터페이스를 선언함에 있어 위에서 말한 인터페이스 오염에 가까운 방식의 사고를 해왔고 실제 작성된 코드들 중 몇몇은 저렇게 프로젝트 상에 작성했다.
또한 이번챕터에서 주석된 부분에서 인터페이스를 주로 밖으로 내보내는 것이 아닌 인풋으로 사용하는 것이 일반적이라고 말하고 있다.
이렇게 인터페이스를 인풋으로 사용하게 되면? 위에 작성한 코드처럼 분리가 가능해지며, 명확한 API 설계가 가능해진다.
참 go라는 언어에 대해서 공부를 하면 할수록 부족한 부분이 너무 많이 느껴진다.
그래서 더 심도 있게 공부하고 go에 매력을 느끼는 것 같다.