시작하며

Go에서 인터페이스(Interface)는 객체지향 언어에서 흔히 볼 수 있는 개념이지만, Go만의 독특한 Duck Typing 방식으로 동작한다. 인터페이스를 통해 코드 간의 결합도를 낮추고 유연한 설계를 만드는 방법을 살펴본다.
Interface 기본
Interface 선언 형태
// type interface_name interface_keyword
type employeeInterface interface {
Work()
Rest(hour int) int
// 메서드 선언 시 주의사항
// 1 메서드는 반드시 메서드명이 있어야 한다.
// 2 Overload 안된다.
// 3 인터페이스에서는 메서드 구현을 포함하지 않는다.
}Interface 사용 이유
Interface를 사용하면 구체적으로 구현한 객체가 아닌, Interface 만으로 메소드를 호출할 수 있다. 따라서 필요에 따라 객체를 갈아 끼우기 용이하다. 또한 그 과정에서 발생하는 코드의 수정량도 매우 적어지며, Interface를 사용하는 로직 입장에서는 안에 어떤 객체가 들어오는지 알아야 할 필요 없이 코드를 유연하게 사용할 수 있다.
Interface는 이런 측면에서 Abstraction Layer(추상화 계층)이다. 또한 이러한 계층을 통해 서비스 제공자와 서비스 사용자 간의 연결고리를 끊는 것을 decoupling(디커플링)이라고 한다.
Interface 추가 기능
Inner Interface
Interface 안에 Interface를 인자로 가질 수 있다. 구현체가 Interface 안의 모든 메소드를 구현해야만 그 Interface로 사용될 수 있다.
빈 Interface{}는 어디에 쓰나?
빈 Interface인 interface{}는 어떤 값이든 받을 수 있는 함수, 메소드 등을 만들 때 사용한다. 예를 들어 함수의 인자로 빈 interface를 받으면, 해당 interface의 타입에 따라 다른 로직이 동작하게 만들 수도 있다.
Interface의 기본값
nil 값이다. 따라서 Interface를 사용할 때에는 nil 값인지 여부를 꼭 확인하고 사용해야 한다.
Interface 변환
Interface를 다른 타입으로 변환하기
Interface 본래의 구체화된 타입으로 복원하는 경우에 많이 사용한다. 내부적으로 어떤 인스턴스를 가리키고 있었는지도 중요한 문제이다.
var testItf Interface
origin := testItf.(beforeType)type Stringer interface{
String() string
}
type Student struct{
Age int
}
func (s *Student) String() string {
return fmt.Sprintf("Student Age: %d", s.Age)
}
func PrintAge(stringer Stringer){
s := stringer.(*Student)
fmt.Printf("age : %d\n", s.Age)
}
func main(){
s := &Student{15}
// Stringer 인터페이스는 Age를 가지고 있지 않기 때문에 본래 타입인 Struct로 변환하지 않으면 접근 불가
// stringer 변수 내부에서 *Student를 가리키고 있으므로 무사히 변환 가능
PrintAge(s)
}Interface를 다른 인터페이스로 타입 변환하기
인터페이스가 인터페이스로 변환하는 경우에는 변경 전 인터페이스를 포함하지 않아도 된다. 하지만 변경하려고 하는 인터페이스를 근본 인터페이스가 포함하고 있는 상태여야 한다.
package main
type Reader interface{
Read()
}
type Closer interface{
Close()
}
type File struct{
}
func(f *File) Read(){
}
// func(f *File) Close(){
// }
func ReadFile(reader Reader){
c := reader.(Closer)
c.Close()
// sol 1.
// c, ok := reader.(Closer)
// if ok{
// c.Close()
// }
// sol 2.
// if c, ok := reader.(Closer); ok{
// c.Close()
// }
}
func main(){
file := &File{}
ReadFile(file)
}정리하며
Go의 인터페이스는 Duck Typing 방식으로 동작하기 때문에 구현 시점에 명시적으로 인터페이스 구현 여부를 선언할 필요가 없다. 이를 통해 서비스 제공자가 아닌 사용자 중심의 코딩이 가능해진다. 인터페이스를 잘 활용하면 결합도를 낮추고(decoupling), 코드 변경 없이 구현체만 교체하는 유연한 설계를 만들 수 있다.
연습 문제
Q1. Interface를 사용하는 이유는 무엇인가?
Interface를 사용하면 구체적으로 구현한 객체가 아닌 Interface만으로 메소드를 호출할 수 있다.
필요에 따라 객체를 갈아 끼우기 용이하고, 코드 수정량도 매우 적어진다.
(Abstraction Layer 이자 Decoupling 작용)
Q2. Duck Typing이란 무엇이며, 장점은 무엇인가?
Interface 구현 여부를 타입 선언 시점에 명시적으로 나타낼 필요 없이, 인터페이스에 정의한 메서드의 포함 여부만으로
특정 인터페이스를 포함하는지 여부를 결정하는 것. Java처럼 implements를 명시하지 않아도 되므로
사용자 중심(인터페이스 적용 여부를 사용자가 선택)의 코딩이 가능하다.
Q3. 아래 코드에서 런타임 에러가 발생하지 않도록 두 가지 방법으로 고쳐라.
// sol 1.
c, ok := reader.(Closer)
if ok {
c.Close()
}
// sol 2.
if c, ok := reader.(Closer); ok {
c.Close()
}