다형성(polymorphism)

2022. 5. 24. 14:35OOP

 (개인적으로 쉽게 이해갈것도 같으면서도 , 심화하면 꽤나 깊이 내려갈거같은 느낌... )

검색결과들...

 

다형성(polymorphism)

조상(부모)클래스의 인스턴스를 이용하여 자손타입의 클래스를 다룬다거나, 메소드 오버로딩을 통하여 동일 이름의 메소드를 이용하여 다양한 형태의 파라미터를 다루는 것을 뜻한다.  하나의 객체가 여러 가지 타입을 가질 수 있는 것.

이때 참조 변수가 사용할 수 있는 멤버의 개수가 실제 인스턴스의 멤버 개수보다 같거나 적어야 참조할 수 있습니다

하지만 반대의 경우인 자식 클래스 타입의 참조 변수로는 부모 클래스 타입의 인스턴스를 참조할 수 없습니다.

참조 변수가 사용할 수 있는 멤버의 개수가 실제 인스턴스의 멤버 개수보다 많기 때문입니다.

- 하나의 코드가 여러 자료형으로 구현되어 실행되는 것

- 같은 코드에서 여러 다른 실행 결과가 나옴

 

사용하는이유:

1.상위클래스에서는 공통적인 부분을 제공하고 하위클래스에서는 각 클래스에 맞는 기능 구현

2. 여러 클래스를 하나의 타입(상위클래스)으로 핸들링 할 수 있음

3. 나중에 추가하는 경우 상속과 메서드 재정의를 활용하여 확장성 있는 프로그램을 만들 수 있음.

예제

class Parent { ... }

class Child extends Parent { ... }

...

Parent pa = new Parent(); // 허용

Child ch = new Child();   // 허용

Parent pc = new Child();  // 허용

Child cp = new Parent();  // 오류 발생.

 

참조 변수의 타입 변환

자바에서는 참조 변수도 다음과 같은 조건에 따라 타입 변환을 할 수 있습니다.

 

1. 서로 상속 관계에 있는 클래스 사이에만 타입 변환을 할 수 있습니다.

2. 자식 클래스 타입에서 부모 클래스 타입으로의 타입 변환은 생략할 수 있습니다.

3. 하지만 부모 클래스 타입에서 자식 클래스 타입으로의 타입 변환은 반드시 명시해야 합니다.

 

참조 변수의 타입 변환도 기본 타입의 타입 변환과 마찬가지로 타입 캐스트 연산자(())를 사용합니다.

문법

(변환할타입의클래스이름) 변환할참조변수

 

다음 예제는 참조 변수의 타입 변환을 보여주는 예제입니다.

예제

class Parent { ... }

class Child extends Parent { ... }

class Brother extends Parent { ... }

...

Parent pa01 = null;

Child ch = new Child();

Parent pa02 = new Parent();

Brother br = null;

 

pa01 = ch;          // pa01 = (Parent)ch; 와 같으며, 타입 변환을 생략할 수 있음.

br = (Brother)pa02; // 타입 변환을 생략할 수 없음.

br = (Brother)ch;   // 직접적인 상속 관계가 아니므로, 오류 발생.


instanceof 연산자

이러한 다형성으로 인해 런타임에 참조 변수가 실제로 참조하고 있는 인스턴스의 타입을 확인할 필요성이 생깁니다.

자바에서는 instanceof 연산자를 제공하여, 참조 변수가 참조하고 있는 인스턴스의 실제 타입을 확인할 수 있도록 해줍니다.

 

자바에서 instanceof 연산자는 다음과 같이 사용합니다.

문법

참조변수 instanceof 클래스이름

 

왼쪽에 전달된 참조 변수가 실제로 참조하고 있는 인스턴스의 타입이 오른쪽에 전달된 클래스 타입이면 true를 반환하고, 아니면 false를 반환합니다.

만약에 참조 변수가 null을 가리키고 있으면 false를 반환합니다.

 

다음 예제는 참조 변수가 실제로 가리키고 있는 인스턴스의 타입을 instanceof 연산자로 확인하는 예제입니다.

예제

class Parent { }

class Child extends Parent { }

class Brother extends Parent { }

 

public class Polymorphism01 {

    public static void main(String[] args) {

        Parent p = new Parent();

        System.out.println(p instanceof Object); // true

        System.out.println(p instanceof Parent); // true

        System.out.println(p instanceof Child);  // false

        System.out.println();

 

        Parent c = new Child();

        System.out.println(c instanceof Object); // true

        System.out.println(c instanceof Parent); // true

        System.out.println(c instanceof Child);  // true

    }

}

다형성을 구현하는 방법

대표적으로 알려져있는 “오버로딩”, “오버라이딩”, “함수형 인터페이스”

오버로딩(Overloading)

메소드 오버로딩을 예시로 들어보자. 자바의 PrintStream.class에 정의되어 있는 println이라는 함수는 다음과 같이 매개변수만 다른 여러 개의 메소드가 정의되어 있다. 매개변수로 배열을 넣을 때, 문자열을 넣을 때, 그리고 객체를 넣을 때 모두 println이라는 메소드 시그니처를 호출하여 원하는 내용을 출력하는 기능을 수행한다.

public class PrintStream {
	...
	public void println() {
		this.newLine();
	}

	public void println(boolean x) {
  		synchronized(this) {
      	this.print(x);
      	this.newLine();
  		}
	}

	public void println(char x) {
    	synchronized(this) {
        	this.print(x);
        	this.newLine();
    	}
	}

	public void println(int x) {
    	synchronized(this) {
        	this.print(x);
        	this.newLine();
    	}
	}
	...
}

오버로딩은 여러 종류의 타입을 받아들여 결국엔 같은 기능을 하도록 만들기 위한 작업이다. 이 역시 메소드를 동적으로 호출할 수 있으니 다형성이라고 할 수 있다. 하지만 메소드를 오버로딩하는 경우 요구사항이 변경되었을 때 모든 메소드에서 수정이 수반되므로 필요한 경우에만 적절히 고려하여 사용하는 것이 좋을 듯 하다. 필자는 보통 생성자 오버로딩을 많이 사용한다.

오버라이딩(Overriding)

오버로딩과 이름이 비슷해 헷갈려하는 개발자들도 있을 것이다. 오버라이딩은 상위 클래스의 메서드를 하위 클래스에서 재정의하는 것을 말한다. 따라서 여기서는 상속의 개념이 추가된다. 아래 예시로 보인 추상 클래스 Figure에는 하위 클래스에서 오버라이드 해야 할 메소드가 정의되어 있다.

public abstract class Figure {
    protected int dot;
    protected int area;

    public Figure(final int dot, final int area) {
        this.dot = dot;
        this.area = area;
    }

    public abstract void display();

	  // getter
}

Figure을 상속받은 하위 클래스인 Triangle 객체는 해당 객체에 맞는 기능을 구현한다.

public class Triangle extends Figure {
    public Triangle(final int dot, final int area) {
        super(dot, area);
    }

    @Override
    public void display() {
        System.out.printf("넓이가 %d인 삼각형입니다.", area);
    }
}

만약 사각형 객체를 추가하고 싶다면, 같은 방식으로 Figure을 상속받되 메소드 부분에서 사각형에 맞는 display 메소드를 구현해주면 된다. 이렇게 하면 추후 도형 객체가 추가되더라도 도형 객체가 실제로 사용되는 비즈니스 로직의 변경을 최소화할 수 있다.

public static void main(String[] args) {
    Figure figure = new Triangle(3, 10); // 도형 객체 추가 또는 변경 시 이 부분만 수정

    for (int i = 0; i < figure.getDot(); i++) {
        figure.display();
    }
}

만약 여기서 다형성을 사용하지 않고 도형 객체를 추가하는 로직을 생각해 본다면 아마 다음과 같이 if-else분기가 늘어나게 될 것이다. 도형이 2개 밖에 없는데도 벌써 코드양 차이가 보이는가?

public static void main1(String[] args) {
    int dot = SCANNER.nextInt();

    if (dot == 3) {
        Triangle triangle = new Triangle(3, 10);
        for (int i = 0; i < triangle.getDot(); i++) {
            triangle.display();
        }
    } else if(dot == 4) {
        Rectangle rectangle = new Rectangle(4, 20);
        for (int i = 0; i < rectangle.getDot(); i++) {
            rectangle.display();
        }
    }
	  ....

}

여기까지 오버라이드 방식으로 다형성을 구현하는 방법을 살펴보았다. 예시에서는 추상클래스를 사용했지만, 인터페이스도 구현의 정도만 차이가 있을 뿐 같은 사용 방식은 같다. 오버라이드 다형성 방식을 잘 활용하면, 기능의 확장과 객체의 수정에 유연한 구조를 가져갈 수 있다.

함수형 인터페이스(Funtional Interface)

마지막으로는 함수형 인터페이스 방식을 살펴보자. 함수형 인터페이스(Functional Interface)란, 람다식을 사용하기 위한 API로 자바에서 제공하는 인터페이스에 구현할 메소드가 하나 뿐인 인터페이스를 의미한다. 함수형 인터페이스는 enum과 함께 사용한다면 다형성의 장점을 경험할 수 있다.

가장 간단한 예시로 문자열 계산기를 예시로 들어보겠다.

public enum Operator {
    PLUS("+", (a, b) -> a + b),
    MINUS("-", (a, b) -> a - b),
    MULTIPLY("*", (a, b) -> a * b),
    DIVIDE("/", (a, b) -> a / b);

    private final String sign;
    private final BiFunction<Long, Long, Long> bi;

    Operator(String sign, BiFunction<Long, Long, Long> bi) {
        this.sign = sign;
        this.bi = bi;
    }

	  public static long calculate(long a, long b, String sign) {
    	  Operator operator = Arrays.stream(values())
            	.filter(v -> v.sign.equals(sign))
            	.findFirst()
            	.orElseThrow(IllegalArgumentException::new);

    	  return operator.bi.apply(a, b);
	  }
}

사칙연산을 할 수 있는 각각의 연산자를 enum으로 미리 정의하고 연산 방식을 BiFuntion을 사용한 람다식으로 정의할 수 있다. 이때 연산자를 추가해야할 경우 enum에 추가하기만 하면, 실질적인 연산을 수행하는 calculate 메소드는 아무런 수정없이도 기능을 확장할 수 있다.

public static void main(String[] args) {
    String question = "4*7";
    String[] values = question.split("");

    long a = Long.parseLong(values[0]);
    long b = Long.parseLong(values[2]);

    long result = Operator.calculate(a, b, values[1]);
    System.out.println(result); //28
}

결론

필자는 개인적으로 다형성이 객체 지향 패러다임의 주요 특성 중 가장 핵심이라고 생각한다. 변화에 유연한 소프트웨어를 만들기 위해서 객체 지향 패러다임을 사용하는 것이라면, 그러한 목적 달성에 중추적인 역할을 “다형성”이 해내기 때문이다.

우리는 객체 지향으로 설계를 잘하기 위한 원칙인 SOLID원칙을 알고 있다. SOLID 중에서도 가장 어려운 OCP(Open-Closed Principle)와 DIP(Dependency Inversion Principle)는 다형성을 기본으로 하고 있다. 물론 다형성을 사용하기 위해서는 추상화, 상속 개념이 필요하긴 하지만 본질적인 목적을 본다면 결국은 다형성을 잘 활용하는 것이 코드의 중복을 줄이면서 변경과 확장에 유연한 객체 지향적인 코드를 작성하는데 유용하다는 것을 알 수 있을 것이다.

정리하면 다형성은 하나의 타입에 여러 객체를 대입할 수 있는 성질이고, 이것을 구현하기 위해서는 여러 객체들 중 공통 특성으로 타입을 추상화하고 그것을 상속(인터페이스라면 구현)해야한다. 이 특성들을 유기적으로 잘 활용했을 때, 비로소 객체 지향에 가까운 코드를 작성할 수 있을 것이라 생각한다.

 

'OOP' 카테고리의 다른 글

캡슐화  (0) 2022.05.27
추상화  (0) 2022.05.25