다형성

bp.chys·2020년 4월 9일
0

OOP & Design Pattern

목록 보기
8/17

객체 지향에서의 다형성

  • 객체 지향 프로그래밍에서 사용되는 다형성은 유니버셜(universal) 다형성임시(Ad Hoc)다형성으로 분류할 수 있다.
    • 유니버셜 다형성 : 매개변수(Parametric) 다형성 + 포함(Inclusion) 다형성
    • 임시 다형성 : 오버로딩(Overloading) 다형성 + 강제(Coercion) 다형성
  • 일반적으로 가장 널리 알려진 형태의 다형성은 포함 다형성이다.
  • 포함 다형성은 서브타입 다형성이라고도 불리며, 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력을 의미한다.
  • 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다. 이는 인터페이스가 동일해야한다는 것을 말한다.

다형성의 원리

업캐스팅

  • 업캐스팅은 서로 다른 클래스의 인스턴스를 동일한 타입에 할당하는 것을 가능하게 해준다.
  • 부모 클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용하더라도 메시지를 처리하는 데는 아무런 문제가 없으며, 컴파일러는 명시적인 타입 변환 없이도 자식 클래스가 부모 클래스를 대체할 수 있게 허용한다.

동적 바인딩

  • 객체지향 언어에서는 메시지를 수신했을 때 실행될 메서드가 런타임에 결정된다.
  • 이처럼 실행될 메서드를 런타임에 결정하는 방식을 동적 바인딩 혹은 지연 바인딩이라고 한다.

동적인 문맥

  • 메시지를 수신한 객체가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 동적으로 바뀐다.
  • 동적인 문맥을 결정하는 것은 바로 메시지를 수신한 객체를 가리키는 self 참조다.
  • self 참조가 특정 객체의 인스턴스를 가리키고 있다면 메서드를 탐색할 문맥은 해당 클래스에서 시작해서 Object 클래스에서 종료되는 상속 계층이 된다.
  • 동일한 코드라고 하더라도 self 참조가 가리키는 객체가 무엇인지에 따라 메서드 탐색을 위한 상속 계층의 범위가 동적으로 변한다.
  • 따라서 self 참조가 가리키는 객체의 타입을 변경함으로써 객체가 실행될 문맥을 동적으로 바꿀 수 있다.

다형성 예시

오버로딩(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분기가 늘어나게 될 것이다.
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과 함께 사용한다면 다형성의 장점을 경험할 수 있다.
  • 가장 간단한 예시로 문자열 계산기를 예시로 들어보겠다.
  • 사칙연산을 할 수 있는 각각의 연산자를 enum으로 미리 정의하고 연산 방식을 BiFuntion을 사용한 람다식으로 정의할 수 있다.
  • 이때 연산자를 추가해야할 경우 enum에 추가하기만 하면, 실질적인 연산을 수행하는 calculate 메소드는 아무런 수정없이도 기능을 확장할 수 있다.
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);
	  }
}
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
}

결론

변화에 유연한 소프트웨어를 만들기 위해서 객체 지향 패러다임을 사용하는 것이라면, 그러한 목적 달성에 중추적인 방법으로 다형성을 적용할 수 있다.

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


참고자료

  • NHN 기술세미나, 객체지향 입문 - 최범균
  • 오브젝트 - 조영호
profile
하루에 한걸음씩, 꾸준히

0개의 댓글