The Ultimate Go Study Guide의 내용을 참고하여 작성했습니다.
소프트웨어를 설계할 때 구체적인 타입Concrete type을 이용한 높은 결합도 대신 인터페이스를 이용한 느슨한 결합을 추구해야 합니다. 서브 타이핑을 이용한 다형성 제공은 부모 자식 타입 간의 결합도가 높아서 다양한 문제를 일으킬 수 있습니다. 반면 인터페이스를 이용한 다형성 제공은 결합도가 낮아져 변경에 보다 더 유연하게 대처할 수 있게 해줍니다.
그렇다면 모든 것을 인터페이스로 정의하여 구현하는 것은 바람직한 설계일까요? 여기서는 잘못된 인터페이스 사용 예제를 설명합니다.
인터페이스를 잘못 사용 방법 중 하나가 성급한 추상화입니다. 인터페이스를 이용해서 설계를 했을때 디커플링을 통해 얻은 이득이 확실하지 않다면 인터페이스를 사용하지 말아야합니다. 예제를 통해서 알아보겠습니다.
<1>
에서 Server
인터페이스를 정의하고 있습니다. 인터페이스는 코드 사용자 입장에서 API가되는데 너무 많은 동작이 정의되어 있습니다. <2>
에서 server
는 Server
인터페이스를 구현하고 있습니다.
package server // <1> type Server interface { Start() error Stop() error Wait() error } // <2> type server struct { host string } ...
<3>
에서 Server
타입을 리턴하는 NewServer()
라는 팩토리 함수를 정의합니다. 내보내기하지 않은 server
타입의 포인터를 인터페이스에 저장하여 리턴하는 냄새나는 코드입니다. 또한 Go 언어에서는 인터페이스를 리턴하도록 만드는 것을 냄새나는 코드로 취급하기도 합니다.
근거는 "인터페이스를 받아들이고 구조체를 리턴하라" 라는 가이드가 있습니다. 함수 호출 시 인터페이스를 반환하는 이유는 사용자가 반환 값을 가지고 할 수 있는 행위를 표현하기 위해서입니다.
Go 언어에서는 암시적 인터페이스 구현을 지원하고 있습니다. 이 때문에 미리 추상화할 필요 없이 사후에 추상화 필요성이 생길 경우 우아한 방법으로 추상화하는 것이 가능합니다.
... // <3> func NewServer(host string) Server { return &server{host} } func (s *server) Start() error { return nil } func (s *server) Stop() error { return nil } func (s *server) Wait() error { return nil }
<4>
에서 srv
가 인터페이스가 아닌 구체적인 타입이어도 아무런 문제가 없습니다. 이런 방시의 인터페이스 사용은 디커플링 같은 이점도 주지 않고 있습니다. 그냥 단순히 추상화 수준만 높여서 코드를 복잡하게 보이게 만들 뿐입니다.
func main() { // <4> srv := server.NewServer("localhost") srv.Start() srv.Stop() srv.Wait() }
위 코드의 문제를 정리해보면,
server
구조체의 전체 기능과 동일한 기능을 정의한 Server
인터페이스를 정의합니다.Server
인터페이스는 내보내기 하지만, server
구조체는 내보내기 하지 않습니다.NewServer()
팩토리 함수는 반환값으로 내보내기 하지 않은 server
타입을 지정한 Server
인터페이스를 반환합니다.Server
인터페이스를 제거해도 사용자 API에 변경 사항이 없습니다.Server
인터페이스가 API 변경에 대해서 디커플링하고 있지 않습니다.이제 위에서 살펴본 예제에서 인터페이스를 오용한 부분을 바로잡아 보겠습니다.
<1>
에서 인터페이스를 사용하던 부분을 제거하고 Server
구조체를 내보내기 합니다. <2>
에서 NewServer()
함수는 Server
구조체의 포인터를 반환합니다. 이후 나머지 코드에서는 Server
구조체를 바로 사용하는 것으로 인터페이스 오용을 바로잡았습니다.
package server // <1> type Server struct { host string } // <2> func NewServer(host string) *Server { return &Server{host} } func (s *Server) Start() error { return nil } func (s *Server) Stop() error { return nil } func (s *Server) Wait() error { return nil }
func main() { srv := server.NewServer("localhost") srv.Start() srv.Stop() srv.Wait() }
참고 자료에서는 아래와 같은 경우에 인터페이스를 사용하라고 가이드하고 있습니다.
개인적으로는 인터페이스를 이용해서 더 적극적으로 추상화를 해야한다고 생각하는 경우가 더 있습니다.
익스트림 프로그래밍XP의 원칙 중 필요하다고 간주할 때까지 기능을 추가하지 않는 것이 좋다YAGNI(You aren't gonna need it)는 원칙이 있습니다. 추상화에도 아래와 같이 인용해 볼 수 있겠네요.
실제로 필요할 때 추상화하고, 필요하다고 예상될 때는 절대로 추상화하지 마세요.
발표를 들으며 항상 배웁니다. 좋은 발표 감사합니다 ❤️