본문으로 건너뛰기

타입 단언과 타입 스위치

인터페이스 값에서 구체적인 타입을 꺼내거나 타입에 따라 분기하는 것이 타입 단언(type assertion)과 타입 스위치(type switch)입니다. 이를 통해 Go에서 다형성을 안전하게 구현할 수 있습니다.

타입 단언(Type Assertion)

인터페이스 값이 특정 타입임을 주장하고 해당 타입의 값을 꺼냅니다.

package main

import "fmt"

type Animal interface {
Sound() string
}

type Dog struct{ Name string }
type Cat struct{ Name string }

func (d Dog) Sound() string { return "멍멍" }
func (c Cat) Sound() string { return "야옹" }

func main() {
var a Animal = Dog{Name: "바둑이"}

// 1. 단순 타입 단언 — 실패 시 패닉!
dog := a.(Dog)
fmt.Println(dog.Name, dog.Sound()) // 바둑이 멍멍

// 2. 안전한 타입 단언 — ok 패턴 (권장)
if dog, ok := a.(Dog); ok {
fmt.Println("Dog:", dog.Name)
} else {
fmt.Println("Dog가 아닙니다")
}

if cat, ok := a.(Cat); ok {
fmt.Println("Cat:", cat.Name)
} else {
fmt.Println("Cat이 아닙니다") // 출력됨
}

// 3. 잘못된 타입 단언 — 패닉 발생
defer func() {
if r := recover(); r != nil {
fmt.Println("패닉 복구:", r)
}
}()
_ = a.(Cat) // panic: interface conversion: interface {} is Dog, not Cat
}

안전한 타입 단언 패턴

실무에서는 항상 ok 패턴을 사용합니다.

package main

import "fmt"

type Shape interface {
Area() float64
}

type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }
type Triangle struct{ Base, Height float64 }

func (c Circle) Area() float64 { return 3.14159 * c.Radius * c.Radius }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (t Triangle) Area() float64 { return 0.5 * t.Base * t.Height }

// 인터페이스 추가 메서드 확인
type Resizable interface {
Resize(factor float64) Shape
}

func (c Circle) Resize(factor float64) Shape {
return Circle{Radius: c.Radius * factor}
}

func processShape(s Shape) {
fmt.Printf("기본 넓이: %.2f\n", s.Area())

// 추가 기능 지원 여부 확인
if r, ok := s.(Resizable); ok {
bigger := r.Resize(2.0)
fmt.Printf("2배 크기 넓이: %.2f\n", bigger.Area())
} else {
fmt.Println("이 도형은 크기 조절을 지원하지 않습니다")
}
}

func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 6},
Triangle{Base: 3, Height: 4},
}

for _, s := range shapes {
fmt.Printf("=== %T ===\n", s)
processShape(s)
}
}

타입 스위치(Type Switch)

여러 타입에 따라 분기할 때 switch v.(type) 구문을 사용합니다.

package main

import "fmt"

func describe(i any) string {
switch v := i.(type) {
case nil:
return "nil"
case int:
return fmt.Sprintf("정수: %d", v)
case float64:
return fmt.Sprintf("실수: %.2f", v)
case bool:
if v {
return "불리언: true"
}
return "불리언: false"
case string:
return fmt.Sprintf("문자열: %q (길이: %d)", v, len(v))
case []int:
return fmt.Sprintf("int 슬라이스: %v (길이: %d)", v, len(v))
case map[string]any:
return fmt.Sprintf("맵: %v", v)
case error:
return fmt.Sprintf("에러: %s", v.Error())
default:
return fmt.Sprintf("알 수 없는 타입: %T = %v", v, v)
}
}

func main() {
values := []any{
nil,
42,
3.14,
true,
false,
"hello",
[]int{1, 2, 3},
map[string]any{"key": "value"},
fmt.Errorf("에러 발생"),
struct{ X, Y int }{1, 2},
}

for _, v := range values {
fmt.Println(describe(v))
}
}

타입 스위치 — 인터페이스 조합

package main

import (
"fmt"
"math"
)

type Shape interface {
Area() float64
}

type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }
type Triangle struct{ A, B, C float64 } // 세 변의 길이

func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}

func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

func (t Triangle) Area() float64 {
// 헤론의 공식
s := (t.A + t.B + t.C) / 2
return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}

// 타입별 상세 정보 출력
func shapeDetail(s Shape) string {
switch v := s.(type) {
case Circle:
return fmt.Sprintf("원(반지름=%.1f, 지름=%.1f)", v.Radius, v.Radius*2)
case Rectangle:
return fmt.Sprintf("사각형(%.1f×%.1f)", v.Width, v.Height)
case Triangle:
return fmt.Sprintf("삼각형(변: %.1f, %.1f, %.1f)", v.A, v.B, v.C)
default:
return fmt.Sprintf("미지의 도형(%T)", v)
}
}

// 여러 타입을 한 케이스에서 처리
func isPolygon(s Shape) bool {
switch s.(type) {
case Rectangle, Triangle:
return true
default:
return false
}
}

func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 3, Height: 4},
Triangle{A: 3, B: 4, C: 5},
}

for _, s := range shapes {
fmt.Printf("%s\n 넓이=%.2f, 다각형=%v\n",
shapeDetail(s), s.Area(), isPolygon(s))
}
}

JSON 파싱의 타입 단언

실제 현업에서 자주 쓰이는 JSON 파싱 후 타입 단언 패턴입니다.

package main

import (
"encoding/json"
"fmt"
)

func parseJSON(data string) {
var result any
if err := json.Unmarshal([]byte(data), &result); err != nil {
fmt.Println("파싱 에러:", err)
return
}

// JSON 파싱 결과는 map[string]any, []any, float64, string, bool, nil
processValue("root", result)
}

func processValue(key string, v any) {
switch val := v.(type) {
case map[string]any:
fmt.Printf("%s: {객체}\n", key)
for k, child := range val {
processValue(key+"."+k, child)
}
case []any:
fmt.Printf("%s: [배열, 길이=%d]\n", key, len(val))
for i, item := range val {
processValue(fmt.Sprintf("%s[%d]", key, i), item)
}
case float64:
fmt.Printf("%s: 숫자=%v\n", key, val)
case string:
fmt.Printf("%s: 문자열=%q\n", key, val)
case bool:
fmt.Printf("%s: 불리언=%v\n", key, val)
case nil:
fmt.Printf("%s: null\n", key)
}
}

func main() {
jsonData := `{
"name": "홍길동",
"age": 30,
"active": true,
"scores": [95.5, 87.0, 92.3],
"address": {
"city": "서울",
"zip": "04524"
},
"memo": null
}`

parseJSON(jsonData)
}

errors.As와 errors.Is — 에러 타입 단언

Go의 에러 처리에서 타입 단언이 사용되는 실전 패턴입니다.

package main

import (
"errors"
"fmt"
)

// 커스텀 에러 타입
type ValidationError struct {
Field string
Message string
}

func (e *ValidationError) Error() string {
return fmt.Sprintf("검증 오류 [%s]: %s", e.Field, e.Message)
}

type NotFoundError struct {
Resource string
ID int
}

func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s(ID=%d)을 찾을 수 없습니다", e.Resource, e.ID)
}

var ErrPermissionDenied = errors.New("권한이 없습니다")

func findUser(id int) error {
if id <= 0 {
return &ValidationError{Field: "id", Message: "양수여야 합니다"}
}
if id > 100 {
return fmt.Errorf("사용자 조회 실패: %w", &NotFoundError{Resource: "User", ID: id})
}
if id == 13 {
return fmt.Errorf("접근 차단: %w", ErrPermissionDenied)
}
return nil
}

func handleError(err error) {
if err == nil {
fmt.Println("성공!")
return
}

// errors.As: 특정 타입으로 언래핑
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("입력값 오류 — 필드: %s, 내용: %s\n", ve.Field, ve.Message)
return
}

var nfe *NotFoundError
if errors.As(err, &nfe) {
fmt.Printf("리소스 없음 — %s ID=%d\n", nfe.Resource, nfe.ID)
return
}

// errors.Is: 특정 에러 값 확인
if errors.Is(err, ErrPermissionDenied) {
fmt.Println("권한 오류 — 로그인 필요")
return
}

fmt.Println("알 수 없는 오류:", err)
}

func main() {
testCases := []int{-1, 42, 999, 13}
for _, id := range testCases {
fmt.Printf("ID=%d: ", id)
handleError(findUser(id))
}
}

인터페이스 업캐스팅과 다운캐스팅

package main

import "fmt"

type Base interface {
BaseMethod() string
}

type Extended interface {
Base
ExtendedMethod() string
}

type MyType struct {
value string
}

func (m MyType) BaseMethod() string { return "base: " + m.value }
func (m MyType) ExtendedMethod() string { return "extended: " + m.value }

func useBase(b Base) {
fmt.Println(b.BaseMethod())

// 다운캐스팅: Base → Extended
if ext, ok := b.(Extended); ok {
fmt.Println(ext.ExtendedMethod())
}

// 구체 타입으로 캐스팅
if mt, ok := b.(MyType); ok {
fmt.Println("구체 타입:", mt.value)
}
}

func main() {
m := MyType{value: "hello"}

// 업캐스팅: 구체 타입 → 인터페이스 (자동)
var b Base = m
var e Extended = m

useBase(b)
fmt.Println("---")
useBase(e) // Extended도 Base를 구현
}

핵심 정리

  • v.(T): 단언 실패 시 패닉 → 확신할 때만 사용
  • v, ok := i.(T): ok 패턴 → 실무에서 항상 이 방식 사용
  • switch v := i.(type): 여러 타입 분기 → 타입별 처리에 최적
  • errors.As/errors.Is: 에러 타입 단언의 표준 방식
  • 타입 단언은 인터페이스 값에만 사용 가능 (구체 타입에 불가)