본문 바로가기

WhiteShip Java Study : 자바 처음부터 멀리까지

디스패치, 다이나믹 디스패치, 더블 디스패치

Dynamic Method Dispatch

 

Dependency 의존성

하나의 클래스 A 가 있다.

 

다른 클래스 B 가 있다.

 

B 클래스의 내부 구현에서 A 클래스로 인스턴스를 만들었다.

B 클래스 내부에서 만든 A 의 인스턴스로 A 클래스의 메서드도 사용했다.

 

그렇게 에플리케이션을 잘 사용하다가, A 클래스의 메서드를 삭제했다.

 

그럼 B 클래스는 어떻게 될까?

 

당연히 오류를 뱉을 것이다.

B는 A를 사용하고 있었으니까.

 

이렇게 B 가 A를 사용하고 있는 상태를 B 는 A를 의존한다 라고 하고, 두 클래스는 의존관계가 있으며 이런 것을 의존성이라 한다.

 

디스패치

디스패치란 어떤 메서드를 호출할 것인가 를 결정하는 과정을 말한다.

즉 메서드의 의존성을 결정하는 과정이라 할 수 있다.

  • 정적 디스패치
  • 동적 디스패치

 

정적 디스패치

public class Person {
    private int age;

    public void print() {
        System.out.println("Hello");
    }

    public void print(String greeting) {
        System.out.println(greeting);
    }

    public void printJob() {
    }
}
public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        p.print();
        p.print("hi");
    }
}

출력 :
Hello
Hi

이 코드에서 p 객체의 오버로딩된 두개의 메서드 print 를 실행했다.

 

정적 디스패치는 런타임이 아닌 컴파일타임에 컴파일러, 사용자, 바이트코드 모두 어떤 메서드가 실행될 지 아는 것이다.

p 객체의 두 print 메서드 중 어떤 메서드를 실행할 지 컴파일 타임에 결정한다.

바이트 코드를 보자.

 

   L1
    LINENUMBER 6 L1
    ALOAD 1
    INVOKEVIRTUAL com/whiteship/white_ship_study/week6/Dispatch/Person.print ()V
   L2
    LINENUMBER 7 L2
    ALOAD 1
    LDC "hi"
    INVOKEVIRTUAL com/whiteship/white_ship_study/week6/Dispatch/Person.print (Ljava/lang/String;)V

()V, (Ljava/lang/String;)V 이렇게 다른 메서드를 실행함을 바이트코드는 이미 알고있다.

 

이런 것을 정적 디스패치라 한다.

 

 

동적 디스패치

public abstract class Job {
    abstract void printJob();
}
public class Student extends Job {
    @Override
    public void printJob() {
        System.out.println("Student");
    }
}
public class Teacher extends Job {
    @Override
    public void printJob() {
        System.out.println("Teacher");
    }
}
public class DynamicDispatch {
    public static void main(String[] args) {
        Job student = new Student();
        Job teacher = new Teacher();

        student.printJob();
        teacher.printJob();
        
//        List<Job> jobList = Arrays.asList(new Teacher(), new Student());
//        jobList.forEach(Job::printJob);
    }
}

 

Job 클래스를 상속받는 두 클래스 Student, Teacher 가 있다.

 

이 두 클래스를 Job 타입 객체로 만들었다.

 

그리고 두 객체의 printJob() 을 실행시킨다.

 

이 때, main 메서드 안의 student.printJob(), teacher.printJob() 은 컴파일타임에 어떤 클래스의 printJob 을 실행하는지 알까?

아니, 애초에 Job 이라는 abstract 클래스의 메서드로 접근하는데 컴파일타임에 하위 타입의 printJob() 의 구현을 고려할까?

 

이번에도 바이트코드를 보자.

   L0
    LINENUMBER 5 L0
    NEW com/whiteship/white_ship_study/week6/Dispatch/Student
    DUP
    INVOKESPECIAL com/whiteship/white_ship_study/week6/Dispatch/Student.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 6 L1
    NEW com/whiteship/white_ship_study/week6/Dispatch/Teacher
    DUP
    INVOKESPECIAL com/whiteship/white_ship_study/week6/Dispatch/Teacher.<init> ()V
    ASTORE 2
   L2
    LINENUMBER 8 L2
    ALOAD 1
    INVOKEVIRTUAL com/whiteship/white_ship_study/week6/Dispatch/Job.printJob ()V
   L3
    LINENUMBER 9 L3
    ALOAD 2
    INVOKEVIRTUAL com/whiteship/white_ship_study/week6/Dispatch/Job.printJob ()V

아래의 Job.printJob 이 같은걸로 알 수 있듯이,

바이트코드상에서는 Job 타입의 printJob 을 실행함을 알 뿐, 어떤 클래스의 메서드인지는 모른다.

 

어떤 메서드를 실행시킬까는 런타임에 결정하게 되는 것이다.

이를 동적 디스패치라 한다.

 

그럼 어떻게 결정할까.

Job teacher = new Teacher(); 라고 객체를 생성하면 클래스에서 this 라는 자기자신 객체의 정보를 담은 참조자를 가지는 것 처럼 reciver parameter 라는 자기 객체의 정보를 담은 인자를 같이가지고 Job이라는 타입에 저장하게 된다.

 

런타임에 메서드를 호출할 때 이 reciver parameter 를 보고 어떤 객체인지 판단 후 그 클래스의 메서드를 호출한다.

 

 

 

더블 디스패치

public class Double {
    interface Post {
        void postOn(SNS sns); 
    }
    
    static class Text implements Post {
        public void postOn(SNS sns) {
            if (sns instanceof Facebook) {
                System.out.println("facebook - text");
            }
            if (sns instanceof Twitter) {
                System.out.println("twitter - text");
            }
        }
    }
    
    static class Picture implements Post {
        public void postOn(SNS sns) {
            if (sns instanceof Facebook) {
                System.out.println("facebook - picture");
            }
            if (sns instanceof Twitter) {
                System.out.println("twitter - picture");
            }
        }
    }

    interface SNS {}
    
    static class Facebook implements SNS {
    }
    
    static class Twitter implements SNS {
    }

    public static void main(String[] args) {
        List<Post> posts = Arrays.asList(new Text(), new Picture());
        List<SNS> sns = Arrays.asList(new Twitter(), new Facebook());

        posts.forEach(post -> sns.forEach(sn -> post.postOn(sn)));
    }
}

Sns 인터페이스를 구현하는 Facebook, Twitter 이 있다.

Post 인터페이스를 구현하는 Text, Picture 이 있다.

 

2 x 2 의 동작에 대해 모든 로직을 따로 만들어보고 싶다는 생각이 들었다 치자.

 

SNS 객체를 인수로 가지고 Post 타입의 객체의 postOn() 으로 접근하면, 다이나믹 디스패치가 적용되어 Text 인지 Picture 인지 판단하고알맞은 클래스의 postOn() 를 실행할지 런타임에서 결정하게 된다.

 

이후 인수 SNS 객체타입을 instanceOf 를 가지고 로직을 분기한다.

하지만 이런 방법에는 치명적인 문제가 있다.

 

Post 의 하위 클래스 Text, Picture 은 SNS 를 사용하고 있다. 의존성이 있다는 것이다.

Post 가 SNS 를 의존하고 있다.

 

만약에 SNS 타입이 하나 더 생기면? 의존하고 있는 클래스에 변경이 있다면 ?

class Instagram implements SNS {} 가 새로 생긴다면 그에 따른 로직을 정의하기 위해 새로운 if() 를 추가해야 한다.

이것이 문제다.

의존하고 있는 클래스에 변경이 있다면, 클라이언트 코드도 바뀌어야 한다는 것.

 

하지만 추가하지 않아도 컴파일타임에 에러를 뱉지는 않는다.

개발자가 실수로 추가 if 를 추가하지 않고, 클라이언트가 instagram 객체를 준다면? Exception 이 발생하며 프로그램이 종료될 것이다.

 

 

 

public class DoubleDispatch {
    interface Post {
        void postOn(SNS sns);
    }
    
    static class Text implements Post {
        public void postOn(SNS sns) {
            sns.post(this);
        }
    }
    
    static class Picture implements Post {
        public void postOn(SNS sns) {
            sns.post(this);
        }
    }

    interface SNS {
        void post(Text text);
        void post(Picture picture);
    }

    static class Facebook implements SNS {

        @Override
        public void post(Text text) {
            System.out.println("facebook - text");
        }

        @Override
        public void post(Picture picture) {
            System.out.println("facebook - picture");
        }
    }

    static class Twitter implements SNS {
        @Override
        public void post(Text text) {
            System.out.println("twitter - text");
        }

        @Override
        public void post(Picture picture) {
            System.out.println("twitter - picture");
        }
    }

    public static void main(String[] args) {
        List<Post> posts = Arrays.asList(new Text(), new Picture());
        List<SNS> sns = Arrays.asList(new Twitter(), new Facebook());

        posts.forEach(post -> sns.forEach(sn -> post.postOn(sn)));
    }
}

그래서 나온 해결책이 이 방법이고 이 방법이 더블 디스패치이다.

먼저번의 코드와 다른점을 잘 확인하자.

SNS 타입 클래스들에 post 라는 메서드가 새로 생겼다.

그리고 출력을 SNS 에서 하게 바뀌었다.

 

 

말했듯이, Post 에서 SNS 을 사용하고 의존하는 구조다.

 

Post 타입에 저장된 post 객체의 reciver parameter 로 자신의 Post 타입이 Text 인지 Picture 인지 결정한다. -> 다이나믹 디스패치

 

이후 적절한 클래스의 postOn 메서드로 들어오면, 인자로 들어온 SNS의 메서드 post 를 호출한다. 자기자신의 객체정보를 담은 this 를 인자로 넘기면서. 여기서 sns.post() 이 부분이 또 다이나믹 디스패치인 것이다.

 

이후 적절한 SNS 타입의 하위 클래스를 찾아가 오버로딩된 post() 메서드들 중 알맞은 메서드를 실행한다. 

 

그럼 두번에 걸쳐 다이나믹 디스패치가 일어나므로 이를 더블 디스패치라 한다.

 

 

더블 디스패치는 의존성을 담당하는 SNS 에서 새로운 Instagram 이 생겼다고 해도 클라이언트인 Post 하위 클래스들은 변경점이 없다.

public class DoubleDispatch {
    interface Post {
        void postOn(SNS sns);
    }
    static class Text implements Post {
        public void postOn(SNS sns) {
            sns.post(this);
        }
    }
    static class Picture implements Post {
        public void postOn(SNS sns) {
            sns.post(this);
        }
    }

    interface SNS {
        void post(Text text);
        void post(Picture picture);
    }

    static class Facebook implements SNS {

        @Override
        public void post(Text text) {
            System.out.println("facebook - text");
        }

        @Override
        public void post(Picture picture) {
            System.out.println("facebook - picture");
        }
    }

    static class Twitter implements SNS {
        @Override
        public void post(Text text) {
            System.out.println("twitter - text");
        }

        @Override
        public void post(Picture picture) {
            System.out.println("twitter - picture");
        }
    }

    // 새로운 Instagram
    static class Instagram implements SNS {
        @Override
        public void post(Text text) {
            System.out.println("instagram - text");
        }

        @Override
        public void post(Picture picture) {
            System.out.println("instagram - picture");
        }
    }

    public static void main(String[] args) {
        List<Post> posts = Arrays.asList(new Text(), new Picture());
        List<SNS> sns = Arrays.asList(new Twitter(), new Facebook(), new Instagram()); // 인스타그램도 추가

        posts.forEach(post -> sns.forEach(sn -> post.postOn(sn)));
    }
}

 

 

 

이 더블 디스패치를 활용하여 범용적으로 쓰기 위한 패턴을 visitor 패턴이라 한다.

기회가 된다면 정리하여 포스팅하도록 하겠다..!

 

 

 

 

 

 

참고:

www.youtube.com/watch?v=s-tXAHub6vg

thecodinglog.github.io/design/2019/10/29/visitor-pattern.html

'WhiteShip Java Study : 자바 처음부터 멀리까지' 카테고리의 다른 글

인터페이스  (0) 2021.02.04
패키지  (0) 2021.02.02
상속  (0) 2021.02.01
클래스  (0) 2021.01.30
4주차) 과제  (0) 2021.01.30