golang으로 만나보는 Duck Typing

Duck Typing ( 이하 덕 타이핑) 이란 방식이 세상에 알려진지는 상당한 시간이 지났다.  이미 덕 타이핑을 이해하고 덕 타이핑 동작 방식을 수용한 Program Language 를 도입하여 사용하는 이들도 있지만 아직까지 덕 타이핑이 무엇인지 모르는 분들이 주위에 상당하기에 이 글을 통해서  덕 타이핑에 대한 개념과 Golang 기반 Example Code를 작성하여 이해를 돕고자 합니다.

duck_typing

덕 타이핑 이란?

덕 타이핑이란 용어는 2000년 파이선 뉴스그룹에 Alex Martelli 가 올린 다음의 글이 시초가 되었다.

"In other words, don't check whether it IS-a duck: check whether it QUACKS-like-a duck, WALKS-like-a duck, etc, etc, depending on exactly what subset of duck-like behaviour you need to play your language-games with."

요약하자면

"그게 오리인지 검사하지 말고,  당신이 오리의 무슨 행동이 필요한지에 따라서 오리처럼 우는지, 오리처럼 걷는지 기타 등등... 적절한 행동을 오리처럼 하는지 검사 하세요"

즉 사람이라도 오리처럼 꽥꽥울고, 오리처럼 뒤뚱뒤뚱 걸으면 그건 사람이 아니라 오리라고 판단 하겠다는 말이다.

덕 타이핑은  이게 전부다!

진짜 이게 전부입니다! :-)

사실 위의 인용구는 덕 테스트의 개념이고 덕 타이핑은 덕 테스트가 방식으로 진화된것이라고 할 수 있다.  덕 테스트는 동적 언어의 다형성(polymorphism)지원&구현에 관한 논쟁에서 자주 언급되어져 왔었고 그렇다면 정적, 동적 언어는 무엇인지에 대해 안 짚어 볼 수 가 없다.

Static, Dynamic Language

우선 정적(Static), 동적(Dynamic) 언어의 특징과 차이점에 대해 간략하게 정리해 보면.

정적 언어

  • 변수 또는 함수의 Type을 미리 지정해야 함.
  • 컴파일 시점에 Type Check가 가능.

동적 언어

  • 변수 또는 함수의 Type을 지정하지 않고 사용할 수 있음.
  • Type Check는 runtime시점에서만 알 수 있음.
  • Type지정이 안되어 있어 소스 분석 어려움.

정적, 동적 언어의 가장 큰 차이점은 Type정보를 지정해야 하냐? 아니냐? 로 볼 수 있다.

(요즘들어서는 전통적인 정적 언어들도 일부 동적 언어의 기능을 도입한 경우도 볼 수 있음)

다음 pseudo code는 정적,동적 언어의 Type관련 차이점을 보여주고 있다.

1
2
3
4
a  = 9
b = "9"
c = concatenate(  str(a),  b)
d = add(a,  int(b)  )
1
2
3
4
a  = 9
b = "9"
c = concatenate(a, b)  // produces "99"
d = add(a, b)          // produces 18

동적 언어는 이런 코딩 간결함 덕분에 초기 Learning Curve가 크지 않아 Business가 고착화되지 않아 잦은 기능변경이 필요한 Startup 에서 많이들 선택하고 있고 이를 동적 언어의 매력이라 말하기도 한다.

하지만 이런 동적 언어도 한가지 문제점이 있는데 컴파일 과정이 없다보니 간단한 문법 오류 조차도 Runtime시점에 나타난 다는것이다.

비단 문법오류가 아니더라도 Runtime시점에 기대했던 객체가 다른객체로 전달될 가능성이 있기 때문에 그로 인해 예상치 못한 실행결과를 나타내기도 한다. 물론 이런 문제점은 테스트 코드로 커버 할 수 있으며 테스트 커버리지율이 높아 질 수록 테스트 단계에서 발견되는 오류가 많아지고 이를 해결하면서 더욱더 견고한  Program이 될 수 있고 더불어 테스트 코드를 작성하는 좋은 습관을 만들어 갈수도 있다. 하지만 이는 어디까지나 보완책일뿐 동적언어가 가지고는 이 문제점에서 자유로워 질 순 없다.

이 처럼 정적, 동적언어는 서로 일장일단이 존재하며 동적 언어로 시작한 후 Business가 고착화 되면서 정적 언어로 변경한 Startup 도 심심치 않게 볼 수 있다.

그렇다면 동적 언어의 개발속도와 편리성, 컴파일 기반의 정적 언어의 안정감을 둘 다 가질 수는 없는 것인가?

Static + Dynamic  Language

Golang (이하 Go)개발자팀은 개발자들이 직면하고 있는 이런 문제점을 해결하고자 많은 고민을 했고 앞서 거론한 동적, 정적 언의 장점을 잘 버무리고 싶어 했다.

Go는 컴파일 기반의 정적 Type언어이다. 하지만 동적 언어의 특성 또한 수용해서 동적 언어 스타일의 코딩이 가능하다.

즉 컴파일러의 보장을 받으면서 동적 언어의 장점을 사용할 수 있다는 것이다.  이를 가능하게 해주는 것이 바로 덕 타이핑 방식으로 작동하는 Go의 인터페이스이다. Go에서 인터페이스의 역할은 객체의 동작을 표현하는 것이고,  인터페이스가 표현한대로 동작하는 객체는 인터페이스를 사용 할 수 있다.

정적 언어의 경우 특정 인터페이스를 사용하기 위해선 명시적 키워드를 사용하여야 한다. ex) implements 하지만 Go에선 단지 원하는 인터페이스의 동작을 구현한것 만으로 해당 인터페이스를 사용한것이 된다. (Go만 이런식으로 사용하는 것은 아님)

즉 "만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다." 이 개념으로 말이다.

자 그럼 Go에서 덕타이핑이 어떤식으로 사용되는지 예제 코드를 통해서 알아보자.

앞서 말한것 처럼 Go에선 인터페이스에 동작을 표현 하도록 되어 있다.

1
2
3
4
type Duck interface {
    Quack() string // 쾍쾍 거리는 동작
    Walk() string// 걷는 동작
}

Duck 인터페이스를 구현한 Swan 구조체

1
2
3
4
5
6
7
8
type Swan struct {
}
func (s Swan) Quack() string {
    return "고니 울름소리"
}
func (s Swan) Walk() string {
    return "고니 걷는 모습"
}

Duck 인터페이스를 구현한 Chicken 구조체

1
2
3
4
5
6
7
8
type Chicken struct {
}
func (c Chicken) Quack() string {
    return "닭 울름소리"
}
func (c Chicken) Walk() string {
    return "닭 걷는 모습"
}

위 Swan, Chicken 구조체를 보면 Duck 인터페이스를 구현한다는 명시적 keyword가 전혀 보이지 않는다. 그저 Duck 인터페이스의 동작을 구현한것으로 이들은 Duck 인터페이스 타입으로 사용될 수 있는것이다.

다음 예제는 Duck 인터페이스를 구현 했다면 "깜짝 놀랬을 때 울고 난 후 걷는다(도망) 간다" 가 정상적으로 동작하는지를 보는 예이다.

(Go에서 함수(function)와 메소드(Method)는 엄연히 틀린 개념이다. 우리가 일반적으로 알고 있는 함수의 개념을 함수라고 불려지고 메소드는 구조체에 추가시킬 동작을 일컽는 개념 이므로 오해하면 안된다.)

1
2
3
4
5
6
7
8
9
10
11
12
//깜짝 놀랬을 때 울고 난 후 걷는다(도망) 간다
func Scare(duck Duck) {
    // Duck의 동작을 구현한 어떤 객체라도 argument로 전달 될 수 있다.
    fmt.Println(duck.Quack())
    fmt.Println(duck.Walk())
}
func main() {
    var swan Duck = Swan{}
    Scare(swan)
    var chicken Duck = Chicken{}
    Scare(chicken)
}

실행하면 아래와 같은 결과를 확인 할 수 있다.

duck_type_execute

자 그렇다면 Duck 인터페이스를 구현하지 않은 경우는 어떻게 될까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func Scare(duck Duck) {
    // Duck의 동작을 구현한 어떤 객체라도 argument로 전달 될 수 있다.
    fmt.Println(duck.Quack())
    fmt.Println(duck.Walk())
}
type Human struct {
}
func main() {
    var swan Duck = Swan{}
    Scare(swan)
    fmt.Println("-----------------------------------")
    var chicken Duck = Chicken{}
    Scare(chicken)
    fmt.Println("-----------------------------------"
    var human Duck = Human{} // 컴파일 에러 발생.
    Scare(human)
}

일반적인 동적 언어라면 실행시점에서 에러가 발생하겠지만 Go는 컴파일 타임이 존재 하므로 컴파일 에러가 발생해 실행조차 할 수 없다.

많은 컴파일 언어에서는 특정 라이브러리나 프레임웤과 호환되는 객체를 만들려면 특정 인터페이스를 구현하거나 클래스를 상속받아 구현 하는게 일반적이고 이렇게 생성된 클래스는 특정 라이브러리나 프레임웤과 강한 연결고리가 형성 되기에 변경이나 확장에 용이 하지 않다.

반면  Go에선 덕 타이핑 방식으로 작동하는 인터페이스를 통해 위 예제와 같은 다형성을 지원하고 있고  이런 특징덕에 정적 Type언어임에도 불구하고 모듈간 결합도가 낮고 확장하기 쉬운 이점을 가지고 있다.

마치며

지금까지 덕 타이핑이라는 주제로 정적 동적 언어의 특징에 대해 살펴봤고 Go에서 덕 타이핑이 어떤식으로 구현되는지 매우 심플하게 알아봤다. 덕 타이핑은 프로그래머들 사이에 상당히 논란이 많은 개념이다.  누구는 덕 타이핑 개념은 "오리의 동작을 잘 구현 했을꺼야?" 라는 믿음을 바탕으로 하기에 프로그래머의 실수에 의한 오류가 항상 존재함을 Language측면에서 강제 해야한다는 의견도 있고, 누구는 그런 제약이 존재함으로써 복잡도가 증가하고 유지보수 비용이 많이 든다는 의견도 있고, 누구는 그런 자유도 때문에 코딩이 재미있고 생산성이 향상되고 확장이 용이하다고 말하는 사람도 있다.

정답은 없다!

스스로 만들고자 하는 목적물에 이런 장,단점이 적절 한가를 판단하고 사용하면  될거라고 본다.

다만 본인에게 익숙한 어떤 개념이나 기술에 너무 집착하거나 얽매이지 않았으면 하는 바램을 가지며 글을 마친다.


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.