클래스와 인터페이스 #5

세나정·2023년 3월 3일
0

클래스?

클래스는 코드를 조직하고 이해할 수 있는 방법을 제공할 뿐 아니라 캡슐화의 주요 단위이기도 함
타입스크립트 클래스의 기능 대부분은 C#에서 빌려옴
가시성 한정자, 프로퍼티 초기자 (property initializer), 다형성, 데코레이터 인터페이스 등을 지원함

타입스크립트 클래스를 컴파일 하면 일반 자바스크립트 클래스가 되므로 믹스인 같은 자바스크립트 표현식도 타입 안정성을 유지하며 사용할 수 있음

프로퍼티 초기자와 데코레이터 같은 타입스크립트 기능 일부는 자바스크립트에서도 지원하므로 실제 런타인 코드를 생성,
반면 가시성 접근자, 인터페이스, 제네릭 등은 타입스크립트만의 고유 기능이므로 컴파일 타임에만 존재하며 응용 프로그램을 자바스크립트로 컴파일 할 때는 아무 코드도 생성 X

- 클래스와 상속

두 명이 체스를 둘 수 있는 API를 제공하는 체스 엔진을 만들어보자

// 체스 게임
class Game {}
// 체스 말
class Piece {}
// 체스 말의 좌표 집합
class Position {} 

// 체스에는 6가지 말이 있음 

class King extends Piece {}
class Queen extends Piece {}
class Bishop extends Piece {}
class Knight extends Piece {}
class Rook extends Piece {}
class Pawn extends Piece {}

모든 말은 색과 현재 위치정보를 갖고
체스에서는 좌표 쌍 (문자, 숫자)로 말의 위치를 표시
문자는 x축을 따라 왼쪽에서 오른쪽으로 증가하며
숫자는 y축을 따라 아래에서 위로 증가함

이제 Piece 클래스에 색과 위치를 추가하자

type Color = 'Black' | 'White'
type File = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H'
type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 // 1

class Position {
	constructor (
    	private file: File, // 2 
        private rank: Rank
        ) {}
}

class Piece {
	protected position: Position // 3 
    constructor (
    	private readonly color: Color, // 4
        file: File,
        rank: Rank
        ) {
        	this.position = new Position(file, rank)
     }
}
  1. 색 랭크, 파일의 종류가 많지 않아 모든 값을 타입 리터럴로 직접 열거 가능
    모든 문자열과 숫자를 가질 수 있는 이들 타입 도메인에 특정 문자열과 숫자만 넣어 제한을 추가하여 안정성을 확보

  2. private 접근 한정자는 자동으로 매개변수를 this에 할당
    즉, Position 인스턴스 안의 코드는 이 매개변수를 읽고 쓸 수 있지만 Position 인스턴스 외부에서는 접근할 수 없음 Position 인스턴스끼리는 다른 인스턴스의 비공개 (private)멤버에 접근할 수 있음

  3. protected도 private처럼 프로퍼티를 this에 할당하지만 private과 달리 Piece의 인스턴스와 Piece의 서브 클래스 인스턴스 모두에 접근을 허용

  4. new Piece는 color, file, rank 세 개의 매개변수를 받고 color에 private과 readonly를 추가 한정자로 지정, private은 color를 this로 할당해서 Piece의 인스턴스에서만 이 변수에 접근할 수 있게 만들고 readonly는 초기에 값을 할당한 다음엔 더 이상 값을 덮어쓸 수 없게 함

public : 어디에서나 접근가능, 기본적으로 주어지는 접근 수준
protected : 이 클래스와 서브클래스의 인스턴스에서만 접근 가능
private 이 클래스의 인스턴스에서만 접근 가능

여기서는 Piece 클래스를 정의함, 사용자가 Piece 인스턴스를 직접 생성하지 못하게 막고 대신 Queen이나 Bishop등 Piece 클래스를 상속받은 클래스를 통해서만 인스턴스화할 수 있도록 허용

abstract 키워드 (추상화)를 이용해 타입 시스템이 규칙을 강제하도록 설정 가능

이렇게하여 Piece를 직접 인스턴스화 하려고 시도하면 타입 스크립트가 다음과 같이 에러를 발생시킴

추상화에 필요한 abstract키워드는 해당 클래스를 바로 인스턴스화 할 수 없음을 의미할 뿐 필요한 메서드를 추상 클래스에 자유롭게 추가할 수 있음

이제 Piece 클래스의 의미

  • canMoveTo라는 메서드를 주어진 시그니처와 호환되도록 구현해야함을 하위 클래스에 알림, Piece를 상속 받았으나 canMoveTo 메서드를 구현하지 않으면 컴파일 타임에 에러가 발생, 추상 클래스를 구현할 때는 추상 메서드도 반드시 구현
  • moveTo의 기본 구현을 포함 (필요하다면 서브 클래스에서 오버라이드 할 수 있음)
    moveTo에는 한정자가 없으므로 기본적으로 public

이제 이 canMoveTo를 King클래스에 구현해 넣어보자
두 체스 말의 거리를 쉽게 계산하도록 distanceFrom이라는 유용한 함수도 추가구현

그 이후 새 게임을 만들 때 자동으로 보드와 말을 만듦

Rank와 File의 타입을 타입 리터럴로 엄격하게 설정했으므로 'J'와 같은 미리 지정되지 않은 문자나 12 같이 범위를 벗어나는 숫자는 타입스크립트가 컴파일 타임에 에러를 발생시킴

요약


- super

자바스크립트와 마찬가지로 타입스크립트도 super 호출을 지원
자식 클래스가 부모 클래스에 정의된 메서드를 오버라이드하면 (예: Queen, Piece 둘 다 메서드를 구현하는 상황) 자식 인스턴스는 super를 이용해 부모 버전의 메서드를 호출할 수 있음 (예: super.take) 타입스크립트는 두 가지 super 호출을 지원

super로 부모 클래스의 메서드에만 접근할 수 있고 프로퍼티에는 접근할 수 없다는 사실을 기억


- this를 반환타입으로 사용

this를 값뿐아니라 타입으로도 사용 가능
클래스를 정의할 때라면 메서드의 반환 타입을 지정할 때 this 타입을 유용하게 활용 가능
ES6의 Set자료구조를 두 가지 연산 (집합에 숫자 더하기, 주어진 숫자가 집합에 포함되어 있는지 확인하기)만 지원하도록 간단하게 구현

let set = new Set
set.add(1).add(2).add(3)
set.has(2) // ture
set.has(4) // false

Set 클래스의 has 메서드부터 정의

add를 호출하면 Set 인스턴스를 반환해야함 (중복없이니까)

지금까지는 특별한 문제가 없지만 이번엔 Set을 상속받는 서브클래스를 만들어봄

Set의 add 메서드는 여전히 Set을 반환하므로 서브클래스에서는 MutableSet을 반환하도록 오버라이드 함 (부모 내용 함 더쓰기?, 반환 MutableSet)

이렇게 되면 클래스를 상속받는 서브클래스가 해야하는 귀찮은 작업이 생김
즉, 서브클래스는 this를 반환하는 모든 메서드의 시그니처를 오버라이드 해야함

하지만 이제 다음처럼 add의 반환을 set이 아닌 this로 지정하면 타입스크립트가 알아서 해줌

이제 Set의 this는 Set인스턴스를, MutableSet의 this는 MutableSet 인스턴스를 자동으로 가리키므로 add 메서드를 오버라이드 할 필요가 없음 (반환을 자기 자신으로)

- 인터페이스

클래스는 인터페이스를 통해 사용할 때가 많음
타입 별칭처럼 인터페이스도 타입에 이름을 지어주는 수단
타입 별칭과 인터페이스는 문법만 다를 뿐 거의 같은 기능을 수행

type 스시 = {
	calories :number 
    salty :boolean
    tasty :boolean
}

interface 스시 = {
	calories :number 
    salty :boolean
    tasty :boolean
}

type에서도 &을 활용할 수 있고 인터페이스도 extends를 활용할 수 있음

인터페이스가 반드시 다른 인터페이스를 상속받아야 하는 것은 아님, 사실 인터페이스는 객체타입, 클래스, 다른 인터페이스 모두를 상속 받을 수 있음

타입과 인터페이스의 차이점?

첫째, 타입 별칭은 더 일반적이어서 타입 별칭의 오른편에는 타입 표현식 (타입, &, | 등 타입 연산자)를 포함한 모든 타입이 등장 가능 반면 인스턴스의 오른편에는 반드시 형태가 나와야함 예를 들어, 다음과 같은 타입 별칭 코드는 인터페이스로 다시 작성할 수 없음

type A = number
type B = A | string

둘째, 인터페이스를 상속할 때 타입스크립트는 상속받는 인터페이스의 타입에 상위 인터페이스를 할당할 수 있는지를 확인 (중복이 불가)

근데 여기서 bad도 or 연산을 한다면 number와 string을 쓸 수 있는 거 아닌가?

인터섹션(&)을 활용하면 상황이 달라짐 앞의 예에서 인터페이스는 타입 별칭으로 바꾸고 extends는 인터섹션(&)으로 바꾸면 확장하는 타입을 최대한 조합하는 방향으로 동작함

결과적으로 컴파일에러가 아닌 bad를 오버로드한 시그니처가 만들어짐

셋째, 이름과 범위가 같은 인터페이스가 여러 개 있다면 이들이 자동으로 합쳐짐
(선언합침)

-- 선언합침

똑같은 이름의 인터페이스를 두 개 정의하면 타입스크립트는 자동으로 둘을 하나의 인터페이스로 합침

이제 이 코드를 타입 별칭으로 표현하면 당연히 있다고 나옴

type User = { // 에러 TS2300: 중복된 식별자 'User'
	name: string
}

제네릭을 활용하여 선언한 인터페이스의 경우 제네릭들의 선언 방법과 이름까지 똑같아야 합칠 수 있음

interface User {
	age: string
}


Age는 string으로부터다!라고 제네릭

-- 구현

클래스를 선언할 때 implements라는 키워드를 이용해 특정 인터페이스를 만족시킴을 표현할 수 있음
다른 명시적 어노테이션처럼 implements로 타입 수준의 제한을 추가하면 구현에 문제가 있을 때 어디가 잘못됐는지 쉽게 파악할 수 있음
어댑터, 팩토리, 전략 등 흔한 디자인 패턴을 구현하는 대표적인 방식이기도 함

Cat을 Animal이 선언하는 모든 메서드를 구현해야하며, 필요하다면 메서드나 프로퍼티를 추가로 구현할 수 있음

인터페이스로 인스턴스 속성을 정의할 수는 있지만 한정자는 선언할 수 없고 static 키워드도 사용할 수 없다
객체안의 객체타입처럼 인스턴스 프로퍼티를 readonly로 설정할 수 있다.

한 클래스가 하나의 인터페이스만 구현할 수 있는 것은 아니고 필요하면 여러 인터페이스를 구현할 수 있음

이러한 기능들은 완전한 타입 안전성을 제공함
프로퍼티를 빼먹거나 구현에 문제가 있으면 타입스크립트가 바로 지적해줌

-- 인터페이스 구현 vs. 추상클래스 상속

인터페이스 구현은 추상 클래스의 상속과 매우 비슷하지만 인터페이스가 범용적인 개념이라면 추상 클래스는 특별한 목적과 풍부한 기능을 가짐

인터페이스는 형태를 정의하는 수단
값 수준에서 객체, 배열, 함수, 클래스, 클래스인스턴스를 정의할 수 있고 아무런 JS 코드도 생성하지 않으며 그저 컴파일 타임에만 존재

추상클래스는 오직 클래스만 정의할 수 있음 추상클래스는 런타임의 자바스크립트 클래스 코드를 만듦
생성자와 기본 구현을 가지고 프로퍼티와 메서드에 접근 한정자를 지정할 수 있음
모두 인터페이스에는 제공되지 않는 기능들

결론적으로 여러 클래스에서 공유하는 구현이라면 추상 클래스를 사용 가볍게 "이 클래스는 ~입니다"는 게 목적이면 인터페이스를 사용

- 클래스는 구조 기반 타입 지원

TS에서 클래스를 비교할 때 이름이 아니라 구조를 기준으로 삼기 때문에 예를 들어, Zebra를 인수로 받는 함수에 Poodle을 전달한다고 해서 반드시 에러를 발생시키진 않음

두 클래스가 .trot()를 구현하며 서로 호환하므로 아무런 문제가 없다
이름으로 구별하는 것이 아닌 구조를 기준으로 삼기 때문이다

하지만, private이나 protected 필드를 갖는 클래스엔 불가

- 클래스는 값과 타입을 모두 선언

타입 스크립트의 거의 모든 것은 값 아니면 타입

// 값
let a = 1999
function b() { }

// 타입
type a = number
interface b { 
	() : void
}

값과 타입은 타입스크립트에서 별도의 네임스페이스에 존재
용어를 어떻게 사용하는지에 따라선 타입스크립트가 알아서 해석함

세상에서 가장 단순한 데이터베이스인 StringDatabase라는 클래스를 만들어보자

이 클래스 선언 코드는 어떤 타입일지 StringDatabase의 인스턴스 타입은 다음과 같음

다음은 typeof StringDatabase의 생성자 타입이다

StringDatabaseConstructor는 .from이라는 한 개의 메서드를 포함하여 new는 StringDatabase를 반환함

그렇기에 두 인터페이스를 합치면 StringDatabase 클래스의 생성자와 인스턴스가 완성됨
new()코드를 생성자 시그니처라고 부르며 생성자 시그니처는 new연산자로 해당 타입을 인스턴스화 할 수 있음을 정의하는 TS의 방식

앞서 말한대로 타입스크립트는 구조를 기반으로 타입을 구분하기 때문에 이 방식이 클래스가 무엇인지 기술하는 최선

위에는 인수를 전혀 받지 않는 생성자이지만 인수를 받는 생성자도 선언할 수 있음
다음 예시는 StringDatabase가 선택적으로 초기 상태를 받도록 수정한 모습

클래스 정의는 용어를 값 수준과 타입 수준으로 생성할 뿐 아니라, 타입 수준에서는 두 개의 용어를 생성함 하나의 클래스는 인스턴스를 가리키며 다른 하나는 클래스 생성자 자체를 가리킴

- 다형성

용어 정리

클래스

함수는 기능을 포장하는 기술 이라면 클래스는 그렇게 만들어진 변수와 함수 중 연관있는 변수와 함수를 선별해 포장하는 기술.
포장하는 이유는 객체 단위로 코드를 그룹화하고 재사용하기 위함.

인스턴스

클래스를 사용하려면 일반적으로 인스턴스를 생성해서 사용.
한 페이지 내에 두개 이상의 같은 동작을 하는 UI를 만들경우 두개의 클래스를 만드는 것이 아니라 하나의 클래스를 만든 후 두개의 인스턴스를 만들어 사용.

객체

객체라는 용어는 인스턴스의 다른 말 (두 용어 모두 클래스의 실체를 나타냄.)
명확히 구분해서 설명하면 인스턴스 라는 용어는 new 키워드를 이용해 클래스의 실체를 생성할 때 주로 사용하며 객체라는 용어는 인스턴스 생성 후 클래스 에서 제공하는 프로퍼티와 메서드를 사용할 때 주로 사용.

프로퍼티(property)

클래스 내부에 만드는 변수를 프로퍼티 라고 부름. (멤버변수 라고도 함)
프로퍼티에는 주로 객체 내부에서 사용하는 일반적인 정보와 객체 내부 함수(메서드) 에서 처리한 결과값이 저장.

메서드(method)

클래스에 만드는 함수를 메서드 라고 부르며 멤버함수 라고도 부름.
메서드는 주로 객체의 프로퍼티 값을 변경하거나 알아내는 기능과 클래스를 대표하는 기능이 담기게 됨.

클래스와 인터페이스도 기본값과 상한/하한 설정을 포함한 다양한 제네릭 타입 매개변수 기능을 지원
제네릭 타입의 범위는 클래스나 인터페이스 전체가 되게 할 수도 있고 특정 메서드로 한정할 수도 있음

  1. class와 함께 제네릭을 선언했으므로 클래스 전체에서 타입을 사용 가능, MyMap의 모든 인스턴스 메서드와 인스턴스 프로퍼티에서 K와 V를 사용할 수 있음
  2. constructor에는 제네릭 타입을 선언할 수 없음 constructor 대신 class 선언에 사용해야함
  3. 클래스로 한정된 제네릭 타입은 클래스 내부의 어디에서나 사용 가능
  4. 인스턴스의 메서드는 클래스 수준 제네릭을 사용 할 수 있고 자신만의 제네릭도 추가로 선언할 수 있음 .merge는 클래스 수준 제네릭인 K와 V에 더해 자신만의 제네릭 타입인 K1와 V1을 추가로 선언 (MyMap뒤)
  5. 정적 메서드는 클래스의 인스턴스 변수에 값 수준에 따라 접근할 수 없듯이 클래스 수준의 제네릭을 사용할 수 없음 따라서 of는 선언한 K와 V에 접근할 수 없고 자신만의 K와 V를 직접 선언

인터페이스에도 제네릭을 사용할 수 있음

함수와 마찬가지로 제네릭에 구체 타입을 명시하거나 타입스크립트가 타입으로 추론하도록 가능

- 믹스인

동작과 프로퍼티를 클래스로 혼합할 수 있게 해주는 패턴

  • 상태를 가질 수 있다 (예: 인스턴스 프로퍼티)
  • 구체 메서드만 제공할 수 있음 (추상 메서드는 안됨)
  • 생성자를 가질 수 있음 (클래스가 혼합된 순서와 같은 순서로 호출됨)

- 데코레이터

타입스크립트의 실험적 기능으로 클래스, 클래스 메서드, 프로퍼티, 메서드 매개변수를 활용한 메타 프로그래밍에 깔끔한 문법을 제공

장식하는 대상의 함수를 호출하는 기능을 제공하는 문법

예를들어 메소드 / 클래스 / 프로퍼티 / 파라미터 위에 @함수 를 장식해줌으로써, 코드가 실행(런타임)이 되면 데코레이터 함수가 실행되어, 장식한 멤버를 보다 파워풀하게 꾸며주는 것으로 이해하면 된다.

@(at) 기호는 타입스크립트에게 이것이 데코레이터임을 알려주고, 타입스크립트는 클래스 실행 시 플러그인 형태로 실행되게 해준다.

타입스크립트가 기본으로 제공하는 데코레이터는 없고 모든 데코레이터는 직접 구현하거나 NPM으로 설치해야함
모든 종류의 데코레이터는 특정 시그니처를 만족하는 일반 함수일 뿐

타입스크립트는 데코레이터가 장식하는 대상의 형태를 바꾸지 않는다고, 즉 메서드나 프로퍼티를 추가하거나 삭제하지 않았다고 가정함

반환된 클래스를 전달된 클래스에 할당할 수 있는지는 컴파일 타임에만 확인하며, 코드를 작성할 떈 데코레이터가 어떻게 확장되는지 추적하지 않음

- final 클래스 흉내

TS에서 final을 지원하진 않지만 클래스에서 final의 효과를 흉내내기는 쉬움

final 키워드는 클래스나 메서드를 확장하거나 오버라이드할 수 없게 만드는 기능

오버로딩 (Overloading, 과적하다)

오버로딩의 정의는 자바의 한 클래스 내에 이미 사용하려는 이름과 같은 이름을 가진 메소드가 있더라도 매개변수의 개수 또는 타입이 다르면, 같은 이름을 사용해서 메소드를 정의할 수 있다.

오버로딩의 조건

메소드의 이름이 같고, 매개변수의 개수나 타입이 달라야 한다. 주의할 점은 '리턴 값만' 다른 것은 오버로딩을 할 수 없다는 것이다.


오버라이딩 (Overriding)

부모 클래스로부터 상속받은 메소드를 자식 클래스에서 재정의하는 것을 오버라이딩이라고 한다. 상속받은 메소드를 그대로 사용할 수도 있지만, 자식 클래스에서 상황에 맞게 변경해야하는 경우 오버라이딩할 필요가 생긴다.

오버라이딩의 조건

오버라이딩은 부모 클래스의 메소드를 재정의하는 것이므로, 자식 클래스에서는 오버라이딩하고자 하는 메소드의 이름, 매개변수, 리턴 값이 모두 같아야 한다.

타입스크립트에서는 비공개 생성자 (private constructor)로 final 클래스를 흉내 낼 수 있음

생성자를 private로 선언하면 new로 인스턴스를 생성하거나 클래스를 확장할 수 없음

클래스 상속만 막아야 하는 상황이지만 비공개 생성자를 이용하면 클래스를 인스턴스화하는 기능도 같이 사라지지만, final 클래스는 상속만 막을 뿐 인스턴스는 정상적으로 만들 수 있음

이 문제를 해결하기 위해선 다음처럼 쉽게 해결이 가능함

MessageQueue의 API를 조금 바꿨지만 컴파일 타임에 성공적으로 확장을 막을 수 있음

- 디자인 패턴

타입스크립트로 디자인 패턴을 구현해보아야 객체 지향 프로그래밍을 다뤘다고 이야기 할 수 있음

-- 팩토리 패턴

팩토리 패턴은 어떤 객체를 만들지를 전적으로 팩토리에 위임함
예시로 신발 팩토리를 만듦

이 예시에서는 type을 활용하였지만 interface를 사용해도 된다는 사실을 잊으면 안 됨

  1. type을 유니온 타입으로 지정하여 컴파일 타임에 호출, 컴파일 타임에 유효하지 않은 type을 전달하지 못하도록 방지하여 .create 타입 안전성을 최대한 강화함

  2. switch문을 이용하여 누락된 shoe 타입이 없는지 타입스크립트가 쉽게 확인할 수 있음

이번 예를 통해 컴패니언 객체로 타입 Shoe와 값 Shoe를 같은 이름으로 선언 (TS는 값과 타입의 네임스페이스를 따로 관리)
이렇게 하여 이 값이 해당 타입과 관련한 메서드를 제공한다는 정보를 알 수 있음
이 팩토리를 이용하려면 그저 .create를 호출

Shoe.create('boot') // Shoe 

-- 빌더 패턴

빌더 패턴으로 객체의 생성과 객체 구현 방식을 분리할 수 있음
Map, Set, 제이쿼리 등의 자료구조를 사용해봤다면 빌더 패턴에 친숙할 것임

우선 클래스의 뼈대를 정의한 후에

class RequestBuilder {}

.setURL 메서드를 추가


1. url 이라는 비공개 변수 (초기값 null)로 사용자가 설정한 URL을 추적
2. setURL의 반환 타입은 this 즉, setURL을 호출한 특정 RequestBuilder의 인스턴스

그 후 나머지 메서드를 차례로 추가


요약

5장에서 살펴본 것들은 클래스 선언 방법, 클래스 상속과 인터페이스 구현 방법, 클래스를 인스턴스화 할 수 없도록 abstract를 추가하는 방법, 클래스의 필드와 메서드에 static을 추가하고 인스턴스에는 추가하지 않는 방법, private, protected, public 가시성 한정자로 필드와 메서드의 접근을 제어하는 방법, readonly 한정자로 필드에 값을 기록할 수 없게 만드는 법 등을 배움. this와 super를 안전하게 사용하는 방법과 이들이 클래스의 값과 클래스의 타입 모두에 어떤 의미인지 확인함
타입별칭과 인터페이스의 차이, 선언 합치기 개념, 클래스에 제네릭 타입을 사용하는 방법 등을 살펴봄
마지막으로 믹스인, 데코레이터, final 클래스 흉내내기 등 다양한 고급 패턴도 다룸
클래스를 활용한 두가지 유명한 디자인 패턴을 살펴봄

profile
기록, 꺼내 쓸 수 있는 즐거움

0개의 댓글