Polymorphism은 many라는 뜻의 poly와, form이라는 뜻의 morph의 조합으로, 다양한 형태many forms를 의미한다. 자바에서는 동일한 네이밍을 가지고 여러 형태의 액션을 취하는 테크닉을 의미한다.
static method overloading을 통해 컴파일 시간 다형성을 실현할 수 있으며, overriding을 통해 실행시간 다형성runtime polyporphism을 실현할 수 있다.
실행시간 다형성이 일반적으로 말하는 다형성이다. 컴파일 타임이 아닌, 런타임에 upcasting된 (복습하는 의미로 링크된 포스트의 참조 타입 변환 부분 참조.) 자식 클래스의 오버라이딩된 메소드를 호출한다. 실행시간에 어떤 자식클래스의 오버라이딩 된 메소드를 호출할지가 명확해지기 때문이다. 실행시간 다형성의 다른 말은 dynamic method dispatch이다.
dynamic은 runtime의 동의어로 사용되며, dispatch는 어떤 메소드를 호출할지 결정하는 것이다.
예를 들어 다음과 같은 상속 관계를 가진 코드가 있다.
코드를 보면 overriding을 통해 메소드 재정의를 해두고, 메인 함수에서 동일한 이름의 메소드를 호출하고 있다. 결과는 어떻게 될까?
static class Super {
void print() {
System.out.println("super's print");
}
}
static class Sub1 extends Super{
@Override
void print() {
System.out.println("sub1's print");
}
}
static class Sub2 extends Super{
@Override
void print() {
System.out.println("sub2's print");
}
}
public static void main(String[] args) {
Super reference = new Super(); // 1)
reference.print();
reference = new Sub1(); // 2)
reference.print();
reference = new Sub2(); // 3)
reference.print();
}
Super 타입의 객체인 reference에 자식 객체인 Sub1과 Sub2를 대입하면 upcasting이 이루어지고, reference는 대입 될때마다 자식 객체의 주소를 가리키게 된다. 혼란이 있을까 언급하자면, reference가 3개의 객체를 모두 가리키는 것이 아니라 대입될때마다 순서대로 가리키게 된다.
그래서 다음과 같은 결과가 나온다.
결과
super's print
sub1's print
sub2's print
upcasting과 overriding을 통해 runtime polymorphism을 구현할 수 있다.
더블 디스패치는 런타임 디스패치를 두번 시도하는 것이다. 예를 들어(토비님 유튜브 예시) 텍스트와 사진 두가지의 포스트를 올릴수 있고, 내가 하는 모든 SNS에 포스트를 한꺼번에 올리는 코드를 작성했다고 가정하자.
interface Post { void postOn(SNS sns); }
static class Text implements Post {
@Override
public void postOn(SNS sns) {
if(sns instanceof Instagram) {
// logic which is applicable to Instagram
}
else if (sns instanceof Twitter) {
// logic which is applicable to Twitter
}
}
}
static class Picture implements Post {
@Override
public void postOn(SNS sns) {
if(sns instanceof Instagram) {
// logic which is applicable to Instagram
}
else if (sns instanceof Twitter) {
// logic which is applicable to Twitter
}
}
}
interface SNS { }
static class Instagram implements SNS{ }
static class Twitter implements SNS{ }
위의 코드의 문제점은 다른 타입의 SNS가 추가될 때마다 SNS의 인스턴스 타입을 판별하는 if문을 계속 추가해줘야 한다. 만약, 또다른 SNS인 Facebook 로직을 Text와 Post에 구현하고 if문을 추가하지 않았다면 Facebook 업로드 로직을 처리할 수가 없는 상태이다. 즉, 코드 관리의 cost가 있는 코드이다. 위의 문제를 double dispatch를 통해 해결할 수 있다.
먼저, 포스트 업로드하는 비즈니스 로직을 SNS 으로 옮기고, Post 타입의 객체인 Text와 Picture에서 sns의 post 메소드를 호출하면서 자기 자신을 인자로 넘긴다. 이 기법이 Double dispatch이다.
interface Post { void postOn(SNS sns); }
static class Text implements Post {
@Override
public void postOn(SNS sns) {
sns.post(this);
}
}
static class Picture implements Post {
@Override
public void postOn(SNS sns) {
sns.post(this);
}
}
interface SNS {
void post(Text post);
void post(Picture post);
}
static class Instagram implements SNS{
@Override
public void post(Text post) {
// text upload logic which is applicable to Instagram
}
@Override
public void post(Picture post) {
//picture upload logic which is applicable to Instagram
}
}
static class Twitter implements SNS{
@Override
public void post(Text post) {
// text upload logic which is applicable to Twitter
}
@Override
public void post(Picture post) {
// picture upload logic which is applicable to Twitter
}
}
결과적으로, 새로운 Facebook sns를 구현해 업로드하는 클라이언트 코드를 작성해도 Post의 코드를 건드릴 필요가 전혀없다. Facebook 코드만 충실히 구현되있으면 된다. 앞선 코드보다 코드 관리 cost가 확 줄어들고, 확장에 대해선 열리고 수정에 대해 닫힌 객체지향성을 지킨 코드가 되었다.
다이나믹 디스패치의 조건을 파라미터에 대해 걸지 말자! 파라미터로 넘어온 객체의 메소드를 호출하며 자기 자신을 넘기자.
토비님은 현업에서 더블 디스패치를 거의 적용할 일이 없지만, 두가지 이상의 객체가 계층적 구조를 형성하고 메소드 호출이 자기 자신과 연관이 있는 경우 더블 디스패치를 적용하면 좋다고 언급하셨다.
참고
https://www.javatpoint.com/runtime-polymorphism-in-java
https://www.geeksforgeeks.org/dynamic-method-dispatch-runtime-polymorphism-java/
https://en.wikipedia.org/wiki/Dynamic_dispatch
https://youtu.be/s-tXAHub6vg
계속해서 문서를 업데이트하고 있습니다. 언제든지 댓글피드백 남겨주세요. 😉