Double Dispatch란 무엇일까?

김태훈·2023년 8월 29일
0

Java

목록 보기
3/3
post-thumbnail

개요

해당 글을 읽으면 Double Dispatch라는 개념을 이해할 수 있습니다.
최소한 저한테는 되게 추상적이고 어려운 개념이었는데요.
해당 개념을 풀어서 설명하기 위해 우주선을 행성에 착륙시키는 상황을 가정해보았습니다.

자, 시작해 보겠습니다.
우리는 우주선을 만드는 엔지니어입니다.
우주선이 행성에 착륙하는 로직을 만들어야 합니다.

행성 만들기

우선 태양계에는 수성, 금성, 지구만 존재한다고 가정할게요. (다 하기에는 너무 많아요)

public interface Planet {

}
public class Mercury implements Planet {
}
public class Venus implements Planet {
}
public class Earth implements Planet {
}

위와 같이 행성들에 대한 코드를 짰습니다. 이어서 우주선을 만들어볼게요.

public interface SpaceShip {
    void landing(Planet planet);
    void landing(Mercury planet);
    void landing(Venus planet);
    void landing(Earth planet);
}

위 인터페이스에서 landing이라는 메서드를 행성마다 오버로딩했는데요.
각 행성마다 착륙방식을 다르게 설계해야 하기 때문입니다.

예를 들면 수성에 착륙할 때는 대기가 너무 희박해서 특별한 낙하 장비를 사용해야 한다고 합니다.
금성에 착륙할 때는 대기가 두꺼워서 낙하산만으로 착륙이 가능합니다.
반면 지구에 착륙할 때는 모든 필요없는 장비들을 떼내고 물에 빠집니다.

저 우주선 잘 모릅니다. 실제로 착륙할 때 어떻게 하는지 몰라요. 😔
그냥 각 행성에 착륙하기 위해서는 다른 방법을 사용해야 하구나~ 정도만 이해하고 넘어갑시다.

위 인터페이스를 바탕으로 탐사선 TaehoonSpaceShip 만들어보겠습니다.

public class TaehoonSpaceShip implements SpaceShip {

    @Override
    public void landing(Planet planet) {
        System.out.println("기본 착륙 방식");
    }

    @Override
    public void landing(Mercury planet) {
        System.out.println("낙하 장비를 펼칩니다");
        System.out.println("밑으로 공기를 뿜습니다");
        System.out.println("수성 착륙");
    }

    @Override
    public void landing(Venus planet) {
		System.out.println("낙하산을 펼칩니다");
        System.out.println("금성 착륙");
    }

    @Override
    public void landing(Earch planet) {
        System.out.println("낙하 장치를 펼칩니다");
		System.out.println("속도가 줄어들면 운전석이 분리됩니다");
        System.out.println("물에 떨어지길 기도합니다");
        System.out.println("지구 착륙");        
    }
}

이제 메인함수에서 TaehoonSpaceShip를 착륙시켜보겠습니다.
해당 메인함수를 실행하면 어떤 결과가 나올까요? 한 번 고민해보세요.

import java.util.ArrayList;
import java.util.List;
import org.example.planet.Earth;
import org.example.planet.Mercury;
import org.example.planet.Planet;
import org.example.planet.Venus;

public class Main {
    public static void main(String[] args) {
        SpaceShip ship = new TaehoonSpaceShip();
        // Mercury, Venus, Earch 인스턴스를 planets에 추가한다.
        List<Planet> planets = new ArrayList<>();
        planets.add(new Mercury());
        planets.add(new Venus());
        planets.add(new Earth());
		
        // 각각의 행성에 TaehoonSpaceShip 인스턴스를 landing한다.
        for (Planet planet : planets) {
            ship.landing(planet);
        }
    }
}

첫 번째 로직에 대한 결과

어? 이상합니다. 분명 각 행성별로 착륙 로직을 작성했는데 기본 로직으로 동작하고 있습니다.

바로 그 이유는 ship.landing(planet) 메서드에서 파라미터로 Planet 타입의 객체를 받았기 때문입니다.
planet의 구현체는 Mercury, Venus, Earth지만 자바 입장에서는 알 수 없습니다.
그래서 아래 로직이 실행된겁니다.

    @Override
    public void landing(Planet planet) {
        System.out.println("기본 착륙 방식");
    }

해당 문제를 어떻게 해결할 수 있을까요?
가장 쉬운 방법은 다음과 같습니다.

방법 1 - Main 함수에서 분기를 만들기

public class Main {
    public static void main(String[] args) {
        SpaceShip ship = new TaehoonSpaceShip();
        List<Planet> planets = new ArrayList<>();
        planets.add(new Mercury());
        planets.add(new Venus());
        planets.add(new Earth());

        for (Planet planet : planets) {
        	if (planet instanceof Mercury) {
				ship.landing((Mercury) planet);
            } else if (planet instanceof Venus) {
            ... 생략
            }
        }
    }
}

근데 이렇게 짜면 안됩니다.

왜냐, Main 함수는 해당 로직을 사용하는 클라이언트입니다.
클라이언트가 로직을 사용할 때 특정 메서드의 내부 로직을 알아야 하는 건 전혀 객체지향적이지 않습니다.

정리하면 클라이언트가 내부 구조를 몰라도 사용할 수 있게 만들어야 합니다.
Planet과 Spaceship 만을 수정해서 만들어봅시다.

방법 2 - Double Dispatch

import org.example.SpaceShip;

public interface Planet {
	// 추상메서드가 추가되었습니다.
    void landed(SpaceShip spaceShip);

}
import org.example.SpaceShip;

public class Mercury implements Planet {
    @Override
    public void landed(SpaceShip spaceShip) {
        spaceShip.landing(this);
    }
}
import org.example.SpaceShip;

public class Venus implements Planet {
    @Override
    public void landed(SpaceShip spaceShip) {
        spaceShip.landing(this);
    }
}
import org.example.SpaceShip;

public class Earth implements Planet {
    @Override
    public void landed(SpaceShip spaceShip) {
        spaceShip.landing(this);
    }
}
import java.util.ArrayList;
import java.util.List;
import org.example.planet.Earth;
import org.example.planet.Mercury;
import org.example.planet.Planet;
import org.example.planet.Venus;

public class Main {
    public static void main(String[] args) {
        SpaceShip ship = new TaehoonSpaceShip();
        List<Planet> planets = new ArrayList<>();
        planets.add(new Mercury());
        planets.add(new Venus());
        planets.add(new Earth());

        for (Planet planet : planets) {
//            ship.landing(planet);

			// 해당 코드가 추가되었습니다
            planet.landed(ship);
            System.out.println("\n");
        }
    }
}

위 코드를 실행하면 다음과 같은 결과가 나옵니다.

TaehoonSpaceShip는 각 행성마다 올바른 방식으로 착륙하는 걸 확인할 수 있습니다.
뭐가 달라진걸까요?

방법 2 설명

아래 내용들이 변경되었습니다.

  • Planet 인터페이스에 landed 메서드가 추가되었습니다.
  • 각 Planet 구현체들은 파라미터로 받은 spaceShip의 landing 메서드를 실행합니다.

글로만 읽으면 어려우니 그림을 첨부하겠습니다.

기존 구조

기존에는 아래와 같이 메서드를 호출합니다.

변경된 구조

변경된 구조는 다음과 같습니다.

핵심은 아래와 같습니다.

Spaceship 객체에서 Planet 구현체를 파라미터로 받으려면 어떻게 해야 할까?

위 문제를 해결하기 위해 우리는 Planet 구현체에서 landed 라는 메서드를 만들었습니다.
landed 메서드에서는 SpaceShip 객체의 landing을 호출해줍니다.
spaceShip.landing(this)를 실행하면, 파라미터로 Mercury, Venus 등과 같은 구현체가 넘어갑니다.
이제 SpaceShip 객체에서는 각 행성에 따른 적절한 메서드가 실행되겠죠??

이러한 방식을 Double Dispatch 라고 합니다.

정리하겠습니다.
자바의 한계로, 파라미터로 인터페이스 형태로 넘겨주면 구현체로 받을 수 없습니다. (내부에서 instanceof 메서드로 컨버팅을 해줘야함)
그래서 구현체를 직접 넘겨주는 방식을 사용합니다.
그 방법으로 Double Dispatch이라는 기법을 사용했습니다.

결과 확인

이제 Spaceship 객체만 만들면 알아서 잘 동작하는 걸 확인할 수 있습니다.
확인해볼까요?

import org.example.planet.Earth;
import org.example.planet.Mercury;
import org.example.planet.Planet;
import org.example.planet.Venus;

public class NewSpaceShip implements SpaceShip {

    @Override
    public void landing(Planet planet) {
        System.out.println("기본 착륙 방식");
    }

    @Override
    public void landing(Mercury planet) {
        System.out.println("기술발전으로 그냥 착륙합니다");
        System.out.println("수성 착륙");
    }

    @Override
    public void landing(Venus planet) {
        System.out.println("기술발전으로 그냥 착륙합니다");
        System.out.println("금성 착륙");

    }

    @Override
    public void landing(Earth planet) {
        System.out.println("지구 착륙");
    }
}
public class Main {
    public static void main(String[] args) {
//        SpaceShip ship = new TaehoonSpaceShip();
        SpaceShip ship = new NewSpaceShip();

        List<Planet> planets = new ArrayList<>();
        planets.add(new Mercury());
        planets.add(new Venus());
        planets.add(new Earth());

        for (Planet planet : planets) {
            planet.landed(ship);
            System.out.println("\n");
        }
    }
}

마치며

해당 방식을 사용해 OCP 원칙을 지킬 수 있었습니다.
다른 종류의 Spaceship 객체를 계속 찍어내도 프로그램에 문제가 생기지 않습니다.
(다만 Planet은 바뀌지 않는다는 전제가 필요합니다~)

Double Dispatch 방식은 Visitor 디자인 패턴에서 사용되는 방식입니다.
잘 이해해두고 자기 걸로 만드시길 바라겠습니다.

profile
작은 지식 모아모아

0개의 댓글