티스토리 뷰

Java

토비의봄#01. Double Dispatch

LichKing 2016. 11. 7. 22:57

토비의 스프링으로 유명한 토비님의 방송을 보고 정리를 시작한다.


1. Dispatch

1. (특히 특별한 목적을 위해) 보내다   2. (편지・소포・메시지를) 보내다   3. 신속히 해...

네이버 검색결과이다. 자바는 객체지향 프로그래밍언어로서 객체들간의 메세지 전송을 기반으로 문제를 해결하게된다. 메세지 전송이라는 표현은 결국 메서드를 호출하는것인데 그것을 dispatch라고 부르는 것이다.

dispatch는 static dispatch와 dynamic dispatch가 있는데 static은 구현클래스를 이용해 컴파일타임에서부터 어떤 메서드가 호출될지 정해져있는것이고, dynamic은 인터페이스를 이용해 참조함으로서 호출되는 메서드가 동적으로 정해지는걸 말한다.


public class Test {
public static void main(String[] arg) {
Dispatch dispatch = new Dispatch();
System.out.println(dispatch.method());
}
}

class Dispatch{
public String method(){
return "hello dispatch";
}
}

Static Dispatch

자바에서 객체 생성은 런타임시에 호출된다. 즉 컴파일타임에 알수있는건 타입에 대한 정보이다. 타입자체가 Dispatch라는 구현클래스이기때문에 해당 메서드를 호출하면 어떤 메서드가 호출될지 정적으로 정해진다. 이에대한 정보는 컴파일이 종료된 후 바이트코드에도 드러나게된다.


public class Test {
public static void main(String[] arg) {
Dispatchable dispatch = new Dispatch();
System.out.println(dispatch.method());
}
}

class Dispatch implements Dispatchable {
public String method(){
return "hello dispatch";
}
}

interface Dispatchable{
String method();
}

Dynamic Dispatch

인터페이스를 타입으로 메서드를 호출한다. 컴파일러는 타입에 대한 정보를 알고있으므로 런타임시에 호출 객체를 확인해 해당 객체의 메서드를 호출한다. 런타임시에 호출 객체를 알 수 있으므로 바이트코드에도 어떤 객체의 메서드를 호출해야하는지 드러나지 않는다.


예제코드에서 method() 메서드는 인자가 없는 메서드이지만 자바는 묵시적으로 항상 호출 객체를 인자로 보내게된다. 호출 객체를 인자로 보내기때문에 this를 이용해 메서드 내부에서 호출객체를 참조할 수 있는 것이다. 또한 이것이 dynamic dispatch의 근거가 되게된다.


2. Method Signature, Method Type

면접 볼때 method signature를 적으라는 문제가 새록새록 기억나는 주제였다. 메서드 시그니처가 대충 뭔지는 알았지만 정확하게는 몰라서 당시에는 그냥 선언부를 전부 다 적었다.

접근제어자 반환타입 메서드명 (인자)

이번에 토비님께서 메서드 시그니처에 대해 정확히 정리해주셨다. 앞으로는 까먹지말아야지..

메서드 시그니처는 그것만으로 메서드를 구분지을수 있는 근거가 되어야한다. 그에 따라 자바에서 메서드 시그니처는 메서드명 (인자) 가 되게된다. 한가지 유의할 점은 반환 타입은 시그니처에 포함되지 않는다는 것이다. 쉽게 말하면 오버로딩이 되면 시그니처가 다르다고 보면 된다.



아래메서드는 접근제어자가 빠지고 반환타입이 달라졌는데 이미 존재하는 메서드라고 컴파일 에러가 발생하고있다.


public String method01(String str){
return "";
}

void method01(int num){

}

인자 타입이 달라지면 에러는 없어진다.


public String method01(String str){
return "";
}

void method01(String str1, String str2){

}

인자 타입이 같아도 개수가 달라지면 에러가 없어진다. 인자의 타입과 개수까지 시그니처에 포함된다는걸 알 수 있다.


그 다음으로는 Java8에서 추가된 Method Reference로 인해 등장한 용어인데 Method Type 이라는 용어가 생겼다. 메서드 타입에 포함되는것은 반환타입 타입파라미터 인자 예외 가 포함되게된다. 메서드 타입이 같으면 메서드 레퍼런스로 표현이 가능하게 되는것이다.


public class Test {
public static void main(String[] arg) {
Dispatch dispatch = new Dispatch();
dispatch.method("hello", System.out::println);
}
}

class Dispatch {
public void method(String str, Consumer<String> consumer){
consumer.accept(str);
}
}

method() 메서드는 Consumer 객체를 인자로 받는다. Consumer 구현체는 메서드 레퍼런스로 구현했는데, 저런식의 표현이 같으려면 메서드 타입이 일치해야한다는 것이다.


void accept(T t);
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}

Consumer 인터페이스의 메서드 선언부와 println() 메서드의 선언부이다. 타입파라미터로 <String>을 지정해줬기때문에 accept() 메서드의 T는 String으로 치환해서 봤을때 위에 메서드타입이라고 말한것들이 일치하고있다. 메서드 타입이 동일할때 메서드 레퍼런스를 사용할 수 있는것이다.


3. Double Dispatch

1화 방송의 핵심 주제인 더블 디스패치이다. 윗 설명에서 디스패치가 무엇인지 알아보았다. 동적 디스패치(Dynamic Dispatch)는 메서드를 호출하는 인자가 구현체가 없는 인터페이스 타입인 경우 런타임시에 객체를 찾아 알맞는 메서드를 호출하는 것이다. 동적 디스패치를 이용하면 이런 코드가 가능하다.

public class Test {
public static void main(String[] arg) {
List<SmartPhone> phoneList = Arrays.asList(new Iphone(), new Gallaxy());
Game game = new Game();
phoneList.forEach(game::play);
}
}

interface SmartPhone{
}

class Iphone implements SmartPhone{

}

class Gallaxy implements SmartPhone{

}

class Game {
public void play(SmartPhone phone) {
System.out.println("game play [" +phone.getClass().getSimpleName()+ "]");
}
}

스마트폰 리스트를 순회하면서 각각 게임을 하고있다. 스마트폰 리스트 인터페이스를 타입파라미터로 전달했기때문에 동적디스패치로 인해 출력내용은 모두 다르다. 앞에서 말했듯이 이는 인터페이스로 참조하고있는 객체 레퍼런스를 동적으로 추적하기때문이다.

그럼 여기서 한가지 고민해보자. Game play가 스마트폰 구현체별로 다르게 구현되어야한다면 어떻게될까?


class Game {
public void play(SmartPhone phone) {
if(phone instanceof Iphone) {
System.out.println("iphone play [" + phone.getClass().getSimpleName() + "]");
}

if(phone instanceof Gallaxy) {
System.out.println("gallaxy play [" + phone.getClass().getSimpleName() + "]");
}
}
}

가장 쉽게 떠올릴수 있는 해결책이다. 하지만 그냥 한눈에 봐도 뭔가 아닌거같다. 만약 SmartPhone의 구현체로 Optimus가 추가된다면 Game 클래스까지 변경이 발생하게된다. 전혀 OOP적인 방법이 아니다.


class Game {
public void play(Iphone phone) {
System.out.println("iphone play [" + phone.getClass().getSimpleName() + "]");
}

public void play(Gallaxy phone) {
System.out.println("gallaxy play [" + phone.getClass().getSimpleName() + "]");
}
}

이런 방법은 어떨까?


public static void main(String[] arg) {
List<SmartPhone> phoneList = Arrays.asList(new Iphone(), new Gallaxy());
Game game = new Game();
phoneList.forEach(game::play); // 컴파일 에러 발생
}

직접 해보면 알겠지만 컴파일 에러가 발생한다. 자바는 하위 타입으로의 묵시적 형변환을 지원하지 않기때문에 명시적으로 형변환을 해야한다. 하지만 2개의 요소가 서로 타입이 다르므로 명시적 형변환을 하려면 반복문을 사용할 수 없다. 제네릭같은 방법을 이용한다고 하더라도 구현체마다 다른 행동을 해야하므로 하나의 메서드로 모으는건 의미가 없다.

자바에서 지원을 하고말고는 논외로하고 잠깐 생각해본다면, 어차피 런타임시에 어떤 객체가 들어오는지를 확인해서 서로 다른 메서드를 호출해주는 동적 디스패치가 존재한다면 이를 인자에도 적용할 수 있지 않을까? 그렇게되면 알맞은 SmartPhone 구현체를 확인해 알아서 각각 메서드를 호출시켜주면 참 고마울것이다.

하지만 당연하게도 자바에서는 그런걸 지원하지않는다. 이런 이유로 자바를 싱글 디스패치(Single Dispatch) 언어라고 한다.


자, 이제 if문을 없애자. 이런식으로 수정해주면 된다.

interface SmartPhone{
void game(Game game);
}

class Iphone implements SmartPhone{
@Override
public void game(Game game) {
System.out.println("iphone play [" + this.getClass().getSimpleName() + "]");
}
}

class Gallaxy implements SmartPhone{
@Override
public void game(Game game) {
System.out.println("gallaxy play [" + this.getClass().getSimpleName() + "]");
}
}

class Game {
public void play(SmartPhone phone) {
phone.game(this);
}
}

기존 Game클래스에 존재하던 비즈니스 로직을 각각 자기자신이 직접 처리하게끔 수정했다. 다형성을 한번 더 꼬아서 사용하는 것이다. 이때는 디스패치가 2번 일어나게되는데 play() 메서드를 찾기위한 정적 디스패치가 발생하고, game()메서드를 호출하는 객체를 찾기위한 동적 디스패치가 발생하게된다. 나는 지금 Game 클래스를 구현 클래스로 만들었기때문에 정적1번 동적1번이 발생하지만 Game 클래스역시 인터페이스를 기반으로 구현하여 인터페이스로 참조를 하게된다면 동적 디스패치가 2번 발생할것이다. 처음이 정적이든 동적이든 play() 메서드를 찾기위한 디스패치만 발생하던 기존 코드에서 play() 메서드 내부에 비즈니스로직을 호출하는 실제 객체를 찾기위한 동적 디스패치가 1번 더 발생하면서 더블 디스패치(Double Dispatch)가 되는 것이다.


또한 SmartPhone의 구현체들인 Iphone, Gallaxy가 비즈니스 로직을 직접 구현하기때문에 추후 Optimus 클래스 등 신규 구현체가 추가되더라도 Game 클래스에는 변경이 없다. OCP를 만족하게 바뀐 것이다.

언어의 한계적 특성으로 인해 인자에 동적 디스패치를 활용하지못하기때문에, 이를 위해 한번 더 다형성을 이용함으로서 호출객체 동적 디스패치를 이용하는 기법이다.


마침 얼마전 실무에서 인터페이스로 인자를 받고 구현체별로 다른 행위를 했어야하는 일이 있었다. 그때 당시에 instanceof 연산사를 이용해서 해결했었는데, 하면서도 참 이건 아니라는 생각이 많이 들었었다. 구현체를 신경쓰지 않기위해 인터페이스를 사용해 인자를 받는데 메서드 내부에서 구현체를 알아야한다는건 캡슐화가 깨진다고 생각했기때문이다. 하지만 그런 의문에서도 해결법을 찾지못해 그냥 instanceof로 처리했었는데 더블 디스패치를 적용하면 메서드가 구현체를 알 필요가 없어지게끔 리팩토링이 가능할것 같다. 출근하면 바로 수정해야겠다.


추가 : 성공적으로 수정했고 매우 만족스러운 코드가 나왔다.

'Java' 카테고리의 다른 글

토비의봄#03. Type Intersection  (0) 2016.12.16
토비의봄#02. Super Type Token  (0) 2016.11.12
토비의봄#01. Double Dispatch  (4) 2016.11.07
Java8#05. Optional  (0) 2016.10.31
리스트돌릴땐 무조건 foreach를 사용하자  (10) 2016.10.30
Java8#04. 스트림(Stream)  (3) 2016.10.19
공유하기 링크
TAG
댓글
댓글쓰기 폼