인터페이스
Go 언어에서 인터페이스는 메서드 시그니처 집합을 정의하되 메서드 구현은 제공하지 않는 추상 타입입니다. 인터페이스의 핵심 개념은 동작을 설명하는 것이며, 구체적인 동작 구현은 인터페이스를 구현한 타입에서 제공합니다. 인터페이스는 Go 언어에서 다형성, 느슨한 결합 및 코드 재구현을 구현하는 데 널리 사용됩니다.
개념
Go 의 인터페이스 발전사에는 분수령이 있었습니다. Go1.17 및 그 이전 버전에서 공식 레퍼런스 매뉴얼의 인터페이스 정의는 다음과 같습니다: 메서드 집합.
An interface type specifies a method set called its interface.
인터페이스 구현의 정의는 다음과 같습니다.
A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface
번역하면, 어떤 타입의 메서드 집합이 인터페이스의 메서드 집합의 슈퍼세트이고, 해당 타입의 값을 인터페이스 타입의 변수에 저장할 수 있을 때, 해당 타입이 인터페이스를 구현했다고 합니다.
하지만 Go1.18 에서 인터페이스 정의가 변경되어 타입 집합으로 정의되었습니다.
An interface type defines a type set.
인터페이스 구현의 정의는 다음과 같습니다.
A variable of interface type can store a value of any type that is in the type set of the interface. Such a type is said to implement the interface
번역하면, 어떤 타입이 인터페이스의 타입 집합 내에 있고, 해당 타입의 값을 인터페이스 타입의 변수에 저장할 수 있을 때, 해당 타입이 인터페이스를 구현했다고 합니다. 또한 다음과 같은 추가 정의도 제공했습니다.
다음과 같은 경우 타입 T 가 인터페이스 I 를 구현했다고 할 수 있습니다.
- T 가 인터페이스가 아니며 인터페이스 I 의 타입 집합 요소인 경우
- T 가 인터페이스이며 T 의 타입 집합이 인터페이스 I 의 타입 집합의 서브세트인 경우
T 가 인터페이스를 구현하면 T 의 값도 해당 인터페이스를 구현합니다.
Go 가 1.18 에서 가장 큰 변화는 제네릭을 추가한 것이며, 새로운 인터페이스 정의는 제네릭을 위해 설계되었지만 기존 인터페이스 사용에는 전혀 영향을 미치지 않습니다. 또한 인터페이스는 두 가지로 분류됩니다.
- 기본 인터페이스 (
Basic Interface): 메서드 집합만 포함하는 인터페이스 - 일반 인터페이스 (
General Interface): 타입 집합을 포함하는 인터페이스
메서드 집합은 메서드의 집합이며, 타입 집합은 타입의 집합입니다.
TIP
이 개념이 다소 난해하게 느껴질 수 있지만, 실제로 위의 내용 전체를 이해할 필요는 없습니다.
기본 인터페이스
앞서 기본 인터페이스는 메서드 집합이라고 설명했습니다.
선언
먼저 인터페이스가 어떻게 생겼는지 살펴보겠습니다.
type Person interface {
Say(string) string
Walk(int)
}이는 Person 인터페이스로, 두 개의 공개 메서드 Walk 와 Say 가 있습니다. 인터페이스 내에서는 함수 매개변수 이름이 더 이상 중요하지 않지만, 매개변수 이름과 반환 값 이름을 추가하는 것도 허용됩니다.
초기화
인터페이스만으로는 초기화할 수 없습니다. 구체적인 구현이 없는 규격 집합일 뿐이기 때문입니다. 하지만 선언은 할 수 있습니다.
func main() {
var person Person
fmt.Println(person)
}출력
<nil>구현
예를 하나 살펴보겠습니다. 한 건설 회사가 특수 규격의 크레인을 원하여 크레인의 특수 규격과 도면을 제공하고 크레인에는 크레인 작업과 화물 들어올리기 기능이 있어야 한다고 명시했습니다. 건설 회사는 크레인을 제조할 책임이 없으며 규격만 제공했을 뿐입니다. 이것이 바로 인터페이스입니다. 회사 A 가 주문을 받아 자체 기술로 걸작 크레인을 제조하여 건설 회사에 인도했습니다. 건설 회사는 어떤 기술로 구현되었는지 신경 쓰지 않으며 걸작 크레인인지도 신경 쓰지 않습니다. 크레인 작업과 화물 들어올리기 기능만 있으면 됩니다. 단순히 일반 크레인으로 사용할 뿐입니다. 규격에 따라 구체적인 기능을 제공하는 것이 바로 구현입니다. 인터페이스 규격에 따라 기능을 사용하고 내부 구현을 추상화하는 것이 바로 인터페이스 지향 프로그래밍입니다. 시간이 지나 걸작 크레인에 고장이 발생하고 회사 A 도 도산하자, 회사 B 가 규격에 따라 더욱 강력한 메가 크레인을 제조했습니다. 크레인 작업과 화물 들어올리기 기능이 동일하므로 걸작 크레인과 완벽하게 호환되며 건설 일정에 영향을 주지 않아 공사가 무사히 완료되었습니다. 내부 구현은 변경되지만 기능은 변하지 않아 기존 사용에 영향을 주지 않고 자유롭게 교체할 수 있는 것이 바로 인터페이스 지향 프로그래밍의 장점입니다.
다음은 Go 로 위 상황을 설명한 것입니다.
// 크레인 인터페이스
type Crane interface {
JackUp() string
Hoist() string
}
// 크레인 A
type CraneA struct {
work int // 내부 필드가 다르다는 것은 내부 디테일이 다르다는 것을 의미
}
func (c CraneA) Work() {
fmt.Println("기술 A 사용")
}
func (c CraneA) JackUp() string {
c.Work()
return "jackup"
}
func (c CraneA) Hoist() string {
c.Work()
return "hoist"
}
// 크레인 B
type CraneB struct {
boot string
}
func (c CraneB) Boot() {
fmt.Println("기술 B 사용")
}
func (c CraneB) JackUp() string {
c.Boot()
return "jackup"
}
func (c CraneB) Hoist() string {
c.Boot()
return "hoist"
}
type ConstructionCompany struct {
Crane Crane // Crane 타입으로만 크레인 저장
}
func (c *ConstructionCompany) Build() {
fmt.Println(c.Crane.JackUp())
fmt.Println(c.Crane.Hoist())
fmt.Println("건축 완료")
}
func main() {
// 크레인 A 사용
company := ConstructionCompany{CraneA{}}
company.Build()
fmt.Println()
// 크레인 B 로 교체
company.Crane = CraneB{}
company.Build()
}출력
기술 A 사용
jackup
기술 A 사용
hoist
건축 완료
기술 B 사용
jackup
기술 B 사용
hoist
건축 완료위 예제에서 인터페이스 구현이 암시적이라는 것을 알 수 있습니다. 이는 기본 인터페이스 구현에 대한 공식 정의와도 일치합니다: 메서드 집합이 인터페이스 메서드 집합의 슈퍼세트이므로, Go 에서 인터페이스를 구현할 때 implements 키워드를 사용하여 명시적으로 어떤 인터페이스를 구현할지 지정할 필요가 없습니다. 인터페이스의 모든 메서드를 구현하기만 하면 해당 인터페이스를 구현한 것입니다. 구현이 있으면 인터페이스를 초기화할 수 있습니다. 건설 회사 구조체는 Crane 타입의 멤버 변수를 선언하여 Crane 인터페이스를 구현한 모든 값을 저장할 수 있습니다. Crane 타입 변수이므로 접근할 수 있는 메서드는 JackUp 과 Hoist 뿐이며, Work 와 Boot 같은 다른 내부 메서드는 접근할 수 없습니다.
앞서 모든 사용자 정의 타입은 메서드를 가질 수 있다고 언급했습니다. 구현 정의에 따르면 모든 사용자 정의 타입은 인터페이스를 구현할 수 있습니다. 다음은 몇 가지 특수한 예제입니다.
type Person interface {
Say(string) string
Walk(int)
}
type Man interface {
Exercise()
Person
}Man 인터페이스의 메서드 집합은 Person 의 슈퍼세트이므로 Man 도 Person 인터페이스를 구현합니다. 이는 더 많이 "상속"에 가깝습니다.
type Number int
func (n Number) Say(s string) string {
return "bibibibibi"
}
func (n Number) Walk(i int) {
fmt.Println("can not walk")
}타입 Number 의 기반 타입은 int 입니다. 다른 언어에서는 터무니없어 보일 수 있지만, Number 의 메서드 집합은 실제로 Person 의 슈퍼세트이므로 구현으로 간주됩니다.
type Func func()
func (f Func) Say(s string) string {
f()
return "bibibibibi"
}
func (f Func) Walk(i int) {
f()
fmt.Println("can not walk")
}
func main() {
var function Func
function = func() {
fmt.Println("do somthing")
}
function()
}마찬가지로 함수 타입도 인터페이스를 구현할 수 있습니다.
빈 인터페이스
type Any interface{
}Any 인터페이스는 메서드 집합이 없습니다. 구현 정의에 따르면 모든 타입은 Any 인터페이스를 구현합니다. 모든 타입의 메서드 집합은 공집합의 슈퍼세트이기 때문입니다. 따라서 Any 인터페이스는 모든 타입의 값을 저장할 수 있습니다.
func main() {
var anything Any
anything = 1
println(anything)
fmt.Println(anything)
anything = "something"
println(anything)
fmt.Println(anything)
anything = complex(1, 2)
println(anything)
fmt.Println(anything)
anything = 1.2
println(anything)
fmt.Println(anything)
anything = []int{}
println(anything)
fmt.Println(anything)
anything = map[string]int{}
println(anything)
fmt.Println(anything)
}출력
(0xe63580,0xeb8b08)
1
(0xe63d80,0xeb8c48)
something
(0xe62ac0,0xeb8c58)
(1+2i)
(0xe62e00,0xeb8b00)
1.2
(0xe61a00,0xc0000080d8)
[]
(0xe69720,0xc00007a7b0)
map[]출력을 통해 두 가지 출력 결과가 일치하지 않음을 알 수 있습니다. 실제로 인터페이스 내부는 (val, type) 으로 구성된 튜플로 볼 수 있습니다. type 은 구체적인 타입으로, 메서드 호출 시 구체적인 타입의 구체적인 값을 호출합니다.
interface{}이것도 빈 인터페이스이지만 익명 빈 인터페이스입니다. 개발 시에는 any 를 사용하여 interface{} 를 대체하는 경우가 많습니다. 모든 타입의 값을 수신한다는 것을 나타내기 위함입니다. 예제는 다음과 같습니다.
func main() {
DoSomething(map[int]string{})
}
func DoSomething(anything interface{}) interface{} {
return anything
}이후 업데이트에서 공식은 또 다른 해결책을 제시했습니다. 편의를 위해 any 를 사용하여 interface{} 를 대체할 수 있으며, 둘은 완전히 동등합니다. 전자는 단순히 타입 별칭일 뿐입니다. 다음과 같습니다.
type any = interface{}빈 인터페이스를 비교할 때는 기반 타입을 비교합니다. 타입이 일치하지 않으면 false 이며, 그 후에야 값을 비교합니다. 예를 들어
func main() {
var a interface{}
var b interface{}
a = 1
b = "1"
fmt.Println(a == b)
a = 1
b = 1
fmt.Println(a == b)
}출력은 다음과 같습니다.
false
true기반 타입이 비교 불가능하면 panic 이 발생합니다. Go 의 경우 내장 데이터 타입의 비교 가능 여부는 다음과 같습니다.
| 타입 | 비교 가능 | 기준 |
|---|---|---|
| 숫자 타입 | 예 | 값이 같은지 |
| 문자열 타입 | 예 | 값이 같은지 |
| 배열 타입 | 예 | 배열의 모든 요소가 같은지 |
| 슬라이스 타입 | 아니요 | 비교 불가 |
| 구조체 | 예 | 필드 값이 모두 같은지 |
| map 타입 | 아니요 | 비교 불가 |
| 채널 | 예 | 주소가 같은지 |
| 포인터 | 예 | 포인터가 저장한 주소가 같은지 |
| 인터페이스 | 예 | 저장된 데이터가 같은지 |
Go 에는 모든 비교 가능 타입을 나타내는 전용 인터페이스 타입이 있습니다. 바로 comparable 입니다.
type comparable interface{ comparable }TIP
비교 불가능한 타입을 비교하려고 하면 panic 이 발생합니다.
일반 인터페이스
일반 인터페이스는 제네릭을 위해 설계되었습니다. 제네릭을 익히면 일반 인터페이스도 자연스럽게 익히게 됩니다. 자세한 내용은 제네릭 문서를 참조하시기 바랍니다.
