🌈일상/대외활동

[세미나] COMMIT - 타입으로 견고하게 다형성으로 유연하게

뉴발자 2024. 1. 22.
728x90

 

 

 

 

 

 

 

 

 

 

 

 

 

 

그림 1-1. COMMIT - 타입으로 견고하게 다형성으로 유연하게

 

 

값의 능력

let msg = "Hello World";
msg.toUpperCase();

let date = new Date();
date.getHours();

 

위의 코드에서 보듯 값에는 각자의 능력이 있다.

 

string 타입인 msg는 toUpperCase() 메소드를 호출할 수 있는 능력이 있고

 

Date 타입인 date는 getHours() 메소드를 호출할 수 있는 능력이 있다.

 

반대로 msg는 getHours() 메소드를 호출할 수 없고

 

date는 toUpperCase() 메소드를 사용할 수 없다.

 

이렇듯 값을 능력에 따라 분류하는 것타입(Type)이라고 칭한다.

 - boolean : true, false
 - number : -2, 0, 1, 3.14, ...
 - string : "", "x", "hello world", ...
 - Date : ...

 

 

타입 오류 (Type Error)

let date = "2024.01.17 17:30:00";
date.getHours();

TypeError: date.getHours is not a function

 

date 변수는 Date 형식을 띄고 있지만 string 형식이다.

 

따라서 getHours() 메소드를 호출하는 경우에 타입 오류(버그, bug)가 발생하게 된다.

 

버그는 사용자가 원하는 동작이 아닌 경우를 말한다.

 

물론 모든 버그가 타입 오류는 아니지만 타입 오류인 경우가 대부분이다.

 

 

타입 검사 (Type Checking)

타입 검사기를 사용하면 프로그램을 실행하지 않고도 타입 오류가 있는지 알 수 있다.

 

코드를 작성하면 타입 검사기에서 먼저 타입 검사를 진행하게 된다.

 

여기서 코드에 문제가 없으면 통과가 되고 코드에 문제가 있으면 거부와 함께 거부 이유를 보여준다.

그림 2-1. 타입 검사기

 

우리가 원하는 타입 검사는 코드를 검사한 후 통과한 코드는 타입 오류가 절대 안일어나고

 

코드에 오류가 있는 경우 타입 오류가 반드시 있다고 보고 타입 오류가 없도록 수정하도록 동작하는 타입 검사이다.

그림 2-2. 이상적인 타입 검사기

 

하지만 위 방법은 논리적으로 불가능하다.

 

현실적인 타입 검사기는 다음 그림과 같다.

그림 2-3. 현실적인 타입 검사

 

타입 검사기를 통과하게 되면 '내 프로그램은 믿을만하구나'라고 생각할 수 있게 된다.

 

이것을 타입 안정성 (Type Safety, Type Soundness)라고 한다.

 

 

타입 검사기가 있는 언어와 없는 언어

타입 검사기 O 타입 검사기 X
C, Java, R, TS Python, JS, Ruby

 

타입 검사기가 있는 언어정적 타입 언어 (Statically Typed Language)라고 부르고

 

타입 검사기가 없는 언어동적 타입 언어 (Dynamically Typed Language)라고 부른다.

 

정적 타입 언어는 프로그램을 실행하지 않고도 확인할 수 있고 동적 타입 언어는 프로그램을 실행해봐야만 확인할 수 있다.

 

 

타입 검사 사용하기

타입 검사기가 거부하는 경우에는 두 가지의 경우가 있다.

 

첫 번째는 타입 오류가 일어나는 경우이고, 두 번째는 타입 오류는 안일어나지만 타입 검사기에서 거부 당하는 경우이다.

 

첫 번째 경우는 다음과 같다.

그림 3-1. 타입 검사기

 

string 형식인 변수 date로 getHours() 메소드를 호출하면서 발생하는 오류이다.

 

타입 검사기를 사용하면 위 그림과 같이 오류를 반환해준다.

error: Property 'getHours' does not exist on type 'string'

 

 

이처럼 타입 오류가 발생하는 경우에는 타입 오류가 없게 끔 수정하도록 타입 검사기에서 오류를 띄운다.

 

 

두 번째 경우는 다음과 같다.

function getDate(b: boolean) {
  let date = b ? "January 17th" : new Date();
  
  if(b) {
    date.toUpperCase();
  } else {
    date.getHours();
  }
};

 

 

위 코드에서 boolean 타입인 b의 값이 true인 경우 date 변수에 string이 들어간 후 toUpperCase() 메소드를 호출하게 된다.

 

b의 값이 false인 경우 date 변수에 Date 타입이 들어간 후 getHours() 메소드를 호출하게 된다 .

 

단순히 눈으로 봤을 때는 정상적으로 동작하는 코드처럼 보일 것이다.

 

하지만 실제 코드를 작성해보면 다음과 같은 오류가 발생하게 된다.

Property 'toUpperCase' does not exist on type 'string | Date'.
Property 'getHours' does not exist on type 'string | Date'.

 

이유는 date의 타입이 string이 될 수도 있고 Date가 될 수도 있기때문에 타입 검사기에서 date 변수의 타입을 추론하게 된다.

 

따라서 date의 타입은 string | Date가 되기 때문에 타입 오류가 발생하게 된다.

 

위 코드를 정상적으로 동작시키려면 다음과 같이 작성해주면 된다.

function getDate(b: boolean) {
  if(b) {
    let date = "January 17th";
    date.toUpperCase();
  } else {
    let date = new Date();
    date.getHours();
  }
};

 

 

타입 검사기의 원리

'작은 부품에서 큰 부품으로'

그림 4-1. 타입 검사의 원리 (1)

 

위 예시 코드로 타입 검사의 동작 원리를 설명하겠다.

 

먼저 "x"의 타입이 무엇인지 검사한다.

그림 4-2. 타입 검사의 원리 (2)

 

x의 타입을 확인한 후 toUpperCase() 메소드를 사용할 수 있는지 검사한다.

그림 4-3. 타입 검사의 원리 (3)

 

마지막으로 getHours() 메소드를 사용할 수 있는지 검사한다.

그림 4-4. 타입 검사의 원리 (4)

 

타입 검사기가 직접 타입을 검사하기도 하지만 프로그래머가 제공한 정보를 참고하기도 한다.

function add(n: number, m: number): number {
  return n+m;
}

add(1, 2).toUpperCase();

 

프로그래머가 표시한 매개변수 타입과 리턴 타입을 참고해서 타입을 검사한다.

 

위 코드에서 두 number 타입의 매개변수를 더해 number 타입을 리턴하는 함수이다.

 

하지만 string 타입의 속성인 toUpperCase() 메소드를 호출하면서 타입 에러가 발생하게 된다.

 

우리가 보기엔 맞는 코드 같지만 타입 검사기는 국소하게 코드를 보기때문에 오류가 발생하게 된다.

function add(n: number, m: number): number {
  return n+m;
}

add(1, 2).toUpperCase();
// error: Property 'toUpperCase' does not exist on type 'number'

 

또한 toUpperCase() 메소드를 사용하기 위해 리턴 타입으로 string 타입을 지정해도 타입 에러가 발생하게 된다.

// error: Type 'number' is not assignable to type 'string'
function add(n: number, m: number): string {
  return n+m;
}

add(1, 2).toUpperCase();
728x90

 

 

타입 추론

리턴 타입을 생략하더라도 생략한 타입 표시를 타입 검사기가 추론 후 에러를 발생시키게 된다.

 

단, 언어마다 생략 가능한 타입은 다르다.

 

(타입 스크립트의 경우 리턴 타입은 생략이 가능하지만, 매개변수 타입의 생략은 불가능하다.)

// 리턴 타입 생략
function add(n: number, m: number) {
  return n+m;
}

add(1, 2).toUpperCase();
// error: Property 'toUpperCase' does not exist on type 'number'

 

 

타입 정보의 활용

타입 검사기를 활용하면 mouse hover시 변수의 타입을 보여주고 타입에 따라서 자동 완성 기능을 제공한다.

그림 5-1. 마우스 오버 시 타입을 보여준다.

 

그림 5-2. 타입에 따라 자동 완성 기는을 제공한다.

 

 

정적 타입 언어 vs 동적 타입 언어

종류 특징
정적 타입언어 • 타입 오류를 찾기 쉽다.
코드 편집기가 타입 정보를 잘 활용한다.
큰 프로그램을 잘 만드는데 유용하다.

ex) Scala, TypeScript
동적 타입 언어 올바른 코드가 거부당하지 않는다.
타입 표시가 필요없다.
작은 프로그램을 빠르게 만드는데 유용하다.

ex) Ruby, JavaScript

 

 

현실적인 타입 검사기

위에서 말한 현실적인 타입 검사기의 이론상 모습은 다음과 같다.

그림 6-1. 현실적인 타입 검사기 - 이론

 

하지만 실제 타입 검사기는 다음과 같은 구조를 가지고 있다.

그림 6-2. 현실적인 타입 검사기 - 실제

 

타입 검사에서 버그가 발생하는 이유는 세 가지가 있다.

 

1. 타입 검사기 구현 실수

타입 검사기 github의 이슈 페이지를 확인해보면 아직도 많은 이슈들이 발생하고 있다.

그림 7-1. 타입 검사기 구현 실수

 

사람이 하는 작업이기 때문에 완벽할 수 없고 구현중에 발생한 실수로 인해 버그가 발생한다.

 

2. 언어 설계 오류

타입 검사기를 개발한 사람 중 한명인 Nada Amin 은 다음와 같은 말을 했다.

Java and scala's type systems are unsound: the existential crisis of null pointers

 

번역하자면 '자바와 스칼라의 타입 시스템은 안전하지 않다' 란 뜻이다.

 

3. 의도적으로 제공하는 위험한 기능

let msg: any = "hello world";
let date: Date = msg;

date.getHours();

 

any 타입을 선언하게 되면 코드 자체에서는 타입 오류가 발생하지 않는다.

 

any는 어떠한 타입도 될 수 있기 때문에 타입 스크립트에서 사용하지 않는 것을 권고한다.

 

위 코드를 실행하면 다음과 같은 에러가 발생하게 된다.

TypeError: date.getHours is not a function

 

 

다형성 (Polymorphism)

다형성이란?

하나의 변수/함수 등이 여러 개의 타입이 될 수 있는 기능을 뜻한다.

그림 8-1. 다형성

 

다형성의 종류

 • 서브타입에 의한 다형성 (SubType Polymorphism)

 • 매개변수에 의한 다형성 (Parametric Polymorphism)

 • 오버로딩에 의한 다형성 (Ad Hoc Polymorphism)

 

다형성의 종류는 세 가지로 나뉜다.

 

서브타입에 의한 다형성객체 지향 언어에서 흔히 얘기하는 다형성이다.

 

매개변수에 의한 다형성함수형 언어나 학계에서 흔히 얘기하는 다형성이다.

 

객체의 타입

let x = {"name": "John"};

 

x 객체의 타입을 지정하는 방법은 무엇일까?

 

대표적인 방법으로 interface를 생성해서 객체의 타입을 지정해주는 방법이 있다.

interface Person {
  name: string;
}

let x: Person = {"name": "John"};

 

이렇게 생성한 객체 x를 인자로 받아 string 타입을 리턴하는 함수는 다음과 같다.

interface Person {
  name: string;
}

let x: Person = {"name": "John"};

function greet(p: Person) {
  return `Hello, ${p.name}`;
};
greet(x);

 

서브타입에 의한 다형성

그렇다면 객체 x에 name과 grade 속성을 가진 Student 타입을 지정한 후 같은 코드를 동작시킬 경우 어떻게 될까?

interface Person {
  name: string;
}

interface Student {
  name: string;
  grade: number;
}

let x: Student = { "name": "John", "grade": 1 };

function greet(p: Person) {
  return `Hello, ${p.name}`;
};
greet(x);

 

Student타입에는 Person과 같이 name 속성이 있기 때문에 정상적으로 동작된다.

 

이유는 Student 타입은 Person의 서브타입이기 때문이다.

 

따라서 x의 타입은 Student이면서 동시에 Person이다.

x의 타입이 T일 때, 타입 T가 S의 서브타입이면, x의 타입은 S이기도 하다.

 

직관적으로 볼 때, "T는 S이다"가 참이면 T는 S의 서브타입이 된다.

 

쉽게 얘기하자면 다음과 같다.

그림 8-2. 서브 타입에 의한 다형성

 

"학생은 사람이다" 라고 한다면 이것은 맞는 말이다.

 

하지만 반대로 "사람은 학생이다" 라고 한다면 모든 사람은 학생이 아니기 때문에 틀린 말이 된다.

 

따라서 name과 grade를 가진 객체는 name을 가진 객체이다.

 

반대로 name을 가진 객체는 name과 grade를 가진 객체가 아닌 것이다.

 

구조에 따른 서브타입 (Structural Subtyping)

T의 모든 속성이 S에 있으면, T는 S의 서브타입이 된다.

interface Person {
  name: string;
};

interface Student {
  name: string;
  grade: number;
};

 

위 코드를 extends를 사용해서 다음과 같이 작성할 수도 있다.

interface Person {
  name: string;
}

interface Student extends Person {
  grade: number;
}

let x: Student = { "name": "John", "grade": 1 };

function greet(p: Person) {
  return `Hello, ${p.name}`;
};
greet(x);

 

이름에 따른 서브타입 (Nominal Subtyping)

Jave에서 사용되는 서브타입 방식이다.

 

다음과 같은 경우 Student는 Person의 서브타입이다.

class Person {
  String name;
}

class Student extends Person {
  int grade;
}

 

하지만 다음과 같은 경우 Student는 Person의 서브타입이 아니다.

class Person {
  String name;
}

class Student {
  String name;
  int grade;
}

 

객체에 같은 속성을 가지고 있더라도 상속 관계에 있지 않다면 두 객체는 서브타입이 되지 않는다.

 

서브타입에 의한 다형성의 한계

function pick(x: number, y: number): number {
  return Math.random() < 0.5 ? x : y;
};

let n: number = pick(10, 20);

 

위 함수는 Math.random() 메소드를 사용해서 0.5보다 작으면 x, 크면 y 값을 리턴해주는 함수이다.

 

여기서 string 타입 s에 pick() 메소드를 사용해서 두 인자의 타입을 string으로 준다면 타입 오류가 발생할 것이다.

function pick(x: number, y: number): number {
  return Math.random() < 0.5 ? x : y;
};

let n: number = pick(10, 20);

let s: string = pick("a", "b");
// error: Type 'number' is not assignable to type 'string'
// error: Argument of type 'string' is not assignable to parameter of type 'number'

 

하지만 pick 함수의 매개변수 타입과 리턴 타입을 모두 unknown으로 지정한다면 어떻게 될까?

 

모든 타입은 unknown의 서브타입이기때문에 정상적인 코드로 인식돼서 타입 오류가 발생하지않게 된다.

그림 8-3. unknown 타입으로 지정한 경우

 

하지만 반대로 number나 string 타입을 지정하게 되면 타입 오류가 발생하게 된다.

그림 8-4. number 또는 string 타입으로 지정한 경우

 

매개변수에 의한 다형성

함수를 선언할 때 들어가는 값을 '매개변수'라고 하고 함수 호출 시 들어가는 값을 '인자'라고 한다.

 

매개변수에는 변수명과 타입이 들어가고 인자에는 선언된 매개변수 타입에 맞는 값이 들어간다.

그림 8-3. 매개변수와 인자

 

제네릭스 (Generics)

function pick(x: number, y: number): number {
  return Math.random() < 0.5 ? x : y;
};

pick(10, 20);

 

위 코드에서는 pick 함수를 호출하기 위해 인자 값으로 number 타입 값만을 사용할 수 있다.

 

만약 인자 값으로 string을 넣고 string을 리턴받고 싶다면 함수를 새로 작성해야 할 것이다.

 

그렇게되면 타입때문에 타입만 다른 같은 코드를 한 번 더 작성하게 된다.

function pick(x: number, y: number): number {
  return Math.random() < 0.5 ? x : y;
};

let n: number = pick(10, 20);

function pick2(x: string, y: string): string {
  return Math.random() < 0.5 ? x : y;
};

let s: string = pick2("a", "b");

 

이 부분을 해결하기 위해 '제네릭'을 사용하게 된다.

 

제네릭을 사용해서 코드를 작성하면 하나의 함수 원하는 타입을 지정해서 호출할 수 있게 된다.

function pick<T>(x: T, y: T): T {
  return Math.random() < 0.5 ? x : y;
};

let n: number = pick<number>(10, 20);
let s: string = pick<string>("a", "b");

 

함수를 선언할 때 들어가는 제네릭을 '타입 매개변수'라 하고 호출할 때 들어가는 인자를 '타입 인자'라고 한다.

 

 

후기

타입 스크립트로 코드를 구현하면서 한 번 쯤은 사용해본 기능들이지만 강의를 들으며 복습하는 느낌이 들어서 좋았다.

 

지금까지의 프로젝트엔 any 타입을 많이 선언했고 문제없이 동작된다고 생각했지만 강의를 듣고 반성하게 됐다.

 

타입 스크립트 학습을 꾸준히해서 더욱 견고하게 짜여진 완성도 높은 코드를 목표로 나아가야겠다.

 

 

 

 

 

 

 

 

 

 

728x90

댓글