Post

18. 인터페이스

18. 인터페이스

💡 인터페이스를 어떤 구체 타입으로 변경하는 것은 Type Assertion이라고 한다! (Type Conversion, 타입 변환과는 다르다)

✅ 0. 추가 내용

  • 아래 내용에서 나오는 타입 변환Type Assertion이다. Type Conversion(타입 변환)과는 다른 개념이므로 혼용하면 안된다 !!!

1.1 Type assertion (형 단언)

  • 대상은 반드시 인터페이스 값이어야 함. (콘크리트 → 콘크리트는 assertion 불가)
  • 런타임에 v의 동적 타입이 T인지 검사.
  • 실패 시 panic, 안전하게 하려면 comma, ok 사용.

1.2 Type conversion (형 변환)

  • 인터페이스 아니어도 됨. 대부분의 “호환 가능한” 타입 사이에서 명시적으로 변환.
  • 의미: C의 캐스트처럼 비트 재해석이 아니라, 목표 타입의 규칙에 맞춘 새 값 생성(때로는 복사/할당 발생).
  • 실패는 보통 컴파일 에러(불가능한 변환)이며, 런타임 panic은 아님.

✅ 1. 인터페이스

  • 인퍼테이스를 이용하면 메서드 구현을 포함한 구체화된 객체가 아닌 추상화된 객체로 상호작용할 수 있다
    1
    2
    3
    4
    
    type DuckInterface interface {
      Fly()
      Walk(distance int) int
    }
    
  • type 키워드, 인터페이스명, intefface 키워드를 통해 선언한다
  • 내부 블록에는 메서드 집합을 작성한다.
    • 메서드는 반드시 메서드명이 있어야 한다
    • 매개변수와 반환이 다르더라도 동일한 메서드명을 가질 수 없다
    • 인터페이스에서는 메서드 구현을 포함하지 않는다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

type Stringer interface {
	String() string
}

type Student struct {
	Name string
	Age  int
}

func (s Student) String() string {
	return fmt.Sprintf("%v is %d years old", s.Name, s.Age)
}

func main() {
	student := Student{"Minsu", 12}
	var stringer Stringer = student

	fmt.Printf("%s\n", stringer.String()) // Minsu is 12 years old
}
  • Stringer 인터페이스는 String() string 메서드를 포함한다. → string 타입을 반환하는 String() 메서드를 구현하는 모든 타입은 Stringer 인터페이스로 사용될 수 있다
    • 인터페이스 내의 모든 메서드를 구현해야 인터페이스로 사용될 수 있다

✅ 2. 인터페이스 왜 쓰나?

  • 인터페이스는 객체지향 프로그램에서 아주 중요한 역할을 한다
  • 인터페이스를 이용하면 구체화된 객체가 아닌 인터페이스로 메서드를 호출할 수 있기 때문에 큰 코드 수정 없이 구체화된 객체만 바꿔서 사용할 수 있게 된다

✅ 3. 덕 타이핑

만약 어떤 새를 봤는데 그 새가 오리처럼 걷고 오리처럼 날고 오리처럼 소리내면 나는 그 새를 오리라고 부르겠다

  • implements와 같은 키워드를 사용하지 않고, 인터페이스에 정의된 메서드들을 모두 포함한다면 그 인터페이스로 간주한다는 방식
  • 덕 타이핑에서는 인터페이스 구현 여부를 타입 선언 시 하는게 아니라 인터페이스가 사용될 때 결정한다. 따라서 서비스 제공자가 인터페이스를 정의할 필요 없이 서비스 이용자가 필요에 따라 인터페이스를 정의해서 사용할 수 있다

✅ 4. 인터페이스 기능 더 알기

5.1 포함된 인터페이스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Reader interface {
	Read() (n int, err error)
	Close() error
}

type Writer interface {
	Write() (n int, err error)
	Close() error
}

type ReadWriter interface {
	Reader
	Writer
}
  • 다른 인터페이스를 포함하는 인터페이스를 선언하면 인터페이스들의 메서드 집합을 모두 포함한다
  • 위 예시처럼 동일한 메서드가 있다면 하나의 메서드만 인터페이스에 포함된다

5.2 빈 인터페이스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func PrintVal(v interface{}) { // any로 대체 가능
	switch t := v.(type) {
	case int:
		fmt.Printf("v is int %d\n", int(t))
	case float64:
		fmt.Printf("v is float64 %d\n", float64(t))
	case string:
		fmt.Printf("v is string %d\n", string(t))
	default:
		fmt.Printf("Not Supported Type: %T:%v\n", t, t)
	}
}

func main() {
	PrintVal(10)
	PrintVal("Hello")
	PrintVal(6.4)
}
  • 빈 인터페이스는 interface{}와 같이 사용하며 어떤 값이든 받을 수 있는 함수, 메서드, 변수값을 만들 때 사용한다

    v.(type)는 switch문 내부에서만 사용 가능한 문법이다
    Go 1.18부터 interface{}는 미리 선언된 별칭인 any로 대체 가능하다

5.3 인터페이스 기본값

  • 인터페이스 변수의 가본값은 nil이다
  • 구현체가 없어 메모리 주솟값이 nil이기 때문에 런타임 에러가 발생한다
1
2
3
4
5
6
7
8
9
10
package main

type Attacker interface {
	Attack()
}

func main() {
	var att Attacker
	att.Attack() // panic: runtime error: invalid memory address
}

✅ 6. 인터페이스 변환하기

6.1 구체화된 타입으로 변환

  • v.(Type) 형식으로 인터페이스 변수를 구체화된 타입으로 변환할 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

type Stringer interface {
	String() string
}

type Student struct {
	Age int
}

func (s *Student) String() string {
	return fmt.Sprintf("Age: %v\n", s.Age)
}

func PrintAge(stringer Stringer) {
	s := stringer.(*Student)
	fmt.Printf("Age: %d\n", s.Age)
}

func main() {
	s := &Student{15}

	PrintAge(s)
}
  • Stringer 인터페이스는 메서드밖에 없기 때문에 인터페이스 변수에서 Age 필드에 직접 접근할 수 없다
  • s := stringer.(*Student)로 변환 후 필드에 접근할 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import "fmt"

type Stringer interface {
	String() string
}

type Student struct {
}

func (s *Student) String() string {
	return "Student"
}

type Actor struct {
}

func (a *Actor) String() string {
	return "Actor"
}

func Convert(stringer Stringer) {
	student := stringer.(*Student)
	fmt.Println(student)
}

func main() {
	var a *Actor = &Actor{}

	Convert(a) // panic: interface conversion: main.Stringer is *main.Actor, not *main.Student
}
  • *Student 타입과 *Actor 타입 모두 String() 메서드를 구현해 Stringer 인터페이스로 사용할 수 있다
  • 하지만 Conver 메서드의 인자로 받은 stringer에 *Actor 타입 인스턴스가 들어있기 때문에 런타임 에러가 발생한다

6.2 다른 인터페이스로 타입 변환

  • 인터페이스가 가리키고 있는 실제 인스턴스가 다른 인터페이스를 포함하는 경우 인터페이스 간 타입 변환이 가능하다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

type Reader interface {
	Read()
}

type Writer interface {
	Write()
}

type File struct {
}

func (f *File) Read() {

}

func ReadFile(reader Reader) {
	w := reader.(Writer) // panic: interface conversion: *main.File is not main.Writer: missing method Write
	w.Write()
}

func main() {
	file := &File{}

	ReadFile(file)
}
  • File 구조체가 Reader 인터페이스는 구현했지만 Writer 인터페이스는 구현하지 않았다
  • ReadFile(reader Reader) 메서드에서 Reader 인터페이스 → Writer 인터페이스간 변환은 컴파일 시점에는 문제가 없지만(실제 인스턴스가 두 인터페이스를 모두 구현했는지 확인 불가) reader 인터페이스 변수가 *File 타입 인스턴스를 가리키므로 Writer 인터페이스를 사용할 수 없어 런타임 에러가 발생한다
1
2
3
4
5
6
func ReadFile(reader Reader) {
	w, ok := reader.(Writer)
	if ok {
		w.Write()
	}
}
  • 타입 변환 시 변환 성공 여부를 같이 받을 수 있으며, 이 때는 타입 변환이 불가능하더라도 두 번째 반환값이 false로 반환될 뿐 런 타임 에러는 발생하지 않는다
1
2
3
if w, ok := reader.(Writer); ok {
	...
}
  • 많은 개발자가 이렇게 한 줄로 줄여서 표현하는 것을 더 선호한다
This post is licensed under CC BY 4.0 by the author.