[Go] 인터페이스 본문
Interface
인터페이스란 구현을 포함하지 않는 메소드 집합으로, 구체화된 타입이 아닌 인터페이스만 가지고 메소드를 호출할 수 있어 추후 프로그램 요구사항 변경 시 유연하게 대처할 수 있다.
Go에서는 인터페이스 구현 여부를 특정 타입이 인터페이스에 해당하는 메소드를 가지고 있는지로 판단하는 덕 타이핑을 지원한다.
인터페이스를 사용하는 것으로 객체 간 상호작용을 정의할 수 있으며, 덕 타이핑을 통해 사용자 중심의 코딩이 가능하다.
인터페이스 선언 방법은 다음과 같다.
type DuckInterface interface {
Fly()
Walk(distance int) int
}
인터페이스 또한 구조체처럼 타입의 한 종류이기 때문에 type
키워드를 작성해야 한다. 인터페이스를 작성할 때 유의해야 할 몇 가지 주의 사항을 살펴보자.
- 메소드는 반드시 메소드명이 있어야 한다
- 매개변수와 반환이 다르더라도 이름이 동일한 메소드는 있을 수 없다
- 인터페이스에서는 메소드 구현을 포함하지 않는다
필자의 생각으로 가장 주의깊게 봐야하는 것이 바로 2번 항목이다. 메소드 포스팅에서도 그러했듯이 Java를 사용해본 적이 있는 개발자들에게 인터페이스라는 이름이 굉장히 친숙할 것이다.
하지만, Go에서는 메소드 오버로딩을 지원하지 않기 때문에 매개변수와 반환 값이 다르더라도 동일한 메소드명을 사용할 수 없다.
인터페이스의 사용 방법을 나타낸 간단한 코드 예제다.
package main
import "fmt"
type Stringer interface {
String() string
}
type Student struct {
Name string
Age int
}
func (s Student) String() string {
return fmt.Sprintf("이름 : %s, 나이 : %d", s.Name, s.Age)
}
func main() {
student := Student{ "철수", 12 }
var stringer Stringer
stringer = student
fmt.Printf("%s\n", string.String()) // 이름 : 철수, 나이 : 12
}
덕 타이핑
Go언어에서는 어떤 타입이 인터페이스를 포함하고 있는지 여부를 결정할 때 덕 타이핑 방식을 사용한다.
덕 타이핑이란 타입 선언 시 인터페이스 구현 여부를 명시적으로 나타낼 필요 없이 인터페이스에 정의한 메소드 포함 여부만으로 결정하는 방식이다.
덕 타이핑 방식의 장점은 사용자 중심의 코딩을 할 수 있다는 점이다.
덕 타이핑에서는 인터페이스 구현 여부를 타입 선언에서 하는 게 아니라 인터페이스가 사용될 때 해당 타입이 인터페이스에 정의된 메소드를 포함했는지 여부로 결정하기 때문에, 서비스 제공자가 인터페이스를 정의할 필요 없이 구체화된 객체만 제공하고 서비스 이용자가 필요에 따라 인터페이스를 정의해서 사용할 수 있다.
인터페이스를 포함하는 인터페이스
구조체가 다른 구조체를 포함하는 필드를 가질 수 있듯이, 인터페이스 또한 다른 인터페이스를 포함할 수 있다. 이를 포함된 인터페이스라고 한다.
다음 예제를 살펴보자.
type Reader interface {
Read() (n int, err error)
Close() error
}
type Writer interface {
Write() (n int, err error)
Close() error
}
type ReadWriter interface {
Reader // Reader의 메소드 집합을 포함한다.
Writer // Writer의 메소드 집합을 포함한다.
}
위의 예제에서 ReadWriter
인터페이스에 포함된 Reader
, Writer
인터페이스에 동일한 이름의 메소드인 Close()
가 존재한다.
이 경우, 두 인터페이스 존재하는 각각의 Close()
메소드가 하나로 합쳐져서 하나의 Close()
메소드만 ReadWriter
인터페이스에 포함되게 된다.
그리고 덕 타이핑에 의해서 Read()
, Write()
, Close()
메소드를 구현한 타입은 세 인터페이스를 모두 사용 가능하다.
빈 인터페이스
interface{}
는 메소드가 없는 빈 인터페이스로 모든 타입을 받을 수 있는 함수, 메소드, 변수값을 만들 때 주로 사용된다.
package main
import "fmt"
func PrintVal(v interface{}) {
switch t := v.(type) {
case int:
fmt.Println("v is int")
case float64:
fmt.Println("v is float64")
case string:
fmt.Println("v is string")
default:
fmt.Println("Not Supported Type")
}
}
type Student struct {
Name string
}
func main() {
PrintVal(10) // v is int
PrintVal(3.14) // v is float64
PrintVal("hello") // v is string
Printval(Student{15}) // Not Supported Type
}
인터페이스의 기본값은 nil
인터페이스의 기본값은 유효하지 않은 메모리 주소를 나타내는 nil
이다.
만약 인터페이스 변수를 초기화하지 않고 사용하게 될 경우 인터페이스의 기본값은 nil
이기 때문에, 런 타임 에러가 발생하게 된다.
type Attacker interface {
Attack()
}
func main() {
var att Attacker
att.Attack() // runtime error: invalid memory address or nil pointer dereference
}
인터페이스 변환
인터페이스 변수를 타입 변환을 통해서 구체화된 다른 타입이나 다른 인터페이스로도 변환할 수 있다.
구체화된 다른 타입으로 변환하는 방법은 인터페이스를 본래의 구체화된 타입으로 복원할 때 주로 사용한다. 사용 방법은 인터페이스 변수 뒤에 .
을 찍고 ()
안에 변경하려는 타입을 써주면 된다.
var a Interface
t := a.(ConcreteType)
구체적인 사용 예시를 위해 코드를 작성해보자.
package main
import "fmt"
type Stringer interface {
String() string
}
type Student struct {
Name string
}
func (s *Student) String() string {
return fmt.Sprintf("Student Name: %s", s.Name)
}
func PrintAge(stringer Stringer) {
s := string.(*Student)
fmt.Printf("Name: %s\n", s.Name)
}
func main() {
s := &Student{"철수"}
PrintAge(s) // Name: 철수
}
위의 코드에서 Stringer
인터페이스를 *Student
타입으로 형변환시켰다. 그 이유는 Stringer
인터페이스는 String()
메소드만 존재하기 때문에 Name
필드에 접근할 수 없기 때문이다.
즉, Name
필드를 호출하기 위해 인터페이스를 형변환시킨 것이다. PrintAge()
로 넘겨진 stringer
인스턴스 변수 내부에 *Student
타입 인스턴스를 가리키고 있었기 때문에 에러 없이 변환이 이루어진다.
다른 인터페이스로 타입 변환하기
인터페이스를 또 다른 인터페이스로 변환할 수 있다. 구체화된 타입으로 변환할 때와는 달리 변경되는 인터페이스가 변경 전 인터페이스를 포함하지 않아도 된다.
하지만 인터페이스가 가리키고 있는 실제 인스턴스가 변환하고자 하는 다른 인터페이스를 포함해야 한다.
Student
타입이 Interface A
와 Interface B
인터페이스를 모두 포함하고 있는 경우에 다음과 같이 Student
인스턴스를 가리키고 있는 Interface A
변수 a
는 Interface B
로 변환이 가능하다.
var a Interface_A = Student{}
b := a.(Interface_B)
타입 변환이 아예 불가능한 타입이라면 컴파일 타임 에러가 발생하고 문법적으로 문제 없지만, 실행 도중 타입 변환에 실패하는 경우에는 런 타임 에러가 발생한다.
이를 예방하기 위해 타입 변환 가능 여부를 확인하여 런 타임 에러가 발생하지 않는 타입 변환 방법에 대해 알아보자.
타입 변환 반환값을 두 개의 변수로 받으면 타입 변환 가능 여부를 두 번째 반환값으로 알려준다. 이때 타입 변환이 불가능하면 두 번째 반환값이 false
로 반환되며, 런 타임 에러는 발생하지 않는다.
var a Interface
t, ok := a.(Type)
만약 변환에 실패하더라도 런 타임 에러를 방지할 수 있기 때문에 인터페이스 변환 시 아래의 코드와 같이 변환 여부를 확인하기를 추천한다.
func ReadFile(reader Reader) {
if c, ok := reader.(Closer); ok {
...
}
}
'Dev' 카테고리의 다른 글
단일 트랜잭션 유지 그리고 PGMQ (1) | 2024.12.15 |
---|---|
[Go] 함수 고급 (0) | 2024.12.02 |
[Go] 메소드 (2) | 2024.11.09 |
[Go] 슬라이스 (0) | 2024.11.07 |
그림으로 이해하는 객체 지향 설계 5원칙 [SOLID] (2) | 2024.07.20 |