본문 바로가기

[OOP] 다형성 (객체 지향과 디자인 패턴)

 

들어가며

스타트업의 개발자로서 좋은 설계가 우선인가, 아니면 빠른 기능 개발이 우선인가 항상 고민하곤 했습니다. 제가 다녔던 스타트업은 빠르게 기능 개발을 해서, 시장에서 인정받아야 했기에, 빠른 시간 안에 많은 기능을 개발해야 했습니다. 그러다 문득 추가 기능을 개발하거나, 기존 기능을 수정해야 할 때, 많은 어려움을 겪곤 했습니다. 그렇게 일을 마치고, 더 나은 개발자가 되기 위한 공부를 하면서 제가 작성한 코드는 배려가 부족한 코드였다는 것을 깨달았습니다.

 

제가 개발한 코드는 당장의 기능 개발은 빠르게 할 수 있더라도, 재사용성이 대단히 떨어졌고, 가독성 또한 대단히 떨어졌습니다. 또한 기능을 수정하거나 추가할 때도 광범위한 코드를 건드려야만 했기에 효율성도 떨어졌습니다. 빠르게 기능을 개발하기 위해 적었던 코드가 언젠가는 발목을 잡을 수 있는 코드가 될 수 있다는 것을 깨달았습니다. 그렇다면 비즈니스에 도움이 되고, 다른 팀원들에게도 도움이 될 수 있는, 배려가 담긴 코드를 작성하기 위해서는 어떻게 해야 할까 고민하기 시작했습니다. 배려가 담긴 코드를 작성하기 위해 공부한 내용에 대해 작성해보고자 합니다. 이 글은 도서 객체 지향과 디자인 패턴을 바탕으로 작성됐습니다.

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

다형성

객체 지향 패러다임의 4원칙(캡슐화, 추상화, 다형성, 상속) 중의 하나인 다형성에 대해 알아보겠습니다. 다형성(polymorphism)은 '여러 모양'을 의미하는 그리스 단어이고 다형성에서 형은 타입(type)을 의미합니다. 쉽게 말하면 프로그래밍 언어에서 다형성이란, 한 객체가 여러 타입을 가질 수 있다는 뜻입니다. 그럼 반대로 단형성이라는 것은 하나의 객체에 하나의 타입만 대응할 수 있습니다. 먼저 단형성의 예시를 살펴보겠습니다.

 

예를 들어, 특정한 자료형을 문자열로 바꾼다고 가정하겠습니다. 그렇다면, 숫자나 날짜 자료형을 문자열로 바꾸기 위해 아래와 같은 메서드들을 정의할 수 있습니다.

 

//숫자를 문자열로 바꾸는 경우
age: string = stringFromNumber(number);

//날짜를 문자열로 바꾸는 경우
today: string = stringFromDate(date);

 

 

숫자를 문자열로 바꿀 때는 stringFromNumber(), 날짜를 문자열로 바꿀 때는 stringFromDate()를 사용하는 것입니다. 이것은 단형성 체계의 언어에서는 함수의 이름이 중복될 수 없기 때문입니다. (물론 오버 로딩을 사용한다면 함수의 이름이 중복되어도 무방합니다.)

 

아무튼, 위와 같이 비슷한 기능을 하는 함수의 이름을 자료형에 따라 끝도 없이 나열한다면 상상만 해도 끔찍할 것입니다. 그래서 다형성 체계의 언어에서는 함수의 이름을 같게 하여 위의 작업을 아래와 같이 간결하게 만들 수 있습니다. 

 

 

//숫자를 문자열로 바꾸는 경우
age: string = stringValue(number);

//날짜를 문자열로 바꾸는 경우
today: string = stringValue(date);

 

 

물론, 여러 자료형에 따라 문자열로 바꾸는 작업 자체가 줄어든 것은 아니지만 메서드 하나의 이름만 가지고도 기억하기 쉽고 메서드의 이름을 절약한다는 장점이 있습니다. 개발을 하다 보면 특히 이 메서드나 변수명을 이름 짓기가 어려운데, 그 부담을 덜어줄 수 있습니다. 위 사례는 어디까지나 다형성의 하나의 예시이며, 지금부터 다형성을 구현하는 방법을 알아보겠습니다.

 

 

 

 

 


 

 

 

 

 

다형성 구현 방법

다형성을 구현하는 방법은 대표적으로 오버 라이딩. 오버 로딩, 함수형 인터페이스를 사용하는 것이 있습니다. 그중 오버 라이딩부터 살펴보겠습니다. 

 

 

 

오버 라이딩으로 다형성 구현하기

메서드 오버 라이딩은(overriding)은 부모 클래스에 정의된 메서드를 자식 클래스에서 새로 구현하는 것을 일컫는 개념입니다. 주로 클래스 상속이나 인터페이스 상속을 통해 구현할 수 있습니다. 여기서 오버 라이딩할 대상이 있는 부모 클래스를 오버 라이든 클래스(overridden class)라고 합니다. 오버 라이든 클래스에는 오버 라이든 메서드(overridden method)가 존재합니다. 오버 라이든 메서드는 파생 클래스에 정의된 메서드에 오버 라이딩돼 오버 라이딩 메서드로 새롭게 재정의됩니다.

 

오버 라이딩으로 메서드가 재정의되려면 기본적으로 오버 라이든 메서드와 오버 라이딩 메서드는 서로 이름이 같아야 합니다. 그리고 오버 라이딩을 위해 다음 두 조건을 만족해야 합니다. 

 

 

조건 1: 오버 라이든 메서드의 매개변수 타입은 오버 라이딩 메서드의 매개변수 타입과 같거나 상위 타입이어야 한다.
조건 2: 오버 라이든 메서드의 매개변수 개수가 오버 라이딩 메서드의 매개변수 개수와 같거나 많아야 한다. 
(단, 조건 1이 성립된다는 전제가 있어야 함)

 

class Bird {
  constructor() {}
  flight(kmDistance = 0, kmSpeed = 0) {
    console.log(`새가 ${kmDistance}km를 ${kmSpeed}km의 속도로 비행했습니다.`);
  }
}

class Eagle extends Bird {
  constructor() {
    super();
  }

  flight(kmDistance2 = 0) {
    console.log(`독수리가 ${kmDistance2}km를 비행했습니다.`);
  }
}

const bird = new Bird();
bird.flight(1000, 100); // 새가 1000km를 100km의 속도로 비행했습니다.

const eagle = new Eagle();
eagle.flight(); // 독수리가 0km를 비행했습니다.
eagle.flight(1000); // 독수리가 1000km를 비행했습니다.

 

 

위 코드는 조건 1과 조건 2를 모두 만족하는 예제 코드입니다. flight 메서드는 이름은 같지만, Bird의 flight 메서드가 오버 라이든 메서드이며, Bird를 상속받은 Eagle 클래스의 flight 메서드가 오버 라이딩 메서드입니다. 오버 라이딩 메서드를 보면 오버 라이든 메서드보다 매개 변수의 숫자가 적으며, 타입이 같습니다. 

 

 

 

오버 로딩으로 다형성 구현하기

메서드 오버 로딩은 메서드의 이름이 같지만 매개변수의 타입을 다르게 정의하는 방법을 일컫습니다. 오버 로딩을 활용하기 위해서는 몇 가지 규칙을 지켜야 합니다. 규칙은 아래와 같습니다.

 

  • 메서드의 이름이 같아야 한다.
  • 매개 변수의 개수 또는 타입이 달라야 한다.
  • 매개 변수는 같고, 리턴 타입이 다를 때는 성립하지 않는다.
  • 오버 로딩된 메서드들은 매개 변수로만 구분될 수 있다.

 

4가지를 작성했지만, 핵심은 매개 변수의 타입, 개수, 순서 중에 하나 이상 달라야 합니다. 오버 로딩은 오버 라이딩 메서드를 오버 로딩하는 방법과 인터페이스를 클래스에서 구현하여 오버 로딩하는 방법이 있습니다. 먼저 오버 라이딩 메서드가 오버 로딩을 수행하는 방법에 대해 알아보겠습니다.

 

 

class SingleTypeChecker {
  typeCheck(value: string): void {
    console.log(`${typeof value} : ${value}`);
  }
}

class UnionTypeChecker extends SingleTypeChecker {
  constructor() {
    super();
  }

  typeCheck(value: number): void;
  typeCheck(value: string): void;
  typeCheck(value: any): void {
    if (typeof value === 'number') {
      console.log('숫자 : ', value);
    } else if (typeof value === 'string') {
      console.log('문자열 : ', value);
    } else {
      console.log('기타 : ', value);
    }
  }
}

const unionTypeChecker = new UnionTypeChecker();
unionTypeChecker.typeCheck(123); // 숫자 : 123
unionTypeChecker.typeCheck('happy'); // 문자열 : happy
// 에러
// unionTypeChecker.typeCheck(true);

 

 

오버로드는 함수 이름은 같지만, 매개변수 선언 형태가 다른 특성을 가지고 있습니다. 위 예제에서는 any 타입에 제약을 가해 number와 string만을 받을 수 있도록 typeCheck 메서드를 재정의했습니다.

 

또한 인터페이스를 이용해서 오버 로딩을 구현할 수도 있습니다. 인터페이스를 이용해 오버 로딩을 하려면 인터페이스에 오버 로딩할 기본 메서드를 선언해 줍니다. 그리고 인터페이스를 구현할 클래스에서 기본 메서드를 구현해 줍니다. 아래의 예제를 살펴보겠습니다.

 

 

interface IPoint {
  getX(x: any): any;
}

class Point implements IPoint {
  getX(x?: number | string): any {
    if (typeof x === "number") {
      return x;
    } else if (typeof x === "string") {
      return x;
    }
  }
}

let p = new Point();
console.log(p.getX()); // undefined
console.log(p.getX("hello")); // hello
console.log(p.getX(123)); // 123

 

 

인터페이스를 사용하면 선언과 구현을 분리할 수 있고 구현부의 구조를 강제화 할 수 있습니다. 이 점에서 로직과 구조가 섞여 있는 클래스를 상속해 오버 로딩하는 것보다 구조만을 포함하고 있는 인터페이스를 이용하는 것이 복잡도가 낮습니다

 

 

다형성 예시

그럼 다형성에 대한 예시를 살펴보겠습니다.  먼저 자식 클래스가 부모 클래스를 상속하고 있을 때 부모 클래스를 타입으로 가지는 객체 참조 변수에 자식 클래스의 객체가 할당(구조 타이핑)됨으로써 다형성을 지니게 됩니다. 이런 구현 상속은 보통 상위 클래스에 정의된 기능을 재사용하기 위한 목적으로 사용됩니다. 

 

 

class Planet {
  public diameter: number; //지름
  protected isTransduction: boolean = true; //공전
  
  getIsTransduction(): boolean {
    return this.isTransduction;
  }
  
  stopTransduction(): void {
    console.log("stop1");
    this.isTransduction = false;
  }
}

class Earth extends Planet {
  public features: string[] = ["soil", "water", "oxyzen"];
  stopTransduction(): void {
    console.log("stop2");
    this.isTransduction = false;
  }
}

let earth: Planet = new Earth();
earth.diameter = 12656.2;
console.log("1번 : " + earth.diameter); // 12656.2
console.log("2번 : " + earth.getIsTransduction()); //true
earth.stopTransduction(); //stop2
console.log("3번 : " + earth.getIsTransduction()); // false
console.log(earth.features); // 접근불가

 

 

이러한 상속 관계에서 부모 클래스(Planet)의 타입으로 지정된 객체 참조 변수(earth)는 자식 클래스의 객체(new Earth())를 할당받더라도 실제 동작은 부모 클래스를 기준으로 실행됩니다. 따라서 earth는 부모 클래스에 선언된 getIsTransduction()에 접근할 수 있지만 자식 클래스에 선언된 멤버 변수(features)에는 접근할 수 없습니다. 여기서 유의해서 볼 점은 stopTransduction() 메서드는 오버 라이든 메서드보다 오버 라이딩 메서드가 우선으로 호출됩니다. 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

마치며

객체지향 프로그래밍의 필요성을 점점 느끼고 있습니다. 언젠가, 코드에 대한 역할과 책임을 명확하게 보여줄 수 있는 코드를 작성할 수 있다면 동료들이, 일을 더 수월하게 할 수 있지 않을까 생각합니다. 배려가 담긴 코드를 작성하기 위해 꾸준하게 노력하고 싶습니다. 

 

 

 

 

 

 


 

 

 

 

 

출처

 

TypeScript 클래스와 인터페이스

객체지향 프로그래밍(OOP, Object-Oriented Programming)은 애플리케이션을 개발할 때 코드 중복을 획기적으로 줄일 수 있는 방법이다. 객체지향 프로그래밍은 커다란 문제를 클래스라는 단위로 나누고

velog.io

 

TypeScript #5 클래스와 인터페이스 Part2

지난번 포스팅에 이어서 타입스크립트의 클래스와 인터페이스에 대해서 조금 더 정리해 보려고 한다. ㅎㅎ 오버라이딩(Overriding) 오버라이딩은 부모 클래스에 정의된 메서드를 자식 클래스에서

devowen.com

 

 

다형성(Polymorphism)이란?

객체 지향 패러다임의…

tecoble.techcourse.co.kr

 

객체 지향 프로그래밍 입문 - 인프런 | 강의

잘 하는 개발자가 되기 위해서는 유연한 코드를 작성할 줄 알아야합니다. 객체 지향을 이용해서 변경하기 좋은 유연한 코드를 만드는 방법을 알아보세요., - 강의 소개 | 인프런...

www.inflearn.com

 

객체 지향과 디자인 패턴 - YES24

객체 지향 안내서. 객체, 책임, 의존, 캡슐화 등 객체 지향의 주요 개념들을 쉬운 예제와 그림을 통해 이해하기 쉽게 설명한다.

www.yes24.com

 

[OOP] 다형성(Polymorphism)이란?

안녕하세요? 제이온입니다. 이번 시간에는 객체 지향 패러다임의 4원칙(캡슐화, 다형성, 상속, 추상화) 중의 하나인 다형성에 대해 알아보겠습니다. 다형성이란? 위키피디아에 따르면, 다형성을

steady-coding.tistory.com

 

[타입스크립트] 오버로딩과 오버라이딩

들어가며 개발을 하면서, 자바스크립트의 언어적 특성 때문에 많은 불편함을 느꼈습니다.  타입 에러를 제대로 확인 못하고 코드를 배포했다가 서버에 문제가 생긴 적이 있었습니다. 런타임 시

overcome-the-limits.tistory.com