녕후킴

녕후킴

블로그 주인의 프로필 그림

다양한 타입 호환성 비교 방법

0 views

Norminal Typing, Structural Typing, Duck Typing은 타입 시스템이 타입 호환성을 비교하는 서로 다른 방법들이다.

T1 타입이 T2 타입과 호환 가능하다고 할때, T2 타입이 T1 타입과 호환 가능함을 의미하지는 않는다. 예를들면, Integer 데이터를 Decimal 데이터로 바꾸는 것은 가능하지만, Decimal 데이터를 Integer 데이터로 바꾸는 것은 불가능하다. (Decimal 타입이 무엇인지 이해하려면 깊게 들어가야 하므로, 여기서는 단순히 Decimal이 Integer의 자식 타입 정도로만 이해하고 넘어가자.)

Norminal Typing은 타입 호환성을 비교하기 위해서 '이름'을 사용하고, Structural Typing은 타입 호환성을 비교하기 위해서 '구조'를 사용한다. Duck Typing은 사용에 기반을 둔 '구조'를 사용하며(당장 이해가 어렵지만 뒤에 예제를 보면 무슨 말인지 이해가 될 것이다.), 보통 동적 타입 언어에서 발견할 수 있다.

1. Norminal Typing

Norminal Typing을 사용하는 타입 시스템에서는 타입 호환성을 위해서 다음 두 가지 중 하나가 만족되어야 한다.

  1. 타입 이름이 매칭되어야 함 (동일한 클래스로부터 만들어진 객체들이라고 표현하면 이해가 쉽다.)
  2. 하나의 타입이 다른 타입의 부모 타입이어야 함 (상속 관계에 있는 클래스들로부터 만들어진 객체들이라고 표현하면 이해가 쉽다.)

먼저 1의 상황에 대해서 이야기해보자. 아래는 C# 예제다. Dog와 Cat 클래스가 name 프로퍼티와 makeNoise 메서드를 가지고 있다고 가정하자. 그리고 다음과 같이 객체를 생성하고,

Dog cat = new Dog('Mars'); Cat cat = new Cat('Venus');

다음 메서드에 위 두가지 객체를 각각 전달한다고 할때,

static public void makeNoise(Dog obj) { obj.makeNoise(); }

Dog 인스턴스에 대해선 동작을 하지만 Cat 인스턴스에 대해서는 동작하지 않는다. 설령 두 인스턴스의 모양이 동일하더라도, C#은 둘을 호환 가능하다고 취급하지 않는 것이다.

이렇듯 Nominal Typing은 서로 다른 타입으로부터 생성된 객체는 호환이 불가능하다. 앞서 2에서 이야기한 상속 관계가 아니라면 말이다.

2. Norminal Typing with Subtypes

A를 상속받는 B가 존재한다고 할때, B를 A 로 호환하는 것은 가능하지만, A를 B 로 호환하는 것은 불가능하다.

앞선 예제에 이어서, 다음과 같이 BullDog이 Dog를 상속받는다고 가정해보자.

public class BullDog:Dog { public String breedName; public BullDog(string name, string breedName) : base(name) { this.breedName=breedName; } }

이제 BullDog 타입은 Dog의 대체 타입으로 사용 가능하기 때문에, makeNoise 메서드의 인자로 전달할 수 있게된다.

BullDog bulldog = new BullDog("Mars","BullDog"); makeNoise(bulldog);

만약 다음과 같이 makeNoise의 파라미터 타입을 Dog로 바꾼다면, BullDog 인자를 전달하면 동작하지 않게된다.

static public void makeNoise(BullDog obj) { obj.makeNoise(); }
Dog dog = new Dog("Mars"); makeNoise(dog); //error CS1503: Argument 1: cannot convert from 'Dog' to 'BullDog'

3. Structural Typing

Structural Typing에서는 타입의 이름보다 타입의 형태가 더 중요하다. 그리고 Typescript는 Structural Typing 시스템이다. 앞선 예제를 Typescript로 옮겨보자.

Dog와 Cat 클래스 모두 다음과 같이 name 프로퍼티와 makeNoise 메서드가 존재한다고 할때,

class Dog { constructor(public name: string) {} makeNoise() { console.log('Woof') } } class Cat { constructor(public name: string) {} makeNoise() { console.log('Miau') } }

makeNoise 함수의 인자로 두개 모두 전달이 가능하다.

let dog = new Dog('Mars') let cat = new Cat('Venus') function makeNoise(obj: Dog) { obj.makeNoise() } makeNoise(dog) //Ok makeNoise(cat) //Ok

만약 Person 클래스가 makeNoise 메서드를 갖고 있고, name 프로퍼티를 갖고 있지 않다면, 앞선 makeNoise 함수에 전달이 불가능하게 된다.

class Person { constructor(public firstName: string, public lastName:string) { } makeNoise() { console.log('Helloooo') } } let person= new Person("Jon","Snow") makeNoise(person) //Error //Argument of type 'Person' is not assignable to parameter of type 'Dog'. // Property 'name' is missing in type 'Person' but required in type 'Dog'.

만약 Person 클래스에 name 프로퍼티가 있고, 더불어서 address 프로퍼티가 존재해도 makeNoise의 인자로 전달이 가능하다.

class Person { constructor(public name:string,public address: string) { } makeNoise() { console.log('Helloooo') } } let person= new Person("Jon Snow","North") makeNoise(person)

4. Duck Typing

Duck Typing에서 이름과 형태는 중요하지 않다. Duck Typing은 어떤 동작을 위해 필요한 메서드와 프로퍼티가 존재해야 하며, 보통 자바스크립트와 같은 동적 타입 언어에서 찾아볼 수 있다.

"어떤 것이 오리처럼 걷고, 오리처럼 수영하고, 오리처럼 꽥꽥거리면 그것은 아마도 오리일 것이다"라는 것에서 Duck Typing이 유래했다. 만약 어떤 객체가 어떤 행동을 하길 기대하고, 그 객체가 그 행동을 한다면 개발자는 이 객체를 사용할 수 있다. 이 객체의 모양과 타입은 전혀 중요하지않다.

아래는 person과 backAccount 두가지 객체를 포함하고 있는 자바스크립트 예제이다. 그들은 동일한 타입을 공유하지도 않고, 동일한 구조를 갖고있지도 않다. 다만 동일하게 someFn 메서드를 갖고 있을 때, invokeSomeFn이라는 someFn 메서드를 호출하는 함수의 인자로 각각의 객체를 문제 없이 전달할 수 있다.

let person = { name: 'Jon', someFn: function () { console.log('hello ' + this.name + ' king in the north') }, } let bankAccount = { accountNo: '100', someFn: function () { console.log('Please deppost some money') }, } invokeSomeFn = function (obj) { obj.someFn() } invokeSomeFn(person) invokeSomeFn(bankAccount)

5. Norminal vs Structural vs Duck

Norminal Typing이 가장 덜 유연하고, Duck Typing이 가장 유연하며, Structural Typing은 그 사이에 있다.

타입이 덜 유연할수록 타입을 명시적으로든 암시적으로든 지정해주어야하기 때문에, 더 많은 코드가 요구되고 덜 유연할 수 있지만, 가독성이 좋아지고, 에러가 덜 발생하며, 버그를 쉽게 찾아 수정할 수 있다.

참고 문헌

Structural, Nominal, and Duck typing